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
,