2 // @compilation_level SIMPLE_OPTIMIZATIONS
5 * @license Highcharts JS v2.1.5 (2011-06-22)
7 * (c) 2009-2011 Torstein Hønsi
9 * License: www.highcharts.com/license
13 /*jslint forin: true */
14 /*global document, window, navigator, setInterval, clearInterval, clearTimeout, setTimeout, location, jQuery, $ */
17 // encapsulated variables
21 mathRound = math.round,
22 mathFloor = math.floor,
30 deg2rad = mathPI * 2 / 360,
34 userAgent = navigator.userAgent,
35 isIE = /msie/i.test(userAgent) && !win.opera,
36 docMode8 = doc.documentMode === 8,
37 isWebKit = /AppleWebKit/.test(userAgent),
38 isFirefox = /Firefox/.test(userAgent),
39 //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',
43 hasTouch = doc.documentElement.ontouchstart !== undefined,
48 timeFactor = 1, // 1 = JavaScript time, 1000 = Unix time
51 dateFormat, // function
56 // some constants for frequently used strings
59 ABSOLUTE = 'absolute',
60 RELATIVE = 'relative',
62 PREFIX = 'highcharts-',
69 * Empirical lowest possible opacities for TRACKER_FILL
73 * IE9: 0.00000000001 (unlimited)
74 * FF: 0.00000000001 (unlimited)
77 * Opera: 0.00000000001 (unlimited)
79 TRACKER_FILL = 'rgba(192,192,192,'+ (hasSVG ? 0.000001 : 0.002) +')', // invisible but clickable
81 HOVER_STATE = 'hover',
82 SELECT_STATE = 'select',
84 // time methods, changed based on whether or not UTC is used
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.
108 merge = adapter.merge,
109 hyphenate = adapter.hyphenate,
110 addEvent = adapter.addEvent,
111 removeEvent = adapter.removeEvent,
112 fireEvent = adapter.fireEvent,
113 animate = adapter.animate,
116 // lookup over the types and the associated classes
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) {
137 * Shortcut for parseInt
140 function pInt(s, mag) {
141 return parseInt(s, mag || 10);
148 function isString(s) {
149 return typeof s === 'string';
154 * @param {Object} obj
156 function isObject(obj) {
157 return typeof obj === 'object';
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
178 * @param {Mixed} item
180 function erase(arr, item) {
183 if (arr[i] === item) {
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) {
209 setAttribute = 'setAttribute',
212 // if the prop is a string
213 if (isString(prop)) {
215 if (defined(value)) {
217 elem[setAttribute](prop, 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)) {
227 elem[setAttribute](key, prop[key]);
233 * Check if an element is an array, and if not, make it into an array. Like
236 function splat(obj) {
237 if (!obj || obj.constructor !== Array) {
246 * Return the first value that is defined. Like MooTools' $.pick.
249 var args = arguments,
252 length = args.length;
253 for (i = 0; i < length; i++) {
255 if (typeof arg !== 'undefined' && arg !== null) {
261 * Make a style string from a JS object
262 * @param {Object} style
264 function serializeCSS(style) {
267 // serialize the declaration
269 s += key +':'+ style[key] + ';';
275 * Set CSS on a given element
277 * @param {Object} styles Style object with camel case property names
279 function css (el, styles) {
281 if (styles && styles.opacity !== UNDEFINED) {
282 styles.filter = 'alpha(opacity='+ (styles.opacity * 100) +')';
285 extend(el.style, styles);
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) {
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));
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);
319 css(el, {padding: 0, border: NONE, margin: 0});
325 parent.appendChild(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);
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](),
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
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)
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
418 'y': fullYear.toString().substr(2, 2), // Two digits year, like 09 for 2009
419 'Y': fullYear, // Four digits year, like 2009
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
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.
450 function getPosition (el) {
451 var p = { left: el.offsetLeft, top: el.offsetTop };
452 el = el.offsetParent;
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;
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) {
486 * Utility for iterating over an array. Parameters are reversed compared to jQuery.
488 * @param {Function} fn
490 each = function(arr, fn) {
493 for (; i < len; i++) {
494 if (fn.call(arr[i], arr[i], i, arr) === false) {
508 * @param {Function} fn
510 map = function(arr, fn){
511 //return jQuery.map(arr, fn);
513 i = 0, len = arr.length;
514 for (; i < len; i++) {
515 results[i] = fn.call(arr[i], arr[i], i, arr);
522 * Deep merge two objects and return a third object
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
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.
580 el[detachedType] = el[type];
585 jQ(el).trigger(event);
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
601 * @param {Object} params
602 * @param {Object} options jQuery-like animation options: duration, easing, callback
604 animate = function (el, params, options) {
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
612 $el.animate(params, options);
616 * Stop running animation
618 stop = function (el) {
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;
635 jQ.fx.step._default = function(fx){
637 if (elem.attr) { // is SVG element wrapper
638 elem.attr(fx.prop, fx.now);
640 oldStepDefault.apply(this, arguments);
644 jQ.fx.step.d = function(fx) {
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
652 var ends = pathAnim.init(elem, elem.d, elem.toD);
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);
670 r = oldCur.apply(this, arguments);
678 * Add a global listener for mousemove events
680 /*addEvent(doc, 'mousemove', function(e) {
681 if (globalMouseMove) {
687 * Path interpolation algorithm used across adapters
691 * Prepare start and end values so that the path can be animated one to one
693 init: function(elem, fromD, toD) {
695 var shift = elem.shift,
696 bezier = fromD.indexOf('C') > -1,
697 numParams = bezier ? 7 : 3,
701 start = fromD.split(' '),
702 end = [].concat(toD), // copy
705 sixify = function(arr) { // in splines make move points have six parameters like bezier curves
709 arr.splice(i + 1, 0, arr[i+1], arr[i+2], arr[i+1], arr[i+2]);
719 // pull out the base lines before padding
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
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
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);
755 * Interpolate each value of the path and return the array
757 step: function(start, end, pos, complete) {
762 if (pos === 1) { // land on the final path without adjustment points appended in the ends
765 } else if (i === end.length && pos < 1) {
767 startVal = parseFloat(start[i]);
769 isNaN(startVal) ? // a letter instruction like M or L
771 pos * (parseFloat(end[i] - startVal)) + startVal;
774 } else { // if animation is finished or length not matching, land on right value
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) {
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);
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
840 garbageBin = createElement(DIV);
843 // move the node and empty bin
845 garbageBin.appendChild(element);
847 garbageBin.innerHTML = '';
850 /* ****************************************************************************
851 * Handle the options *
852 *****************************************************************************/
855 defaultLabelOptions = {
861 /*formatter: function() {
872 colors: ['#4572A7', '#AA4643', '#89A54E', '#80699B', '#3D96AE',
873 '#DB843D', '#92A8CD', '#A47D7C', '#B5CA92'],
874 symbols: ['circle', 'diamond', 'square', 'triangle', 'triangle-down'],
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'],
881 resetZoom: 'Reset zoom',
882 resetZoomTitle: 'Reset zoom level 1:1',
893 //events: { load, selection },
897 //marginBottom: null,
899 borderColor: '#4572A7',
902 defaultSeriesType: 'line',
903 ignoreHiddenSeries: true,
911 fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif', // default font
914 backgroundColor: '#FFFFFF',
915 //plotBackgroundColor: null,
916 plotBorderColor: '#C0C0C0'
917 //plotBorderWidth: 0,
927 // verticalAlign: 'top',
940 // verticalAlign: 'top',
948 line: { // base series options
949 allowPointSelect: false,
954 // connectNulls: false, // docs
957 //enableMouseTracking: true,
959 //legendIndex: 0, // docs (+ pie points)
968 lineColor: '#FFFFFF',
970 states: { // states for a single point
975 fillColor: '#FFFFFF',
976 lineColor: '#000000',
984 dataLabels: merge(defaultLabelOptions, {
987 formatter: function() {
995 states: { // states for the entire series
998 //lineWidth: base + 1,
1000 // lineWidth: base + 1,
1008 stickyTracking: true
1015 //font: defaultFont,
1024 layout: 'horizontal',
1025 labelFormatter: function() {
1028 // lineHeight: 16, // docs: deprecated
1030 borderColor: '#909090',
1035 // backgroundColor: null,
1050 itemCheckboxStyle: {
1052 width: '13px', // for IE precision
1055 // itemWidth: undefined,
1058 verticalAlign: 'bottom',
1059 // width: undefined,
1074 backgroundColor: 'white',
1083 backgroundColor: 'rgba(255, 255, 255, .85)',
1086 //formatter: defaultFormatter,
1089 snap: hasTouch ? 25 : 10,
1094 whiteSpace: 'nowrap'
1107 text: 'Highcharts.com',
1108 href: 'http://www.highcharts.com',
1112 verticalAlign: 'bottom',
1124 var defaultXAxisOptions = {
1125 // allowDecimals: null,
1126 // alternateGridColor: null,
1128 dateTimeLabelFormats: {
1138 gridLineColor: '#C0C0C0',
1139 // gridLineDashStyle: 'solid', // docs
1140 // gridLineWidth: 0,
1143 labels: defaultLabelOptions,
1145 lineColor: '#C0D0E0',
1153 minorGridLineColor: '#E0E0E0',
1154 // minorGridLineDashStyle: null,
1155 minorGridLineWidth: 1,
1156 minorTickColor: '#A0A0A0',
1157 //minorTickInterval: null,
1159 minorTickPosition: 'outside', // inside or outside
1160 //minorTickWidth: 0,
1166 // labels: { align, x, verticalAlign, y, style, rotation, textAlign }
1172 // labels: { align, x, verticalAlign, y, style, rotation, textAlign }
1175 // showFirstLabel: true,
1176 // showLastLabel: false,
1179 tickColor: '#C0D0E0',
1180 //tickInterval: null,
1182 tickmarkPlacement: 'between', // on or between
1183 tickPixelInterval: 100,
1184 tickPosition: 'outside',
1188 align: 'middle', // low, middle or high
1189 //margin: 0 for horizontal, 10 for vertical axes,
1194 //font: defaultFont.replace('normal', 'bold')
1200 type: 'linear' // linear, logarithmic or datetime // docs
1203 defaultYAxisOptions = merge(defaultXAxisOptions, {
1206 tickPixelInterval: 72,
1207 showLastLabel: true,
1227 //verticalAlign: dynamic,
1228 //textAlign: dynamic,
1230 formatter: function() {
1233 style: defaultLabelOptions.style
1237 defaultLeftAxisOptions = {
1247 defaultRightAxisOptions = {
1257 defaultBottomAxisOptions = { // horizontal axis
1262 // staggerLines: null
1268 defaultTopAxisOptions = merge(defaultBottomAxisOptions, {
1271 // staggerLines: null
1279 var defaultPlotOptions = defaultOptions.plotOptions,
1280 defaultSeriesOptions = defaultPlotOptions.line;
1281 //defaultPlotOptions.line = merge(defaultSeriesOptions);
1282 defaultPlotOptions.spline = merge(defaultSeriesOptions);
1283 defaultPlotOptions.scatter = merge(defaultSeriesOptions, {
1291 defaultPlotOptions.area = merge(defaultSeriesOptions, {
1293 // lineColor: null, // overrides color, but lets fillColor be unaltered
1294 // fillOpacity: 0.75,
1298 defaultPlotOptions.areaspline = merge(defaultPlotOptions.area);
1299 defaultPlotOptions.column = merge(defaultSeriesOptions, {
1300 borderColor: '#FFFFFF',
1303 //colorByPoint: undefined,
1305 marker: null, // point options are specified in the base options
1316 borderColor: '#000000',
1325 defaultPlotOptions.bar = merge(defaultPlotOptions.column, {
1332 defaultPlotOptions.pie = merge(defaultSeriesOptions, {
1333 //dragType: '', // n/a
1334 borderColor: '#FFFFFF',
1336 center: ['50%', '50%'],
1337 colorByPoint: true, // always true for pies
1340 // connectorWidth: 1,
1341 // connectorColor: '#606060',
1342 // connectorPadding: 5,
1345 formatter: function() {
1346 return this.point.name;
1351 legendType: 'point',
1352 marker: null, // point options are specified in the base options
1354 showInLegend: false,
1365 // set the default time methods
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) {
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);
1386 rgba = [pInt(result[1]), pInt(result[2]), pInt(result[3]), parseFloat(result[4], 10)];
1391 result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(input);
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) {
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') {
1412 ret = 'rgba('+ rgba.join(',') +')';
1421 * Brighten the color
1422 * @param {Number} alpha
1424 function brighten(alpha) {
1425 if (isNumber(alpha) && alpha !== 0) {
1427 for (i = 0; i < 3; i++) {
1428 rgba[i] += pInt(alpha * 255);
1433 if (rgba[i] > 255) {
1441 * Set the color's opacity to a given alpha value
1442 * @param {Number} alpha
1444 function setOpacity(alpha) {
1449 // initialize: parse the input
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);
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);
1497 * Set or get a given attribute
1498 * @param {Object|String} hash
1499 * @param {Mixed|Undefined} val
1501 attr: function(hash, val) {
1506 element = this.element,
1507 nodeName = element.nodeName,
1508 renderer = this.renderer,
1510 shadows = this.shadows,
1514 // single key-value pair
1515 if (isString(hash) && defined(val)) {
1521 // used as a getter: first argument is a string, second is undefined
1522 if (isString(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);
1539 skipAttr = false; // reset
1544 if (value && value.join) { // join path
1545 value = value.join(' ');
1547 if (/(NaN| {2}|^$)/.test(value)) {
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')) +')');
1569 } else if (key === 'fill') {
1570 value = renderer.color(value, element, key);
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') {
1579 this.updateTransform();
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') {
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,')
1602 .split(','); // ending comma
1606 value[i] = pInt(value[i]) * hash['stroke-width'];
1609 value = value.join(',');
1613 } else if (key === 'isTracker') {
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);
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) {
1640 if (this.symbolName && /^(x|y|r|start|end|innerR)/.test(key)) {
1643 if (!hasSetSymbolSize) {
1644 this.symbolAttr(hash);
1645 hasSetSymbolSize = true;
1650 // let the shadow follow the main element
1651 if (shadows && /^(width|height|visibility|x|y|d)$/.test(key)) {
1654 attr(shadows[i], key, value);
1659 if ((key === 'width' || key === 'height') && nodeName === 'rect' && value < 0) {
1663 if (key === 'text') {
1664 // only one node allowed
1665 this.textStr = value;
1667 renderer.buildText(this);
1669 } else if (!skipAttr) {
1670 //element.setAttribute(key, value);
1671 attr(element, key, value);
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
1684 * @param {Object} hash
1686 symbolAttr: function(hash) {
1689 each(['x', 'y', 'r', 'start', 'end', 'width', 'height', 'innerR'], function(key) {
1690 wrapper[key] = pick(hash[key], wrapper[key]);
1694 d: wrapper.renderer.symbols[wrapper.symbolName](
1695 mathRound(wrapper.x * 2) / 2, // Round to halves. Issue #274.
1696 mathRound(wrapper.y * 2) / 2,
1699 start: wrapper.start,
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
1722 * @param {Number} width
1723 * @param {Number} height
1725 crisp: function(strokeWidth, x, y, width, height) {
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];
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,
1764 if (styles && styles.color) {
1765 styles.fill = styles.color;
1768 // save the styles in an object
1776 elemWrapper.styles = styles;
1779 if (defined(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
1789 delete styles.width;
1791 css(elemWrapper.element, styles);
1794 style: serializeCSS(styles)
1800 if (textWidth && elemWrapper.added) {
1801 elemWrapper.renderer.buildText(elemWrapper);
1808 * Add an event listener
1809 * @param {String} eventType
1810 * @param {Function} handler
1812 on: function(eventType, handler) {
1815 if (hasTouch && eventType === 'click') {
1816 eventType = 'touchstart';
1822 // simplest possible event model for internal use
1823 this.element['on'+ eventType] = fn;
1829 * Move an object and its children by x and y values
1833 translate: function(x, y) {
1841 * Invert a group, rotate and flip
1843 invert: function() {
1845 wrapper.inverted = true;
1846 wrapper.updateTransform();
1851 * Private method to update the transform attribute based on internal
1854 updateTransform: function() {
1856 translateX = wrapper.translateX || 0,
1857 translateY = wrapper.translateY || 0,
1858 inverted = wrapper.inverted,
1859 rotation = wrapper.rotation,
1862 // flipping affects translate as adjustment for flipping around the group's axis
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;
1874 if (translateX || translateY) {
1875 transform.push('translate('+ translateX +','+ translateY +')');
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);
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
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);
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;
1956 * Get the bounding box (width, height, x and y) for the element
1958 getBBox: function() {
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());
1970 bBox = { width: 0, height: 0 };
1973 height = bBox.height;
1975 // adjust for rotated text
1977 bBox.width = mathAbs(height * mathSin(rad)) + mathAbs(width * mathCos(rad));
1978 bBox.height = mathAbs(height * mathCos(rad)) + mathAbs(width * mathSin(rad));
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
1992 height = bBox.height;
2001 return this.attr({ visibility: VISIBLE });
2008 return this.attr({ visibility: HIDDEN });
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'),
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
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))
2054 parentNode.insertBefore(element, otherElement);
2060 // default: append at the end
2061 parentNode.appendChild(element);
2069 * Destroy the element and element wrapper
2071 destroy: function() {
2073 element = wrapper.element || {},
2074 shadows = wrapper.shadows,
2075 parentNode = element.parentNode,
2079 element.onclick = element.onmouseout = element.onmouseover = element.onmousemove = null;
2080 stop(wrapper); // stop running animations
2084 parentNode.removeChild(element);
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];
2108 * Empty a group element
2111 var element = this.element,
2112 childNodes = element.childNodes,
2113 i = childNodes.length;
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) {
2128 element = this.element,
2130 // compensate for inverted plot area
2131 transform = this.parentInverted ? '(-1,-1)' : '(1,1)';
2135 for (i = 1; i <= 3; i++) {
2136 shadow = element.cloneNode(0);
2139 'stroke': 'rgb(0, 0, 0)',
2140 'stroke-opacity': 0.05 * i,
2141 'stroke-width': 7 - 2 * i,
2142 'transform': 'translate'+ transform,
2147 group.element.appendChild(shadow);
2149 element.parentNode.insertBefore(shadow, element);
2152 shadows.push(shadow);
2155 this.shadows = shadows;
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,
2184 boxWrapper = renderer.createElement('svg')
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);
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>')
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
2236 width = textStyles && pInt(textStyles.width),
2237 textLineHeight = textStyles && textStyles['line-height'],
2239 GET_COMPUTED_STYLE = 'getComputedStyle',
2240 i = childNodes.length;
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)) {
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(/</g, '<')
2275 .replace(/>/g, '>');
2277 // issue #38 workaround.
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;
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
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
2320 attr(tspan, attributes);
2323 textNode.appendChild(tspan);
2327 // check width and apply soft breaks
2329 var words = span.replace(/-/g, '- ').split(' '),
2334 while (words.length || rest.length) {
2335 actualWidth = textNode.getBBox().width;
2336 tooLong = actualWidth > width;
2337 if (!tooLong || words.length === 1) { // new line needed
2341 tspan = doc.createElementNS(SVG_NS, 'tspan');
2343 dy: textLineHeight || 16,
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());
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);
2388 * @param {Array} path An SVG path in array form
2390 path: function (path) {
2391 return this.createElement('path').attr({
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) ?
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
2437 return this.symbol('arc', x || 0, y || 0, r || 0, {
2438 innerR: innerR || 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) {
2459 strokeWidth = x.strokeWidth;
2462 var wrapper = this.createElement('rect').attr({
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']({
2492 alignedObjects[i].align();
2498 * @param {String} name The group will be given a class name of 'highcharts-{name}'.
2499 * This can be used for styling and scripting.
2502 return this.createElement('g').attr(
2503 defined(name) && { 'class': PREFIX + name }
2509 * @param {String} src
2512 * @param {Number} width
2513 * @param {Number} height
2515 image: function(src, x, y, width, height) {
2517 preserveAspectRatio: NONE
2521 // optional properties
2522 if (arguments.length > 1) {
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',
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);
2547 * Draw a symbol out of pre-defined shape paths from the namespace 'symbol' object.
2549 * @param {Object} symbol
2552 * @param {Object} radius
2553 * @param {Object} options
2555 symbol: function(symbol, x, y, radius, options) {
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(
2570 imageRegex = /^url\((.*?)\)$/,
2576 obj = this.path(path);
2577 // expando properties for use in animate and attr
2585 extend(obj, options);
2590 } else if (imageRegex.test(symbol)) {
2592 var centerImage = function(img, size) {
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)
2614 centerImage(obj, imageSize);
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() {
2623 centerImage(obj, symbolSizes[imageSrc] = [img.width, img.height]);
2631 obj = this.circle(x, y, radius);
2638 * An extendable collection of functions for defining symbol paths.
2641 'square': function (x, y, radius) {
2642 var len = 0.707 * radius;
2652 'triangle': function (x, y, radius) {
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) {
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) {
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;
2690 x + radius * cosStart,
2691 y + radius * sinStart,
2696 longArc, // long or short arc
2698 x + radius * cosEnd,
2699 y + radius * sinEnd,
2701 x + innerRadius * cosEnd,
2702 y + innerRadius * sinEnd,
2704 innerRadius, // x radius
2705 innerRadius, // y radius
2707 longArc, // long or short arc
2709 x + innerRadius * cosStart,
2710 y + innerRadius * sinStart,
2718 * Define a clipping rectangle
2719 * @param {String} id
2722 * @param {Number} width
2723 * @param {Number} height
2725 clipRect: function (x, y, width, height) {
2727 id = PREFIX + idCounter++,
2729 clipPath = this.createElement('clipPath').attr({
2733 wrapper = this.rect(x, y, width, height, 0).add(clipPath);
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) {
2748 regexRgba = /^rgba/;
2749 if (color && color.linearGradient) {
2750 var renderer = this,
2751 strLinearGradient = 'linearGradient',
2752 linearGradient = color[strLinearGradient],
2753 id = PREFIX + idCounter++,
2757 gradientObject = renderer.createElement(strLinearGradient).attr({
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');
2772 stopColor = stop[1];
2775 renderer.createElement('stop').attr({
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');
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,
2811 x = mathRound(pick(x, 0));
2812 y = mathRound(pick(y, 0));
2814 wrapper = this.createElement('text')
2821 'font-family': defaultChartStyle.fontFamily,
2822 'font-size': defaultChartStyle.fontSize
2829 }; // end SVGRenderer
2832 Renderer = SVGRenderer;
2836 /* ****************************************************************************
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 *****************************************************************************/
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;');
2867 style.push('visibility: ', nodeName === DIV ? HIDDEN : VISIBLE);
2870 markup.push(' style="', style.join(''), '"/>');
2872 // create element with default attributes and style
2874 markup = nodeName === DIV || nodeName === 'span' || nodeName === 'img' ?
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) {
2889 renderer = wrapper.renderer,
2890 element = wrapper.element,
2892 inverted = parent && parent.inverted,
2894 // get the parent node
2895 parentNode = parent ?
2896 parent.element || parent :
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 });
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();
2923 * Get or set attributes
2925 attr: function(hash, val) {
2929 element = this.element || {},
2930 elemStyle = element.style,
2931 nodeName = element.nodeName,
2932 renderer = this.renderer,
2933 symbolName = this.symbolName,
2936 shadows = this.shadows,
2940 // single key-value pair
2941 if (isString(hash) && defined(val)) {
2947 // used as a getter, val is undefined
2948 if (isString(hash)) {
2950 if (key === 'strokeWidth' || key === 'stroke-width') {
2951 ret = this.strokeweight;
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
2968 if (!hasSetSymbolSize) {
2969 this.symbolAttr(hash);
2971 hasSetSymbolSize = true;
2976 } else if (key === 'd') {
2977 value = value || [];
2978 this.d = value.join(' '); // used in getter for animation
2982 var convertedPath = [];
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;
2992 else if (value[i] === 'Z') {
2993 convertedPath[i] = 'x';
2996 convertedPath[i] = value[i];
3000 value = convertedPath.join(' ') || 'x';
3001 element.path = value;
3007 shadows[i].path = value;
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;
3021 css(childNodes[i], { visibility: value });
3023 if (value === VISIBLE) { // issue 74
3029 elemStyle[key] = value;
3037 } else if (/^(width|height)$/.test(key)) {
3040 // clipping rectangle special
3041 if (this.updateClipping) {
3043 this.updateClipping();
3047 elemStyle[key] = value;
3053 } else if (/^(x|y)$/.test(key)) {
3055 this[key] = value; // used in getter
3057 if (element.tagName === 'SPAN') {
3058 this.updateTransform();
3061 elemStyle[{ x: 'left', y: 'top' }[key]] = value;
3065 } else if (key === 'class') {
3066 // IE8 Standards mode has problems retrieving the className
3067 element.className = value;
3070 } else if (key === 'stroke') {
3072 value = renderer.color(value, element, key);
3074 key = 'strokecolor';
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)) {
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 */
3095 } else if (key === 'fill') {
3097 if (nodeName === 'SPAN') { // text color
3098 elemStyle.color = value;
3100 element.filled = value !== NONE ? true : false;
3102 value = renderer.color(value, element, key);
3107 // translation for animation
3108 } else if (key === 'translateX' || key === 'translateY' || key === 'rotation' || key === 'align') {
3109 if (key === 'align') {
3113 this.updateTransform();
3118 // text for rotated and non-rotated elements
3119 else if (key === 'text') {
3121 element.innerHTML = value;
3126 // let the shadow follow the main element
3127 if (shadows && key === 'visibility') {
3130 shadows[i].style[key] = value;
3137 if (docMode8) { // IE8 setAttribute bug
3138 element[key] = value;
3140 attr(element, key, value);
3149 * Set the element's clipping to a predefined rectangle
3151 * @param {String} id The id of the clip rectangle
3153 clip: function(clipRect) {
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) {
3170 element = wrapper.element,
3171 textWidth = styles && element.tagName === 'SPAN' && styles.width;
3176 whiteSpace: 'normal'
3180 delete styles.width;
3181 wrapper.textWidth = textWidth;
3182 wrapper.updateTransform();
3185 wrapper.styles = extend(wrapper.styles, styles);
3186 css(wrapper.element, styles);
3192 * Extend element.destroy by removing it from the clip members array
3194 destroy: function() {
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
3208 var element = this.element,
3209 childNodes = element.childNodes,
3210 i = childNodes.length,
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() {
3227 element = wrapper.element,
3228 bBox = wrapper.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
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;
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
3270 this.alignOnAdd = true;
3275 elem = wrapper.element,
3276 translateX = wrapper.translateX || 0,
3277 translateY = wrapper.translateY || 0,
3280 align = wrapper.textAlign || 'left',
3281 alignCorrection = { left: 0, center: 0.5, right: 1 }[align],
3282 nonLeft = align && align !== 'left';
3285 if (translateX || translateY) {
3287 marginLeft: translateX,
3288 marginTop: translateY
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') {
3302 rotation = wrapper.rotation,
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
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;
3333 if (width > textWidth) {
3335 width: textWidth +PX,
3337 whiteSpace: 'normal'
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
3354 xCorr -= width * alignCorrection * (costheta < 0 ? -1 : 1);
3356 yCorr -= height * alignCorrection * (sintheta < 0 ? -1 : 1);
3363 // record correction
3364 wrapper.xCorr = xCorr;
3365 wrapper.yCorr = yCorr;
3368 // apply position with correction
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) {
3386 element = this.element,
3387 renderer = this.renderer,
3389 elemStyle = element.style,
3391 path = element.path;
3393 // some times empty paths are not strings
3394 if (path && typeof path.value !== 'string') {
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),
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);
3417 group.element.appendChild(shadow);
3419 element.parentNode.insertBefore(shadow, element);
3423 shadows.push(shadow);
3427 this.shadows = shadows;
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,
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.
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, {
3505 getCSS: function(inverted) {
3506 var rect = this,//clipRect.element.style,
3509 right = left + rect.width,
3510 bottom = top + rect.height,
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) {
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) {
3548 regexRgba = /^rgba/,
3551 if (color && color.linearGradient) {
3555 linearGradient = color.linearGradient,
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');
3568 stopColor = stop[1];
3574 opacity1 = stopOpacity;
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
3589 // when colors attribute is used, the meanings of opacity and o:opacity2
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');
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);',
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 +'" />');
3631 markup = markup.replace('style="', 'style="'+ vmlStyle);
3634 } else { // add namespace
3635 markup = markup.replace('<', '<hcv:');
3642 * Create rotated and aligned text
3643 * @param {String} str
3647 text: function(str, x, y) {
3649 var defaultChartStyle = defaultOptions.chart.style;
3651 return this.createElement('span')
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) {
3670 return this.createElement('shape').attr({
3671 // subpixel precision down to 0.1 (width and height = 10px)
3672 coordsize: '100 100',
3678 * Create and return a circle element. In VML circles are implemented as
3679 * shapes, which is faster than v:oval
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
3699 // set the class name
3701 attribs = { 'className': PREFIX + name, 'class': PREFIX + name };
3704 // the div to hold HTML and clipping
3705 wrapper = this.createElement(DIV).attr(attribs);
3711 * VML override to create a regular HTML image
3712 * @param {String} src
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) {
3734 * VML uses a shape for rect to overcome bugs and rotation problems
3736 rect: function(x, y, width, height, r, strokeWidth) {
3743 strokeWidth = x.strokeWidth;
3746 var wrapper = this.symbol('rect');
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;
3762 left: pInt(parentStyle.width) - 10,
3763 top: pInt(parentStyle.height) - 10,
3769 * Symbol definitions that override the parent SVG renderer's symbols
3773 // VML specific arc function
3774 arc: function (x, y, radius, options) {
3775 var start = options.start,
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.
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);
3797 'wa', // clockwise arc to
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
3823 // Add circle symbol path. This performs significantly faster than v:oval.
3824 circle: function (x, y, r) {
3826 'wa', // clockwisearcto
3835 //'x', // finish path
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)) {
3853 var width = options.width,
3854 height = options.height,
3855 right = left + width,
3856 bottom = top + height;
3858 r = mathMin(r, width, height);
3875 right - 2 * r, bottom - 2 * r,
3883 left, bottom - 2 * r,
3884 left + 2 * r, bottom,
3892 left + 2 * r, top + 2 * r,
3906 Renderer = VMLRenderer;
3908 /* ****************************************************************************
3910 * END OF INTERNET EXPLORER <= 8 SPECIFIC CODE *
3912 *****************************************************************************/
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) ?
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,
3945 chartSubtitleOptions,
3966 chartEvents = optionsChart.events,
3967 runChartClick = chartEvents && !!chartEvents.click,
3969 isInsidePlot, // function
3983 chartPosition,// = getPosition(container),
3984 hasCartesianSeries = optionsChart.showAxes,
3987 maxTicks, // handle the greatest amount of ticks on grouped axes
3994 drawChartBox, // function
3995 getMargins, // function
3996 resetMargins, // function
3997 setChartSize, // function
4000 zoomOut; // function
4004 * Create a new axis object
4005 * @param {Object} chart
4006 * @param {Object} options
4008 function Axis (chart, options) {
4011 var isXAxis = options.isX,
4012 opposite = options.opposite, // needed in setOptions
4013 horiz = inverted ? !isXAxis : isXAxis,
4015 (opposite ? 0 /* top */ : 2 /* bottom */) :
4016 (opposite ? 1 /* right*/ : 3 /* left */ ),
4021 isXAxis ? defaultXAxisOptions : defaultYAxisOptions,
4022 [defaultTopAxisOptions, defaultRightAxisOptions,
4023 defaultBottomAxisOptions, defaultLeftAxisOptions][side],
4028 type = options.type,
4029 isDatetimeAxis = type === 'datetime',
4030 isLog = type === 'logarithmic',
4031 offset = options.offset || 0,
4032 xOrY = isXAxis ? 'x' : 'y',
4034 transA, // translation factor
4035 oldTransA, // used for prerendering
4036 transB = horiz ? plotLeft : marginBottom, // translation addend
4038 getPlotLinePath, // fn
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
4057 events = options.events,
4059 plotLinesAndBands = [],
4063 tickPositions, // array containing predefined positions
4066 alternateBands = {},
4069 axisTitleMargin,// = options.title.margin,
4070 dateTimeLabelFormat,
4071 categories = options.categories,
4072 labelFormatter = options.labels.formatter || // can be overwritten by dynamic format
4074 var value = this.value,
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
4095 staggerLines = horiz && options.labels.staggerLines,
4096 reversed = options.reversed,
4097 tickmarkOffset = (categories && options.tickmarkPlacement === 'between') ? 0.5 : 0;
4102 function Tick(pos, minor) {
4114 * Write the tick label
4116 addLabel: function() {
4118 labelOptions = options.labels,
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),
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)
4141 css = width && { width: mathMax(1, mathRound(width - 2 * (labelOptions.padding || 10))) +PX };
4142 css = extend(css, labelOptions.style);
4145 if (label === UNDEFINED) {
4147 defined(str) && withLabel && labelOptions.enabled ?
4154 align: labelOptions.align,
4155 rotation: labelOptions.rotation
4157 // without position absolute, IE export sometimes is wrong
4164 label.attr({ text: str })
4169 * Get the offset height or width of the label
4171 getLabelSize: function() {
4172 var label = this.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) {
4185 major = !tick.minor,
4188 labelOptions = options.labels,
4189 gridLine = tick.gridLine,
4190 gridLineWidth = major ? options.gridLineWidth : options.minorGridLineWidth,
4191 gridLineColor = major ? options.gridLineColor : options.minorGridLineColor,
4193 options.gridLineDashStyle :
4194 options.minorGridLineDashStyle,
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,
4208 // get x and y position for ticks and labels
4210 translate(pos + tickmarkOffset, null, null, old) + transB :
4211 plotLeft + offset + (opposite ? ((old && oldChartWidth) || chartWidth) - marginRight - plotLeft : 0);
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) {
4223 stroke: gridLineColor,
4224 'stroke-width': gridLineWidth
4227 attribs.dashstyle = dashStyle;
4229 tick.gridLine = gridLine =
4231 renderer.path(gridLinePath)
4232 .attr(attribs).add(gridGroup) :
4235 if (gridLine && gridLinePath) {
4242 // create the tick mark
4245 // negate the length
4246 if (tickPosition === 'inside') {
4247 tickLength = -tickLength;
4250 tickLength = -tickLength;
4253 markPath = renderer.crispLine([
4258 x + (horiz ? 0 : -tickLength),
4259 y + (horiz ? tickLength : 0)
4262 if (mark) { // updating
4266 } else { // first time
4267 tick.mark = renderer.path(
4271 'stroke-width': tickWidth
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
4291 y += (index / (step || 1) % staggerLines) * 16;
4295 // show those indices dividable by step
4296 label[index % step ? 'hide' : 'show']();
4299 label[tick.isNew ? 'attr' : 'animate']({
4308 * Destructor for the tick prototype
4310 destroy: function() {
4314 if (tick[n] && 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;
4328 plotLine.options = options;
4329 plotLine.id = options.id;
4336 PlotLineOrBand.prototype = {
4339 * Render the plot line or plot band. If it is already existing,
4342 render: function () {
4343 var plotLine = this,
4344 options = plotLine.options,
4345 optionsLabel = options.label,
4346 label = plotLine.label,
4347 width = options.width,
4349 toPath, // bands only
4350 from = options.from,
4351 dashStyle = options.dashStyle,
4352 svgElem = plotLine.svgElem,
4360 color = options.color,
4361 zIndex = options.zIndex,
4362 events = options.events,
4367 path = getPlotLinePath(options.value, width);
4370 'stroke-width': width
4373 attribs.dashstyle = dashStyle;
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) {
4392 } else { // outside the axis area
4402 if (defined(zIndex)) {
4403 attribs.zIndex = zIndex;
4406 // common for lines and bands
4411 }, null, svgElem.onGetPath);
4414 svgElem.onGetPath = function() {
4418 } else if (path && path.length) {
4419 plotLine.svgElem = svgElem = renderer.path(path)
4420 .attr(attribs).add();
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) {
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
4446 // add the SVG element
4448 plotLine.label = label = renderer.text(
4454 align: optionsLabel.textAlign || optionsLabel.align,
4455 rotation: optionsLabel.rotation,
4458 .css(optionsLabel.style)
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, {
4471 width: mathMax.apply(math, xs) - x,
4472 height: mathMax.apply(math, ys) - y
4476 } else if (label) { // move out of sight
4485 * Remove the plot line or band
4487 destroy: function() {
4492 if (obj[n] && obj[n].destroy) {
4493 obj[n].destroy(); // destroy SVG wrappers
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
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) {
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});
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) {
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() {
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) {
4606 // match this axis against the series' given or implicated axis
4607 each(['xAxis', 'yAxis'], function(strAxis) {
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
4626 // ignore hidden series if opted
4627 if (!serie.visible && optionsChart.ignoreHiddenSeries) {
4640 stacking = serie.options.stacking;
4641 usePercentage = stacking === 'percent';
4643 // create a stack for this particular series type
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) {
4660 if (serie.isCartesian) { // line, column etc. need axes, pie doesn't
4661 each(serie.data, function(point, i) {
4662 var pointX = point.x,
4664 isNegative = pointY < 0,
4665 pointStack = isNegative ? negPointStack : posPointStack,
4666 key = isNegative ? negKey : stackKey,
4671 if (dataMin === null) {
4673 // start out with the first point
4674 dataMin = dataMax = point[xOrY];
4679 if (pointX > dataMax) {
4681 } else if (pointX < dataMin) {
4687 else if (defined(pointY)) {
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) {
4698 } else if (pointLow < dataMin) {
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) {
4744 localA = old ? oldTransA : transA,
4745 localMin = old ? oldMin : min,
4753 sign *= -1; // canvas coordinates inverts the value
4754 cvsOffset = axisLength;
4756 if (reversed) { // reversed axis
4758 cvsOffset -= sign * axisLength;
4761 if (backwards) { // reverse translation
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) {
4774 returnValue = sign * (val - localMin) * localA + cvsOffset; // from value to chart pixel
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) {
4792 translatedValue = translate(value, null, null, old),
4793 cHeight = (old && oldChartHeight) || chartHeight,
4794 cWidth = (old && oldChartWidth) || chartWidth,
4797 x1 = x2 = mathRound(translatedValue + transB);
4798 y1 = y2 = mathRound(cHeight - translatedValue - transB);
4800 if (isNaN(translatedValue)) { // no min or max
4805 y2 = cHeight - marginBottom;
4806 if (x1 < plotLeft || x1 > plotLeft + plotWidth) {
4811 x2 = cWidth - marginRight;
4812 if (y1 < plotTop || y1 > plotTop + plotHeight) {
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) {
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
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) {
4856 // multiply back to the correct magnitude
4857 interval *= magnitude;
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() {
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,
4879 'second', // unit name
4880 oneSecond, // fixed incremental unit
4881 [1, 2, 5, 10, 15, 30] // allowed multiples
4883 'minute', // unit name
4884 oneMinute, // fixed incremental unit
4885 [1, 2, 5, 10, 15, 30] // allowed multiples
4887 'hour', // unit name
4888 oneHour, // fixed incremental unit
4889 [1, 2, 3, 4, 6, 8, 12] // allowed multiples
4892 oneDay, // fixed incremental unit
4893 [1, 2] // allowed multiples
4895 'week', // unit name
4896 oneWeek, // fixed incremental unit
4897 [1, 2] // allowed multiples
4908 unit = units[6], // default unit is years
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++) {
4916 multiples = unit[2];
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) {
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
5009 time += interval * multitude;
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;
5038 * Set the tick positions of a linear axis to round values like whole tens or every five.
5040 function setLinearTickPositions() {
5043 roundedMin = correctFloat(mathFloor(min / tickInterval) * tickInterval),
5044 roundedMax = correctFloat(mathCeil(max / tickInterval) * tickInterval);
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) {
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) :
5076 axisLength = horiz ? plotWidth : plotHeight;
5078 // linked axis gets the extremes from the parent axis
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
5088 min = pick(userMin, options.min, dataMin);
5089 max = pick(userMax, options.max, dataMax);
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;
5119 } else if (isLinked && !tickIntervalOption &&
5120 tickPixelIntervalOption === linkedParent.options.tickPixelInterval) {
5121 tickInterval = linkedParent.tickInterval;
5123 tickInterval = pick(
5125 categories ? // for categoried axis, 1 is default, for linear axis use tickPix
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();
5144 setLinearTickPositions();
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))) {
5154 if (categories || !defined(pick(options.max, userMax))) {
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) {
5165 } else if (min > roundedMin) {
5166 tickPositions.shift();
5169 if (options.endOnTick) {
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
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() {
5232 // get data extremes if needed
5233 getSeriesExtremes();
5235 // get fixed positions based on tickInterval
5238 // the translation factor used in translate function
5240 transA = axisLength / ((max - min) || 1);
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
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
5274 }, function() { // the default event handler
5282 chart.redraw(animation);
5289 * Get the actual axis extremes
5291 function getExtremes() {
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) {
5309 } else if (max < threshold) {
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);
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),
5335 axisTitleOptions = options.title,
5336 labelOptions = options.labels,
5337 directionFactor = [-1, 1, 1, -1][side],
5341 axisGroup = renderer.g('axis')
5342 .attr({ zIndex: 7 })
5344 gridGroup = renderer.g('grid')
5345 .attr({ zIndex: 1 })
5349 labelOffset = 0; // reset
5351 if (hasData || isLinked) {
5352 each(tickPositions, function(pos) {
5354 ticks[pos] = new Tick(pos);
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(),
5372 labelOffset += (staggerLines - 1) * 16;
5375 } else { // doesn't have data
5382 if (axisTitleOptions && axisTitleOptions.text) {
5383 if (!axis.axisTitle) {
5384 axis.axisTitle = renderer.text(
5385 axisTitleOptions.text,
5391 rotation: axisTitleOptions.rotation || 0,
5393 axisTitleOptions.textAlign ||
5394 { low: 'left', middle: 'center', high: 'right' }[axisTitleOptions.align]
5396 .css(axisTitleOptions.style)
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]);
5410 (side !== 2 && labelOffset && directionFactor * options.labels[horiz ? 'y' : 'x']) +
5413 axisOffset[side] = mathMax(
5415 axisTitleMargin + titleOffset + directionFactor * offset
5424 var axisTitleOptions = options.title,
5425 stackLabelOptions = options.stackLabels,
5426 alternateGridColor = options.alternateGridColor,
5427 lineWidth = options.lineWidth,
5431 hasRendered = chart.hasRendered,
5432 slideInTicks = hasRendered && defined(oldMin) && !isNaN(oldMin),
5433 hasData = associatedSeries.length && defined(min) && defined(max);
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) {
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();
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({
5483 to: tickPositions[i + 1] !== UNDEFINED ? tickPositions[i + 1] : max,
5484 color: alternateGridColor
5487 if (!alternateBands[pos]) {
5488 alternateBands[pos] = new PlotLineOrBand();
5490 alternateBands[pos].options = {
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)
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());
5524 // remove inactive ticks
5525 each([ticks, minorTicks, alternateBands], function(coll) {
5528 if (!coll[pos].isActive) {
5529 coll[pos].destroy();
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.
5544 lineLeft = plotLeft + (opposite ? plotWidth : 0) + offset;
5545 lineTop = chartHeight - marginBottom - (opposite ? plotHeight : 0) + offset;
5547 linePath = renderer.crispLine([
5557 chartWidth - marginRight :
5561 chartHeight - marginBottom
5564 axisLine = renderer.path(linePath)
5566 stroke: options.lineColor,
5567 'stroke-width': lineWidth,
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
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
5593 //(isIE ? fontSize / 3 : 0)+ // preliminary fix for vml's centerline
5594 (side === 2 ? fontSize : 0);
5596 axis.axisTitle[hasRendered ? 'animate' : 'attr']({
5599 offAxis + (opposite ? plotWidth : 0) + offset +
5600 (axisTitleOptions.x || 0), // x
5602 offAxis - (opposite ? plotHeight : 0) + offset:
5603 alongAxis + (axisTitleOptions.y || 0) // y
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')
5618 visibility: VISIBLE,
5621 .translate(plotLeft, plotTop)
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;
5645 if (plotLinesAndBands[i].id === id) {
5646 plotLinesAndBands[i].destroy();
5652 * Redraw the axis to reflect changes in the data or axis extremes
5656 // hide tooltip and hover states
5657 if (tracker.resetTracker) {
5658 tracker.resetTracker();
5664 // move plot lines and bands
5665 each(plotLinesAndBands, function(plotLine) {
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) {
5688 series.setTooltipPoints(true);
5692 // optionally redraw
5693 axis.isDirty = true;
5695 if (pick(doRedraw, true)) {
5704 // inverted charts have reversed xAxes as default
5705 if (inverted && isXAxis && reversed === UNDEFINED) {
5710 // expose some variables
5712 addPlotBand: addPlotBandOrLine,
5713 addPlotLine: addPlotBandOrLine,
5714 adjustTickAmount: adjustTickAmount,
5715 categories: categories,
5716 getExtremes: getExtremes,
5717 getPlotLinePath: getPlotLinePath,
5718 getThreshold: getThreshold,
5721 plotLinesAndBands: plotLinesAndBands,
5722 getOffset: getOffset,
5724 setCategories: setCategories,
5725 setExtremes: setExtremes,
5727 setTickPositions: setTickPositions,
5728 translate: translate,
5730 removePlotBand: removePlotBandOrLine,
5731 removePlotLine: removePlotBandOrLine,
5736 // register event listeners
5737 for (eventType in events) {
5738 addEvent(axis, eventType, events[eventType]);
5748 * The toolbar object
5750 * @param {Object} chart
5752 function Toolbar(chart) {
5755 function add(id, text, title, fn) {
5757 var button = renderer.text(
5762 .css(options.toolbar.itemStyle)
5765 x: - marginRight - 20,
5769 /*.on('touchstart', function(e) {
5770 e.stopPropagation(); // don't fire the container event
5778 buttons[id] = button;
5781 function remove(id) {
5782 discardElement(buttons[id].element);
5794 * The tooltip object
5795 * @param {Object} options Tooltip options
5797 function Tooltip (options) {
5799 borderWidth = options.borderWidth,
5800 crosshairsOptions = options.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,
5813 // remove padding CSS and apply padding on box instead
5816 // create the elements
5817 var group = renderer.g('tooltip')
5818 .attr({ zIndex: 8 })
5821 box = renderer.rect(boxOffLeft, boxOffLeft, 0, 0, options.borderRadius, borderWidth)
5823 fill: options.backgroundColor,
5824 'stroke-width': borderWidth
5827 .shadow(options.shadow),
5828 label = renderer.text('', padding + boxOffLeft, pInt(style.fontSize) + padding + boxOffLeft)
5829 .attr({ zIndex: 1 })
5836 * In case no user defined formatter is given, this will be used
5838 function defaultFormatter() {
5840 items = pThis.points || splat(pThis),
5841 xAxis = items[0].series.xAxis,
5843 isDateTime = xAxis && xAxis.options.type === 'datetime',
5844 useHeader = isString(x) || isDateTime,
5850 ['<span style="font-size: 10px">' +
5851 (isDateTime ? dateFormat('%A, %b %e, %Y', x) : x) +
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);
5889 if (!tooltipIsHidden) {
5890 var hoverPoints = chart.hoverPoints;
5894 each(crosshairs, function(crosshair) {
5900 // hide previous hoverPoints and set new
5902 each(hoverPoints, function(point) {
5906 chart.hoverPoints = null;
5909 tooltipIsHidden = true;
5915 * Refresh the tooltip's text and position.
5916 * @param {Object} point
5919 function refresh(point) {
5931 tooltipPos = point.tooltipPos,
5932 formatter = options.formatter || defaultFormatter,
5933 hoverPoints = chart.hoverPoints;
5935 // shared tooltip, array is sent over
5938 // hide previous hoverPoints and set new
5940 each(hoverPoints, function(point) {
5944 chart.hoverPoints = point;
5946 each(point, function(item, i) {
5947 /*var series = item.series,
5948 hoverPoint = series.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
5963 x: point[0].category
5965 textConfig.points = pointConfig;
5968 // single point tooltip
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) {
5993 if (tooltipIsHidden) {
5995 tooltipIsHidden = false;
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
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
6028 } else if (boxY + boxHeight > chartHeight) {
6029 boxY = chartHeight - boxHeight - 5; // below
6033 move(mathRound(boxX - boxOffLeft), mathRound(boxY - boxOffLeft));
6040 if (crosshairsOptions) {
6041 crosshairsOptions = splat(crosshairsOptions); // [x, y]
6044 i = crosshairsOptions.length,
6049 axis = point.series[i ? 'yAxis' : 'xAxis'];
6050 if (crosshairsOptions[i] && axis) {
6052 .getPlotLinePath(point[i ? 'y' : 'x'], 1);
6053 if (crosshairs[i]) {
6054 crosshairs[i].attr({ d: path, visibility: VISIBLE });
6058 'stroke-width': crosshairsOptions[i].width || 1,
6059 stroke: crosshairsOptions[i].color || '#C0C0C0',
6062 if (crosshairsOptions[i].dashStyle) {
6063 attribs.dashstyle = crosshairsOptions[i].dashStyle;
6065 crosshairs[i] = renderer.path(path)
6085 * The mouse tracker object
6086 * @param {Object} chart
6087 * @param {Object} options
6089 function MouseTracker (chart, options) {
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) {
6108 pageZoomFix = isWebKit && doc.width / doc.documentElement.clientWidth - 1,
6114 // common IE normalizing
6117 e.target = e.srcElement;
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
6135 if (ePos.layerX === UNDEFINED) { // Opera and iOS
6136 chartX = ePos.pageX - chartPosLeft;
6137 chartY = ePos.pageY - chartPosTop;
6144 // correct for page zoom bug in WebKit
6146 chartX += mathRound((pageZoomFix + 1) * chartPosLeft - chartPosLeft);
6147 chartY += mathRound((pageZoomFix + 1) * chartPosTop - chartPosTop);
6157 * Get the click position in terms of axis values.
6159 * @param {Object} e A mouse event
6161 function getMouseCoordinates(e) {
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({
6175 e.chartX - plotLeft :
6176 plotHeight - e.chartY + plotTop,
6185 * With line type charts with a single tracker, get the point closest to the mouse
6187 function onmousemove (e) {
6190 hoverPoint = chart.hoverPoint,
6191 hoverSeries = chart.hoverSeries,
6194 distance = chartWidth,
6195 index = inverted ? e.chartY : e.chartX - plotLeft; // wtf?
6198 if (tooltip && options.shared) {
6201 // loop over all series and find the ones with points closest to the mouse
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);
6211 // remove furthest points
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
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;
6251 hoverPoint.onMouseOut();
6255 hoverSeries.onMouseOut();
6266 * Mouse up or outside the plot area
6269 if (selectionMarker) {
6270 var selectionData = {
6274 selectionBox = selectionMarker.getBBox(),
6275 selectionLeft = selectionBox.x - plotLeft,
6276 selectionTop = selectionBox.y - plotTop;
6279 // a selection has been made
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(
6290 plotHeight - selectionTop - selectionBox.height,
6296 selectionMax = translate(
6298 selectionLeft + selectionBox.width :
6299 plotHeight - selectionTop,
6306 selectionData[isXAxis ? 'xAxis' : 'yAxis'].push({
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
6351 if (e && e.touches && e.touches.length > 1) {
6356 e = normalizeMouseEvent(e);
6357 if (!hasTouch) { // not for touch devices
6358 e.returnValue = false;
6361 var chartX = e.chartX,
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) {
6371 } else if (!runChartClick && !isOutsidePlot) {
6376 // cancel on mouse outside
6377 if (isOutsidePlot) {
6379 if (!lastWasOutsidePlot) {
6380 // reset the tracker
6384 // drop the selection if any and reset mouseIsDown and hasDragged
6386 if (chartX < plotLeft) {
6388 } else if (chartX > plotLeft + plotWidth) {
6389 chartX = plotLeft + plotWidth;
6392 if (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) {
6409 if (hasCartesianSeries && (zoomX || zoomY) &&
6410 isInsidePlot(mouseDownX - plotLeft, mouseDownY - plotTop)) {
6411 if (!selectionMarker) {
6412 selectionMarker = renderer.rect(
6415 zoomHor ? 1 : plotWidth,
6416 zoomVert ? 1 : plotHeight,
6420 fill: 'rgba(69,114,167,0.25)',
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) {
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
6477 * Allow dragging the finger over the chart to read the values on touch
6480 container.ontouchmove = mouseMove;
6483 * Allow dragging the finger over the chart to read the values on touch
6486 container.ontouchend = function() {
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
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, {
6519 // the point click event
6520 hoverPoint.firePointEvent('click', e);
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
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 })
6550 // then position - this happens on load and after resizing and changing
6551 // axis or box positions
6553 trackerGroup.translate(plotLeft, plotTop);
6556 width: chart.plotWidth,
6557 height: chart.plotHeight
6565 placeTrackerGroup();
6566 if (options.enabled) {
6567 chart.tooltip = tooltip = Tooltip(options);
6572 // set the fixed interval ticking for the smooth tooltip
6573 tooltipInterval = setInterval(function() {
6579 // expose properties
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) {
6601 var horizontal = options.layout === 'horizontal',
6602 symbolWidth = options.symbolWidth,
6603 symbolPadding = options.symbolPadding,
6605 style = options.style,
6606 itemStyle = options.itemStyle,
6607 itemHoverStyle = options.itemHoverStyle,
6608 itemHiddenStyle = options.itemHiddenStyle,
6609 padding = pInt(style.padding),
6611 //lineHeight = options.lineHeight || 16,
6613 initialItemX = 4 + padding + symbolWidth + symbolPadding,
6619 legendBorderWidth = options.borderWidth,
6620 legendBackgroundColor = options.backgroundColor,
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,
6647 legendItem.css({ fill: textColor });
6650 legendLine.attr({ stroke: lineColor });
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;
6675 legendLine.translate(itemX, itemY - 4);
6679 x: itemX + legendSymbol.xOff,
6680 y: itemY + legendSymbol.yOff
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) {
6702 item[key].destroy();
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;
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) {
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);
6768 .on('click', function(event) {
6769 var strLegendItemClick = 'legendItemClick',
6770 fnLegendItemClick = function() {
6774 // click the name or symbol
6775 if (item.firePointEvent) { // point
6776 item.firePointEvent(strLegendItemClick, null, fnLegendItemClick);
6778 fireEvent(item, strLegendItemClick, null, fnLegendItemClick);
6781 .attr({ zIndex: 2 })
6785 if (!simpleSymbol && itemOptions && itemOptions.lineWidth) {
6787 'stroke-width': itemOptions.lineWidth,
6790 if (itemOptions.dashStyle) {
6791 attrs.dashstyle = itemOptions.dashStyle;
6793 item.legendLine = renderer.path([
6795 -symbolWidth - symbolPadding,
6805 // draw a simple symbol
6806 if (simpleSymbol) { // bar|pie|area|column
6808 legendSymbol = renderer.rect(
6809 (symbolX = -symbolWidth - symbolPadding),
6815 //'stroke-width': 0,
6817 }).add(legendGroup);
6821 else if (itemOptions && itemOptions.marker && itemOptions.marker.enabled) {
6822 legendSymbol = renderer.symbol(
6824 (symbolX = -symbolWidth / 2 - symbolPadding),
6826 itemOptions.marker.radius
6828 //.attr(item.pointAttr[NORMAL_STATE])
6829 .attr({ zIndex: 3 })
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', {
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
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;
6881 // position the newly generated or reordered items
6882 positionItem(item, itemX, itemY);
6888 itemY += itemHeight;
6891 // the width of the widest item
6892 offsetWidth = widthOption || mathMax(
6893 horizontal ? itemX - initialItemX : itemWidth,
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;
6915 legendGroup = renderer.g('legend')
6916 .attr({ zIndex: 7 })
6921 // add each series or point
6923 each(series, function(serie) {
6924 var seriesOptions = serie.options;
6926 if (!seriesOptions.showInLegend) {
6930 // use points or series for the legend item depending on legendType
6931 allItems = allItems.concat(seriesOptions.legendType === 'point' ?
6938 // sort by legendIndex
6939 allItems.sort(function(a, b) {
6940 return (a.options.legendIndex || 0) - (b.options.legendIndex || 0);
6944 if (reversedLegend) {
6949 each(allItems, renderItem);
6954 legendWidth = widthOption || offsetWidth;
6955 legendHeight = lastItemY - y + itemHeight;
6957 if (legendBorderWidth || legendBackgroundColor) {
6958 legendWidth += 2 * padding;
6959 legendHeight += 2 * padding;
6962 box = renderer.rect(
6967 options.borderRadius,
6968 legendBorderWidth || 0
6970 stroke: options.borderColor,
6971 'stroke-width': legendBorderWidth || 0,
6972 fill: legendBackgroundColor || NONE
6975 .shadow(options.shadow);
6977 } else if (legendWidth > 0 && legendHeight > 0) {
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'],
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, {
7001 height: legendHeight
7002 }), true, spacingBox);
7005 positionCheckboxes();
7014 addEvent(chart, 'endResize', positionCheckboxes);
7018 colorizeItem: colorizeItem,
7019 destroyItem: destroyItem,
7020 renderLegend: renderLegend
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],
7036 hasRendered = chart.hasRendered;
7038 // an inverted chart can't take a column series and vice versa
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) {
7055 if (serie.isCartesian) {
7056 hasCartesianSeries = serie.isCartesian;
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
7072 * @return {Object} series The newly created series object
7074 function addSeries(options, redraw, animation) {
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
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) {
7109 * Adjust all axes tick amounts
7111 function adjustTickAmounts() {
7112 if (optionsChart.alignTicks !== false) {
7113 each(axes, function(axis) {
7114 axis.adjustTickAmount();
7121 * Redraw legend, axes or series based on updated data
7123 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
7126 function redraw(animation) {
7127 var redrawLegend = chart.isDirtyLegend,
7129 isDirtyBox = chart.isDirtyBox, // todo: check if it has actually changed?
7130 seriesLength = series.length,
7132 clipRect = chart.clipRect,
7135 setAnimation(animation, chart);
7137 // link stacked series
7140 if (serie.isDirty && serie.options.stacking) {
7141 hasStackedSeries = true;
7145 if (hasStackedSeries) { // mark others as dirty
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
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) {
7182 each(axes, function(axis) {
7186 adjustTickAmounts();
7190 each(axes, function(axis) {
7191 if (axis.isDirty || isDirtyBox) {
7193 isDirtyBox = true; // always redraw box to reflect changes in the axis labels
7200 // the plot areas size has changed
7203 placeTrackerGroup();
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
7226 // hide tooltip and hover states
7227 if (tracker && tracker.resetTracker) {
7228 tracker.resetTracker();
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
7246 loadingDiv = createElement(DIV, {
7247 className: 'highcharts-loading'
7248 }, extend(loadingOptions.style, {
7249 left: plotLeft + PX,
7251 width: plotWidth + PX,
7252 height: plotHeight + PX,
7257 loadingSpan = createElement(
7260 loadingOptions.labelStyle,
7267 loadingSpan.innerHTML = str || options.lang.loading;
7270 if (!loadingShown) {
7271 css(loadingDiv, { opacity: 0, display: '' });
7272 animate(loadingDiv, {
7273 opacity: loadingOptions.style.opacity
7275 duration: loadingOptions.showDuration
7277 loadingShown = true;
7281 * Hide the loading layer
7283 function hideLoading() {
7284 animate(loadingDiv, {
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
7305 for (i = 0; i < axes.length; i++) {
7306 if (axes[i].options.id === id) {
7312 for (i = 0; i < series.length; i++) {
7313 if (series[i].options.id === id) {
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) {
7331 * Create the Axis instances based on the config options
7333 function getAxes() {
7334 var xAxisOptions = options.xAxis || {},
7335 yAxisOptions = options.yAxis || {},
7338 // make sure the options are arrays and add some members
7339 xAxisOptions = splat(xAxisOptions);
7340 each(xAxisOptions, function(axis, i) {
7345 yAxisOptions = splat(yAxisOptions);
7346 each(yAxisOptions, function(axis, i) {
7350 // concatenate all axis options into one array
7351 axes = xAxisOptions.concat(yAxisOptions);
7353 // loop the options and construct axis objects
7356 axes = map(axes, function(axisOptions) {
7357 axis = new Axis(chart, axisOptions);
7358 chart[axis.isXAxis ? 'xAxis' : 'yAxis'].push(axis);
7363 adjustTickAmounts();
7368 * Get the currently selected points from all series
7370 function getSelectedPoints() {
7372 each(series, function(serie) {
7373 points = points.concat( grep( serie.data, function(point) {
7374 return point.selected;
7381 * Get the currently selected series
7383 function getSelectedSeries() {
7384 return grep(series, function (serie) {
7385 return serie.selected;
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
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);
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
7445 ['title', titleOptions, chartTitleOptions],
7446 ['subtitle', subtitleOptions, chartSubtitleOptions]
7449 title = chart[name],
7450 titleOptions = arr[1],
7451 chartTitleOptions = arr[2];
7453 if (title && titleOptions) {
7454 title.destroy(); // remove old
7457 if (chartTitleOptions && chartTitleOptions.text && !title) {
7458 chart[name] = renderer.text(
7459 chartTitleOptions.text,
7464 align: chartTitleOptions.align,
7465 'class': 'highcharts-'+ name,
7468 .css(chartTitleOptions.style)
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
7509 if (!renderTo.offsetWidth) {
7510 renderToClone = renderTo.cloneNode(0);
7511 css(renderToClone, {
7516 doc.body.appendChild(renderToClone);
7519 // get the width and height
7522 // create the inner container
7523 chart.container = container = createElement(DIV, {
7524 className: 'highcharts-container' +
7525 (optionsChart.className ? ' '+ optionsChart.className : ''),
7529 overflow: HIDDEN, // needed for context menu (avoid scrollbars) and
7530 // content overflow in IE
7531 width: chartWidth + PX,
7532 height: chartHeight + PX,
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();
7555 left: (-(rect.left - pInt(rect.left))) + PX,
7556 top: (-(rect.top - pInt(rect.top))) + PX
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,
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
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(
7605 legendWidth - legendX + legendMargin + spacingRight
7608 } else if (align === 'left') {
7609 if (!defined(optionsMarginLeft)) {
7612 legendWidth + legendX + legendMargin + spacingLeft
7616 } else if (verticalAlign === 'top') {
7617 if (!defined(optionsMarginTop)) {
7620 legendHeight + legendY + legendMargin + spacingTop
7624 } else if (verticalAlign === 'bottom') {
7625 if (!defined(optionsMarginBottom)) {
7626 marginBottom = mathMax(
7628 legendHeight - legendY + legendMargin + spacingBottom
7634 // pre-render axes to get labels offset width
7635 if (hasCartesianSeries) {
7636 each(axes, function(axis) {
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];
7659 * Add the event handlers necessary for auto resizing
7662 function initReflow() {
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);
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;
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);
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;
7717 each(axes, function(axis) {
7718 axis.isDirty = true;
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
7734 chartTitle.align(null, null, spacingBox);
7736 if (chartSubtitle) {
7737 chartSubtitle.align(null, null, spacingBox);
7743 oldChartHeight = null;
7744 fireEvent(chart, 'resize');
7746 // fire endResize and set isResizing back
7747 setTimeout(function() {
7748 fireEvent(chart, 'endResize', null, function() {
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;
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,
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)
7811 stroke: optionsChart.borderColor,
7812 'stroke-width': chartBorderWidth,
7813 fill: chartBackgroundColor || NONE
7816 .shadow(optionsChart.shadow);
7818 chartBackground.animate(
7819 chartBackground.crisp(null, null, null, chartWidth - mgn, chartHeight - mgn)
7826 if (plotBackgroundColor) {
7827 if (!plotBackground) {
7828 plotBackground = renderer.rect(plotLeft, plotTop, plotWidth, plotHeight, 0)
7830 fill: plotBackgroundColor
7833 .shadow(optionsChart.plotShadow);
7835 plotBackground.animate(plotSize);
7838 if (plotBackgroundImage) {
7840 plotBGImage = renderer.image(plotBackgroundImage, plotLeft, plotTop, plotWidth, plotHeight)
7843 plotBGImage.animate(plotSize);
7848 if (optionsChart.plotBorderWidth) {
7850 plotBorder = renderer.rect(plotLeft, plotTop, plotWidth, plotHeight, 0, optionsChart.plotBorderWidth)
7852 stroke: optionsChart.plotBorderColor,
7853 'stroke-width': optionsChart.plotBorderWidth,
7859 plotBorder.crisp(null, plotLeft, plotTop, plotWidth, plotHeight)
7865 chart.isDirtyBox = false;
7869 * Render all graphics for the chart
7871 function render () {
7872 var labels = options.labels,
7873 credits = options.credits,
7881 legend = chart.legend = new Legend(chart);
7883 // Get margins by pre-rendering axes
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
7896 if (hasCartesianSeries) {
7897 each(axes, function(axis) {
7904 if (!chart.seriesGroup) {
7905 chart.seriesGroup = renderer.g('series-group')
7906 .attr({ zIndex: 3 })
7909 each(series, function(serie) {
7911 serie.setTooltipPoints();
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
7932 .attr({ zIndex: 2 })
7939 // Toolbar (don't redraw)
7940 if (!chart.toolbar) {
7941 chart.toolbar = Toolbar(chart);
7945 if (credits.enabled && !chart.credits) {
7946 creditsHref = credits.href;
7952 .on('click', function() {
7954 location.href = creditsHref;
7958 align: credits.position.align,
7963 .align(credits.position);
7966 placeTrackerGroup();
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');
7990 removeEvent(win, 'unload', destroy);
7993 each(axes, function(axis) {
7997 // destroy each series
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);
8007 parentNode.removeChild(container);
8015 if (renderer) { // can break in IE when destroyed before finished loading
8016 renderer.alignedObjects = null;
8019 // memory and CPU leak
8020 clearInterval(tooltipInterval);
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) {
8048 // Set to zero for each new chart
8052 // create the container
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);
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;
8078 fireEvent(chart, 'load');
8080 //globalAnimation = true;
8084 callback.apply(chart, [chart]);
8086 each(chart.callbacks, function(fn) {
8087 fn.apply(chart, [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
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;
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');
8138 $('<button>+</button>')
8139 .insertBefore($container)
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)
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)
8159 if (origChartWidth === UNDEFINED) {
8160 origChartWidth = chartWidth;
8161 origChartHeight = chartHeight;
8163 chart.resize(origChartWidth, origChartHeight);
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() {};
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) {
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) {
8202 point.color = point.options.color = point.color || defaultColors[colorCounter++];
8204 // loop back to zero
8205 if (colorCounter >= defaultColors.length) {
8210 series.chart.pointCount++;
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) {
8221 series = point.series;
8223 point.config = options;
8225 // onedimensional array input
8226 if (isNumber(options) || options === null) {
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() {
8265 series = point.series,
8268 series.chart.pointCount--;
8270 if (point === series.chart.hoverPoint) {
8273 series.chart.hoverPoints = null; // remove reference
8275 // remove all events
8278 each(['graphic', 'tracker', 'group', 'dataLabel', 'connector'], function(prop) {
8280 point[prop].destroy();
8284 if (point.legendItem) { // pies have legend items
8285 point.series.chart.legend.destroyItem(point);
8288 for (prop in point) {
8296 * Return the configuration hash needed for the data label and tooltip formatters
8298 getLabelConfig: function() {
8303 series: point.series,
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) {
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
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() {
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);
8360 point.setState(HOVER_STATE);
8361 chart.hoverPoint = point;
8364 onMouseOut: function() {
8366 point.firePointEvent('mouseOut');
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) {
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
8398 update: function(options, redraw, animation) {
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);
8413 if (isObject(options)) {
8414 series.getAttribs();
8416 graphic.attr(point.pointAttr[series.state]);
8421 series.isDirty = true;
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
8434 remove: function(redraw, animation) {
8436 series = point.series,
8437 chart = series.chart,
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() {
8452 series.isDirty = true;
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) {
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) {
8496 options = merge(point.series.options.point, point.options),
8497 events = options.events,
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) {
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
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)))
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
8550 if (!stateMarkerGraphic) {
8551 series.stateMarkerGraphic = stateMarkerGraphic = chart.renderer.circle(
8552 0, 0, pointAttr[state].r
8554 .attr(pointAttr[state])
8558 stateMarkerGraphic.translate(
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 = {
8585 pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
8586 stroke: 'lineColor',
8587 'stroke-width': 'lineWidth',
8591 init: function(chart, options) {
8596 index = chart.series.length;
8598 series.chart = chart;
8599 options = series.setOptions(options); // merge with plotOptions
8601 // set some variables
8605 name: options.name || 'Series '+ (index + 1),
8606 state: NORMAL_STATE,
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]);
8618 (events && events.click) ||
8619 (options.point && options.point.events && options.point.events.click) ||
8620 options.allowPointSelect
8622 chart.runTrackerClick = true;
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() {
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;
8653 * Sort the data and remove duplicates
8655 cleanData: function() {
8657 chart = series.chart,
8661 chartSmallestInterval = chart.smallestInterval,
8665 // sort the data points
8666 data.sort(function(a, b){
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--) {
8674 if (data[i - 1].x == data[i].x) {
8675 data[i - 1].destroy();
8676 data.splice(i - 1, 1); // remove the duplicate
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]) {
8690 // find the closes pair of points
8691 for (i = data.length - 1; i >= 0; i--) {
8693 interval = data[i].x - data[i - 1].x;
8694 if (interval > 0 && (smallestInterval === UNDEFINED || interval < smallestInterval)) {
8695 smallestInterval = interval;
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() {
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));
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,
8738 plotOptions[this.type],
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) {
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) {
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
8777 addPoint: function(options, redraw, shift, animation) {
8780 graph = series.graph,
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;
8795 redraw = pick(redraw, true);
8799 data[0].remove(false);
8801 series.getAttribs();
8805 series.isDirty = true;
8812 * Replace the series data with a new set of data
8813 * @param {Object} data
8814 * @param {Object} redraw
8816 setData: function(data, redraw) {
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
8834 oldData[i].destroy();
8841 series.getSegments();
8844 // cache attributes for shapes
8845 series.getAttribs();
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
8863 remove: function(redraw, animation) {
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() {
8881 chart.isDirtyLegend = chart.isDirtyBox = true;
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() {
8897 chart = series.chart,
8898 stacking = series.options.stacking,
8899 categories = series.xAxis.categories,
8900 yAxis = series.yAxis,
8904 // do the translation
8906 var point = data[i],
8909 yBottom = point.low,
8910 stack = yAxis.stacks[(yValue < 0 ? '-' : '') + series.stackKey],
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);
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
8946 point.category = categories && categories[point.x] !== UNDEFINED ?
8947 categories[point.x] : point.x;
8952 * Memoize tooltip texts and positions
8954 setTooltipPoints: function (renew) {
8956 chart = series.chart,
8957 inverted = chart.inverted,
8959 plotSize = mathRound((inverted ? chart.plotTop : chart.plotLeft) + chart.plotSizeX),
8962 tooltipPoints = []; // a lookup array for each pixel in the x dimension
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
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)) :
8988 while (low <= high) {
8989 tooltipPoints[inverted ? plotSize - low++ : low++] = point;
8992 series.tooltipPoints = tooltipPoints;
8999 * Series mouse over handler
9001 onMouseOver: function() {
9003 chart = series.chart,
9004 hoverSeries = chart.hoverSeries;
9006 if (!hasTouch && chart.mouseIsDown) {
9010 // set normal state to previous series
9011 if (hoverSeries && hoverSeries !== series) {
9012 hoverSeries.onMouseOut();
9015 // trigger the event, but to save processing time,
9017 if (series.options.events.mouseOver) {
9018 fireEvent(series, 'mouseOver');
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();
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
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
9047 hoverPoint.onMouseOut();
9050 // fire the mouse out event
9051 if (series && options.events.mouseOut) {
9052 fireEvent(series, 'mouseOut');
9057 if (tooltip && !options.stickyTracking) {
9063 chart.hoverSeries = null;
9067 * Animate in the series
9069 animate: function(init) {
9071 chart = series.chart,
9072 clipRect = series.clipRect,
9073 animation = series.options.animation;
9075 if (animation && !isObject(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
9087 width: chart.plotSizeX
9090 // delete this function to allow it only once
9091 this.animate = null;
9099 drawPoints: function(){
9103 chart = series.chart,
9111 if (series.options.marker.enabled) {
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*/
9127 pointAttr = point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE];
9128 radius = pointAttr.r;
9130 if (graphic) { // update
9137 point.graphic = chart.renderer.symbol(
9138 pick(point.marker && point.marker.symbol, series.symbol),
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,
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]);
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() {
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,
9192 stroke: seriesColor,
9198 seriesPointAttr = [],
9200 pointAttrToOptions = series.pointAttrToOptions,
9201 hasPointSpecificOptions,
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]);
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
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) {
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
9293 pointAttr = seriesPointAttr;
9296 point.pointAttr = pointAttr;
9304 * Clear DOM objects and free up memory
9306 destroy: function() {
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
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) {
9327 // destroy all SVGElements associated to the series
9328 each(['area', 'graph', 'dataLabelsGroup', 'group', 'tracker'], function(prop) {
9331 // issue 134 workaround
9332 destroy = issue134 && prop === 'group' ?
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) {
9361 options = series.options.dataLabels,
9363 dataLabelsGroup = series.dataLabelsGroup,
9364 chart = series.chart,
9365 inverted = chart.inverted,
9366 seriesType = series.type,
9368 stacking = series.options.stacking,
9369 isBarLike = seriesType === 'column' || seriesType === 'bar',
9370 vAlignIsNull = options.verticalAlign === null,
9371 yIsNull = options.y === null;
9375 // In stacked series the default label placement is inside the bars
9377 options = merge(options, {verticalAlign: 'middle'});
9380 // If no y delta is specified, try to create a good default
9382 options = merge(options, {y: {top: 14, middle: 4, bottom: -6}[options.verticalAlign]});
9385 // In non stacked series the default label placement is on top of the bars
9387 options = merge(options, {verticalAlign: 'top'});
9390 // If no y delta is specified, set the default
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')
9402 visibility: series.visible ? VISIBLE : HIDDEN,
9405 .translate(chart.plotLeft, chart.plotTop)
9409 // determine the color
9410 color = options.color;
9411 if (color === 'auto') { // 1.0 backwards compatibility
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;
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
9436 // vertically centered
9437 if (inverted && !options.y) {
9438 y = y + pInt(dataLabel.styles.lineHeight) * 0.9 - dataLabel.getBBox().height / 2;
9448 } else if (defined(str)) {
9449 dataLabel = point.dataLabel = chart.renderer.text(
9456 rotation: options.rotation,
9460 .add(dataLabelsGroup);
9461 // vertically centered
9462 if (inverted && !options.y) {
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,
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) {
9496 options = series.options,
9497 chart = series.chart,
9498 graph = series.graph,
9502 group = series.group,
9503 color = options.lineColor || series.color,
9504 lineWidth = options.lineWidth,
9505 dashStyle = options.dashStyle,
9507 renderer = chart.renderer,
9508 translatedThreshold = series.yAxis.getThreshold(options.threshold || 0),
9509 useArea = /^area/.test(series.type),
9510 singlePoints = [], // used in drawTracker
9515 // divide into segments and build graph and area paths
9516 each(series.segments, function(segment) {
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));
9528 segmentPath.push(i ? L : M);
9531 if (i && options.step) {
9532 var lastPoint = segment[i - 1];
9539 // normal line to next point
9547 // add the segment to the graph, or a single point for tracking
9548 if (segment.length > 1) {
9549 graphPath = graphPath.concat(segmentPath);
9551 singlePoints.push(segment[0]);
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,
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
9593 Color(series.color).setOpacity(options.fillOpacity || 0.75).get()
9596 area.animate({ d: areaPath });
9600 series.area = series.chart.renderer.path(areaPath)
9609 //graph.animate({ d: graphPath.join(' ') });
9610 graph.animate({ d: graphPath });
9616 'stroke-width': lineWidth
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() {
9634 chart = series.chart,
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.
9648 clipRect = series.clipRect = !chart.hasRendered && chart.clipRect ?
9650 renderer.clipRect(0, 0, chart.plotSizeX, chart.plotSizeY);
9651 if (!chart.clipRect) {
9652 chart.clipRect = clipRect;
9658 if (!series.group) {
9659 group = series.group = renderer.g('series');
9661 if (chart.inverted) {
9662 setInvert = function() {
9664 width: chart.plotWidth,
9665 height: chart.plotHeight
9669 setInvert(); // do it now
9670 addEvent(chart, 'resize', setInvert); // do it on resize
9672 group.clip(series.clipRect)
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
9685 series.animate(true);
9688 // cache attributes for shapes
9689 //series.getAttribs();
9691 // draw the graph if any
9692 if (series.drawGraph) {
9697 series.drawPoints();
9699 // draw the mouse tracking area
9700 if (series.options.enableMouseTracking !== false) {
9701 series.drawTracker();
9704 // run the animation
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));
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() {
9729 chart = series.chart,
9730 clipRect = series.clipRect,
9731 group = series.group;
9735 clipRect.animate({ // for chart resize
9736 width: chart.plotSizeX,
9737 height: chart.plotSizeY
9741 // reposition on resize
9743 if (chart.inverted) {
9745 width: chart.plotWidth,
9746 height: chart.plotHeight
9751 translateX: chart.plotLeft,
9752 translateY: chart.plotTop
9757 series.setTooltipPoints(true);
9762 * Set the state of the graph
9764 setState: function(state) {
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) {
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) {
9800 chart = series.chart,
9801 legendItem = series.legendItem,
9802 seriesGroup = series.group,
9803 seriesTracker = series.tracker,
9804 dataLabelsGroup = series.dataLabelsGroup,
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]();
9828 if (point.tracker) {
9829 point.tracker[showOrHide]();
9835 if (dataLabelsGroup) {
9836 dataLabelsGroup[showOrHide]();
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) {
9862 fireEvent(series, showOrHide);
9869 this.setVisible(true);
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) {
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() {
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,
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;
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);
9942 tracker.attr({ d: trackerPath });
9945 series.tracker = chart.renderer.path(trackerPath)
9948 stroke: TRACKER_FILL,
9950 'stroke-width' : options.lineWidth + 2 * snap,
9951 visibility: series.visible ? VISIBLE : HIDDEN,
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();
9965 .add(chart.trackerGroup);
9970 }; // end Series prototype
9976 var LineSeries = extendClass(Series);
9977 seriesTypes.line = LineSeries;
9982 var AreaSeries = extendClass(Series, {
9985 seriesTypes.area = AreaSeries;
9991 * SplineSeries object
9993 var SplineSeries = extendClass( Series, {
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],
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,
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
10057 ret = [M, plotX, plotY];
10060 // curve from last point to this
10064 lastPoint.rightContX || lastPoint.plotX,
10065 lastPoint.rightContY || lastPoint.plotY,
10066 leftContX || plotX,
10067 leftContY || plotY,
10071 lastPoint.rightContX = lastPoint.rightContY = null; // reset for updating series later
10076 seriesTypes.spline = SplineSeries;
10081 * AreaSplineSeries object
10083 var AreaSplineSeries = extendClass(SplineSeries, {
10086 seriesTypes.areaspline = AreaSplineSeries;
10089 * ColumnSeries object
10091 var ColumnSeries = extendClass(Series, {
10093 pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
10094 stroke: 'borderColor',
10095 'stroke-width': 'borderWidth',
10100 Series.prototype.init.apply(this, arguments);
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() {
10124 chart = series.chart,
10125 options = series.options,
10126 stacking = options.stacking,
10127 borderWidth = options.borderWidth,
10129 reversedXAxis = series.xAxis.reversed,
10130 categories = series.xAxis.categories,
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];
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],
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;
10201 mathAbs(barY - translatedThreshold) > minPointLength ? // stacked
10202 yBottom - minPointLength : // keep position
10203 translatedThreshold - (plotY <= translatedThreshold ? minPointLength : 0);
10205 trackerY = barY - 3;
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({}, [
10224 r: options.borderRadius
10226 if (borderWidth % 2) { // correct for shorting in crisp method, visible in stacked columns with 1px border
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),
10241 getSymbol: function(){
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() {
10256 options = series.options,
10257 renderer = series.chart.renderer,
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
10270 graphic.animate(shapeArgs);
10273 point.graphic = renderer[point.shapeType](shapeArgs)
10274 .attr(point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE])
10276 .shadow(options.shadow);
10283 * Draw the individual tracker elements.
10284 * This method is inherited by scatter and pie charts too.
10286 drawTracker: function() {
10288 chart = series.chart,
10289 renderer = chart.renderer,
10292 trackerLabel = +new Date(),
10293 cursor = series.options.cursor,
10294 css = cursor && { cursor: cursor },
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);
10307 renderer[point.shapeType](shapeArgs)
10309 isTracker: trackerLabel,
10310 fill: TRACKER_FILL,
10311 visibility: series.visible ? VISIBLE : HIDDEN,
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();
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) {
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;
10363 y: series.yAxis.translate(0, 0, 1)
10368 height: shapeArgs.height,
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() {
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, {
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, {
10418 * Extend the base Series' translate method by adding shape type and
10419 * arguments for the point trackers
10421 translate: function() {
10424 Series.prototype.translate.apply(series);
10426 each(series.data, function(point) {
10427 point.shapeType = 'circle';
10428 point.shapeArgs = {
10431 r: series.chart.options.tooltip.snap
10438 * Create individual tracker elements for each point
10440 //drawTracker: ColumnSeries.prototype.drawTracker,
10441 drawTracker: function() {
10443 cursor = series.options.cursor,
10444 css = cursor && { cursor: cursor },
10447 each(series.data, function(point) {
10448 graphic = point.graphic;
10449 if (graphic) { // doesn't exist for null points
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();
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);
10488 //visible: options.visible !== false,
10490 visible: point.visible !== false,
10491 name: pick(point.name, 'Slice')
10494 // add event listener for select
10495 toggleSlice = function() {
10498 addEvent(point, 'select', toggleSlice);
10499 addEvent(point, 'unselect', toggleSlice);
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) {
10511 chart = point.series.chart,
10512 tracker = point.tracker,
10513 dataLabel = point.dataLabel,
10514 connector = point.connector,
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]();
10527 dataLabel[method]();
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) {
10544 series = point.series,
10545 chart = series.chart,
10546 slicedTranslation = point.slicedTranslation,
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;
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, {
10574 isCartesian: false,
10575 pointClass: PiePoint,
10576 pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
10577 stroke: 'borderColor',
10578 'stroke-width': 'borderWidth',
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) {
10596 data = series.data;
10598 each(data, function(point) {
10599 var graphic = point.graphic,
10600 args = point.shapeArgs,
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() {
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,
10642 data = series.data,
10645 smallestSize = mathMin(plotWidth, plotHeight),
10647 radiusX, // the x component of the radius vector for a given point
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);
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:
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] +
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) {
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;
10691 point.shapeType = 'arc';
10692 point.shapeArgs = {
10695 r: positions[2] / 2,
10696 innerR: positions[3] / 2,
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
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
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
10726 angle < circ / 4 ? 'left' : 'right', // alignment
10727 angle // center angle
10732 point.percentage = fraction * 100;
10733 point.total = total;
10737 this.setTooltipPoints();
10741 * Render the slices
10743 render: function() {
10746 // cache attributes for shapes
10747 //series.getAttribs();
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) {
10762 series.isDirty = false; // means data is in accordance with what you see
10766 * Draw the data points
10768 drawPoints: function() {
10770 chart = series.chart,
10771 renderer = chart.renderer,
10776 shadow = series.options.shadow,
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 })
10795 // create the group the first time
10797 group = point.group = renderer.g('point')
10798 .attr({ zIndex: 5 })
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]);
10806 shadowGroup.translate(groupTranslation[0], groupTranslation[1]);
10812 graphic.animate(shapeArgs);
10815 renderer.arc(shapeArgs)
10817 point.pointAttr[NORMAL_STATE],
10818 { 'stroke-linejoin': 'round' }
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() {
10838 data = series.data,
10840 chart = series.chart,
10841 options = series.options.dataLabels,
10842 connectorPadding = pick(options.connectorPadding, 10),
10843 connectorWidth = pick(options.connectorWidth, 1),
10846 outside = options.distance > 0,
10851 centerY = series.center[1],
10852 quarters = [// divide the points into quarters for anti collision
10854 [], // bottom right
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],
10879 } else if (angle < mathPI / 2) {
10881 } else if (angle < mathPI) {
10886 quarters[quarter].push(point);
10888 quarters[1].reverse();
10889 quarters[3].reverse();
10891 // define the sorting algorithm
10892 sort = function(a,b) {
10895 /* Loop over the points in each quartile, starting from the top and bottom
10896 * of the pie to detect overlapping labels.
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;
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++) {
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;
10922 labelPos = point.labelPos;
10923 visibility = VISIBLE;
10928 // assume all labels have equal height
10929 if (!labelHeight) {
10930 labelHeight = dataLabel && dataLabel.getBBox().height;
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)) {
10944 visibility = HIDDEN;
10952 if (point.visible === false) {
10953 visibility = HIDDEN;
10956 if (visibility === VISIBLE) {
10962 // move or place the data label
10965 visibility: visibility,
10967 })[dataLabel.moved ? 'animate' : 'attr']({
10969 ({ left: connectorPadding, right: -connectorPadding }[labelPos[6]] || 0),
10972 dataLabel.moved = true;
10974 // draw the connector
10975 if (outside && connectorWidth) {
10976 connector = point.connector;
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
10990 connector.animate({ d: connectorPath });
10991 connector.attr('visibility', visibility);
10994 point.connector = connector = series.chart.renderer.path(connectorPath).attr({
10995 'stroke-width': connectorWidth,
10996 stroke: options.connectorColor || '#606060',
10997 visibility: visibility,
11000 .translate(chart.plotLeft, chart.plotTop)
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
11028 dateFormat: dateFormat,
11029 pathAnim: pathAnim,
11030 getOptions: getOptions,
11031 numberFormat: numberFormat,
11034 Renderer: Renderer,
11035 seriesTypes: seriesTypes,
11036 setOptions: setOptions,
11039 // Expose utility funcitons for modules
11040 addEvent: addEvent,
11041 createElement: createElement,
11042 discardElement: discardElement,
11049 extendClass: extendClass,