2
// @compilation_level SIMPLE_OPTIMIZATIONS
5
* @license Highcharts JS v2.2.1 (2012-03-15)
7
* (c) 2009-2011 Torstein Hønsi
9
* License: www.highcharts.com/license
13
/*global Highcharts, document, window, navigator, setInterval, clearInterval, clearTimeout, setTimeout, location, jQuery, $, console */
16
// encapsulated variables
21
mathRound = math.round,
22
mathFloor = math.floor,
30
deg2rad = mathPI * 2 / 360,
34
userAgent = navigator.userAgent,
35
isIE = /msie/i.test(userAgent) && !win.opera,
36
docMode8 = doc.documentMode === 8,
37
isWebKit = /AppleWebKit/.test(userAgent),
38
isFirefox = /Firefox/.test(userAgent),
39
SVG_NS = 'http://www.w3.org/2000/svg',
40
hasSVG = !!doc.createElementNS && !!doc.createElementNS(SVG_NS, 'svg').createSVGRect,
41
hasBidiBug = isFirefox && parseInt(userAgent.split('Firefox/')[1], 10) < 4, // issue #38
42
useCanVG = !hasSVG && !isIE && !!doc.createElement('canvas').getContext,
44
hasTouch = doc.documentElement.ontouchstart !== UNDEFINED,
49
dateFormat, // function
54
// some constants for frequently used strings
56
ABSOLUTE = 'absolute',
57
RELATIVE = 'relative',
59
PREFIX = 'highcharts-',
66
* Empirical lowest possible opacities for TRACKER_FILL
70
* IE9: 0.00000000001 (unlimited)
71
* FF: 0.00000000001 (unlimited)
74
* Opera: 0.00000000001 (unlimited)
76
TRACKER_FILL = 'rgba(192,192,192,' + (hasSVG ? 0.000001 : 0.002) + ')', // invisible but clickable
77
//TRACKER_FILL = 'rgba(192,192,192,0.5)',
79
HOVER_STATE = 'hover',
80
SELECT_STATE = 'select',
81
MILLISECOND = 'millisecond',
90
// constants for attributes
92
LINEAR_GRADIENT = 'linearGradient',
95
STROKE_WIDTH = 'stroke-width',
97
// time methods, changed based on whether or not UTC is used
111
// check for a custom HighchartsAdapter defined prior to this file
112
globalAdapter = win.HighchartsAdapter,
113
adapter = globalAdapter || {},
115
// Utility functions. If the HighchartsAdapter is not defined, adapter is an empty object
116
// and all the utility functions will be null. In that case they are populated by the
117
// default adapters below.
118
getScript = adapter.getScript,
121
offset = adapter.offset,
123
merge = adapter.merge,
124
addEvent = adapter.addEvent,
125
removeEvent = adapter.removeEvent,
126
fireEvent = adapter.fireEvent,
127
animate = adapter.animate,
130
// lookup over the types and the associated classes
133
// The Highcharts namespace
137
* Extend an object with the members of another
138
* @param {Object} a The object to be extended
139
* @param {Object} b The object to add to the first one
141
function extend(a, b) {
153
* Take an array and turn into a hash with even number arguments as keys and odd numbers as
154
* values. Allows creating constants for commonly used style properties, attributes etc.
155
* Avoid it in performance critical situations like looping
160
length = args.length,
162
for (; i < length; i++) {
163
obj[args[i++]] = args[i];
169
* Shortcut for parseInt
171
* @param {Number} mag Magnitude
173
function pInt(s, mag) {
174
return parseInt(s, mag || 10);
181
function isString(s) {
182
return typeof s === 'string';
187
* @param {Object} obj
189
function isObject(obj) {
190
return typeof obj === 'object';
195
* @param {Object} obj
197
function isArray(obj) {
198
return Object.prototype.toString.call(obj) === '[object Array]';
205
function isNumber(n) {
206
return typeof n === 'number';
209
function log2lin(num) {
210
return math.log(num) / math.LN10;
212
function lin2log(num) {
213
return math.pow(10, num);
217
* Remove last occurence of an item from an array
219
* @param {Mixed} item
221
function erase(arr, item) {
224
if (arr[i] === item) {
233
* Returns true if the object is not null or undefined. Like MooTools' $.defined.
234
* @param {Object} obj
236
function defined(obj) {
237
return obj !== UNDEFINED && obj !== null;
241
* Set or get an attribute or an object of attributes. Can't use jQuery attr because
242
* it attempts to set expando properties on the SVG element, which is not allowed.
244
* @param {Object} elem The DOM element to receive the attribute(s)
245
* @param {String|Object} prop The property or an abject of key-value pairs
246
* @param {String} value The value if a single property is set
248
function attr(elem, prop, value) {
250
setAttribute = 'setAttribute',
253
// if the prop is a string
254
if (isString(prop)) {
256
if (defined(value)) {
258
elem[setAttribute](prop, value);
261
} else if (elem && elem.getAttribute) { // elem not defined when printing pie demo...
262
ret = elem.getAttribute(prop);
265
// else if prop is defined, it is a hash of key/value pairs
266
} else if (defined(prop) && isObject(prop)) {
268
elem[setAttribute](key, prop[key]);
274
* Check if an element is an array, and if not, make it into an array. Like
277
function splat(obj) {
278
return isArray(obj) ? obj : [obj];
283
* Return the first value that is defined. Like MooTools' $.pick.
286
var args = arguments,
289
length = args.length;
290
for (i = 0; i < length; i++) {
292
if (typeof arg !== 'undefined' && arg !== null) {
299
* Set CSS on a given element
301
* @param {Object} styles Style object with camel case property names
303
function css(el, styles) {
305
if (styles && styles.opacity !== UNDEFINED) {
306
styles.filter = 'alpha(opacity=' + (styles.opacity * 100) + ')';
309
extend(el.style, styles);
313
* Utility function to create element with attributes and styles
314
* @param {Object} tag
315
* @param {Object} attribs
316
* @param {Object} styles
317
* @param {Object} parent
318
* @param {Object} nopad
320
function createElement(tag, attribs, styles, parent, nopad) {
321
var el = doc.createElement(tag);
326
css(el, {padding: 0, border: NONE, margin: 0});
332
parent.appendChild(el);
338
* Extend a prototyped class by new members
339
* @param {Object} parent
340
* @param {Object} members
342
function extendClass(parent, members) {
343
var object = function () {};
344
object.prototype = new parent();
345
extend(object.prototype, members);
350
* Format a number and return a string based on input settings
351
* @param {Number} number The input number to format
352
* @param {Number} decimals The amount of decimals
353
* @param {String} decPoint The decimal point, defaults to the one given in the lang options
354
* @param {String} thousandsSep The thousands separator, defaults to the one given in the lang options
356
function numberFormat(number, decimals, decPoint, thousandsSep) {
357
var lang = defaultOptions.lang,
358
// http://kevin.vanzonneveld.net/techblog/article/javascript_equivalent_for_phps_number_format/
360
c = isNaN(decimals = mathAbs(decimals)) ? 2 : decimals,
361
d = decPoint === undefined ? lang.decimalPoint : decPoint,
362
t = thousandsSep === undefined ? lang.thousandsSep : thousandsSep,
363
s = n < 0 ? "-" : "",
364
i = String(pInt(n = mathAbs(+n || 0).toFixed(c))),
365
j = i.length > 3 ? i.length % 3 : 0;
367
return s + (j ? i.substr(0, j) + t : "") + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + t) +
368
(c ? d + mathAbs(n - i).toFixed(c).slice(2) : "");
372
* Pad a string to a given length by adding 0 to the beginning
373
* @param {Number} number
374
* @param {Number} length
376
function pad(number, length) {
377
// Create an array of the remaining length +1 and join it with 0's
378
return new Array((length || 2) + 1 - String(number).length).join(0) + number;
382
* Based on http://www.php.net/manual/en/function.strftime.php
383
* @param {String} format
384
* @param {Number} timestamp
385
* @param {Boolean} capitalize
387
dateFormat = function (format, timestamp, capitalize) {
388
if (!defined(timestamp) || isNaN(timestamp)) {
389
return 'Invalid date';
391
format = pick(format, '%Y-%m-%d %H:%M:%S');
393
var date = new Date(timestamp),
394
key, // used in for constuct below
395
// get the basic time values
396
hours = date[getHours](),
397
day = date[getDay](),
398
dayOfMonth = date[getDate](),
399
month = date[getMonth](),
400
fullYear = date[getFullYear](),
401
lang = defaultOptions.lang,
402
langWeekdays = lang.weekdays,
403
/* // uncomment this and the 'W' format key below to enable week numbers
404
weekNumber = function () {
405
var clone = new Date(date.valueOf()),
406
day = clone[getDay]() == 0 ? 7 : clone[getDay](),
408
clone.setDate(clone[getDate]() + 4 - day);
409
dayNumber = mathFloor((clone.getTime() - new Date(clone[getFullYear](), 0, 1, -6)) / 86400000);
410
return 1 + mathFloor(dayNumber / 7);
414
// list all format keys
418
'a': langWeekdays[day].substr(0, 3), // Short weekday, like 'Mon'
419
'A': langWeekdays[day], // Long weekday, like 'Monday'
420
'd': pad(dayOfMonth), // Two digit day of the month, 01 to 31
421
'e': dayOfMonth, // Day of the month, 1 through 31
423
// Week (none implemented)
427
'b': lang.shortMonths[month], // Short month, like 'Jan'
428
'B': lang.months[month], // Long month, like 'January'
429
'm': pad(month + 1), // Two digit month number, 01 through 12
432
'y': fullYear.toString().substr(2, 2), // Two digits year, like 09 for 2009
433
'Y': fullYear, // Four digits year, like 2009
436
'H': pad(hours), // Two digits hours in 24h format, 00 through 23
437
'I': pad((hours % 12) || 12), // Two digits hours in 12h format, 00 through 11
438
'l': (hours % 12) || 12, // Hours in 12h format, 1 through 12
439
'M': pad(date[getMinutes]()), // Two digits minutes, 00 through 59
440
'p': hours < 12 ? 'AM' : 'PM', // Upper case AM or PM
441
'P': hours < 12 ? 'am' : 'pm', // Lower case AM or PM
442
'S': pad(date.getSeconds()), // Two digits seconds, 00 through 59
443
'L': pad(mathRound(timestamp % 1000), 3) // Milliseconds (naming from Ruby)
448
for (key in replacements) {
449
format = format.replace('%' + key, replacements[key]);
452
// Optionally capitalize the string and return
453
return capitalize ? format.substr(0, 1).toUpperCase() + format.substr(1) : format;
457
* Take an interval and normalize it to multiples of 1, 2, 2.5 and 5
458
* @param {Number} interval
459
* @param {Array} multiples
460
* @param {Number} magnitude
461
* @param {Object} options
463
function normalizeTickInterval(interval, multiples, magnitude, options) {
466
// round to a tenfold of 1, 2, 2.5 or 5
467
magnitude = pick(magnitude, 1);
468
normalized = interval / magnitude;
470
// multiples for a linear scale
472
multiples = [1, 2, 2.5, 5, 10];
474
// the allowDecimals option
475
if (options && options.allowDecimals === false) {
476
if (magnitude === 1) {
477
multiples = [1, 2, 5, 10];
478
} else if (magnitude <= 0.1) {
479
multiples = [1 / magnitude];
484
// normalize the interval to the nearest multiple
485
for (i = 0; i < multiples.length; i++) {
486
interval = multiples[i];
487
if (normalized <= (multiples[i] + (multiples[i + 1] || multiples[i])) / 2) {
492
// multiply back to the correct magnitude
493
interval *= magnitude;
499
* Get a normalized tick interval for dates. Returns a configuration object with
500
* unit range (interval), count and name. Used to prepare data for getTimeTicks.
501
* Previously this logic was part of getTimeTicks, but as getTimeTicks now runs
502
* of segments in stock charts, the normalizing logic was extracted in order to
503
* prevent it for running over again for each segment having the same interval.
506
function normalizeTimeTickInterval(tickInterval, unitsOption) {
507
var units = unitsOption || [[
508
MILLISECOND, // unit name
509
[1, 2, 5, 10, 20, 25, 50, 100, 200, 500] // allowed multiples
512
[1, 2, 5, 10, 15, 30]
515
[1, 2, 5, 10, 15, 30]
518
[1, 2, 3, 4, 6, 8, 12]
532
unit = units[units.length - 1], // default unit is years
533
interval = timeUnits[unit[0]],
538
// loop through the units to find the one that best fits the tickInterval
539
for (i = 0; i < units.length; i++) {
541
interval = timeUnits[unit[0]];
546
// lessThan is in the middle between the highest multiple and the next unit.
547
var lessThan = (interval * multiples[multiples.length - 1] +
548
timeUnits[units[i + 1][0]]) / 2;
550
// break and keep the current unit
551
if (tickInterval <= lessThan) {
557
// prevent 2.5 years intervals, though 25, 250 etc. are allowed
558
if (interval === timeUnits[YEAR] && tickInterval < 5 * interval) {
559
multiples = [1, 2, 5];
562
// prevent 2.5 years intervals, though 25, 250 etc. are allowed
563
if (interval === timeUnits[YEAR] && tickInterval < 5 * interval) {
564
multiples = [1, 2, 5];
568
count = normalizeTickInterval(tickInterval / interval, multiples);
578
* Set the tick positions to a time unit that makes sense, for example
579
* on the first of each month or on every Monday. Return an array
580
* with the time positions. Used in datetime axes as well as for grouping
581
* data on a datetime axis.
583
* @param {Object} normalizedInterval The interval in axis values (ms) and the count
584
* @param {Number} min The minimum in axis values
585
* @param {Number} max The maximum in axis values
586
* @param {Number} startOfWeek
588
function getTimeTicks(normalizedInterval, min, max, startOfWeek) {
589
var tickPositions = [],
592
useUTC = defaultOptions.global.useUTC,
593
minYear, // used in months and years as a basis for Date.UTC()
594
minDate = new Date(min),
595
interval = normalizedInterval.unitRange,
596
count = normalizedInterval.count;
600
if (interval >= timeUnits[SECOND]) { // second
601
minDate.setMilliseconds(0);
602
minDate.setSeconds(interval >= timeUnits[MINUTE] ? 0 :
603
count * mathFloor(minDate.getSeconds() / count));
606
if (interval >= timeUnits[MINUTE]) { // minute
607
minDate[setMinutes](interval >= timeUnits[HOUR] ? 0 :
608
count * mathFloor(minDate[getMinutes]() / count));
611
if (interval >= timeUnits[HOUR]) { // hour
612
minDate[setHours](interval >= timeUnits[DAY] ? 0 :
613
count * mathFloor(minDate[getHours]() / count));
616
if (interval >= timeUnits[DAY]) { // day
617
minDate[setDate](interval >= timeUnits[MONTH] ? 1 :
618
count * mathFloor(minDate[getDate]() / count));
621
if (interval >= timeUnits[MONTH]) { // month
622
minDate[setMonth](interval >= timeUnits[YEAR] ? 0 :
623
count * mathFloor(minDate[getMonth]() / count));
624
minYear = minDate[getFullYear]();
627
if (interval >= timeUnits[YEAR]) { // year
628
minYear -= minYear % count;
629
minDate[setFullYear](minYear);
632
// week is a special case that runs outside the hierarchy
633
if (interval === timeUnits[WEEK]) {
634
// get start of current week, independent of count
635
minDate[setDate](minDate[getDate]() - minDate[getDay]() +
636
pick(startOfWeek, 1));
640
// get tick positions
642
minYear = minDate[getFullYear]();
643
var time = minDate.getTime(),
644
minMonth = minDate[getMonth](),
645
minDateDate = minDate[getDate]();
647
// iterate and add tick positions at appropriate values
649
tickPositions.push(time);
651
// if the interval is years, use Date.UTC to increase years
652
if (interval === timeUnits[YEAR]) {
653
time = makeTime(minYear + i * count, 0);
655
// if the interval is months, use Date.UTC to increase months
656
} else if (interval === timeUnits[MONTH]) {
657
time = makeTime(minYear, minMonth + i * count);
659
// if we're using global time, the interval is not fixed as it jumps
660
// one hour at the DST crossover
661
} else if (!useUTC && (interval === timeUnits[DAY] || interval === timeUnits[WEEK])) {
662
time = makeTime(minYear, minMonth, minDateDate +
663
i * count * (interval === timeUnits[DAY] ? 1 : 7));
665
// else, the interval is fixed and we use simple addition
667
time += interval * count;
669
// mark new days if the time is dividable by day
670
if (interval <= timeUnits[HOUR] && time % timeUnits[DAY] === 0) {
671
higherRanks[time] = DAY;
678
// push the last time
679
tickPositions.push(time);
681
// record information on the chosen unit - for dynamic label formatter
682
tickPositions.info = extend(normalizedInterval, {
683
higherRanks: higherRanks,
684
totalRange: interval * count
687
return tickPositions;
691
* Helper class that contains variuos counters that are local to the chart.
693
function ChartCounters() {
698
ChartCounters.prototype = {
700
* Wraps the color counter if it reaches the specified length.
702
wrapColor: function (length) {
703
if (this.color >= length) {
709
* Wraps the symbol counter if it reaches the specified length.
711
wrapSymbol: function (length) {
712
if (this.symbol >= length) {
719
* Utility method extracted from Tooltip code that places a tooltip in a chart without spilling over
720
* and not covering the point it self.
722
function placeBox(boxWidth, boxHeight, outerLeft, outerTop, outerWidth, outerHeight, point, distance, preferRight) {
724
// keep the box within the chart area
725
var pointX = point.x,
727
x = pointX + outerLeft + (preferRight ? distance : -boxWidth - distance),
728
y = pointY - boxHeight + outerTop + 15, // 15 means the point is 15 pixels up from the bottom of the tooltip
731
// it is too far to the left, adjust it
733
x = outerLeft + pointX + distance;
736
// Test to see if the tooltip is too far to the right,
737
// if it is, move it back to be inside and then up to not cover the point.
738
if ((x + boxWidth) > (outerLeft + outerWidth)) {
739
x -= (x + boxWidth) - (outerLeft + outerWidth);
740
y = pointY - boxHeight + outerTop - distance;
744
// if it is now above the plot area, align it to the top of the plot area
745
if (y < outerTop + 5) {
748
// If the tooltip is still covering the point, move it below instead
749
if (alignedRight && pointY >= y && pointY <= (y + boxHeight)) {
750
y = pointY + outerTop + distance; // below
752
} else if (y + boxHeight > outerTop + outerHeight) {
753
y = outerTop + outerHeight - boxHeight - distance; // below
760
* Utility method that sorts an object array and keeping the order of equal items.
761
* ECMA script standard does not specify the behaviour when items are equal.
763
function stableSort(arr, sortFunction) {
764
var length = arr.length,
768
// Add index to each item
769
for (i = 0; i < length; i++) {
770
arr[i].ss_i = i; // stable sort index
773
arr.sort(function (a, b) {
774
sortValue = sortFunction(a, b);
775
return sortValue === 0 ? a.ss_i - b.ss_i : sortValue;
778
// Remove index from items
779
for (i = 0; i < length; i++) {
780
delete arr[i].ss_i; // stable sort index
785
* Non-recursive method to find the lowest member of an array. Math.min raises a maximum
786
* call stack size exceeded error in Chrome when trying to apply more than 150.000 points. This
787
* method is slightly slower, but safe.
789
function arrayMin(data) {
802
* Non-recursive method to find the lowest member of an array. Math.min raises a maximum
803
* call stack size exceeded error in Chrome when trying to apply more than 150.000 points. This
804
* method is slightly slower, but safe.
806
function arrayMax(data) {
819
* Utility method that destroys any SVGElement or VMLElement that are properties on the given object.
820
* It loops all properties and invokes destroy if there is a destroy method. The property is
823
function destroyObjectProperties(obj) {
826
// If the object is non-null and destroy is defined
827
if (obj[n] && obj[n].destroy) {
828
// Invoke the destroy
832
// Delete the property from the object.
839
* Discard an element by moving it to the bin and delete
840
* @param {Object} The HTML node to discard
842
function discardElement(element) {
843
// create a garbage bin element, not part of the DOM
845
garbageBin = createElement(DIV);
848
// move the node and empty bin
850
garbageBin.appendChild(element);
852
garbageBin.innerHTML = '';
856
* Provide error messages for debugging, with links to online explanation
858
function error(code, stop) {
859
var msg = 'Highcharts error #' + code + ': www.highcharts.com/errors/' + code;
862
} else if (win.console) {
868
* Fix JS round off float errors
869
* @param {Number} num
871
function correctFloat(num) {
878
* The time unit lookup
880
/*jslint white: true*/
887
WEEK, 7 * 24 * 3600000,
888
MONTH, 30 * 24 * 3600000,
891
/*jslint white: false*/
893
* Path interpolation algorithm used across adapters
897
* Prepare start and end values so that the path can be animated one to one
899
init: function (elem, fromD, toD) {
901
var shift = elem.shift,
902
bezier = fromD.indexOf('C') > -1,
903
numParams = bezier ? 7 : 3,
907
start = fromD.split(' '),
908
end = [].concat(toD), // copy
911
sixify = function (arr) { // in splines make move points have six parameters like bezier curves
915
arr.splice(i + 1, 0, arr[i + 1], arr[i + 2], arr[i + 1], arr[i + 2]);
925
// pull out the base lines before padding
927
startBaseLine = start.splice(start.length - 6, 6);
928
endBaseLine = end.splice(end.length - 6, 6);
931
// if shifting points, prepend a dummy point to the end path
934
end = [].concat(end).splice(0, numParams).concat(end);
936
elem.shift = 0; // reset for following animations
938
// copy and append last point until the length matches the end length
940
endLength = end.length;
941
while (start.length < endLength) {
943
//bezier && sixify(start);
944
slice = [].concat(start).splice(start.length - numParams, numParams);
945
if (bezier) { // disable first control point
946
slice[numParams - 6] = slice[numParams - 2];
947
slice[numParams - 5] = slice[numParams - 1];
949
start = start.concat(slice);
953
if (startBaseLine) { // append the base lines for areas
954
start = start.concat(startBaseLine);
955
end = end.concat(endBaseLine);
961
* Interpolate each value of the path and return the array
963
step: function (start, end, pos, complete) {
968
if (pos === 1) { // land on the final path without adjustment points appended in the ends
971
} else if (i === end.length && pos < 1) {
973
startVal = parseFloat(start[i]);
975
isNaN(startVal) ? // a letter instruction like M or L
977
pos * (parseFloat(end[i] - startVal)) + startVal;
980
} else { // if animation is finished or length not matching, land on right value
989
* Set the global animation to either a given value, or fall back to the
990
* given chart's animation option
991
* @param {Object} animation
992
* @param {Object} chart
994
function setAnimation(animation, chart) {
995
globalAnimation = pick(animation, chart.animation);
999
* Define the adapter for frameworks. If an external adapter is not defined,
1000
* Highcharts reverts to the built-in jQuery adapter.
1002
if (globalAdapter && globalAdapter.init) {
1003
// Initialize the adapter with the pathAnim object that takes care
1004
// of path animations.
1005
globalAdapter.init(pathAnim);
1007
if (!globalAdapter && win.jQuery) {
1011
* Downloads a script and executes a callback when done.
1012
* @param {String} scriptLocation
1013
* @param {Function} callback
1015
getScript = jQ.getScript;
1018
* Utility for iterating over an array. Parameters are reversed compared to jQuery.
1019
* @param {Array} arr
1020
* @param {Function} fn
1022
each = function (arr, fn) {
1025
for (; i < len; i++) {
1026
if (fn.call(arr[i], arr[i], i, arr) === false) {
1039
* @param {Array} arr
1040
* @param {Function} fn
1042
map = function (arr, fn) {
1043
//return jQuery.map(arr, fn);
1047
for (; i < len; i++) {
1048
results[i] = fn.call(arr[i], arr[i], i, arr);
1055
* Deep merge two objects and return a third object
1057
merge = function () {
1058
var args = arguments;
1059
return jQ.extend(true, null, args[0], args[1], args[2], args[3]);
1063
* Get the position of an element relative to the top left of the page
1065
offset = function (el) {
1066
return jQ(el).offset();
1070
* Add an event listener
1071
* @param {Object} el A HTML element or custom object
1072
* @param {String} event The event type
1073
* @param {Function} fn The event handler
1075
addEvent = function (el, event, fn) {
1076
jQ(el).bind(event, fn);
1080
* Remove event added with addEvent
1081
* @param {Object} el The object
1082
* @param {String} eventType The event type. Leave blank to remove all events.
1083
* @param {Function} handler The function to remove
1085
removeEvent = function (el, eventType, handler) {
1086
// workaround for jQuery issue with unbinding custom events:
1087
// http://forum.jquery.com/topic/javascript-error-when-unbinding-a-custom-event-using-jquery-1-4-2
1088
var func = doc.removeEventListener ? 'removeEventListener' : 'detachEvent';
1089
if (doc[func] && !el[func]) {
1090
el[func] = function () {};
1093
jQ(el).unbind(eventType, handler);
1097
* Fire an event on a custom object
1098
* @param {Object} el
1099
* @param {String} type
1100
* @param {Object} eventArguments
1101
* @param {Function} defaultFunction
1103
fireEvent = function (el, type, eventArguments, defaultFunction) {
1104
var event = jQ.Event(type),
1105
detachedType = 'detached' + type,
1108
extend(event, eventArguments);
1110
// Prevent jQuery from triggering the object method that is named the
1111
// same as the event. For example, if the event is 'select', jQuery
1112
// attempts calling el.select and it goes into a loop.
1114
el[detachedType] = el[type];
1118
// Wrap preventDefault and stopPropagation in try/catch blocks in
1119
// order to prevent JS errors when cancelling events on non-DOM
1121
each(['preventDefault', 'stopPropagation'], function (fn) {
1122
var base = event[fn];
1123
event[fn] = function () {
1127
if (fn === 'preventDefault') {
1128
defaultPrevented = true;
1135
jQ(el).trigger(event);
1137
// attach the method
1138
if (el[detachedType]) {
1139
el[type] = el[detachedType];
1140
el[detachedType] = null;
1143
if (defaultFunction && !event.isDefaultPrevented() && !defaultPrevented) {
1144
defaultFunction(event);
1149
* Animate a HTML element or SVG element wrapper
1150
* @param {Object} el
1151
* @param {Object} params
1152
* @param {Object} options jQuery-like animation options: duration, easing, callback
1154
animate = function (el, params, options) {
1157
el.toD = params.d; // keep the array form for paths, used in jQ.fx.step.d
1158
params.d = 1; // because in jQuery, animating to an array has a different meaning
1162
$el.animate(params, options);
1166
* Stop running animation
1168
stop = function (el) {
1173
//=== Extend jQuery on init
1175
/*jslint unparam: true*//* allow unused param x in this function */
1176
jQ.extend(jQ.easing, {
1177
easeOutQuad: function (x, t, b, c, d) {
1178
return -c * (t /= d) * (t - 2) + b;
1181
/*jslint unparam: false*/
1183
// extend the animate function to allow SVG animations
1184
var jFx = jQuery.fx,
1187
// extend some methods to check for elem.attr, which means it is a Highcharts SVG object
1188
each(['cur', '_default', 'width', 'height'], function (fn, i) {
1189
var obj = i ? jStep : jFx.prototype, // 'cur', the getter' relates to jFx.prototype
1193
if (base) { // step.width and step.height don't exist in jQuery < 1.7
1195
// create the extended function replacement
1196
obj[fn] = function (fx) {
1198
// jFx.prototype.cur does not use fx argument
1204
// jFX.prototype.cur returns the current value. The other ones are setters
1205
// and returning a value has no effect.
1206
return elem.attr ? // is SVG element wrapper
1207
elem.attr(fx.prop, fx.now) : // apply the SVG wrapper's method
1208
base.apply(this, arguments); // use jQuery's built-in method
1214
jStep.d = function (fx) {
1218
// Normally start and end should be set in state == 0, but sometimes,
1219
// for reasons unknown, this doesn't happen. Perhaps state == 0 is skipped
1222
var ends = pathAnim.init(elem, elem.d, elem.toD);
1229
// interpolate each value of the path
1230
elem.attr('d', pathAnim.step(fx.start, fx.end, fx.pos, elem.toD));
1235
/* ****************************************************************************
1236
* Handle the options *
1237
*****************************************************************************/
1240
defaultLabelOptions = {
1246
/*formatter: function () {
1257
colors: ['#4572A7', '#AA4643', '#89A54E', '#80699B', '#3D96AE',
1258
'#DB843D', '#92A8CD', '#A47D7C', '#B5CA92'],
1259
symbols: ['circle', 'diamond', 'square', 'triangle', 'triangle-down'],
1261
loading: 'Loading...',
1262
months: ['January', 'February', 'March', 'April', 'May', 'June', 'July',
1263
'August', 'September', 'October', 'November', 'December'],
1264
shortMonths: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
1265
weekdays: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
1267
resetZoom: 'Reset zoom',
1268
resetZoomTitle: 'Reset zoom level 1:1',
1273
canvasToolsURL: 'http://code.highcharts.com/2.2.1/modules/canvas-tools.js'
1277
//alignTicks: false,
1280
//events: { load, selection },
1283
//marginRight: null,
1284
//marginBottom: null,
1286
borderColor: '#4572A7',
1289
defaultSeriesType: 'line',
1290
ignoreHiddenSeries: true,
1298
fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif', // default font
1301
backgroundColor: '#FFFFFF',
1302
//plotBackgroundColor: null,
1303
plotBorderColor: '#C0C0C0',
1304
//plotBorderWidth: 0,
1305
//plotShadow: false,
1314
//verticalAlign: 'top',
1317
// relativeTo: 'plot'
1321
text: 'Chart title',
1326
// verticalAlign: 'top',
1339
// verticalAlign: 'top',
1347
line: { // base series options
1348
allowPointSelect: false,
1349
showCheckbox: false,
1353
//connectNulls: false,
1354
//cursor: 'default',
1357
//enableMouseTracking: true,
1368
lineColor: '#FFFFFF',
1370
states: { // states for a single point
1375
fillColor: '#FFFFFF',
1376
lineColor: '#000000',
1384
dataLabels: merge(defaultLabelOptions, {
1387
formatter: function () {
1390
// backgroundColor: undefined,
1391
// borderColor: undefined,
1392
// borderRadius: undefined,
1393
// borderWidth: undefined,
1397
cropThreshold: 300, // draw points outside the plot area when the number of points is less than this
1402
states: { // states for the entire series
1405
//lineWidth: base + 1,
1407
// lineWidth: base + 1,
1415
stickyTracking: true
1417
//pointFormat: '<span style="color:{series.color}">{series.name}</span>: <b>{point.y}</b>'
1418
//valueDecimals: null,
1419
//xDateFormat: '%A, %b %e, %Y',
1423
// turboThreshold: 1000
1430
//font: defaultFont,
1439
layout: 'horizontal',
1440
labelFormatter: function () {
1444
borderColor: '#909090',
1449
// backgroundColor: null,
1458
//cursor: 'pointer', removed as of #601
1464
itemCheckboxStyle: {
1466
width: '13px', // for IE precision
1469
// itemWidth: undefined,
1472
verticalAlign: 'bottom',
1473
// width: undefined,
1479
// hideDuration: 100,
1488
backgroundColor: 'white',
1497
backgroundColor: 'rgba(255, 255, 255, .85)',
1500
//formatter: defaultFormatter,
1501
headerFormat: '<span style="font-size: 10px">{point.key}</span><br/>',
1502
pointFormat: '<span style="color:{series.color}">{series.name}</span>: <b>{point.y}</b><br/>',
1505
snap: hasTouch ? 25 : 10,
1510
whiteSpace: 'nowrap'
1512
//xDateFormat: '%A, %b %e, %Y',
1513
//valueDecimals: null,
1520
text: 'Highcharts.com',
1521
href: 'http://www.highcharts.com',
1525
verticalAlign: 'bottom',
1537
/*jslint white: true*/
1538
var defaultXAxisOptions = {
1539
// allowDecimals: null,
1540
// alternateGridColor: null,
1542
dateTimeLabelFormats: hash(
1543
MILLISECOND, '%H:%M:%S.%L',
1553
gridLineColor: '#C0C0C0',
1554
// gridLineDashStyle: 'solid',
1555
// gridLineWidth: 0,
1558
labels: defaultLabelOptions,
1560
lineColor: '#C0D0E0',
1568
minorGridLineColor: '#E0E0E0',
1569
// minorGridLineDashStyle: null,
1570
minorGridLineWidth: 1,
1571
minorTickColor: '#A0A0A0',
1572
//minorTickInterval: null,
1574
minorTickPosition: 'outside', // inside or outside
1575
//minorTickWidth: 0,
1581
// labels: { align, x, verticalAlign, y, style, rotation, textAlign }
1587
// labels: { align, x, verticalAlign, y, style, rotation, textAlign }
1590
// showFirstLabel: true,
1591
// showLastLabel: true,
1594
tickColor: '#C0D0E0',
1595
//tickInterval: null,
1597
tickmarkPlacement: 'between', // on or between
1598
tickPixelInterval: 100,
1599
tickPosition: 'outside',
1603
align: 'middle', // low, middle or high
1604
//margin: 0 for horizontal, 10 for vertical axes,
1609
//font: defaultFont.replace('normal', 'bold')
1615
type: 'linear' // linear, logarithmic or datetime
1618
defaultYAxisOptions = merge(defaultXAxisOptions, {
1621
tickPixelInterval: 72,
1622
showLastLabel: true,
1642
//verticalAlign: dynamic,
1643
//textAlign: dynamic,
1645
formatter: function () {
1648
style: defaultLabelOptions.style
1652
defaultLeftAxisOptions = {
1662
defaultRightAxisOptions = {
1672
defaultBottomAxisOptions = { // horizontal axis
1677
overflow: 'justify' // docs
1678
// staggerLines: null
1684
defaultTopAxisOptions = merge(defaultBottomAxisOptions, {
1688
// staggerLines: null
1691
/*jslint white: false*/
1696
var defaultPlotOptions = defaultOptions.plotOptions,
1697
defaultSeriesOptions = defaultPlotOptions.line;
1698
//defaultPlotOptions.line = merge(defaultSeriesOptions);
1699
defaultPlotOptions.spline = merge(defaultSeriesOptions);
1700
defaultPlotOptions.scatter = merge(defaultSeriesOptions, {
1708
headerFormat: '<span style="font-size: 10px; color:{series.color}">{series.name}</span><br/>',
1709
pointFormat: 'x: <b>{point.x}</b><br/>y: <b>{point.y}</b><br/>'
1712
defaultPlotOptions.area = merge(defaultSeriesOptions, {
1714
// lineColor: null, // overrides color, but lets fillColor be unaltered
1715
// fillOpacity: 0.75,
1719
defaultPlotOptions.areaspline = merge(defaultPlotOptions.area);
1720
defaultPlotOptions.column = merge(defaultSeriesOptions, {
1721
borderColor: '#FFFFFF',
1724
//colorByPoint: undefined,
1726
marker: null, // point options are specified in the base options
1730
cropThreshold: 50, // when there are more points, they will not animate out of the chart on xAxis.setExtremes
1731
pointRange: null, // null means auto, meaning 1 in a categorized axis and least distance between points if not categories
1739
borderColor: '#000000',
1749
defaultPlotOptions.bar = merge(defaultPlotOptions.column, {
1754
verticalAlign: 'middle'
1757
defaultPlotOptions.pie = merge(defaultSeriesOptions, {
1758
//dragType: '', // n/a
1759
borderColor: '#FFFFFF',
1761
center: ['50%', '50%'],
1762
colorByPoint: true, // always true for pies
1765
// connectorWidth: 1,
1766
// connectorColor: point.color,
1767
// connectorPadding: 5,
1770
formatter: function () {
1771
return this.point.name;
1773
// softConnector: true,
1777
legendType: 'point',
1778
marker: null, // point options are specified in the base options
1780
showInLegend: false,
1791
// set the default time methods
1797
* Set the time methods globally based on the useUTC option. Time method can be either
1798
* local time or UTC (default).
1800
function setTimeMethods() {
1801
var useUTC = defaultOptions.global.useUTC,
1802
GET = useUTC ? 'getUTC' : 'get',
1803
SET = useUTC ? 'setUTC' : 'set';
1805
makeTime = useUTC ? Date.UTC : function (year, month, date, hours, minutes, seconds) {
1815
getMinutes = GET + 'Minutes';
1816
getHours = GET + 'Hours';
1817
getDay = GET + 'Day';
1818
getDate = GET + 'Date';
1819
getMonth = GET + 'Month';
1820
getFullYear = GET + 'FullYear';
1821
setMinutes = SET + 'Minutes';
1822
setHours = SET + 'Hours';
1823
setDate = SET + 'Date';
1824
setMonth = SET + 'Month';
1825
setFullYear = SET + 'FullYear';
1830
* Merge the default options with custom options and return the new options structure
1831
* @param {Object} options The new custom options
1833
function setOptions(options) {
1835
// Pull out axis options and apply them to the respective default axis options
1836
defaultXAxisOptions = merge(defaultXAxisOptions, options.xAxis);
1837
defaultYAxisOptions = merge(defaultYAxisOptions, options.yAxis);
1838
options.xAxis = options.yAxis = UNDEFINED;
1840
// Merge in the default options
1841
defaultOptions = merge(defaultOptions, options);
1846
return defaultOptions;
1850
* Get the updated default options. Merely exposing defaultOptions for outside modules
1851
* isn't enough because the setOptions method creates a new object.
1853
function getOptions() {
1854
return defaultOptions;
1860
* Handle color operations. The object methods are chainable.
1861
* @param {String} input The input color in either rbga or hex format
1863
var Color = function (input) {
1864
// declare variables
1865
var rgba = [], result;
1868
* Parse the input color to rgba array
1869
* @param {String} input
1871
function init(input) {
1874
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);
1876
rgba = [pInt(result[1]), pInt(result[2]), pInt(result[3]), parseFloat(result[4], 10)];
1878
result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(input);
1880
rgba = [pInt(result[1], 16), pInt(result[2], 16), pInt(result[3], 16), 1];
1886
* Return the color a specified format
1887
* @param {String} format
1889
function get(format) {
1892
// it's NaN if gradient colors on a column chart
1893
if (rgba && !isNaN(rgba[0])) {
1894
if (format === 'rgb') {
1895
ret = 'rgb(' + rgba[0] + ',' + rgba[1] + ',' + rgba[2] + ')';
1896
} else if (format === 'a') {
1899
ret = 'rgba(' + rgba.join(',') + ')';
1908
* Brighten the color
1909
* @param {Number} alpha
1911
function brighten(alpha) {
1912
if (isNumber(alpha) && alpha !== 0) {
1914
for (i = 0; i < 3; i++) {
1915
rgba[i] += pInt(alpha * 255);
1920
if (rgba[i] > 255) {
1928
* Set the color's opacity to a given alpha value
1929
* @param {Number} alpha
1931
function setOpacity(alpha) {
1936
// initialize: parse the input
1943
setOpacity: setOpacity
1949
* A wrapper object for SVG elements
1951
function SVGElement() {}
1953
SVGElement.prototype = {
1955
* Initialize the SVG renderer
1956
* @param {Object} renderer
1957
* @param {String} nodeName
1959
init: function (renderer, nodeName) {
1961
wrapper.element = nodeName === 'span' ?
1962
createElement(nodeName) :
1963
doc.createElementNS(SVG_NS, nodeName);
1964
wrapper.renderer = renderer;
1966
* A collection of attribute setters. These methods, if defined, are called right before a certain
1967
* attribute is set on an element wrapper. Returning false prevents the default attribute
1968
* setter to run. Returning a value causes the default setter to set that value. Used in
1971
wrapper.attrSetters = {};
1974
* Animate a given attribute
1975
* @param {Object} params
1976
* @param {Number} options The same options as in jQuery animation
1977
* @param {Function} complete Function to perform at the end of animation
1979
animate: function (params, options, complete) {
1980
var animOptions = pick(options, globalAnimation, true);
1981
stop(this); // stop regardless of animation actually running, or reverting to .attr (#607)
1983
animOptions = merge(animOptions);
1984
if (complete) { // allows using a callback with the global animation without overwriting it
1985
animOptions.complete = complete;
1987
animate(this, params, animOptions);
1996
* Set or get a given attribute
1997
* @param {Object|String} hash
1998
* @param {Mixed|Undefined} val
2000
attr: function (hash, val) {
2007
element = wrapper.element,
2008
nodeName = element.nodeName,
2009
renderer = wrapper.renderer,
2011
attrSetters = wrapper.attrSetters,
2012
shadows = wrapper.shadows,
2016
// single key-value pair
2017
if (isString(hash) && defined(val)) {
2023
// used as a getter: first argument is a string, second is undefined
2024
if (isString(hash)) {
2026
if (nodeName === 'circle') {
2027
key = { x: 'cx', y: 'cy' }[key] || key;
2028
} else if (key === 'strokeWidth') {
2029
key = 'stroke-width';
2031
ret = attr(element, key) || wrapper[key] || 0;
2033
if (key !== 'd' && key !== 'visibility') { // 'd' is string in animation step
2034
ret = parseFloat(ret);
2041
skipAttr = false; // reset
2044
// check for a specific attribute setter
2045
result = attrSetters[key] && attrSetters[key](value, key);
2047
if (result !== false) {
2049
if (result !== UNDEFINED) {
2050
value = result; // the attribute setter has returned a new value to set
2055
if (value && value.join) { // join path
2056
value = value.join(' ');
2058
if (/(NaN| {2}|^$)/.test(value)) {
2061
wrapper.d = value; // shortcut for animations
2063
// update child tspans x values
2064
} else if (key === 'x' && nodeName === 'text') {
2065
for (i = 0; i < element.childNodes.length; i++) {
2066
child = element.childNodes[i];
2067
// if the x values are equal, the tspan represents a linebreak
2068
if (attr(child, 'x') === attr(element, 'x')) {
2069
//child.setAttribute('x', value);
2070
attr(child, 'x', value);
2074
if (wrapper.rotation) {
2075
attr(element, 'transform', 'rotate(' + wrapper.rotation + ' ' + value + ' ' +
2076
pInt(hash.y || attr(element, 'y')) + ')');
2080
} else if (key === 'fill') {
2081
value = renderer.color(value, element, key);
2084
} else if (nodeName === 'circle' && (key === 'x' || key === 'y')) {
2085
key = { x: 'cx', y: 'cy' }[key] || key;
2087
// rectangle border radius
2088
} else if (nodeName === 'rect' && key === 'r') {
2095
// translation and text rotation
2096
} else if (key === 'translateX' || key === 'translateY' || key === 'rotation' || key === 'verticalAlign') {
2097
wrapper[key] = value;
2098
wrapper.updateTransform();
2101
// apply opacity as subnode (required by legacy WebKit and Batik)
2102
} else if (key === 'stroke') {
2103
value = renderer.color(value, element, key);
2105
// emulate VML's dashstyle implementation
2106
} else if (key === 'dashstyle') {
2107
key = 'stroke-dasharray';
2108
value = value && value.toLowerCase();
2109
if (value === 'solid') {
2113
.replace('shortdashdotdot', '3,1,1,1,1,1,')
2114
.replace('shortdashdot', '3,1,1,1')
2115
.replace('shortdot', '1,1,')
2116
.replace('shortdash', '3,1,')
2117
.replace('longdash', '8,3,')
2118
.replace(/dot/g, '1,3,')
2119
.replace('dash', '4,3,')
2121
.split(','); // ending comma
2125
value[i] = pInt(value[i]) * hash['stroke-width'];
2127
value = value.join(',');
2131
} else if (key === 'isTracker') {
2132
wrapper[key] = value;
2134
// IE9/MooTools combo: MooTools returns objects instead of numbers and IE9 Beta 2
2135
// is unable to cast them. Test again with final IE9.
2136
} else if (key === 'width') {
2137
value = pInt(value);
2140
} else if (key === 'align') {
2141
key = 'text-anchor';
2142
value = { left: 'start', center: 'middle', right: 'end' }[value];
2144
// Title requires a subnode, #431
2145
} else if (key === 'title') {
2146
var title = doc.createElementNS(SVG_NS, 'title');
2147
title.appendChild(doc.createTextNode(value));
2148
element.appendChild(title);
2151
// jQuery animate changes case
2152
if (key === 'strokeWidth') {
2153
key = 'stroke-width';
2156
// Chrome/Win < 6 bug (http://code.google.com/p/chromium/issues/detail?id=15461)
2157
if (isWebKit && key === 'stroke-width' && value === 0) {
2162
if (wrapper.symbolName && /^(x|y|r|start|end|innerR|anchorX|anchorY)/.test(key)) {
2165
if (!hasSetSymbolSize) {
2166
wrapper.symbolAttr(hash);
2167
hasSetSymbolSize = true;
2172
// let the shadow follow the main element
2173
if (shadows && /^(width|height|visibility|x|y|d|transform)$/.test(key)) {
2176
attr(shadows[i], key, value);
2181
if ((key === 'width' || key === 'height') && nodeName === 'rect' && value < 0) {
2188
if (key === 'text') {
2189
// only one node allowed
2190
wrapper.textStr = value;
2191
if (wrapper.added) {
2192
renderer.buildText(wrapper);
2194
} else if (!skipAttr) {
2195
attr(element, key, value);
2204
// Workaround for our #732, WebKit's issue https://bugs.webkit.org/show_bug.cgi?id=78385
2205
// TODO: If the WebKit team fix this bug before the final release of Chrome 18, remove the workaround.
2206
if (isWebKit && /Chrome\/(18|19)/.test(userAgent)) {
2207
if (nodeName === 'text' && (hash.x !== UNDEFINED || hash.y !== UNDEFINED)) {
2208
var parent = element.parentNode,
2209
next = element.nextSibling;
2212
parent.removeChild(element);
2214
parent.insertBefore(element, next);
2216
parent.appendChild(element);
2221
// End of workaround for #732
2227
* If one of the symbol size affecting parameters are changed,
2228
* check all the others only once for each call to an element's
2230
* @param {Object} hash
2232
symbolAttr: function (hash) {
2235
each(['x', 'y', 'r', 'start', 'end', 'width', 'height', 'innerR', 'anchorX', 'anchorY'], function (key) {
2236
wrapper[key] = pick(hash[key], wrapper[key]);
2240
d: wrapper.renderer.symbols[wrapper.symbolName](wrapper.x, wrapper.y, wrapper.width, wrapper.height, wrapper)
2245
* Apply a clipping path to this object
2246
* @param {String} id
2248
clip: function (clipRect) {
2249
return this.attr('clip-path', 'url(' + this.renderer.url + '#' + clipRect.id + ')');
2253
* Calculate the coordinates needed for drawing a rectangle crisply and return the
2254
* calculated attributes
2255
* @param {Number} strokeWidth
2258
* @param {Number} width
2259
* @param {Number} height
2261
crisp: function (strokeWidth, x, y, width, height) {
2269
strokeWidth = strokeWidth || wrapper.strokeWidth || (wrapper.attr && wrapper.attr('stroke-width')) || 0;
2270
normalizer = mathRound(strokeWidth) % 2 / 2; // mathRound because strokeWidth can sometimes have roundoff errors
2272
// normalize for crisp edges
2273
values.x = mathFloor(x || wrapper.x || 0) + normalizer;
2274
values.y = mathFloor(y || wrapper.y || 0) + normalizer;
2275
values.width = mathFloor((width || wrapper.width || 0) - 2 * normalizer);
2276
values.height = mathFloor((height || wrapper.height || 0) - 2 * normalizer);
2277
values.strokeWidth = strokeWidth;
2279
for (key in values) {
2280
if (wrapper[key] !== values[key]) { // only set attribute if changed
2281
wrapper[key] = attribs[key] = values[key];
2289
* Set styles for the element
2290
* @param {Object} styles
2292
css: function (styles) {
2293
/*jslint unparam: true*//* allow unused param a in the regexp function below */
2294
var elemWrapper = this,
2295
elem = elemWrapper.element,
2296
textWidth = styles && styles.width && elem.nodeName === 'text',
2299
hyphenate = function (a, b) { return '-' + b.toLowerCase(); };
2300
/*jslint unparam: false*/
2303
if (styles && styles.color) {
2304
styles.fill = styles.color;
2307
// Merge the new styles with the old ones
2314
elemWrapper.styles = styles;
2316
// serialize and set style attribute
2317
if (isIE && !hasSVG) { // legacy IE doesn't support setting style attribute
2319
delete styles.width;
2321
css(elemWrapper.element, styles);
2324
serializedCss += n.replace(/([A-Z])/g, hyphenate) + ':' + styles[n] + ';';
2327
style: serializedCss
2333
if (textWidth && elemWrapper.added) {
2334
elemWrapper.renderer.buildText(elemWrapper);
2341
* Add an event listener
2342
* @param {String} eventType
2343
* @param {Function} handler
2345
on: function (eventType, handler) {
2348
if (hasTouch && eventType === 'click') {
2349
eventType = 'touchstart';
2355
// simplest possible event model for internal use
2356
this.element['on' + eventType] = fn;
2362
* Move an object and its children by x and y values
2366
translate: function (x, y) {
2374
* Invert a group, rotate and flip
2376
invert: function () {
2378
wrapper.inverted = true;
2379
wrapper.updateTransform();
2384
* Apply CSS to HTML elements. This is used in text within SVG rendering and
2385
* by the VML renderer
2387
htmlCss: function (styles) {
2389
element = wrapper.element,
2390
textWidth = styles && element.tagName === 'SPAN' && styles.width;
2393
delete styles.width;
2394
wrapper.textWidth = textWidth;
2395
wrapper.updateTransform();
2398
wrapper.styles = extend(wrapper.styles, styles);
2399
css(wrapper.element, styles);
2407
* VML and useHTML method for calculating the bounding box based on offsets
2408
* @param {Boolean} refresh Whether to force a fresh value from the DOM or to
2409
* use the cached value
2411
* @return {Object} A hash containing values for x, y, width and height
2414
htmlGetBBox: function (refresh) {
2416
element = wrapper.element,
2417
bBox = wrapper.bBox;
2419
// faking getBBox in exported SVG in legacy IE
2420
if (!bBox || refresh) {
2421
// faking getBBox in exported SVG in legacy IE
2422
if (element.nodeName === 'text') {
2423
element.style.position = ABSOLUTE;
2426
bBox = wrapper.bBox = {
2427
x: element.offsetLeft,
2428
y: element.offsetTop,
2429
width: element.offsetWidth,
2430
height: element.offsetHeight
2438
* VML override private method to update elements based on internal
2439
* properties based on SVG transform
2441
htmlUpdateTransform: function () {
2442
// aligning non added elements is expensive
2444
this.alignOnAdd = true;
2449
renderer = wrapper.renderer,
2450
elem = wrapper.element,
2451
translateX = wrapper.translateX || 0,
2452
translateY = wrapper.translateY || 0,
2455
align = wrapper.textAlign || 'left',
2456
alignCorrection = { left: 0, center: 0.5, right: 1 }[align],
2457
nonLeft = align && align !== 'left',
2458
shadows = wrapper.shadows;
2461
if (translateX || translateY) {
2463
marginLeft: translateX,
2464
marginTop: translateY
2466
if (shadows) { // used in labels/tooltip
2467
each(shadows, function (shadow) {
2469
marginLeft: translateX + 1,
2470
marginTop: translateY + 1
2477
if (wrapper.inverted) { // wrapper is a group
2478
each(elem.childNodes, function (child) {
2479
renderer.invertChild(child, elem);
2483
if (elem.tagName === 'SPAN') {
2486
rotation = wrapper.rotation,
2492
textWidth = pInt(wrapper.textWidth),
2493
xCorr = wrapper.xCorr || 0,
2494
yCorr = wrapper.yCorr || 0,
2495
currentTextTransform = [rotation, align, elem.innerHTML, wrapper.textWidth].join(',');
2497
if (currentTextTransform !== wrapper.cTT) { // do the calculations and DOM access only if properties changed
2499
if (defined(rotation)) {
2500
radians = rotation * deg2rad; // deg to rad
2501
costheta = mathCos(radians);
2502
sintheta = mathSin(radians);
2504
// Adjust for alignment and rotation. Rotation of useHTML content is not yet implemented
2505
// but it can probably be implemented for Firefox 3.5+ on user request. FF3.5+
2506
// has support for CSS3 transform. The getBBox method also needs to be updated
2507
// to compensate for the rotation, like it currently does for SVG.
2508
// Test case: http://highcharts.com/tests/?file=text-rotation
2510
filter: rotation ? ['progid:DXImageTransform.Microsoft.Matrix(M11=', costheta,
2511
', M12=', -sintheta, ', M21=', sintheta, ', M22=', costheta,
2512
', sizingMethod=\'auto expand\')'].join('') : NONE
2516
width = pick(wrapper.elemWidth, elem.offsetWidth);
2517
height = pick(wrapper.elemHeight, elem.offsetHeight);
2520
if (width > textWidth) {
2522
width: textWidth + PX,
2524
whiteSpace: 'normal'
2530
baseline = renderer.fontMetrics(elem.style.fontSize).b;
2531
xCorr = costheta < 0 && -width;
2532
yCorr = sintheta < 0 && -height;
2534
// correct for baseline and corners spilling out after rotation
2535
quad = costheta * sintheta < 0;
2536
xCorr += sintheta * baseline * (quad ? 1 - alignCorrection : alignCorrection);
2537
yCorr -= costheta * baseline * (rotation ? (quad ? alignCorrection : 1 - alignCorrection) : 1);
2539
// correct for the length/height of the text
2541
xCorr -= width * alignCorrection * (costheta < 0 ? -1 : 1);
2543
yCorr -= height * alignCorrection * (sintheta < 0 ? -1 : 1);
2550
// record correction
2551
wrapper.xCorr = xCorr;
2552
wrapper.yCorr = yCorr;
2555
// apply position with correction
2557
left: (x + xCorr) + PX,
2558
top: (y + yCorr) + PX
2561
// record current text transform
2562
wrapper.cTT = currentTextTransform;
2567
* Private method to update the transform attribute based on internal
2570
updateTransform: function () {
2572
translateX = wrapper.translateX || 0,
2573
translateY = wrapper.translateY || 0,
2574
inverted = wrapper.inverted,
2575
rotation = wrapper.rotation,
2578
// flipping affects translate as adjustment for flipping around the group's axis
2580
translateX += wrapper.attr('width');
2581
translateY += wrapper.attr('height');
2585
if (translateX || translateY) {
2586
transform.push('translate(' + translateX + ',' + translateY + ')');
2591
transform.push('rotate(90) scale(-1,1)');
2592
} else if (rotation) { // text rotation
2593
transform.push('rotate(' + rotation + ' ' + wrapper.x + ' ' + wrapper.y + ')');
2596
if (transform.length) {
2597
attr(wrapper.element, 'transform', transform.join(' '));
2601
* Bring the element to the front
2603
toFront: function () {
2604
var element = this.element;
2605
element.parentNode.appendChild(element);
2611
* Break down alignment options like align, verticalAlign, x and y
2612
* to x and y relative to the chart.
2614
* @param {Object} alignOptions
2615
* @param {Boolean} alignByTranslate
2616
* @param {Object} box The box to align to, needs a width and height
2619
align: function (alignOptions, alignByTranslate, box) {
2620
var elemWrapper = this;
2622
if (!alignOptions) { // called on resize
2623
alignOptions = elemWrapper.alignOptions;
2624
alignByTranslate = elemWrapper.alignByTranslate;
2625
} else { // first call on instanciate
2626
elemWrapper.alignOptions = alignOptions;
2627
elemWrapper.alignByTranslate = alignByTranslate;
2628
if (!box) { // boxes other than renderer handle this internally
2629
elemWrapper.renderer.alignedObjects.push(elemWrapper);
2633
box = pick(box, elemWrapper.renderer);
2635
var align = alignOptions.align,
2636
vAlign = alignOptions.verticalAlign,
2637
x = (box.x || 0) + (alignOptions.x || 0), // default: left align
2638
y = (box.y || 0) + (alignOptions.y || 0), // default: top align
2643
if (/^(right|center)$/.test(align)) {
2644
x += (box.width - (alignOptions.width || 0)) /
2645
{ right: 1, center: 2 }[align];
2647
attribs[alignByTranslate ? 'translateX' : 'x'] = mathRound(x);
2651
if (/^(bottom|middle)$/.test(vAlign)) {
2652
y += (box.height - (alignOptions.height || 0)) /
2653
({ bottom: 1, middle: 2 }[vAlign] || 1);
2656
attribs[alignByTranslate ? 'translateY' : 'y'] = mathRound(y);
2658
// animate only if already placed
2659
elemWrapper[elemWrapper.placed ? 'animate' : 'attr'](attribs);
2660
elemWrapper.placed = true;
2661
elemWrapper.alignAttr = attribs;
2667
* Get the bounding box (width, height, x and y) for the element
2669
getBBox: function (refresh) {
2674
rotation = wrapper.rotation,
2675
element = wrapper.element,
2676
rad = rotation * deg2rad;
2679
if (element.namespaceURI === SVG_NS) {
2680
try { // Fails in Firefox if the container has display: none.
2682
bBox = element.getBBox ?
2683
// SVG: use extend because IE9 is not allowed to change width and height in case
2684
// of rotation (below)
2685
extend({}, element.getBBox()) :
2686
// Canvas renderer: // TODO: can this be removed now that we're checking for the SVG NS?
2688
width: element.offsetWidth,
2689
height: element.offsetHeight
2693
// If the bBox is not set, the try-catch block above failed. The other condition
2694
// is for Opera that returns a width of -Infinity on hidden elements.
2695
if (!bBox || bBox.width < 0) {
2696
bBox = { width: 0, height: 0 };
2700
height = bBox.height;
2702
// adjust for rotated text
2704
bBox.width = mathAbs(height * mathSin(rad)) + mathAbs(width * mathCos(rad));
2705
bBox.height = mathAbs(height * mathCos(rad)) + mathAbs(width * mathSin(rad));
2708
// VML Renderer or useHTML within SVG
2710
bBox = wrapper.htmlGetBBox(refresh);
2720
return this.attr({ visibility: VISIBLE });
2727
return this.attr({ visibility: HIDDEN });
2732
* @param {Object|Undefined} parent Can be an element, an element wrapper or undefined
2733
* to append the element to the renderer.box.
2735
add: function (parent) {
2737
var renderer = this.renderer,
2738
parentWrapper = parent || renderer,
2739
parentNode = parentWrapper.element || renderer.box,
2740
childNodes = parentNode.childNodes,
2741
element = this.element,
2742
zIndex = attr(element, 'zIndex'),
2749
this.parentInverted = parent && parent.inverted;
2751
// build formatted text
2752
if (this.textStr !== undefined) {
2753
renderer.buildText(this);
2756
// mark the container as having z indexed children
2758
parentWrapper.handleZ = true;
2759
zIndex = pInt(zIndex);
2762
// insert according to this and other elements' zIndex
2763
if (parentWrapper.handleZ) { // this element or any of its siblings has a z index
2764
for (i = 0; i < childNodes.length; i++) {
2765
otherElement = childNodes[i];
2766
otherZIndex = attr(otherElement, 'zIndex');
2767
if (otherElement !== element && (
2768
// insert before the first element with a higher zIndex
2769
pInt(otherZIndex) > zIndex ||
2770
// if no zIndex given, insert before the first element with a zIndex
2771
(!defined(zIndex) && defined(otherZIndex))
2774
parentNode.insertBefore(element, otherElement);
2781
// default: append at the end
2783
parentNode.appendChild(element);
2789
// fire an event for internal hooks
2790
fireEvent(this, 'add');
2796
* Removes a child either by removeChild or move to garbageBin.
2797
* Issue 490; in VML removeChild results in Orphaned nodes according to sIEve, discardElement does not.
2799
safeRemoveChild: function (element) {
2800
var parentNode = element.parentNode;
2802
parentNode.removeChild(element);
2807
* Destroy the element and element wrapper
2809
destroy: function () {
2811
element = wrapper.element || {},
2812
shadows = wrapper.shadows,
2818
element.onclick = element.onmouseout = element.onmouseover = element.onmousemove = null;
2819
stop(wrapper); // stop running animations
2821
if (wrapper.clipPath) {
2822
wrapper.clipPath = wrapper.clipPath.destroy();
2825
// Destroy stops in case this is a gradient object
2826
if (wrapper.stops) {
2827
for (i = 0; i < wrapper.stops.length; i++) {
2828
wrapper.stops[i] = wrapper.stops[i].destroy();
2830
wrapper.stops = null;
2834
wrapper.safeRemoveChild(element);
2838
each(shadows, function (shadow) {
2839
wrapper.safeRemoveChild(shadow);
2843
// destroy label box
2848
// remove from alignObjects
2849
erase(wrapper.renderer.alignedObjects, wrapper);
2851
for (key in wrapper) {
2852
delete wrapper[key];
2859
* Empty a group element
2861
empty: function () {
2862
var element = this.element,
2863
childNodes = element.childNodes,
2864
i = childNodes.length;
2867
element.removeChild(childNodes[i]);
2872
* Add a shadow to the element. Must be done after the element is added to the DOM
2873
* @param {Boolean} apply
2875
shadow: function (apply, group) {
2879
element = this.element,
2881
// compensate for inverted plot area
2882
transform = this.parentInverted ? '(-1,-1)' : '(1,1)';
2886
for (i = 1; i <= 3; i++) {
2887
shadow = element.cloneNode(0);
2890
'stroke': 'rgb(0, 0, 0)',
2891
'stroke-opacity': 0.05 * i,
2892
'stroke-width': 7 - 2 * i,
2893
'transform': 'translate' + transform,
2898
group.element.appendChild(shadow);
2900
element.parentNode.insertBefore(shadow, element);
2903
shadows.push(shadow);
2906
this.shadows = shadows;
2915
* The default SVG renderer
2917
var SVGRenderer = function () {
2918
this.init.apply(this, arguments);
2920
SVGRenderer.prototype = {
2921
Element: SVGElement,
2924
* Initialize the SVGRenderer
2925
* @param {Object} container
2926
* @param {Number} width
2927
* @param {Number} height
2928
* @param {Boolean} forExport
2930
init: function (container, width, height, forExport) {
2931
var renderer = this,
2935
boxWrapper = renderer.createElement('svg')
2940
container.appendChild(boxWrapper.element);
2942
// object properties
2943
renderer.isSVG = true;
2944
renderer.box = boxWrapper.element;
2945
renderer.boxWrapper = boxWrapper;
2946
renderer.alignedObjects = [];
2947
renderer.url = isIE ? '' : loc.href.replace(/#.*?$/, '')
2948
.replace(/([\('\)])/g, '\\$1'); // Page url used for internal references. #24, #672.
2949
renderer.defs = this.createElement('defs').add();
2950
renderer.forExport = forExport;
2951
renderer.gradients = {}; // Object where gradient SvgElements are stored
2953
renderer.setSize(width, height, false);
2957
* Destroys the renderer and its allocated members.
2959
destroy: function () {
2960
var renderer = this,
2961
rendererDefs = renderer.defs;
2962
renderer.box = null;
2963
renderer.boxWrapper = renderer.boxWrapper.destroy();
2965
// Call destroy on all gradient elements
2966
destroyObjectProperties(renderer.gradients || {});
2967
renderer.gradients = null;
2969
// Defs are null in VMLRenderer
2970
// Otherwise, destroy them here.
2972
renderer.defs = rendererDefs.destroy();
2975
renderer.alignedObjects = null;
2981
* Create a wrapper for an SVG element
2982
* @param {Object} nodeName
2984
createElement: function (nodeName) {
2985
var wrapper = new this.Element();
2986
wrapper.init(this, nodeName);
2991
* Dummy function for use in canvas renderer
2993
draw: function () {},
2996
* Parse a simple HTML string into SVG tspans
2998
* @param {Object} textNode The parent text SVG node
3000
buildText: function (wrapper) {
3001
var textNode = wrapper.element,
3002
lines = pick(wrapper.textStr, '').toString()
3003
.replace(/<(b|strong)>/g, '<span style="font-weight:bold">')
3004
.replace(/<(i|em)>/g, '<span style="font-style:italic">')
3005
.replace(/<a/g, '<span')
3006
.replace(/<\/(b|strong|i|em|a)>/g, '</span>')
3008
childNodes = textNode.childNodes,
3009
styleRegex = /style="([^"]+)"/,
3010
hrefRegex = /href="([^"]+)"/,
3011
parentX = attr(textNode, 'x'),
3012
textStyles = wrapper.styles,
3013
width = textStyles && pInt(textStyles.width),
3014
textLineHeight = textStyles && textStyles.lineHeight,
3016
GET_COMPUTED_STYLE = 'getComputedStyle',
3017
i = childNodes.length;
3021
textNode.removeChild(childNodes[i]);
3024
if (width && !wrapper.added) {
3025
this.box.appendChild(textNode); // attach it to the DOM to read offset width
3028
// remove empty line at end
3029
if (lines[lines.length - 1] === '') {
3034
each(lines, function (line, lineNo) {
3035
var spans, spanNo = 0, lineHeight;
3037
line = line.replace(/<span/g, '|||<span').replace(/<\/span>/g, '</span>|||');
3038
spans = line.split('|||');
3040
each(spans, function (span) {
3041
if (span !== '' || spans.length === 1) {
3042
var attributes = {},
3043
tspan = doc.createElementNS(SVG_NS, 'tspan');
3044
if (styleRegex.test(span)) {
3048
span.match(styleRegex)[1].replace(/(;| |^)color([ :])/, '$1fill$2')
3051
if (hrefRegex.test(span)) {
3052
attr(tspan, 'onclick', 'location.href=\"' + span.match(hrefRegex)[1] + '\"');
3053
css(tspan, { cursor: 'pointer' });
3056
span = (span.replace(/<(.|\n)*?>/g, '') || ' ')
3057
.replace(/</g, '<')
3058
.replace(/>/g, '>');
3060
// issue #38 workaround.
3065
arr.push(span.charAt(i));
3067
span = arr.join('');
3070
// add the text node
3071
tspan.appendChild(doc.createTextNode(span));
3073
if (!spanNo) { // first span in a line, align it to the left
3074
attributes.x = parentX;
3076
// Firefox ignores spaces at the front or end of the tspan
3077
attributes.dx = 3; // space
3080
// first span on subsequent line, add the line height
3084
// allow getting the right offset height in exporting in IE
3085
if (!hasSVG && wrapper.renderer.forExport) {
3086
css(tspan, { display: 'block' });
3089
// Webkit and opera sometimes return 'normal' as the line height. In that
3090
// case, webkit uses offsetHeight, while Opera falls back to 18
3091
lineHeight = win[GET_COMPUTED_STYLE] &&
3092
pInt(win[GET_COMPUTED_STYLE](lastLine, null).getPropertyValue('line-height'));
3094
if (!lineHeight || isNaN(lineHeight)) {
3095
lineHeight = textLineHeight || lastLine.offsetHeight || 18;
3097
attr(tspan, 'dy', lineHeight);
3099
lastLine = tspan; // record for use in next line
3103
attr(tspan, attributes);
3106
textNode.appendChild(tspan);
3110
// check width and apply soft breaks
3112
var words = span.replace(/-/g, '- ').split(' '),
3117
while (words.length || rest.length) {
3118
actualWidth = wrapper.getBBox().width;
3119
tooLong = actualWidth > width;
3120
if (!tooLong || words.length === 1) { // new line needed
3124
tspan = doc.createElementNS(SVG_NS, 'tspan');
3126
dy: textLineHeight || 16,
3129
textNode.appendChild(tspan);
3131
if (actualWidth > width) { // a single word is pressing it out
3132
width = actualWidth;
3135
} else { // append to existing line tspan
3136
tspan.removeChild(tspan.firstChild);
3137
rest.unshift(words.pop());
3140
tspan.appendChild(doc.createTextNode(words.join(' ').replace(/- /g, '-')));
3150
* Create a button with preset states
3151
* @param {String} text
3154
* @param {Function} callback
3155
* @param {Object} normalState
3156
* @param {Object} hoverState
3157
* @param {Object} pressedState
3159
button: function (text, x, y, callback, normalState, hoverState, pressedState) {
3160
var label = this.label(text, x, y),
3168
verticalGradient = { x1: 0, y1: 0, x2: 0, y2: 1 };
3170
// prepare the attributes
3171
/*jslint white: true*/
3172
normalState = merge(hash(
3176
LINEAR_GRADIENT, verticalGradient,
3188
/*jslint white: false*/
3189
normalStyle = normalState[STYLE];
3190
delete normalState[STYLE];
3192
/*jslint white: true*/
3193
hoverState = merge(normalState, hash(
3196
LINEAR_GRADIENT, verticalGradient,
3203
/*jslint white: false*/
3204
hoverStyle = hoverState[STYLE];
3205
delete hoverState[STYLE];
3207
/*jslint white: true*/
3208
pressedState = merge(normalState, hash(
3211
LINEAR_GRADIENT, verticalGradient,
3218
/*jslint white: false*/
3219
pressedStyle = pressedState[STYLE];
3220
delete pressedState[STYLE];
3223
addEvent(label.element, 'mouseenter', function () {
3224
label.attr(hoverState)
3227
addEvent(label.element, 'mouseleave', function () {
3228
stateOptions = [normalState, hoverState, pressedState][curState];
3229
stateStyle = [normalStyle, hoverStyle, pressedStyle][curState];
3230
label.attr(stateOptions)
3234
label.setState = function (state) {
3237
label.attr(normalState)
3239
} else if (state === 2) {
3240
label.attr(pressedState)
3246
.on('click', function () {
3247
callback.call(label);
3250
.css(extend({ cursor: 'default' }, normalStyle));
3254
* Make a straight line crisper by not spilling out to neighbour pixels
3255
* @param {Array} points
3256
* @param {Number} width
3258
crispLine: function (points, width) {
3259
// points format: [M, 0, 0, L, 100, 0]
3260
// normalize to a crisp line
3261
if (points[1] === points[4]) {
3262
points[1] = points[4] = mathRound(points[1]) + (width % 2 / 2);
3264
if (points[2] === points[5]) {
3265
points[2] = points[5] = mathRound(points[2]) + (width % 2 / 2);
3273
* @param {Array} path An SVG path in array form
3275
path: function (path) {
3276
return this.createElement('path').attr({
3283
* Draw and return an SVG circle
3284
* @param {Number} x The x position
3285
* @param {Number} y The y position
3286
* @param {Number} r The radius
3288
circle: function (x, y, r) {
3289
var attr = isObject(x) ?
3297
return this.createElement('circle').attr(attr);
3301
* Draw and return an arc
3302
* @param {Number} x X position
3303
* @param {Number} y Y position
3304
* @param {Number} r Radius
3305
* @param {Number} innerR Inner radius like used in donut charts
3306
* @param {Number} start Starting angle
3307
* @param {Number} end Ending angle
3309
arc: function (x, y, r, innerR, start, end) {
3310
// arcs are defined as symbols for the ability to set
3311
// attributes in attr and animate
3321
return this.symbol('arc', x || 0, y || 0, r || 0, r || 0, {
3322
innerR: innerR || 0,
3329
* Draw and return a rectangle
3330
* @param {Number} x Left position
3331
* @param {Number} y Top position
3332
* @param {Number} width
3333
* @param {Number} height
3334
* @param {Number} r Border corner radius
3335
* @param {Number} strokeWidth A stroke width can be supplied to allow crisp drawing
3337
rect: function (x, y, width, height, r, strokeWidth) {
3343
strokeWidth = x.strokeWidth;
3346
var wrapper = this.createElement('rect').attr({
3352
return wrapper.attr(wrapper.crisp(strokeWidth, x, y, mathMax(width, 0), mathMax(height, 0)));
3356
* Resize the box and re-align all aligned elements
3357
* @param {Object} width
3358
* @param {Object} height
3359
* @param {Boolean} animate
3362
setSize: function (width, height, animate) {
3363
var renderer = this,
3364
alignedObjects = renderer.alignedObjects,
3365
i = alignedObjects.length;
3367
renderer.width = width;
3368
renderer.height = height;
3370
renderer.boxWrapper[pick(animate, true) ? 'animate' : 'attr']({
3376
alignedObjects[i].align();
3382
* @param {String} name The group will be given a class name of 'highcharts-{name}'.
3383
* This can be used for styling and scripting.
3385
g: function (name) {
3386
var elem = this.createElement('g');
3387
return defined(name) ? elem.attr({ 'class': PREFIX + name }) : elem;
3392
* @param {String} src
3395
* @param {Number} width
3396
* @param {Number} height
3398
image: function (src, x, y, width, height) {
3400
preserveAspectRatio: NONE
3404
// optional properties
3405
if (arguments.length > 1) {
3414
elemWrapper = this.createElement('image').attr(attribs);
3416
// set the href in the xlink namespace
3417
if (elemWrapper.element.setAttributeNS) {
3418
elemWrapper.element.setAttributeNS('http://www.w3.org/1999/xlink',
3421
// could be exporting in IE
3422
// using href throws "not supported" in ie7 and under, requries regex shim to fix later
3423
elemWrapper.element.setAttribute('hc-svg-href', src);
3430
* Draw a symbol out of pre-defined shape paths from the namespace 'symbol' object.
3432
* @param {Object} symbol
3435
* @param {Object} radius
3436
* @param {Object} options
3438
symbol: function (symbol, x, y, width, height, options) {
3442
// get the symbol definition function
3443
symbolFn = this.symbols[symbol],
3445
// check if there's a path defined for this symbol
3446
path = symbolFn && symbolFn(
3454
imageRegex = /^url\((.*?)\)$/,
3460
obj = this.path(path);
3461
// expando properties for use in animate and attr
3470
extend(obj, options);
3475
} else if (imageRegex.test(symbol)) {
3477
var centerImage = function (img, size) {
3482
-mathRound(size[0] / 2),
3483
-mathRound(size[1] / 2)
3487
imageSrc = symbol.match(imageRegex)[1];
3488
imageSize = symbolSizes[imageSrc];
3490
// create the image synchronously, add attribs async
3491
obj = this.image(imageSrc)
3498
centerImage(obj, imageSize);
3500
// initialize image to be 0 size so export will still function if there's no cached sizes
3501
obj.attr({ width: 0, height: 0 });
3503
// create a dummy JavaScript image to get the width and height
3504
createElement('img', {
3505
onload: function () {
3508
centerImage(obj, symbolSizes[imageSrc] = [img.width, img.height]);
3519
* An extendable collection of functions for defining symbol paths.
3522
'circle': function (x, y, w, h) {
3523
var cpw = 0.166 * w;
3526
'C', x + w + cpw, y, x + w + cpw, y + h, x + w / 2, y + h,
3527
'C', x - cpw, y + h, x - cpw, y, x + w / 2, y,
3532
'square': function (x, y, w, h) {
3542
'triangle': function (x, y, w, h) {
3551
'triangle-down': function (x, y, w, h) {
3559
'diamond': function (x, y, w, h) {
3562
L, x + w, y + h / 2,
3568
'arc': function (x, y, w, h, options) {
3569
var start = options.start,
3570
radius = options.r || w || h,
3571
end = options.end - 0.000001, // to prevent cos and sin of start and end from becoming equal on 360 arcs
3572
innerRadius = options.innerR,
3573
cosStart = mathCos(start),
3574
sinStart = mathSin(start),
3575
cosEnd = mathCos(end),
3576
sinEnd = mathSin(end),
3577
longArc = options.end - start < mathPI ? 0 : 1;
3581
x + radius * cosStart,
3582
y + radius * sinStart,
3587
longArc, // long or short arc
3589
x + radius * cosEnd,
3590
y + radius * sinEnd,
3592
x + innerRadius * cosEnd,
3593
y + innerRadius * sinEnd,
3595
innerRadius, // x radius
3596
innerRadius, // y radius
3598
longArc, // long or short arc
3600
x + innerRadius * cosStart,
3601
y + innerRadius * sinStart,
3609
* Define a clipping rectangle
3610
* @param {String} id
3613
* @param {Number} width
3614
* @param {Number} height
3616
clipRect: function (x, y, width, height) {
3618
id = PREFIX + idCounter++,
3620
clipPath = this.createElement('clipPath').attr({
3624
wrapper = this.rect(x, y, width, height, 0).add(clipPath);
3626
wrapper.clipPath = clipPath;
3633
* Take a color and return it if it's a string, make it a gradient if it's a
3634
* gradient configuration object. Prior to Highstock, an array was used to define
3635
* a linear gradient with pixel positions relative to the SVG. In newer versions
3636
* we change the coordinates to apply relative to the shape, using coordinates
3637
* 0-1 within the shape. To preserve backwards compatibility, linearGradient
3638
* in this definition is an object of x1, y1, x2 and y2.
3640
* @param {Object} color The color or config object
3642
color: function (color, elem, prop) {
3644
regexRgba = /^rgba/;
3645
if (color && color.linearGradient) {
3646
var renderer = this,
3647
linearGradient = color[LINEAR_GRADIENT],
3648
relativeToShape = !isArray(linearGradient), // keep backwards compatibility
3650
gradients = renderer.gradients,
3652
x1 = linearGradient.x1 || linearGradient[0] || 0,
3653
y1 = linearGradient.y1 || linearGradient[1] || 0,
3654
x2 = linearGradient.x2 || linearGradient[2] || 0,
3655
y2 = linearGradient.y2 || linearGradient[3] || 0,
3658
// Create a unique key in order to reuse gradient objects. #671.
3659
key = [relativeToShape, x1, y1, x2, y2, color.stops.join(',')].join(',');
3661
// If the gradient with the same setup is already created, reuse it
3662
if (gradients[key]) {
3663
id = attr(gradients[key].element, 'id');
3665
// If not, create a new one and keep the reference.
3667
id = PREFIX + idCounter++;
3668
gradientObject = renderer.createElement(LINEAR_GRADIENT)
3675
}, relativeToShape ? null : { gradientUnits: 'userSpaceOnUse' }))
3676
.add(renderer.defs);
3678
// The gradient needs to keep a list of stops to be able to destroy them
3679
gradientObject.stops = [];
3680
each(color.stops, function (stop) {
3682
if (regexRgba.test(stop[1])) {
3683
colorObject = Color(stop[1]);
3684
stopColor = colorObject.get('rgb');
3685
stopOpacity = colorObject.get('a');
3687
stopColor = stop[1];
3690
stopObject = renderer.createElement('stop').attr({
3692
'stop-color': stopColor,
3693
'stop-opacity': stopOpacity
3694
}).add(gradientObject);
3696
// Add the stop element to the gradient
3697
gradientObject.stops.push(stopObject);
3700
// Keep a reference to the gradient object so it is possible to reuse it and
3702
gradients[key] = gradientObject;
3705
return 'url(' + this.url + '#' + id + ')';
3707
// Webkit and Batik can't show rgba.
3708
} else if (regexRgba.test(color)) {
3709
colorObject = Color(color);
3710
attr(elem, prop + '-opacity', colorObject.get('a'));
3712
return colorObject.get('rgb');
3716
// Remove the opacity attribute added above. Does not throw if the attribute is not there.
3717
elem.removeAttribute(prop + '-opacity');
3726
* Add text to the SVG object
3727
* @param {String} str
3728
* @param {Number} x Left position
3729
* @param {Number} y Top position
3730
* @param {Boolean} useHTML Use HTML to render the text
3732
text: function (str, x, y, useHTML) {
3734
// declare variables
3735
var renderer = this,
3736
defaultChartStyle = defaultOptions.chart.style,
3739
if (useHTML && !renderer.forExport) {
3740
return renderer.html(str, x, y);
3743
x = mathRound(pick(x, 0));
3744
y = mathRound(pick(y, 0));
3746
wrapper = renderer.createElement('text')
3753
fontFamily: defaultChartStyle.fontFamily,
3754
fontSize: defaultChartStyle.fontSize
3764
* Create HTML text node. This is used by the VML renderer as well as the SVG
3765
* renderer through the useHTML option.
3767
* @param {String} str
3771
html: function (str, x, y) {
3772
var defaultChartStyle = defaultOptions.chart.style,
3773
wrapper = this.createElement('span'),
3774
attrSetters = wrapper.attrSetters,
3775
element = wrapper.element,
3776
renderer = wrapper.renderer;
3779
attrSetters.text = function (value) {
3780
element.innerHTML = value;
3784
// Various setters which rely on update transform
3785
attrSetters.x = attrSetters.y = attrSetters.align = function (value, key) {
3786
if (key === 'align') {
3787
key = 'textAlign'; // Do not overwrite the SVGElement.align method. Same as VML.
3789
wrapper[key] = value;
3790
wrapper.htmlUpdateTransform();
3794
// Set the default attributes
3802
whiteSpace: 'nowrap',
3803
fontFamily: defaultChartStyle.fontFamily,
3804
fontSize: defaultChartStyle.fontSize
3807
// Use the HTML specific .css method
3808
wrapper.css = wrapper.htmlCss;
3810
// This is specific for HTML within SVG
3811
if (renderer.isSVG) {
3812
wrapper.add = function (svgGroupWrapper) {
3816
container = renderer.box.parentNode;
3818
// Create a mock group to hold the HTML elements
3819
if (svgGroupWrapper) {
3820
htmlGroup = svgGroupWrapper.div;
3822
htmlGroup = svgGroupWrapper.div = createElement(DIV, {
3823
className: attr(svgGroupWrapper.element, 'class')
3826
left: svgGroupWrapper.attr('translateX') + PX,
3827
top: svgGroupWrapper.attr('translateY') + PX
3830
// Ensure dynamic updating position
3831
htmlGroupStyle = htmlGroup.style;
3832
extend(svgGroupWrapper.attrSetters, {
3833
translateX: function (value) {
3834
htmlGroupStyle.left = value + PX;
3836
translateY: function (value) {
3837
htmlGroupStyle.top = value + PX;
3839
visibility: function (value, key) {
3840
htmlGroupStyle[key] = value;
3846
htmlGroup = container;
3849
htmlGroup.appendChild(element);
3852
wrapper.added = true;
3853
if (wrapper.alignOnAdd) {
3854
wrapper.htmlUpdateTransform();
3864
* Utility to return the baseline offset and total line height from the font size
3866
fontMetrics: function (fontSize) {
3867
fontSize = pInt(fontSize || 11);
3869
// Empirical values found by comparing font size and bounding box height.
3870
// Applies to the default font family. http://jsfiddle.net/highcharts/7xvn7/
3871
var lineHeight = fontSize < 24 ? fontSize + 4 : mathRound(fontSize * 1.2),
3872
baseline = mathRound(lineHeight * 0.8);
3881
* Add a label, a text item that can hold a colored or gradient background
3882
* as well as a border and shadow.
3883
* @param {string} str
3886
* @param {String} shape
3887
* @param {Number} anchorX In case the shape has a pointer, like a flag, this is the
3888
* coordinates it should be pinned to
3889
* @param {Number} anchorY
3890
* @param {Boolean} baseline Whether to position the label relative to the text baseline,
3891
* like renderer.text, or to the upper border of the rectangle.
3893
label: function (str, x, y, shape, anchorX, anchorY, useHTML, baseline) {
3895
var renderer = this,
3896
wrapper = renderer.g(),
3897
text = renderer.text('', 0, 0, useHTML)
3913
attrSetters = wrapper.attrSetters;
3916
* This function runs after the label is added to the DOM (when the bounding box is
3917
* available), and after the text of the label is updated to detect the new bounding
3918
* box and reflect it in the border box.
3920
function updateBoxSize() {
3922
style = text.element.style;
3924
bBox = (width === undefined || height === undefined || wrapper.styles.textAlign) &&
3926
wrapper.width = (width || bBox.width) + 2 * padding;
3927
wrapper.height = (height || bBox.height) + 2 * padding;
3929
// update the label-scoped y offset
3930
baselineOffset = padding + renderer.fontMetrics(style && style.fontSize).b;
3933
// create the border box if it is not already present
3935
boxY = baseline ? -baselineOffset : 0;
3937
wrapper.box = box = shape ?
3938
renderer.symbol(shape, 0, boxY, wrapper.width, wrapper.height) :
3939
renderer.rect(0, boxY, wrapper.width, wrapper.height, 0, deferredAttr[STROKE_WIDTH]);
3943
// apply the box attributes
3945
width: wrapper.width,
3946
height: wrapper.height
3948
deferredAttr = null;
3952
* This function runs after setting text or padding, but only if padding is changed
3954
function updateTextPadding() {
3955
var styles = wrapper.styles,
3956
textAlign = styles && styles.textAlign,
3960
// determin y based on the baseline
3961
y = baseline ? 0 : baselineOffset;
3963
// compensate for alignment
3964
if (defined(width) && (textAlign === 'center' || textAlign === 'right')) {
3965
x += { center: 0.5, right: 1 }[textAlign] * (width - bBox.width);
3968
// update if anything changed
3969
if (x !== text.x || y !== text.y) {
3976
// record current values
3982
* Set a box attribute, or defer it if the box is not yet created
3983
* @param {Object} key
3984
* @param {Object} value
3986
function boxAttr(key, value) {
3988
box.attr(key, value);
3990
deferredAttr[key] = value;
3994
function getSizeAfterAdd() {
3996
text: str, // alignment is available now
4005
* After the text element is added, get the desired size of the border box
4006
* and add it before the text in the DOM.
4008
addEvent(wrapper, 'add', getSizeAfterAdd);
4011
* Add specific attribute setters.
4014
// only change local variables
4015
attrSetters.width = function (value) {
4019
attrSetters.height = function (value) {
4023
attrSetters.padding = function (value) {
4024
if (defined(value) && value !== padding) {
4026
updateTextPadding();
4032
// change local variable and set attribue as well
4033
attrSetters.align = function (value) {
4035
return false; // prevent setting text-anchor on the group
4038
// apply these to the box and the text alike
4039
attrSetters.text = function (value, key) {
4040
text.attr(key, value);
4042
updateTextPadding();
4046
// apply these to the box but not to the text
4047
attrSetters[STROKE_WIDTH] = function (value, key) {
4048
crispAdjust = value % 2 / 2;
4049
boxAttr(key, value);
4052
attrSetters.stroke = attrSetters.fill = attrSetters.r = function (value, key) {
4053
boxAttr(key, value);
4056
attrSetters.anchorX = function (value, key) {
4058
boxAttr(key, value + crispAdjust - wrapperX);
4061
attrSetters.anchorY = function (value, key) {
4063
boxAttr(key, value - wrapperY);
4067
// rename attributes
4068
attrSetters.x = function (value) {
4069
value -= { left: 0, center: 0.5, right: 1 }[align] * ((width || bBox.width) + padding);
4070
wrapperX = wrapper.x = mathRound(value); // wrapper.x is for animation getter
4072
wrapper.attr('translateX', wrapperX);
4075
attrSetters.y = function (value) {
4076
wrapperY = wrapper.y = mathRound(value);
4077
wrapper.attr('translateY', value);
4081
// Redirect certain methods to either the box or the text
4082
var baseCss = wrapper.css;
4083
return extend(wrapper, {
4085
* Pick up some properties and apply them to the text instead of the wrapper
4087
css: function (styles) {
4089
var textStyles = {};
4090
styles = merge({}, styles); // create a copy to avoid altering the original object (#537)
4091
each(['fontSize', 'fontWeight', 'fontFamily', 'color', 'lineHeight', 'width'], function (prop) {
4092
if (styles[prop] !== UNDEFINED) {
4093
textStyles[prop] = styles[prop];
4094
delete styles[prop];
4097
text.css(textStyles);
4099
return baseCss.call(wrapper, styles);
4102
* Return the bounding box of the box, not the group
4104
getBBox: function () {
4105
return box.getBBox();
4108
* Apply the shadow to the box
4110
shadow: function (b) {
4115
* Destroy and release memory.
4117
destroy: function () {
4118
removeEvent(wrapper, 'add', getSizeAfterAdd);
4120
// Added by button implementation
4121
removeEvent(wrapper.element, 'mouseenter');
4122
removeEvent(wrapper.element, 'mouseleave');
4125
// Destroy the text element
4126
text = text.destroy();
4128
// Call base implementation to destroy the rest
4129
SVGElement.prototype.destroy.call(wrapper);
4133
}; // end SVGRenderer
4137
Renderer = SVGRenderer;
4140
/* ****************************************************************************
4142
* START OF INTERNET EXPLORER <= 8 SPECIFIC CODE *
4144
* For applications and websites that don't need IE support, like platform *
4145
* targeted mobile apps and web apps, this code can be removed. *
4147
*****************************************************************************/
4153
if (!hasSVG && !useCanVG) {
4156
* The VML element wrapper.
4161
* Initialize a new VML element wrapper. It builds the markup as a string
4162
* to minimize DOM traffic.
4163
* @param {Object} renderer
4164
* @param {Object} nodeName
4166
init: function (renderer, nodeName) {
4168
markup = ['<', nodeName, ' filled="f" stroked="f"'],
4169
style = ['position: ', ABSOLUTE, ';'];
4171
// divs and shapes need size
4172
if (nodeName === 'shape' || nodeName === DIV) {
4173
style.push('left:0;top:0;width:10px;height:10px;');
4176
style.push('visibility: ', nodeName === DIV ? HIDDEN : VISIBLE);
4179
markup.push(' style="', style.join(''), '"/>');
4181
// create element with default attributes and style
4183
markup = nodeName === DIV || nodeName === 'span' || nodeName === 'img' ?
4185
: renderer.prepVML(markup);
4186
wrapper.element = createElement(markup);
4189
wrapper.renderer = renderer;
4190
wrapper.attrSetters = {};
4194
* Add the node to the given parent
4195
* @param {Object} parent
4197
add: function (parent) {
4199
renderer = wrapper.renderer,
4200
element = wrapper.element,
4202
inverted = parent && parent.inverted,
4204
// get the parent node
4205
parentNode = parent ?
4206
parent.element || parent :
4210
// if the parent group is inverted, apply inversion on all children
4211
if (inverted) { // only on groups
4212
renderer.invertChild(element, parentNode);
4215
// issue #140 workaround - related to #61 and #74
4216
if (docMode8 && parentNode.gVis === HIDDEN) {
4217
css(element, { visibility: HIDDEN });
4221
parentNode.appendChild(element);
4223
// align text after adding to be able to read offset
4224
wrapper.added = true;
4225
if (wrapper.alignOnAdd && !wrapper.deferUpdateTransform) {
4226
wrapper.updateTransform();
4229
// fire an event for internal hooks
4230
fireEvent(wrapper, 'add');
4236
* In IE8 documentMode 8, we need to recursively set the visibility down in the DOM
4237
* tree for nested groups. Related to #61, #586.
4239
toggleChildren: function (element, visibility) {
4240
var childNodes = element.childNodes,
4241
i = childNodes.length;
4245
// apply the visibility
4246
css(childNodes[i], { visibility: visibility });
4248
// we have a nested group, apply it to its children again
4249
if (childNodes[i].nodeName === 'DIV') {
4250
this.toggleChildren(childNodes[i], visibility);
4256
* VML always uses htmlUpdateTransform
4258
updateTransform: SVGElement.prototype.htmlUpdateTransform,
4261
* Get or set attributes
4263
attr: function (hash, val) {
4269
element = wrapper.element || {},
4270
elemStyle = element.style,
4271
nodeName = element.nodeName,
4272
renderer = wrapper.renderer,
4273
symbolName = wrapper.symbolName,
4275
shadows = wrapper.shadows,
4277
attrSetters = wrapper.attrSetters,
4280
// single key-value pair
4281
if (isString(hash) && defined(val)) {
4287
// used as a getter, val is undefined
4288
if (isString(hash)) {
4290
if (key === 'strokeWidth' || key === 'stroke-width') {
4291
ret = wrapper.strokeweight;
4302
// check for a specific attribute setter
4303
result = attrSetters[key] && attrSetters[key](value, key);
4305
if (result !== false && value !== null) { // #620
4307
if (result !== UNDEFINED) {
4308
value = result; // the attribute setter has returned a new value to set
4314
if (symbolName && /^(x|y|r|start|end|width|height|innerR|anchorX|anchorY)/.test(key)) {
4315
// if one of the symbol size affecting parameters are changed,
4316
// check all the others only once for each call to an element's
4318
if (!hasSetSymbolSize) {
4319
wrapper.symbolAttr(hash);
4321
hasSetSymbolSize = true;
4325
} else if (key === 'd') {
4326
value = value || [];
4327
wrapper.d = value.join(' '); // used in getter for animation
4331
var convertedPath = [];
4334
// Multiply by 10 to allow subpixel precision.
4335
// Substracting half a pixel seems to make the coordinates
4336
// align with SVG, but this hasn't been tested thoroughly
4337
if (isNumber(value[i])) {
4338
convertedPath[i] = mathRound(value[i] * 10) - 5;
4339
} else if (value[i] === 'Z') { // close the path
4340
convertedPath[i] = 'x';
4342
convertedPath[i] = value[i];
4346
value = convertedPath.join(' ') || 'x';
4347
element.path = value;
4353
shadows[i].path = value;
4358
// directly mapped to css
4359
} else if (key === 'zIndex' || key === 'visibility') {
4361
// workaround for #61 and #586
4362
if (docMode8 && key === 'visibility' && nodeName === 'DIV') {
4363
element.gVis = value;
4364
wrapper.toggleChildren(element, value);
4365
if (value === VISIBLE) { // #74
4371
elemStyle[key] = value;
4379
} else if (key === 'width' || key === 'height') {
4381
value = mathMax(0, value); // don't set width or height below zero (#311)
4383
this[key] = value; // used in getter
4385
// clipping rectangle special
4386
if (wrapper.updateClipping) {
4387
wrapper[key] = value;
4388
wrapper.updateClipping();
4391
elemStyle[key] = value;
4397
} else if (key === 'x' || key === 'y') {
4399
wrapper[key] = value; // used in getter
4400
elemStyle[{ x: 'left', y: 'top' }[key]] = value;
4403
} else if (key === 'class') {
4404
// IE8 Standards mode has problems retrieving the className
4405
element.className = value;
4408
} else if (key === 'stroke') {
4410
value = renderer.color(value, element, key);
4412
key = 'strokecolor';
4415
} else if (key === 'stroke-width' || key === 'strokeWidth') {
4416
element.stroked = value ? true : false;
4417
key = 'strokeweight';
4418
wrapper[key] = value; // used in getter, issue #113
4419
if (isNumber(value)) {
4424
} else if (key === 'dashstyle') {
4425
var strokeElem = element.getElementsByTagName('stroke')[0] ||
4426
createElement(renderer.prepVML(['<stroke/>']), null, null, element);
4427
strokeElem[key] = value || 'solid';
4428
wrapper.dashstyle = value; /* because changing stroke-width will change the dash length
4429
and cause an epileptic effect */
4433
} else if (key === 'fill') {
4435
if (nodeName === 'SPAN') { // text color
4436
elemStyle.color = value;
4438
element.filled = value !== NONE ? true : false;
4440
value = renderer.color(value, element, key);
4445
// translation for animation
4446
} else if (key === 'translateX' || key === 'translateY' || key === 'rotation') {
4447
wrapper[key] = value;
4448
wrapper.updateTransform();
4452
// text for rotated and non-rotated elements
4453
} else if (key === 'text') {
4455
element.innerHTML = value;
4459
// let the shadow follow the main element
4460
if (shadows && key === 'visibility') {
4463
shadows[i].style[key] = value;
4470
if (docMode8) { // IE8 setAttribute bug
4471
element[key] = value;
4473
attr(element, key, value);
4484
* Set the element's clipping to a predefined rectangle
4486
* @param {String} id The id of the clip rectangle
4488
clip: function (clipRect) {
4490
clipMembers = clipRect.members;
4492
clipMembers.push(wrapper);
4493
wrapper.destroyClip = function () {
4494
erase(clipMembers, wrapper);
4496
return wrapper.css(clipRect.getCSS(wrapper.inverted));
4500
* Set styles for the element
4501
* @param {Object} styles
4503
css: SVGElement.prototype.htmlCss,
4506
* Removes a child either by removeChild or move to garbageBin.
4507
* Issue 490; in VML removeChild results in Orphaned nodes according to sIEve, discardElement does not.
4509
safeRemoveChild: function (element) {
4510
// discardElement will detach the node from its parent before attaching it
4511
// to the garbage bin. Therefore it is important that the node is attached and have parent.
4512
var parentNode = element.parentNode;
4514
discardElement(element);
4519
* Extend element.destroy by removing it from the clip members array
4521
destroy: function () {
4524
if (wrapper.destroyClip) {
4525
wrapper.destroyClip();
4528
return SVGElement.prototype.destroy.apply(wrapper);
4532
* Remove all child nodes of a group, except the v:group element
4534
empty: function () {
4535
var element = this.element,
4536
childNodes = element.childNodes,
4537
i = childNodes.length,
4541
node = childNodes[i];
4542
node.parentNode.removeChild(node);
4547
* Add an event listener. VML override for normalizing event parameters.
4548
* @param {String} eventType
4549
* @param {Function} handler
4551
on: function (eventType, handler) {
4552
// simplest possible event model for internal use
4553
this.element['on' + eventType] = function () {
4554
var evt = win.event;
4555
evt.target = evt.srcElement;
4562
* Apply a drop shadow by copying elements and giving them different strokes
4563
* @param {Boolean} apply
4565
shadow: function (apply, group) {
4568
element = this.element,
4569
renderer = this.renderer,
4571
elemStyle = element.style,
4573
path = element.path;
4575
// some times empty paths are not strings
4576
if (path && typeof path.value !== 'string') {
4581
for (i = 1; i <= 3; i++) {
4582
markup = ['<shape isShadow="true" strokeweight="', (7 - 2 * i),
4583
'" filled="false" path="', path,
4584
'" coordsize="100,100" style="', element.style.cssText, '" />'];
4585
shadow = createElement(renderer.prepVML(markup),
4587
left: pInt(elemStyle.left) + 1,
4588
top: pInt(elemStyle.top) + 1
4592
// apply the opacity
4593
markup = ['<stroke color="black" opacity="', (0.05 * i), '"/>'];
4594
createElement(renderer.prepVML(markup), null, null, shadow);
4599
group.element.appendChild(shadow);
4601
element.parentNode.insertBefore(shadow, element);
4605
shadows.push(shadow);
4609
this.shadows = shadows;
4615
VMLElement = extendClass(SVGElement, VMLElement);
4620
var VMLRendererExtension = { // inherit SVGRenderer
4622
Element: VMLElement,
4623
isIE8: userAgent.indexOf('MSIE 8.0') > -1,
4627
* Initialize the VMLRenderer
4628
* @param {Object} container
4629
* @param {Number} width
4630
* @param {Number} height
4632
init: function (container, width, height) {
4633
var renderer = this,
4637
renderer.alignedObjects = [];
4639
boxWrapper = renderer.createElement(DIV);
4640
box = boxWrapper.element;
4641
box.style.position = RELATIVE; // for freeform drawing using renderer directly
4642
container.appendChild(boxWrapper.element);
4645
// generate the containing box
4647
renderer.boxWrapper = boxWrapper;
4650
renderer.setSize(width, height, false);
4652
// The only way to make IE6 and IE7 print is to use a global namespace. However,
4653
// with IE8 the only way to make the dynamic shapes visible in screen and print mode
4654
// seems to be to add the xmlns attribute and the behaviour style inline.
4655
if (!doc.namespaces.hcv) {
4657
doc.namespaces.add('hcv', 'urn:schemas-microsoft-com:vml');
4659
// setup default css
4660
doc.createStyleSheet().cssText =
4661
'hcv\\:fill, hcv\\:path, hcv\\:shape, hcv\\:stroke' +
4662
'{ behavior:url(#default#VML); display: inline-block; } ';
4668
* Define a clipping rectangle. In VML it is accomplished by storing the values
4669
* for setting the CSS style to all associated members.
4673
* @param {Number} width
4674
* @param {Number} height
4676
clipRect: function (x, y, width, height) {
4678
// create a dummy element
4679
var clipRect = this.createElement();
4681
// mimic a rectangle with its style object for automatic updating in attr
4682
return extend(clipRect, {
4688
getCSS: function (inverted) {
4689
var rect = this,//clipRect.element.style,
4692
right = left + rect.width,
4693
bottom = top + rect.height,
4696
mathRound(inverted ? left : top) + 'px,' +
4697
mathRound(inverted ? bottom : right) + 'px,' +
4698
mathRound(inverted ? right : bottom) + 'px,' +
4699
mathRound(inverted ? top : left) + 'px)'
4702
// issue 74 workaround
4703
if (!inverted && docMode8) {
4712
// used in attr and animation to update the clipping of all members
4713
updateClipping: function () {
4714
each(clipRect.members, function (member) {
4715
member.css(clipRect.getCSS(member.inverted));
4724
* Take a color and return it if it's a string, make it a gradient if it's a
4725
* gradient configuration object, and apply opacity.
4727
* @param {Object} color The color or config object
4729
color: function (color, elem, prop) {
4731
regexRgba = /^rgba/,
4734
if (color && color[LINEAR_GRADIENT]) {
4738
linearGradient = color[LINEAR_GRADIENT],
4739
x1 = linearGradient.x1 || linearGradient[0] || 0,
4740
y1 = linearGradient.y1 || linearGradient[1] || 0,
4741
x2 = linearGradient.x2 || linearGradient[2] || 0,
4742
y2 = linearGradient.y2 || linearGradient[3] || 0,
4749
each(color.stops, function (stop, i) {
4750
if (regexRgba.test(stop[1])) {
4751
colorObject = Color(stop[1]);
4752
stopColor = colorObject.get('rgb');
4753
stopOpacity = colorObject.get('a');
4755
stopColor = stop[1];
4761
opacity1 = stopOpacity;
4764
opacity2 = stopOpacity;
4768
// Apply the gradient to fills only.
4769
if (prop === 'fill') {
4770
// calculate the angle based on the linear vector
4771
angle = 90 - math.atan(
4772
(y2 - y1) / // y vector
4773
(x2 - x1) // x vector
4777
// when colors attribute is used, the meanings of opacity and o:opacity2
4779
markup = ['<fill colors="0% ', color1, ',100% ', color2, '" angle="', angle,
4780
'" opacity="', opacity2, '" o:opacity2="', opacity1,
4781
'" type="gradient" focus="100%" method="sigma" />'];
4782
createElement(this.prepVML(markup), null, null, elem);
4784
// Gradients are not supported for VML stroke, return the first color. #722.
4790
// if the color is an rgba color, split it and add a fill node
4791
// to hold the opacity component
4792
} else if (regexRgba.test(color) && elem.tagName !== 'IMG') {
4794
colorObject = Color(color);
4796
markup = ['<', prop, ' opacity="', colorObject.get('a'), '"/>'];
4797
createElement(this.prepVML(markup), null, null, elem);
4799
return colorObject.get('rgb');
4803
var strokeNodes = elem.getElementsByTagName(prop);
4804
if (strokeNodes.length) {
4805
strokeNodes[0].opacity = 1;
4813
* Take a VML string and prepare it for either IE8 or IE6/IE7.
4814
* @param {Array} markup A string array of the VML markup to prepare
4816
prepVML: function (markup) {
4817
var vmlStyle = 'display:inline-block;behavior:url(#default#VML);',
4820
markup = markup.join('');
4822
if (isIE8) { // add xmlns and style inline
4823
markup = markup.replace('/>', ' xmlns="urn:schemas-microsoft-com:vml" />');
4824
if (markup.indexOf('style="') === -1) {
4825
markup = markup.replace('/>', ' style="' + vmlStyle + '" />');
4827
markup = markup.replace('style="', 'style="' + vmlStyle);
4830
} else { // add namespace
4831
markup = markup.replace('<', '<hcv:');
4838
* Create rotated and aligned text
4839
* @param {String} str
4843
text: SVGRenderer.prototype.html,
4846
* Create and return a path element
4847
* @param {Array} path
4849
path: function (path) {
4851
return this.createElement('shape').attr({
4852
// subpixel precision down to 0.1 (width and height = 10px)
4853
coordsize: '100 100',
4859
* Create and return a circle element. In VML circles are implemented as
4860
* shapes, which is faster than v:oval
4865
circle: function (x, y, r) {
4866
return this.symbol('circle').attr({ x: x - r, y: y - r, width: 2 * r, height: 2 * r });
4870
* Create a group using an outer div and an inner v:group to allow rotating
4871
* and flipping. A simple v:group would have problems with positioning
4872
* child HTML elements and CSS clip.
4874
* @param {String} name The name of the group
4876
g: function (name) {
4880
// set the class name
4882
attribs = { 'className': PREFIX + name, 'class': PREFIX + name };
4885
// the div to hold HTML and clipping
4886
wrapper = this.createElement(DIV).attr(attribs);
4892
* VML override to create a regular HTML image
4893
* @param {String} src
4896
* @param {Number} width
4897
* @param {Number} height
4899
image: function (src, x, y, width, height) {
4900
var obj = this.createElement('img')
4901
.attr({ src: src });
4903
if (arguments.length > 1) {
4915
* VML uses a shape for rect to overcome bugs and rotation problems
4917
rect: function (x, y, width, height, r, strokeWidth) {
4923
strokeWidth = x.strokeWidth;
4926
var wrapper = this.symbol('rect');
4929
return wrapper.attr(wrapper.crisp(strokeWidth, x, y, mathMax(width, 0), mathMax(height, 0)));
4933
* In the VML renderer, each child of an inverted div (group) is inverted
4934
* @param {Object} element
4935
* @param {Object} parentNode
4937
invertChild: function (element, parentNode) {
4938
var parentStyle = parentNode.style;
4942
left: pInt(parentStyle.width) - 10,
4943
top: pInt(parentStyle.height) - 10,
4949
* Symbol definitions that override the parent SVG renderer's symbols
4953
// VML specific arc function
4954
arc: function (x, y, w, h, options) {
4955
var start = options.start,
4957
radius = options.r || w || h,
4958
cosStart = mathCos(start),
4959
sinStart = mathSin(start),
4960
cosEnd = mathCos(end),
4961
sinEnd = mathSin(end),
4962
innerRadius = options.innerR,
4963
circleCorrection = 0.08 / radius, // #760
4964
innerCorrection = (innerRadius && 0.25 / innerRadius) || 0;
4966
if (end - start === 0) { // no angle, don't show it.
4969
} else if (2 * mathPI - end + start < circleCorrection) { // full circle
4970
// empirical correction found by trying out the limits for different radii
4971
cosEnd = -circleCorrection;
4972
} else if (end - start < innerCorrection) { // issue #186, another mysterious VML arc problem
4973
cosEnd = mathCos(start + innerCorrection);
4977
'wa', // clockwise arc to
4980
x + radius, // right
4981
y + radius, // bottom
4982
x + radius * cosStart, // start x
4983
y + radius * sinStart, // start y
4984
x + radius * cosEnd, // end x
4985
y + radius * sinEnd, // end y
4988
'at', // anti clockwise arc to
4989
x - innerRadius, // left
4990
y - innerRadius, // top
4991
x + innerRadius, // right
4992
y + innerRadius, // bottom
4993
x + innerRadius * cosEnd, // start x
4994
y + innerRadius * sinEnd, // start y
4995
x + innerRadius * cosStart, // end x
4996
y + innerRadius * sinStart, // end y
5003
// Add circle symbol path. This performs significantly faster than v:oval.
5004
circle: function (x, y, w, h) {
5007
'wa', // clockwisearcto
5013
y + h / 2, // start y
5016
//'x', // finish path
5021
* Add rectangle symbol path which eases rotation and omits arcsize problems
5022
* compared to the built-in VML roundrect shape
5024
* @param {Number} left Left position
5025
* @param {Number} top Top position
5026
* @param {Number} r Border radius
5027
* @param {Object} options Width and height
5030
rect: function (left, top, width, height, options) {
5031
/*for (var n in r) {
5032
logTime && console .log(n)
5035
if (!defined(options)) {
5038
var right = left + width,
5039
bottom = top + height,
5040
r = mathMin(options.r || 0, width, height);
5057
right - 2 * r, bottom - 2 * r,
5065
left, bottom - 2 * r,
5066
left + 2 * r, bottom,
5074
left + 2 * r, top + 2 * r,
5086
VMLRenderer = function () {
5087
this.init.apply(this, arguments);
5089
VMLRenderer.prototype = merge(SVGRenderer.prototype, VMLRendererExtension);
5092
Renderer = VMLRenderer;
5095
/* ****************************************************************************
5097
* END OF INTERNET EXPLORER <= 8 SPECIFIC CODE *
5099
*****************************************************************************/
5100
/* ****************************************************************************
5102
* START OF ANDROID < 3 SPECIFIC CODE. THIS CAN BE REMOVED IF YOU'RE NOT *
5103
* TARGETING THAT SYSTEM. *
5105
*****************************************************************************/
5111
* The CanVGRenderer is empty from start to keep the source footprint small.
5112
* When requested, the CanVGController downloads the rest of the source packaged
5113
* together with the canvg library.
5115
CanVGRenderer = function () {
5116
// Empty constructor
5120
* Handles on demand download of canvg rendering support.
5122
CanVGController = (function () {
5123
// List of renderering calls
5124
var deferredRenderCalls = [];
5127
* When downloaded, we are ready to draw deferred charts.
5129
function drawDeferred() {
5130
var callLength = deferredRenderCalls.length,
5133
// Draw all pending render calls
5134
for (callIndex = 0; callIndex < callLength; callIndex++) {
5135
deferredRenderCalls[callIndex]();
5138
deferredRenderCalls = [];
5142
push: function (func, scriptLocation) {
5143
// Only get the script once
5144
if (deferredRenderCalls.length === 0) {
5145
getScript(scriptLocation, drawDeferred);
5147
// Register render call
5148
deferredRenderCalls.push(func);
5152
} // end CanVGRenderer
5154
/* ****************************************************************************
5156
* END OF ANDROID < 3 SPECIFIC CODE *
5158
*****************************************************************************/
5163
Renderer = VMLRenderer || CanVGRenderer || SVGRenderer;
5167
* @param {Object} options
5168
* @param {Function} callback Function to run when the chart has loaded
5170
function Chart(userOptions, callback) {
5172
// Handle regular options
5174
seriesOptions = userOptions.series; // skip merging data points to increase performance
5175
userOptions.series = null;
5176
options = merge(defaultOptions, userOptions); // do the merge
5177
options.series = userOptions.series = seriesOptions; // set back the series data
5179
var optionsChart = options.chart,
5180
optionsMargin = optionsChart.margin,
5181
margin = isObject(optionsMargin) ?
5183
[optionsMargin, optionsMargin, optionsMargin, optionsMargin],
5184
optionsMarginTop = pick(optionsChart.marginTop, margin[0]),
5185
optionsMarginRight = pick(optionsChart.marginRight, margin[1]),
5186
optionsMarginBottom = pick(optionsChart.marginBottom, margin[2]),
5187
optionsMarginLeft = pick(optionsChart.marginLeft, margin[3]),
5188
spacingTop = optionsChart.spacingTop,
5189
spacingRight = optionsChart.spacingRight,
5190
spacingBottom = optionsChart.spacingBottom,
5191
spacingLeft = optionsChart.spacingLeft,
5194
chartSubtitleOptions,
5215
chartEvents = optionsChart.events,
5216
runChartClick = chartEvents && !!chartEvents.click,
5218
isInsidePlot, // function
5232
hasCartesianSeries = optionsChart.showAxes,
5235
maxTicks, // handle the greatest amount of ticks on grouped axes
5242
drawChartBox, // function
5243
getMargins, // function
5244
resetMargins, // function
5245
setChartSize, // function
5248
zoomOut; // function
5252
* Create a new axis object
5253
* @param {Object} options
5255
function Axis(userOptions) {
5258
var isXAxis = userOptions.isX,
5259
opposite = userOptions.opposite, // needed in setOptions
5260
horiz = inverted ? !isXAxis : isXAxis,
5262
(opposite ? 0 : 2) : // top : bottom
5263
(opposite ? 1 : 3), // right : left
5267
isXAxis ? defaultXAxisOptions : defaultYAxisOptions,
5268
[defaultTopAxisOptions, defaultRightAxisOptions,
5269
defaultBottomAxisOptions, defaultLeftAxisOptions][side],
5275
type = options.type,
5276
isDatetimeAxis = type === 'datetime',
5277
isLog = type === 'logarithmic',
5278
offset = options.offset || 0,
5279
xOrY = isXAxis ? 'x' : 'y',
5282
transA, // translation factor
5283
transB, // translation addend
5284
oldTransA, // used for prerendering
5292
setAxisTranslation, // fn
5293
getPlotLinePath, // fn
5299
minRange = options.minRange || options.maxZoom,
5300
range = options.range,
5309
minPadding = options.minPadding,
5310
maxPadding = options.maxPadding,
5311
minPixelPadding = 0,
5312
isLinked = defined(options.linkedTo),
5314
ignoreMinPadding, // can be set to true by a column or bar series
5317
events = options.events,
5319
plotLinesAndBands = [],
5323
tickPositions, // array containing predefined positions
5324
tickPositioner = options.tickPositioner,
5327
alternateBands = {},
5330
axisTitleMargin,// = options.title.margin,
5331
categories = options.categories,
5332
labelFormatter = options.labels.formatter || // can be overwritten by dynamic format
5334
var value = this.value,
5335
dateTimeLabelFormat = this.dateTimeLabelFormat,
5338
if (dateTimeLabelFormat) { // datetime axis
5339
ret = dateFormat(dateTimeLabelFormat, value);
5341
} else if (tickInterval % 1000000 === 0) { // use M abbreviation
5342
ret = (value / 1000000) + 'M';
5344
} else if (tickInterval % 1000 === 0) { // use k abbreviation
5345
ret = (value / 1000) + 'k';
5347
} else if (!categories && value >= 1000) { // add thousands separators
5348
ret = numberFormat(value, 0);
5350
} else { // strings (categories) and small numbers
5356
staggerLines = horiz && options.labels.staggerLines,
5357
reversed = options.reversed,
5358
tickmarkOffset = (categories && options.tickmarkPlacement === 'between') ? 0.5 : 0;
5363
function Tick(pos, type) {
5366
tick.type = type || '';
5376
* Write the tick label
5378
addLabel: function () {
5381
labelOptions = options.labels,
5383
width = (categories && horiz && categories.length &&
5384
!labelOptions.step && !labelOptions.staggerLines &&
5385
!labelOptions.rotation &&
5386
plotWidth / categories.length) ||
5387
(!horiz && plotWidth / 2),
5388
isFirst = pos === tickPositions[0],
5389
isLast = pos === tickPositions[tickPositions.length - 1],
5391
value = categories && defined(categories[pos]) ? categories[pos] : pos,
5393
tickPositionInfo = tickPositions.info,
5394
dateTimeLabelFormat;
5396
// Set the datetime label format. If a higher rank is set for this position, use that. If not,
5397
// use the general format.
5398
if (isDatetimeAxis && tickPositionInfo) {
5399
dateTimeLabelFormat = options.dateTimeLabelFormats[tickPositionInfo.higherRanks[pos] || tickPositionInfo.unitName];
5402
// set properties for access in render method
5403
tick.isFirst = isFirst;
5404
tick.isLast = isLast;
5407
str = labelFormatter.call({
5412
dateTimeLabelFormat: dateTimeLabelFormat,
5413
value: isLog ? correctFloat(lin2log(value)) : value
5418
css = width && { width: mathMax(1, mathRound(width - 2 * (labelOptions.padding || 10))) + PX };
5419
css = extend(css, labelOptions.style);
5422
if (!defined(label)) {
5424
defined(str) && labelOptions.enabled ?
5429
labelOptions.useHTML
5432
align: labelOptions.align,
5433
rotation: labelOptions.rotation
5435
// without position absolute, IE export sometimes is wrong
5449
* Get the offset height or width of the label
5451
getLabelSize: function () {
5452
var label = this.label;
5454
((this.labelBBox = label.getBBox()))[horiz ? 'height' : 'width'] :
5459
* Find how far the labels extend to the right and left of the tick's x position. Used for anti-collision
5460
* detection with overflow logic.
5462
getLabelSides: function () {
5463
var bBox = this.labelBBox, // assume getLabelSize has run at this point
5464
labelOptions = options.labels,
5466
leftSide = width * { left: 0, center: 0.5, right: 1 }[labelOptions.align] - labelOptions.x;
5468
return [-leftSide, width - leftSide];
5472
* Handle the label overflow by adjusting the labels to the left and right edge, or
5473
* hide them if they collide into the neighbour label.
5475
handleOverflow: function (index) {
5477
isFirst = this.isFirst,
5478
isLast = this.isLast,
5482
if (isFirst || isLast) {
5484
var sides = this.getLabelSides(),
5485
leftSide = sides[0],
5486
rightSide = sides[1],
5487
plotLeft = chart.plotLeft,
5488
plotRight = plotLeft + axis.len,
5489
neighbour = ticks[tickPositions[index + (isFirst ? 1 : -1)]],
5490
neighbourEdge = neighbour && neighbour.label.x + neighbour.getLabelSides()[isFirst ? 0 : 1];
5492
if ((isFirst && !reversed) || (isLast && reversed)) {
5493
// Is the label spilling out to the left of the plot area?
5494
if (x + leftSide < plotLeft) {
5496
// Align it to plot left
5497
x = plotLeft - leftSide;
5499
// Hide it if it now overlaps the neighbour label
5500
if (neighbour && x + rightSide > neighbourEdge) {
5506
// Is the label spilling out to the right of the plot area?
5507
if (x + rightSide > plotRight) {
5509
// Align it to plot right
5510
x = plotRight - rightSide;
5512
// Hide it if it now overlaps the neighbour label
5513
if (neighbour && x + leftSide < neighbourEdge) {
5520
// Set the modified x position of the label
5527
* Put everything in place
5529
* @param index {Number}
5530
* @param old {Boolean} Use old coordinates to prepare an animation into new position
5532
render: function (index, old) {
5537
labelOptions = options.labels,
5538
gridLine = tick.gridLine,
5539
gridPrefix = type ? type + 'Grid' : 'grid',
5540
tickPrefix = type ? type + 'Tick' : 'tick',
5541
gridLineWidth = options[gridPrefix + 'LineWidth'],
5542
gridLineColor = options[gridPrefix + 'LineColor'],
5543
dashStyle = options[gridPrefix + 'LineDashStyle'],
5544
tickLength = options[tickPrefix + 'Length'],
5545
tickWidth = options[tickPrefix + 'Width'] || 0,
5546
tickColor = options[tickPrefix + 'Color'],
5547
tickPosition = options[tickPrefix + 'Position'],
5551
step = labelOptions.step,
5552
cHeight = (old && oldChartHeight) || chartHeight,
5558
// get x and y position for ticks and labels
5560
translate(pos + tickmarkOffset, null, null, old) + transB :
5561
axisLeft + offset + (opposite ? ((old && oldChartWidth) || chartWidth) - axisRight - axisLeft : 0);
5564
cHeight - axisBottom + offset - (opposite ? axisHeight : 0) :
5565
cHeight - translate(pos + tickmarkOffset, null, null, old) - transB;
5567
// create the grid line
5568
if (gridLineWidth) {
5569
gridLinePath = getPlotLinePath(pos + tickmarkOffset, gridLineWidth, old);
5571
if (gridLine === UNDEFINED) {
5573
stroke: gridLineColor,
5574
'stroke-width': gridLineWidth
5577
attribs.dashstyle = dashStyle;
5582
tick.gridLine = gridLine =
5584
renderer.path(gridLinePath)
5585
.attr(attribs).add(gridGroup) :
5589
// If the parameter 'old' is set, the current call will be followed
5590
// by another call, therefore do not do any animations this time
5591
if (!old && gridLine && gridLinePath) {
5598
// create the tick mark
5601
// negate the length
5602
if (tickPosition === 'inside') {
5603
tickLength = -tickLength;
5606
tickLength = -tickLength;
5609
markPath = renderer.crispLine([
5614
x + (horiz ? 0 : -tickLength),
5615
y + (horiz ? tickLength : 0)
5618
if (mark) { // updating
5622
} else { // first time
5623
tick.mark = renderer.path(
5627
'stroke-width': tickWidth
5632
// the label is created on init - now move it into place
5633
if (label && !isNaN(x)) {
5634
x = x + labelOptions.x - (tickmarkOffset && horiz ?
5635
tickmarkOffset * transA * (reversed ? -1 : 1) : 0);
5636
y = y + labelOptions.y - (tickmarkOffset && !horiz ?
5637
tickmarkOffset * transA * (reversed ? 1 : -1) : 0);
5639
// vertically centered
5640
if (!defined(labelOptions.y)) {
5641
y += pInt(label.styles.lineHeight) * 0.9 - label.getBBox().height / 2;
5645
// correct for staggered labels
5647
y += (index / (step || 1) % staggerLines) * 16;
5650
// Cache x and y to be able to read final position before animation
5654
// apply show first and show last
5655
if ((tick.isFirst && !pick(options.showFirstLabel, 1)) ||
5656
(tick.isLast && !pick(options.showLastLabel, 1))) {
5659
// Handle label overflow and show or hide accordingly
5660
} else if (!staggerLines && horiz && labelOptions.overflow === 'justify' && !tick.handleOverflow(index)) {
5665
if (step && index % step) {
5666
// show those indices dividable by step
5670
// Set the new position, and show or hide
5672
label[tick.isNew ? 'attr' : 'animate']({
5687
* Destructor for the tick prototype
5689
destroy: function () {
5690
destroyObjectProperties(this);
5695
* The object wrapper for plot lines and plot bands
5696
* @param {Object} options
5698
function PlotLineOrBand(options) {
5699
var plotLine = this;
5701
plotLine.options = options;
5702
plotLine.id = options.id;
5709
PlotLineOrBand.prototype = {
5712
* Render the plot line or plot band. If it is already existing,
5715
render: function () {
5716
var plotLine = this,
5717
halfPointRange = (axis.pointRange || 0) / 2,
5718
options = plotLine.options,
5719
optionsLabel = options.label,
5720
label = plotLine.label,
5721
width = options.width,
5723
from = options.from,
5724
value = options.value,
5725
toPath, // bands only
5726
dashStyle = options.dashStyle,
5727
svgElem = plotLine.svgElem,
5735
color = options.color,
5736
zIndex = options.zIndex,
5737
events = options.events,
5740
// logarithmic conversion
5742
from = log2lin(from);
5744
value = log2lin(value);
5749
path = getPlotLinePath(value, width);
5752
'stroke-width': width
5755
attribs.dashstyle = dashStyle;
5757
} else if (defined(from) && defined(to)) { // plot band
5758
// keep within plot area
5759
from = mathMax(from, min - halfPointRange);
5760
to = mathMin(to, max + halfPointRange);
5762
toPath = getPlotLinePath(to);
5763
path = getPlotLinePath(from);
5764
if (path && toPath) {
5771
} else { // outside the axis area
5781
if (defined(zIndex)) {
5782
attribs.zIndex = zIndex;
5785
// common for lines and bands
5790
}, null, svgElem.onGetPath);
5793
svgElem.onGetPath = function () {
5797
} else if (path && path.length) {
5798
plotLine.svgElem = svgElem = renderer.path(path)
5799
.attr(attribs).add();
5803
addEvent = function (eventType) {
5804
svgElem.on(eventType, function (e) {
5805
events[eventType].apply(plotLine, [e]);
5808
for (eventType in events) {
5809
addEvent(eventType);
5814
// the plot band/line label
5815
if (optionsLabel && defined(optionsLabel.text) && path && path.length && axisWidth > 0 && axisHeight > 0) {
5817
optionsLabel = merge({
5818
align: horiz && toPath && 'center',
5819
x: horiz ? !toPath && 4 : 10,
5820
verticalAlign : !horiz && toPath && 'middle',
5821
y: horiz ? toPath ? 16 : 10 : toPath ? 6 : -4,
5822
rotation: horiz && !toPath && 90
5825
// add the SVG element
5827
plotLine.label = label = renderer.text(
5833
align: optionsLabel.textAlign || optionsLabel.align,
5834
rotation: optionsLabel.rotation,
5837
.css(optionsLabel.style)
5841
// get the bounding box and align the label
5842
xs = [path[1], path[4], pick(path[6], path[1])];
5843
ys = [path[2], path[5], pick(path[7], path[2])];
5847
label.align(optionsLabel, false, {
5850
width: arrayMax(xs) - x,
5851
height: arrayMax(ys) - y
5855
} else if (label) { // move out of sight
5864
* Remove the plot line or band
5866
destroy: function () {
5869
destroyObjectProperties(obj);
5871
// remove it from the lookup
5872
erase(plotLinesAndBands, obj);
5877
* The class for stack items
5879
function StackItem(options, isNegative, x, stackOption) {
5880
var stackItem = this;
5882
// Tells if the stack is negative
5883
stackItem.isNegative = isNegative;
5885
// Save the options to be able to style the label
5886
stackItem.options = options;
5888
// Save the x value to be able to position the label later
5891
// Save the stack option on the series configuration object
5892
stackItem.stack = stackOption;
5894
// The align options and text align varies on whether the stack is negative and
5895
// if the chart is inverted or not.
5896
// First test the user supplied value, then use the dynamic.
5897
stackItem.alignOptions = {
5898
align: options.align || (inverted ? (isNegative ? 'left' : 'right') : 'center'),
5899
verticalAlign: options.verticalAlign || (inverted ? 'middle' : (isNegative ? 'bottom' : 'top')),
5900
y: pick(options.y, inverted ? 4 : (isNegative ? 14 : -6)),
5901
x: pick(options.x, inverted ? (isNegative ? -6 : 6) : 0)
5904
stackItem.textAlign = options.textAlign || (inverted ? (isNegative ? 'right' : 'left') : 'center');
5907
StackItem.prototype = {
5908
destroy: function () {
5909
destroyObjectProperties(this);
5913
* Sets the total of this stack. Should be called when a serie is hidden or shown
5914
* since that will affect the total of other stacks.
5916
setTotal: function (total) {
5922
* Renders the stack total label and adds it to the stack label group.
5924
render: function (group) {
5925
var stackItem = this, // aliased this
5926
str = stackItem.options.formatter.call(stackItem); // format the text in the label
5928
// Change the text to reflect the new total and set visibility to hidden in case the serie is hidden
5929
if (stackItem.label) {
5930
stackItem.label.attr({text: str, visibility: HIDDEN});
5934
chart.renderer.text(str, 0, 0) // dummy positions, actual position updated with setOffset method in columnseries
5935
.css(stackItem.options.style) // apply style
5936
.attr({align: stackItem.textAlign, // fix the text-anchor
5937
rotation: stackItem.options.rotation, // rotation
5938
visibility: HIDDEN }) // hidden until setOffset is called
5939
.add(group); // add to the labels-group
5944
* Sets the offset that the stack has from the x value and repositions the label.
5946
setOffset: function (xOffset, xWidth) {
5947
var stackItem = this, // aliased this
5948
neg = stackItem.isNegative, // special treatment is needed for negative stacks
5949
y = axis.translate(stackItem.total, 0, 0, 0, 1), // stack value translated mapped to chart coordinates
5950
yZero = axis.translate(0), // stack origin
5951
h = mathAbs(y - yZero), // stack height
5952
x = chart.xAxis[0].translate(stackItem.x) + xOffset, // stack x position
5953
plotHeight = chart.plotHeight,
5954
stackBox = { // this is the box for the complete stack
5955
x: inverted ? (neg ? y : y - h) : x,
5956
y: inverted ? plotHeight - x - xWidth : (neg ? (plotHeight - y - h) : plotHeight - y),
5957
width: inverted ? h : xWidth,
5958
height: inverted ? xWidth : h
5961
if (stackItem.label) {
5963
.align(stackItem.alignOptions, null, stackBox) // align the label to the box
5964
.attr({visibility: VISIBLE}); // set visibility
5970
* Get the minimum and maximum for the series of each axis
5972
function getSeriesExtremes() {
5977
// reset dataMin and dataMax in case we're redrawing
5978
dataMin = dataMax = null;
5980
// loop through this axis' series
5981
each(axis.series, function (series) {
5983
if (series.visible || !optionsChart.ignoreHiddenSeries) {
5985
var seriesOptions = series.options,
5996
threshold = seriesOptions.threshold,
6001
// Validate threshold in logarithmic axes
6002
if (isLog && threshold <= 0) {
6003
threshold = seriesOptions.threshold = null;
6006
// Get dataMin and dataMax for X axes
6008
xData = series.xData;
6010
dataMin = mathMin(pick(dataMin, xData[0]), arrayMin(xData));
6011
dataMax = mathMax(pick(dataMax, xData[0]), arrayMax(xData));
6014
// Get dataMin and dataMax for Y axes, as well as handle stacking and processed data
6019
cropped = series.cropped,
6020
xExtremes = series.xAxis.getExtremes(),
6024
hasModifyValue = !!series.modifyValue;
6028
stacking = seriesOptions.stacking;
6029
usePercentage = stacking === 'percent';
6031
// create a stack for this particular series type
6033
stackOption = seriesOptions.stack;
6034
stackKey = series.type + pick(stackOption, '');
6035
negKey = '-' + stackKey;
6036
series.stackKey = stackKey; // used in translate
6038
posPointStack = posStack[stackKey] || []; // contains the total values for each x
6039
posStack[stackKey] = posPointStack;
6041
negPointStack = negStack[negKey] || [];
6042
negStack[negKey] = negPointStack;
6044
if (usePercentage) {
6050
// processData can alter series.pointRange, so this goes after
6051
//findPointRange = series.pointRange === null;
6053
xData = series.processedXData;
6054
yData = series.processedYData;
6055
yDataLength = yData.length;
6057
// loop over the non-null y values and read them into a local array
6058
for (i = 0; i < yDataLength; i++) {
6061
if (y !== null && y !== UNDEFINED) {
6063
// read stacked values into a stack based on the x value,
6064
// the sign of y and the stack key
6066
isNegative = y < threshold;
6067
pointStack = isNegative ? negPointStack : posPointStack;
6068
key = isNegative ? negKey : stackKey;
6071
defined(pointStack[x]) ?
6072
pointStack[x] + y : y;
6080
// If the StackItem is there, just update the values,
6081
// if not, create one first
6082
if (!stacks[key][x]) {
6083
stacks[key][x] = new StackItem(options.stackLabels, isNegative, x, stackOption);
6085
stacks[key][x].setTotal(y);
6088
// general hook, used for Highstock compare values feature
6089
} else if (hasModifyValue) {
6090
y = series.modifyValue(y);
6093
// get the smallest distance between points
6095
distance = mathAbs(xData[i] - xData[i - 1]);
6096
pointRange = pointRange === UNDEFINED ? distance : mathMin(distance, pointRange);
6099
// for points within the visible range, including the first point outside the
6100
// visible range, consider y extremes
6101
if (cropped || ((xData[i + 1] || x) >= xExtremes.min && (xData[i - 1] || x) <= xExtremes.max)) {
6104
if (j) { // array, like ohlc data
6106
if (y[j] !== null) {
6107
activeYData[activeCounter++] = y[j];
6111
activeYData[activeCounter++] = y;
6117
// record the least unit distance
6118
/*if (findPointRange) {
6119
series.pointRange = pointRange || 1;
6121
series.closestPointRange = pointRange;*/
6123
// Get the dataMin and dataMax so far. If percentage is used, the min and max are
6124
// always 0 and 100. If the length of activeYData is 0, continue with null values.
6125
if (!usePercentage && activeYData.length) {
6126
dataMin = mathMin(pick(dataMin, activeYData[0]), arrayMin(activeYData));
6127
dataMax = mathMax(pick(dataMax, activeYData[0]), arrayMax(activeYData));
6130
// Adjust to threshold
6131
if (defined(threshold)) {
6132
if (dataMin >= threshold) {
6133
dataMin = threshold;
6134
ignoreMinPadding = true;
6135
} else if (dataMax < threshold) {
6136
dataMax = threshold;
6137
ignoreMaxPadding = true;
6147
* Translate from axis value to pixel position on the chart, or back
6150
translate = function (val, backwards, cvsCoord, old, handleLog) {
6154
localA = old ? oldTransA : transA,
6155
localMin = old ? oldMin : min,
6157
postTranslate = options.ordinal || (isLog && handleLog);
6164
sign *= -1; // canvas coordinates inverts the value
6165
cvsOffset = axisLength;
6167
if (reversed) { // reversed axis
6169
cvsOffset -= sign * axisLength;
6172
if (backwards) { // reverse translation
6174
val = axisLength - val;
6176
returnValue = val / localA + localMin; // from chart pixel to value
6177
if (postTranslate) { // log and ordinal axes
6178
returnValue = axis.lin2val(returnValue);
6181
} else { // normal translation, from axis value to pixel, relative to plot
6182
if (postTranslate) { // log and ordinal axes
6183
val = axis.val2lin(val);
6186
returnValue = sign * (val - localMin) * localA + cvsOffset + (sign * minPixelPadding);
6193
* Create the path for a plot line that goes from the given value on
6194
* this axis, across the plot to the opposite side
6195
* @param {Number} value
6196
* @param {Number} lineWidth Used for calculation crisp line
6197
* @param {Number] old Use old coordinates (for resizing and rescaling)
6199
getPlotLinePath = function (value, lineWidth, old) {
6204
translatedValue = translate(value, null, null, old),
6205
cHeight = (old && oldChartHeight) || chartHeight,
6206
cWidth = (old && oldChartWidth) || chartWidth,
6209
x1 = x2 = mathRound(translatedValue + transB);
6210
y1 = y2 = mathRound(cHeight - translatedValue - transB);
6212
if (isNaN(translatedValue)) { // no min or max
6217
y2 = cHeight - axisBottom;
6218
if (x1 < axisLeft || x1 > axisLeft + axisWidth) {
6223
x2 = cWidth - axisRight;
6225
if (y1 < axisTop || y1 > axisTop + axisHeight) {
6231
renderer.crispLine([M, x1, y1, L, x2, y2], lineWidth || 0);
6235
* Set the tick positions of a linear axis to round values like whole tens or every five.
6237
function getLinearTickPositions(tickInterval, min, max) {
6241
roundedMin = correctFloat(mathFloor(min / tickInterval) * tickInterval),
6242
roundedMax = correctFloat(mathCeil(max / tickInterval) * tickInterval),
6245
// Populate the intermediate values
6247
while (pos <= roundedMax) {
6249
// Place the tick on the rounded value
6250
tickPositions.push(pos);
6252
// Always add the raw tickInterval, not the corrected one.
6253
pos = correctFloat(pos + tickInterval);
6255
// If the interval is not big enough in the current min - max range to actually increase
6256
// the loop variable, we need to break out to prevent endless loop. Issue #619
6257
if (pos === lastPos) {
6261
// Record the last value
6264
return tickPositions;
6268
* Set the tick positions of a logarithmic axis
6270
function getLogTickPositions(interval, min, max, minor) {
6272
// Since we use this method for both major and minor ticks,
6273
// use a local variable and return the result
6278
axis._minorAutoInterval = null;
6281
// First case: All ticks fall on whole logarithms: 1, 10, 100 etc.
6282
if (interval >= 0.5) {
6283
interval = mathRound(interval);
6284
positions = getLinearTickPositions(interval, min, max);
6286
// Second case: We need intermediary ticks. For example
6287
// 1, 2, 4, 6, 8, 10, 20, 40 etc.
6288
} else if (interval >= 0.08) {
6289
var roundedMin = mathFloor(min),
6298
if (interval > 0.3) {
6299
intermediate = [1, 2, 4];
6300
} else if (interval > 0.15) { // 0.2 equals five minor ticks per 1, 10, 100 etc
6301
intermediate = [1, 2, 4, 6, 8];
6302
} else { // 0.1 equals ten minor ticks per 1, 10, 100 etc
6303
intermediate = [1, 2, 3, 4, 5, 6, 7, 8, 9];
6306
for (i = roundedMin; i < max + 1 && !break2; i++) {
6307
len = intermediate.length;
6308
for (j = 0; j < len && !break2; j++) {
6309
pos = log2lin(lin2log(i) * intermediate[j]);
6312
positions.push(lastPos);
6315
if (lastPos > max) {
6322
// Third case: We are so deep in between whole logarithmic values that
6323
// we might as well handle the tick positions like a linear axis. For
6324
// example 1.01, 1.02, 1.03, 1.04.
6326
var realMin = lin2log(min),
6327
realMax = lin2log(max),
6328
tickIntervalOption = options[minor ? 'minorTickInterval' : 'tickInterval'],
6329
filteredTickIntervalOption = tickIntervalOption === 'auto' ? null : tickIntervalOption,
6330
tickPixelIntervalOption = options.tickPixelInterval / (minor ? 5 : 1),
6331
totalPixelLength = minor ? axisLength / tickPositions.length : axisLength;
6334
filteredTickIntervalOption,
6335
axis._minorAutoInterval,
6336
(realMax - realMin) * tickPixelIntervalOption / (totalPixelLength || 1)
6339
interval = normalizeTickInterval(
6342
math.pow(10, mathFloor(math.log(interval) / math.LN10))
6345
positions = map(getLinearTickPositions(
6352
axis._minorAutoInterval = interval / 5;
6356
// Set the axis-level tickInterval variable
6358
tickInterval = interval;
6364
* Return the minor tick positions. For logarithmic axes, reuse the same logic
6365
* as for major ticks.
6367
function getMinorTickPositions() {
6368
var minorTickPositions = [],
6374
len = tickPositions.length;
6375
for (i = 1; i < len; i++) {
6376
minorTickPositions = minorTickPositions.concat(
6377
getLogTickPositions(minorTickInterval, tickPositions[i - 1], tickPositions[i], true)
6382
for (pos = min + (tickPositions[0] - min) % minorTickInterval; pos <= max; pos += minorTickInterval) {
6383
minorTickPositions.push(pos);
6387
return minorTickPositions;
6391
* Adjust the min and max for the minimum range. Keep in mind that the series data is
6392
* not yet processed, so we don't have information on data cropping and grouping, or
6393
* updated axis.pointRange or series.pointRange. The data can't be processed until
6394
* we have finally established min and max.
6396
function adjustForMinRange() {
6398
spaceAvailable = dataMax - dataMin >= minRange,
6407
// Set the automatic minimum range based on the closest point distance
6408
if (isXAxis && minRange === UNDEFINED && !isLog) {
6410
if (defined(options.min) || defined(options.max)) {
6411
minRange = null; // don't do this again
6415
// Find the closest distance between raw data points, as opposed to
6416
// closestPointRange that applies to processed points (cropped and grouped)
6417
each(axis.series, function (series) {
6418
xData = series.xData;
6419
loopLength = series.xIncrement ? 1 : xData.length - 1;
6420
for (i = loopLength; i > 0; i--) {
6421
distance = xData[i] - xData[i - 1];
6422
if (closestDataRange === UNDEFINED || distance < closestDataRange) {
6423
closestDataRange = distance;
6427
minRange = mathMin(closestDataRange * 5, dataMax - dataMin);
6431
// if minRange is exceeded, adjust
6432
if (max - min < minRange) {
6434
zoomOffset = (minRange - max + min) / 2;
6436
// if min and max options have been set, don't go beyond it
6437
minArgs = [min - zoomOffset, pick(options.min, min - zoomOffset)];
6438
if (spaceAvailable) { // if space is available, stay within the data range
6439
minArgs[2] = dataMin;
6441
min = arrayMax(minArgs);
6443
maxArgs = [min + minRange, pick(options.max, min + minRange)];
6444
if (spaceAvailable) { // if space is availabe, stay within the data range
6445
maxArgs[2] = dataMax;
6448
max = arrayMin(maxArgs);
6450
// now if the max is adjusted, adjust the min back
6451
if (max - min < minRange) {
6452
minArgs[0] = max - minRange;
6453
minArgs[1] = pick(options.min, max - minRange);
6454
min = arrayMax(minArgs);
6460
* Set the tick positions to round values and optionally extend the extremes
6461
* to the nearest tick
6463
function setTickPositions(secondPass) {
6466
linkedParentExtremes,
6467
tickIntervalOption = options.tickInterval,
6468
tickPixelIntervalOption = options.tickPixelInterval;
6470
// linked axis gets the extremes from the parent axis
6472
linkedParent = chart[isXAxis ? 'xAxis' : 'yAxis'][options.linkedTo];
6473
linkedParentExtremes = linkedParent.getExtremes();
6474
min = pick(linkedParentExtremes.min, linkedParentExtremes.dataMin);
6475
max = pick(linkedParentExtremes.max, linkedParentExtremes.dataMax);
6476
if (options.type !== linkedParent.options.type) {
6477
error(11, 1); // Can't link axes of different type
6479
} else { // initial min and max from the extreme data values
6480
min = pick(userMin, options.min, dataMin);
6481
max = pick(userMax, options.max, dataMax);
6485
if (!secondPass && mathMin(min, dataMin) <= 0) {
6486
error(10, 1); // Can't plot negative values on log axis
6492
// handle zoomed range
6494
userMin = min = mathMax(min, max - range); // #618
6497
range = null; // don't use it when running setExtremes
6501
// adjust min and max for the minimum range
6502
adjustForMinRange();
6504
// pad the values to get clear of the chart's edges
6505
if (!categories && !usePercentage && !isLinked && defined(min) && defined(max)) {
6506
length = (max - min) || 1;
6507
if (!defined(options.min) && !defined(userMin) && minPadding && (dataMin < 0 || !ignoreMinPadding)) {
6508
min -= length * minPadding;
6510
if (!defined(options.max) && !defined(userMax) && maxPadding && (dataMax > 0 || !ignoreMaxPadding)) {
6511
max += length * maxPadding;
6516
if (min === max || min === undefined || max === undefined) {
6518
} else if (isLinked && !tickIntervalOption &&
6519
tickPixelIntervalOption === linkedParent.options.tickPixelInterval) {
6520
tickInterval = linkedParent.tickInterval;
6522
tickInterval = pick(
6524
categories ? // for categoried axis, 1 is default, for linear axis use tickPix
6526
(max - min) * tickPixelIntervalOption / (axisLength || 1)
6530
// Now we're finished detecting min and max, crop and group series data. This
6531
// is in turn needed in order to find tick positions in ordinal axes.
6532
if (isXAxis && !secondPass) {
6533
each(axis.series, function (series) {
6534
series.processData(min !== oldMin || max !== oldMax);
6538
// set the translation factor used in translate function
6539
setAxisTranslation();
6541
// hook for ordinal axes. To do: merge with below
6542
if (axis.beforeSetTickPositions) {
6543
axis.beforeSetTickPositions();
6546
// hook for extensions, used in Highstock ordinal axes
6547
if (axis.postProcessTickInterval) {
6548
tickInterval = axis.postProcessTickInterval(tickInterval);
6551
// for linear axes, get magnitude and normalize the interval
6552
if (!isDatetimeAxis && !isLog) { // linear
6553
magnitude = math.pow(10, mathFloor(math.log(tickInterval) / math.LN10));
6554
if (!defined(options.tickInterval)) {
6555
tickInterval = normalizeTickInterval(tickInterval, null, magnitude, options);
6559
// record the tick interval for linked axis
6560
axis.tickInterval = tickInterval;
6562
// get minorTickInterval
6563
minorTickInterval = options.minorTickInterval === 'auto' && tickInterval ?
6564
tickInterval / 5 : options.minorTickInterval;
6566
// find the tick positions
6567
tickPositions = options.tickPositions || (tickPositioner && tickPositioner.apply(axis, [min, max]));
6568
if (!tickPositions) {
6569
if (isDatetimeAxis) {
6570
tickPositions = (axis.getNonLinearTimeTicks || getTimeTicks)(
6571
normalizeTimeTickInterval(tickInterval, options.units),
6574
options.startOfWeek,
6575
axis.ordinalPositions,
6576
axis.closestPointRange,
6580
tickPositions = getLogTickPositions(tickInterval, min, max);
6582
tickPositions = getLinearTickPositions(tickInterval, min, max);
6588
// reset min/max or remove extremes based on start/end on tick
6589
var roundedMin = tickPositions[0],
6590
roundedMax = tickPositions[tickPositions.length - 1];
6592
if (options.startOnTick) {
6594
} else if (min > roundedMin) {
6595
tickPositions.shift();
6598
if (options.endOnTick) {
6600
} else if (max < roundedMax) {
6601
tickPositions.pop();
6604
// record the greatest number of ticks for multi axis
6605
if (!maxTicks) { // first call, or maxTicks have been reset after a zoom operation
6612
if (!isDatetimeAxis && tickPositions.length > maxTicks[xOrY] && options.alignTicks !== false) {
6613
maxTicks[xOrY] = tickPositions.length;
6619
* When using multiple axes, adjust the number of ticks to match the highest
6620
* number of ticks in that group
6622
function adjustTickAmount() {
6624
if (maxTicks && maxTicks[xOrY] && !isDatetimeAxis && !categories && !isLinked && options.alignTicks !== false) { // only apply to linear scale
6625
var oldTickAmount = tickAmount,
6626
calculatedTickAmount = tickPositions.length;
6628
// set the axis-level tickAmount to use below
6629
tickAmount = maxTicks[xOrY];
6631
if (calculatedTickAmount < tickAmount) {
6632
while (tickPositions.length < tickAmount) {
6633
tickPositions.push(correctFloat(
6634
tickPositions[tickPositions.length - 1] + tickInterval
6637
transA *= (calculatedTickAmount - 1) / (tickAmount - 1);
6638
max = tickPositions[tickPositions.length - 1];
6641
if (defined(oldTickAmount) && tickAmount !== oldTickAmount) {
6642
axis.isDirty = true;
6650
* Set the scale based on data min and max, user set min and max or options
6653
function setScale() {
6661
oldAxisLength = axisLength;
6663
// set the new axisLength
6664
axisLength = horiz ? axisWidth : axisHeight;
6665
isDirtyAxisLength = axisLength !== oldAxisLength;
6667
// is there new data?
6668
each(axis.series, function (series) {
6669
if (series.isDirtyData || series.isDirty ||
6670
series.xAxis.isDirty) { // when x axis is dirty, we need new data extremes for y as well
6675
// do we really need to go through all this?
6676
if (isDirtyAxisLength || isDirtyData || isLinked ||
6677
userMin !== oldUserMin || userMax !== oldUserMax) {
6679
// get data extremes if needed
6680
getSeriesExtremes();
6682
// get fixed positions based on tickInterval
6685
// record old values to decide whether a rescale is necessary later on (#540)
6686
oldUserMin = userMin;
6687
oldUserMax = userMax;
6691
for (type in stacks) {
6692
for (i in stacks[type]) {
6693
stacks[type][i].cum = stacks[type][i].total;
6698
// Mark as dirty if it is not already set to dirty and extremes have changed. #595.
6699
if (!axis.isDirty) {
6700
axis.isDirty = isDirtyAxisLength || min !== oldMin || max !== oldMax;
6706
* Set the extremes and optionally redraw
6707
* @param {Number} newMin
6708
* @param {Number} newMax
6709
* @param {Boolean} redraw
6710
* @param {Boolean|Object} animation Whether to apply animation, and optionally animation
6712
* @param {Object} eventArguments
6715
function setExtremes(newMin, newMax, redraw, animation, eventArguments) {
6717
redraw = pick(redraw, true); // defaults to true
6719
// Extend the arguments with min and max
6720
eventArguments = extend(eventArguments, {
6726
fireEvent(axis, 'setExtremes', eventArguments, function () { // the default event handler
6731
// Mark for running afterSetExtremes
6732
axis.isDirtyExtremes = true;
6736
chart.redraw(animation);
6742
* Update translation information
6744
setAxisTranslation = function () {
6745
var range = max - min,
6748
seriesClosestPointRange;
6750
// adjust translation for padding
6753
pointRange = linkedParent.pointRange;
6755
each(axis.series, function (series) {
6756
pointRange = mathMax(pointRange, series.pointRange);
6757
seriesClosestPointRange = series.closestPointRange;
6758
if (!series.noSharedTooltip && defined(seriesClosestPointRange)) {
6759
closestPointRange = defined(closestPointRange) ?
6760
mathMin(closestPointRange, seriesClosestPointRange) :
6761
seriesClosestPointRange;
6766
// pointRange means the width reserved for each point, like in a column chart
6767
axis.pointRange = pointRange;
6769
// closestPointRange means the closest distance between points. In columns
6770
// it is mostly equal to pointRange, but in lines pointRange is 0 while closestPointRange
6771
// is some other value
6772
axis.closestPointRange = closestPointRange;
6777
axis.translationSlope = transA = axisLength / ((range + pointRange) || 1);
6778
transB = horiz ? axisLeft : axisBottom; // translation addend
6779
minPixelPadding = transA * (pointRange / 2);
6783
* Update the axis metrics
6785
function setAxisSize() {
6787
var offsetLeft = options.offsetLeft || 0,
6788
offsetRight = options.offsetRight || 0;
6791
axisLeft = pick(options.left, plotLeft + offsetLeft);
6792
axisTop = pick(options.top, plotTop);
6793
axisWidth = pick(options.width, plotWidth - offsetLeft + offsetRight);
6794
axisHeight = pick(options.height, plotHeight);
6795
axisBottom = chartHeight - axisHeight - axisTop;
6796
axisRight = chartWidth - axisWidth - axisLeft;
6797
axisLength = horiz ? axisWidth : axisHeight;
6799
// expose to use in Series object and navigator
6800
axis.left = axisLeft;
6802
axis.len = axisLength;
6807
* Get the actual axis extremes
6809
function getExtremes() {
6811
min: isLog ? correctFloat(lin2log(min)) : min,
6812
max: isLog ? correctFloat(lin2log(max)) : max,
6821
* Get the zero plane either based on zero or on the min or max value.
6822
* Used in bar and area plots
6824
function getThreshold(threshold) {
6825
var realMin = isLog ? lin2log(min) : min,
6826
realMax = isLog ? lin2log(max) : max;
6828
if (realMin > threshold || threshold === null) {
6829
threshold = realMin;
6830
} else if (realMax < threshold) {
6831
threshold = realMax;
6834
return translate(threshold, 0, 1, 0, 1);
6838
* Add a plot band or plot line after render time
6840
* @param options {Object} The plotBand or plotLine configuration object
6842
function addPlotBandOrLine(options) {
6843
var obj = new PlotLineOrBand(options).render();
6844
plotLinesAndBands.push(obj);
6849
* Render the tick labels to a preliminary position to get their sizes
6851
function getOffset() {
6853
var hasData = axis.series.length && defined(min) && defined(max),
6854
showAxis = hasData || pick(options.showEmpty, true),
6858
axisTitleOptions = options.title,
6859
labelOptions = options.labels,
6860
directionFactor = [-1, 1, 1, -1][side],
6864
axisGroup = renderer.g('axis')
6865
.attr({ zIndex: 7 })
6867
gridGroup = renderer.g('grid')
6868
.attr({ zIndex: options.gridZIndex || 1 })
6872
labelOffset = 0; // reset
6874
if (hasData || isLinked) {
6875
each(tickPositions, function (pos) {
6877
ticks[pos] = new Tick(pos);
6879
ticks[pos].addLabel(); // update labels depending on tick interval
6884
each(tickPositions, function (pos) {
6885
// left side must be align: right and right side must have align: left for labels
6886
if (side === 0 || side === 2 || { 1: 'left', 3: 'right' }[side] === labelOptions.align) {
6888
// get the highest offset
6889
labelOffset = mathMax(
6890
ticks[pos].getLabelSize(),
6898
labelOffset += (staggerLines - 1) * 16;
6901
} else { // doesn't have data
6908
if (axisTitleOptions && axisTitleOptions.text) {
6910
axisTitle = axis.axisTitle = renderer.text(
6911
axisTitleOptions.text,
6914
axisTitleOptions.useHTML
6918
rotation: axisTitleOptions.rotation || 0,
6920
axisTitleOptions.textAlign ||
6921
{ low: 'left', middle: 'center', high: 'right' }[axisTitleOptions.align]
6923
.css(axisTitleOptions.style)
6925
axisTitle.isNew = true;
6929
titleOffset = axisTitle.getBBox()[horiz ? 'height' : 'width'];
6930
titleMargin = pick(axisTitleOptions.margin, horiz ? 5 : 10);
6931
titleOffsetOption = axisTitleOptions.offset;
6934
// hide or show the title depending on whether showEmpty is set
6935
axisTitle[showAxis ? 'show' : 'hide']();
6940
// handle automatic or user set offset
6941
offset = directionFactor * pick(options.offset, axisOffset[side]);
6944
pick(titleOffsetOption,
6945
labelOffset + titleMargin +
6946
(side !== 2 && labelOffset && directionFactor * options.labels[horiz ? 'y' : 'x'])
6949
axisOffset[side] = mathMax(
6951
axisTitleMargin + titleOffset + directionFactor * offset
6960
var axisTitleOptions = options.title,
6961
stackLabelOptions = options.stackLabels,
6962
alternateGridColor = options.alternateGridColor,
6963
lineWidth = options.lineWidth,
6967
hasRendered = chart.hasRendered,
6968
slideInTicks = hasRendered && defined(oldMin) && !isNaN(oldMin),
6969
hasData = axis.series.length && defined(min) && defined(max),
6970
showAxis = hasData || pick(options.showEmpty, true),
6974
// If the series has data draw the ticks. Else only the line and title
6975
if (hasData || isLinked) {
6978
if (minorTickInterval && !categories) {
6979
each(getMinorTickPositions(), function (pos) {
6980
if (!minorTicks[pos]) {
6981
minorTicks[pos] = new Tick(pos, 'minor');
6984
// render new ticks in old position
6985
if (slideInTicks && minorTicks[pos].isNew) {
6986
minorTicks[pos].render(null, true);
6990
minorTicks[pos].isActive = true;
6991
minorTicks[pos].render();
6995
// Major ticks. Pull out the first item and render it last so that
6996
// we can get the position of the neighbour label. #808.
6997
each(tickPositions.slice(1).concat([tickPositions[0]]), function (pos, i) {
6999
// Reorganize the indices
7000
i = (i === tickPositions.length - 1) ? 0 : i + 1;
7002
// linked axes need an extra check to find out if
7003
if (!isLinked || (pos >= min && pos <= max)) {
7006
ticks[pos] = new Tick(pos);
7009
// render new ticks in old position
7010
if (slideInTicks && ticks[pos].isNew) {
7011
ticks[pos].render(i, true);
7014
ticks[pos].isActive = true;
7015
ticks[pos].render(i);
7020
// alternate grid color
7021
if (alternateGridColor) {
7022
each(tickPositions, function (pos, i) {
7023
if (i % 2 === 0 && pos < max) {
7024
if (!alternateBands[pos]) {
7025
alternateBands[pos] = new PlotLineOrBand();
7028
to = tickPositions[i + 1] !== UNDEFINED ? tickPositions[i + 1] : max;
7029
alternateBands[pos].options = {
7030
from: isLog ? lin2log(from) : from,
7031
to: isLog ? lin2log(to) : to,
7032
color: alternateGridColor
7034
alternateBands[pos].render();
7035
alternateBands[pos].isActive = true;
7040
// custom plot lines and bands
7041
if (!axis._addedPlotLB) { // only first time
7042
each((options.plotLines || []).concat(options.plotBands || []), function (plotLineOptions) {
7043
//plotLinesAndBands.push(new PlotLineOrBand(plotLineOptions).render());
7044
addPlotBandOrLine(plotLineOptions);
7046
axis._addedPlotLB = true;
7053
// remove inactive ticks
7054
each([ticks, minorTicks, alternateBands], function (coll) {
7057
if (!coll[pos].isActive) {
7058
coll[pos].destroy();
7061
coll[pos].isActive = false; // reset
7069
// Static items. As the axis group is cleared on subsequent calls
7070
// to render, these items are added outside the group.
7073
lineLeft = axisLeft + (opposite ? axisWidth : 0) + offset;
7074
lineTop = chartHeight - axisBottom - (opposite ? axisHeight : 0) + offset;
7076
linePath = renderer.crispLine([
7086
chartWidth - axisRight :
7090
chartHeight - axisBottom
7093
axisLine = renderer.path(linePath)
7095
stroke: options.lineColor,
7096
'stroke-width': lineWidth,
7101
axisLine.animate({ d: linePath });
7104
// show or hide the line depending on options.showEmpty
7105
axisLine[showAxis ? 'show' : 'hide']();
7109
if (axisTitle && showAxis) {
7110
// compute anchor points for each of the title align options
7111
var margin = horiz ? axisLeft : axisTop,
7112
fontSize = pInt(axisTitleOptions.style.fontSize || 12),
7113
// the position in the length direction of the axis
7115
low: margin + (horiz ? 0 : axisLength),
7116
middle: margin + axisLength / 2,
7117
high: margin + (horiz ? axisLength : 0)
7118
}[axisTitleOptions.align],
7120
// the position in the perpendicular direction of the axis
7121
offAxis = (horiz ? axisTop + axisHeight : axisLeft) +
7122
(horiz ? 1 : -1) * // horizontal axis reverses the margin
7123
(opposite ? -1 : 1) * // so does opposite axes
7125
(side === 2 ? fontSize : 0);
7127
axisTitle[axisTitle.isNew ? 'attr' : 'animate']({
7130
offAxis + (opposite ? axisWidth : 0) + offset +
7131
(axisTitleOptions.x || 0), // x
7133
offAxis - (opposite ? axisHeight : 0) + offset :
7134
alongAxis + (axisTitleOptions.y || 0) // y
7136
axisTitle.isNew = false;
7140
if (stackLabelOptions && stackLabelOptions.enabled) {
7141
var stackKey, oneStack, stackCategory,
7142
stackTotalGroup = axis.stackTotalGroup;
7144
// Create a separate group for the stack total labels
7145
if (!stackTotalGroup) {
7146
axis.stackTotalGroup = stackTotalGroup =
7147
renderer.g('stack-labels')
7149
visibility: VISIBLE,
7152
.translate(plotLeft, plotTop)
7156
// Render each stack total
7157
for (stackKey in stacks) {
7158
oneStack = stacks[stackKey];
7159
for (stackCategory in oneStack) {
7160
oneStack[stackCategory].render(stackTotalGroup);
7164
// End stacked totals
7166
axis.isDirty = false;
7170
* Remove a plot band or plot line from the chart by id
7171
* @param {Object} id
7173
function removePlotBandOrLine(id) {
7174
var i = plotLinesAndBands.length;
7176
if (plotLinesAndBands[i].id === id) {
7177
plotLinesAndBands[i].destroy();
7183
* Update the axis title by options
7185
function setTitle(newTitleOptions, redraw) {
7186
options.title = merge(options.title, newTitleOptions);
7188
axisTitle = axisTitle.destroy();
7189
axis.isDirty = true;
7191
if (pick(redraw, true)) {
7197
* Redraw the axis to reflect changes in the data or axis extremes
7201
// hide tooltip and hover states
7202
if (tracker.resetTracker) {
7203
tracker.resetTracker();
7209
// move plot lines and bands
7210
each(plotLinesAndBands, function (plotLine) {
7214
// mark associated series as dirty and ready for redraw
7215
each(axis.series, function (series) {
7216
series.isDirty = true;
7222
* Set new axis categories and optionally redraw
7223
* @param {Array} newCategories
7224
* @param {Boolean} doRedraw
7226
function setCategories(newCategories, doRedraw) {
7227
// set the categories
7228
axis.categories = userOptions.categories = categories = newCategories;
7230
// force reindexing tooltips
7231
each(axis.series, function (series) {
7233
series.setTooltipPoints(true);
7237
// optionally redraw
7238
axis.isDirty = true;
7240
if (pick(doRedraw, true)) {
7246
* Destroys an Axis instance.
7248
function destroy() {
7251
// Remove the events
7254
// Destroy each stack total
7255
for (stackKey in stacks) {
7256
destroyObjectProperties(stacks[stackKey]);
7258
stacks[stackKey] = null;
7261
// Destroy stack total group
7262
if (axis.stackTotalGroup) {
7263
axis.stackTotalGroup = axis.stackTotalGroup.destroy();
7266
// Destroy collections
7267
each([ticks, minorTicks, alternateBands, plotLinesAndBands], function (coll) {
7268
destroyObjectProperties(coll);
7271
// Destroy local variables
7272
each([axisLine, axisGroup, gridGroup, axisTitle], function (obj) {
7277
axisLine = axisGroup = gridGroup = axisTitle = null;
7285
chart[isXAxis ? 'xAxis' : 'yAxis'].push(axis);
7287
// inverted charts have reversed xAxes as default
7288
if (inverted && isXAxis && reversed === UNDEFINED) {
7293
// expose some variables
7295
addPlotBand: addPlotBandOrLine,
7296
addPlotLine: addPlotBandOrLine,
7297
adjustTickAmount: adjustTickAmount,
7298
categories: categories,
7299
getExtremes: getExtremes,
7300
getPlotLinePath: getPlotLinePath,
7301
getThreshold: getThreshold,
7304
plotLinesAndBands: plotLinesAndBands,
7305
getOffset: getOffset,
7307
setAxisSize: setAxisSize,
7308
setAxisTranslation: setAxisTranslation,
7309
setCategories: setCategories,
7310
setExtremes: setExtremes,
7312
setTickPositions: setTickPositions,
7313
translate: translate,
7315
removePlotBand: removePlotBandOrLine,
7316
removePlotLine: removePlotBandOrLine,
7319
series: [], // populated by Series
7324
// register event listeners
7325
for (eventType in events) {
7326
addEvent(axis, eventType, events[eventType]);
7329
// extend logarithmic axis
7331
axis.val2lin = log2lin;
7332
axis.lin2val = lin2log;
7339
* The tooltip object
7340
* @param {Object} options Tooltip options
7342
function Tooltip(options) {
7344
borderWidth = options.borderWidth,
7345
crosshairsOptions = options.crosshairs,
7347
style = options.style,
7348
shared = options.shared,
7349
padding = pInt(style.padding),
7350
tooltipIsHidden = true,
7354
// remove padding CSS and apply padding on box instead
7358
var label = renderer.label('', 0, 0, null, null, null, options.useHTML)
7361
fill: options.backgroundColor,
7362
'stroke-width': borderWidth,
7363
r: options.borderRadius,
7370
// When using canVG the shadow shows up as a gray circle
7371
// even if the tooltip is hidden.
7373
label.shadow(options.shadow);
7377
* Destroy the tooltip and its elements.
7379
function destroy() {
7380
each(crosshairs, function (crosshair) {
7382
crosshair.destroy();
7386
// Destroy and clear local variables
7388
label = label.destroy();
7393
* In case no user defined formatter is given, this will be used
7395
function defaultFormatter() {
7397
items = pThis.points || splat(pThis),
7398
series = items[0].series,
7402
s = [series.tooltipHeaderFormatter(items[0].key)];
7405
each(items, function (item) {
7406
series = item.series;
7407
s.push((series.tooltipFormatter && series.tooltipFormatter(item)) ||
7408
item.point.tooltipFormatter(series.tooltipOptions.pointFormat));
7412
s.push(options.footerFormat || '');
7418
* Provide a soft movement for the tooltip
7420
* @param {Number} finalX
7421
* @param {Number} finalY
7423
function move(finalX, finalY) {
7425
// get intermediate values for animation
7426
currentX = tooltipIsHidden ? finalX : (2 * currentX + finalX) / 3;
7427
currentY = tooltipIsHidden ? finalY : (currentY + finalY) / 2;
7429
// move to the intermediate value
7430
label.attr({ x: currentX, y: currentY });
7432
// run on next tick of the mouse tracker
7433
if (mathAbs(finalX - currentX) > 1 || mathAbs(finalY - currentY) > 1) {
7434
tooltipTick = function () {
7435
move(finalX, finalY);
7446
if (!tooltipIsHidden) {
7447
var hoverPoints = chart.hoverPoints;
7451
// hide previous hoverPoints and set new
7453
each(hoverPoints, function (point) {
7457
chart.hoverPoints = null;
7460
tooltipIsHidden = true;
7466
* Hide the crosshairs
7468
function hideCrosshairs() {
7469
each(crosshairs, function (crosshair) {
7477
* Refresh the tooltip's text and position.
7478
* @param {Object} point
7481
function refresh(point) {
7490
tooltipPos = point.tooltipPos,
7491
formatter = options.formatter || defaultFormatter,
7492
hoverPoints = chart.hoverPoints,
7496
// shared tooltip, array is sent over
7497
if (shared && !(point.series && point.series.noSharedTooltip)) {
7500
// hide previous hoverPoints and set new
7502
each(hoverPoints, function (point) {
7506
chart.hoverPoints = point;
7508
each(point, function (item) {
7509
item.setState(HOVER_STATE);
7510
plotY += item.plotY; // for average
7512
pointConfig.push(item.getLabelConfig());
7515
plotX = point[0].plotX;
7516
plotY = mathRound(plotY) / point.length; // mathRound because Opera 10 has problems here
7519
x: point[0].category
7521
textConfig.points = pointConfig;
7524
// single point tooltip
7526
textConfig = point.getLabelConfig();
7528
text = formatter.call(textConfig);
7530
// register the current series
7531
currentSeries = point.series;
7533
// get the reference point coordinates (pie charts use tooltipPos)
7534
plotX = pick(plotX, point.plotX);
7535
plotY = pick(plotY, point.plotY);
7537
x = mathRound(tooltipPos ? tooltipPos[0] : (inverted ? plotWidth - plotY : plotX));
7538
y = mathRound(tooltipPos ? tooltipPos[1] : (inverted ? plotHeight - plotX : plotY));
7541
// For line type series, hide tooltip if the point falls outside the plot
7542
show = shared || !currentSeries.isCartesian || currentSeries.tooltipOutsidePlot || isInsidePlot(x, y);
7544
// update the inner HTML
7545
if (text === false || !show) {
7550
if (tooltipIsHidden) {
7552
tooltipIsHidden = false;
7560
// set the stroke color of the box
7561
borderColor = options.borderColor || point.color || currentSeries.color || '#606060';
7566
placedTooltipPoint = placeBox(
7574
pick(options.distance, 12),
7579
move(mathRound(placedTooltipPoint.x), mathRound(placedTooltipPoint.y));
7584
if (crosshairsOptions) {
7585
crosshairsOptions = splat(crosshairsOptions); // [x, y]
7588
i = crosshairsOptions.length,
7593
axis = point.series[i ? 'yAxis' : 'xAxis'];
7594
if (crosshairsOptions[i] && axis) {
7595
path = axis.getPlotLinePath(
7596
i ? pick(point.stackY, point.y) : point.x, // #814
7599
if (crosshairs[i]) {
7600
crosshairs[i].attr({ d: path, visibility: VISIBLE });
7604
'stroke-width': crosshairsOptions[i].width || 1,
7605
stroke: crosshairsOptions[i].color || '#C0C0C0',
7606
zIndex: crosshairsOptions[i].zIndex || 2
7608
if (crosshairsOptions[i].dashStyle) {
7609
attribs.dashstyle = crosshairsOptions[i].dashStyle;
7611
crosshairs[i] = renderer.path(path)
7618
fireEvent(chart, 'tooltipRefresh', {
7622
borderColor: borderColor
7633
hideCrosshairs: hideCrosshairs,
7639
* The mouse tracker object
7640
* @param {Object} options
7642
function MouseTracker(options) {
7649
zoomType = useCanVG ? '' : optionsChart.zoomType,
7650
zoomX = /x/.test(zoomType),
7651
zoomY = /y/.test(zoomType),
7652
zoomHor = (zoomX && !inverted) || (zoomY && inverted),
7653
zoomVert = (zoomY && !inverted) || (zoomX && inverted);
7656
* Add crossbrowser support for chartX and chartY
7657
* @param {Object} e The event object in standard browsers
7659
function normalizeMouseEvent(e) {
7666
// common IE normalizing
7669
e.target = e.srcElement;
7672
// jQuery only copies over some properties. IE needs e.x and iOS needs touches.
7673
if (e.originalEvent) {
7674
e = e.originalEvent;
7677
// The same for MooTools. It renames e.pageX to e.page.x. #445.
7683
ePos = e.touches ? e.touches.item(0) : e;
7685
// get mouse position
7686
chartPosition = offset(container);
7687
chartPosLeft = chartPosition.left;
7688
chartPosTop = chartPosition.top;
7690
// chartX and chartY
7691
if (isIE) { // IE including IE9 that has pageX but in a different meaning
7695
chartX = ePos.pageX - chartPosLeft;
7696
chartY = ePos.pageY - chartPosTop;
7700
chartX: mathRound(chartX),
7701
chartY: mathRound(chartY)
7706
* Get the click position in terms of axis values.
7708
* @param {Object} e A mouse event
7710
function getMouseCoordinates(e) {
7715
each(axes, function (axis) {
7716
var translate = axis.translate,
7717
isXAxis = axis.isXAxis,
7718
isHorizontal = inverted ? !isXAxis : isXAxis;
7720
coordinates[isXAxis ? 'xAxis' : 'yAxis'].push({
7724
e.chartX - plotLeft :
7725
plotHeight - e.chartY + plotTop,
7734
* With line type charts with a single tracker, get the point closest to the mouse
7736
function onmousemove(e) {
7739
hoverPoint = chart.hoverPoint,
7740
hoverSeries = chart.hoverSeries,
7743
distance = chartWidth,
7744
index = inverted ? e.chartY : e.chartX - plotLeft; // wtf?
7747
if (tooltip && options.shared && !(hoverSeries && hoverSeries.noSharedTooltip)) {
7750
// loop over all series and find the ones with points closest to the mouse
7752
for (j = 0; j < i; j++) {
7753
if (series[j].visible &&
7754
series[j].options.enableMouseTracking !== false &&
7755
!series[j].noSharedTooltip && series[j].tooltipPoints.length) {
7756
point = series[j].tooltipPoints[index];
7757
point._dist = mathAbs(index - point.plotX);
7758
distance = mathMin(distance, point._dist);
7762
// remove furthest points
7765
if (points[i]._dist > distance) {
7766
points.splice(i, 1);
7769
// refresh the tooltip if necessary
7770
if (points.length && (points[0].plotX !== hoverX)) {
7771
tooltip.refresh(points);
7772
hoverX = points[0].plotX;
7776
// separate tooltip and general mouse events
7777
if (hoverSeries && hoverSeries.tracker) { // only use for line-type series with common tracker
7780
point = hoverSeries.tooltipPoints[index];
7782
// a new point is hovered, refresh the tooltip
7783
if (point && point !== hoverPoint) {
7785
// trigger the events
7786
point.onMouseOver();
7795
* Reset the tracking by hiding the tooltip, the hover series state and the hover point
7797
function resetTracker() {
7798
var hoverSeries = chart.hoverSeries,
7799
hoverPoint = chart.hoverPoint;
7802
hoverPoint.onMouseOut();
7806
hoverSeries.onMouseOut();
7811
tooltip.hideCrosshairs();
7818
* Mouse up or outside the plot area
7821
if (selectionMarker) {
7822
var selectionData = {
7826
selectionBox = selectionMarker.getBBox(),
7827
selectionLeft = selectionBox.x - plotLeft,
7828
selectionTop = selectionBox.y - plotTop;
7831
// a selection has been made
7834
// record each axis' min and max
7835
each(axes, function (axis) {
7836
if (axis.options.zoomEnabled !== false) {
7837
var translate = axis.translate,
7838
isXAxis = axis.isXAxis,
7839
isHorizontal = inverted ? !isXAxis : isXAxis,
7840
selectionMin = translate(
7843
plotHeight - selectionTop - selectionBox.height,
7849
selectionMax = translate(
7851
selectionLeft + selectionBox.width :
7852
plotHeight - selectionTop,
7859
selectionData[isXAxis ? 'xAxis' : 'yAxis'].push({
7861
min: mathMin(selectionMin, selectionMax), // for reversed axes,
7862
max: mathMax(selectionMin, selectionMax)
7866
fireEvent(chart, 'selection', selectionData, zoom);
7869
selectionMarker = selectionMarker.destroy();
7872
css(container, { cursor: 'auto' });
7874
chart.mouseIsDown = mouseIsDown = hasDragged = false;
7875
removeEvent(doc, hasTouch ? 'touchend' : 'mouseup', drop);
7880
* Special handler for mouse move that will hide the tooltip when the mouse leaves the plotarea.
7882
function hideTooltipOnMouseMove(e) {
7883
var pageX = defined(e.pageX) ? e.pageX : e.page.x, // In mootools the event is wrapped and the page x/y position is named e.page.x
7884
pageY = defined(e.pageX) ? e.pageY : e.page.y; // Ref: http://mootools.net/docs/core/Types/DOMEvent
7886
if (chartPosition &&
7887
!isInsidePlot(pageX - chartPosition.left - plotLeft,
7888
pageY - chartPosition.top - plotTop)) {
7894
* When mouse leaves the container, hide the tooltip.
7896
function hideTooltipOnMouseLeave() {
7898
chartPosition = null; // also reset the chart position, used in #149 fix
7902
* Set the JS events on the container element
7904
function setDOMEvents() {
7905
var lastWasOutsidePlot = true;
7907
* Record the starting position of a dragoperation
7909
container.onmousedown = function (e) {
7910
e = normalizeMouseEvent(e);
7912
// issue #295, dragging not always working in Firefox
7913
if (!hasTouch && e.preventDefault) {
7917
// record the start position
7918
chart.mouseIsDown = mouseIsDown = true;
7919
chart.mouseDownX = mouseDownX = e.chartX;
7920
mouseDownY = e.chartY;
7922
addEvent(doc, hasTouch ? 'touchend' : 'mouseup', drop);
7925
// The mousemove, touchmove and touchstart event handler
7926
var mouseMove = function (e) {
7928
// let the system handle multitouch operations like two finger scroll
7930
if (e && e.touches && e.touches.length > 1) {
7935
e = normalizeMouseEvent(e);
7936
if (!hasTouch) { // not for touch devices
7937
e.returnValue = false;
7940
var chartX = e.chartX,
7942
isOutsidePlot = !isInsidePlot(chartX - plotLeft, chartY - plotTop);
7944
// on touch devices, only trigger click if a handler is defined
7945
if (hasTouch && e.type === 'touchstart') {
7946
if (attr(e.target, 'isTracker')) {
7947
if (!chart.runTrackerClick) {
7950
} else if (!runChartClick && !isOutsidePlot) {
7955
// cancel on mouse outside
7956
if (isOutsidePlot) {
7958
/*if (!lastWasOutsidePlot) {
7959
// reset the tracker
7963
// drop the selection if any and reset mouseIsDown and hasDragged
7965
if (chartX < plotLeft) {
7967
} else if (chartX > plotLeft + plotWidth) {
7968
chartX = plotLeft + plotWidth;
7971
if (chartY < plotTop) {
7973
} else if (chartY > plotTop + plotHeight) {
7974
chartY = plotTop + plotHeight;
7979
if (mouseIsDown && e.type !== 'touchstart') { // make selection
7981
// determine if the mouse has moved more than 10px
7982
hasDragged = Math.sqrt(
7983
Math.pow(mouseDownX - chartX, 2) +
7984
Math.pow(mouseDownY - chartY, 2)
7986
if (hasDragged > 10) {
7987
var clickedInside = isInsidePlot(mouseDownX - plotLeft, mouseDownY - plotTop);
7990
if (hasCartesianSeries && (zoomX || zoomY) && clickedInside) {
7991
if (!selectionMarker) {
7992
selectionMarker = renderer.rect(
7995
zoomHor ? 1 : plotWidth,
7996
zoomVert ? 1 : plotHeight,
8000
fill: optionsChart.selectionMarkerFill || 'rgba(69,114,167,0.25)',
8007
// adjust the width of the selection marker
8008
if (selectionMarker && zoomHor) {
8009
var xSize = chartX - mouseDownX;
8010
selectionMarker.attr({
8011
width: mathAbs(xSize),
8012
x: (xSize > 0 ? 0 : xSize) + mouseDownX
8015
// adjust the height of the selection marker
8016
if (selectionMarker && zoomVert) {
8017
var ySize = chartY - mouseDownY;
8018
selectionMarker.attr({
8019
height: mathAbs(ySize),
8020
y: (ySize > 0 ? 0 : ySize) + mouseDownY
8025
if (clickedInside && !selectionMarker && optionsChart.panning) {
8030
} else if (!isOutsidePlot) {
8035
lastWasOutsidePlot = isOutsidePlot;
8037
// when outside plot, allow touch-drag by returning true
8038
return isOutsidePlot || !hasCartesianSeries;
8042
* When the mouse enters the container, run mouseMove
8044
container.onmousemove = mouseMove;
8047
* When the mouse leaves the container, hide the tracking (tooltip).
8049
addEvent(container, 'mouseleave', hideTooltipOnMouseLeave);
8051
// issue #149 workaround
8052
// The mouseleave event above does not always fire. Whenever the mouse is moving
8053
// outside the plotarea, hide the tooltip
8054
addEvent(doc, 'mousemove', hideTooltipOnMouseMove);
8056
container.ontouchstart = function (e) {
8057
// For touch devices, use touchmove to zoom
8058
if (zoomX || zoomY) {
8059
container.onmousedown(e);
8061
// Show tooltip and prevent the lower mouse pseudo event
8066
* Allow dragging the finger over the chart to read the values on touch
8069
container.ontouchmove = mouseMove;
8072
* Allow dragging the finger over the chart to read the values on touch
8075
container.ontouchend = function () {
8082
// MooTools 1.2.3 doesn't fire this in IE when using addEvent
8083
container.onclick = function (e) {
8084
var hoverPoint = chart.hoverPoint;
8085
e = normalizeMouseEvent(e);
8087
e.cancelBubble = true; // IE specific
8092
// Detect clicks on trackers or tracker groups, #783
8093
if (hoverPoint && (attr(e.target, 'isTracker') || attr(e.target.parentNode, 'isTracker'))) {
8094
var plotX = hoverPoint.plotX,
8095
plotY = hoverPoint.plotY;
8097
// add page position info
8098
extend(hoverPoint, {
8099
pageX: chartPosition.left + plotLeft +
8100
(inverted ? plotWidth - plotY : plotX),
8101
pageY: chartPosition.top + plotTop +
8102
(inverted ? plotHeight - plotX : plotY)
8105
// the series click event
8106
fireEvent(hoverPoint.series, 'click', extend(e, {
8110
// the point click event
8111
hoverPoint.firePointEvent('click', e);
8114
extend(e, getMouseCoordinates(e));
8116
// fire a click event in the chart
8117
if (isInsidePlot(e.chartX - plotLeft, e.chartY - plotTop)) {
8118
fireEvent(chart, 'click', e);
8124
// reset mouseIsDown and hasDragged
8131
* Destroys the MouseTracker object and disconnects DOM events.
8133
function destroy() {
8134
// Destroy the tracker group element
8135
if (chart.trackerGroup) {
8136
chart.trackerGroup = trackerGroup = chart.trackerGroup.destroy();
8139
removeEvent(container, 'mouseleave', hideTooltipOnMouseLeave);
8140
removeEvent(doc, 'mousemove', hideTooltipOnMouseMove);
8141
container.onclick = container.onmousedown = container.onmousemove = container.ontouchstart = container.ontouchend = container.ontouchmove = null;
8147
if (!trackerGroup) {
8148
chart.trackerGroup = trackerGroup = renderer.g('tracker')
8149
.attr({ zIndex: 9 })
8153
if (options.enabled) {
8154
chart.tooltip = tooltip = Tooltip(options);
8156
// set the fixed interval ticking for the smooth tooltip
8157
tooltipInterval = setInterval(function () {
8166
// expose properties
8170
resetTracker: resetTracker,
8171
normalizeMouseEvent: normalizeMouseEvent,
8179
* The overview of the chart's series
8181
var Legend = function () {
8183
var options = chart.options.legend;
8185
if (!options.enabled) {
8189
var horizontal = options.layout === 'horizontal',
8190
symbolWidth = options.symbolWidth,
8191
symbolPadding = options.symbolPadding,
8193
style = options.style,
8194
itemStyle = options.itemStyle,
8195
itemHoverStyle = options.itemHoverStyle,
8196
itemHiddenStyle = merge(itemStyle, options.itemHiddenStyle),
8197
padding = options.padding || pInt(style.padding),
8199
itemMarginTop = options.itemMarginTop || 0,
8200
itemMarginBottom = options.itemMarginBottom || 0,
8203
initialItemX = 4 + padding + symbolWidth + symbolPadding,
8204
initialItemY = padding + itemMarginTop + y - 5, // 5 is the number of pixels above the text
8210
legendBorderWidth = options.borderWidth,
8211
legendBackgroundColor = options.backgroundColor,
8214
widthOption = options.width,
8215
series = chart.series,
8216
reversedLegend = options.reversed;
8221
* Set the colors for the legend item
8222
* @param {Object} item A Series or Point instance
8223
* @param {Object} visible Dimmed or colored
8225
function colorizeItem(item, visible) {
8226
var legendItem = item.legendItem,
8227
legendLine = item.legendLine,
8228
legendSymbol = item.legendSymbol,
8229
hiddenColor = itemHiddenStyle.color,
8230
textColor = visible ? options.itemStyle.color : hiddenColor,
8231
symbolColor = visible ? item.color : hiddenColor;
8234
legendItem.css({ fill: textColor });
8237
legendLine.attr({ stroke: symbolColor });
8241
stroke: symbolColor,
8248
* Position the legend item
8249
* @param {Object} item A Series or Point instance
8250
* @param {Object} visible Dimmed or colored
8252
function positionItem(item) {
8253
var legendItem = item.legendItem,
8254
legendLine = item.legendLine,
8255
legendItemPos = item._legendItemPos,
8256
itemX = legendItemPos[0],
8257
itemY = legendItemPos[1],
8258
legendSymbol = item.legendSymbol,
8260
checkbox = item.checkbox;
8264
x: ltr ? itemX : legendWidth - itemX,
8269
legendLine.translate(
8270
ltr ? itemX : legendWidth - itemX,
8275
symbolX = itemX + legendSymbol.xOff;
8277
x: ltr ? symbolX : legendWidth - symbolX,
8278
y: itemY + legendSymbol.yOff
8288
* Destroy a single legend item
8289
* @param {Object} item The series or point
8291
function destroyItem(item) {
8292
var checkbox = item.checkbox;
8294
// destroy SVG elements
8295
each(['legendItem', 'legendLine', 'legendSymbol'], function (key) {
8297
item[key].destroy();
8302
discardElement(item.checkbox);
8309
* Destroys the legend.
8311
function destroy() {
8313
box = box.destroy();
8317
legendGroup = legendGroup.destroy();
8322
* Position the checkboxes after the width is determined
8324
function positionCheckboxes() {
8325
each(allItems, function (item) {
8326
var checkbox = item.checkbox,
8327
alignAttr = legendGroup.alignAttr;
8330
left: (alignAttr.translateX + item.legendItemWidth + checkbox.x - 40) + PX,
8331
top: (alignAttr.translateY + checkbox.y - 11) + PX
8338
* Render a single specific legend item
8339
* @param {Object} item A series or point
8341
function renderItem(item) {
8349
li = item.legendItem,
8350
series = item.series || item,
8351
itemOptions = series.options,
8352
strokeWidth = (itemOptions && itemOptions.borderWidth) || 0;
8355
if (!li) { // generate it once, later move it
8357
// let these series types use a simple symbol
8358
simpleSymbol = /^(bar|pie|area|column)$/.test(series.type);
8360
// generate the list item text
8361
item.legendItem = li = renderer.text(
8362
options.labelFormatter.call(item),
8367
.css(item.visible ? itemStyle : itemHiddenStyle)
8368
.on('mouseover', function () {
8369
item.setState(HOVER_STATE);
8370
li.css(itemHoverStyle);
8372
.on('mouseout', function () {
8373
li.css(item.visible ? itemStyle : itemHiddenStyle);
8376
.on('click', function () {
8377
var strLegendItemClick = 'legendItemClick',
8378
fnLegendItemClick = function () {
8382
// click the name or symbol
8383
if (item.firePointEvent) { // point
8384
item.firePointEvent(strLegendItemClick, null, fnLegendItemClick);
8386
fireEvent(item, strLegendItemClick, null, fnLegendItemClick);
8390
align: ltr ? 'left' : 'right',
8396
if (!simpleSymbol && itemOptions && itemOptions.lineWidth) {
8398
'stroke-width': itemOptions.lineWidth,
8401
if (itemOptions.dashStyle) {
8402
attrs.dashstyle = itemOptions.dashStyle;
8404
item.legendLine = renderer.path([
8406
(-symbolWidth - symbolPadding) * (ltr ? 1 : -1),
8409
(-symbolPadding) * (ltr ? 1 : -1),
8416
// draw a simple symbol
8417
if (simpleSymbol) { // bar|pie|area|column
8419
legendSymbol = renderer.rect(
8420
(symbolX = -symbolWidth - symbolPadding),
8426
//'stroke-width': 0,
8428
}).add(legendGroup);
8431
symbolX += symbolWidth;
8434
} else if (itemOptions && itemOptions.marker && itemOptions.marker.enabled) { // draw the marker
8435
radius = itemOptions.marker.radius;
8436
legendSymbol = renderer.symbol(
8438
(symbolX = -symbolWidth / 2 - symbolPadding - radius),
8439
(symbolY = -4 - radius),
8443
.attr(item.pointAttr[NORMAL_STATE])
8444
.attr({ zIndex: 3 })
8448
symbolX += symbolWidth / 2;
8454
legendSymbol.xOff = symbolX + (strokeWidth % 2 / 2);
8455
legendSymbol.yOff = symbolY + (strokeWidth % 2 / 2);
8458
item.legendSymbol = legendSymbol;
8460
// colorize the items
8461
colorizeItem(item, item.visible);
8464
// add the HTML checkbox on top
8465
if (itemOptions && itemOptions.showCheckbox) {
8466
item.checkbox = createElement('input', {
8468
checked: item.selected,
8469
defaultChecked: item.selected // required by IE7
8470
}, options.itemCheckboxStyle, container);
8472
addEvent(item.checkbox, 'click', function (event) {
8473
var target = event.target;
8474
fireEvent(item, 'checkboxClick', {
8475
checked: target.checked
8486
// calculate the positions for the next line
8487
bBox = li.getBBox();
8489
itemWidth = item.legendItemWidth =
8490
options.itemWidth || symbolWidth + symbolPadding + bBox.width + padding;
8491
itemHeight = bBox.height;
8493
// if the item exceeds the width, start a new line
8494
if (horizontal && itemX - initialItemX + itemWidth >
8495
(widthOption || (chartWidth - 2 * padding - initialItemX))) {
8496
itemX = initialItemX;
8497
itemY += itemMarginTop + itemHeight + itemMarginBottom;
8500
// If the item exceeds the height, start a new column
8501
if (!horizontal && itemY + options.y + itemHeight > chartHeight - spacingTop - spacingBottom) {
8502
itemY = initialItemY;
8503
itemX += maxItemWidth;
8507
// Set the edge positions
8508
maxItemWidth = mathMax(maxItemWidth, itemWidth);
8509
lastItemY = mathMax(lastItemY, itemY + itemMarginBottom);
8511
// cache the position of the newly generated or reordered items
8512
item._legendItemPos = [itemX, itemY];
8518
itemY += itemMarginTop + itemHeight + itemMarginBottom;
8521
// the width of the widest item
8522
offsetWidth = widthOption || mathMax(
8523
(itemX - initialItemX) + (horizontal ? 0 : itemWidth),
8530
* Render the legend. This method can be called both before and after
8531
* chart.render. If called after, it will only rearrange items instead
8532
* of creating new ones.
8534
function renderLegend() {
8535
itemX = initialItemX;
8536
itemY = initialItemY;
8541
legendGroup = renderer.g('legend')
8542
// #414, #759. Trackers will be drawn above the legend, but we have
8543
// to sacrifice that because tooltips need to be above the legend
8544
// and trackers above tooltips
8545
.attr({ zIndex: 7 })
8550
// add each series or point
8552
each(series, function (serie) {
8553
var seriesOptions = serie.options;
8555
if (!seriesOptions.showInLegend) {
8559
// use points or series for the legend item depending on legendType
8560
allItems = allItems.concat(
8561
serie.legendItems ||
8562
(seriesOptions.legendType === 'point' ?
8569
// sort by legendIndex
8570
stableSort(allItems, function (a, b) {
8571
return (a.options.legendIndex || 0) - (b.options.legendIndex || 0);
8575
if (reversedLegend) {
8580
each(allItems, renderItem);
8584
legendWidth = widthOption || offsetWidth;
8585
legendHeight = lastItemY - y + itemHeight;
8587
if (legendBorderWidth || legendBackgroundColor) {
8588
legendWidth += 2 * padding;
8589
legendHeight += 2 * padding;
8592
box = renderer.rect(
8597
options.borderRadius,
8598
legendBorderWidth || 0
8600
stroke: options.borderColor,
8601
'stroke-width': legendBorderWidth || 0,
8602
fill: legendBackgroundColor || NONE
8605
.shadow(options.shadow);
8608
} else if (legendWidth > 0 && legendHeight > 0) {
8609
box[box.isNew ? 'attr' : 'animate'](
8610
box.crisp(null, null, null, legendWidth, legendHeight)
8615
// hide the border if no items
8616
box[allItems.length ? 'show' : 'hide']();
8619
// Now that the legend width and height are extablished, put the items in the
8621
each(allItems, positionItem);
8623
// 1.x compatibility: positioning based on style
8624
var props = ['left', 'right', 'top', 'bottom'],
8629
if (style[prop] && style[prop] !== 'auto') {
8630
options[i < 2 ? 'align' : 'verticalAlign'] = prop;
8631
options[i < 2 ? 'x' : 'y'] = pInt(style[prop]) * (i % 2 ? -1 : 1);
8635
if (allItems.length) {
8636
legendGroup.align(extend(options, {
8638
height: legendHeight
8639
}), true, spacingBox);
8643
positionCheckboxes();
8652
addEvent(chart, 'endResize', positionCheckboxes);
8656
colorizeItem: colorizeItem,
8657
destroyItem: destroyItem,
8658
renderLegend: renderLegend,
8669
* Initialize an individual series, called internally before render time
8671
function initSeries(options) {
8672
var type = options.type || optionsChart.type || optionsChart.defaultSeriesType,
8673
typeClass = seriesTypes[type],
8675
hasRendered = chart.hasRendered;
8677
// an inverted chart can't take a column series and vice versa
8679
if (inverted && type === 'column') {
8680
typeClass = seriesTypes.bar;
8681
} else if (!inverted && type === 'bar') {
8682
typeClass = seriesTypes.column;
8686
serie = new typeClass();
8688
serie.init(chart, options);
8690
// set internal chart properties
8691
if (!hasRendered && serie.inverted) {
8694
if (serie.isCartesian) {
8695
hasCartesianSeries = serie.isCartesian;
8704
* Add a series dynamically after time
8706
* @param {Object} options The config options
8707
* @param {Boolean} redraw Whether to redraw the chart after adding. Defaults to true.
8708
* @param {Boolean|Object} animation Whether to apply animation, and optionally animation
8711
* @return {Object} series The newly created series object
8713
function addSeries(options, redraw, animation) {
8717
setAnimation(animation, chart);
8718
redraw = pick(redraw, true); // defaults to true
8720
fireEvent(chart, 'addSeries', { options: options }, function () {
8721
series = initSeries(options);
8722
series.isDirty = true;
8724
chart.isDirtyLegend = true; // the series array is out of sync with the display
8735
* Check whether a given point is within the plot area
8737
* @param {Number} x Pixel x relative to the plot area
8738
* @param {Number} y Pixel y relative to the plot area
8740
isInsidePlot = function (x, y) {
8748
* Adjust all axes tick amounts
8750
function adjustTickAmounts() {
8751
if (optionsChart.alignTicks !== false) {
8752
each(axes, function (axis) {
8753
axis.adjustTickAmount();
8760
* Redraw legend, axes or series based on updated data
8762
* @param {Boolean|Object} animation Whether to apply animation, and optionally animation
8765
function redraw(animation) {
8766
var redrawLegend = chart.isDirtyLegend,
8768
isDirtyBox = chart.isDirtyBox, // todo: check if it has actually changed?
8769
seriesLength = series.length,
8771
clipRect = chart.clipRect,
8774
setAnimation(animation, chart);
8776
// link stacked series
8779
if (serie.isDirty && serie.options.stacking) {
8780
hasStackedSeries = true;
8784
if (hasStackedSeries) { // mark others as dirty
8788
if (serie.options.stacking) {
8789
serie.isDirty = true;
8794
// handle updated data in the series
8795
each(series, function (serie) {
8796
if (serie.isDirty) { // prepare the data so axis can read it
8797
if (serie.options.legendType === 'point') {
8798
redrawLegend = true;
8803
// handle added or removed series
8804
if (redrawLegend && legend.renderLegend) { // series or pie points are added or removed
8805
// draw legend graphics
8806
legend.renderLegend();
8808
chart.isDirtyLegend = false;
8812
if (hasCartesianSeries) {
8819
each(axes, function (axis) {
8823
adjustTickAmounts();
8827
each(axes, function (axis) {
8829
// Fire 'afterSetExtremes' only if extremes are set
8830
if (axis.isDirtyExtremes) { // #821
8831
axis.isDirtyExtremes = false;
8832
fireEvent(axis, 'afterSetExtremes', axis.getExtremes()); // #747, #751
8835
if (axis.isDirty || isDirtyBox) {
8837
isDirtyBox = true; // #792
8844
// the plot areas size has changed
8851
clipRect.animate({ // for chart resize
8852
width: chart.plotSizeX,
8853
height: chart.plotSizeY + 1
8860
// redraw affected series
8861
each(series, function (serie) {
8862
if (serie.isDirty && serie.visible &&
8863
(!serie.isCartesian || serie.xAxis)) { // issue #153
8869
// hide tooltip and hover states
8870
if (tracker && tracker.resetTracker) {
8871
tracker.resetTracker();
8878
fireEvent(chart, 'redraw'); // jQuery breaks this when calling it from addEvent. Overwrites chart.redraw
8884
* Dim the chart and show a loading text or symbol
8885
* @param {String} str An optional text to show in the loading label instead of the default one
8887
function showLoading(str) {
8888
var loadingOptions = options.loading;
8890
// create the layer at the first call
8892
loadingDiv = createElement(DIV, {
8893
className: PREFIX + 'loading'
8894
}, extend(loadingOptions.style, {
8895
left: plotLeft + PX,
8897
width: plotWidth + PX,
8898
height: plotHeight + PX,
8903
loadingSpan = createElement(
8906
loadingOptions.labelStyle,
8913
loadingSpan.innerHTML = str || options.lang.loading;
8916
if (!loadingShown) {
8917
css(loadingDiv, { opacity: 0, display: '' });
8918
animate(loadingDiv, {
8919
opacity: loadingOptions.style.opacity
8921
duration: loadingOptions.showDuration || 0
8923
loadingShown = true;
8927
* Hide the loading layer
8929
function hideLoading() {
8931
animate(loadingDiv, {
8934
duration: options.loading.hideDuration || 100,
8935
complete: function () {
8936
css(loadingDiv, { display: NONE });
8940
loadingShown = false;
8944
* Get an axis, series or point object by id.
8945
* @param id {String} The id as given in the configuration options
8953
for (i = 0; i < axes.length; i++) {
8954
if (axes[i].options.id === id) {
8960
for (i = 0; i < series.length; i++) {
8961
if (series[i].options.id === id) {
8967
for (i = 0; i < series.length; i++) {
8968
points = series[i].points || [];
8969
for (j = 0; j < points.length; j++) {
8970
if (points[j].id === id) {
8979
* Create the Axis instances based on the config options
8981
function getAxes() {
8982
var xAxisOptions = options.xAxis || {},
8983
yAxisOptions = options.yAxis || {},
8987
// make sure the options are arrays and add some members
8988
xAxisOptions = splat(xAxisOptions);
8989
each(xAxisOptions, function (axis, i) {
8994
yAxisOptions = splat(yAxisOptions);
8995
each(yAxisOptions, function (axis, i) {
8999
// concatenate all axis options into one array
9000
optionsArray = xAxisOptions.concat(yAxisOptions);
9002
each(optionsArray, function (axisOptions) {
9003
axis = new Axis(axisOptions);
9006
adjustTickAmounts();
9011
* Get the currently selected points from all series
9013
function getSelectedPoints() {
9015
each(series, function (serie) {
9016
points = points.concat(grep(serie.points, function (point) {
9017
return point.selected;
9024
* Get the currently selected series
9026
function getSelectedSeries() {
9027
return grep(series, function (serie) {
9028
return serie.selected;
9033
* Display the zoom button
9035
function showResetZoom() {
9036
var lang = defaultOptions.lang,
9037
btnOptions = optionsChart.resetZoomButton,
9038
theme = btnOptions.theme,
9039
states = theme.states,
9040
box = btnOptions.relativeTo === 'chart' ? null : {
9046
chart.resetZoomButton = renderer.button(lang.resetZoom, null, null, zoomOut, theme, states && states.hover)
9048
align: btnOptions.position.align,
9049
title: lang.resetZoomTitle
9052
.align(btnOptions.position, false, box);
9058
zoomOut = function () {
9059
var resetZoomButton = chart.resetZoomButton;
9061
fireEvent(chart, 'selection', { resetSelection: true }, zoom);
9062
if (resetZoomButton) {
9063
chart.resetZoomButton = resetZoomButton.destroy();
9067
* Zoom into a given portion of the chart given by axis coordinates
9068
* @param {Object} event
9070
zoom = function (event) {
9072
// add button to reset selection
9075
if (chart.resetZoomEnabled !== false && !chart.resetZoomButton) { // hook for Stock charts etc.
9079
// if zoom is called with no arguments, reset the axes
9080
if (!event || event.resetSelection) {
9081
each(axes, function (axis) {
9082
if (axis.options.zoomEnabled !== false) {
9083
axis.setExtremes(null, null, false);
9087
} else { // else, zoom in on all axes
9088
each(event.xAxis.concat(event.yAxis), function (axisData) {
9089
var axis = axisData.axis;
9091
// don't zoom more than minRange
9092
if (chart.tracker[axis.isXAxis ? 'zoomX' : 'zoomY']) {
9093
axis.setExtremes(axisData.min, axisData.max, false);
9102
pick(optionsChart.animation, chart.pointCount < 100) // animation
9108
* Pan the chart by dragging the mouse across the pane. This function is called
9109
* on mouse move, and the distance to pan is computed from chartX compared to
9110
* the first chartX position in the dragging operation.
9112
chart.pan = function (chartX) {
9114
var xAxis = chart.xAxis[0],
9115
mouseDownX = chart.mouseDownX,
9116
halfPointRange = xAxis.pointRange / 2,
9117
extremes = xAxis.getExtremes(),
9118
newMin = xAxis.translate(mouseDownX - chartX, true) + halfPointRange,
9119
newMax = xAxis.translate(mouseDownX + plotWidth - chartX, true) - halfPointRange,
9120
hoverPoints = chart.hoverPoints;
9122
// remove active points for shared tooltip
9124
each(hoverPoints, function (point) {
9129
if (newMin > mathMin(extremes.dataMin, extremes.min) && newMax < mathMax(extremes.dataMax, extremes.max)) {
9130
xAxis.setExtremes(newMin, newMax, true, false);
9133
chart.mouseDownX = chartX; // set new reference for next run
9134
css(container, { cursor: 'move' });
9138
* Show the title and subtitle of the chart
9140
* @param titleOptions {Object} New title options
9141
* @param subtitleOptions {Object} New subtitle options
9144
function setTitle(titleOptions, subtitleOptions) {
9146
chartTitleOptions = merge(options.title, titleOptions);
9147
chartSubtitleOptions = merge(options.subtitle, subtitleOptions);
9149
// add title and subtitle
9151
['title', titleOptions, chartTitleOptions],
9152
['subtitle', subtitleOptions, chartSubtitleOptions]
9155
title = chart[name],
9156
titleOptions = arr[1],
9157
chartTitleOptions = arr[2];
9159
if (title && titleOptions) {
9160
title = title.destroy(); // remove old
9162
if (chartTitleOptions && chartTitleOptions.text && !title) {
9163
chart[name] = renderer.text(
9164
chartTitleOptions.text,
9167
chartTitleOptions.useHTML
9170
align: chartTitleOptions.align,
9171
'class': PREFIX + name,
9172
zIndex: chartTitleOptions.zIndex || 4
9174
.css(chartTitleOptions.style)
9176
.align(chartTitleOptions, false, spacingBox);
9183
* Get chart width and height according to options and container size
9185
function getChartSize() {
9187
containerWidth = (renderToClone || renderTo).offsetWidth;
9188
containerHeight = (renderToClone || renderTo).offsetHeight;
9189
chart.chartWidth = chartWidth = optionsChart.width || containerWidth || 600;
9190
chart.chartHeight = chartHeight = optionsChart.height ||
9191
// the offsetHeight of an empty container is 0 in standard browsers, but 19 in IE7:
9192
(containerHeight > 19 ? containerHeight : 400);
9197
* Get the containing element, determine the size and create the inner container
9198
* div to hold the chart
9200
function getContainer() {
9201
renderTo = optionsChart.renderTo;
9202
containerId = PREFIX + idCounter++;
9204
if (isString(renderTo)) {
9205
renderTo = doc.getElementById(renderTo);
9208
// Display an error if the renderTo is wrong
9213
// remove previous chart
9214
renderTo.innerHTML = '';
9216
// If the container doesn't have an offsetWidth, it has or is a child of a node
9217
// that has display:none. We need to temporarily move it out to a visible
9218
// state to determine the size, else the legend and tooltips won't render
9220
if (!renderTo.offsetWidth) {
9221
renderToClone = renderTo.cloneNode(0);
9222
css(renderToClone, {
9227
doc.body.appendChild(renderToClone);
9230
// get the width and height
9233
// create the inner container
9234
chart.container = container = createElement(DIV, {
9235
className: PREFIX + 'container' +
9236
(optionsChart.className ? ' ' + optionsChart.className : ''),
9240
overflow: HIDDEN, // needed for context menu (avoid scrollbars) and
9241
// content overflow in IE
9242
width: chartWidth + PX,
9243
height: chartHeight + PX,
9245
lineHeight: 'normal' // #427
9246
}, optionsChart.style),
9247
renderToClone || renderTo
9250
chart.renderer = renderer =
9251
optionsChart.forExport ? // force SVG, used for SVG export
9252
new SVGRenderer(container, chartWidth, chartHeight, true) :
9253
new Renderer(container, chartWidth, chartHeight);
9256
// If we need canvg library, extend and configure the renderer
9257
// to get the tracker for translating mouse events
9258
renderer.create(chart, container, chartWidth, chartHeight);
9261
// Issue 110 workaround:
9262
// In Firefox, if a div is positioned by percentage, its pixel position may land
9263
// between pixels. The container itself doesn't display this, but an SVG element
9264
// inside this container will be drawn at subpixel precision. In order to draw
9265
// sharp lines, this must be compensated for. This doesn't seem to work inside
9266
// iframes though (like in jsFiddle).
9267
var subPixelFix, rect;
9268
if (isFirefox && container.getBoundingClientRect) {
9269
subPixelFix = function () {
9270
css(container, { left: 0, top: 0 });
9271
rect = container.getBoundingClientRect();
9273
left: (-(rect.left - pInt(rect.left))) + PX,
9274
top: (-(rect.top - pInt(rect.top))) + PX
9282
addEvent(win, 'resize', subPixelFix);
9284
// remove it on chart destroy
9285
addEvent(chart, 'destroy', function () {
9286
removeEvent(win, 'resize', subPixelFix);
9292
* Calculate margins by rendering axis labels in a preliminary position. Title,
9293
* subtitle and legend have already been rendered at this stage, but will be
9294
* moved into their final positions
9296
getMargins = function () {
9297
var legendOptions = options.legend,
9298
legendMargin = pick(legendOptions.margin, 10),
9299
legendX = legendOptions.x,
9300
legendY = legendOptions.y,
9301
align = legendOptions.align,
9302
verticalAlign = legendOptions.verticalAlign,
9307
// adjust for title and subtitle
9308
if ((chart.title || chart.subtitle) && !defined(optionsMarginTop)) {
9309
titleOffset = mathMax(
9310
(chart.title && !chartTitleOptions.floating && !chartTitleOptions.verticalAlign && chartTitleOptions.y) || 0,
9311
(chart.subtitle && !chartSubtitleOptions.floating && !chartSubtitleOptions.verticalAlign && chartSubtitleOptions.y) || 0
9314
plotTop = mathMax(plotTop, titleOffset + pick(chartTitleOptions.margin, 15) + spacingTop);
9317
// adjust for legend
9318
if (legendOptions.enabled && !legendOptions.floating) {
9319
if (align === 'right') { // horizontal alignment handled first
9320
if (!defined(optionsMarginRight)) {
9321
marginRight = mathMax(
9323
legendWidth - legendX + legendMargin + spacingRight
9326
} else if (align === 'left') {
9327
if (!defined(optionsMarginLeft)) {
9330
legendWidth + legendX + legendMargin + spacingLeft
9334
} else if (verticalAlign === 'top') {
9335
if (!defined(optionsMarginTop)) {
9338
legendHeight + legendY + legendMargin + spacingTop
9342
} else if (verticalAlign === 'bottom') {
9343
if (!defined(optionsMarginBottom)) {
9344
marginBottom = mathMax(
9346
legendHeight - legendY + legendMargin + spacingBottom
9352
// adjust for scroller
9353
if (chart.extraBottomMargin) {
9354
marginBottom += chart.extraBottomMargin;
9356
if (chart.extraTopMargin) {
9357
plotTop += chart.extraTopMargin;
9360
// pre-render axes to get labels offset width
9361
if (hasCartesianSeries) {
9362
each(axes, function (axis) {
9367
if (!defined(optionsMarginLeft)) {
9368
plotLeft += axisOffset[3];
9370
if (!defined(optionsMarginTop)) {
9371
plotTop += axisOffset[0];
9373
if (!defined(optionsMarginBottom)) {
9374
marginBottom += axisOffset[2];
9376
if (!defined(optionsMarginRight)) {
9377
marginRight += axisOffset[1];
9385
* Add the event handlers necessary for auto resizing
9388
function initReflow() {
9390
function reflow(e) {
9391
var width = optionsChart.width || renderTo.offsetWidth,
9392
height = optionsChart.height || renderTo.offsetHeight,
9393
target = e ? e.target : win; // #805 - MooTools doesn't supply e
9395
// Width and height checks for display:none. Target is doc in IE8 and Opera,
9396
// win in Firefox, Chrome and IE9.
9397
if (width && height && (target === win || target === doc)) {
9399
if (width !== containerWidth || height !== containerHeight) {
9400
clearTimeout(reflowTimeout);
9401
reflowTimeout = setTimeout(function () {
9402
resize(width, height, false);
9405
containerWidth = width;
9406
containerHeight = height;
9409
addEvent(win, 'resize', reflow);
9410
addEvent(chart, 'destroy', function () {
9411
removeEvent(win, 'resize', reflow);
9416
* Fires endResize event on chart instance.
9418
function fireEndResize() {
9420
fireEvent(chart, 'endResize', null, function () {
9427
* Resize the chart to a given width and height
9428
* @param {Number} width
9429
* @param {Number} height
9430
* @param {Object|Boolean} animation
9432
resize = function (width, height, animation) {
9433
var chartTitle = chart.title,
9434
chartSubtitle = chart.subtitle;
9438
// set the animation for the current process
9439
setAnimation(animation, chart);
9441
oldChartHeight = chartHeight;
9442
oldChartWidth = chartWidth;
9443
if (defined(width)) {
9444
chart.chartWidth = chartWidth = mathRound(width);
9446
if (defined(height)) {
9447
chart.chartHeight = chartHeight = mathRound(height);
9451
width: chartWidth + PX,
9452
height: chartHeight + PX
9454
renderer.setSize(chartWidth, chartHeight, animation);
9456
// update axis lengths for more correct tick intervals:
9457
plotWidth = chartWidth - plotLeft - marginRight;
9458
plotHeight = chartHeight - plotTop - marginBottom;
9462
each(axes, function (axis) {
9463
axis.isDirty = true;
9467
// make sure non-cartesian series are also handled
9468
each(series, function (serie) {
9469
serie.isDirty = true;
9472
chart.isDirtyLegend = true; // force legend redraw
9473
chart.isDirtyBox = true; // force redraw of plot and chart border
9479
chartTitle.align(null, null, spacingBox);
9481
if (chartSubtitle) {
9482
chartSubtitle.align(null, null, spacingBox);
9488
oldChartHeight = null;
9489
fireEvent(chart, 'resize');
9491
// fire endResize and set isResizing back
9492
// If animation is disabled, fire without delay
9493
if (globalAnimation === false) {
9495
} else { // else set a timeout with the animation duration
9496
setTimeout(fireEndResize, (globalAnimation && globalAnimation.duration) || 500);
9501
* Set the public chart properties. This is done before and after the pre-render
9502
* to determine margin sizes
9504
setChartSize = function () {
9506
chart.plotLeft = plotLeft = mathRound(plotLeft);
9507
chart.plotTop = plotTop = mathRound(plotTop);
9508
chart.plotWidth = plotWidth = mathRound(chartWidth - plotLeft - marginRight);
9509
chart.plotHeight = plotHeight = mathRound(chartHeight - plotTop - marginBottom);
9511
chart.plotSizeX = inverted ? plotHeight : plotWidth;
9512
chart.plotSizeY = inverted ? plotWidth : plotHeight;
9517
width: chartWidth - spacingLeft - spacingRight,
9518
height: chartHeight - spacingTop - spacingBottom
9521
each(axes, function (axis) {
9523
axis.setAxisTranslation();
9528
* Initial margins before auto size margins are applied
9530
resetMargins = function () {
9531
plotTop = pick(optionsMarginTop, spacingTop);
9532
marginRight = pick(optionsMarginRight, spacingRight);
9533
marginBottom = pick(optionsMarginBottom, spacingBottom);
9534
plotLeft = pick(optionsMarginLeft, spacingLeft);
9535
axisOffset = [0, 0, 0, 0]; // top, right, bottom, left
9539
* Draw the borders and backgrounds for chart and plot area
9541
drawChartBox = function () {
9542
var chartBorderWidth = optionsChart.borderWidth || 0,
9543
chartBackgroundColor = optionsChart.backgroundColor,
9544
plotBackgroundColor = optionsChart.plotBackgroundColor,
9545
plotBackgroundImage = optionsChart.plotBackgroundImage,
9555
mgn = chartBorderWidth + (optionsChart.shadow ? 8 : 0);
9557
if (chartBorderWidth || chartBackgroundColor) {
9558
if (!chartBackground) {
9559
chartBackground = renderer.rect(mgn / 2, mgn / 2, chartWidth - mgn, chartHeight - mgn,
9560
optionsChart.borderRadius, chartBorderWidth)
9562
stroke: optionsChart.borderColor,
9563
'stroke-width': chartBorderWidth,
9564
fill: chartBackgroundColor || NONE
9567
.shadow(optionsChart.shadow);
9569
chartBackground.animate(
9570
chartBackground.crisp(null, null, null, chartWidth - mgn, chartHeight - mgn)
9577
if (plotBackgroundColor) {
9578
if (!plotBackground) {
9579
plotBackground = renderer.rect(plotLeft, plotTop, plotWidth, plotHeight, 0)
9581
fill: plotBackgroundColor
9584
.shadow(optionsChart.plotShadow);
9586
plotBackground.animate(plotSize);
9589
if (plotBackgroundImage) {
9591
plotBGImage = renderer.image(plotBackgroundImage, plotLeft, plotTop, plotWidth, plotHeight)
9594
plotBGImage.animate(plotSize);
9599
if (optionsChart.plotBorderWidth) {
9601
plotBorder = renderer.rect(plotLeft, plotTop, plotWidth, plotHeight, 0, optionsChart.plotBorderWidth)
9603
stroke: optionsChart.plotBorderColor,
9604
'stroke-width': optionsChart.plotBorderWidth,
9610
plotBorder.crisp(null, plotLeft, plotTop, plotWidth, plotHeight)
9616
chart.isDirtyBox = false;
9620
* Detect whether the chart is inverted, either by setting the chart.inverted option
9621
* or adding a bar series to the configuration options
9623
function setInverted() {
9626
inverted || // it is set before
9627
optionsChart.inverted ||
9628
optionsChart.type === BAR || // default series type
9629
optionsChart.defaultSeriesType === BAR // backwards compatible
9631
seriesOptions = options.series,
9632
i = seriesOptions && seriesOptions.length;
9634
// check if a bar series is present in the config options
9635
while (!isInverted && i--) {
9636
if (seriesOptions[i].type === BAR) {
9641
// set the chart property and the chart scope variable
9642
chart.inverted = inverted = isInverted;
9646
* Render all graphics for the chart
9649
var labels = options.labels,
9650
credits = options.credits,
9658
legend = chart.legend = new Legend();
9660
// Get margins by pre-rendering axes
9662
each(axes, function (axis) {
9666
each(axes, function (axis) {
9667
axis.setTickPositions(true); // update to reflect the new margins
9669
adjustTickAmounts();
9670
getMargins(); // second pass to check for new labels
9673
// Draw the borders and backgrounds
9677
if (hasCartesianSeries) {
9678
each(axes, function (axis) {
9685
if (!chart.seriesGroup) {
9686
chart.seriesGroup = renderer.g('series-group')
9687
.attr({ zIndex: 3 })
9690
each(series, function (serie) {
9692
serie.setTooltipPoints();
9699
each(labels.items, function () {
9700
var style = extend(labels.style, this.style),
9701
x = pInt(style.left) + plotLeft,
9702
y = pInt(style.top) + plotTop + 12;
9704
// delete to prevent rewriting in IE
9713
.attr({ zIndex: 2 })
9721
if (credits.enabled && !chart.credits) {
9722
creditsHref = credits.href;
9723
chart.credits = renderer.text(
9728
.on('click', function () {
9730
location.href = creditsHref;
9734
align: credits.position.align,
9739
.align(credits.position);
9743
chart.hasRendered = true;
9748
* Clean up memory usage
9750
function destroy() {
9752
parentNode = container && container.parentNode;
9754
// If the chart is destroyed already, do nothing.
9755
// This will happen if if a script invokes chart.destroy and
9756
// then it will be called again on win.unload
9757
if (chart === null) {
9761
// fire the chart.destoy event
9762
fireEvent(chart, 'destroy');
9767
// ==== Destroy collections:
9771
axes[i] = axes[i].destroy();
9774
// Destroy each series
9777
series[i] = series[i].destroy();
9780
// ==== Destroy chart properties:
9781
each(['title', 'subtitle', 'seriesGroup', 'clipRect', 'credits', 'tracker', 'scroller', 'rangeSelector'], function (name) {
9782
var prop = chart[name];
9785
chart[name] = prop.destroy();
9789
// ==== Destroy local variables:
9790
each([chartBackground, plotBorder, plotBackground, legend, tooltip, renderer, tracker], function (obj) {
9791
if (obj && obj.destroy) {
9795
chartBackground = plotBorder = plotBackground = legend = tooltip = renderer = tracker = null;
9797
// remove container and all SVG
9798
if (container) { // can break in IE when destroyed before finished loading
9799
container.innerHTML = '';
9800
removeEvent(container);
9802
discardElement(container);
9809
// memory and CPU leak
9810
clearInterval(tooltipInterval);
9821
* Prepare for first rendering after all data are loaded
9823
function firstRender() {
9824
// VML namespaces can't be added until after complete. Listening
9825
// for Perini's doScroll hack is not enough.
9826
var ONREADYSTATECHANGE = 'onreadystatechange',
9827
COMPLETE = 'complete';
9828
// Note: in spite of JSLint's complaints, win == win.top is required
9829
/*jslint eqeq: true*/
9830
if ((!hasSVG && (win == win.top && doc.readyState !== COMPLETE)) || (useCanVG && !win.canvg)) {
9831
/*jslint eqeq: false*/
9833
// Delay rendering until canvg library is downloaded and ready
9834
CanVGController.push(firstRender, options.global.canvasToolsURL);
9836
doc.attachEvent(ONREADYSTATECHANGE, function () {
9837
doc.detachEvent(ONREADYSTATECHANGE, firstRender);
9838
if (doc.readyState === COMPLETE) {
9846
// create the container
9849
// Run an early event after the container and renderer are established
9850
fireEvent(chart, 'init');
9852
// Initialize range selector for stock charts
9853
if (Highcharts.RangeSelector && options.rangeSelector.enabled) {
9854
chart.rangeSelector = new Highcharts.RangeSelector(chart);
9860
// Set the common inversion and transformation for inverted series after initSeries
9866
// Initialize the series
9867
each(options.series || [], function (serieOptions) {
9868
initSeries(serieOptions);
9871
// Run an event where series and axes can be added
9872
//fireEvent(chart, 'beforeRender');
9874
// Initialize scroller for stock charts
9875
if (Highcharts.Scroller && (options.navigator.enabled || options.scrollbar.enabled)) {
9876
chart.scroller = new Highcharts.Scroller(chart);
9879
chart.render = render;
9881
// depends on inverted and on margins being set
9882
chart.tracker = tracker = new MouseTracker(options.tooltip);
9891
callback.apply(chart, [chart]);
9893
each(chart.callbacks, function (fn) {
9894
fn.apply(chart, [chart]);
9898
// If the chart was rendered outside the top container, put it back in
9899
if (renderToClone) {
9900
renderTo.appendChild(container);
9901
discardElement(renderToClone);
9904
fireEvent(chart, 'load');
9910
// Set up auto resize
9911
if (optionsChart.reflow !== false) {
9912
addEvent(chart, 'load', initReflow);
9915
// Chart event handlers
9917
for (eventType in chartEvents) {
9918
addEvent(chart, eventType, chartEvents[eventType]);
9923
chart.options = options;
9924
chart.series = series;
9933
// Expose methods and variables
9934
chart.addSeries = addSeries;
9935
chart.animation = useCanVG ? false : pick(optionsChart.animation, true);
9937
chart.destroy = destroy;
9939
chart.getSelectedPoints = getSelectedPoints;
9940
chart.getSelectedSeries = getSelectedSeries;
9941
chart.hideLoading = hideLoading;
9942
chart.initSeries = initSeries;
9943
chart.isInsidePlot = isInsidePlot;
9944
chart.redraw = redraw;
9945
chart.setSize = resize;
9946
chart.setTitle = setTitle;
9947
chart.showLoading = showLoading;
9948
chart.pointCount = 0;
9949
chart.counters = new ChartCounters();
9951
if ($) $(function () {
9952
$container = $('#container');
9956
$('<button>+</button>')
9957
.insertBefore($container)
9958
.click(function () {
9959
if (origChartWidth === UNDEFINED) {
9960
origChartWidth = chartWidth;
9961
origChartHeight = chartHeight;
9963
chart.resize(chartWidth *= 1.1, chartHeight *= 1.1);
9965
$('<button>-</button>')
9966
.insertBefore($container)
9967
.click(function () {
9968
if (origChartWidth === UNDEFINED) {
9969
origChartWidth = chartWidth;
9970
origChartHeight = chartHeight;
9972
chart.resize(chartWidth *= 0.9, chartHeight *= 0.9);
9974
$('<button>1:1</button>')
9975
.insertBefore($container)
9976
.click(function () {
9977
if (origChartWidth === UNDEFINED) {
9978
origChartWidth = chartWidth;
9979
origChartHeight = chartHeight;
9981
chart.resize(origChartWidth, origChartHeight);
9995
// Hook for exporting module
9996
Chart.prototype.callbacks = [];
9998
* The Point object and prototype. Inheritable and used as base for PiePoint
10000
var Point = function () {};
10001
Point.prototype = {
10004
* Initialize the point
10005
* @param {Object} series The series object containing this point
10006
* @param {Object} options The data in either number, array or object format
10008
init: function (series, options, x) {
10010
counters = series.chart.counters,
10012
point.series = series;
10013
point.applyOptions(options, x);
10014
point.pointAttr = {};
10016
if (series.options.colorByPoint) {
10017
defaultColors = series.chart.options.colors;
10018
if (!point.options) {
10019
point.options = {};
10021
point.color = point.options.color = point.color || defaultColors[counters.color++];
10023
// loop back to zero
10024
counters.wrapColor(defaultColors.length);
10027
series.chart.pointCount++;
10031
* Apply the options containing the x and y data and possible some extra properties.
10032
* This is called on point init or from point.update.
10034
* @param {Object} options
10036
applyOptions: function (options, x) {
10038
series = point.series,
10039
optionsType = typeof options;
10041
point.config = options;
10043
// onedimensional array input
10044
if (optionsType === 'number' || options === null) {
10046
} else if (typeof options[0] === 'number') { // two-dimentional array
10047
point.x = options[0];
10048
point.y = options[1];
10049
} else if (optionsType === 'object' && typeof options.length !== 'number') { // object input
10050
// copy options directly to point
10051
extend(point, options);
10052
point.options = options;
10054
// This is the fastest way to detect if there are individual point dataLabels that need
10055
// to be considered in drawDataLabels. These can only occur in object configs.
10056
if (options.dataLabels) {
10057
series._hasPointLabels = true;
10059
} else if (typeof options[0] === 'string') { // categorized data with name in first position
10060
point.name = options[0];
10061
point.y = options[1];
10065
* If no x is set by now, get auto incremented value. All points must have an
10066
* x value, however the y value can be null to create a gap in the series
10068
// todo: skip this? It is only used in applyOptions, in translate it should not be used
10069
if (point.x === UNDEFINED) {
10070
point.x = x === UNDEFINED ? series.autoIncrement() : x;
10078
* Destroy a point to clear memory. Its reference still stays in series.data.
10080
destroy: function () {
10082
series = point.series,
10083
hoverPoints = series.chart.hoverPoints,
10086
series.chart.pointCount--;
10090
erase(hoverPoints, point);
10092
if (point === series.chart.hoverPoint) {
10093
point.onMouseOut();
10095
series.chart.hoverPoints = null;
10097
// remove all events
10098
if (point.graphic || point.dataLabel) { // removeEvent and destroyElements are performance expensive
10099
removeEvent(point);
10100
point.destroyElements();
10103
if (point.legendItem) { // pies have legend items
10104
point.series.chart.legend.destroyItem(point);
10107
for (prop in point) {
10108
point[prop] = null;
10115
* Destroy SVG elements associated with the point
10117
destroyElements: function () {
10119
props = ['graphic', 'tracker', 'dataLabel', 'group', 'connector', 'shadowGroup'],
10125
point[prop] = point[prop].destroy();
10131
* Return the configuration hash needed for the data label and tooltip formatters
10133
getLabelConfig: function () {
10138
key: point.name || point.category,
10139
series: point.series,
10141
percentage: point.percentage,
10142
total: point.total || point.stackTotal
10147
* Toggle the selection status of a point
10148
* @param {Boolean} selected Whether to select or unselect the point.
10149
* @param {Boolean} accumulate Whether to add to the previous selection. By default,
10150
* this happens if the control key (Cmd on Mac) was pressed during clicking.
10152
select: function (selected, accumulate) {
10154
series = point.series,
10155
chart = series.chart;
10157
selected = pick(selected, !point.selected);
10159
// fire the event with the defalut handler
10160
point.firePointEvent(selected ? 'select' : 'unselect', { accumulate: accumulate }, function () {
10161
point.selected = selected;
10162
point.setState(selected && SELECT_STATE);
10164
// unselect all other points unless Ctrl or Cmd + click
10166
each(chart.getSelectedPoints(), function (loopPoint) {
10167
if (loopPoint.selected && loopPoint !== point) {
10168
loopPoint.selected = false;
10169
loopPoint.setState(NORMAL_STATE);
10170
loopPoint.firePointEvent('unselect');
10177
onMouseOver: function () {
10179
series = point.series,
10180
chart = series.chart,
10181
tooltip = chart.tooltip,
10182
hoverPoint = chart.hoverPoint;
10184
// set normal state to previous series
10185
if (hoverPoint && hoverPoint !== point) {
10186
hoverPoint.onMouseOut();
10189
// trigger the event
10190
point.firePointEvent('mouseOver');
10192
// update the tooltip
10193
if (tooltip && (!tooltip.shared || series.noSharedTooltip)) {
10194
tooltip.refresh(point);
10198
point.setState(HOVER_STATE);
10199
chart.hoverPoint = point;
10202
onMouseOut: function () {
10204
point.firePointEvent('mouseOut');
10207
point.series.chart.hoverPoint = null;
10211
* Extendable method for formatting each point's tooltip line
10213
* @return {String} A string to be concatenated in to the common tooltip text
10215
tooltipFormatter: function (pointFormat) {
10217
series = point.series,
10218
seriesTooltipOptions = series.tooltipOptions,
10219
split = String(point.y).split('.'),
10220
originalDecimals = split[1] ? split[1].length : 0,
10221
match = pointFormat.match(/\{(series|point)\.[a-zA-Z]+\}/g),
10222
splitter = /[{\.}]/,
10230
// loop over the variables defined on the form {series.name}, {point.y} etc
10233
if (isString(key) && key !== pointFormat) { // IE matches more than just the variables
10235
// Split it further into parts
10236
parts = (' ' + key).split(splitter); // add empty string because IE and the rest handles it differently
10237
obj = { 'point': point, 'series': series }[parts[1]];
10240
// Add some preformatting
10241
if (obj === point && (prop === 'y' || prop === 'open' || prop === 'high' ||
10242
prop === 'low' || prop === 'close')) {
10243
replacement = (seriesTooltipOptions.valuePrefix || seriesTooltipOptions.yPrefix || '') +
10244
numberFormat(point[prop], pick(seriesTooltipOptions.valueDecimals, seriesTooltipOptions.yDecimals, originalDecimals)) +
10245
(seriesTooltipOptions.valueSuffix || seriesTooltipOptions.ySuffix || '');
10247
// Automatic replacement
10249
replacement = obj[prop];
10252
pointFormat = pointFormat.replace(key, replacement);
10256
return pointFormat;
10260
* Update the point with new options (typically x/y data) and optionally redraw the series.
10262
* @param {Object} options Point options as defined in the series.data array
10263
* @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
10264
* @param {Boolean|Object} animation Whether to apply animation, and optionally animation
10268
update: function (options, redraw, animation) {
10270
series = point.series,
10271
graphic = point.graphic,
10273
data = series.data,
10274
dataLength = data.length,
10275
chart = series.chart;
10277
redraw = pick(redraw, true);
10279
// fire the event with a default handler of doing the update
10280
point.firePointEvent('update', { options: options }, function () {
10282
point.applyOptions(options);
10285
if (isObject(options)) {
10286
series.getAttribs();
10288
graphic.attr(point.pointAttr[series.state]);
10292
// record changes in the parallel arrays
10293
for (i = 0; i < dataLength; i++) {
10294
if (data[i] === point) {
10295
series.xData[i] = point.x;
10296
series.yData[i] = point.y;
10297
series.options.data[i] = options;
10303
series.isDirty = true;
10304
series.isDirtyData = true;
10306
chart.redraw(animation);
10312
* Remove a point and optionally redraw the series and if necessary the axes
10313
* @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
10314
* @param {Boolean|Object} animation Whether to apply animation, and optionally animation
10317
remove: function (redraw, animation) {
10319
series = point.series,
10320
chart = series.chart,
10322
data = series.data,
10323
dataLength = data.length;
10325
setAnimation(animation, chart);
10326
redraw = pick(redraw, true);
10328
// fire the event with a default handler of removing the point
10329
point.firePointEvent('remove', null, function () {
10331
//erase(series.data, point);
10333
for (i = 0; i < dataLength; i++) {
10334
if (data[i] === point) {
10336
// splice all the parallel arrays
10338
series.options.data.splice(i, 1);
10339
series.xData.splice(i, 1);
10340
series.yData.splice(i, 1);
10349
series.isDirty = true;
10350
series.isDirtyData = true;
10360
* Fire an event on the Point object. Must not be renamed to fireEvent, as this
10361
* causes a name clash in MooTools
10362
* @param {String} eventType
10363
* @param {Object} eventArgs Additional event arguments
10364
* @param {Function} defaultFunction Default event handler
10366
firePointEvent: function (eventType, eventArgs, defaultFunction) {
10368
series = this.series,
10369
seriesOptions = series.options;
10371
// load event handlers on demand to save time on mouseover/out
10372
if (seriesOptions.point.events[eventType] || (point.options && point.options.events && point.options.events[eventType])) {
10373
this.importEvents();
10376
// add default handler if in selection mode
10377
if (eventType === 'click' && seriesOptions.allowPointSelect) {
10378
defaultFunction = function (event) {
10379
// Control key is for Windows, meta (= Cmd key) for Mac, Shift for Opera
10380
point.select(null, event.ctrlKey || event.metaKey || event.shiftKey);
10384
fireEvent(this, eventType, eventArgs, defaultFunction);
10387
* Import events from the series' and point's options. Only do it on
10388
* demand, to save processing time on hovering.
10390
importEvents: function () {
10391
if (!this.hasImportedEvents) {
10393
options = merge(point.series.options.point, point.options),
10394
events = options.events,
10397
point.events = events;
10399
for (eventType in events) {
10400
addEvent(point, eventType, events[eventType]);
10402
this.hasImportedEvents = true;
10408
* Set the point's state
10409
* @param {String} state
10411
setState: function (state) {
10413
plotX = point.plotX,
10414
plotY = point.plotY,
10415
series = point.series,
10416
stateOptions = series.options.states,
10417
markerOptions = defaultPlotOptions[series.type].marker && series.options.marker,
10418
normalDisabled = markerOptions && !markerOptions.enabled,
10419
markerStateOptions = markerOptions && markerOptions.states[state],
10420
stateDisabled = markerStateOptions && markerStateOptions.enabled === false,
10421
stateMarkerGraphic = series.stateMarkerGraphic,
10422
chart = series.chart,
10424
pointAttr = point.pointAttr;
10426
state = state || NORMAL_STATE; // empty string
10429
// already has this state
10430
state === point.state ||
10431
// selected points don't respond to hover
10432
(point.selected && state !== SELECT_STATE) ||
10433
// series' state options is disabled
10434
(stateOptions[state] && stateOptions[state].enabled === false) ||
10435
// point marker's state options is disabled
10436
(state && (stateDisabled || (normalDisabled && !markerStateOptions.enabled)))
10442
// apply hover styles to the existing point
10443
if (point.graphic) {
10444
radius = markerOptions && point.graphic.symbolName && pointAttr[state].r;
10445
point.graphic.attr(merge(
10447
radius ? { // new symbol attributes (#507, #612)
10455
// if a graphic is not applied to each point in the normal state, create a shared
10456
// graphic for the hover state
10458
if (!stateMarkerGraphic) {
10459
radius = markerOptions.radius;
10460
series.stateMarkerGraphic = stateMarkerGraphic = chart.renderer.symbol(
10467
.attr(pointAttr[state])
10468
.add(series.group);
10471
stateMarkerGraphic.translate(
10477
if (stateMarkerGraphic) {
10478
stateMarkerGraphic[state ? 'show' : 'hide']();
10482
point.state = state;
10487
* @classDescription The base function which all other series types inherit from. The data in the series is stored
10488
* in various arrays.
10490
* - First, series.options.data contains all the original config options for
10491
* each point whether added by options or methods like series.addPoint.
10492
* - Next, series.data contains those values converted to points, but in case the series data length
10493
* exceeds the cropThreshold, or if the data is grouped, series.data doesn't contain all the points. It
10494
* only contains the points that have been created on demand.
10495
* - Then there's series.points that contains all currently visible point objects. In case of cropping,
10496
* the cropped-away points are not part of this array. The series.points array starts at series.cropStart
10497
* compared to series.data and series.options.data. If however the series data is grouped, these can't
10498
* be correlated one to one.
10499
* - series.xData and series.processedXData contain clean x values, equivalent to series.data and series.points.
10500
* - series.yData and series.processedYData contain clean x values, equivalent to series.data and series.points.
10502
* @param {Object} chart
10503
* @param {Object} options
10505
var Series = function () {};
10507
Series.prototype = {
10512
sorted: true, // requires the data to be sorted
10513
pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
10514
stroke: 'lineColor',
10515
'stroke-width': 'lineWidth',
10519
init: function (chart, options) {
10524
index = chart.series.length;
10526
series.chart = chart;
10527
series.options = options = series.setOptions(options); // merge with plotOptions
10532
// set some variables
10535
name: options.name || 'Series ' + (index + 1),
10536
state: NORMAL_STATE,
10538
visible: options.visible !== false, // true by default
10539
selected: options.selected === true // false by default
10544
options.animation = false;
10547
// register event listeners
10548
events = options.events;
10549
for (eventType in events) {
10550
addEvent(series, eventType, events[eventType]);
10553
(events && events.click) ||
10554
(options.point && options.point.events && options.point.events.click) ||
10555
options.allowPointSelect
10557
chart.runTrackerClick = true;
10561
series.getSymbol();
10564
series.setData(options.data, false);
10571
* Set the xAxis and yAxis properties of cartesian series, and register the series
10572
* in the axis.series array
10574
bindAxes: function () {
10576
seriesOptions = series.options,
10577
chart = series.chart,
10580
if (series.isCartesian) {
10582
each(['xAxis', 'yAxis'], function (AXIS) { // repeat for xAxis and yAxis
10584
each(chart[AXIS], function (axis) { // loop through the chart's axis objects
10586
axisOptions = axis.options;
10588
// apply if the series xAxis or yAxis option mathches the number of the
10589
// axis, or if undefined, use the first axis
10590
if ((seriesOptions[AXIS] === axisOptions.index) ||
10591
(seriesOptions[AXIS] === UNDEFINED && axisOptions.index === 0)) {
10593
// register this series in the axis.series lookup
10594
axis.series.push(series);
10596
// set this series.xAxis or series.yAxis reference
10597
series[AXIS] = axis;
10599
// mark dirty for redraw
10600
axis.isDirty = true;
10610
* Return an auto incremented x value based on the pointStart and pointInterval options.
10611
* This is only used if an x value is not given for the point that calls autoIncrement.
10613
autoIncrement: function () {
10615
options = series.options,
10616
xIncrement = series.xIncrement;
10618
xIncrement = pick(xIncrement, options.pointStart, 0);
10620
series.pointInterval = pick(series.pointInterval, options.pointInterval, 1);
10622
series.xIncrement = xIncrement + series.pointInterval;
10627
* Divide the series data into segments divided by null values.
10629
getSegments: function () {
10634
points = series.points,
10635
pointsLength = points.length;
10637
if (pointsLength) { // no action required for []
10639
// if connect nulls, just remove null points
10640
if (series.options.connectNulls) {
10643
if (points[i].y === null) {
10644
points.splice(i, 1);
10647
if (points.length) {
10648
segments = [points];
10651
// else, split on null points
10653
each(points, function (point, i) {
10654
if (point.y === null) {
10655
if (i > lastNull + 1) {
10656
segments.push(points.slice(lastNull + 1, i));
10659
} else if (i === pointsLength - 1) { // last value
10660
segments.push(points.slice(lastNull + 1, i + 1));
10667
series.segments = segments;
10670
* Set the series options by merging from the options tree
10671
* @param {Object} itemOptions
10673
setOptions: function (itemOptions) {
10675
chart = series.chart,
10676
chartOptions = chart.options,
10677
plotOptions = chartOptions.plotOptions,
10678
data = itemOptions.data,
10681
itemOptions.data = null; // remove from merge to prevent looping over the data set
10684
plotOptions[this.type],
10685
plotOptions.series,
10689
// Re-insert the data array to the options and the original config (#717)
10690
options.data = itemOptions.data = data;
10692
// the tooltip options are merged between global and series specific options
10693
series.tooltipOptions = merge(chartOptions.tooltip, options.tooltip);
10699
* Get the series' color
10701
getColor: function () {
10702
var defaultColors = this.chart.options.colors,
10703
counters = this.chart.counters;
10704
this.color = this.options.color || defaultColors[counters.color++] || '#0000ff';
10705
counters.wrapColor(defaultColors.length);
10708
* Get the series' symbol
10710
getSymbol: function () {
10712
seriesMarkerOption = series.options.marker,
10713
chart = series.chart,
10714
defaultSymbols = chart.options.symbols,
10715
counters = chart.counters;
10716
series.symbol = seriesMarkerOption.symbol || defaultSymbols[counters.symbol++];
10718
// don't substract radius in image symbols (#604)
10719
if (/^url/.test(series.symbol)) {
10720
seriesMarkerOption.radius = 0;
10722
counters.wrapSymbol(defaultSymbols.length);
10726
* Add a point dynamically after chart load time
10727
* @param {Object} options Point options as given in series.data
10728
* @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
10729
* @param {Boolean} shift If shift is true, a point is shifted off the start
10730
* of the series as one is appended to the end.
10731
* @param {Boolean|Object} animation Whether to apply animation, and optionally animation
10734
addPoint: function (options, redraw, shift, animation) {
10736
data = series.data,
10737
graph = series.graph,
10738
area = series.area,
10739
chart = series.chart,
10740
xData = series.xData,
10741
yData = series.yData,
10742
currentShift = (graph && graph.shift) || 0,
10743
dataOptions = series.options.data,
10745
//point = (new series.pointClass()).init(series, options);
10747
setAnimation(animation, chart);
10749
// Make graph animate sideways
10750
if (graph && shift) {
10751
graph.shift = currentShift + 1;
10754
if (shift) { // #780
10755
area.shift = currentShift + 1;
10757
area.isArea = true; // needed in animation, both with and without shift
10760
// Optional redraw, defaults to true
10761
redraw = pick(redraw, true);
10763
// Get options and push the point to xData, yData and series.options. In series.generatePoints
10764
// the Point instance will be created on demand and pushed to the series.data array.
10765
point = { series: series };
10766
series.pointClass.prototype.applyOptions.apply(point, [options]);
10767
xData.push(point.x);
10768
yData.push(series.valueCount === 4 ? [point.open, point.high, point.low, point.close] : point.y);
10769
dataOptions.push(options);
10772
// Shift the first point off the parallel arrays
10773
// todo: consider series.removePoint(i) method
10776
data[0].remove(false);
10781
dataOptions.shift();
10784
series.getAttribs();
10787
series.isDirty = true;
10788
series.isDirtyData = true;
10795
* Replace the series data with a new set of data
10796
* @param {Object} data
10797
* @param {Object} redraw
10799
setData: function (data, redraw) {
10801
oldData = series.points,
10802
options = series.options,
10803
initialColor = series.initialColor,
10804
chart = series.chart,
10808
// reset properties
10809
series.xIncrement = null;
10810
series.pointRange = (series.xAxis && series.xAxis.categories && 1) || options.pointRange;
10812
if (defined(initialColor)) { // reset colors for pie
10813
chart.counters.color = initialColor;
10819
dataLength = data ? data.length : [],
10820
turboThreshold = options.turboThreshold || 1000,
10822
ohlc = series.valueCount === 4;
10824
// In turbo mode, only one- or twodimensional arrays of numbers are allowed. The
10825
// first value is tested, and we assume that all the rest are defined the same
10826
// way. Although the 'for' loops are similar, they are repeated inside each
10827
// if-else conditional for max performance.
10828
if (dataLength > turboThreshold) {
10830
// find the first non-null point
10832
while (firstPoint === null && i < dataLength) {
10833
firstPoint = data[i];
10838
if (isNumber(firstPoint)) { // assume all points are numbers
10839
var x = pick(options.pointStart, 0),
10840
pointInterval = pick(options.pointInterval, 1);
10842
for (i = 0; i < dataLength; i++) {
10844
yData[i] = data[i];
10845
x += pointInterval;
10847
series.xIncrement = x;
10848
} else if (isArray(firstPoint)) { // assume all points are arrays
10849
if (ohlc) { // [x, o, h, l, c]
10850
for (i = 0; i < dataLength; i++) {
10853
yData[i] = pt.slice(1, 5);
10856
for (i = 0; i < dataLength; i++) {
10863
error(12); // Highcharts expects configs to be numbers or arrays in turbo mode
10866
for (i = 0; i < dataLength; i++) {
10867
pt = { series: series };
10868
series.pointClass.prototype.applyOptions.apply(pt, [data[i]]);
10870
yData[i] = ohlc ? [pt.open, pt.high, pt.low, pt.close] : pt.y;
10875
series.options.data = data;
10876
series.xData = xData;
10877
series.yData = yData;
10879
// destroy old points
10880
i = (oldData && oldData.length) || 0;
10882
if (oldData[i] && oldData[i].destroy) {
10883
oldData[i].destroy();
10888
series.isDirty = series.isDirtyData = chart.isDirtyBox = true;
10889
if (pick(redraw, true)) {
10890
chart.redraw(false);
10895
* Remove a series and optionally redraw the chart
10897
* @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
10898
* @param {Boolean|Object} animation Whether to apply animation, and optionally animation
10902
remove: function (redraw, animation) {
10904
chart = series.chart;
10905
redraw = pick(redraw, true);
10907
if (!series.isRemoving) { /* prevent triggering native event in jQuery
10908
(calling the remove function from the remove event) */
10909
series.isRemoving = true;
10911
// fire the event with a default handler of removing the point
10912
fireEvent(series, 'remove', null, function () {
10915
// destroy elements
10920
chart.isDirtyLegend = chart.isDirtyBox = true;
10922
chart.redraw(animation);
10927
series.isRemoving = false;
10931
* Process the data by cropping away unused data points if the series is longer
10932
* than the crop threshold. This saves computing time for lage series.
10934
processData: function (force) {
10936
processedXData = series.xData, // copied during slice operation below
10937
processedYData = series.yData,
10938
dataLength = processedXData.length,
10940
cropEnd = dataLength,
10944
xAxis = series.xAxis,
10945
i, // loop variable
10946
options = series.options,
10947
cropThreshold = options.cropThreshold,
10948
isCartesian = series.isCartesian;
10950
// If the series data or axes haven't changed, don't go through this. Return false to pass
10951
// the message on to override methods like in data grouping.
10952
if (isCartesian && !series.isDirty && !xAxis.isDirty && !series.yAxis.isDirty && !force) {
10956
// optionally filter out points outside the plot area
10957
if (isCartesian && series.sorted && (!cropThreshold || dataLength > cropThreshold || series.forceCrop)) {
10958
var extremes = xAxis.getExtremes(),
10959
min = extremes.min,
10960
max = extremes.max;
10962
// it's outside current extremes
10963
if (processedXData[dataLength - 1] < min || processedXData[0] > max) {
10964
processedXData = [];
10965
processedYData = [];
10967
// only crop if it's actually spilling out
10968
} else if (processedXData[0] < min || processedXData[dataLength - 1] > max) {
10970
// iterate up to find slice start
10971
for (i = 0; i < dataLength; i++) {
10972
if (processedXData[i] >= min) {
10973
cropStart = mathMax(0, i - 1);
10977
// proceed to find slice end
10978
for (; i < dataLength; i++) {
10979
if (processedXData[i] > max) {
10985
processedXData = processedXData.slice(cropStart, cropEnd);
10986
processedYData = processedYData.slice(cropStart, cropEnd);
10992
// Find the closest distance between processed points
10993
for (i = processedXData.length - 1; i > 0; i--) {
10994
distance = processedXData[i] - processedXData[i - 1];
10995
if (distance > 0 && (closestPointRange === UNDEFINED || distance < closestPointRange)) {
10996
closestPointRange = distance;
11000
// Record the properties
11001
series.cropped = cropped; // undefined or true
11002
series.cropStart = cropStart;
11003
series.processedXData = processedXData;
11004
series.processedYData = processedYData;
11006
if (options.pointRange === null) { // null means auto, as for columns, candlesticks and OHLC
11007
series.pointRange = closestPointRange || 1;
11009
series.closestPointRange = closestPointRange;
11014
* Generate the data point after the data has been processed by cropping away
11015
* unused points and optionally grouped in Highcharts Stock.
11017
generatePoints: function () {
11019
options = series.options,
11020
dataOptions = options.data,
11021
data = series.data,
11023
processedXData = series.processedXData,
11024
processedYData = series.processedYData,
11025
pointClass = series.pointClass,
11026
processedDataLength = processedXData.length,
11027
cropStart = series.cropStart || 0,
11029
hasGroupedData = series.hasGroupedData,
11034
if (!data && !hasGroupedData) {
11036
arr.length = dataOptions.length;
11037
data = series.data = arr;
11040
for (i = 0; i < processedDataLength; i++) {
11041
cursor = cropStart + i;
11042
if (!hasGroupedData) {
11043
if (data[cursor]) {
11044
point = data[cursor];
11046
data[cursor] = point = (new pointClass()).init(series, dataOptions[cursor], processedXData[i]);
11050
// splat the y data in case of ohlc data array
11051
points[i] = (new pointClass()).init(series, [processedXData[i]].concat(splat(processedYData[i])));
11055
// Hide cropped-away points - this only runs when the number of points is above cropThreshold, or when
11056
// swithching view from non-grouped data to grouped data (#637)
11057
if (data && (processedDataLength !== (dataLength = data.length) || hasGroupedData)) {
11058
for (i = 0; i < dataLength; i++) {
11059
if (i === cropStart && !hasGroupedData) { // when has grouped data, clear all points
11060
i += processedDataLength;
11063
data[i].destroyElements();
11068
series.data = data;
11069
series.points = points;
11073
* Translate data points from raw data values to chart specific positioning data
11074
* needed later in drawPoints, drawGraph and drawTracker.
11076
translate: function () {
11077
if (!this.processedXData) { // hidden series
11078
this.processData();
11080
this.generatePoints();
11082
chart = series.chart,
11083
options = series.options,
11084
stacking = options.stacking,
11085
xAxis = series.xAxis,
11086
categories = xAxis.categories,
11087
yAxis = series.yAxis,
11088
points = series.points,
11089
dataLength = points.length,
11090
hasModifyValue = !!series.modifyValue,
11092
allStackSeries = yAxis.series,
11093
i = allStackSeries.length;
11095
// Is it the last visible series?
11097
if (allStackSeries[i].visible) {
11098
if (i === series.index) {
11099
isLastSeries = true;
11105
// Translate each point
11106
for (i = 0; i < dataLength; i++) {
11107
var point = points[i],
11110
yBottom = point.low,
11111
stack = yAxis.stacks[(yValue < options.threshold ? '-' : '') + series.stackKey],
11115
// get the plotX translation
11116
point.plotX = mathRound(xAxis.translate(xValue, 0, 0, 0, 1) * 10) / 10; // Math.round fixes #591
11118
// calculate the bottom y value for stacked series
11119
if (stacking && series.visible && stack && stack[xValue]) {
11120
pointStack = stack[xValue];
11121
pointStackTotal = pointStack.total;
11122
pointStack.cum = yBottom = pointStack.cum - yValue; // start from top
11123
yValue = yBottom + yValue;
11125
if (isLastSeries) {
11126
yBottom = options.threshold;
11129
if (stacking === 'percent') {
11130
yBottom = pointStackTotal ? yBottom * 100 / pointStackTotal : 0;
11131
yValue = pointStackTotal ? yValue * 100 / pointStackTotal : 0;
11134
point.percentage = pointStackTotal ? point.y * 100 / pointStackTotal : 0;
11135
point.stackTotal = pointStackTotal;
11136
point.stackY = yValue;
11139
// Set translated yBottom or remove it
11140
point.yBottom = defined(yBottom) ?
11141
yAxis.translate(yBottom, 0, 1, 0, 1) :
11144
// general hook, used for Highstock compare mode
11145
if (hasModifyValue) {
11146
yValue = series.modifyValue(yValue, point);
11149
// Set the the plotY value, reset it for redraws
11150
point.plotY = (typeof yValue === 'number') ?
11151
mathRound(yAxis.translate(yValue, 0, 1, 0, 1) * 10) / 10 : // Math.round fixes #591
11154
// set client related positions for mouse tracking
11155
point.clientX = chart.inverted ?
11156
chart.plotHeight - point.plotX :
11157
point.plotX; // for mouse tracking
11160
point.category = categories && categories[point.x] !== UNDEFINED ?
11161
categories[point.x] : point.x;
11166
// now that we have the cropped data, build the segments
11167
series.getSegments();
11170
* Memoize tooltip texts and positions
11172
setTooltipPoints: function (renew) {
11174
chart = series.chart,
11175
inverted = chart.inverted,
11178
plotSize = mathRound((inverted ? chart.plotTop : chart.plotLeft) + chart.plotSizeX),
11181
xAxis = series.xAxis,
11184
tooltipPoints = []; // a lookup array for each pixel in the x dimension
11186
// don't waste resources if tracker is disabled
11187
if (series.options.enableMouseTracking === false) {
11193
series.tooltipPoints = null;
11196
// concat segments to overcome null values
11197
each(series.segments || series.points, function (segment) {
11198
points = points.concat(segment);
11201
// loop the concatenated points and apply each point to all the closest
11203
if (xAxis && xAxis.reversed) {
11204
points = points.reverse();//reverseArray(points);
11207
//each(points, function (point, i) {
11208
pointsLength = points.length;
11209
for (i = 0; i < pointsLength; i++) {
11211
low = points[i - 1] ? points[i - 1]._high + 1 : 0;
11212
high = point._high = points[i + 1] ?
11213
(mathFloor((point.plotX + (points[i + 1] ? points[i + 1].plotX : plotSize)) / 2)) :
11216
while (low <= high) {
11217
tooltipPoints[inverted ? plotSize - low++ : low++] = point;
11220
series.tooltipPoints = tooltipPoints;
11224
* Format the header of the tooltip
11226
tooltipHeaderFormatter: function (key) {
11228
tooltipOptions = series.tooltipOptions,
11229
xDateFormat = tooltipOptions.xDateFormat || '%A, %b %e, %Y',
11230
xAxis = series.xAxis,
11231
isDateTime = xAxis && xAxis.options.type === 'datetime';
11233
return tooltipOptions.headerFormat
11234
.replace('{point.key}', isDateTime ? dateFormat(xDateFormat, key) : key)
11235
.replace('{series.name}', series.name)
11236
.replace('{series.color}', series.color);
11240
* Series mouse over handler
11242
onMouseOver: function () {
11244
chart = series.chart,
11245
hoverSeries = chart.hoverSeries;
11247
if (!hasTouch && chart.mouseIsDown) {
11251
// set normal state to previous series
11252
if (hoverSeries && hoverSeries !== series) {
11253
hoverSeries.onMouseOut();
11256
// trigger the event, but to save processing time,
11258
if (series.options.events.mouseOver) {
11259
fireEvent(series, 'mouseOver');
11263
series.setState(HOVER_STATE);
11264
chart.hoverSeries = series;
11268
* Series mouse out handler
11270
onMouseOut: function () {
11271
// trigger the event only if listeners exist
11273
options = series.options,
11274
chart = series.chart,
11275
tooltip = chart.tooltip,
11276
hoverPoint = chart.hoverPoint;
11278
// trigger mouse out on the point, which must be in this series
11280
hoverPoint.onMouseOut();
11283
// fire the mouse out event
11284
if (series && options.events.mouseOut) {
11285
fireEvent(series, 'mouseOut');
11289
// hide the tooltip
11290
if (tooltip && !options.stickyTracking && !tooltip.shared) {
11294
// set normal state
11296
chart.hoverSeries = null;
11300
* Animate in the series
11302
animate: function (init) {
11304
chart = series.chart,
11305
clipRect = series.clipRect,
11306
animation = series.options.animation;
11308
if (animation && !isObject(animation)) {
11312
if (init) { // initialize the animation
11313
if (!clipRect.isAnimating) { // apply it only for one of the series
11314
clipRect.attr('width', 0);
11315
clipRect.isAnimating = true;
11318
} else { // run the animation
11320
width: chart.plotSizeX
11323
// delete this function to allow it only once
11324
this.animate = null;
11332
drawPoints: function () {
11335
points = series.points,
11336
chart = series.chart,
11346
if (series.options.marker.enabled) {
11350
plotX = point.plotX;
11351
plotY = point.plotY;
11352
graphic = point.graphic;
11354
// only draw the point if y is defined
11355
if (plotY !== UNDEFINED && !isNaN(plotY)) {
11358
pointAttr = point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE];
11359
radius = pointAttr.r;
11360
symbol = pick(point.marker && point.marker.symbol, series.symbol);
11361
isImage = symbol.indexOf('url') === 0;
11363
if (graphic) { // update
11364
graphic.animate(extend({
11367
}, graphic.symbolName ? { // don't apply to image symbols #507
11371
} else if (radius > 0 || isImage) {
11372
point.graphic = chart.renderer.symbol(
11380
.add(series.group);
11389
* Convert state properties from API naming conventions to SVG attributes
11391
* @param {Object} options API options object
11392
* @param {Object} base1 SVG attribute object to inherit from
11393
* @param {Object} base2 Second level SVG attribute object to inherit from
11395
convertAttribs: function (options, base1, base2, base3) {
11396
var conversion = this.pointAttrToOptions,
11401
options = options || {};
11402
base1 = base1 || {};
11403
base2 = base2 || {};
11404
base3 = base3 || {};
11406
for (attr in conversion) {
11407
option = conversion[attr];
11408
obj[attr] = pick(options[option], base1[attr], base2[attr], base3[attr]);
11414
* Get the state attributes. Each series type has its own set of attributes
11415
* that are allowed to change on a point's state change. Series wide attributes are stored for
11416
* all series, and additionally point specific attributes are stored for all
11417
* points with individual marker options. If such options are not defined for the point,
11418
* a reference to the series wide attributes is stored in point.pointAttr.
11420
getAttribs: function () {
11422
normalOptions = defaultPlotOptions[series.type].marker ? series.options.marker : series.options,
11423
stateOptions = normalOptions.states,
11424
stateOptionsHover = stateOptions[HOVER_STATE],
11425
pointStateOptionsHover,
11426
seriesColor = series.color,
11428
stroke: seriesColor,
11431
points = series.points,
11434
seriesPointAttr = [],
11436
pointAttrToOptions = series.pointAttrToOptions,
11437
hasPointSpecificOptions,
11440
// series type specific modifications
11441
if (series.options.marker) { // line, spline, area, areaspline, scatter
11443
// if no hover radius is given, default to normal radius + 2
11444
stateOptionsHover.radius = stateOptionsHover.radius || normalOptions.radius + 2;
11445
stateOptionsHover.lineWidth = stateOptionsHover.lineWidth || normalOptions.lineWidth + 1;
11447
} else { // column, bar, pie
11449
// if no hover color is given, brighten the normal color
11450
stateOptionsHover.color = stateOptionsHover.color ||
11451
Color(stateOptionsHover.color || seriesColor)
11452
.brighten(stateOptionsHover.brightness).get();
11455
// general point attributes for the series normal state
11456
seriesPointAttr[NORMAL_STATE] = series.convertAttribs(normalOptions, normalDefaults);
11458
// HOVER_STATE and SELECT_STATE states inherit from normal state except the default radius
11459
each([HOVER_STATE, SELECT_STATE], function (state) {
11460
seriesPointAttr[state] =
11461
series.convertAttribs(stateOptions[state], seriesPointAttr[NORMAL_STATE]);
11465
series.pointAttr = seriesPointAttr;
11468
// Generate the point-specific attribute collections if specific point
11469
// options are given. If not, create a referance to the series wide point
11474
normalOptions = (point.options && point.options.marker) || point.options;
11475
if (normalOptions && normalOptions.enabled === false) {
11476
normalOptions.radius = 0;
11478
hasPointSpecificOptions = false;
11480
// check if the point has specific visual options
11481
if (point.options) {
11482
for (key in pointAttrToOptions) {
11483
if (defined(normalOptions[pointAttrToOptions[key]])) {
11484
hasPointSpecificOptions = true;
11491
// a specific marker config object is defined for the individual point:
11492
// create it's own attribute collection
11493
if (hasPointSpecificOptions) {
11496
stateOptions = normalOptions.states || {}; // reassign for individual point
11497
pointStateOptionsHover = stateOptions[HOVER_STATE] = stateOptions[HOVER_STATE] || {};
11499
// if no hover color is given, brighten the normal color
11500
if (!series.options.marker) { // column, bar, point
11501
pointStateOptionsHover.color =
11502
Color(pointStateOptionsHover.color || point.options.color)
11503
.brighten(pointStateOptionsHover.brightness ||
11504
stateOptionsHover.brightness).get();
11508
// normal point state inherits series wide normal state
11509
pointAttr[NORMAL_STATE] = series.convertAttribs(normalOptions, seriesPointAttr[NORMAL_STATE]);
11511
// inherit from point normal and series hover
11512
pointAttr[HOVER_STATE] = series.convertAttribs(
11513
stateOptions[HOVER_STATE],
11514
seriesPointAttr[HOVER_STATE],
11515
pointAttr[NORMAL_STATE]
11517
// inherit from point normal and series hover
11518
pointAttr[SELECT_STATE] = series.convertAttribs(
11519
stateOptions[SELECT_STATE],
11520
seriesPointAttr[SELECT_STATE],
11521
pointAttr[NORMAL_STATE]
11526
// no marker config object is created: copy a reference to the series-wide
11527
// attribute collection
11529
pointAttr = seriesPointAttr;
11532
point.pointAttr = pointAttr;
11540
* Clear DOM objects and free up memory
11542
destroy: function () {
11544
chart = series.chart,
11545
seriesClipRect = series.clipRect,
11546
issue134 = /AppleWebKit\/533/.test(userAgent),
11549
data = series.data || [],
11555
fireEvent(series, 'destroy');
11557
// remove all events
11558
removeEvent(series);
11561
each(['xAxis', 'yAxis'], function (AXIS) {
11562
axis = series[AXIS];
11564
erase(axis.series, series);
11565
axis.isDirty = true;
11569
// remove legend items
11570
if (series.legendItem) {
11571
series.chart.legend.destroyItem(series);
11574
// destroy all points with their elements
11578
if (point && point.destroy) {
11582
series.points = null;
11584
// If this series clipRect is not the global one (which is removed on chart.destroy) we
11585
// destroy it here.
11586
if (seriesClipRect && seriesClipRect !== chart.clipRect) {
11587
series.clipRect = seriesClipRect.destroy();
11590
// destroy all SVGElements associated to the series
11591
each(['area', 'graph', 'dataLabelsGroup', 'group', 'tracker'], function (prop) {
11592
if (series[prop]) {
11594
// issue 134 workaround
11595
destroy = issue134 && prop === 'group' ?
11599
series[prop][destroy]();
11603
// remove from hoverSeries
11604
if (chart.hoverSeries === series) {
11605
chart.hoverSeries = null;
11607
erase(chart.series, series);
11609
// clear all members
11610
for (prop in series) {
11611
delete series[prop];
11616
* Draw the data labels
11618
drawDataLabels: function () {
11621
seriesOptions = series.options,
11622
options = seriesOptions.dataLabels;
11624
if (options.enabled || series._hasPointLabels) {
11627
points = series.points,
11631
dataLabelsGroup = series.dataLabelsGroup,
11632
chart = series.chart,
11633
xAxis = series.xAxis,
11634
groupLeft = xAxis ? xAxis.left : chart.plotLeft,
11635
yAxis = series.yAxis,
11636
groupTop = yAxis ? yAxis.top : chart.plotTop,
11637
renderer = chart.renderer,
11638
inverted = chart.inverted,
11639
seriesType = series.type,
11640
stacking = seriesOptions.stacking,
11641
isBarLike = seriesType === 'column' || seriesType === 'bar',
11642
vAlignIsNull = options.verticalAlign === null,
11643
yIsNull = options.y === null,
11644
fontMetrics = renderer.fontMetrics(options.style.fontSize), // height and baseline
11645
fontLineHeight = fontMetrics.h,
11646
fontBaseline = fontMetrics.b,
11653
middle: fontBaseline - fontLineHeight / 2,
11654
bottom: -fontLineHeight + fontBaseline
11657
// In stacked series the default label placement is inside the bars
11658
if (vAlignIsNull) {
11659
options = merge(options, {verticalAlign: 'middle'});
11662
// If no y delta is specified, try to create a good default
11664
options = merge(options, { y: defaultYs[options.verticalAlign]});
11667
// In non stacked series the default label placement is on top of the bars
11668
if (vAlignIsNull) {
11669
options = merge(options, {verticalAlign: 'top'});
11671
// If no y delta is specified, try to create a good default (like default bar)
11672
} else if (yIsNull) {
11673
options = merge(options, { y: defaultYs[options.verticalAlign]});
11680
// create a separate group for the data labels to avoid rotation
11681
if (!dataLabelsGroup) {
11682
dataLabelsGroup = series.dataLabelsGroup =
11683
renderer.g('data-labels')
11685
visibility: series.visible ? VISIBLE : HIDDEN,
11688
.translate(groupLeft, groupTop)
11691
dataLabelsGroup.translate(groupLeft, groupTop);
11694
// make the labels for each point
11695
generalOptions = options;
11696
each(points, function (point) {
11698
dataLabel = point.dataLabel;
11700
// Merge in individual options from point
11701
options = generalOptions; // reset changes from previous points
11702
pointOptions = point.options;
11703
if (pointOptions && pointOptions.dataLabels) {
11704
options = merge(options, pointOptions.dataLabels);
11706
enabled = options.enabled;
11708
// Get the positions
11710
var plotX = (point.barX && point.barX + point.barW / 2) || pick(point.plotX, -999),
11711
plotY = pick(point.plotY, -999),
11713
// if options.y is null, which happens by default on column charts, set the position
11714
// above or below the column depending on the threshold
11715
individualYDelta = options.y === null ?
11716
(point.y >= seriesOptions.threshold ?
11717
-fontLineHeight + fontBaseline : // below the threshold
11718
fontBaseline) : // above the threshold
11721
x = (inverted ? chart.plotWidth - plotY : plotX) + options.x;
11722
y = mathRound((inverted ? chart.plotHeight - plotX : plotY) + individualYDelta);
11726
// If the point is outside the plot area, destroy it. #678, #820
11727
if (dataLabel && series.isCartesian && (!chart.isInsidePlot(x, y) || !enabled)) {
11728
point.dataLabel = dataLabel.destroy();
11730
// Individual labels are disabled if the are explicitly disabled
11731
// in the point options, or if they fall outside the plot area.
11732
} else if (enabled) {
11734
var align = options.align;
11737
str = options.formatter.call(point.getLabelConfig(), options);
11739
// in columns, align the string to the column
11740
if (seriesType === 'column') {
11741
x += { left: -1, right: 1 }[align] * point.barW / 2 || 0;
11744
if (!stacking && inverted && point.y < 0) {
11749
// Determine the color
11750
options.style.color = pick(options.color, options.style.color, series.color, 'black');
11753
// update existing label
11755
// vertically centered
11763
// create new label
11764
} else if (defined(str)) {
11765
dataLabel = point.dataLabel = renderer[options.rotation ? 'text' : 'label']( // labels don't support rotation
11773
true // baseline for backwards compat
11777
fill: options.backgroundColor,
11778
stroke: options.borderColor,
11779
'stroke-width': options.borderWidth,
11780
r: options.borderRadius,
11781
rotation: options.rotation,
11782
padding: options.padding,
11785
.css(options.style)
11786
.add(dataLabelsGroup)
11787
.shadow(options.shadow);
11790
if (isBarLike && seriesOptions.stacking && dataLabel) {
11791
var barX = point.barX,
11796
dataLabel.align(options, null,
11798
x: inverted ? chart.plotWidth - barY - barH : barX,
11799
y: inverted ? chart.plotHeight - barX - barW : barY,
11800
width: inverted ? barH : barW,
11801
height: inverted ? barW : barH
11812
* Draw the actual graph
11814
drawGraph: function () {
11816
options = series.options,
11817
chart = series.chart,
11818
graph = series.graph,
11821
area = series.area,
11822
group = series.group,
11823
color = options.lineColor || series.color,
11824
lineWidth = options.lineWidth,
11825
dashStyle = options.dashStyle,
11827
renderer = chart.renderer,
11828
translatedThreshold = series.yAxis.getThreshold(options.threshold),
11829
useArea = /^area/.test(series.type),
11830
singlePoints = [], // used in drawTracker
11835
// divide into segments and build graph and area paths
11836
each(series.segments, function (segment) {
11839
// build the segment line
11840
each(segment, function (point, i) {
11842
if (series.getPointSpline) { // generate the spline as defined in the SplineSeries object
11843
segmentPath.push.apply(segmentPath, series.getPointSpline(segment, point, i));
11847
// moveTo or lineTo
11848
segmentPath.push(i ? L : M);
11851
if (i && options.step) {
11852
var lastPoint = segment[i - 1];
11859
// normal line to next point
11867
// add the segment to the graph, or a single point for tracking
11868
if (segment.length > 1) {
11869
graphPath = graphPath.concat(segmentPath);
11871
singlePoints.push(segment[0]);
11876
var areaSegmentPath = [],
11878
segLength = segmentPath.length;
11879
for (i = 0; i < segLength; i++) {
11880
areaSegmentPath.push(segmentPath[i]);
11882
if (segLength === 3) { // for animation from 1 to two points
11883
areaSegmentPath.push(L, segmentPath[1], segmentPath[2]);
11885
if (options.stacking && series.type !== 'areaspline') {
11887
// Follow stack back. Todo: implement areaspline. A general solution could be to
11888
// reverse the entire graphPath of the previous series, though may be hard with
11889
// splines and with series with different extremes
11890
for (i = segment.length - 1; i >= 0; i--) {
11893
if (i < segment.length - 1 && options.step) {
11894
areaSegmentPath.push(segment[i + 1].plotX, segment[i].yBottom);
11897
areaSegmentPath.push(segment[i].plotX, segment[i].yBottom);
11900
} else { // follow zero line back
11901
areaSegmentPath.push(
11903
segment[segment.length - 1].plotX,
11904
translatedThreshold,
11907
translatedThreshold
11910
areaPath = areaPath.concat(areaSegmentPath);
11914
// used in drawTracker:
11915
series.graphPath = graphPath;
11916
series.singlePoints = singlePoints;
11918
// draw the area if area series or areaspline
11922
Color(series.color).setOpacity(options.fillOpacity || 0.75).get()
11925
area.animate({ d: areaPath });
11929
series.area = series.chart.renderer.path(areaPath)
11938
stop(graph); // cancel running animations, #459
11939
graph.animate({ d: graphPath });
11945
'stroke-width': lineWidth
11948
attribs.dashstyle = dashStyle;
11951
series.graph = renderer.path(graphPath)
11952
.attr(attribs).add(group).shadow(options.shadow);
11958
* Initialize and perform group inversion on series.group and series.trackerGroup
11960
invertGroups: function () {
11962
group = series.group,
11963
trackerGroup = series.trackerGroup,
11964
chart = series.chart;
11966
// A fixed size is needed for inversion to work
11967
function setInvert() {
11969
width: series.yAxis.len,
11970
height: series.xAxis.len
11973
// Set the series.group size
11974
group.attr(size).invert();
11976
// Set the tracker group size
11977
if (trackerGroup) {
11978
trackerGroup.attr(size).invert();
11982
addEvent(chart, 'resize', setInvert); // do it on resize
11983
addEvent(series, 'destroy', function () {
11984
removeEvent(chart, 'resize', setInvert);
11988
setInvert(); // do it now
11990
// On subsequent render and redraw, just do setInvert without setting up events again
11991
series.invertGroups = setInvert;
11995
* Render the graph and markers
11997
render: function () {
11999
chart = series.chart,
12001
options = series.options,
12002
doClip = options.clip !== false,
12003
animation = options.animation,
12004
doAnimation = animation && series.animate,
12005
duration = doAnimation ? (animation && animation.duration) || 500 : 0,
12006
clipRect = series.clipRect,
12007
renderer = chart.renderer;
12010
// Add plot area clipping rectangle. If this is before chart.hasRendered,
12011
// create one shared clipRect.
12013
// Todo: since creating the clip property, the clipRect is created but
12014
// never used when clip is false. A better way would be that the animation
12015
// would run, then the clipRect destroyed.
12017
clipRect = series.clipRect = !chart.hasRendered && chart.clipRect ?
12019
renderer.clipRect(0, 0, chart.plotSizeX, chart.plotSizeY + 1);
12020
if (!chart.clipRect) {
12021
chart.clipRect = clipRect;
12027
if (!series.group) {
12028
group = series.group = renderer.g('series');
12031
visibility: series.visible ? VISIBLE : HIDDEN,
12032
zIndex: options.zIndex
12034
.translate(series.xAxis.left, series.yAxis.top)
12035
.add(chart.seriesGroup);
12038
series.drawDataLabels();
12040
// initiate the animation
12042
series.animate(true);
12045
// cache attributes for shapes
12046
series.getAttribs();
12048
// draw the graph if any
12049
if (series.drawGraph) {
12050
series.drawGraph();
12054
series.drawPoints();
12056
// draw the mouse tracking area
12057
if (series.options.enableMouseTracking !== false) {
12058
series.drawTracker();
12061
// Handle inverted series and tracker groups
12062
if (chart.inverted) {
12063
series.invertGroups();
12066
// Do the initial clipping. This must be done after inverting for VML.
12067
if (doClip && !series.hasRendered) {
12068
group.clip(clipRect);
12069
if (series.trackerGroup) {
12070
series.trackerGroup.clip(chart.clipRect);
12075
// run the animation
12080
// finish the individual clipRect
12081
setTimeout(function () {
12082
clipRect.isAnimating = false;
12083
group = series.group; // can be destroyed during the timeout
12084
if (group && clipRect !== chart.clipRect && clipRect.renderer) {
12086
group.clip((series.clipRect = chart.clipRect));
12088
clipRect.destroy();
12092
series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see
12093
// (See #322) series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see
12094
series.hasRendered = true;
12098
* Redraw the series after an update in the axes.
12100
redraw: function () {
12102
chart = series.chart,
12103
wasDirtyData = series.isDirtyData, // cache it here as it is set to false in render, but used after
12104
group = series.group;
12106
// reposition on resize
12108
if (chart.inverted) {
12110
width: chart.plotWidth,
12111
height: chart.plotHeight
12116
translateX: series.xAxis.left,
12117
translateY: series.yAxis.top
12121
series.translate();
12122
series.setTooltipPoints(true);
12125
if (wasDirtyData) {
12126
fireEvent(series, 'updatedData');
12131
* Set the state of the graph
12133
setState: function (state) {
12135
options = series.options,
12136
graph = series.graph,
12137
stateOptions = options.states,
12138
lineWidth = options.lineWidth;
12140
state = state || NORMAL_STATE;
12142
if (series.state !== state) {
12143
series.state = state;
12145
if (stateOptions[state] && stateOptions[state].enabled === false) {
12150
lineWidth = stateOptions[state].lineWidth || lineWidth + 1;
12153
if (graph && !graph.dashstyle) { // hover is turned off for dashed lines in VML
12154
graph.attr({ // use attr because animate will cause any other animation on the graph to stop
12155
'stroke-width': lineWidth
12156
}, state ? 0 : 500);
12162
* Set the visibility of the graph
12164
* @param vis {Boolean} True to show the series, false to hide. If UNDEFINED,
12165
* the visibility is toggled.
12167
setVisible: function (vis, redraw) {
12169
chart = series.chart,
12170
legendItem = series.legendItem,
12171
seriesGroup = series.group,
12172
seriesTracker = series.tracker,
12173
dataLabelsGroup = series.dataLabelsGroup,
12176
points = series.points,
12178
ignoreHiddenSeries = chart.options.chart.ignoreHiddenSeries,
12179
oldVisibility = series.visible;
12181
// if called without an argument, toggle visibility
12182
series.visible = vis = vis === UNDEFINED ? !oldVisibility : vis;
12183
showOrHide = vis ? 'show' : 'hide';
12185
// show or hide series
12186
if (seriesGroup) { // pies don't have one
12187
seriesGroup[showOrHide]();
12190
// show or hide trackers
12191
if (seriesTracker) {
12192
seriesTracker[showOrHide]();
12193
} else if (points) {
12197
if (point.tracker) {
12198
point.tracker[showOrHide]();
12204
if (dataLabelsGroup) {
12205
dataLabelsGroup[showOrHide]();
12209
chart.legend.colorizeItem(series, vis);
12213
// rescale or adapt to resized chart
12214
series.isDirty = true;
12215
// in a stack, all other series are affected
12216
if (series.options.stacking) {
12217
each(chart.series, function (otherSeries) {
12218
if (otherSeries.options.stacking && otherSeries.visible) {
12219
otherSeries.isDirty = true;
12224
if (ignoreHiddenSeries) {
12225
chart.isDirtyBox = true;
12227
if (redraw !== false) {
12231
fireEvent(series, showOrHide);
12237
show: function () {
12238
this.setVisible(true);
12244
hide: function () {
12245
this.setVisible(false);
12250
* Set the selected state of the graph
12252
* @param selected {Boolean} True to select the series, false to unselect. If
12253
* UNDEFINED, the selection state is toggled.
12255
select: function (selected) {
12257
// if called without an argument, toggle
12258
series.selected = selected = (selected === UNDEFINED) ? !series.selected : selected;
12260
if (series.checkbox) {
12261
series.checkbox.checked = selected;
12264
fireEvent(series, selected ? 'select' : 'unselect');
12268
* Create a group that holds the tracking object or objects. This allows for
12269
* individual clipping and placement of each series tracker.
12271
drawTrackerGroup: function () {
12272
var trackerGroup = this.trackerGroup,
12273
chart = this.chart;
12275
if (this.isCartesian) {
12277
// Generate it on first call
12278
if (!trackerGroup) {
12279
this.trackerGroup = trackerGroup = chart.renderer.g()
12281
zIndex: this.options.zIndex || 1
12283
.add(chart.trackerGroup);
12286
// Place it on first and subsequent (redraw) calls
12287
trackerGroup.translate(this.xAxis.left, this.yAxis.top);
12291
return trackerGroup;
12295
* Draw the tracker object that sits above all data labels and markers to
12296
* track mouse events on the graph or points. For the line type charts
12297
* the tracker uses the same graphPath, but with a greater stroke width
12298
* for better control.
12300
drawTracker: function () {
12302
options = series.options,
12303
trackerPath = [].concat(series.graphPath),
12304
trackerPathLength = trackerPath.length,
12305
chart = series.chart,
12306
renderer = chart.renderer,
12307
snap = chart.options.tooltip.snap,
12308
tracker = series.tracker,
12309
cursor = options.cursor,
12310
css = cursor && { cursor: cursor },
12311
singlePoints = series.singlePoints,
12312
trackerGroup = series.drawTrackerGroup(),
12316
// Extend end points. A better way would be to use round linecaps,
12317
// but those are not clickable in VML.
12318
if (trackerPathLength) {
12319
i = trackerPathLength + 1;
12321
if (trackerPath[i] === M) { // extend left side
12322
trackerPath.splice(i + 1, 0, trackerPath[i + 1] - snap, trackerPath[i + 2], L);
12324
if ((i && trackerPath[i] === M) || i === trackerPathLength) { // extend right side
12325
trackerPath.splice(i, 0, L, trackerPath[i - 2] + snap, trackerPath[i - 1]);
12330
// handle single points
12331
for (i = 0; i < singlePoints.length; i++) {
12332
singlePoint = singlePoints[i];
12333
trackerPath.push(M, singlePoint.plotX - snap, singlePoint.plotY,
12334
L, singlePoint.plotX + snap, singlePoint.plotY);
12339
// draw the tracker
12341
tracker.attr({ d: trackerPath });
12345
series.tracker = renderer.path(trackerPath)
12348
stroke: TRACKER_FILL,
12350
'stroke-linejoin': 'bevel',
12351
'stroke-width' : options.lineWidth + 2 * snap,
12352
visibility: series.visible ? VISIBLE : HIDDEN
12354
.on(hasTouch ? 'touchstart' : 'mouseover', function () {
12355
if (chart.hoverSeries !== series) {
12356
series.onMouseOver();
12359
.on('mouseout', function () {
12360
if (!options.stickyTracking) {
12361
series.onMouseOut();
12365
.add(trackerGroup);
12370
}; // end Series prototype
12374
* LineSeries object
12376
var LineSeries = extendClass(Series);
12377
seriesTypes.line = LineSeries;
12380
* AreaSeries object
12382
var AreaSeries = extendClass(Series, {
12385
seriesTypes.area = AreaSeries;
12391
* SplineSeries object
12393
var SplineSeries = extendClass(Series, {
12397
* Draw the actual graph
12399
getPointSpline: function (segment, point, i) {
12400
var smoothing = 1.5, // 1 means control points midway between points, 2 means 1/3 from the point, 3 is 1/4 etc
12401
denom = smoothing + 1,
12402
plotX = point.plotX,
12403
plotY = point.plotY,
12404
lastPoint = segment[i - 1],
12405
nextPoint = segment[i + 1],
12412
// find control points
12413
if (i && i < segment.length - 1) {
12414
var lastX = lastPoint.plotX,
12415
lastY = lastPoint.plotY,
12416
nextX = nextPoint.plotX,
12417
nextY = nextPoint.plotY,
12420
leftContX = (smoothing * plotX + lastX) / denom;
12421
leftContY = (smoothing * plotY + lastY) / denom;
12422
rightContX = (smoothing * plotX + nextX) / denom;
12423
rightContY = (smoothing * plotY + nextY) / denom;
12425
// have the two control points make a straight line through main point
12426
correction = ((rightContY - leftContY) * (rightContX - plotX)) /
12427
(rightContX - leftContX) + plotY - rightContY;
12429
leftContY += correction;
12430
rightContY += correction;
12432
// to prevent false extremes, check that control points are between
12433
// neighbouring points' y values
12434
if (leftContY > lastY && leftContY > plotY) {
12435
leftContY = mathMax(lastY, plotY);
12436
rightContY = 2 * plotY - leftContY; // mirror of left control point
12437
} else if (leftContY < lastY && leftContY < plotY) {
12438
leftContY = mathMin(lastY, plotY);
12439
rightContY = 2 * plotY - leftContY;
12441
if (rightContY > nextY && rightContY > plotY) {
12442
rightContY = mathMax(nextY, plotY);
12443
leftContY = 2 * plotY - rightContY;
12444
} else if (rightContY < nextY && rightContY < plotY) {
12445
rightContY = mathMin(nextY, plotY);
12446
leftContY = 2 * plotY - rightContY;
12449
// record for drawing in next point
12450
point.rightContX = rightContX;
12451
point.rightContY = rightContY;
12455
// moveTo or lineTo
12457
ret = [M, plotX, plotY];
12458
} else { // curve from last point to this
12461
lastPoint.rightContX || lastPoint.plotX,
12462
lastPoint.rightContY || lastPoint.plotY,
12463
leftContX || plotX,
12464
leftContY || plotY,
12468
lastPoint.rightContX = lastPoint.rightContY = null; // reset for updating series later
12473
seriesTypes.spline = SplineSeries;
12478
* AreaSplineSeries object
12480
var AreaSplineSeries = extendClass(SplineSeries, {
12483
seriesTypes.areaspline = AreaSplineSeries;
12486
* ColumnSeries object
12488
var ColumnSeries = extendClass(Series, {
12490
tooltipOutsidePlot: true,
12491
pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
12492
stroke: 'borderColor',
12493
'stroke-width': 'borderWidth',
12497
init: function () {
12498
Series.prototype.init.apply(this, arguments);
12501
chart = series.chart;
12503
// if the series is added dynamically, force redraw of other
12504
// series affected by a new column
12505
if (chart.hasRendered) {
12506
each(chart.series, function (otherSeries) {
12507
if (otherSeries.type === series.type) {
12508
otherSeries.isDirty = true;
12515
* Translate each point to the plot area coordinate system and find shape positions
12517
translate: function () {
12519
chart = series.chart,
12520
options = series.options,
12521
stacking = options.stacking,
12522
borderWidth = options.borderWidth,
12524
xAxis = series.xAxis,
12525
reversedXAxis = xAxis.reversed,
12530
Series.prototype.translate.apply(series);
12532
// Get the total number of column type series.
12533
// This is called on every series. Consider moving this logic to a
12534
// chart.orderStacks() function and call it on init, addSeries and removeSeries
12535
each(chart.series, function (otherSeries) {
12536
if (otherSeries.type === series.type && otherSeries.visible &&
12537
series.options.group === otherSeries.options.group) { // used in Stock charts navigator series
12538
if (otherSeries.options.stacking) {
12539
stackKey = otherSeries.stackKey;
12540
if (stackGroups[stackKey] === UNDEFINED) {
12541
stackGroups[stackKey] = columnCount++;
12543
columnIndex = stackGroups[stackKey];
12545
columnIndex = columnCount++;
12547
otherSeries.columnIndex = columnIndex;
12551
// calculate the width and position of each column based on
12552
// the number of column series in the plot, the groupPadding
12553
// and the pointPadding options
12554
var points = series.points,
12555
categoryWidth = mathAbs(xAxis.translationSlope) * (xAxis.ordinalSlope || xAxis.closestPointRange || 1),
12556
groupPadding = categoryWidth * options.groupPadding,
12557
groupWidth = categoryWidth - 2 * groupPadding,
12558
pointOffsetWidth = groupWidth / columnCount,
12559
optionPointWidth = options.pointWidth,
12560
pointPadding = defined(optionPointWidth) ? (pointOffsetWidth - optionPointWidth) / 2 :
12561
pointOffsetWidth * options.pointPadding,
12562
pointWidth = mathCeil(mathMax(pick(optionPointWidth, pointOffsetWidth - 2 * pointPadding), 1 + 2 * borderWidth)),
12563
colIndex = (reversedXAxis ? columnCount -
12564
series.columnIndex : series.columnIndex) || 0,
12565
pointXOffset = pointPadding + (groupPadding + colIndex *
12566
pointOffsetWidth - (categoryWidth / 2)) *
12567
(reversedXAxis ? -1 : 1),
12568
threshold = options.threshold,
12569
translatedThreshold = series.yAxis.getThreshold(threshold),
12570
minPointLength = pick(options.minPointLength, 5);
12572
// record the new values
12573
each(points, function (point) {
12574
var plotY = point.plotY,
12575
yBottom = pick(point.yBottom, translatedThreshold),
12576
barX = point.plotX + pointXOffset,
12577
barY = mathCeil(mathMin(plotY, yBottom)),
12578
barH = mathCeil(mathMax(plotY, yBottom) - barY),
12579
stack = series.yAxis.stacks[(point.y < 0 ? '-' : '') + series.stackKey],
12582
// Record the offset'ed position and width of the bar to be able to align the stacking total correctly
12583
if (stacking && series.visible && stack && stack[point.x]) {
12584
stack[point.x].setOffset(pointXOffset, pointWidth);
12587
// handle options.minPointLength
12588
if (mathAbs(barH) < minPointLength) {
12589
if (minPointLength) {
12590
barH = minPointLength;
12592
mathAbs(barY - translatedThreshold) > minPointLength ? // stacked
12593
yBottom - minPointLength : // keep position
12594
translatedThreshold - (plotY <= translatedThreshold ? minPointLength : 0);
12605
// create shape type and shape args that are reused in drawPoints and drawTracker
12606
point.shapeType = 'rect';
12612
r: options.borderRadius,
12613
strokeWidth: borderWidth
12616
if (borderWidth % 2) { // correct for shorting in crisp method, visible in stacked columns with 1px border
12618
shapeArgs.height += 1;
12620
point.shapeArgs = shapeArgs;
12622
// make small columns responsive to mouse
12623
point.trackerArgs = mathAbs(barH) < 3 && merge(point.shapeArgs, {
12631
getSymbol: function () {
12635
* Columns have no graph
12637
drawGraph: function () {},
12640
* Draw the columns. For bars, the series.group is rotated, so the same coordinates
12641
* apply for columns and bars. This method is inherited by scatter series.
12644
drawPoints: function () {
12646
options = series.options,
12647
renderer = series.chart.renderer,
12652
// draw the columns
12653
each(series.points, function (point) {
12654
var plotY = point.plotY;
12655
if (plotY !== UNDEFINED && !isNaN(plotY) && point.y !== null) {
12656
graphic = point.graphic;
12657
shapeArgs = point.shapeArgs;
12658
if (graphic) { // update
12660
graphic.animate(renderer.Element.prototype.crisp.apply({}, [
12661
shapeArgs.strokeWidth,
12669
point.graphic = graphic = renderer[point.shapeType](shapeArgs)
12670
.attr(point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE])
12672
.shadow(options.shadow);
12680
* Draw the individual tracker elements.
12681
* This method is inherited by scatter and pie charts too.
12683
drawTracker: function () {
12685
chart = series.chart,
12686
renderer = chart.renderer,
12689
trackerLabel = +new Date(),
12690
options = series.options,
12691
cursor = options.cursor,
12692
css = cursor && { cursor: cursor },
12693
trackerGroup = series.drawTrackerGroup(),
12696
each(series.points, function (point) {
12697
tracker = point.tracker;
12698
shapeArgs = point.trackerArgs || point.shapeArgs;
12699
delete shapeArgs.strokeWidth;
12700
if (point.y !== null) {
12701
if (tracker) {// update
12702
tracker.attr(shapeArgs);
12706
renderer[point.shapeType](shapeArgs)
12708
isTracker: trackerLabel,
12709
fill: TRACKER_FILL,
12710
visibility: series.visible ? VISIBLE : HIDDEN
12712
.on(hasTouch ? 'touchstart' : 'mouseover', function (event) {
12713
rel = event.relatedTarget || event.fromElement;
12714
if (chart.hoverSeries !== series && attr(rel, 'isTracker') !== trackerLabel) {
12715
series.onMouseOver();
12717
point.onMouseOver();
12720
.on('mouseout', function (event) {
12721
if (!options.stickyTracking) {
12722
rel = event.relatedTarget || event.toElement;
12723
if (attr(rel, 'isTracker') !== trackerLabel) {
12724
series.onMouseOut();
12729
.add(point.group || trackerGroup); // pies have point group - see issue #118
12737
* Animate the column heights one by one from zero
12738
* @param {Boolean} init Whether to initialize the animation or run it
12740
animate: function (init) {
12742
points = series.points,
12743
options = series.options;
12745
if (!init) { // run the animation
12747
* Note: Ideally the animation should be initialized by calling
12748
* series.group.hide(), and then calling series.group.show()
12749
* after the animation was started. But this rendered the shadows
12750
* invisible in IE8 standards mode. If the columns flicker on large
12751
* datasets, this is the cause.
12754
each(points, function (point) {
12755
var graphic = point.graphic,
12756
shapeArgs = point.shapeArgs,
12757
yAxis = series.yAxis,
12758
threshold = options.threshold;
12764
y: defined(threshold) ?
12765
yAxis.getThreshold(threshold) :
12766
yAxis.translate(yAxis.getExtremes().min, 0, 1, 0, 1)
12771
height: shapeArgs.height,
12773
}, options.animation);
12778
// delete this function to allow it only once
12779
series.animate = null;
12784
* Remove this series from the chart
12786
remove: function () {
12788
chart = series.chart;
12790
// column and bar series affects other series of the same type
12791
// as they are either stacked or grouped
12792
if (chart.hasRendered) {
12793
each(chart.series, function (otherSeries) {
12794
if (otherSeries.type === series.type) {
12795
otherSeries.isDirty = true;
12800
Series.prototype.remove.apply(series, arguments);
12803
seriesTypes.column = ColumnSeries;
12805
var BarSeries = extendClass(ColumnSeries, {
12807
init: function () {
12808
this.inverted = true;
12809
ColumnSeries.prototype.init.apply(this, arguments);
12812
seriesTypes.bar = BarSeries;
12815
* The scatter series class
12817
var ScatterSeries = extendClass(Series, {
12821
* Extend the base Series' translate method by adding shape type and
12822
* arguments for the point trackers
12824
translate: function () {
12827
Series.prototype.translate.apply(series);
12829
each(series.points, function (point) {
12830
point.shapeType = 'circle';
12831
point.shapeArgs = {
12834
r: series.chart.options.tooltip.snap
12840
* Add tracking event listener to the series group, so the point graphics
12841
* themselves act as trackers
12843
drawTracker: function () {
12845
cursor = series.options.cursor,
12846
css = cursor && { cursor: cursor },
12847
points = series.points,
12851
// Set an expando property for the point index, used below
12853
graphic = points[i].graphic;
12854
if (graphic) { // doesn't exist for null points
12855
graphic.element._i = i;
12859
// Add the event listeners, we need to do this only once
12860
if (!series._hasTracking) {
12865
.on(hasTouch ? 'touchstart' : 'mouseover', function (e) {
12866
series.onMouseOver();
12867
if (e.target._i !== UNDEFINED) { // undefined on graph in scatterchart
12868
points[e.target._i].onMouseOver();
12871
.on('mouseout', function () {
12872
if (!series.options.stickyTracking) {
12873
series.onMouseOut();
12878
series._hasTracking = true;
12883
seriesTypes.scatter = ScatterSeries;
12886
* Extended point object for pies
12888
var PiePoint = extendClass(Point, {
12890
* Initiate the pie slice
12892
init: function () {
12894
Point.prototype.init.apply(this, arguments);
12899
//visible: options.visible !== false,
12901
visible: point.visible !== false,
12902
name: pick(point.name, 'Slice')
12905
// add event listener for select
12906
toggleSlice = function () {
12909
addEvent(point, 'select', toggleSlice);
12910
addEvent(point, 'unselect', toggleSlice);
12916
* Toggle the visibility of the pie slice
12917
* @param {Boolean} vis Whether to show the slice or not. If undefined, the
12918
* visibility is toggled
12920
setVisible: function (vis) {
12922
chart = point.series.chart,
12923
tracker = point.tracker,
12924
dataLabel = point.dataLabel,
12925
connector = point.connector,
12926
shadowGroup = point.shadowGroup,
12929
// if called without an argument, toggle visibility
12930
point.visible = vis = vis === UNDEFINED ? !point.visible : vis;
12932
method = vis ? 'show' : 'hide';
12934
point.group[method]();
12939
dataLabel[method]();
12942
connector[method]();
12945
shadowGroup[method]();
12947
if (point.legendItem) {
12948
chart.legend.colorizeItem(point, vis);
12953
* Set or toggle whether the slice is cut out from the pie
12954
* @param {Boolean} sliced When undefined, the slice state is toggled
12955
* @param {Boolean} redraw Whether to redraw the chart. True by default.
12957
slice: function (sliced, redraw, animation) {
12959
series = point.series,
12960
chart = series.chart,
12961
slicedTranslation = point.slicedTranslation,
12964
setAnimation(animation, chart);
12966
// redraw is true by default
12967
redraw = pick(redraw, true);
12969
// if called without an argument, toggle
12970
sliced = point.sliced = defined(sliced) ? sliced : !point.sliced;
12973
translateX: (sliced ? slicedTranslation[0] : chart.plotLeft),
12974
translateY: (sliced ? slicedTranslation[1] : chart.plotTop)
12976
point.group.animate(translation);
12977
if (point.shadowGroup) {
12978
point.shadowGroup.animate(translation);
12985
* The Pie series class
12987
var PieSeries = extendClass(Series, {
12989
isCartesian: false,
12990
pointClass: PiePoint,
12991
pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
12992
stroke: 'borderColor',
12993
'stroke-width': 'borderWidth',
12998
* Pies have one color each point
13000
getColor: function () {
13001
// record first color for use in setData
13002
this.initialColor = this.chart.counters.color;
13006
* Animate the column heights one by one from zero
13008
animate: function () {
13010
points = series.points;
13012
each(points, function (point) {
13013
var graphic = point.graphic,
13014
args = point.shapeArgs,
13030
}, series.options.animation);
13034
// delete this function to allow it only once
13035
series.animate = null;
13040
* Extend the basic setData method by running processData and generatePoints immediately,
13041
* in order to access the points from the legend.
13043
setData: function () {
13044
Series.prototype.setData.apply(this, arguments);
13045
this.processData();
13046
this.generatePoints();
13049
* Do translation for pie slices
13051
translate: function () {
13052
this.generatePoints();
13056
cumulative = -0.25, // start at top
13057
precision = 1000, // issue #172
13058
options = series.options,
13059
slicedOffset = options.slicedOffset,
13060
connectorOffset = slicedOffset + options.borderWidth,
13061
positions = options.center.concat([options.size, options.innerSize || 0]),
13062
chart = series.chart,
13063
plotWidth = chart.plotWidth,
13064
plotHeight = chart.plotHeight,
13068
points = series.points,
13071
smallestSize = mathMin(plotWidth, plotHeight),
13073
radiusX, // the x component of the radius vector for a given point
13075
labelDistance = options.dataLabels.distance;
13077
// get positions - either an integer or a percentage string must be given
13078
positions = map(positions, function (length, i) {
13080
isPercent = /%$/.test(length);
13082
// i == 0: centerX, relative to width
13083
// i == 1: centerY, relative to height
13084
// i == 2: size, relative to smallestSize
13085
// i == 4: innerSize, relative to smallestSize
13086
[plotWidth, plotHeight, smallestSize, smallestSize][i] *
13087
pInt(length) / 100 :
13091
// utility for getting the x value from a given y, used for anticollision logic in data labels
13092
series.getX = function (y, left) {
13094
angle = math.asin((y - positions[1]) / (positions[2] / 2 + labelDistance));
13096
return positions[0] +
13098
(mathCos(angle) * (positions[2] / 2 + labelDistance));
13101
// set center for later use
13102
series.center = positions;
13104
// get the total sum
13105
each(points, function (point) {
13109
each(points, function (point) {
13110
// set start and end angle
13111
fraction = total ? point.y / total : 0;
13112
start = mathRound(cumulative * circ * precision) / precision;
13113
cumulative += fraction;
13114
end = mathRound(cumulative * circ * precision) / precision;
13117
point.shapeType = 'arc';
13118
point.shapeArgs = {
13121
r: positions[2] / 2,
13122
innerR: positions[3] / 2,
13127
// center for the sliced out slice
13128
angle = (end + start) / 2;
13129
point.slicedTranslation = map([
13130
mathCos(angle) * slicedOffset + chart.plotLeft,
13131
mathSin(angle) * slicedOffset + chart.plotTop
13134
// set the anchor point for tooltips
13135
radiusX = mathCos(angle) * positions[2] / 2;
13136
radiusY = mathSin(angle) * positions[2] / 2;
13137
point.tooltipPos = [
13138
positions[0] + radiusX * 0.7,
13139
positions[1] + radiusY * 0.7
13142
// set the anchor point for data labels
13144
positions[0] + radiusX + mathCos(angle) * labelDistance, // first break of connector
13145
positions[1] + radiusY + mathSin(angle) * labelDistance, // a/a
13146
positions[0] + radiusX + mathCos(angle) * connectorOffset, // second break, right outside pie
13147
positions[1] + radiusY + mathSin(angle) * connectorOffset, // a/a
13148
positions[0] + radiusX, // landing point for connector
13149
positions[1] + radiusY, // a/a
13150
labelDistance < 0 ? // alignment
13152
angle < circ / 4 ? 'left' : 'right', // alignment
13153
angle // center angle
13157
point.percentage = fraction * 100;
13158
point.total = total;
13163
this.setTooltipPoints();
13167
* Render the slices
13169
render: function () {
13172
// cache attributes for shapes
13173
series.getAttribs();
13177
// draw the mouse tracking area
13178
if (series.options.enableMouseTracking !== false) {
13179
series.drawTracker();
13182
this.drawDataLabels();
13184
if (series.options.animation && series.animate) {
13188
// (See #322) series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see
13189
series.isDirty = false; // means data is in accordance with what you see
13193
* Draw the data points
13195
drawPoints: function () {
13197
chart = series.chart,
13198
renderer = chart.renderer,
13203
shadow = series.options.shadow,
13208
each(series.points, function (point) {
13209
graphic = point.graphic;
13210
shapeArgs = point.shapeArgs;
13211
group = point.group;
13212
shadowGroup = point.shadowGroup;
13214
// put the shadow behind all points
13215
if (shadow && !shadowGroup) {
13216
shadowGroup = point.shadowGroup = renderer.g('shadow')
13217
.attr({ zIndex: 4 })
13221
// create the group the first time
13223
group = point.group = renderer.g('point')
13224
.attr({ zIndex: 5 })
13228
// if the point is sliced, use special translation, else use plot area traslation
13229
groupTranslation = point.sliced ? point.slicedTranslation : [chart.plotLeft, chart.plotTop];
13230
group.translate(groupTranslation[0], groupTranslation[1]);
13232
shadowGroup.translate(groupTranslation[0], groupTranslation[1]);
13237
graphic.animate(shapeArgs);
13240
renderer.arc(shapeArgs)
13242
point.pointAttr[NORMAL_STATE],
13243
{ 'stroke-linejoin': 'round' }
13246
.shadow(shadow, shadowGroup);
13249
// detect point specific visibility
13250
if (point.visible === false) {
13251
point.setVisible(false);
13259
* Override the base drawDataLabels method by pie specific functionality
13261
drawDataLabels: function () {
13263
data = series.data,
13265
chart = series.chart,
13266
options = series.options.dataLabels,
13267
connectorPadding = pick(options.connectorPadding, 10),
13268
connectorWidth = pick(options.connectorWidth, 1),
13271
softConnector = pick(options.softConnector, true),
13272
distanceOption = options.distance,
13273
seriesCenter = series.center,
13274
radius = seriesCenter[2] / 2,
13275
centerY = seriesCenter[1],
13276
outside = distanceOption > 0,
13280
halves = [// divide the points into right and left halves for anti collision
13292
// get out if not enabled
13293
if (!options.enabled) {
13297
// run parent method
13298
Series.prototype.drawDataLabels.apply(series);
13300
// arrange points for detection collision
13301
each(data, function (point) {
13302
if (point.dataLabel) { // it may have been cancelled in the base method (#407)
13304
point.labelPos[7] < mathPI / 2 ? 0 : 1
13308
halves[1].reverse();
13310
// define the sorting algorithm
13311
sort = function (a, b) {
13315
// assume equal label heights
13316
labelHeight = halves[0][0] && halves[0][0].dataLabel && halves[0][0].dataLabel.getBBox().height;
13318
/* Loop over the points in each quartile, starting from the top and bottom
13319
* of the pie to detect overlapping labels.
13326
points = halves[i],
13328
length = points.length,
13333
for (pos = centerY - radius - distanceOption; pos <= centerY + radius + distanceOption; pos += labelHeight) {
13335
// visualize the slot
13337
var slotX = series.getX(pos, i) + chart.plotLeft - (i ? 100 : 0),
13338
slotY = pos + chart.plotTop;
13339
if (!isNaN(slotX)) {
13340
chart.renderer.rect(slotX, slotY - 7, 100, labelHeight)
13346
chart.renderer.text('Slot '+ (slots.length - 1), slotX, slotY + 4)
13353
slotsLength = slots.length;
13355
// if there are more values than available slots, remove lowest values
13356
if (length > slotsLength) {
13357
// create an array for sorting and ranking the points within each quarter
13358
rankArr = [].concat(points);
13359
rankArr.sort(sort);
13362
rankArr[j].rank = j;
13366
if (points[j].rank >= slotsLength) {
13367
points.splice(j, 1);
13370
length = points.length;
13373
// The label goes to the nearest open slot, but not closer to the edge than
13374
// the label's index.
13375
for (j = 0; j < length; j++) {
13378
labelPos = point.labelPos;
13380
var closest = 9999,
13384
// find the closest slot index
13385
for (slotI = 0; slotI < slotsLength; slotI++) {
13386
distance = mathAbs(slots[slotI] - labelPos[1]);
13387
if (distance < closest) {
13388
closest = distance;
13393
// if that slot index is closer to the edges of the slots, move it
13394
// to the closest appropriate slot
13395
if (slotIndex < j && slots[j] !== null) { // cluster at the top
13397
} else if (slotsLength < length - j + slotIndex && slots[j] !== null) { // cluster at the bottom
13398
slotIndex = slotsLength - length + j;
13399
while (slots[slotIndex] === null) { // make sure it is not taken
13403
// Slot is taken, find next free slot below. In the next run, the next slice will find the
13404
// slot above these, because it is the closest one
13405
while (slots[slotIndex] === null) { // make sure it is not taken
13410
usedSlots.push({ i: slotIndex, y: slots[slotIndex] });
13411
slots[slotIndex] = null; // mark as taken
13413
// sort them in order to fill in from the top
13414
usedSlots.sort(sort);
13417
// now the used slots are sorted, fill them up sequentially
13418
for (j = 0; j < length; j++) {
13421
labelPos = point.labelPos;
13422
dataLabel = point.dataLabel;
13423
var slot = usedSlots.pop(),
13424
naturalY = labelPos[1];
13426
visibility = point.visible === false ? HIDDEN : VISIBLE;
13427
slotIndex = slot.i;
13429
// if the slot next to currrent slot is free, the y value is allowed
13430
// to fall back to the natural position
13432
if ((naturalY > y && slots[slotIndex + 1] !== null) ||
13433
(naturalY < y && slots[slotIndex - 1] !== null)) {
13437
// get the x - use the natural x position for first and last slot, to prevent the top
13438
// and botton slice connectors from touching each other on either side
13439
x = series.getX(slotIndex === 0 || slotIndex === slots.length - 1 ? naturalY : y, i);
13441
// move or place the data label
13444
visibility: visibility,
13446
})[dataLabel.moved ? 'animate' : 'attr']({
13448
({ left: connectorPadding, right: -connectorPadding }[labelPos[6]] || 0),
13451
dataLabel.moved = true;
13453
// draw the connector
13454
if (outside && connectorWidth) {
13455
connector = point.connector;
13457
connectorPath = softConnector ? [
13459
x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label
13461
x, y, // first break, next to the label
13462
2 * labelPos[2] - labelPos[4], 2 * labelPos[3] - labelPos[5],
13463
labelPos[2], labelPos[3], // second break
13465
labelPos[4], labelPos[5] // base
13468
x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label
13470
labelPos[2], labelPos[3], // second break
13472
labelPos[4], labelPos[5] // base
13476
connector.animate({ d: connectorPath });
13477
connector.attr('visibility', visibility);
13480
point.connector = connector = series.chart.renderer.path(connectorPath).attr({
13481
'stroke-width': connectorWidth,
13482
stroke: options.connectorColor || point.color || '#606060',
13483
visibility: visibility,
13486
.translate(chart.plotLeft, chart.plotTop)
13495
* Draw point specific tracker objects. Inherit directly from column series.
13497
drawTracker: ColumnSeries.prototype.drawTracker,
13500
* Pies don't have point marker symbols
13502
getSymbol: function () {}
13505
seriesTypes.pie = PieSeries;
13508
// global variables
13509
extend(Highcharts, {
13511
dateFormat: dateFormat,
13512
pathAnim: pathAnim,
13513
getOptions: getOptions,
13514
hasBidiBug: hasBidiBug,
13515
numberFormat: numberFormat,
13518
Renderer: Renderer,
13519
SVGRenderer: SVGRenderer,
13520
VMLRenderer: VMLRenderer,
13521
CanVGRenderer: CanVGRenderer,
13522
seriesTypes: seriesTypes,
13523
setOptions: setOptions,
13526
// Expose utility funcitons for modules
13527
addEvent: addEvent,
13528
removeEvent: removeEvent,
13529
createElement: createElement,
13530
discardElement: discardElement,
13538
extendClass: extendClass,
13539
placeBox: placeBox,
13540
product: 'Highcharts',