2
// @compilation_level SIMPLE_OPTIMIZATIONS
5
* @license Highstock JS v1.3.10 (2014-03-10)
7
* (c) 2009-2014 Torstein Honsi
9
* License: www.highcharts.com/license
13
/*global Highcharts, document, window, navigator, setInterval, clearInterval, clearTimeout, setTimeout, location, jQuery, $, console, each, grep */
16
// encapsulated variables
21
mathRound = math.round,
22
mathFloor = math.floor,
30
deg2rad = mathPI * 2 / 360,
34
userAgent = navigator.userAgent,
36
isIE = /msie/i.test(userAgent) && !isOpera,
37
docMode8 = doc.documentMode === 8,
38
isWebKit = /AppleWebKit/.test(userAgent),
39
isFirefox = /Firefox/.test(userAgent),
40
isTouchDevice = /(Mobile|Android|Windows Phone)/.test(userAgent),
41
SVG_NS = 'http://www.w3.org/2000/svg',
42
hasSVG = !!doc.createElementNS && !!doc.createElementNS(SVG_NS, 'svg').createSVGRect,
43
hasBidiBug = isFirefox && parseInt(userAgent.split('Firefox/')[1], 10) < 4, // issue #38
44
useCanVG = !hasSVG && !isIE && !!doc.createElement('canvas').getContext,
51
dateFormat, // function
55
noop = function () {},
57
PRODUCT = 'Highstock',
60
// some constants for frequently used strings
62
ABSOLUTE = 'absolute',
63
RELATIVE = 'relative',
65
PREFIX = 'highcharts-',
71
numRegex = /^[0-9]+$/,
73
HOVER_STATE = 'hover',
74
SELECT_STATE = 'select',
75
MILLISECOND = 'millisecond',
84
// Object for extending Axis
85
AxisPlotLineOrBandExtension,
87
// constants for attributes
88
STROKE_WIDTH = 'stroke-width',
90
// time methods, changed based on whether or not UTC is used
106
// lookup over the types and the associated classes
109
// The Highcharts namespace
110
var Highcharts = win.Highcharts = win.Highcharts ? error(16, true) : {};
113
* Extend an object with the members of another
114
* @param {Object} a The object to be extended
115
* @param {Object} b The object to add to the first one
117
function extend(a, b) {
129
* Deep merge two or more objects and return a third object. If the first argument is
130
* true, the contents of the second object is copied into the first object.
131
* Previously this function redirected to jQuery.extend(true), but this had two limitations.
132
* First, it deep merged arrays, which lead to workarounds in Highcharts. Second,
133
* it copied properties from extended prototypes.
140
doCopy = function (copy, original) {
143
// An object is replacing a primitive
144
if (typeof copy !== 'object') {
148
for (key in original) {
149
if (original.hasOwnProperty(key)) {
150
value = original[key];
152
// Copy the contents of objects, but not arrays or DOM nodes
153
if (value && typeof value === 'object' && Object.prototype.toString.call(value) !== '[object Array]'
154
&& key !== 'renderTo' && typeof value.nodeType !== 'number') {
155
copy[key] = doCopy(copy[key] || {}, value);
157
// Primitives and arrays are copied over directly
159
copy[key] = original[key];
166
// If first argument is true, copy into the existing object. Used in setOptions.
167
if (args[0] === true) {
169
args = Array.prototype.slice.call(args, 2);
172
// For each argument, extend the return
174
for (i = 0; i < len; i++) {
175
ret = doCopy(ret, args[i]);
182
* Take an array and turn into a hash with even number arguments as keys and odd numbers as
183
* values. Allows creating constants for commonly used style properties, attributes etc.
184
* Avoid it in performance critical situations like looping
189
length = args.length,
191
for (; i < length; i++) {
192
obj[args[i++]] = args[i];
198
* Shortcut for parseInt
200
* @param {Number} mag Magnitude
202
function pInt(s, mag) {
203
return parseInt(s, mag || 10);
210
function isString(s) {
211
return typeof s === 'string';
216
* @param {Object} obj
218
function isObject(obj) {
219
return typeof obj === 'object';
224
* @param {Object} obj
226
function isArray(obj) {
227
return Object.prototype.toString.call(obj) === '[object Array]';
234
function isNumber(n) {
235
return typeof n === 'number';
238
function log2lin(num) {
239
return math.log(num) / math.LN10;
241
function lin2log(num) {
242
return math.pow(10, num);
246
* Remove last occurence of an item from an array
248
* @param {Mixed} item
250
function erase(arr, item) {
253
if (arr[i] === item) {
262
* Returns true if the object is not null or undefined. Like MooTools' $.defined.
263
* @param {Object} obj
265
function defined(obj) {
266
return obj !== UNDEFINED && obj !== null;
270
* Set or get an attribute or an object of attributes. Can't use jQuery attr because
271
* it attempts to set expando properties on the SVG element, which is not allowed.
273
* @param {Object} elem The DOM element to receive the attribute(s)
274
* @param {String|Object} prop The property or an abject of key-value pairs
275
* @param {String} value The value if a single property is set
277
function attr(elem, prop, value) {
279
setAttribute = 'setAttribute',
282
// if the prop is a string
283
if (isString(prop)) {
285
if (defined(value)) {
287
elem[setAttribute](prop, value);
290
} else if (elem && elem.getAttribute) { // elem not defined when printing pie demo...
291
ret = elem.getAttribute(prop);
294
// else if prop is defined, it is a hash of key/value pairs
295
} else if (defined(prop) && isObject(prop)) {
297
elem[setAttribute](key, prop[key]);
303
* Check if an element is an array, and if not, make it into an array. Like
306
function splat(obj) {
307
return isArray(obj) ? obj : [obj];
312
* Return the first value that is defined. Like MooTools' $.pick.
315
var args = arguments,
318
length = args.length;
319
for (i = 0; i < length; i++) {
321
if (typeof arg !== 'undefined' && arg !== null) {
328
* Set CSS on a given element
330
* @param {Object} styles Style object with camel case property names
332
function css(el, styles) {
333
if (isIE && !hasSVG) { // #2686
334
if (styles && styles.opacity !== UNDEFINED) {
335
styles.filter = 'alpha(opacity=' + (styles.opacity * 100) + ')';
338
extend(el.style, styles);
342
* Utility function to create element with attributes and styles
343
* @param {Object} tag
344
* @param {Object} attribs
345
* @param {Object} styles
346
* @param {Object} parent
347
* @param {Object} nopad
349
function createElement(tag, attribs, styles, parent, nopad) {
350
var el = doc.createElement(tag);
355
css(el, {padding: 0, border: NONE, margin: 0});
361
parent.appendChild(el);
367
* Extend a prototyped class by new members
368
* @param {Object} parent
369
* @param {Object} members
371
function extendClass(parent, members) {
372
var object = function () {};
373
object.prototype = new parent();
374
extend(object.prototype, members);
379
* Format a number and return a string based on input settings
380
* @param {Number} number The input number to format
381
* @param {Number} decimals The amount of decimals
382
* @param {String} decPoint The decimal point, defaults to the one given in the lang options
383
* @param {String} thousandsSep The thousands separator, defaults to the one given in the lang options
385
function numberFormat(number, decimals, decPoint, thousandsSep) {
386
var lang = defaultOptions.lang,
387
// http://kevin.vanzonneveld.net/techblog/article/javascript_equivalent_for_phps_number_format/
389
c = decimals === -1 ?
390
(n.toString().split('.')[1] || '').length : // preserve decimals
391
(isNaN(decimals = mathAbs(decimals)) ? 2 : decimals),
392
d = decPoint === undefined ? lang.decimalPoint : decPoint,
393
t = thousandsSep === undefined ? lang.thousandsSep : thousandsSep,
394
s = n < 0 ? "-" : "",
395
i = String(pInt(n = mathAbs(n).toFixed(c))),
396
j = i.length > 3 ? i.length % 3 : 0;
398
return s + (j ? i.substr(0, j) + t : "") + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + t) +
399
(c ? d + mathAbs(n - i).toFixed(c).slice(2) : "");
403
* Pad a string to a given length by adding 0 to the beginning
404
* @param {Number} number
405
* @param {Number} length
407
function pad(number, length) {
408
// Create an array of the remaining length +1 and join it with 0's
409
return new Array((length || 2) + 1 - String(number).length).join(0) + number;
413
* Wrap a method with extended functionality, preserving the original function
414
* @param {Object} obj The context object that the method belongs to
415
* @param {String} method The name of the method to extend
416
* @param {Function} func A wrapper function callback. This function is called with the same arguments
417
* as the original function, except that the original function is unshifted and passed as the first
420
function wrap(obj, method, func) {
421
var proceed = obj[method];
422
obj[method] = function () {
423
var args = Array.prototype.slice.call(arguments);
424
args.unshift(proceed);
425
return func.apply(this, args);
430
* Based on http://www.php.net/manual/en/function.strftime.php
431
* @param {String} format
432
* @param {Number} timestamp
433
* @param {Boolean} capitalize
435
dateFormat = function (format, timestamp, capitalize) {
436
if (!defined(timestamp) || isNaN(timestamp)) {
437
return 'Invalid date';
439
format = pick(format, '%Y-%m-%d %H:%M:%S');
441
var date = new Date(timestamp - timezoneOffset),
442
key, // used in for constuct below
443
// get the basic time values
444
hours = date[getHours](),
445
day = date[getDay](),
446
dayOfMonth = date[getDate](),
447
month = date[getMonth](),
448
fullYear = date[getFullYear](),
449
lang = defaultOptions.lang,
450
langWeekdays = lang.weekdays,
452
// List all format keys. Custom formats can be added from the outside.
453
replacements = extend({
456
'a': langWeekdays[day].substr(0, 3), // Short weekday, like 'Mon'
457
'A': langWeekdays[day], // Long weekday, like 'Monday'
458
'd': pad(dayOfMonth), // Two digit day of the month, 01 to 31
459
'e': dayOfMonth, // Day of the month, 1 through 31
461
// Week (none implemented)
465
'b': lang.shortMonths[month], // Short month, like 'Jan'
466
'B': lang.months[month], // Long month, like 'January'
467
'm': pad(month + 1), // Two digit month number, 01 through 12
470
'y': fullYear.toString().substr(2, 2), // Two digits year, like 09 for 2009
471
'Y': fullYear, // Four digits year, like 2009
474
'H': pad(hours), // Two digits hours in 24h format, 00 through 23
475
'I': pad((hours % 12) || 12), // Two digits hours in 12h format, 00 through 11
476
'l': (hours % 12) || 12, // Hours in 12h format, 1 through 12
477
'M': pad(date[getMinutes]()), // Two digits minutes, 00 through 59
478
'p': hours < 12 ? 'AM' : 'PM', // Upper case AM or PM
479
'P': hours < 12 ? 'am' : 'pm', // Lower case AM or PM
480
'S': pad(date.getSeconds()), // Two digits seconds, 00 through 59
481
'L': pad(mathRound(timestamp % 1000), 3) // Milliseconds (naming from Ruby)
482
}, Highcharts.dateFormats);
486
for (key in replacements) {
487
while (format.indexOf('%' + key) !== -1) { // regex would do it in one line, but this is faster
488
format = format.replace('%' + key, typeof replacements[key] === 'function' ? replacements[key](timestamp) : replacements[key]);
492
// Optionally capitalize the string and return
493
return capitalize ? format.substr(0, 1).toUpperCase() + format.substr(1) : format;
497
* Format a single variable. Similar to sprintf, without the % prefix.
499
function formatSingle(format, val) {
500
var floatRegex = /f$/,
501
decRegex = /\.([0-9])/,
502
lang = defaultOptions.lang,
505
if (floatRegex.test(format)) { // float
506
decimals = format.match(decRegex);
507
decimals = decimals ? decimals[1] : -1;
512
format.indexOf(',') > -1 ? lang.thousandsSep : ''
515
val = dateFormat(format, val);
521
* Format a string according to a subset of the rules of Python's String.format method.
523
function format(str, ctx) {
535
while ((index = str.indexOf(splitter)) !== -1) {
537
segment = str.slice(0, index);
538
if (isInside) { // we're on the closing bracket looking back
540
valueAndFormat = segment.split(':');
541
path = valueAndFormat.shift().split('.'); // get first and leave format
545
// Assign deeper paths
546
for (i = 0; i < len; i++) {
550
// Format the replacement
551
if (valueAndFormat.length) {
552
val = formatSingle(valueAndFormat.join(':'), val);
555
// Push the result and advance the cursor
562
str = str.slice(index + 1); // the rest
563
isInside = !isInside; // toggle
564
splitter = isInside ? '}' : '{'; // now look for next matching bracket
571
* Get the magnitude of a number
573
function getMagnitude(num) {
574
return math.pow(10, mathFloor(math.log(num) / math.LN10));
578
* Take an interval and normalize it to multiples of 1, 2, 2.5 and 5
579
* @param {Number} interval
580
* @param {Array} multiples
581
* @param {Number} magnitude
582
* @param {Object} options
584
function normalizeTickInterval(interval, multiples, magnitude, options) {
587
// round to a tenfold of 1, 2, 2.5 or 5
588
magnitude = pick(magnitude, 1);
589
normalized = interval / magnitude;
591
// multiples for a linear scale
593
multiples = [1, 2, 2.5, 5, 10];
595
// the allowDecimals option
596
if (options && options.allowDecimals === false) {
597
if (magnitude === 1) {
598
multiples = [1, 2, 5, 10];
599
} else if (magnitude <= 0.1) {
600
multiples = [1 / magnitude];
605
// normalize the interval to the nearest multiple
606
for (i = 0; i < multiples.length; i++) {
607
interval = multiples[i];
608
if (normalized <= (multiples[i] + (multiples[i + 1] || multiples[i])) / 2) {
613
// multiply back to the correct magnitude
614
interval *= magnitude;
621
* Helper class that contains variuos counters that are local to the chart.
623
function ChartCounters() {
628
ChartCounters.prototype = {
630
* Wraps the color counter if it reaches the specified length.
632
wrapColor: function (length) {
633
if (this.color >= length) {
639
* Wraps the symbol counter if it reaches the specified length.
641
wrapSymbol: function (length) {
642
if (this.symbol >= length) {
650
* Utility method that sorts an object array and keeping the order of equal items.
651
* ECMA script standard does not specify the behaviour when items are equal.
653
function stableSort(arr, sortFunction) {
654
var length = arr.length,
658
// Add index to each item
659
for (i = 0; i < length; i++) {
660
arr[i].ss_i = i; // stable sort index
663
arr.sort(function (a, b) {
664
sortValue = sortFunction(a, b);
665
return sortValue === 0 ? a.ss_i - b.ss_i : sortValue;
668
// Remove index from items
669
for (i = 0; i < length; i++) {
670
delete arr[i].ss_i; // stable sort index
675
* Non-recursive method to find the lowest member of an array. Math.min raises a maximum
676
* call stack size exceeded error in Chrome when trying to apply more than 150.000 points. This
677
* method is slightly slower, but safe.
679
function arrayMin(data) {
692
* Non-recursive method to find the lowest member of an array. Math.min raises a maximum
693
* call stack size exceeded error in Chrome when trying to apply more than 150.000 points. This
694
* method is slightly slower, but safe.
696
function arrayMax(data) {
709
* Utility method that destroys any SVGElement or VMLElement that are properties on the given object.
710
* It loops all properties and invokes destroy if there is a destroy method. The property is
712
* @param {Object} The object to destroy properties on
713
* @param {Object} Exception, do not destroy this property, only delete it.
715
function destroyObjectProperties(obj, except) {
718
// If the object is non-null and destroy is defined
719
if (obj[n] && obj[n] !== except && obj[n].destroy) {
720
// Invoke the destroy
724
// Delete the property from the object.
731
* Discard an element by moving it to the bin and delete
732
* @param {Object} The HTML node to discard
734
function discardElement(element) {
735
// create a garbage bin element, not part of the DOM
737
garbageBin = createElement(DIV);
740
// move the node and empty bin
742
garbageBin.appendChild(element);
744
garbageBin.innerHTML = '';
748
* Provide error messages for debugging, with links to online explanation
750
function error(code, stop) {
751
var msg = 'Highcharts error #' + code + ': www.highcharts.com/errors/' + code;
754
} else if (win.console) {
760
* Fix JS round off float errors
761
* @param {Number} num
763
function correctFloat(num) {
770
* Set the global animation to either a given value, or fall back to the
771
* given chart's animation option
772
* @param {Object} animation
773
* @param {Object} chart
775
function setAnimation(animation, chart) {
776
globalAnimation = pick(animation, chart.animation);
780
* The time unit lookup
782
/*jslint white: true*/
789
WEEK, 7 * 24 * 3600000,
790
MONTH, 31 * 24 * 3600000,
793
/*jslint white: false*/
795
* Path interpolation algorithm used across adapters
799
* Prepare start and end values so that the path can be animated one to one
801
init: function (elem, fromD, toD) {
803
var shift = elem.shift,
804
bezier = fromD.indexOf('C') > -1,
805
numParams = bezier ? 7 : 3,
809
start = fromD.split(' '),
810
end = [].concat(toD), // copy
813
sixify = function (arr) { // in splines make move points have six parameters like bezier curves
817
arr.splice(i + 1, 0, arr[i + 1], arr[i + 2], arr[i + 1], arr[i + 2]);
827
// pull out the base lines before padding
829
startBaseLine = start.splice(start.length - 6, 6);
830
endBaseLine = end.splice(end.length - 6, 6);
833
// if shifting points, prepend a dummy point to the end path
834
if (shift <= end.length / numParams && start.length === end.length) {
836
end = [].concat(end).splice(0, numParams).concat(end);
839
elem.shift = 0; // reset for following animations
841
// copy and append last point until the length matches the end length
843
endLength = end.length;
844
while (start.length < endLength) {
846
//bezier && sixify(start);
847
slice = [].concat(start).splice(start.length - numParams, numParams);
848
if (bezier) { // disable first control point
849
slice[numParams - 6] = slice[numParams - 2];
850
slice[numParams - 5] = slice[numParams - 1];
852
start = start.concat(slice);
856
if (startBaseLine) { // append the base lines for areas
857
start = start.concat(startBaseLine);
858
end = end.concat(endBaseLine);
864
* Interpolate each value of the path and return the array
866
step: function (start, end, pos, complete) {
871
if (pos === 1) { // land on the final path without adjustment points appended in the ends
874
} else if (i === end.length && pos < 1) {
876
startVal = parseFloat(start[i]);
878
isNaN(startVal) ? // a letter instruction like M or L
880
pos * (parseFloat(end[i] - startVal)) + startVal;
883
} else { // if animation is finished or length not matching, land on right value
892
* The default HighchartsAdapter for jQuery
894
win.HighchartsAdapter = win.HighchartsAdapter || ($ && {
897
* Initialize the adapter by applying some extensions to jQuery
899
init: function (pathAnim) {
901
// extend the animate function to allow SVG animations
906
propHooks = Tween && Tween.propHooks,
907
opacityHook = $.cssHooks.opacity;
909
/*jslint unparam: true*//* allow unused param x in this function */
911
easeOutQuad: function (x, t, b, c, d) {
912
return -c * (t /= d) * (t - 2) + b;
915
/*jslint unparam: false*/
917
// extend some methods to check for elem.attr, which means it is a Highcharts SVG object
918
$.each(['cur', '_default', 'width', 'height', 'opacity'], function (i, fn) {
922
// Handle different parent objects
924
obj = Fx.prototype; // 'cur', the getter, relates to Fx.prototype
926
} else if (fn === '_default' && Tween) { // jQuery 1.8 model
931
// Overwrite the method
933
if (base) { // step.width and step.height don't exist in jQuery < 1.7
935
// create the extended function replacement
936
obj[fn] = function (fx) {
940
// Fx.prototype.cur does not use fx argument
943
// Don't run animations on textual properties like align (#1821)
944
if (fx.prop === 'align') {
951
// Fx.prototype.cur returns the current value. The other ones are setters
952
// and returning a value has no effect.
953
return elem.attr ? // is SVG element wrapper
954
elem.attr(fx.prop, fn === 'cur' ? UNDEFINED : fx.now) : // apply the SVG wrapper's method
955
base.apply(this, arguments); // use jQuery's built-in method
960
// Extend the opacity getter, needed for fading opacity with IE9 and jQuery 1.10+
961
wrap(opacityHook, 'get', function (proceed, elem, computed) {
962
return elem.attr ? (elem.opacity || 0) : proceed.call(this, elem, computed);
966
// Define the setter function for d (path definitions)
967
dSetter = function (fx) {
971
// Normally start and end should be set in state == 0, but sometimes,
972
// for reasons unknown, this doesn't happen. Perhaps state == 0 is skipped
975
ends = pathAnim.init(elem, elem.d, elem.toD);
982
// interpolate each value of the path
983
elem.attr('d', pathAnim.step(fx.start, fx.end, fx.pos, elem.toD));
998
* Utility for iterating over an array. Parameters are reversed compared to jQuery.
1000
* @param {Function} fn
1002
this.each = Array.prototype.forEach ?
1003
function (arr, fn) { // modern browsers
1004
return Array.prototype.forEach.call(arr, fn);
1007
function (arr, fn) { // legacy
1010
for (; i < len; i++) {
1011
if (fn.call(arr[i], arr[i], i, arr) === false) {
1018
* Register Highcharts as a plugin in the respective framework
1020
$.fn.highcharts = function () {
1021
var constr = 'Chart', // default constructor
1027
if (isString(args[0])) {
1029
args = Array.prototype.slice.call(args, 1);
1034
if (options !== UNDEFINED) {
1035
/*jslint unused:false*/
1036
options.chart = options.chart || {};
1037
options.chart.renderTo = this[0];
1038
chart = new Highcharts[constr](options, args[1]);
1040
/*jslint unused:true*/
1043
// When called without parameters or with the return argument, get a predefined chart
1044
if (options === UNDEFINED) {
1045
ret = charts[attr(this[0], 'data-highcharts-chart')];
1055
* Downloads a script and executes a callback when done.
1056
* @param {String} scriptLocation
1057
* @param {Function} callback
1059
getScript: $.getScript,
1062
* Return the index of an item in an array, or -1 if not found
1067
* A direct link to jQuery methods. MooTools and Prototype adapters must be implemented for each case of method.
1068
* @param {Object} elem The HTML element
1069
* @param {String} method Which method to run on the wrapped element
1071
adapterRun: function (elem, method) {
1072
return $(elem)[method]();
1082
* @param {Array} arr
1083
* @param {Function} fn
1085
map: function (arr, fn) {
1086
//return jQuery.map(arr, fn);
1090
for (; i < len; i++) {
1091
results[i] = fn.call(arr[i], arr[i], i, arr);
1098
* Get the position of an element relative to the top left of the page
1100
offset: function (el) {
1101
return $(el).offset();
1105
* Add an event listener
1106
* @param {Object} el A HTML element or custom object
1107
* @param {String} event The event type
1108
* @param {Function} fn The event handler
1110
addEvent: function (el, event, fn) {
1111
$(el).bind(event, fn);
1115
* Remove event added with addEvent
1116
* @param {Object} el The object
1117
* @param {String} eventType The event type. Leave blank to remove all events.
1118
* @param {Function} handler The function to remove
1120
removeEvent: function (el, eventType, handler) {
1121
// workaround for jQuery issue with unbinding custom events:
1122
// http://forum.jQuery.com/topic/javascript-error-when-unbinding-a-custom-event-using-jQuery-1-4-2
1123
var func = doc.removeEventListener ? 'removeEventListener' : 'detachEvent';
1124
if (doc[func] && el && !el[func]) {
1125
el[func] = function () {};
1128
$(el).unbind(eventType, handler);
1132
* Fire an event on a custom object
1133
* @param {Object} el
1134
* @param {String} type
1135
* @param {Object} eventArguments
1136
* @param {Function} defaultFunction
1138
fireEvent: function (el, type, eventArguments, defaultFunction) {
1139
var event = $.Event(type),
1140
detachedType = 'detached' + type,
1143
// Remove warnings in Chrome when accessing layerX and layerY. Although Highcharts
1144
// never uses these properties, Chrome includes them in the default click event and
1145
// raises the warning when they are copied over in the extend statement below.
1147
// To avoid problems in IE (see #1010) where we cannot delete the properties and avoid
1148
// testing if they are there (warning in chrome) the only option is to test if running IE.
1149
if (!isIE && eventArguments) {
1150
delete eventArguments.layerX;
1151
delete eventArguments.layerY;
1154
extend(event, eventArguments);
1156
// Prevent jQuery from triggering the object method that is named the
1157
// same as the event. For example, if the event is 'select', jQuery
1158
// attempts calling el.select and it goes into a loop.
1160
el[detachedType] = el[type];
1164
// Wrap preventDefault and stopPropagation in try/catch blocks in
1165
// order to prevent JS errors when cancelling events on non-DOM
1167
/*jslint unparam: true*/
1168
$.each(['preventDefault', 'stopPropagation'], function (i, fn) {
1169
var base = event[fn];
1170
event[fn] = function () {
1174
if (fn === 'preventDefault') {
1175
defaultPrevented = true;
1180
/*jslint unparam: false*/
1183
$(el).trigger(event);
1185
// attach the method
1186
if (el[detachedType]) {
1187
el[type] = el[detachedType];
1188
el[detachedType] = null;
1191
if (defaultFunction && !event.isDefaultPrevented() && !defaultPrevented) {
1192
defaultFunction(event);
1197
* Extension method needed for MooTools
1199
washMouseEvent: function (e) {
1200
var ret = e.originalEvent || e;
1202
// computed by jQuery, needed by IE8
1203
if (ret.pageX === UNDEFINED) { // #1236
1204
ret.pageX = e.pageX;
1205
ret.pageY = e.pageY;
1212
* Animate a HTML element or SVG element wrapper
1213
* @param {Object} el
1214
* @param {Object} params
1215
* @param {Object} options jQuery-like animation options: duration, easing, callback
1217
animate: function (el, params, options) {
1220
el.style = {}; // #1881
1223
el.toD = params.d; // keep the array form for paths, used in $.fx.step.d
1224
params.d = 1; // because in jQuery, animating to an array has a different meaning
1228
if (params.opacity !== UNDEFINED && el.attr) {
1229
params.opacity += 'px'; // force jQuery to use same logic as width and height (#2161)
1231
$el.animate(params, options);
1235
* Stop running animation
1237
stop: function (el) {
1244
// check for a custom HighchartsAdapter defined prior to this file
1245
var globalAdapter = win.HighchartsAdapter,
1246
adapter = globalAdapter || {};
1248
// Initialize the adapter
1249
if (globalAdapter) {
1250
globalAdapter.init.call(globalAdapter, pathAnim);
1254
// Utility functions. If the HighchartsAdapter is not defined, adapter is an empty object
1255
// and all the utility functions will be null. In that case they are populated by the
1256
// default adapters below.
1257
var adapterRun = adapter.adapterRun,
1258
getScript = adapter.getScript,
1259
inArray = adapter.inArray,
1260
each = adapter.each,
1261
grep = adapter.grep,
1262
offset = adapter.offset,
1264
addEvent = adapter.addEvent,
1265
removeEvent = adapter.removeEvent,
1266
fireEvent = adapter.fireEvent,
1267
washMouseEvent = adapter.washMouseEvent,
1268
animate = adapter.animate,
1269
stop = adapter.stop;
1273
/* ****************************************************************************
1274
* Handle the options *
1275
*****************************************************************************/
1278
defaultLabelOptions = {
1284
/*formatter: function () {
1295
colors: ['#2f7ed8', '#0d233a', '#8bbc21', '#910000', '#1aadce', '#492970',
1296
'#f28f43', '#77a1e5', '#c42525', '#a6c96a'],
1297
//colors: ['#8085e8', '#252530', '#90ee7e', '#8d4654', '#2b908f', '#76758e', '#f6a45c', '#7eb5ee', '#f45b5b', '#9ff0cf'],
1298
symbols: ['circle', 'diamond', 'square', 'triangle', 'triangle-down'],
1300
loading: 'Loading...',
1301
months: ['January', 'February', 'March', 'April', 'May', 'June', 'July',
1302
'August', 'September', 'October', 'November', 'December'],
1303
shortMonths: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
1304
weekdays: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
1306
numericSymbols: ['k', 'M', 'G', 'T', 'P', 'E'], // SI prefixes used in axis labels
1307
resetZoom: 'Reset zoom',
1308
resetZoomTitle: 'Reset zoom level 1:1',
1313
//timezoneOffset: 0,
1314
canvasToolsURL: 'http://code.highcharts.com/stock/1.3.10/modules/canvas-tools.js',
1315
VMLRadialGradientURL: 'http://code.highcharts.com/stock/1.3.10/gfx/vml-radial-gradient.png'
1319
//alignTicks: false,
1322
//events: { load, selection },
1325
//marginRight: null,
1326
//marginBottom: null,
1328
borderColor: '#4572A7',
1331
defaultSeriesType: 'line',
1332
ignoreHiddenSeries: true,
1335
spacing: [10, 10, 15, 10],
1338
//spacingBottom: 15,
1341
// fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif', // default font
1344
backgroundColor: '#FFFFFF',
1345
//plotBackgroundColor: null,
1346
plotBorderColor: '#C0C0C0',
1347
//plotBorderWidth: 0,
1348
//plotShadow: false,
1357
//verticalAlign: 'top',
1360
// relativeTo: 'plot'
1364
text: 'Chart title',
1369
// verticalAlign: 'top',
1372
color: '#274b6d',//#3E576F',
1382
// verticalAlign: 'top',
1390
line: { // base series options
1391
allowPointSelect: false,
1392
showCheckbox: false,
1396
//connectNulls: false,
1397
//cursor: 'default',
1400
//enableMouseTracking: true,
1412
lineColor: '#FFFFFF',
1414
states: { // states for a single point
1420
fillColor: '#FFFFFF',
1421
lineColor: '#000000',
1429
dataLabels: merge(defaultLabelOptions, {
1432
formatter: function () {
1433
return this.y === null ? '' : numberFormat(this.y, -1);
1435
verticalAlign: 'bottom', // above singular point
1437
// backgroundColor: undefined,
1438
// borderColor: undefined,
1439
// borderRadius: undefined,
1440
// borderWidth: undefined,
1444
cropThreshold: 300, // draw points outside the plot area when the number of points is less than this
1448
//showInLegend: null, // auto: true for standalone series, false for linked series
1449
states: { // states for the entire series
1452
//lineWidth: base + 1,
1454
// lineWidth: base + 1,
1462
stickyTracking: true,
1464
//pointFormat: '<span style="color:{series.color}">{series.name}</span>: <b>{point.y}</b>'
1465
//valueDecimals: null,
1466
//xDateFormat: '%A, %b %e, %Y',
1470
turboThreshold: 1000
1477
//font: defaultFont,
1486
layout: 'horizontal',
1487
labelFormatter: function () {
1491
borderColor: '#909090',
1495
activeColor: '#274b6d',
1497
inactiveColor: '#CCC'
1498
// style: {} // text styles
1503
// backgroundColor: null,
1512
//cursor: 'pointer', removed as of #601
1518
itemCheckboxStyle: {
1520
width: '13px', // for IE precision
1523
// itemWidth: undefined,
1526
verticalAlign: 'bottom',
1527
// width: undefined,
1539
// hideDuration: 100,
1548
backgroundColor: 'white',
1558
backgroundColor: 'rgba(255, 255, 255, .85)',
1561
dateTimeLabelFormats: {
1562
millisecond: '%A, %b %e, %H:%M:%S.%L',
1563
second: '%A, %b %e, %H:%M:%S',
1564
minute: '%A, %b %e, %H:%M',
1565
hour: '%A, %b %e, %H:%M',
1566
day: '%A, %b %e, %Y',
1567
week: 'Week from %A, %b %e, %Y',
1571
//formatter: defaultFormatter,
1572
headerFormat: '<span style="font-size: 10px">{point.key}</span><br/>',
1573
pointFormat: '<span style="color:{series.color}">{series.name}</span>: <b>{point.y}</b><br/>',
1576
snap: isTouchDevice ? 25 : 10,
1582
whiteSpace: 'nowrap'
1584
//xDateFormat: '%A, %b %e, %Y',
1585
//valueDecimals: null,
1592
text: 'Highcharts.com',
1593
href: 'http://www.highcharts.com',
1597
verticalAlign: 'bottom',
1612
var defaultPlotOptions = defaultOptions.plotOptions,
1613
defaultSeriesOptions = defaultPlotOptions.line;
1615
// set the default time methods
1621
* Set the time methods globally based on the useUTC option. Time method can be either
1622
* local time or UTC (default).
1624
function setTimeMethods() {
1625
var useUTC = defaultOptions.global.useUTC,
1626
GET = useUTC ? 'getUTC' : 'get',
1627
SET = useUTC ? 'setUTC' : 'set';
1630
timezoneOffset = ((useUTC && defaultOptions.global.timezoneOffset) || 0) * 60000;
1631
makeTime = useUTC ? Date.UTC : function (year, month, date, hours, minutes, seconds) {
1641
getMinutes = GET + 'Minutes';
1642
getHours = GET + 'Hours';
1643
getDay = GET + 'Day';
1644
getDate = GET + 'Date';
1645
getMonth = GET + 'Month';
1646
getFullYear = GET + 'FullYear';
1647
setMinutes = SET + 'Minutes';
1648
setHours = SET + 'Hours';
1649
setDate = SET + 'Date';
1650
setMonth = SET + 'Month';
1651
setFullYear = SET + 'FullYear';
1656
* Merge the default options with custom options and return the new options structure
1657
* @param {Object} options The new custom options
1659
function setOptions(options) {
1661
// Copy in the default options
1662
defaultOptions = merge(true, defaultOptions, options);
1667
return defaultOptions;
1671
* Get the updated default options. Until 3.0.7, merely exposing defaultOptions for outside modules
1672
* wasn't enough because the setOptions method created a new object.
1674
function getOptions() {
1675
return defaultOptions;
1680
* Handle color operations. The object methods are chainable.
1681
* @param {String} input The input color in either rbga or hex format
1683
var rgbaRegEx = /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*\)/,
1684
hexRegEx = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/,
1685
rgbRegEx = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/;
1687
var Color = function (input) {
1688
// declare variables
1689
var rgba = [], result, stops;
1692
* Parse the input color to rgba array
1693
* @param {String} input
1695
function init(input) {
1698
if (input && input.stops) {
1699
stops = map(input.stops, function (stop) {
1700
return Color(stop[1]);
1706
result = rgbaRegEx.exec(input);
1708
rgba = [pInt(result[1]), pInt(result[2]), pInt(result[3]), parseFloat(result[4], 10)];
1711
result = hexRegEx.exec(input);
1713
rgba = [pInt(result[1], 16), pInt(result[2], 16), pInt(result[3], 16), 1];
1716
result = rgbRegEx.exec(input);
1718
rgba = [pInt(result[1]), pInt(result[2]), pInt(result[3]), 1];
1726
* Return the color a specified format
1727
* @param {String} format
1729
function get(format) {
1734
ret.stops = [].concat(ret.stops);
1735
each(stops, function (stop, i) {
1736
ret.stops[i] = [ret.stops[i][0], stop.get(format)];
1739
// it's NaN if gradient colors on a column chart
1740
} else if (rgba && !isNaN(rgba[0])) {
1741
if (format === 'rgb') {
1742
ret = 'rgb(' + rgba[0] + ',' + rgba[1] + ',' + rgba[2] + ')';
1743
} else if (format === 'a') {
1746
ret = 'rgba(' + rgba.join(',') + ')';
1755
* Brighten the color
1756
* @param {Number} alpha
1758
function brighten(alpha) {
1760
each(stops, function (stop) {
1761
stop.brighten(alpha);
1764
} else if (isNumber(alpha) && alpha !== 0) {
1766
for (i = 0; i < 3; i++) {
1767
rgba[i] += pInt(alpha * 255);
1772
if (rgba[i] > 255) {
1780
* Set the color's opacity to a given alpha value
1781
* @param {Number} alpha
1783
function setOpacity(alpha) {
1788
// initialize: parse the input
1796
setOpacity: setOpacity
1802
* A wrapper object for SVG elements
1804
function SVGElement() {}
1806
SVGElement.prototype = {
1808
* Initialize the SVG renderer
1809
* @param {Object} renderer
1810
* @param {String} nodeName
1812
init: function (renderer, nodeName) {
1814
wrapper.element = nodeName === 'span' ?
1815
createElement(nodeName) :
1816
doc.createElementNS(SVG_NS, nodeName);
1817
wrapper.renderer = renderer;
1819
* A collection of attribute setters. These methods, if defined, are called right before a certain
1820
* attribute is set on an element wrapper. Returning false prevents the default attribute
1821
* setter to run. Returning a value causes the default setter to set that value. Used in
1824
wrapper.attrSetters = {};
1827
* Default base for animation
1831
* Animate a given attribute
1832
* @param {Object} params
1833
* @param {Number} options The same options as in jQuery animation
1834
* @param {Function} complete Function to perform at the end of animation
1836
animate: function (params, options, complete) {
1837
var animOptions = pick(options, globalAnimation, true);
1838
stop(this); // stop regardless of animation actually running, or reverting to .attr (#607)
1840
animOptions = merge(animOptions, {}); //#2625
1841
if (complete) { // allows using a callback with the global animation without overwriting it
1842
animOptions.complete = complete;
1844
animate(this, params, animOptions);
1853
* Set or get a given attribute
1854
* @param {Object|String} hash
1855
* @param {Mixed|Undefined} val
1857
attr: function (hash, val) {
1864
element = wrapper.element,
1865
nodeName = element.nodeName.toLowerCase(), // Android2 requires lower for "text"
1866
renderer = wrapper.renderer,
1869
attrSetters = wrapper.attrSetters,
1870
shadows = wrapper.shadows,
1875
// single key-value pair
1876
if (isString(hash) && defined(val)) {
1882
// used as a getter: first argument is a string, second is undefined
1883
if (isString(hash)) {
1885
if (nodeName === 'circle') {
1886
key = { x: 'cx', y: 'cy' }[key] || key;
1887
} else if (key === 'strokeWidth') {
1888
key = 'stroke-width';
1890
ret = attr(element, key) || wrapper[key] || 0;
1891
if (key !== 'd' && key !== 'visibility' && key !== 'fill') { // 'd' is string in animation step
1892
ret = parseFloat(ret);
1899
skipAttr = false; // reset
1902
// check for a specific attribute setter
1903
result = attrSetters[key] && attrSetters[key].call(wrapper, value, key);
1905
if (result !== false) {
1906
if (result !== UNDEFINED) {
1907
value = result; // the attribute setter has returned a new value to set
1913
if (value && value.join) { // join path
1914
value = value.join(' ');
1916
if (/(NaN| {2}|^$)/.test(value)) {
1919
//wrapper.d = value; // shortcut for animations
1921
// update child tspans x values
1922
} else if (key === 'x' && nodeName === 'text') {
1923
for (i = 0; i < element.childNodes.length; i++) {
1924
child = element.childNodes[i];
1925
// if the x values are equal, the tspan represents a linebreak
1926
if (attr(child, 'x') === attr(element, 'x')) {
1927
//child.setAttribute('x', value);
1928
attr(child, 'x', value);
1932
} else if (wrapper.rotation && (key === 'x' || key === 'y')) {
1936
} else if (key === 'fill') {
1937
value = renderer.color(value, element, key);
1940
} else if (nodeName === 'circle' && (key === 'x' || key === 'y')) {
1941
key = { x: 'cx', y: 'cy' }[key] || key;
1943
// rectangle border radius
1944
} else if (nodeName === 'rect' && key === 'r') {
1951
// translation and text rotation
1952
} else if (key === 'translateX' || key === 'translateY' || key === 'rotation' ||
1953
key === 'verticalAlign' || key === 'scaleX' || key === 'scaleY') {
1957
// apply opacity as subnode (required by legacy WebKit and Batik)
1958
} else if (key === 'stroke') {
1959
value = renderer.color(value, element, key);
1961
// emulate VML's dashstyle implementation
1962
} else if (key === 'dashstyle') {
1963
key = 'stroke-dasharray';
1964
value = value && value.toLowerCase();
1965
if (value === 'solid') {
1969
.replace('shortdashdotdot', '3,1,1,1,1,1,')
1970
.replace('shortdashdot', '3,1,1,1')
1971
.replace('shortdot', '1,1,')
1972
.replace('shortdash', '3,1,')
1973
.replace('longdash', '8,3,')
1974
.replace(/dot/g, '1,3,')
1975
.replace('dash', '4,3,')
1977
.split(','); // ending comma
1981
value[i] = pInt(value[i]) * pick(hash['stroke-width'], wrapper['stroke-width']);
1983
value = value.join(',');
1986
// IE9/MooTools combo: MooTools returns objects instead of numbers and IE9 Beta 2
1987
// is unable to cast them. Test again with final IE9.
1988
} else if (key === 'width') {
1989
value = pInt(value);
1992
} else if (key === 'align') {
1993
key = 'text-anchor';
1994
value = { left: 'start', center: 'middle', right: 'end' }[value];
1996
// Title requires a subnode, #431
1997
} else if (key === 'title') {
1998
titleNode = element.getElementsByTagName('title')[0];
2000
titleNode = doc.createElementNS(SVG_NS, 'title');
2001
element.appendChild(titleNode);
2003
titleNode.textContent = value;
2006
// jQuery animate changes case
2007
if (key === 'strokeWidth') {
2008
key = 'stroke-width';
2011
// In Chrome/Win < 6 as well as Batik, the stroke attribute can't be set when the stroke-
2012
// width is 0. #1369
2013
if (key === 'stroke-width' || key === 'stroke') {
2014
wrapper[key] = value;
2015
// Only apply the stroke attribute if the stroke width is defined and larger than 0
2016
if (wrapper.stroke && wrapper['stroke-width']) {
2017
attr(element, 'stroke', wrapper.stroke);
2018
attr(element, 'stroke-width', wrapper['stroke-width']);
2019
wrapper.hasStroke = true;
2020
} else if (key === 'stroke-width' && value === 0 && wrapper.hasStroke) {
2021
element.removeAttribute('stroke');
2022
wrapper.hasStroke = false;
2028
if (wrapper.symbolName && /^(x|y|width|height|r|start|end|innerR|anchorX|anchorY)/.test(key)) {
2031
if (!hasSetSymbolSize) {
2032
wrapper.symbolAttr(hash);
2033
hasSetSymbolSize = true;
2038
// let the shadow follow the main element
2039
if (shadows && /^(width|height|visibility|x|y|d|transform|cx|cy|r)$/.test(key)) {
2046
mathMax(value - (shadows[i].cutHeight || 0), 0) :
2053
if ((key === 'width' || key === 'height') && nodeName === 'rect' && value < 0) {
2057
// Record for animation and quick access without polling the DOM
2058
wrapper[key] = value;
2061
if (key === 'text') {
2062
if (value !== wrapper.textStr) {
2064
// Delete bBox memo when the text changes
2065
delete wrapper.bBox;
2067
wrapper.textStr = value;
2068
if (wrapper.added) {
2069
renderer.buildText(wrapper);
2072
} else if (!skipAttr) {
2073
//attr(element, key, value);
2074
if (value !== undefined) {
2075
element.setAttribute(key, value);
2083
// Update transform. Do this outside the loop to prevent redundant updating for batch setting
2086
wrapper.updateTransform();
2096
* Add a class name to an element
2098
addClass: function (className) {
2099
var element = this.element,
2100
currentClassName = attr(element, 'class') || '';
2102
if (currentClassName.indexOf(className) === -1) {
2103
attr(element, 'class', currentClassName + ' ' + className);
2107
/* hasClass and removeClass are not (yet) needed
2108
hasClass: function (className) {
2109
return attr(this.element, 'class').indexOf(className) !== -1;
2111
removeClass: function (className) {
2112
attr(this.element, 'class', attr(this.element, 'class').replace(className, ''));
2118
* If one of the symbol size affecting parameters are changed,
2119
* check all the others only once for each call to an element's
2121
* @param {Object} hash
2123
symbolAttr: function (hash) {
2126
each(['x', 'y', 'r', 'start', 'end', 'width', 'height', 'innerR', 'anchorX', 'anchorY'], function (key) {
2127
wrapper[key] = pick(hash[key], wrapper[key]);
2131
d: wrapper.renderer.symbols[wrapper.symbolName](
2142
* Apply a clipping path to this object
2143
* @param {String} id
2145
clip: function (clipRect) {
2146
return this.attr('clip-path', clipRect ? 'url(' + this.renderer.url + '#' + clipRect.id + ')' : NONE);
2150
* Calculate the coordinates needed for drawing a rectangle crisply and return the
2151
* calculated attributes
2152
* @param {Number} strokeWidth
2155
* @param {Number} width
2156
* @param {Number} height
2158
crisp: function (rect) {
2164
strokeWidth = rect.strokeWidth || wrapper.strokeWidth || (wrapper.attr && wrapper.attr('stroke-width')) || 0;
2166
normalizer = mathRound(strokeWidth) % 2 / 2; // mathRound because strokeWidth can sometimes have roundoff errors
2168
// normalize for crisp edges
2169
rect.x = mathFloor(rect.x || wrapper.x || 0) + normalizer;
2170
rect.y = mathFloor(rect.y || wrapper.y || 0) + normalizer;
2171
rect.width = mathFloor((rect.width || wrapper.width || 0) - 2 * normalizer);
2172
rect.height = mathFloor((rect.height || wrapper.height || 0) - 2 * normalizer);
2173
rect.strokeWidth = strokeWidth;
2176
if (wrapper[key] !== rect[key]) { // only set attribute if changed
2177
wrapper[key] = attribs[key] = rect[key];
2185
* Set styles for the element
2186
* @param {Object} styles
2188
css: function (styles) {
2189
var elemWrapper = this,
2190
oldStyles = elemWrapper.styles,
2192
elem = elemWrapper.element,
2197
hasNew = !oldStyles;
2200
if (styles && styles.color) {
2201
styles.fill = styles.color;
2204
// Filter out existing styles to increase performance (#2640)
2207
if (styles[n] !== oldStyles[n]) {
2208
newStyles[n] = styles[n];
2214
textWidth = elemWrapper.textWidth = styles && styles.width && elem.nodeName.toLowerCase() === 'text' && pInt(styles.width);
2216
// Merge the new styles with the old ones
2225
elemWrapper.styles = styles;
2227
if (textWidth && (useCanVG || (!hasSVG && elemWrapper.renderer.forExport))) {
2228
delete styles.width;
2231
// serialize and set style attribute
2232
if (isIE && !hasSVG) {
2233
css(elemWrapper.element, styles);
2235
/*jslint unparam: true*/
2236
hyphenate = function (a, b) { return '-' + b.toLowerCase(); };
2237
/*jslint unparam: false*/
2239
serializedCss += n.replace(/([A-Z])/g, hyphenate) + ':' + styles[n] + ';';
2241
attr(elem, 'style', serializedCss); // #1881
2246
if (textWidth && elemWrapper.added) {
2247
elemWrapper.renderer.buildText(elemWrapper);
2255
* Add an event listener
2256
* @param {String} eventType
2257
* @param {Function} handler
2259
on: function (eventType, handler) {
2260
var svgElement = this,
2261
element = svgElement.element;
2264
if (hasTouch && eventType === 'click') {
2265
element.ontouchstart = function (e) {
2266
svgElement.touchEventFired = Date.now();
2268
handler.call(element, e);
2270
element.onclick = function (e) {
2271
if (userAgent.indexOf('Android') === -1 || Date.now() - (svgElement.touchEventFired || 0) > 1100) { // #2269
2272
handler.call(element, e);
2276
// simplest possible event model for internal use
2277
element['on' + eventType] = handler;
2283
* Set the coordinates needed to draw a consistent radial gradient across
2284
* pie slices regardless of positioning inside the chart. The format is
2285
* [centerX, centerY, diameter] in pixels.
2287
setRadialReference: function (coordinates) {
2288
this.element.radialReference = coordinates;
2293
* Move an object and its children by x and y values
2297
translate: function (x, y) {
2305
* Invert a group, rotate and flip
2307
invert: function () {
2309
wrapper.inverted = true;
2310
wrapper.updateTransform();
2315
* Private method to update the transform attribute based on internal
2318
updateTransform: function () {
2320
translateX = wrapper.translateX || 0,
2321
translateY = wrapper.translateY || 0,
2322
scaleX = wrapper.scaleX,
2323
scaleY = wrapper.scaleY,
2324
inverted = wrapper.inverted,
2325
rotation = wrapper.rotation,
2328
// flipping affects translate as adjustment for flipping around the group's axis
2330
translateX += wrapper.attr('width');
2331
translateY += wrapper.attr('height');
2334
// Apply translate. Nearly all transformed elements have translation, so instead
2335
// of checking for translate = 0, do it always (#1767, #1846).
2336
transform = ['translate(' + translateX + ',' + translateY + ')'];
2340
transform.push('rotate(90) scale(-1,1)');
2341
} else if (rotation) { // text rotation
2342
transform.push('rotate(' + rotation + ' ' + (wrapper.x || 0) + ' ' + (wrapper.y || 0) + ')');
2346
if (defined(scaleX) || defined(scaleY)) {
2347
transform.push('scale(' + pick(scaleX, 1) + ' ' + pick(scaleY, 1) + ')');
2350
if (transform.length) {
2351
attr(wrapper.element, 'transform', transform.join(' '));
2355
* Bring the element to the front
2357
toFront: function () {
2358
var element = this.element;
2359
element.parentNode.appendChild(element);
2365
* Break down alignment options like align, verticalAlign, x and y
2366
* to x and y relative to the chart.
2368
* @param {Object} alignOptions
2369
* @param {Boolean} alignByTranslate
2370
* @param {String[Object} box The box to align to, needs a width and height. When the
2371
* box is a string, it refers to an object in the Renderer. For example, when
2372
* box is 'spacingBox', it refers to Renderer.spacingBox which holds width, height
2373
* x and y properties.
2376
align: function (alignOptions, alignByTranslate, box) {
2383
renderer = this.renderer,
2384
alignedObjects = renderer.alignedObjects;
2386
// First call on instanciate
2388
this.alignOptions = alignOptions;
2389
this.alignByTranslate = alignByTranslate;
2390
if (!box || isString(box)) { // boxes other than renderer handle this internally
2391
this.alignTo = alignTo = box || 'renderer';
2392
erase(alignedObjects, this); // prevent duplicates, like legendGroup after resize
2393
alignedObjects.push(this);
2394
box = null; // reassign it below
2397
// When called on resize, no arguments are supplied
2399
alignOptions = this.alignOptions;
2400
alignByTranslate = this.alignByTranslate;
2401
alignTo = this.alignTo;
2404
box = pick(box, renderer[alignTo], renderer);
2407
align = alignOptions.align;
2408
vAlign = alignOptions.verticalAlign;
2409
x = (box.x || 0) + (alignOptions.x || 0); // default: left align
2410
y = (box.y || 0) + (alignOptions.y || 0); // default: top align
2413
if (align === 'right' || align === 'center') {
2414
x += (box.width - (alignOptions.width || 0)) /
2415
{ right: 1, center: 2 }[align];
2417
attribs[alignByTranslate ? 'translateX' : 'x'] = mathRound(x);
2421
if (vAlign === 'bottom' || vAlign === 'middle') {
2422
y += (box.height - (alignOptions.height || 0)) /
2423
({ bottom: 1, middle: 2 }[vAlign] || 1);
2426
attribs[alignByTranslate ? 'translateY' : 'y'] = mathRound(y);
2428
// Animate only if already placed
2429
this[this.placed ? 'animate' : 'attr'](attribs);
2431
this.alignAttr = attribs;
2437
* Get the bounding box (width, height, x and y) for the element
2439
getBBox: function () {
2441
bBox = wrapper.bBox,
2442
renderer = wrapper.renderer,
2445
rotation = wrapper.rotation,
2446
element = wrapper.element,
2447
styles = wrapper.styles,
2448
rad = rotation * deg2rad,
2449
textStr = wrapper.textStr,
2452
// Since numbers are monospaced, and numerical labels appear a lot in a chart,
2453
// we assume that a label of n characters has the same bounding box as others
2454
// of the same length.
2455
if (textStr === '' || numRegex.test(textStr)) {
2456
numKey = textStr.toString().length + (styles ? ('|' + styles.fontSize + '|' + styles.fontFamily) : '');
2457
bBox = renderer.cache[numKey];
2464
if (element.namespaceURI === SVG_NS || renderer.forExport) {
2465
try { // Fails in Firefox if the container has display: none.
2467
bBox = element.getBBox ?
2468
// SVG: use extend because IE9 is not allowed to change width and height in case
2469
// of rotation (below)
2470
extend({}, element.getBBox()) :
2471
// Canvas renderer and legacy IE in export mode
2473
width: element.offsetWidth,
2474
height: element.offsetHeight
2478
// If the bBox is not set, the try-catch block above failed. The other condition
2479
// is for Opera that returns a width of -Infinity on hidden elements.
2480
if (!bBox || bBox.width < 0) {
2481
bBox = { width: 0, height: 0 };
2485
// VML Renderer or useHTML within SVG
2488
bBox = wrapper.htmlGetBBox();
2492
// True SVG elements as well as HTML elements in modern browsers using the .useHTML option
2493
// need to compensated for rotation
2494
if (renderer.isSVG) {
2496
height = bBox.height;
2498
// Workaround for wrong bounding box in IE9 and IE10 (#1101, #1505, #1669, #2568)
2499
if (isIE && styles && styles.fontSize === '11px' && height.toPrecision(3) === '16.9') {
2500
bBox.height = height = 14;
2503
// Adjust for rotated text
2505
bBox.width = mathAbs(height * mathSin(rad)) + mathAbs(width * mathCos(rad));
2506
bBox.height = mathAbs(height * mathCos(rad)) + mathAbs(width * mathSin(rad));
2511
wrapper.bBox = bBox;
2513
renderer.cache[numKey] = bBox;
2522
show: function (inherit) {
2523
return this.attr({ visibility: inherit ? 'inherit' : VISIBLE });
2530
return this.attr({ visibility: HIDDEN });
2533
fadeOut: function (duration) {
2534
var elemWrapper = this;
2535
elemWrapper.animate({
2538
duration: duration || 150,
2539
complete: function () {
2547
* @param {Object|Undefined} parent Can be an element, an element wrapper or undefined
2548
* to append the element to the renderer.box.
2550
add: function (parent) {
2552
var renderer = this.renderer,
2553
parentWrapper = parent || renderer,
2554
parentNode = parentWrapper.element || renderer.box,
2556
element = this.element,
2557
zIndex = this.zIndex,
2564
this.parentGroup = parent;
2568
this.parentInverted = parent && parent.inverted;
2570
// build formatted text
2571
if (this.textStr !== undefined) {
2572
renderer.buildText(this);
2575
// mark the container as having z indexed children
2577
parentWrapper.handleZ = true;
2578
zIndex = pInt(zIndex);
2581
// insert according to this and other elements' zIndex
2582
if (parentWrapper.handleZ) { // this element or any of its siblings has a z index
2583
childNodes = parentNode.childNodes;
2584
for (i = 0; i < childNodes.length; i++) {
2585
otherElement = childNodes[i];
2586
otherZIndex = attr(otherElement, 'zIndex');
2587
if (otherElement !== element && (
2588
// insert before the first element with a higher zIndex
2589
pInt(otherZIndex) > zIndex ||
2590
// if no zIndex given, insert before the first element with a zIndex
2591
(!defined(zIndex) && defined(otherZIndex))
2594
parentNode.insertBefore(element, otherElement);
2601
// default: append at the end
2603
parentNode.appendChild(element);
2609
// fire an event for internal hooks
2618
* Removes a child either by removeChild or move to garbageBin.
2619
* Issue 490; in VML removeChild results in Orphaned nodes according to sIEve, discardElement does not.
2621
safeRemoveChild: function (element) {
2622
var parentNode = element.parentNode;
2624
parentNode.removeChild(element);
2629
* Destroy the element and element wrapper
2631
destroy: function () {
2633
element = wrapper.element || {},
2634
shadows = wrapper.shadows,
2635
parentToClean = wrapper.renderer.isSVG && element.nodeName === 'SPAN' && wrapper.parentGroup,
2641
element.onclick = element.onmouseout = element.onmouseover = element.onmousemove = element.point = null;
2642
stop(wrapper); // stop running animations
2644
if (wrapper.clipPath) {
2645
wrapper.clipPath = wrapper.clipPath.destroy();
2648
// Destroy stops in case this is a gradient object
2649
if (wrapper.stops) {
2650
for (i = 0; i < wrapper.stops.length; i++) {
2651
wrapper.stops[i] = wrapper.stops[i].destroy();
2653
wrapper.stops = null;
2657
wrapper.safeRemoveChild(element);
2661
each(shadows, function (shadow) {
2662
wrapper.safeRemoveChild(shadow);
2666
// In case of useHTML, clean up empty containers emulating SVG groups (#1960, #2393).
2667
while (parentToClean && parentToClean.div.childNodes.length === 0) {
2668
grandParent = parentToClean.parentGroup;
2669
wrapper.safeRemoveChild(parentToClean.div);
2670
delete parentToClean.div;
2671
parentToClean = grandParent;
2674
// remove from alignObjects
2675
if (wrapper.alignTo) {
2676
erase(wrapper.renderer.alignedObjects, wrapper);
2679
for (key in wrapper) {
2680
delete wrapper[key];
2687
* Add a shadow to the element. Must be done after the element is added to the DOM
2688
* @param {Boolean|Object} shadowOptions
2690
shadow: function (shadowOptions, group, cutOff) {
2694
element = this.element,
2697
shadowElementOpacity,
2699
// compensate for inverted plot area
2703
if (shadowOptions) {
2704
shadowWidth = pick(shadowOptions.width, 3);
2705
shadowElementOpacity = (shadowOptions.opacity || 0.15) / shadowWidth;
2706
transform = this.parentInverted ?
2708
'(' + pick(shadowOptions.offsetX, 1) + ', ' + pick(shadowOptions.offsetY, 1) + ')';
2709
for (i = 1; i <= shadowWidth; i++) {
2710
shadow = element.cloneNode(0);
2711
strokeWidth = (shadowWidth * 2) + 1 - (2 * i);
2714
'stroke': shadowOptions.color || 'black',
2715
'stroke-opacity': shadowElementOpacity * i,
2716
'stroke-width': strokeWidth,
2717
'transform': 'translate' + transform,
2721
attr(shadow, 'height', mathMax(attr(shadow, 'height') - strokeWidth, 0));
2722
shadow.cutHeight = strokeWidth;
2726
group.element.appendChild(shadow);
2728
element.parentNode.insertBefore(shadow, element);
2731
shadows.push(shadow);
2734
this.shadows = shadows;
2743
* The default SVG renderer
2745
var SVGRenderer = function () {
2746
this.init.apply(this, arguments);
2748
SVGRenderer.prototype = {
2749
Element: SVGElement,
2752
* Initialize the SVGRenderer
2753
* @param {Object} container
2754
* @param {Number} width
2755
* @param {Number} height
2756
* @param {Boolean} forExport
2758
init: function (container, width, height, style, forExport) {
2759
var renderer = this,
2765
boxWrapper = renderer.createElement('svg')
2769
.css(this.getStyle(style));
2770
element = boxWrapper.element;
2771
container.appendChild(element);
2773
// For browsers other than IE, add the namespace attribute (#1978)
2774
if (container.innerHTML.indexOf('xmlns') === -1) {
2775
attr(element, 'xmlns', SVG_NS);
2778
// object properties
2779
renderer.isSVG = true;
2780
renderer.box = element;
2781
renderer.boxWrapper = boxWrapper;
2782
renderer.alignedObjects = [];
2784
// Page url used for internal references. #24, #672, #1070
2785
renderer.url = (isFirefox || isWebKit) && doc.getElementsByTagName('base').length ?
2787
.replace(/#.*?$/, '') // remove the hash
2788
.replace(/([\('\)])/g, '\\$1') // escape parantheses and quotes
2789
.replace(/ /g, '%20') : // replace spaces (needed for Safari only)
2793
desc = this.createElement('desc').add();
2794
desc.element.appendChild(doc.createTextNode('Created with ' + PRODUCT + ' ' + VERSION));
2797
renderer.defs = this.createElement('defs').add();
2798
renderer.forExport = forExport;
2799
renderer.gradients = {}; // Object where gradient SvgElements are stored
2800
renderer.cache = {}; // Cache for numerical bounding boxes
2802
renderer.setSize(width, height, false);
2806
// Issue 110 workaround:
2807
// In Firefox, if a div is positioned by percentage, its pixel position may land
2808
// between pixels. The container itself doesn't display this, but an SVG element
2809
// inside this container will be drawn at subpixel precision. In order to draw
2810
// sharp lines, this must be compensated for. This doesn't seem to work inside
2811
// iframes though (like in jsFiddle).
2812
var subPixelFix, rect;
2813
if (isFirefox && container.getBoundingClientRect) {
2814
renderer.subPixelFix = subPixelFix = function () {
2815
css(container, { left: 0, top: 0 });
2816
rect = container.getBoundingClientRect();
2818
left: (mathCeil(rect.left) - rect.left) + PX,
2819
top: (mathCeil(rect.top) - rect.top) + PX
2827
addEvent(win, 'resize', subPixelFix);
2831
getStyle: function (style) {
2832
return (this.style = extend({
2833
fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif', // default font
2839
* Detect whether the renderer is hidden. This happens when one of the parent elements
2840
* has display: none. #608.
2842
isHidden: function () {
2843
return !this.boxWrapper.getBBox().width;
2847
* Destroys the renderer and its allocated members.
2849
destroy: function () {
2850
var renderer = this,
2851
rendererDefs = renderer.defs;
2852
renderer.box = null;
2853
renderer.boxWrapper = renderer.boxWrapper.destroy();
2855
// Call destroy on all gradient elements
2856
destroyObjectProperties(renderer.gradients || {});
2857
renderer.gradients = null;
2859
// Defs are null in VMLRenderer
2860
// Otherwise, destroy them here.
2862
renderer.defs = rendererDefs.destroy();
2865
// Remove sub pixel fix handler
2866
// We need to check that there is a handler, otherwise all functions that are registered for event 'resize' are removed
2868
if (renderer.subPixelFix) {
2869
removeEvent(win, 'resize', renderer.subPixelFix);
2872
renderer.alignedObjects = null;
2878
* Create a wrapper for an SVG element
2879
* @param {Object} nodeName
2881
createElement: function (nodeName) {
2882
var wrapper = new this.Element();
2883
wrapper.init(this, nodeName);
2888
* Dummy function for use in canvas renderer
2890
draw: function () {},
2893
* Parse a simple HTML string into SVG tspans
2895
* @param {Object} textNode The parent text SVG node
2897
buildText: function (wrapper) {
2898
var textNode = wrapper.element,
2900
forExport = renderer.forExport,
2901
lines = pick(wrapper.textStr, '').toString()
2902
.replace(/<(b|strong)>/g, '<span style="font-weight:bold">')
2903
.replace(/<(i|em)>/g, '<span style="font-style:italic">')
2904
.replace(/<a/g, '<span')
2905
.replace(/<\/(b|strong|i|em|a)>/g, '</span>')
2907
childNodes = textNode.childNodes,
2908
styleRegex = /<.*style="([^"]+)".*>/,
2909
hrefRegex = /<.*href="(http[^"]+)".*>/,
2910
parentX = attr(textNode, 'x'),
2911
textStyles = wrapper.styles,
2912
width = wrapper.textWidth,
2913
textLineHeight = textStyles && textStyles.lineHeight,
2914
i = childNodes.length,
2915
getLineHeight = function (tspan) {
2916
return textLineHeight ?
2917
pInt(textLineHeight) :
2918
renderer.fontMetrics(
2919
/(px|em)$/.test(tspan && tspan.style.fontSize) ?
2920
tspan.style.fontSize :
2921
(textStyles.fontSize || 11)
2927
textNode.removeChild(childNodes[i]);
2930
if (width && !wrapper.added) {
2931
this.box.appendChild(textNode); // attach it to the DOM to read offset width
2934
// remove empty line at end
2935
if (lines[lines.length - 1] === '') {
2940
each(lines, function (line, lineNo) {
2941
var spans, spanNo = 0;
2943
line = line.replace(/<span/g, '|||<span').replace(/<\/span>/g, '</span>|||');
2944
spans = line.split('|||');
2946
each(spans, function (span) {
2947
if (span !== '' || spans.length === 1) {
2948
var attributes = {},
2949
tspan = doc.createElementNS(SVG_NS, 'tspan'),
2951
if (styleRegex.test(span)) {
2952
spanStyle = span.match(styleRegex)[1].replace(/(;| |^)color([ :])/, '$1fill$2');
2953
attr(tspan, 'style', spanStyle);
2955
if (hrefRegex.test(span) && !forExport) { // Not for export - #1529
2956
attr(tspan, 'onclick', 'location.href=\"' + span.match(hrefRegex)[1] + '\"');
2957
css(tspan, { cursor: 'pointer' });
2960
span = (span.replace(/<(.|\n)*?>/g, '') || ' ')
2961
.replace(/</g, '<')
2962
.replace(/>/g, '>');
2964
// Nested tags aren't supported, and cause crash in Safari (#1596)
2967
// add the text node
2968
tspan.appendChild(doc.createTextNode(span));
2970
if (!spanNo) { // first span in a line, align it to the left
2971
attributes.x = parentX;
2973
attributes.dx = 0; // #16
2977
attr(tspan, attributes);
2979
// first span on subsequent line, add the line height
2980
if (!spanNo && lineNo) {
2982
// allow getting the right offset height in exporting in IE
2983
if (!hasSVG && forExport) {
2984
css(tspan, { display: 'block' });
2987
// Set the line height based on the font size of either
2988
// the text element or the tspan element
2992
getLineHeight(tspan),
2993
// Safari 6.0.2 - too optimized for its own good (#1539)
2994
// TODO: revisit this with future versions of Safari
2995
isWebKit && tspan.offsetHeight
3000
textNode.appendChild(tspan);
3004
// check width and apply soft breaks
3006
var words = span.replace(/([^\^])-/g, '$1- ').split(' '), // #1273
3007
hasWhiteSpace = words.length > 1 && textStyles.whiteSpace !== 'nowrap',
3010
clipHeight = wrapper._clipHeight,
3012
dy = getLineHeight(),
3016
while (hasWhiteSpace && (words.length || rest.length)) {
3017
delete wrapper.bBox; // delete cache
3018
bBox = wrapper.getBBox();
3019
actualWidth = bBox.width;
3021
// Old IE cannot measure the actualWidth for SVG elements (#2314)
3022
if (!hasSVG && renderer.forExport) {
3023
actualWidth = renderer.measureSpanWidth(tspan.firstChild.data, wrapper.styles);
3026
tooLong = actualWidth > width;
3027
if (!tooLong || words.length === 1) { // new line needed
3033
if (clipHeight && softLineNo * dy > clipHeight) {
3035
wrapper.attr('title', wrapper.textStr);
3038
tspan = doc.createElementNS(SVG_NS, 'tspan');
3043
if (spanStyle) { // #390
3044
attr(tspan, 'style', spanStyle);
3046
textNode.appendChild(tspan);
3048
if (actualWidth > width) { // a single word is pressing it out
3049
width = actualWidth;
3053
} else { // append to existing line tspan
3054
tspan.removeChild(tspan.firstChild);
3055
rest.unshift(words.pop());
3058
tspan.appendChild(doc.createTextNode(words.join(' ').replace(/- /g, '-')));
3069
* Create a button with preset states
3070
* @param {String} text
3073
* @param {Function} callback
3074
* @param {Object} normalState
3075
* @param {Object} hoverState
3076
* @param {Object} pressedState
3078
button: function (text, x, y, callback, normalState, hoverState, pressedState, disabledState, shape) {
3079
var label = this.label(text, x, y, shape, null, null, null, null, 'button'),
3088
verticalGradient = { x1: 0, y1: 0, x2: 0, y2: 1 };
3090
// Normal state - prepare the attributes
3091
normalState = merge({
3095
linearGradient: verticalGradient,
3107
normalStyle = normalState[STYLE];
3108
delete normalState[STYLE];
3111
hoverState = merge(normalState, {
3114
linearGradient: verticalGradient,
3121
hoverStyle = hoverState[STYLE];
3122
delete hoverState[STYLE];
3125
pressedState = merge(normalState, {
3128
linearGradient: verticalGradient,
3135
pressedStyle = pressedState[STYLE];
3136
delete pressedState[STYLE];
3139
disabledState = merge(normalState, {
3144
disabledStyle = disabledState[STYLE];
3145
delete disabledState[STYLE];
3147
// Add the events. IE9 and IE10 need mouseover and mouseout to funciton (#667).
3148
addEvent(label.element, isIE ? 'mouseover' : 'mouseenter', function () {
3149
if (curState !== 3) {
3150
label.attr(hoverState)
3154
addEvent(label.element, isIE ? 'mouseout' : 'mouseleave', function () {
3155
if (curState !== 3) {
3156
stateOptions = [normalState, hoverState, pressedState][curState];
3157
stateStyle = [normalStyle, hoverStyle, pressedStyle][curState];
3158
label.attr(stateOptions)
3163
label.setState = function (state) {
3164
label.state = curState = state;
3166
label.attr(normalState)
3168
} else if (state === 2) {
3169
label.attr(pressedState)
3171
} else if (state === 3) {
3172
label.attr(disabledState)
3173
.css(disabledStyle);
3178
.on('click', function () {
3179
if (curState !== 3) {
3180
callback.call(label);
3184
.css(extend({ cursor: 'default' }, normalStyle));
3188
* Make a straight line crisper by not spilling out to neighbour pixels
3189
* @param {Array} points
3190
* @param {Number} width
3192
crispLine: function (points, width) {
3193
// points format: [M, 0, 0, L, 100, 0]
3194
// normalize to a crisp line
3195
if (points[1] === points[4]) {
3196
// Substract due to #1129. Now bottom and left axis gridlines behave the same.
3197
points[1] = points[4] = mathRound(points[1]) - (width % 2 / 2);
3199
if (points[2] === points[5]) {
3200
points[2] = points[5] = mathRound(points[2]) + (width % 2 / 2);
3208
* @param {Array} path An SVG path in array form
3210
path: function (path) {
3214
if (isArray(path)) {
3216
} else if (isObject(path)) { // attributes
3219
return this.createElement('path').attr(attr);
3223
* Draw and return an SVG circle
3224
* @param {Number} x The x position
3225
* @param {Number} y The y position
3226
* @param {Number} r The radius
3228
circle: function (x, y, r) {
3229
var attr = isObject(x) ?
3237
return this.createElement('circle').attr(attr);
3241
* Draw and return an arc
3242
* @param {Number} x X position
3243
* @param {Number} y Y position
3244
* @param {Number} r Radius
3245
* @param {Number} innerR Inner radius like used in donut charts
3246
* @param {Number} start Starting angle
3247
* @param {Number} end Ending angle
3249
arc: function (x, y, r, innerR, start, end) {
3261
// Arcs are defined as symbols for the ability to set
3262
// attributes in attr and animate
3263
arc = this.symbol('arc', x || 0, y || 0, r || 0, r || 0, {
3264
innerR: innerR || 0,
3273
* Draw and return a rectangle
3274
* @param {Number} x Left position
3275
* @param {Number} y Top position
3276
* @param {Number} width
3277
* @param {Number} height
3278
* @param {Number} r Border corner radius
3279
* @param {Number} strokeWidth A stroke width can be supplied to allow crisp drawing
3281
rect: function (x, y, width, height, r, strokeWidth) {
3283
r = isObject(x) ? x.r : r;
3285
var wrapper = this.createElement('rect'),
3286
attr = isObject(x) ? x : x === UNDEFINED ? {} : {
3289
width: mathMax(width, 0),
3290
height: mathMax(height, 0)
3293
if (strokeWidth !== UNDEFINED) {
3294
attr.strokeWidth = strokeWidth;
3295
attr = wrapper.crisp(attr);
3302
return wrapper.attr(attr);
3306
* Resize the box and re-align all aligned elements
3307
* @param {Object} width
3308
* @param {Object} height
3309
* @param {Boolean} animate
3312
setSize: function (width, height, animate) {
3313
var renderer = this,
3314
alignedObjects = renderer.alignedObjects,
3315
i = alignedObjects.length;
3317
renderer.width = width;
3318
renderer.height = height;
3320
renderer.boxWrapper[pick(animate, true) ? 'animate' : 'attr']({
3326
alignedObjects[i].align();
3332
* @param {String} name The group will be given a class name of 'highcharts-{name}'.
3333
* This can be used for styling and scripting.
3335
g: function (name) {
3336
var elem = this.createElement('g');
3337
return defined(name) ? elem.attr({ 'class': PREFIX + name }) : elem;
3342
* @param {String} src
3345
* @param {Number} width
3346
* @param {Number} height
3348
image: function (src, x, y, width, height) {
3350
preserveAspectRatio: NONE
3354
// optional properties
3355
if (arguments.length > 1) {
3364
elemWrapper = this.createElement('image').attr(attribs);
3366
// set the href in the xlink namespace
3367
if (elemWrapper.element.setAttributeNS) {
3368
elemWrapper.element.setAttributeNS('http://www.w3.org/1999/xlink',
3371
// could be exporting in IE
3372
// using href throws "not supported" in ie7 and under, requries regex shim to fix later
3373
elemWrapper.element.setAttribute('hc-svg-href', src);
3380
* Draw a symbol out of pre-defined shape paths from the namespace 'symbol' object.
3382
* @param {Object} symbol
3385
* @param {Object} radius
3386
* @param {Object} options
3388
symbol: function (symbol, x, y, width, height, options) {
3392
// get the symbol definition function
3393
symbolFn = this.symbols[symbol],
3395
// check if there's a path defined for this symbol
3396
path = symbolFn && symbolFn(
3405
imageRegex = /^url\((.*?)\)$/,
3412
obj = this.path(path);
3413
// expando properties for use in animate and attr
3422
extend(obj, options);
3427
} else if (imageRegex.test(symbol)) {
3429
// On image load, set the size and position
3430
centerImage = function (img, size) {
3431
if (img.element) { // it may be destroyed in the meantime (#1390)
3437
if (!img.alignByTranslate) { // #185
3439
mathRound((width - size[0]) / 2), // #1378
3440
mathRound((height - size[1]) / 2)
3446
imageSrc = symbol.match(imageRegex)[1];
3447
imageSize = symbolSizes[imageSrc];
3449
// Ireate the image synchronously, add attribs async
3450
obj = this.image(imageSrc)
3458
centerImage(obj, imageSize);
3460
// Initialize image to be 0 size so export will still function if there's no cached sizes.
3462
obj.attr({ width: 0, height: 0 });
3464
// Create a dummy JavaScript image to get the width and height. Due to a bug in IE < 8,
3465
// the created element must be assigned to a variable in order to load (#292).
3466
imageElement = createElement('img', {
3467
onload: function () {
3468
centerImage(obj, symbolSizes[imageSrc] = [this.width, this.height]);
3479
* An extendable collection of functions for defining symbol paths.
3482
'circle': function (x, y, w, h) {
3483
var cpw = 0.166 * w;
3486
'C', x + w + cpw, y, x + w + cpw, y + h, x + w / 2, y + h,
3487
'C', x - cpw, y + h, x - cpw, y, x + w / 2, y,
3492
'square': function (x, y, w, h) {
3502
'triangle': function (x, y, w, h) {
3511
'triangle-down': function (x, y, w, h) {
3519
'diamond': function (x, y, w, h) {
3522
L, x + w, y + h / 2,
3528
'arc': function (x, y, w, h, options) {
3529
var start = options.start,
3530
radius = options.r || w || h,
3531
end = options.end - 0.001, // to prevent cos and sin of start and end from becoming equal on 360 arcs (related: #1561)
3532
innerRadius = options.innerR,
3533
open = options.open,
3534
cosStart = mathCos(start),
3535
sinStart = mathSin(start),
3536
cosEnd = mathCos(end),
3537
sinEnd = mathSin(end),
3538
longArc = options.end - start < mathPI ? 0 : 1;
3542
x + radius * cosStart,
3543
y + radius * sinStart,
3548
longArc, // long or short arc
3550
x + radius * cosEnd,
3551
y + radius * sinEnd,
3553
x + innerRadius * cosEnd,
3554
y + innerRadius * sinEnd,
3556
innerRadius, // x radius
3557
innerRadius, // y radius
3559
longArc, // long or short arc
3561
x + innerRadius * cosStart,
3562
y + innerRadius * sinStart,
3564
open ? '' : 'Z' // close
3570
* Define a clipping rectangle
3571
* @param {String} id
3574
* @param {Number} width
3575
* @param {Number} height
3577
clipRect: function (x, y, width, height) {
3579
id = PREFIX + idCounter++,
3581
clipPath = this.createElement('clipPath').attr({
3585
wrapper = this.rect(x, y, width, height, 0).add(clipPath);
3587
wrapper.clipPath = clipPath;
3594
* Take a color and return it if it's a string, make it a gradient if it's a
3595
* gradient configuration object. Prior to Highstock, an array was used to define
3596
* a linear gradient with pixel positions relative to the SVG. In newer versions
3597
* we change the coordinates to apply relative to the shape, using coordinates
3598
* 0-1 within the shape. To preserve backwards compatibility, linearGradient
3599
* in this definition is an object of x1, y1, x2 and y2.
3601
* @param {Object} color The color or config object
3603
color: function (color, elem, prop) {
3604
var renderer = this,
3606
regexRgba = /^rgba/,
3619
// Apply linear or radial gradients
3620
if (color && color.linearGradient) {
3621
gradName = 'linearGradient';
3622
} else if (color && color.radialGradient) {
3623
gradName = 'radialGradient';
3627
gradAttr = color[gradName];
3628
gradients = renderer.gradients;
3629
stops = color.stops;
3630
radialReference = elem.radialReference;
3632
// Keep < 2.2 kompatibility
3633
if (isArray(gradAttr)) {
3634
color[gradName] = gradAttr = {
3639
gradientUnits: 'userSpaceOnUse'
3643
// Correct the radial gradient for the radial reference system
3644
if (gradName === 'radialGradient' && radialReference && !defined(gradAttr.gradientUnits)) {
3645
gradAttr = merge(gradAttr, {
3646
cx: (radialReference[0] - radialReference[2] / 2) + gradAttr.cx * radialReference[2],
3647
cy: (radialReference[1] - radialReference[2] / 2) + gradAttr.cy * radialReference[2],
3648
r: gradAttr.r * radialReference[2],
3649
gradientUnits: 'userSpaceOnUse'
3653
// Build the unique key to detect whether we need to create a new element (#1282)
3654
for (n in gradAttr) {
3656
key.push(n, gradAttr[n]);
3662
key = key.join(',');
3664
// Check if a gradient object with the same config object is created within this renderer
3665
if (gradients[key]) {
3666
id = gradients[key].id;
3670
// Set the id and create the element
3671
gradAttr.id = id = PREFIX + idCounter++;
3672
gradients[key] = gradientObject = renderer.createElement(gradName)
3674
.add(renderer.defs);
3677
// The gradient needs to keep a list of stops to be able to destroy them
3678
gradientObject.stops = [];
3679
each(stops, function (stop) {
3681
if (regexRgba.test(stop[1])) {
3682
colorObject = Color(stop[1]);
3683
stopColor = colorObject.get('rgb');
3684
stopOpacity = colorObject.get('a');
3686
stopColor = stop[1];
3689
stopObject = renderer.createElement('stop').attr({
3691
'stop-color': stopColor,
3692
'stop-opacity': stopOpacity
3693
}).add(gradientObject);
3695
// Add the stop element to the gradient
3696
gradientObject.stops.push(stopObject);
3700
// Return the reference to the gradient object
3701
return 'url(' + renderer.url + '#' + id + ')';
3703
// Webkit and Batik can't show rgba.
3704
} else if (regexRgba.test(color)) {
3705
colorObject = Color(color);
3706
attr(elem, prop + '-opacity', colorObject.get('a'));
3708
return colorObject.get('rgb');
3712
// Remove the opacity attribute added above. Does not throw if the attribute is not there.
3713
elem.removeAttribute(prop + '-opacity');
3722
* Add text to the SVG object
3723
* @param {String} str
3724
* @param {Number} x Left position
3725
* @param {Number} y Top position
3726
* @param {Boolean} useHTML Use HTML to render the text
3728
text: function (str, x, y, useHTML) {
3730
// declare variables
3731
var renderer = this,
3732
fakeSVG = useCanVG || (!hasSVG && renderer.forExport),
3735
if (useHTML && !renderer.forExport) {
3736
return renderer.html(str, x, y);
3739
x = mathRound(pick(x, 0));
3740
y = mathRound(pick(y, 0));
3742
wrapper = renderer.createElement('text')
3749
// Prevent wrapping from creating false offsetWidths in export in legacy IE (#1079, #1063)
3762
* Utility to return the baseline offset and total line height from the font size
3764
fontMetrics: function (fontSize) {
3765
fontSize = fontSize || this.style.fontSize;
3766
fontSize = /px/.test(fontSize) ? pInt(fontSize) : /em/.test(fontSize) ? parseFloat(fontSize) * 12 : 12;
3768
// Empirical values found by comparing font size and bounding box height.
3769
// Applies to the default font family. http://jsfiddle.net/highcharts/7xvn7/
3770
var lineHeight = fontSize < 24 ? fontSize + 4 : mathRound(fontSize * 1.2),
3771
baseline = mathRound(lineHeight * 0.8);
3780
* Add a label, a text item that can hold a colored or gradient background
3781
* as well as a border and shadow.
3782
* @param {string} str
3785
* @param {String} shape
3786
* @param {Number} anchorX In case the shape has a pointer, like a flag, this is the
3787
* coordinates it should be pinned to
3788
* @param {Number} anchorY
3789
* @param {Boolean} baseline Whether to position the label relative to the text baseline,
3790
* like renderer.text, or to the upper border of the rectangle.
3791
* @param {String} className Class name for the group
3793
label: function (str, x, y, shape, anchorX, anchorY, useHTML, baseline, className) {
3795
var renderer = this,
3796
wrapper = renderer.g(className),
3797
text = renderer.text('', 0, 0, useHTML)
3814
attrSetters = wrapper.attrSetters,
3818
* This function runs after the label is added to the DOM (when the bounding box is
3819
* available), and after the text of the label is updated to detect the new bounding
3820
* box and reflect it in the border box.
3822
function updateBoxSize() {
3825
style = text.element.style;
3827
bBox = (width === undefined || height === undefined || wrapper.styles.textAlign) && text.textStr &&
3829
wrapper.width = (width || bBox.width || 0) + 2 * padding + paddingLeft;
3830
wrapper.height = (height || bBox.height || 0) + 2 * padding;
3832
// update the label-scoped y offset
3833
baselineOffset = padding + renderer.fontMetrics(style && style.fontSize).b;
3837
// create the border box if it is not already present
3839
boxX = mathRound(-alignFactor * padding);
3840
boxY = baseline ? -baselineOffset : 0;
3842
wrapper.box = box = shape ?
3843
renderer.symbol(shape, boxX, boxY, wrapper.width, wrapper.height, deferredAttr) :
3844
renderer.rect(boxX, boxY, wrapper.width, wrapper.height, 0, deferredAttr[STROKE_WIDTH]);
3845
box.attr('fill', NONE).add(wrapper);
3848
// apply the box attributes
3849
if (!box.isImg) { // #1630
3851
width: wrapper.width,
3852
height: wrapper.height
3855
deferredAttr = null;
3860
* This function runs after setting text or padding, but only if padding is changed
3862
function updateTextPadding() {
3863
var styles = wrapper.styles,
3864
textAlign = styles && styles.textAlign,
3865
x = paddingLeft + padding * (1 - alignFactor),
3868
// determin y based on the baseline
3869
y = baseline ? 0 : baselineOffset;
3871
// compensate for alignment
3872
if (defined(width) && bBox && (textAlign === 'center' || textAlign === 'right')) {
3873
x += { center: 0.5, right: 1 }[textAlign] * (width - bBox.width);
3876
// update if anything changed
3877
if (x !== text.x || y !== text.y) {
3884
// record current values
3890
* Set a box attribute, or defer it if the box is not yet created
3891
* @param {Object} key
3892
* @param {Object} value
3894
function boxAttr(key, value) {
3896
box.attr(key, value);
3898
deferredAttr[key] = value;
3903
* After the text element is added, get the desired size of the border box
3904
* and add it before the text in the DOM.
3906
wrapper.onAdd = function () {
3909
text: str, // alignment is available now
3914
if (box && defined(anchorX)) {
3923
* Add specific attribute setters.
3926
// only change local variables
3927
attrSetters.width = function (value) {
3931
attrSetters.height = function (value) {
3935
attrSetters.padding = function (value) {
3936
if (defined(value) && value !== padding) {
3938
updateTextPadding();
3942
attrSetters.paddingLeft = function (value) {
3943
if (defined(value) && value !== paddingLeft) {
3944
paddingLeft = value;
3945
updateTextPadding();
3951
// change local variable and set attribue as well
3952
attrSetters.align = function (value) {
3953
alignFactor = { left: 0, center: 0.5, right: 1 }[value];
3954
return false; // prevent setting text-anchor on the group
3957
// apply these to the box and the text alike
3958
attrSetters.text = function (value, key) {
3959
text.attr(key, value);
3961
updateTextPadding();
3965
// apply these to the box but not to the text
3966
attrSetters[STROKE_WIDTH] = function (value, key) {
3970
crispAdjust = value % 2 / 2;
3971
boxAttr(key, value);
3974
attrSetters.stroke = attrSetters.fill = attrSetters.r = function (value, key) {
3975
if (key === 'fill' && value) {
3978
boxAttr(key, value);
3981
attrSetters.anchorX = function (value, key) {
3983
boxAttr(key, value + crispAdjust - wrapperX);
3986
attrSetters.anchorY = function (value, key) {
3988
boxAttr(key, value - wrapperY);
3992
// rename attributes
3993
attrSetters.x = function (value) {
3994
wrapper.x = value; // for animation getter
3995
value -= alignFactor * ((width || bBox.width) + padding);
3996
wrapperX = mathRound(value);
3998
wrapper.attr('translateX', wrapperX);
4001
attrSetters.y = function (value) {
4002
wrapperY = wrapper.y = mathRound(value);
4003
wrapper.attr('translateY', wrapperY);
4007
// Redirect certain methods to either the box or the text
4008
var baseCss = wrapper.css;
4009
return extend(wrapper, {
4011
* Pick up some properties and apply them to the text instead of the wrapper
4013
css: function (styles) {
4015
var textStyles = {};
4016
styles = merge(styles); // create a copy to avoid altering the original object (#537)
4017
each(['fontSize', 'fontWeight', 'fontFamily', 'color', 'lineHeight', 'width', 'textDecoration', 'textShadow'], function (prop) {
4018
if (styles[prop] !== UNDEFINED) {
4019
textStyles[prop] = styles[prop];
4020
delete styles[prop];
4023
text.css(textStyles);
4025
return baseCss.call(wrapper, styles);
4028
* Return the bounding box of the box, not the group
4030
getBBox: function () {
4032
width: bBox.width + 2 * padding,
4033
height: bBox.height + 2 * padding,
4034
x: bBox.x - padding,
4039
* Apply the shadow to the box
4041
shadow: function (b) {
4048
* Destroy and release memory.
4050
destroy: function () {
4052
// Added by button implementation
4053
removeEvent(wrapper.element, 'mouseenter');
4054
removeEvent(wrapper.element, 'mouseleave');
4057
text = text.destroy();
4060
box = box.destroy();
4062
// Call base implementation to destroy the rest
4063
SVGElement.prototype.destroy.call(wrapper);
4065
// Release local pointers (#1298)
4066
wrapper = renderer = updateBoxSize = updateTextPadding = boxAttr = null;
4070
}; // end SVGRenderer
4074
Renderer = SVGRenderer;
4075
// extend SvgElement for useHTML option
4076
extend(SVGElement.prototype, {
4078
* Apply CSS to HTML elements. This is used in text within SVG rendering and
4079
* by the VML renderer
4081
htmlCss: function (styles) {
4083
element = wrapper.element,
4084
textWidth = styles && element.tagName === 'SPAN' && styles.width;
4087
delete styles.width;
4088
wrapper.textWidth = textWidth;
4089
wrapper.updateTransform();
4092
wrapper.styles = extend(wrapper.styles, styles);
4093
css(wrapper.element, styles);
4099
* VML and useHTML method for calculating the bounding box based on offsets
4100
* @param {Boolean} refresh Whether to force a fresh value from the DOM or to
4101
* use the cached value
4103
* @return {Object} A hash containing values for x, y, width and height
4106
htmlGetBBox: function () {
4108
element = wrapper.element,
4109
bBox = wrapper.bBox;
4111
// faking getBBox in exported SVG in legacy IE
4113
// faking getBBox in exported SVG in legacy IE (is this a duplicate of the fix for #1079?)
4114
if (element.nodeName === 'text') {
4115
element.style.position = ABSOLUTE;
4118
bBox = wrapper.bBox = {
4119
x: element.offsetLeft,
4120
y: element.offsetTop,
4121
width: element.offsetWidth,
4122
height: element.offsetHeight
4130
* VML override private method to update elements based on internal
4131
* properties based on SVG transform
4133
htmlUpdateTransform: function () {
4134
// aligning non added elements is expensive
4136
this.alignOnAdd = true;
4141
renderer = wrapper.renderer,
4142
elem = wrapper.element,
4143
translateX = wrapper.translateX || 0,
4144
translateY = wrapper.translateY || 0,
4147
align = wrapper.textAlign || 'left',
4148
alignCorrection = { left: 0, center: 0.5, right: 1 }[align],
4149
shadows = wrapper.shadows;
4153
marginLeft: translateX,
4154
marginTop: translateY
4156
if (shadows) { // used in labels/tooltip
4157
each(shadows, function (shadow) {
4159
marginLeft: translateX + 1,
4160
marginTop: translateY + 1
4166
if (wrapper.inverted) { // wrapper is a group
4167
each(elem.childNodes, function (child) {
4168
renderer.invertChild(child, elem);
4172
if (elem.tagName === 'SPAN') {
4175
rotation = wrapper.rotation,
4177
textWidth = pInt(wrapper.textWidth),
4178
currentTextTransform = [rotation, align, elem.innerHTML, wrapper.textWidth].join(',');
4180
if (currentTextTransform !== wrapper.cTT) { // do the calculations and DOM access only if properties changed
4183
baseline = renderer.fontMetrics(elem.style.fontSize).b;
4185
// Renderer specific handling of span rotation
4186
if (defined(rotation)) {
4187
wrapper.setSpanRotation(rotation, alignCorrection, baseline);
4190
width = pick(wrapper.elemWidth, elem.offsetWidth);
4193
if (width > textWidth && /[ \-]/.test(elem.textContent || elem.innerText)) { // #983, #1254
4195
width: textWidth + PX,
4197
whiteSpace: 'normal'
4202
wrapper.getSpanCorrection(width, baseline, alignCorrection, rotation, align);
4205
// apply position with correction
4207
left: (x + (wrapper.xCorr || 0)) + PX,
4208
top: (y + (wrapper.yCorr || 0)) + PX
4211
// force reflow in webkit to apply the left and top on useHTML element (#1249)
4213
baseline = elem.offsetHeight; // assigned to baseline for JSLint purpose
4216
// record current text transform
4217
wrapper.cTT = currentTextTransform;
4222
* Set the rotation of an individual HTML span
4224
setSpanRotation: function (rotation, alignCorrection, baseline) {
4225
var rotationStyle = {},
4226
cssTransformKey = isIE ? '-ms-transform' : isWebKit ? '-webkit-transform' : isFirefox ? 'MozTransform' : isOpera ? '-o-transform' : '';
4228
rotationStyle[cssTransformKey] = rotationStyle.transform = 'rotate(' + rotation + 'deg)';
4229
rotationStyle[cssTransformKey + (isFirefox ? 'Origin' : '-origin')] = rotationStyle.transformOrigin = (alignCorrection * 100) + '% ' + baseline + 'px';
4230
css(this.element, rotationStyle);
4234
* Get the correction in X and Y positioning as the element is rotated.
4236
getSpanCorrection: function (width, baseline, alignCorrection) {
4237
this.xCorr = -width * alignCorrection;
4238
this.yCorr = -baseline;
4242
// Extend SvgRenderer for useHTML option.
4243
extend(SVGRenderer.prototype, {
4245
* Create HTML text node. This is used by the VML renderer as well as the SVG
4246
* renderer through the useHTML option.
4248
* @param {String} str
4252
html: function (str, x, y) {
4253
var wrapper = this.createElement('span'),
4254
attrSetters = wrapper.attrSetters,
4255
element = wrapper.element,
4256
renderer = wrapper.renderer;
4259
attrSetters.text = function (value) {
4260
if (value !== element.innerHTML) {
4263
element.innerHTML = this.textStr = value;
4267
// Various setters which rely on update transform
4268
attrSetters.x = attrSetters.y = attrSetters.align = attrSetters.rotation = function (value, key) {
4269
if (key === 'align') {
4270
key = 'textAlign'; // Do not overwrite the SVGElement.align method. Same as VML.
4272
wrapper[key] = value;
4273
wrapper.htmlUpdateTransform();
4278
// Set the default attributes
4286
whiteSpace: 'nowrap',
4287
fontFamily: this.style.fontFamily,
4288
fontSize: this.style.fontSize
4291
// Use the HTML specific .css method
4292
wrapper.css = wrapper.htmlCss;
4294
// This is specific for HTML within SVG
4295
if (renderer.isSVG) {
4296
wrapper.add = function (svgGroupWrapper) {
4299
container = renderer.box.parentNode,
4303
this.parentGroup = svgGroupWrapper;
4305
// Create a mock group to hold the HTML elements
4306
if (svgGroupWrapper) {
4307
htmlGroup = svgGroupWrapper.div;
4310
// Read the parent chain into an array and read from top down
4311
parentGroup = svgGroupWrapper;
4312
while (parentGroup) {
4314
parents.push(parentGroup);
4316
// Move up to the next parent group
4317
parentGroup = parentGroup.parentGroup;
4320
// Ensure dynamically updating position when any parent is translated
4321
each(parents.reverse(), function (parentGroup) {
4324
// Create a HTML div and append it to the parent div to emulate
4325
// the SVG group structure
4326
htmlGroup = parentGroup.div = parentGroup.div || createElement(DIV, {
4327
className: attr(parentGroup.element, 'class')
4330
left: (parentGroup.translateX || 0) + PX,
4331
top: (parentGroup.translateY || 0) + PX
4332
}, htmlGroup || container); // the top group is appended to container
4335
htmlGroupStyle = htmlGroup.style;
4337
// Set listeners to update the HTML div's position whenever the SVG group
4338
// position is changed
4339
extend(parentGroup.attrSetters, {
4340
translateX: function (value) {
4341
htmlGroupStyle.left = value + PX;
4343
translateY: function (value) {
4344
htmlGroupStyle.top = value + PX;
4346
visibility: function (value, key) {
4347
htmlGroupStyle[key] = value;
4354
htmlGroup = container;
4357
htmlGroup.appendChild(element);
4360
wrapper.added = true;
4361
if (wrapper.alignOnAdd) {
4362
wrapper.htmlUpdateTransform();
4372
/* ****************************************************************************
4374
* START OF INTERNET EXPLORER <= 8 SPECIFIC CODE *
4376
* For applications and websites that don't need IE support, like platform *
4377
* targeted mobile apps and web apps, this code can be removed. *
4379
*****************************************************************************/
4384
var VMLRenderer, VMLElement;
4385
if (!hasSVG && !useCanVG) {
4388
* The VML element wrapper.
4390
Highcharts.VMLElement = VMLElement = {
4393
* Initialize a new VML element wrapper. It builds the markup as a string
4394
* to minimize DOM traffic.
4395
* @param {Object} renderer
4396
* @param {Object} nodeName
4398
init: function (renderer, nodeName) {
4400
markup = ['<', nodeName, ' filled="f" stroked="f"'],
4401
style = ['position: ', ABSOLUTE, ';'],
4402
isDiv = nodeName === DIV;
4404
// divs and shapes need size
4405
if (nodeName === 'shape' || isDiv) {
4406
style.push('left:0;top:0;width:1px;height:1px;');
4408
style.push('visibility: ', isDiv ? HIDDEN : VISIBLE);
4410
markup.push(' style="', style.join(''), '"/>');
4412
// create element with default attributes and style
4414
markup = isDiv || nodeName === 'span' || nodeName === 'img' ?
4416
: renderer.prepVML(markup);
4417
wrapper.element = createElement(markup);
4420
wrapper.renderer = renderer;
4421
wrapper.attrSetters = {};
4425
* Add the node to the given parent
4426
* @param {Object} parent
4428
add: function (parent) {
4430
renderer = wrapper.renderer,
4431
element = wrapper.element,
4433
inverted = parent && parent.inverted,
4435
// get the parent node
4436
parentNode = parent ?
4437
parent.element || parent :
4441
// if the parent group is inverted, apply inversion on all children
4442
if (inverted) { // only on groups
4443
renderer.invertChild(element, parentNode);
4447
parentNode.appendChild(element);
4449
// align text after adding to be able to read offset
4450
wrapper.added = true;
4451
if (wrapper.alignOnAdd && !wrapper.deferUpdateTransform) {
4452
wrapper.updateTransform();
4455
// fire an event for internal hooks
4456
if (wrapper.onAdd) {
4464
* VML always uses htmlUpdateTransform
4466
updateTransform: SVGElement.prototype.htmlUpdateTransform,
4469
* Set the rotation of a span with oldIE's filter
4471
setSpanRotation: function () {
4472
// Adjust for alignment and rotation. Rotation of useHTML content is not yet implemented
4473
// but it can probably be implemented for Firefox 3.5+ on user request. FF3.5+
4474
// has support for CSS3 transform. The getBBox method also needs to be updated
4475
// to compensate for the rotation, like it currently does for SVG.
4476
// Test case: http://jsfiddle.net/highcharts/Ybt44/
4478
var rotation = this.rotation,
4479
costheta = mathCos(rotation * deg2rad),
4480
sintheta = mathSin(rotation * deg2rad);
4483
filter: rotation ? ['progid:DXImageTransform.Microsoft.Matrix(M11=', costheta,
4484
', M12=', -sintheta, ', M21=', sintheta, ', M22=', costheta,
4485
', sizingMethod=\'auto expand\')'].join('') : NONE
4490
* Get the positioning correction for the span after rotating.
4492
getSpanCorrection: function (width, baseline, alignCorrection, rotation, align) {
4494
var costheta = rotation ? mathCos(rotation * deg2rad) : 1,
4495
sintheta = rotation ? mathSin(rotation * deg2rad) : 0,
4496
height = pick(this.elemHeight, this.element.offsetHeight),
4498
nonLeft = align && align !== 'left';
4501
this.xCorr = costheta < 0 && -width;
4502
this.yCorr = sintheta < 0 && -height;
4504
// correct for baseline and corners spilling out after rotation
4505
quad = costheta * sintheta < 0;
4506
this.xCorr += sintheta * baseline * (quad ? 1 - alignCorrection : alignCorrection);
4507
this.yCorr -= costheta * baseline * (rotation ? (quad ? alignCorrection : 1 - alignCorrection) : 1);
4508
// correct for the length/height of the text
4510
this.xCorr -= width * alignCorrection * (costheta < 0 ? -1 : 1);
4512
this.yCorr -= height * alignCorrection * (sintheta < 0 ? -1 : 1);
4521
* Converts a subset of an SVG path definition to its VML counterpart. Takes an array
4522
* as the parameter and returns a string.
4524
pathToVML: function (value) {
4526
var i = value.length,
4531
// Multiply by 10 to allow subpixel precision.
4532
// Substracting half a pixel seems to make the coordinates
4533
// align with SVG, but this hasn't been tested thoroughly
4534
if (isNumber(value[i])) {
4535
path[i] = mathRound(value[i] * 10) - 5;
4536
} else if (value[i] === 'Z') { // close the path
4541
// When the start X and end X coordinates of an arc are too close,
4542
// they are rounded to the same value above. In this case, substract or
4543
// add 1 from the end X and Y positions. #186, #760, #1371, #1410.
4544
if (value.isArc && (value[i] === 'wa' || value[i] === 'at')) {
4546
if (path[i + 5] === path[i + 7]) {
4547
path[i + 7] += value[i + 7] > value[i + 5] ? 1 : -1;
4550
if (path[i + 6] === path[i + 8]) {
4551
path[i + 8] += value[i + 8] > value[i + 6] ? 1 : -1;
4558
// Loop up again to handle path shortcuts (#2132)
4559
/*while (i++ < path.length) {
4560
if (path[i] === 'H') { // horizontal line to
4562
path.splice(i + 2, 0, path[i - 1]);
4563
} else if (path[i] === 'V') { // vertical line to
4565
path.splice(i + 1, 0, path[i - 2]);
4568
return path.join(' ') || 'x';
4572
* Get or set attributes
4574
attr: function (hash, val) {
4580
element = wrapper.element || {},
4581
elemStyle = element.style,
4582
nodeName = element.nodeName,
4583
renderer = wrapper.renderer,
4584
symbolName = wrapper.symbolName,
4586
shadows = wrapper.shadows,
4588
attrSetters = wrapper.attrSetters,
4591
// single key-value pair
4592
if (isString(hash) && defined(val)) {
4598
// used as a getter, val is undefined
4599
if (isString(hash)) {
4601
if (key === 'strokeWidth' || key === 'stroke-width') {
4602
ret = wrapper.strokeweight;
4613
// check for a specific attribute setter
4614
result = attrSetters[key] && attrSetters[key].call(wrapper, value, key);
4616
if (result !== false && value !== null) { // #620
4618
if (result !== UNDEFINED) {
4619
value = result; // the attribute setter has returned a new value to set
4625
if (symbolName && /^(x|y|r|start|end|width|height|innerR|anchorX|anchorY)/.test(key)) {
4626
// if one of the symbol size affecting parameters are changed,
4627
// check all the others only once for each call to an element's
4629
if (!hasSetSymbolSize) {
4630
wrapper.symbolAttr(hash);
4632
hasSetSymbolSize = true;
4636
} else if (key === 'd') {
4637
value = value || [];
4638
wrapper.d = value.join(' '); // used in getter for animation
4640
element.path = value = wrapper.pathToVML(value);
4646
shadows[i].path = shadows[i].cutOff ? this.cutOffPath(value, shadows[i].cutOff) : value;
4651
// handle visibility
4652
} else if (key === 'visibility') {
4654
// Handle inherited visibility
4655
if (value === 'inherit') {
4659
// Let the shadow follow the main element
4663
shadows[i].style[key] = value;
4667
// Instead of toggling the visibility CSS property, move the div out of the viewport.
4668
// This works around #61 and #586
4669
if (nodeName === 'DIV') {
4670
value = value === HIDDEN ? '-999em' : 0;
4672
// In order to redraw, IE7 needs the div to be visible when tucked away
4673
// outside the viewport. So the visibility is actually opposite of
4674
// the expected value. This applies to the tooltip only.
4676
elemStyle[key] = value ? VISIBLE : HIDDEN;
4680
elemStyle[key] = value;
4683
// directly mapped to css
4684
} else if (key === 'zIndex') {
4687
elemStyle[key] = value;
4691
// x, y, width, height
4692
} else if (inArray(key, ['x', 'y', 'width', 'height']) !== -1) {
4694
wrapper[key] = value; // used in getter
4696
if (key === 'x' || key === 'y') {
4697
key = { x: 'left', y: 'top' }[key];
4699
value = mathMax(0, value); // don't set width or height below zero (#311)
4702
// clipping rectangle special
4703
if (wrapper.updateClipping) {
4704
wrapper[key] = value; // the key is now 'left' or 'top' for 'x' and 'y'
4705
wrapper.updateClipping();
4708
elemStyle[key] = value;
4714
} else if (key === 'class' && nodeName === 'DIV') {
4715
// IE8 Standards mode has problems retrieving the className
4716
element.className = value;
4719
} else if (key === 'stroke') {
4721
value = renderer.color(value, element, key);
4723
key = 'strokecolor';
4726
} else if (key === 'stroke-width' || key === 'strokeWidth') {
4727
element.stroked = value ? true : false;
4728
key = 'strokeweight';
4729
wrapper[key] = value; // used in getter, issue #113
4730
if (isNumber(value)) {
4735
} else if (key === 'dashstyle') {
4736
var strokeElem = element.getElementsByTagName('stroke')[0] ||
4737
createElement(renderer.prepVML(['<stroke/>']), null, null, element);
4738
strokeElem[key] = value || 'solid';
4739
wrapper.dashstyle = value; /* because changing stroke-width will change the dash length
4740
and cause an epileptic effect */
4744
} else if (key === 'fill') {
4746
if (nodeName === 'SPAN') { // text color
4747
elemStyle.color = value;
4748
} else if (nodeName !== 'IMG') { // #1336
4749
element.filled = value !== NONE ? true : false;
4751
value = renderer.color(value, element, key, wrapper);
4756
// opacity: don't bother - animation is too slow and filters introduce artifacts
4757
} else if (key === 'opacity') {
4763
// rotation on VML elements
4764
} else if (nodeName === 'shape' && key === 'rotation') {
4766
wrapper[key] = element.style[key] = value; // style is for #1873
4768
// Correction for the 1x1 size of the shape container. Used in gauge needles.
4769
element.style.left = -mathRound(mathSin(value * deg2rad) + 1) + PX;
4770
element.style.top = mathRound(mathCos(value * deg2rad)) + PX;
4772
// translation for animation
4773
} else if (key === 'translateX' || key === 'translateY' || key === 'rotation') {
4774
wrapper[key] = value;
4775
wrapper.updateTransform();
4783
if (docMode8) { // IE8 setAttribute bug
4784
element[key] = value;
4786
attr(element, key, value);
4797
* Set the element's clipping to a predefined rectangle
4799
* @param {String} id The id of the clip rectangle
4801
clip: function (clipRect) {
4807
clipMembers = clipRect.members;
4808
erase(clipMembers, wrapper); // Ensure unique list of elements (#1258)
4809
clipMembers.push(wrapper);
4810
wrapper.destroyClip = function () {
4811
erase(clipMembers, wrapper);
4813
cssRet = clipRect.getCSS(wrapper);
4816
if (wrapper.destroyClip) {
4817
wrapper.destroyClip();
4819
cssRet = { clip: docMode8 ? 'inherit' : 'rect(auto)' }; // #1214
4822
return wrapper.css(cssRet);
4827
* Set styles for the element
4828
* @param {Object} styles
4830
css: SVGElement.prototype.htmlCss,
4833
* Removes a child either by removeChild or move to garbageBin.
4834
* Issue 490; in VML removeChild results in Orphaned nodes according to sIEve, discardElement does not.
4836
safeRemoveChild: function (element) {
4837
// discardElement will detach the node from its parent before attaching it
4838
// to the garbage bin. Therefore it is important that the node is attached and have parent.
4839
if (element.parentNode) {
4840
discardElement(element);
4845
* Extend element.destroy by removing it from the clip members array
4847
destroy: function () {
4848
if (this.destroyClip) {
4852
return SVGElement.prototype.destroy.apply(this);
4856
* Add an event listener. VML override for normalizing event parameters.
4857
* @param {String} eventType
4858
* @param {Function} handler
4860
on: function (eventType, handler) {
4861
// simplest possible event model for internal use
4862
this.element['on' + eventType] = function () {
4863
var evt = win.event;
4864
evt.target = evt.srcElement;
4871
* In stacked columns, cut off the shadows so that they don't overlap
4873
cutOffPath: function (path, length) {
4877
path = path.split(/[ ,]/);
4880
if (len === 9 || len === 11) {
4881
path[len - 4] = path[len - 2] = pInt(path[len - 2]) - 10 * length;
4883
return path.join(' ');
4887
* Apply a drop shadow by copying elements and giving them different strokes
4888
* @param {Boolean|Object} shadowOptions
4890
shadow: function (shadowOptions, group, cutOff) {
4893
element = this.element,
4894
renderer = this.renderer,
4896
elemStyle = element.style,
4898
path = element.path,
4902
shadowElementOpacity;
4904
// some times empty paths are not strings
4905
if (path && typeof path.value !== 'string') {
4908
modifiedPath = path;
4910
if (shadowOptions) {
4911
shadowWidth = pick(shadowOptions.width, 3);
4912
shadowElementOpacity = (shadowOptions.opacity || 0.15) / shadowWidth;
4913
for (i = 1; i <= 3; i++) {
4915
strokeWidth = (shadowWidth * 2) + 1 - (2 * i);
4917
// Cut off shadows for stacked column items
4919
modifiedPath = this.cutOffPath(path.value, strokeWidth + 0.5);
4922
markup = ['<shape isShadow="true" strokeweight="', strokeWidth,
4923
'" filled="false" path="', modifiedPath,
4924
'" coordsize="10 10" style="', element.style.cssText, '" />'];
4926
shadow = createElement(renderer.prepVML(markup),
4928
left: pInt(elemStyle.left) + pick(shadowOptions.offsetX, 1),
4929
top: pInt(elemStyle.top) + pick(shadowOptions.offsetY, 1)
4933
shadow.cutOff = strokeWidth + 1;
4936
// apply the opacity
4937
markup = ['<stroke color="', shadowOptions.color || 'black', '" opacity="', shadowElementOpacity * i, '"/>'];
4938
createElement(renderer.prepVML(markup), null, null, shadow);
4943
group.element.appendChild(shadow);
4945
element.parentNode.insertBefore(shadow, element);
4949
shadows.push(shadow);
4953
this.shadows = shadows;
4959
VMLElement = extendClass(SVGElement, VMLElement);
4964
var VMLRendererExtension = { // inherit SVGRenderer
4966
Element: VMLElement,
4967
isIE8: userAgent.indexOf('MSIE 8.0') > -1,
4971
* Initialize the VMLRenderer
4972
* @param {Object} container
4973
* @param {Number} width
4974
* @param {Number} height
4976
init: function (container, width, height, style) {
4977
var renderer = this,
4982
renderer.alignedObjects = [];
4984
boxWrapper = renderer.createElement(DIV)
4985
.css(extend(this.getStyle(style), { position: RELATIVE}));
4986
box = boxWrapper.element;
4987
container.appendChild(boxWrapper.element);
4990
// generate the containing box
4991
renderer.isVML = true;
4993
renderer.boxWrapper = boxWrapper;
4994
renderer.cache = {};
4997
renderer.setSize(width, height, false);
4999
// The only way to make IE6 and IE7 print is to use a global namespace. However,
5000
// with IE8 the only way to make the dynamic shapes visible in screen and print mode
5001
// seems to be to add the xmlns attribute and the behaviour style inline.
5002
if (!doc.namespaces.hcv) {
5004
doc.namespaces.add('hcv', 'urn:schemas-microsoft-com:vml');
5006
// Setup default CSS (#2153, #2368, #2384)
5007
css = 'hcv\\:fill, hcv\\:path, hcv\\:shape, hcv\\:stroke' +
5008
'{ behavior:url(#default#VML); display: inline-block; } ';
5010
doc.createStyleSheet().cssText = css;
5012
doc.styleSheets[0].cssText += css;
5020
* Detect whether the renderer is hidden. This happens when one of the parent elements
5023
isHidden: function () {
5024
return !this.box.offsetWidth;
5028
* Define a clipping rectangle. In VML it is accomplished by storing the values
5029
* for setting the CSS style to all associated members.
5033
* @param {Number} width
5034
* @param {Number} height
5036
clipRect: function (x, y, width, height) {
5038
// create a dummy element
5039
var clipRect = this.createElement(),
5040
isObj = isObject(x);
5042
// mimic a rectangle with its style object for automatic updating in attr
5043
return extend(clipRect, {
5045
left: (isObj ? x.x : x) + 1,
5046
top: (isObj ? x.y : y) + 1,
5047
width: (isObj ? x.width : width) - 1,
5048
height: (isObj ? x.height : height) - 1,
5049
getCSS: function (wrapper) {
5050
var element = wrapper.element,
5051
nodeName = element.nodeName,
5052
isShape = nodeName === 'shape',
5053
inverted = wrapper.inverted,
5055
top = rect.top - (isShape ? element.offsetTop : 0),
5057
right = left + rect.width,
5058
bottom = top + rect.height,
5061
mathRound(inverted ? left : top) + 'px,' +
5062
mathRound(inverted ? bottom : right) + 'px,' +
5063
mathRound(inverted ? right : bottom) + 'px,' +
5064
mathRound(inverted ? top : left) + 'px)'
5067
// issue 74 workaround
5068
if (!inverted && docMode8 && nodeName === 'DIV') {
5077
// used in attr and animation to update the clipping of all members
5078
updateClipping: function () {
5079
each(clipRect.members, function (member) {
5080
member.css(clipRect.getCSS(member));
5089
* Take a color and return it if it's a string, make it a gradient if it's a
5090
* gradient configuration object, and apply opacity.
5092
* @param {Object} color The color or config object
5094
color: function (color, elem, prop, wrapper) {
5095
var renderer = this,
5097
regexRgba = /^rgba/,
5102
// Check for linear or radial gradient
5103
if (color && color.linearGradient) {
5104
fillType = 'gradient';
5105
} else if (color && color.radialGradient) {
5106
fillType = 'pattern';
5114
gradient = color.linearGradient || color.radialGradient,
5124
stops = color.stops,
5128
addFillNode = function () {
5129
// Add the fill subnode. When colors attribute is used, the meanings of opacity and o:opacity2
5131
markup = ['<fill colors="' + colors.join(',') + '" opacity="', opacity2, '" o:opacity2="', opacity1,
5132
'" type="', fillType, '" ', fillAttr, 'focus="100%" method="any" />'];
5133
createElement(renderer.prepVML(markup), null, null, elem);
5136
// Extend from 0 to 1
5137
firstStop = stops[0];
5138
lastStop = stops[stops.length - 1];
5139
if (firstStop[0] > 0) {
5145
if (lastStop[0] < 1) {
5152
// Compute the stops
5153
each(stops, function (stop, i) {
5154
if (regexRgba.test(stop[1])) {
5155
colorObject = Color(stop[1]);
5156
stopColor = colorObject.get('rgb');
5157
stopOpacity = colorObject.get('a');
5159
stopColor = stop[1];
5163
// Build the color attribute
5164
colors.push((stop[0] * 100) + '% ' + stopColor);
5166
// Only start and end opacities are allowed, so we use the first and the last
5168
opacity1 = stopOpacity;
5171
opacity2 = stopOpacity;
5176
// Apply the gradient to fills only.
5177
if (prop === 'fill') {
5179
// Handle linear gradient angle
5180
if (fillType === 'gradient') {
5181
x1 = gradient.x1 || gradient[0] || 0;
5182
y1 = gradient.y1 || gradient[1] || 0;
5183
x2 = gradient.x2 || gradient[2] || 0;
5184
y2 = gradient.y2 || gradient[3] || 0;
5185
fillAttr = 'angle="' + (90 - math.atan(
5186
(y2 - y1) / // y vector
5187
(x2 - x1) // x vector
5188
) * 180 / mathPI) + '"';
5192
// Radial (circular) gradient
5200
radialReference = elem.radialReference,
5202
applyRadialGradient = function () {
5203
if (radialReference) {
5204
bBox = wrapper.getBBox();
5205
cx += (radialReference[0] - bBox.x) / bBox.width - 0.5;
5206
cy += (radialReference[1] - bBox.y) / bBox.height - 0.5;
5207
sizex *= radialReference[2] / bBox.width;
5208
sizey *= radialReference[2] / bBox.height;
5210
fillAttr = 'src="' + defaultOptions.global.VMLRadialGradientURL + '" ' +
5211
'size="' + sizex + ',' + sizey + '" ' +
5212
'origin="0.5,0.5" ' +
5213
'position="' + cx + ',' + cy + '" ' +
5214
'color2="' + color2 + '" ';
5219
// Apply radial gradient
5220
if (wrapper.added) {
5221
applyRadialGradient();
5223
// We need to know the bounding box to get the size and position right
5224
wrapper.onAdd = applyRadialGradient;
5227
// The fill element's color attribute is broken in IE8 standards mode, so we
5228
// need to set the parent shape's fillcolor attribute instead.
5232
// Gradients are not supported for VML stroke, return the first color. #722.
5237
// if the color is an rgba color, split it and add a fill node
5238
// to hold the opacity component
5239
} else if (regexRgba.test(color) && elem.tagName !== 'IMG') {
5241
colorObject = Color(color);
5243
markup = ['<', prop, ' opacity="', colorObject.get('a'), '"/>'];
5244
createElement(this.prepVML(markup), null, null, elem);
5246
ret = colorObject.get('rgb');
5250
var propNodes = elem.getElementsByTagName(prop); // 'stroke' or 'fill' node
5251
if (propNodes.length) {
5252
propNodes[0].opacity = 1;
5253
propNodes[0].type = 'solid';
5262
* Take a VML string and prepare it for either IE8 or IE6/IE7.
5263
* @param {Array} markup A string array of the VML markup to prepare
5265
prepVML: function (markup) {
5266
var vmlStyle = 'display:inline-block;behavior:url(#default#VML);',
5269
markup = markup.join('');
5271
if (isIE8) { // add xmlns and style inline
5272
markup = markup.replace('/>', ' xmlns="urn:schemas-microsoft-com:vml" />');
5273
if (markup.indexOf('style="') === -1) {
5274
markup = markup.replace('/>', ' style="' + vmlStyle + '" />');
5276
markup = markup.replace('style="', 'style="' + vmlStyle);
5279
} else { // add namespace
5280
markup = markup.replace('<', '<hcv:');
5287
* Create rotated and aligned text
5288
* @param {String} str
5292
text: SVGRenderer.prototype.html,
5295
* Create and return a path element
5296
* @param {Array} path
5298
path: function (path) {
5300
// subpixel precision down to 0.1 (width and height = 1px)
5303
if (isArray(path)) {
5305
} else if (isObject(path)) { // attributes
5309
return this.createElement('shape').attr(attr);
5313
* Create and return a circle element. In VML circles are implemented as
5314
* shapes, which is faster than v:oval
5319
circle: function (x, y, r) {
5320
var circle = this.symbol('circle');
5326
circle.isCircle = true; // Causes x and y to mean center (#1682)
5328
return circle.attr({ x: x, y: y });
5332
* Create a group using an outer div and an inner v:group to allow rotating
5333
* and flipping. A simple v:group would have problems with positioning
5334
* child HTML elements and CSS clip.
5336
* @param {String} name The name of the group
5338
g: function (name) {
5342
// set the class name
5344
attribs = { 'className': PREFIX + name, 'class': PREFIX + name };
5347
// the div to hold HTML and clipping
5348
wrapper = this.createElement(DIV).attr(attribs);
5354
* VML override to create a regular HTML image
5355
* @param {String} src
5358
* @param {Number} width
5359
* @param {Number} height
5361
image: function (src, x, y, width, height) {
5362
var obj = this.createElement('img')
5363
.attr({ src: src });
5365
if (arguments.length > 1) {
5377
* For rectangles, VML uses a shape for rect to overcome bugs and rotation problems
5379
createElement: function (nodeName) {
5380
return nodeName === 'rect' ? this.symbol(nodeName) : SVGRenderer.prototype.createElement.call(this, nodeName);
5384
* In the VML renderer, each child of an inverted div (group) is inverted
5385
* @param {Object} element
5386
* @param {Object} parentNode
5388
invertChild: function (element, parentNode) {
5390
parentStyle = parentNode.style,
5391
imgStyle = element.tagName === 'IMG' && element.style; // #1111
5395
left: pInt(parentStyle.width) - (imgStyle ? pInt(imgStyle.top) : 1),
5396
top: pInt(parentStyle.height) - (imgStyle ? pInt(imgStyle.left) : 1),
5400
// Recursively invert child elements, needed for nested composite shapes like box plots and error bars. #1680, #1806.
5401
each(element.childNodes, function (child) {
5402
ren.invertChild(child, element);
5407
* Symbol definitions that override the parent SVG renderer's symbols
5411
// VML specific arc function
5412
arc: function (x, y, w, h, options) {
5413
var start = options.start,
5415
radius = options.r || w || h,
5416
innerRadius = options.innerR,
5417
cosStart = mathCos(start),
5418
sinStart = mathSin(start),
5419
cosEnd = mathCos(end),
5420
sinEnd = mathSin(end),
5423
if (end - start === 0) { // no angle, don't show it.
5428
'wa', // clockwise arc to
5431
x + radius, // right
5432
y + radius, // bottom
5433
x + radius * cosStart, // start x
5434
y + radius * sinStart, // start y
5435
x + radius * cosEnd, // end x
5436
y + radius * sinEnd // end y
5439
if (options.open && !innerRadius) {
5449
'at', // anti clockwise arc to
5450
x - innerRadius, // left
5451
y - innerRadius, // top
5452
x + innerRadius, // right
5453
y + innerRadius, // bottom
5454
x + innerRadius * cosEnd, // start x
5455
y + innerRadius * sinEnd, // start y
5456
x + innerRadius * cosStart, // end x
5457
y + innerRadius * sinStart, // end y
5466
// Add circle symbol path. This performs significantly faster than v:oval.
5467
circle: function (x, y, w, h, wrapper) {
5470
w = h = 2 * wrapper.r;
5473
// Center correction, #1682
5474
if (wrapper && wrapper.isCircle) {
5481
'wa', // clockwisearcto
5487
y + h / 2, // start y
5490
//'x', // finish path
5495
* Add rectangle symbol path which eases rotation and omits arcsize problems
5496
* compared to the built-in VML roundrect shape
5498
* @param {Number} left Left position
5499
* @param {Number} top Top position
5500
* @param {Number} r Border radius
5501
* @param {Object} options Width and height
5504
rect: function (left, top, width, height, options) {
5506
var right = left + width,
5507
bottom = top + height,
5511
// No radius, return the more lightweight square
5512
if (!defined(options) || !options.r) {
5513
ret = SVGRenderer.prototype.symbols.square.apply(0, arguments);
5515
// Has radius add arcs for the corners
5518
r = mathMin(options.r, width, height);
5534
right - 2 * r, bottom - 2 * r,
5542
left, bottom - 2 * r,
5543
left + 2 * r, bottom,
5551
left + 2 * r, top + 2 * r,
5564
Highcharts.VMLRenderer = VMLRenderer = function () {
5565
this.init.apply(this, arguments);
5567
VMLRenderer.prototype = merge(SVGRenderer.prototype, VMLRendererExtension);
5570
Renderer = VMLRenderer;
5573
// This method is used with exporting in old IE, when emulating SVG (see #2314)
5574
SVGRenderer.prototype.measureSpanWidth = function (text, styles) {
5575
var measuringSpan = doc.createElement('span'),
5577
textNode = doc.createTextNode(text);
5579
measuringSpan.appendChild(textNode);
5580
css(measuringSpan, styles);
5581
this.box.appendChild(measuringSpan);
5582
offsetWidth = measuringSpan.offsetWidth;
5583
discardElement(measuringSpan); // #2463
5588
/* ****************************************************************************
5590
* END OF INTERNET EXPLORER <= 8 SPECIFIC CODE *
5592
*****************************************************************************/
5593
/* ****************************************************************************
5595
* START OF ANDROID < 3 SPECIFIC CODE. THIS CAN BE REMOVED IF YOU'RE NOT *
5596
* TARGETING THAT SYSTEM. *
5598
*****************************************************************************/
5604
* The CanVGRenderer is empty from start to keep the source footprint small.
5605
* When requested, the CanVGController downloads the rest of the source packaged
5606
* together with the canvg library.
5608
Highcharts.CanVGRenderer = CanVGRenderer = function () {
5609
// Override the global SVG namespace to fake SVG/HTML that accepts CSS
5610
SVG_NS = 'http://www.w3.org/1999/xhtml';
5614
* Start with an empty symbols object. This is needed when exporting is used (exporting.src.js will add a few symbols), but
5615
* the implementation from SvgRenderer will not be merged in until first render.
5617
CanVGRenderer.prototype.symbols = {};
5620
* Handles on demand download of canvg rendering support.
5622
CanVGController = (function () {
5623
// List of renderering calls
5624
var deferredRenderCalls = [];
5627
* When downloaded, we are ready to draw deferred charts.
5629
function drawDeferred() {
5630
var callLength = deferredRenderCalls.length,
5633
// Draw all pending render calls
5634
for (callIndex = 0; callIndex < callLength; callIndex++) {
5635
deferredRenderCalls[callIndex]();
5638
deferredRenderCalls = [];
5642
push: function (func, scriptLocation) {
5643
// Only get the script once
5644
if (deferredRenderCalls.length === 0) {
5645
getScript(scriptLocation, drawDeferred);
5647
// Register render call
5648
deferredRenderCalls.push(func);
5653
Renderer = CanVGRenderer;
5654
} // end CanVGRenderer
5656
/* ****************************************************************************
5658
* END OF ANDROID < 3 SPECIFIC CODE *
5660
*****************************************************************************/
5665
function Tick(axis, pos, type, noLabel) {
5668
this.type = type || '';
5671
if (!type && !noLabel) {
5678
* Write the tick label
5680
addLabel: function () {
5683
options = axis.options,
5686
categories = axis.categories,
5689
labelOptions = options.labels,
5691
tickPositions = axis.tickPositions,
5692
width = (horiz && categories &&
5693
!labelOptions.step && !labelOptions.staggerLines &&
5694
!labelOptions.rotation &&
5695
chart.plotWidth / tickPositions.length) ||
5696
(!horiz && (chart.margin[3] || chart.chartWidth * 0.33)), // #1580, #1931
5697
isFirst = pos === tickPositions[0],
5698
isLast = pos === tickPositions[tickPositions.length - 1],
5701
value = categories ?
5702
pick(categories[pos], names[pos], pos) :
5705
tickPositionInfo = tickPositions.info,
5706
dateTimeLabelFormat;
5708
// Set the datetime label format. If a higher rank is set for this position, use that. If not,
5709
// use the general format.
5710
if (axis.isDatetimeAxis && tickPositionInfo) {
5711
dateTimeLabelFormat = options.dateTimeLabelFormats[tickPositionInfo.higherRanks[pos] || tickPositionInfo.unitName];
5713
// set properties for access in render method
5714
tick.isFirst = isFirst;
5715
tick.isLast = isLast;
5718
str = axis.labelFormatter.call({
5723
dateTimeLabelFormat: dateTimeLabelFormat,
5724
value: axis.isLog ? correctFloat(lin2log(value)) : value
5728
css = width && { width: mathMax(1, mathRound(width - 2 * (labelOptions.padding || 10))) + PX };
5729
css = extend(css, labelOptions.style);
5732
if (!defined(label)) {
5734
align: axis.labelAlign
5736
if (isNumber(labelOptions.rotation)) {
5737
attr.rotation = labelOptions.rotation;
5739
if (width && labelOptions.ellipsis) {
5740
attr._clipHeight = axis.len / tickPositions.length;
5744
defined(str) && labelOptions.enabled ?
5745
chart.renderer.text(
5749
labelOptions.useHTML
5752
// without position absolute, IE export sometimes is wrong
5754
.add(axis.labelGroup) :
5767
* Get the offset height or width of the label
5769
getLabelSize: function () {
5770
var label = this.label,
5773
label.getBBox()[axis.horiz ? 'height' : 'width'] :
5778
* Find how far the labels extend to the right and left of the tick's x position. Used for anti-collision
5779
* detection with overflow logic.
5781
getLabelSides: function () {
5782
var bBox = this.label.getBBox(),
5785
options = axis.options,
5786
labelOptions = options.labels,
5787
size = horiz ? bBox.width : bBox.height,
5789
labelOptions.x - size * { left: 0, center: 0.5, right: 1 }[axis.labelAlign] :
5795
return [leftSide, rightSide];
5799
* Handle the label overflow by adjusting the labels to the left and right edge, or
5800
* hide them if they collide into the neighbour label.
5802
handleOverflow: function (index, xy) {
5805
isFirst = this.isFirst,
5806
isLast = this.isLast,
5808
pxPos = horiz ? xy.x : xy.y,
5809
reversed = axis.reversed,
5810
tickPositions = axis.tickPositions,
5811
sides = this.getLabelSides(),
5812
leftSide = sides[0],
5813
rightSide = sides[1],
5818
line = this.label.line || 0,
5819
labelEdge = axis.labelEdge,
5820
justifyLabel = axis.justifyLabels && (isFirst || isLast),
5823
// Hide it if it now overlaps the neighbour label
5824
if (labelEdge[line] === UNDEFINED || pxPos + leftSide > labelEdge[line]) {
5825
labelEdge[line] = pxPos + rightSide;
5827
} else if (!justifyLabel) {
5832
justifyToPlot = axis.justifyToPlot;
5833
axisLeft = justifyToPlot ? axis.pos : 0;
5834
axisRight = justifyToPlot ? axisLeft + axis.len : axis.chart.chartWidth;
5836
// Find the firsth neighbour on the same line
5838
index += (isFirst ? 1 : -1);
5839
neighbour = axis.ticks[tickPositions[index]];
5840
} while (tickPositions[index] && (!neighbour || neighbour.label.line !== line));
5842
neighbourEdge = neighbour && neighbour.label.xy && neighbour.label.xy.x + neighbour.getLabelSides()[isFirst ? 0 : 1];
5844
if ((isFirst && !reversed) || (isLast && reversed)) {
5845
// Is the label spilling out to the left of the plot area?
5846
if (pxPos + leftSide < axisLeft) {
5848
// Align it to plot left
5849
pxPos = axisLeft - leftSide;
5851
// Hide it if it now overlaps the neighbour label
5852
if (neighbour && pxPos + rightSide > neighbourEdge) {
5858
// Is the label spilling out to the right of the plot area?
5859
if (pxPos + rightSide > axisRight) {
5861
// Align it to plot right
5862
pxPos = axisRight - rightSide;
5864
// Hide it if it now overlaps the neighbour label
5865
if (neighbour && pxPos + leftSide < neighbourEdge) {
5872
// Set the modified x position of the label
5879
* Get the x and y position for ticks and labels
5881
getPosition: function (horiz, pos, tickmarkOffset, old) {
5882
var axis = this.axis,
5884
cHeight = (old && chart.oldChartHeight) || chart.chartHeight;
5888
axis.translate(pos + tickmarkOffset, null, null, old) + axis.transB :
5889
axis.left + axis.offset + (axis.opposite ? ((old && chart.oldChartWidth) || chart.chartWidth) - axis.right - axis.left : 0),
5892
cHeight - axis.bottom + axis.offset - (axis.opposite ? axis.height : 0) :
5893
cHeight - axis.translate(pos + tickmarkOffset, null, null, old) - axis.transB
5899
* Get the x, y position of the tick label
5901
getLabelPosition: function (x, y, label, horiz, labelOptions, tickmarkOffset, index, step) {
5902
var axis = this.axis,
5903
transA = axis.transA,
5904
reversed = axis.reversed,
5905
staggerLines = axis.staggerLines,
5906
baseline = axis.chart.renderer.fontMetrics(labelOptions.style.fontSize).b,
5907
rotation = labelOptions.rotation;
5909
x = x + labelOptions.x - (tickmarkOffset && horiz ?
5910
tickmarkOffset * transA * (reversed ? -1 : 1) : 0);
5911
y = y + labelOptions.y - (tickmarkOffset && !horiz ?
5912
tickmarkOffset * transA * (reversed ? 1 : -1) : 0);
5914
// Correct for rotation (#1764)
5915
if (rotation && axis.side === 2) {
5916
y -= baseline - baseline * mathCos(rotation * deg2rad);
5919
// Vertically centered
5920
if (!defined(labelOptions.y) && !rotation) { // #1951
5921
y += baseline - label.getBBox().height / 2;
5924
// Correct for staggered labels
5926
label.line = (index / (step || 1) % staggerLines);
5927
y += label.line * (axis.labelOffset / staggerLines);
5937
* Extendible method to return the path of the marker
5939
getMarkPath: function (x, y, tickLength, tickWidth, horiz, renderer) {
5940
return renderer.crispLine([
5945
x + (horiz ? 0 : -tickLength),
5946
y + (horiz ? tickLength : 0)
5951
* Put everything in place
5953
* @param index {Number}
5954
* @param old {Boolean} Use old coordinates to prepare an animation into new position
5956
render: function (index, old, opacity) {
5959
options = axis.options,
5961
renderer = chart.renderer,
5966
labelOptions = options.labels,
5967
gridLine = tick.gridLine,
5968
gridPrefix = type ? type + 'Grid' : 'grid',
5969
tickPrefix = type ? type + 'Tick' : 'tick',
5970
gridLineWidth = options[gridPrefix + 'LineWidth'],
5971
gridLineColor = options[gridPrefix + 'LineColor'],
5972
dashStyle = options[gridPrefix + 'LineDashStyle'],
5973
tickLength = options[tickPrefix + 'Length'],
5974
tickWidth = options[tickPrefix + 'Width'] || 0,
5975
tickColor = options[tickPrefix + 'Color'],
5976
tickPosition = options[tickPrefix + 'Position'],
5980
step = labelOptions.step,
5983
tickmarkOffset = axis.tickmarkOffset,
5984
xy = tick.getPosition(horiz, pos, tickmarkOffset, old),
5987
reverseCrisp = ((horiz && x === axis.pos + axis.len) || (!horiz && y === axis.pos)) ? -1 : 1; // #1480, #1687
5989
this.isActive = true;
5991
// create the grid line
5992
if (gridLineWidth) {
5993
gridLinePath = axis.getPlotLinePath(pos + tickmarkOffset, gridLineWidth * reverseCrisp, old, true);
5995
if (gridLine === UNDEFINED) {
5997
stroke: gridLineColor,
5998
'stroke-width': gridLineWidth
6001
attribs.dashstyle = dashStyle;
6007
attribs.opacity = 0;
6009
tick.gridLine = gridLine =
6011
renderer.path(gridLinePath)
6012
.attr(attribs).add(axis.gridGroup) :
6016
// If the parameter 'old' is set, the current call will be followed
6017
// by another call, therefore do not do any animations this time
6018
if (!old && gridLine && gridLinePath) {
6019
gridLine[tick.isNew ? 'attr' : 'animate']({
6026
// create the tick mark
6027
if (tickWidth && tickLength) {
6029
// negate the length
6030
if (tickPosition === 'inside') {
6031
tickLength = -tickLength;
6033
if (axis.opposite) {
6034
tickLength = -tickLength;
6037
markPath = tick.getMarkPath(x, y, tickLength, tickWidth * reverseCrisp, horiz, renderer);
6038
if (mark) { // updating
6043
} else { // first time
6044
tick.mark = renderer.path(
6048
'stroke-width': tickWidth,
6050
}).add(axis.axisGroup);
6054
// the label is created on init - now move it into place
6055
if (label && !isNaN(x)) {
6056
label.xy = xy = tick.getLabelPosition(x, y, label, horiz, labelOptions, tickmarkOffset, index, step);
6058
// Apply show first and show last. If the tick is both first and last, it is
6059
// a single centered tick, in which case we show the label anyway (#2100).
6060
if ((tick.isFirst && !tick.isLast && !pick(options.showFirstLabel, 1)) ||
6061
(tick.isLast && !tick.isFirst && !pick(options.showLastLabel, 1))) {
6064
// Handle label overflow and show or hide accordingly
6065
} else if (!axis.isRadial && !labelOptions.step && !labelOptions.rotation && !old && opacity !== 0) {
6066
show = tick.handleOverflow(index, xy);
6070
if (step && index % step) {
6071
// show those indices dividable by step
6075
// Set the new position, and show or hide
6076
if (show && !isNaN(xy.y)) {
6077
xy.opacity = opacity;
6078
label[tick.isNew ? 'attr' : 'animate'](xy);
6081
label.attr('y', -9999); // #1338
6087
* Destructor for the tick prototype
6089
destroy: function () {
6090
destroyObjectProperties(this, this.axis);
6095
* The object wrapper for plot lines and plot bands
6096
* @param {Object} options
6098
Highcharts.PlotLineOrBand = function (axis, options) {
6102
this.options = options;
6103
this.id = options.id;
6107
Highcharts.PlotLineOrBand.prototype = {
6110
* Render the plot line or plot band. If it is already existing,
6113
render: function () {
6114
var plotLine = this,
6115
axis = plotLine.axis,
6117
halfPointRange = (axis.pointRange || 0) / 2,
6118
options = plotLine.options,
6119
optionsLabel = options.label,
6120
label = plotLine.label,
6121
width = options.width,
6123
from = options.from,
6124
isBand = defined(from) && defined(to),
6125
value = options.value,
6126
dashStyle = options.dashStyle,
6127
svgElem = plotLine.svgElem,
6135
color = options.color,
6136
zIndex = options.zIndex,
6137
events = options.events,
6139
renderer = axis.chart.renderer;
6141
// logarithmic conversion
6143
from = log2lin(from);
6145
value = log2lin(value);
6150
path = axis.getPlotLinePath(value, width);
6153
'stroke-width': width
6156
attribs.dashstyle = dashStyle;
6158
} else if (isBand) { // plot band
6160
// keep within plot area
6161
from = mathMax(from, axis.min - halfPointRange);
6162
to = mathMin(to, axis.max + halfPointRange);
6164
path = axis.getPlotBandPath(from, to, options);
6168
if (options.borderWidth) {
6169
attribs.stroke = options.borderColor;
6170
attribs['stroke-width'] = options.borderWidth;
6176
if (defined(zIndex)) {
6177
attribs.zIndex = zIndex;
6180
// common for lines and bands
6185
}, null, svgElem.onGetPath);
6188
svgElem.onGetPath = function () {
6192
plotLine.label = label = label.destroy();
6195
} else if (path && path.length) {
6196
plotLine.svgElem = svgElem = renderer.path(path)
6197
.attr(attribs).add();
6201
addEvent = function (eventType) {
6202
svgElem.on(eventType, function (e) {
6203
events[eventType].apply(plotLine, [e]);
6206
for (eventType in events) {
6207
addEvent(eventType);
6212
// the plot band/line label
6213
if (optionsLabel && defined(optionsLabel.text) && path && path.length && axis.width > 0 && axis.height > 0) {
6215
optionsLabel = merge({
6216
align: horiz && isBand && 'center',
6217
x: horiz ? !isBand && 4 : 10,
6218
verticalAlign : !horiz && isBand && 'middle',
6219
y: horiz ? isBand ? 16 : 10 : isBand ? 6 : -4,
6220
rotation: horiz && !isBand && 90
6223
// add the SVG element
6225
plotLine.label = label = renderer.text(
6229
optionsLabel.useHTML
6232
align: optionsLabel.textAlign || optionsLabel.align,
6233
rotation: optionsLabel.rotation,
6236
.css(optionsLabel.style)
6240
// get the bounding box and align the label
6241
xs = [path[1], path[4], pick(path[6], path[1])];
6242
ys = [path[2], path[5], pick(path[7], path[2])];
6246
label.align(optionsLabel, false, {
6249
width: arrayMax(xs) - x,
6250
height: arrayMax(ys) - y
6254
} else if (label) { // move out of sight
6263
* Remove the plot line or band
6265
destroy: function () {
6266
// remove it from the lookup
6267
erase(this.axis.plotLinesAndBands, this);
6270
destroyObjectProperties(this);
6275
* Object with members for extending the Axis prototype
6278
AxisPlotLineOrBandExtension = {
6281
* Create the path for a plot band
6283
getPlotBandPath: function (from, to) {
6284
var toPath = this.getPlotLinePath(to),
6285
path = this.getPlotLinePath(from);
6287
if (path && toPath) {
6294
} else { // outside the axis area
6301
addPlotBand: function (options) {
6302
this.addPlotBandOrLine(options, 'plotBands');
6305
addPlotLine: function (options) {
6306
this.addPlotBandOrLine(options, 'plotLines');
6310
* Add a plot band or plot line after render time
6312
* @param options {Object} The plotBand or plotLine configuration object
6314
addPlotBandOrLine: function (options, coll) {
6315
var obj = new Highcharts.PlotLineOrBand(this, options).render(),
6316
userOptions = this.userOptions;
6319
// Add it to the user options for exporting and Axis.update
6321
userOptions[coll] = userOptions[coll] || [];
6322
userOptions[coll].push(options);
6324
this.plotLinesAndBands.push(obj);
6331
* Remove a plot band or plot line from the chart by id
6332
* @param {Object} id
6334
removePlotBandOrLine: function (id) {
6335
var plotLinesAndBands = this.plotLinesAndBands,
6336
options = this.options,
6337
userOptions = this.userOptions,
6338
i = plotLinesAndBands.length;
6340
if (plotLinesAndBands[i].id === id) {
6341
plotLinesAndBands[i].destroy();
6344
each([options.plotLines || [], userOptions.plotLines || [], options.plotBands || [], userOptions.plotBands || []], function (arr) {
6347
if (arr[i].id === id) {
6356
* Create a new axis object
6357
* @param {Object} chart
6358
* @param {Object} options
6361
this.init.apply(this, arguments);
6367
* Default options for the X axis - the Y axis has extended defaults
6370
// allowDecimals: null,
6371
// alternateGridColor: null,
6373
dateTimeLabelFormats: {
6374
millisecond: '%H:%M:%S.%L',
6384
gridLineColor: '#C0C0C0',
6385
// gridLineDashStyle: 'solid',
6386
// gridLineWidth: 0,
6389
labels: defaultLabelOptions,
6391
lineColor: '#C0D0E0',
6399
minorGridLineColor: '#E0E0E0',
6400
// minorGridLineDashStyle: null,
6401
minorGridLineWidth: 1,
6402
minorTickColor: '#A0A0A0',
6403
//minorTickInterval: null,
6405
minorTickPosition: 'outside', // inside or outside
6406
//minorTickWidth: 0,
6412
// labels: { align, x, verticalAlign, y, style, rotation, textAlign }
6418
// labels: { align, x, verticalAlign, y, style, rotation, textAlign }
6421
// showFirstLabel: true,
6422
// showLastLabel: true,
6425
tickColor: '#C0D0E0',
6426
//tickInterval: null,
6428
tickmarkPlacement: 'between', // on or between
6429
tickPixelInterval: 100,
6430
tickPosition: 'outside',
6434
align: 'middle', // low, middle or high
6435
//margin: 0 for horizontal, 10 for vertical axes,
6440
//font: defaultFont.replace('normal', 'bold')
6446
type: 'linear' // linear, logarithmic or datetime
6450
* This options set extends the defaultOptions for Y axes
6452
defaultYAxisOptions: {
6455
tickPixelInterval: 72,
6456
showLastLabel: true,
6475
//verticalAlign: dynamic,
6476
//textAlign: dynamic,
6478
formatter: function () {
6479
return numberFormat(this.total, -1);
6481
style: defaultLabelOptions.style
6486
* These options extend the defaultOptions for left axes
6488
defaultLeftAxisOptions: {
6499
* These options extend the defaultOptions for right axes
6501
defaultRightAxisOptions: {
6512
* These options extend the defaultOptions for bottom axes
6514
defaultBottomAxisOptions: {
6518
// overflow: undefined,
6519
// staggerLines: null
6526
* These options extend the defaultOptions for left axes
6528
defaultTopAxisOptions: {
6532
// overflow: undefined
6533
// staggerLines: null
6541
* Initialize the axis
6543
init: function (chart, userOptions) {
6546
var isXAxis = userOptions.isX,
6549
// Flag, is the axis horizontal
6550
axis.horiz = chart.inverted ? !isXAxis : isXAxis;
6553
axis.isXAxis = isXAxis;
6554
axis.coll = isXAxis ? 'xAxis' : 'yAxis';
6556
axis.opposite = userOptions.opposite; // needed in setOptions
6557
axis.side = userOptions.side || (axis.horiz ?
6558
(axis.opposite ? 0 : 2) : // top : bottom
6559
(axis.opposite ? 1 : 3)); // right : left
6561
axis.setOptions(userOptions);
6564
var options = this.options,
6565
type = options.type,
6566
isDatetimeAxis = type === 'datetime';
6568
axis.labelFormatter = options.labels.formatter || axis.defaultLabelFormatter; // can be overwritten by dynamic format
6571
// Flag, stagger lines or not
6572
axis.userOptions = userOptions;
6574
//axis.axisTitleMargin = UNDEFINED,// = options.title.margin,
6575
axis.minPixelPadding = 0;
6576
//axis.ignoreMinPadding = UNDEFINED; // can be set to true by a column or bar series
6577
//axis.ignoreMaxPadding = UNDEFINED;
6580
axis.reversed = options.reversed;
6581
axis.zoomEnabled = options.zoomEnabled !== false;
6583
// Initial categories
6584
axis.categories = options.categories || type === 'category';
6588
//axis.axisGroup = UNDEFINED;
6589
//axis.gridGroup = UNDEFINED;
6590
//axis.axisTitle = UNDEFINED;
6591
//axis.axisLine = UNDEFINED;
6594
axis.isLog = type === 'logarithmic';
6595
axis.isDatetimeAxis = isDatetimeAxis;
6597
// Flag, if axis is linked to another axis
6598
axis.isLinked = defined(options.linkedTo);
6600
//axis.linkedParent = UNDEFINED;
6603
//axis.tickPositions = UNDEFINED; // array containing predefined positions
6605
//axis.tickInterval = UNDEFINED;
6606
//axis.minorTickInterval = UNDEFINED;
6608
axis.tickmarkOffset = (axis.categories && options.tickmarkPlacement === 'between') ? 0.5 : 0;
6612
axis.labelEdge = [];
6614
axis.minorTicks = {};
6615
//axis.tickAmount = UNDEFINED;
6617
// List of plotLines/Bands
6618
axis.plotLinesAndBands = [];
6621
axis.alternateBands = {};
6624
//axis.left = UNDEFINED;
6625
//axis.top = UNDEFINED;
6626
//axis.width = UNDEFINED;
6627
//axis.height = UNDEFINED;
6628
//axis.bottom = UNDEFINED;
6629
//axis.right = UNDEFINED;
6630
//axis.transA = UNDEFINED;
6631
//axis.transB = UNDEFINED;
6632
//axis.oldTransA = UNDEFINED;
6634
//axis.oldMin = UNDEFINED;
6635
//axis.oldMax = UNDEFINED;
6636
//axis.oldUserMin = UNDEFINED;
6637
//axis.oldUserMax = UNDEFINED;
6638
//axis.oldAxisLength = UNDEFINED;
6639
axis.minRange = axis.userMinRange = options.minRange || options.maxZoom;
6640
axis.range = options.range;
6641
axis.offset = options.offset || 0;
6644
// Dictionary for stacks
6646
axis.oldStacks = {};
6648
// Min and max in the data
6649
//axis.dataMin = UNDEFINED,
6650
//axis.dataMax = UNDEFINED,
6656
// User set min and max
6657
//axis.userMin = UNDEFINED,
6658
//axis.userMax = UNDEFINED,
6660
// Crosshair options
6661
axis.crosshair = pick(options.crosshair, splat(chart.options.tooltip.crosshairs)[isXAxis ? 0 : 1], false);
6665
events = axis.options.events;
6668
if (inArray(axis, chart.axes) === -1) { // don't add it again on Axis.update()
6669
if (isXAxis && !this.isColorAxis) { // #2713
6670
chart.axes.splice(chart.xAxis.length, 0, axis);
6672
chart.axes.push(axis);
6675
chart[axis.coll].push(axis);
6678
axis.series = axis.series || []; // populated by Series
6680
// inverted charts have reversed xAxes as default
6681
if (chart.inverted && isXAxis && axis.reversed === UNDEFINED) {
6682
axis.reversed = true;
6685
axis.removePlotBand = axis.removePlotBandOrLine;
6686
axis.removePlotLine = axis.removePlotBandOrLine;
6689
// register event listeners
6690
for (eventType in events) {
6691
addEvent(axis, eventType, events[eventType]);
6694
// extend logarithmic axis
6696
axis.val2lin = log2lin;
6697
axis.lin2val = lin2log;
6702
* Merge and set options
6704
setOptions: function (userOptions) {
6705
this.options = merge(
6706
this.defaultOptions,
6707
this.isXAxis ? {} : this.defaultYAxisOptions,
6708
[this.defaultTopAxisOptions, this.defaultRightAxisOptions,
6709
this.defaultBottomAxisOptions, this.defaultLeftAxisOptions][this.side],
6711
defaultOptions[this.coll], // if set in setOptions (#1053)
6718
* The default label formatter. The context is a special config object for the label.
6720
defaultLabelFormatter: function () {
6721
var axis = this.axis,
6723
categories = axis.categories,
6724
dateTimeLabelFormat = this.dateTimeLabelFormat,
6725
numericSymbols = defaultOptions.lang.numericSymbols,
6726
i = numericSymbols && numericSymbols.length,
6729
formatOption = axis.options.labels.format,
6731
// make sure the same symbol is added for all labels on a linear axis
6732
numericSymbolDetector = axis.isLog ? value : axis.tickInterval;
6735
ret = format(formatOption, this);
6737
} else if (categories) {
6740
} else if (dateTimeLabelFormat) { // datetime axis
6741
ret = dateFormat(dateTimeLabelFormat, value);
6743
} else if (i && numericSymbolDetector >= 1000) {
6744
// Decide whether we should add a numeric symbol like k (thousands) or M (millions).
6745
// If we are to enable this in tooltip or other places as well, we can move this
6746
// logic to the numberFormatter and enable it by a parameter.
6747
while (i-- && ret === UNDEFINED) {
6748
multi = Math.pow(1000, i + 1);
6749
if (numericSymbolDetector >= multi && numericSymbols[i] !== null) {
6750
ret = numberFormat(value / multi, -1) + numericSymbols[i];
6755
if (ret === UNDEFINED) {
6756
if (value >= 10000) { // add thousands separators
6757
ret = numberFormat(value, 0);
6759
} else { // small numbers
6760
ret = numberFormat(value, -1, UNDEFINED, ''); // #2466
6768
* Get the minimum and maximum for the series of each axis
6770
getSeriesExtremes: function () {
6774
axis.hasVisibleSeries = false;
6776
// reset dataMin and dataMax in case we're redrawing
6777
axis.dataMin = axis.dataMax = null;
6779
if (axis.buildStacks) {
6783
// loop through this axis' series
6784
each(axis.series, function (series) {
6786
if (series.visible || !chart.options.chart.ignoreHiddenSeries) {
6788
var seriesOptions = series.options,
6790
threshold = seriesOptions.threshold,
6794
axis.hasVisibleSeries = true;
6796
// Validate threshold in logarithmic axes
6797
if (axis.isLog && threshold <= 0) {
6801
// Get dataMin and dataMax for X axes
6803
xData = series.xData;
6805
axis.dataMin = mathMin(pick(axis.dataMin, xData[0]), arrayMin(xData));
6806
axis.dataMax = mathMax(pick(axis.dataMax, xData[0]), arrayMax(xData));
6809
// Get dataMin and dataMax for Y axes, as well as handle stacking and processed data
6812
// Get this particular series extremes
6813
series.getExtremes();
6814
seriesDataMax = series.dataMax;
6815
seriesDataMin = series.dataMin;
6817
// Get the dataMin and dataMax so far. If percentage is used, the min and max are
6818
// always 0 and 100. If seriesDataMin and seriesDataMax is null, then series
6819
// doesn't have active y data, we continue with nulls
6820
if (defined(seriesDataMin) && defined(seriesDataMax)) {
6821
axis.dataMin = mathMin(pick(axis.dataMin, seriesDataMin), seriesDataMin);
6822
axis.dataMax = mathMax(pick(axis.dataMax, seriesDataMax), seriesDataMax);
6825
// Adjust to threshold
6826
if (defined(threshold)) {
6827
if (axis.dataMin >= threshold) {
6828
axis.dataMin = threshold;
6829
axis.ignoreMinPadding = true;
6830
} else if (axis.dataMax < threshold) {
6831
axis.dataMax = threshold;
6832
axis.ignoreMaxPadding = true;
6841
* Translate from axis value to pixel position on the chart, or back
6844
translate: function (val, backwards, cvsCoord, old, handleLog, pointPlacement) {
6848
localA = old ? axis.oldTransA : axis.transA,
6849
localMin = old ? axis.oldMin : axis.min,
6851
minPixelPadding = axis.minPixelPadding,
6852
postTranslate = (axis.options.ordinal || (axis.isLog && handleLog)) && axis.lin2val;
6855
localA = axis.transA;
6858
// In vertical axes, the canvas coordinates start from 0 at the top like in
6861
sign *= -1; // canvas coordinates inverts the value
6862
cvsOffset = axis.len;
6865
// Handle reversed axis
6866
if (axis.reversed) {
6868
cvsOffset -= sign * (axis.sector || axis.len);
6871
// From pixels to value
6872
if (backwards) { // reverse translation
6874
val = val * sign + cvsOffset;
6875
val -= minPixelPadding;
6876
returnValue = val / localA + localMin; // from chart pixel to value
6877
if (postTranslate) { // log and ordinal axes
6878
returnValue = axis.lin2val(returnValue);
6881
// From value to pixels
6883
if (postTranslate) { // log and ordinal axes
6884
val = axis.val2lin(val);
6886
if (pointPlacement === 'between') {
6887
pointPlacement = 0.5;
6889
returnValue = sign * (val - localMin) * localA + cvsOffset + (sign * minPixelPadding) +
6890
(isNumber(pointPlacement) ? localA * pointPlacement * axis.pointRange : 0);
6897
* Utility method to translate an axis value to pixel position.
6898
* @param {Number} value A value in terms of axis units
6899
* @param {Boolean} paneCoordinates Whether to return the pixel coordinate relative to the chart
6900
* or just the axis/pane itself.
6902
toPixels: function (value, paneCoordinates) {
6903
return this.translate(value, false, !this.horiz, null, true) + (paneCoordinates ? 0 : this.pos);
6907
* Utility method to translate a pixel position in to an axis value
6908
* @param {Number} pixel The pixel value coordinate
6909
* @param {Boolean} paneCoordiantes Whether the input pixel is relative to the chart or just the
6912
toValue: function (pixel, paneCoordinates) {
6913
return this.translate(pixel - (paneCoordinates ? 0 : this.pos), true, !this.horiz, null, true);
6917
* Create the path for a plot line that goes from the given value on
6918
* this axis, across the plot to the opposite side
6919
* @param {Number} value
6920
* @param {Number} lineWidth Used for calculation crisp line
6921
* @param {Number] old Use old coordinates (for resizing and rescaling)
6923
getPlotLinePath: function (value, lineWidth, old, force, translatedValue) {
6926
axisLeft = axis.left,
6932
cHeight = (old && chart.oldChartHeight) || chart.chartHeight,
6933
cWidth = (old && chart.oldChartWidth) || chart.chartWidth,
6935
transB = axis.transB;
6937
translatedValue = pick(translatedValue, axis.translate(value, null, null, old));
6938
x1 = x2 = mathRound(translatedValue + transB);
6939
y1 = y2 = mathRound(cHeight - translatedValue - transB);
6941
if (isNaN(translatedValue)) { // no min or max
6944
} else if (axis.horiz) {
6946
y2 = cHeight - axis.bottom;
6947
if (x1 < axisLeft || x1 > axisLeft + axis.width) {
6952
x2 = cWidth - axis.right;
6954
if (y1 < axisTop || y1 > axisTop + axis.height) {
6958
return skip && !force ?
6960
chart.renderer.crispLine([M, x1, y1, L, x2, y2], lineWidth || 1);
6964
* Set the tick positions of a linear axis to round values like whole tens or every five.
6966
getLinearTickPositions: function (tickInterval, min, max) {
6969
roundedMin = correctFloat(mathFloor(min / tickInterval) * tickInterval),
6970
roundedMax = correctFloat(mathCeil(max / tickInterval) * tickInterval),
6973
// Populate the intermediate values
6975
while (pos <= roundedMax) {
6977
// Place the tick on the rounded value
6978
tickPositions.push(pos);
6980
// Always add the raw tickInterval, not the corrected one.
6981
pos = correctFloat(pos + tickInterval);
6983
// If the interval is not big enough in the current min - max range to actually increase
6984
// the loop variable, we need to break out to prevent endless loop. Issue #619
6985
if (pos === lastPos) {
6989
// Record the last value
6992
return tickPositions;
6996
* Return the minor tick positions. For logarithmic axes, reuse the same logic
6997
* as for major ticks.
6999
getMinorTickPositions: function () {
7001
options = axis.options,
7002
tickPositions = axis.tickPositions,
7003
minorTickInterval = axis.minorTickInterval,
7004
minorTickPositions = [],
7010
len = tickPositions.length;
7011
for (i = 1; i < len; i++) {
7012
minorTickPositions = minorTickPositions.concat(
7013
axis.getLogTickPositions(minorTickInterval, tickPositions[i - 1], tickPositions[i], true)
7016
} else if (axis.isDatetimeAxis && options.minorTickInterval === 'auto') { // #1314
7017
minorTickPositions = minorTickPositions.concat(
7019
axis.normalizeTimeTickInterval(minorTickInterval),
7025
if (minorTickPositions[0] < axis.min) {
7026
minorTickPositions.shift();
7029
for (pos = axis.min + (tickPositions[0] - axis.min) % minorTickInterval; pos <= axis.max; pos += minorTickInterval) {
7030
minorTickPositions.push(pos);
7033
return minorTickPositions;
7037
* Adjust the min and max for the minimum range. Keep in mind that the series data is
7038
* not yet processed, so we don't have information on data cropping and grouping, or
7039
* updated axis.pointRange or series.pointRange. The data can't be processed until
7040
* we have finally established min and max.
7042
adjustForMinRange: function () {
7044
options = axis.options,
7048
spaceAvailable = axis.dataMax - axis.dataMin >= axis.minRange,
7057
// Set the automatic minimum range based on the closest point distance
7058
if (axis.isXAxis && axis.minRange === UNDEFINED && !axis.isLog) {
7060
if (defined(options.min) || defined(options.max)) {
7061
axis.minRange = null; // don't do this again
7065
// Find the closest distance between raw data points, as opposed to
7066
// closestPointRange that applies to processed points (cropped and grouped)
7067
each(axis.series, function (series) {
7068
xData = series.xData;
7069
loopLength = series.xIncrement ? 1 : xData.length - 1;
7070
for (i = loopLength; i > 0; i--) {
7071
distance = xData[i] - xData[i - 1];
7072
if (closestDataRange === UNDEFINED || distance < closestDataRange) {
7073
closestDataRange = distance;
7077
axis.minRange = mathMin(closestDataRange * 5, axis.dataMax - axis.dataMin);
7081
// if minRange is exceeded, adjust
7082
if (max - min < axis.minRange) {
7083
var minRange = axis.minRange;
7084
zoomOffset = (minRange - max + min) / 2;
7086
// if min and max options have been set, don't go beyond it
7087
minArgs = [min - zoomOffset, pick(options.min, min - zoomOffset)];
7088
if (spaceAvailable) { // if space is available, stay within the data range
7089
minArgs[2] = axis.dataMin;
7091
min = arrayMax(minArgs);
7093
maxArgs = [min + minRange, pick(options.max, min + minRange)];
7094
if (spaceAvailable) { // if space is availabe, stay within the data range
7095
maxArgs[2] = axis.dataMax;
7098
max = arrayMin(maxArgs);
7100
// now if the max is adjusted, adjust the min back
7101
if (max - min < minRange) {
7102
minArgs[0] = max - minRange;
7103
minArgs[1] = pick(options.min, max - minRange);
7104
min = arrayMax(minArgs);
7108
// Record modified extremes
7114
* Update translation information
7116
setAxisTranslation: function (saveOld) {
7118
range = axis.max - axis.min,
7119
pointRange = axis.axisPointRange || 0,
7122
pointRangePadding = 0,
7123
linkedParent = axis.linkedParent,
7125
hasCategories = !!axis.categories,
7126
transA = axis.transA;
7128
// Adjust translation for padding. Y axis with categories need to go through the same (#1784).
7129
if (axis.isXAxis || hasCategories || pointRange) {
7131
minPointOffset = linkedParent.minPointOffset;
7132
pointRangePadding = linkedParent.pointRangePadding;
7135
each(axis.series, function (series) {
7136
var seriesPointRange = mathMax(axis.isXAxis ? series.pointRange : (axis.axisPointRange || 0), +hasCategories),
7137
pointPlacement = series.options.pointPlacement,
7138
seriesClosestPointRange = series.closestPointRange;
7140
if (seriesPointRange > range) { // #1446
7141
seriesPointRange = 0;
7143
pointRange = mathMax(pointRange, seriesPointRange);
7145
// minPointOffset is the value padding to the left of the axis in order to make
7146
// room for points with a pointRange, typically columns. When the pointPlacement option
7147
// is 'between' or 'on', this padding does not apply.
7148
minPointOffset = mathMax(
7150
isString(pointPlacement) ? 0 : seriesPointRange / 2
7153
// Determine the total padding needed to the length of the axis to make room for the
7154
// pointRange. If the series' pointPlacement is 'on', no padding is added.
7155
pointRangePadding = mathMax(
7157
pointPlacement === 'on' ? 0 : seriesPointRange
7160
// Set the closestPointRange
7161
if (!series.noSharedTooltip && defined(seriesClosestPointRange)) {
7162
closestPointRange = defined(closestPointRange) ?
7163
mathMin(closestPointRange, seriesClosestPointRange) :
7164
seriesClosestPointRange;
7169
// Record minPointOffset and pointRangePadding
7170
ordinalCorrection = axis.ordinalSlope && closestPointRange ? axis.ordinalSlope / closestPointRange : 1; // #988, #1853
7171
axis.minPointOffset = minPointOffset = minPointOffset * ordinalCorrection;
7172
axis.pointRangePadding = pointRangePadding = pointRangePadding * ordinalCorrection;
7174
// pointRange means the width reserved for each point, like in a column chart
7175
axis.pointRange = mathMin(pointRange, range);
7177
// closestPointRange means the closest distance between points. In columns
7178
// it is mostly equal to pointRange, but in lines pointRange is 0 while closestPointRange
7179
// is some other value
7180
axis.closestPointRange = closestPointRange;
7185
axis.oldTransA = transA;
7187
axis.translationSlope = axis.transA = transA = axis.len / ((range + pointRangePadding) || 1);
7188
axis.transB = axis.horiz ? axis.left : axis.bottom; // translation addend
7189
axis.minPixelPadding = transA * minPointOffset;
7193
* Set the tick positions to round values and optionally extend the extremes
7194
* to the nearest tick
7196
setTickPositions: function (secondPass) {
7199
options = axis.options,
7201
isDatetimeAxis = axis.isDatetimeAxis,
7202
isXAxis = axis.isXAxis,
7203
isLinked = axis.isLinked,
7204
tickPositioner = axis.options.tickPositioner,
7205
maxPadding = options.maxPadding,
7206
minPadding = options.minPadding,
7208
linkedParentExtremes,
7209
tickIntervalOption = options.tickInterval,
7210
minTickIntervalOption = options.minTickInterval,
7211
tickPixelIntervalOption = options.tickPixelInterval,
7214
categories = axis.categories;
7216
// linked axis gets the extremes from the parent axis
7218
axis.linkedParent = chart[axis.coll][options.linkedTo];
7219
linkedParentExtremes = axis.linkedParent.getExtremes();
7220
axis.min = pick(linkedParentExtremes.min, linkedParentExtremes.dataMin);
7221
axis.max = pick(linkedParentExtremes.max, linkedParentExtremes.dataMax);
7222
if (options.type !== axis.linkedParent.options.type) {
7223
error(11, 1); // Can't link axes of different type
7225
} else { // initial min and max from the extreme data values
7226
axis.min = pick(axis.userMin, options.min, axis.dataMin);
7227
axis.max = pick(axis.userMax, options.max, axis.dataMax);
7231
if (!secondPass && mathMin(axis.min, pick(axis.dataMin, axis.min)) <= 0) { // #978
7232
error(10, 1); // Can't plot negative values on log axis
7234
axis.min = correctFloat(log2lin(axis.min)); // correctFloat cures #934
7235
axis.max = correctFloat(log2lin(axis.max));
7238
// handle zoomed range
7239
if (axis.range && defined(axis.max)) {
7240
axis.userMin = axis.min = mathMax(axis.min, axis.max - axis.range); // #618
7241
axis.userMax = axis.max;
7243
axis.range = null; // don't use it when running setExtremes
7246
// Hook for adjusting this.min and this.max. Used by bubble series.
7247
if (axis.beforePadding) {
7248
axis.beforePadding();
7251
// adjust min and max for the minimum range
7252
axis.adjustForMinRange();
7254
// Pad the values to get clear of the chart's edges. To avoid tickInterval taking the padding
7255
// into account, we do this after computing tick interval (#1337).
7256
if (!categories && !axis.axisPointRange && !axis.usePercentage && !isLinked && defined(axis.min) && defined(axis.max)) {
7257
length = axis.max - axis.min;
7259
if (!defined(options.min) && !defined(axis.userMin) && minPadding && (axis.dataMin < 0 || !axis.ignoreMinPadding)) {
7260
axis.min -= length * minPadding;
7262
if (!defined(options.max) && !defined(axis.userMax) && maxPadding && (axis.dataMax > 0 || !axis.ignoreMaxPadding)) {
7263
axis.max += length * maxPadding;
7269
if (axis.min === axis.max || axis.min === undefined || axis.max === undefined) {
7270
axis.tickInterval = 1;
7271
} else if (isLinked && !tickIntervalOption &&
7272
tickPixelIntervalOption === axis.linkedParent.options.tickPixelInterval) {
7273
axis.tickInterval = axis.linkedParent.tickInterval;
7275
axis.tickInterval = pick(
7277
categories ? // for categoried axis, 1 is default, for linear axis use tickPix
7279
// don't let it be more than the data range
7280
(axis.max - axis.min) * tickPixelIntervalOption / mathMax(axis.len, tickPixelIntervalOption)
7282
// For squished axes, set only two ticks
7283
if (!defined(tickIntervalOption) && axis.len < tickPixelIntervalOption && !this.isRadial &&
7284
!this.isLog && !categories && options.startOnTick && options.endOnTick) {
7285
keepTwoTicksOnly = true;
7286
axis.tickInterval /= 4; // tick extremes closer to the real values
7290
// Now we're finished detecting min and max, crop and group series data. This
7291
// is in turn needed in order to find tick positions in ordinal axes.
7292
if (isXAxis && !secondPass) {
7293
each(axis.series, function (series) {
7294
series.processData(axis.min !== axis.oldMin || axis.max !== axis.oldMax);
7298
// set the translation factor used in translate function
7299
axis.setAxisTranslation(true);
7301
// hook for ordinal axes and radial axes
7302
if (axis.beforeSetTickPositions) {
7303
axis.beforeSetTickPositions();
7306
// hook for extensions, used in Highstock ordinal axes
7307
if (axis.postProcessTickInterval) {
7308
axis.tickInterval = axis.postProcessTickInterval(axis.tickInterval);
7311
// In column-like charts, don't cramp in more ticks than there are points (#1943)
7312
if (axis.pointRange) {
7313
axis.tickInterval = mathMax(axis.pointRange, axis.tickInterval);
7316
// Before normalizing the tick interval, handle minimum tick interval. This applies only if tickInterval is not defined.
7317
if (!tickIntervalOption && axis.tickInterval < minTickIntervalOption) {
7318
axis.tickInterval = minTickIntervalOption;
7321
// for linear axes, get magnitude and normalize the interval
7322
if (!isDatetimeAxis && !isLog) { // linear
7323
if (!tickIntervalOption) {
7324
axis.tickInterval = normalizeTickInterval(axis.tickInterval, null, getMagnitude(axis.tickInterval), options);
7328
// get minorTickInterval
7329
axis.minorTickInterval = options.minorTickInterval === 'auto' && axis.tickInterval ?
7330
axis.tickInterval / 5 : options.minorTickInterval;
7332
// find the tick positions
7333
axis.tickPositions = tickPositions = options.tickPositions ?
7334
[].concat(options.tickPositions) : // Work on a copy (#1565)
7335
(tickPositioner && tickPositioner.apply(axis, [axis.min, axis.max]));
7336
if (!tickPositions) {
7339
if (!axis.ordinalPositions && (axis.max - axis.min) / axis.tickInterval > mathMax(2 * axis.len, 200)) {
7343
if (isDatetimeAxis) {
7344
tickPositions = axis.getTimeTicks(
7345
axis.normalizeTimeTickInterval(axis.tickInterval, options.units),
7348
options.startOfWeek,
7349
axis.ordinalPositions,
7350
axis.closestPointRange,
7354
tickPositions = axis.getLogTickPositions(axis.tickInterval, axis.min, axis.max);
7356
tickPositions = axis.getLinearTickPositions(axis.tickInterval, axis.min, axis.max);
7359
if (keepTwoTicksOnly) {
7360
tickPositions.splice(1, tickPositions.length - 2);
7363
axis.tickPositions = tickPositions;
7368
// reset min/max or remove extremes based on start/end on tick
7369
var roundedMin = tickPositions[0],
7370
roundedMax = tickPositions[tickPositions.length - 1],
7371
minPointOffset = axis.minPointOffset || 0,
7374
if (options.startOnTick) {
7375
axis.min = roundedMin;
7376
} else if (axis.min - minPointOffset > roundedMin) {
7377
tickPositions.shift();
7380
if (options.endOnTick) {
7381
axis.max = roundedMax;
7382
} else if (axis.max + minPointOffset < roundedMax) {
7383
tickPositions.pop();
7386
// When there is only one point, or all points have the same value on this axis, then min
7387
// and max are equal and tickPositions.length is 1. In this case, add some padding
7388
// in order to center the point, but leave it with one tick. #1337.
7389
if (tickPositions.length === 1) {
7390
singlePad = mathAbs(axis.max || 1) * 0.001; // The lowest possible number to avoid extra padding on columns (#2619)
7391
axis.min -= singlePad;
7392
axis.max += singlePad;
7398
* Set the max ticks of either the x and y axis collection
7400
setMaxTicks: function () {
7402
var chart = this.chart,
7403
maxTicks = chart.maxTicks || {},
7404
tickPositions = this.tickPositions,
7405
key = this._maxTicksKey = [this.coll, this.pos, this.len].join('-');
7407
if (!this.isLinked && !this.isDatetimeAxis && tickPositions && tickPositions.length > (maxTicks[key] || 0) && this.options.alignTicks !== false) {
7408
maxTicks[key] = tickPositions.length;
7410
chart.maxTicks = maxTicks;
7414
* When using multiple axes, adjust the number of ticks to match the highest
7415
* number of ticks in that group
7417
adjustTickAmount: function () {
7420
key = axis._maxTicksKey,
7421
tickPositions = axis.tickPositions,
7422
maxTicks = chart.maxTicks;
7424
if (maxTicks && maxTicks[key] && !axis.isDatetimeAxis && !axis.categories && !axis.isLinked &&
7425
axis.options.alignTicks !== false && this.min !== UNDEFINED) {
7426
var oldTickAmount = axis.tickAmount,
7427
calculatedTickAmount = tickPositions.length,
7430
// set the axis-level tickAmount to use below
7431
axis.tickAmount = tickAmount = maxTicks[key];
7433
if (calculatedTickAmount < tickAmount) {
7434
while (tickPositions.length < tickAmount) {
7435
tickPositions.push(correctFloat(
7436
tickPositions[tickPositions.length - 1] + axis.tickInterval
7439
axis.transA *= (calculatedTickAmount - 1) / (tickAmount - 1);
7440
axis.max = tickPositions[tickPositions.length - 1];
7443
if (defined(oldTickAmount) && tickAmount !== oldTickAmount) {
7444
axis.isDirty = true;
7450
* Set the scale based on data min and max, user set min and max or options
7453
setScale: function () {
7455
stacks = axis.stacks,
7461
axis.oldMin = axis.min;
7462
axis.oldMax = axis.max;
7463
axis.oldAxisLength = axis.len;
7465
// set the new axisLength
7467
//axisLength = horiz ? axisWidth : axisHeight;
7468
isDirtyAxisLength = axis.len !== axis.oldAxisLength;
7470
// is there new data?
7471
each(axis.series, function (series) {
7472
if (series.isDirtyData || series.isDirty ||
7473
series.xAxis.isDirty) { // when x axis is dirty, we need new data extremes for y as well
7478
// do we really need to go through all this?
7479
if (isDirtyAxisLength || isDirtyData || axis.isLinked || axis.forceRedraw ||
7480
axis.userMin !== axis.oldUserMin || axis.userMax !== axis.oldUserMax) {
7483
if (!axis.isXAxis) {
7484
for (type in stacks) {
7485
for (i in stacks[type]) {
7486
stacks[type][i].total = null;
7487
stacks[type][i].cum = 0;
7492
axis.forceRedraw = false;
7494
// get data extremes if needed
7495
axis.getSeriesExtremes();
7497
// get fixed positions based on tickInterval
7498
axis.setTickPositions();
7500
// record old values to decide whether a rescale is necessary later on (#540)
7501
axis.oldUserMin = axis.userMin;
7502
axis.oldUserMax = axis.userMax;
7504
// Mark as dirty if it is not already set to dirty and extremes have changed. #595.
7505
if (!axis.isDirty) {
7506
axis.isDirty = isDirtyAxisLength || axis.min !== axis.oldMin || axis.max !== axis.oldMax;
7508
} else if (!axis.isXAxis) {
7509
if (axis.oldStacks) {
7510
stacks = axis.stacks = axis.oldStacks;
7514
for (type in stacks) {
7515
for (i in stacks[type]) {
7516
stacks[type][i].cum = stacks[type][i].total;
7521
// Set the maximum tick amount
7526
* Set the extremes and optionally redraw
7527
* @param {Number} newMin
7528
* @param {Number} newMax
7529
* @param {Boolean} redraw
7530
* @param {Boolean|Object} animation Whether to apply animation, and optionally animation
7532
* @param {Object} eventArguments
7535
setExtremes: function (newMin, newMax, redraw, animation, eventArguments) {
7539
redraw = pick(redraw, true); // defaults to true
7541
// Extend the arguments with min and max
7542
eventArguments = extend(eventArguments, {
7548
fireEvent(axis, 'setExtremes', eventArguments, function () { // the default event handler
7550
axis.userMin = newMin;
7551
axis.userMax = newMax;
7552
axis.eventArgs = eventArguments;
7554
// Mark for running afterSetExtremes
7555
axis.isDirtyExtremes = true;
7559
chart.redraw(animation);
7565
* Overridable method for zooming chart. Pulled out in a separate method to allow overriding
7568
zoom: function (newMin, newMax) {
7569
var dataMin = this.dataMin,
7570
dataMax = this.dataMax,
7571
options = this.options;
7573
// Prevent pinch zooming out of range. Check for defined is for #1946. #1734.
7574
if (!this.allowZoomOutside) {
7575
if (defined(dataMin) && newMin <= mathMin(dataMin, pick(options.min, dataMin))) {
7578
if (defined(dataMax) && newMax >= mathMax(dataMax, pick(options.max, dataMax))) {
7583
// In full view, displaying the reset zoom button is not required
7584
this.displayBtn = newMin !== UNDEFINED || newMax !== UNDEFINED;
7598
* Update the axis metrics
7600
setAxisSize: function () {
7601
var chart = this.chart,
7602
options = this.options,
7603
offsetLeft = options.offsetLeft || 0,
7604
offsetRight = options.offsetRight || 0,
7611
// Expose basic values to use in Series object and navigator
7612
this.left = left = pick(options.left, chart.plotLeft + offsetLeft);
7613
this.top = top = pick(options.top, chart.plotTop);
7614
this.width = width = pick(options.width, chart.plotWidth - offsetLeft + offsetRight);
7615
this.height = height = pick(options.height, chart.plotHeight);
7616
this.bottom = chart.chartHeight - height - top;
7617
this.right = chart.chartWidth - width - left;
7619
// Direction agnostic properties
7620
this.len = mathMax(horiz ? width : height, 0); // mathMax fixes #905
7621
this.pos = horiz ? left : top; // distance from SVG origin
7625
* Get the actual axis extremes
7627
getExtremes: function () {
7632
min: isLog ? correctFloat(lin2log(axis.min)) : axis.min,
7633
max: isLog ? correctFloat(lin2log(axis.max)) : axis.max,
7634
dataMin: axis.dataMin,
7635
dataMax: axis.dataMax,
7636
userMin: axis.userMin,
7637
userMax: axis.userMax
7642
* Get the zero plane either based on zero or on the min or max value.
7643
* Used in bar and area plots
7645
getThreshold: function (threshold) {
7649
var realMin = isLog ? lin2log(axis.min) : axis.min,
7650
realMax = isLog ? lin2log(axis.max) : axis.max;
7652
if (realMin > threshold || threshold === null) {
7653
threshold = realMin;
7654
} else if (realMax < threshold) {
7655
threshold = realMax;
7658
return axis.translate(threshold, 0, 1, 0, 1);
7662
* Compute auto alignment for the axis label based on which side the axis is on
7663
* and the given rotation for the label
7665
autoLabelAlign: function (rotation) {
7667
angle = (pick(rotation, 0) - (this.side * 90) + 720) % 360;
7669
if (angle > 15 && angle < 165) {
7671
} else if (angle > 195 && angle < 345) {
7680
* Render the tick labels to a preliminary position to get their sizes
7682
getOffset: function () {
7685
renderer = chart.renderer,
7686
options = axis.options,
7687
tickPositions = axis.tickPositions,
7691
invertedSide = chart.inverted ? [1, 0, 3, 2][side] : side,
7697
axisTitleOptions = options.title,
7698
labelOptions = options.labels,
7699
labelOffset = 0, // reset
7700
axisOffset = chart.axisOffset,
7701
clipOffset = chart.clipOffset,
7702
directionFactor = [-1, 1, 1, -1][side],
7705
autoStaggerLines = 1,
7706
maxStaggerLines = pick(labelOptions.maxStaggerLines, 5),
7716
// For reuse in Axis.render
7717
axis.hasData = hasData = (axis.hasVisibleSeries || (defined(axis.min) && defined(axis.max) && !!tickPositions));
7718
axis.showAxis = showAxis = hasData || pick(options.showEmpty, true);
7720
// Set/reset staggerLines
7721
axis.staggerLines = axis.horiz && labelOptions.staggerLines;
7723
// Create the axisGroup and gridGroup elements on first iteration
7724
if (!axis.axisGroup) {
7725
axis.gridGroup = renderer.g('grid')
7726
.attr({ zIndex: options.gridZIndex || 1 })
7728
axis.axisGroup = renderer.g('axis')
7729
.attr({ zIndex: options.zIndex || 2 })
7731
axis.labelGroup = renderer.g('axis-labels')
7732
.attr({ zIndex: labelOptions.zIndex || 7 })
7733
.addClass(PREFIX + axis.coll.toLowerCase() + '-labels')
7737
if (hasData || axis.isLinked) {
7739
// Set the explicit or automatic label alignment
7740
axis.labelAlign = pick(labelOptions.align || axis.autoLabelAlign(labelOptions.rotation));
7743
each(tickPositions, function (pos) {
7745
ticks[pos] = new Tick(axis, pos);
7747
ticks[pos].addLabel(); // update labels depending on tick interval
7751
// Handle automatic stagger lines
7752
if (axis.horiz && !axis.staggerLines && maxStaggerLines && !labelOptions.rotation) {
7753
sortedPositions = axis.reversed ? [].concat(tickPositions).reverse() : tickPositions;
7754
while (autoStaggerLines < maxStaggerLines) {
7758
for (i = 0; i < sortedPositions.length; i++) {
7759
pos = sortedPositions[i];
7760
bBox = ticks[pos].label && ticks[pos].label.getBBox();
7761
w = bBox ? bBox.width : 0;
7762
lineNo = i % autoStaggerLines;
7765
x = axis.translate(pos); // don't handle log
7766
if (lastRight[lineNo] !== UNDEFINED && x < lastRight[lineNo]) {
7769
lastRight[lineNo] = x + w;
7779
if (autoStaggerLines > 1) {
7780
axis.staggerLines = autoStaggerLines;
7785
each(tickPositions, function (pos) {
7786
// left side must be align: right and right side must have align: left for labels
7787
if (side === 0 || side === 2 || { 1: 'left', 3: 'right' }[side] === axis.labelAlign) {
7789
// get the highest offset
7790
labelOffset = mathMax(
7791
ticks[pos].getLabelSize(),
7797
if (axis.staggerLines) {
7798
labelOffset *= axis.staggerLines;
7799
axis.labelOffset = labelOffset;
7803
} else { // doesn't have data
7810
if (axisTitleOptions && axisTitleOptions.text && axisTitleOptions.enabled !== false) {
7811
if (!axis.axisTitle) {
7812
axis.axisTitle = renderer.text(
7813
axisTitleOptions.text,
7816
axisTitleOptions.useHTML
7820
rotation: axisTitleOptions.rotation || 0,
7822
axisTitleOptions.textAlign ||
7823
{ low: 'left', middle: 'center', high: 'right' }[axisTitleOptions.align]
7825
.addClass(PREFIX + this.coll.toLowerCase() + '-title')
7826
.css(axisTitleOptions.style)
7827
.add(axis.axisGroup);
7828
axis.axisTitle.isNew = true;
7832
titleOffset = axis.axisTitle.getBBox()[horiz ? 'height' : 'width'];
7833
titleMargin = pick(axisTitleOptions.margin, horiz ? 5 : 10);
7834
titleOffsetOption = axisTitleOptions.offset;
7837
// hide or show the title depending on whether showEmpty is set
7838
axis.axisTitle[showAxis ? 'show' : 'hide']();
7841
// handle automatic or user set offset
7842
axis.offset = directionFactor * pick(options.offset, axisOffset[side]);
7844
axis.axisTitleMargin =
7845
pick(titleOffsetOption,
7846
labelOffset + titleMargin +
7847
(side !== 2 && labelOffset && directionFactor * options.labels[horiz ? 'y' : 'x'])
7850
axisOffset[side] = mathMax(
7852
axis.axisTitleMargin + titleOffset + directionFactor * axis.offset
7854
clipOffset[invertedSide] = mathMax(clipOffset[invertedSide], mathFloor(options.lineWidth / 2) * 2);
7858
* Get the path for the axis line
7860
getLinePath: function (lineWidth) {
7861
var chart = this.chart,
7862
opposite = this.opposite,
7863
offset = this.offset,
7865
lineLeft = this.left + (opposite ? this.width : 0) + offset,
7866
lineTop = chart.chartHeight - this.bottom - (opposite ? this.height : 0) + offset;
7869
lineWidth *= -1; // crispify the other way - #1480, #1687
7872
return chart.renderer.crispLine([
7882
chart.chartWidth - this.right :
7886
chart.chartHeight - this.bottom
7891
* Position the title
7893
getTitlePosition: function () {
7894
// compute anchor points for each of the title align options
7895
var horiz = this.horiz,
7896
axisLeft = this.left,
7898
axisLength = this.len,
7899
axisTitleOptions = this.options.title,
7900
margin = horiz ? axisLeft : axisTop,
7901
opposite = this.opposite,
7902
offset = this.offset,
7903
fontSize = pInt(axisTitleOptions.style.fontSize || 12),
7905
// the position in the length direction of the axis
7907
low: margin + (horiz ? 0 : axisLength),
7908
middle: margin + axisLength / 2,
7909
high: margin + (horiz ? axisLength : 0)
7910
}[axisTitleOptions.align],
7912
// the position in the perpendicular direction of the axis
7913
offAxis = (horiz ? axisTop + this.height : axisLeft) +
7914
(horiz ? 1 : -1) * // horizontal axis reverses the margin
7915
(opposite ? -1 : 1) * // so does opposite axes
7916
this.axisTitleMargin +
7917
(this.side === 2 ? fontSize : 0);
7922
offAxis + (opposite ? this.width : 0) + offset +
7923
(axisTitleOptions.x || 0), // x
7925
offAxis - (opposite ? this.height : 0) + offset :
7926
alongAxis + (axisTitleOptions.y || 0) // y
7933
render: function () {
7936
reversed = axis.reversed,
7938
renderer = chart.renderer,
7939
options = axis.options,
7941
isLinked = axis.isLinked,
7942
tickPositions = axis.tickPositions,
7944
axisTitle = axis.axisTitle,
7946
minorTicks = axis.minorTicks,
7947
alternateBands = axis.alternateBands,
7948
stackLabelOptions = options.stackLabels,
7949
alternateGridColor = options.alternateGridColor,
7950
tickmarkOffset = axis.tickmarkOffset,
7951
lineWidth = options.lineWidth,
7953
hasRendered = chart.hasRendered,
7954
slideInTicks = hasRendered && defined(axis.oldMin) && !isNaN(axis.oldMin),
7955
hasData = axis.hasData,
7956
showAxis = axis.showAxis,
7958
overflow = options.labels.overflow,
7959
justifyLabels = axis.justifyLabels = horiz && overflow !== false,
7963
axis.labelEdge.length = 0;
7964
axis.justifyToPlot = overflow === 'justify';
7966
// Mark all elements inActive before we go over and mark the active ones
7967
each([ticks, minorTicks, alternateBands], function (coll) {
7970
coll[pos].isActive = false;
7974
// If the series has data draw the ticks. Else only the line and title
7975
if (hasData || isLinked) {
7978
if (axis.minorTickInterval && !axis.categories) {
7979
each(axis.getMinorTickPositions(), function (pos) {
7980
if (!minorTicks[pos]) {
7981
minorTicks[pos] = new Tick(axis, pos, 'minor');
7984
// render new ticks in old position
7985
if (slideInTicks && minorTicks[pos].isNew) {
7986
minorTicks[pos].render(null, true);
7989
minorTicks[pos].render(null, false, 1);
7993
// Major ticks. Pull out the first item and render it last so that
7994
// we can get the position of the neighbour label. #808.
7995
if (tickPositions.length) { // #1300
7996
sortedPositions = tickPositions.slice();
7997
if ((horiz && reversed) || (!horiz && !reversed)) {
7998
sortedPositions.reverse();
8000
if (justifyLabels) {
8001
sortedPositions = sortedPositions.slice(1).concat([sortedPositions[0]]);
8003
each(sortedPositions, function (pos, i) {
8005
// Reorganize the indices
8006
if (justifyLabels) {
8007
i = (i === sortedPositions.length - 1) ? 0 : i + 1;
8010
// linked axes need an extra check to find out if
8011
if (!isLinked || (pos >= axis.min && pos <= axis.max)) {
8014
ticks[pos] = new Tick(axis, pos);
8017
// render new ticks in old position
8018
if (slideInTicks && ticks[pos].isNew) {
8019
ticks[pos].render(i, true, 0.1);
8022
ticks[pos].render(i, false, 1);
8026
// In a categorized axis, the tick marks are displayed between labels. So
8027
// we need to add a tick mark and grid line at the left edge of the X axis.
8028
if (tickmarkOffset && axis.min === 0) {
8030
ticks[-1] = new Tick(axis, -1, null, true);
8032
ticks[-1].render(-1);
8037
// alternate grid color
8038
if (alternateGridColor) {
8039
each(tickPositions, function (pos, i) {
8040
if (i % 2 === 0 && pos < axis.max) {
8041
if (!alternateBands[pos]) {
8042
alternateBands[pos] = new Highcharts.PlotLineOrBand(axis);
8044
from = pos + tickmarkOffset; // #949
8045
to = tickPositions[i + 1] !== UNDEFINED ? tickPositions[i + 1] + tickmarkOffset : axis.max;
8046
alternateBands[pos].options = {
8047
from: isLog ? lin2log(from) : from,
8048
to: isLog ? lin2log(to) : to,
8049
color: alternateGridColor
8051
alternateBands[pos].render();
8052
alternateBands[pos].isActive = true;
8057
// custom plot lines and bands
8058
if (!axis._addedPlotLB) { // only first time
8059
each((options.plotLines || []).concat(options.plotBands || []), function (plotLineOptions) {
8060
axis.addPlotBandOrLine(plotLineOptions);
8062
axis._addedPlotLB = true;
8067
// Remove inactive ticks
8068
each([ticks, minorTicks, alternateBands], function (coll) {
8071
forDestruction = [],
8072
delay = globalAnimation ? globalAnimation.duration || 500 : 0,
8073
destroyInactiveItems = function () {
8074
i = forDestruction.length;
8076
// When resizing rapidly, the same items may be destroyed in different timeouts,
8077
// or the may be reactivated
8078
if (coll[forDestruction[i]] && !coll[forDestruction[i]].isActive) {
8079
coll[forDestruction[i]].destroy();
8080
delete coll[forDestruction[i]];
8088
if (!coll[pos].isActive) {
8089
// Render to zero opacity
8090
coll[pos].render(pos, false, 0);
8091
coll[pos].isActive = false;
8092
forDestruction.push(pos);
8096
// When the objects are finished fading out, destroy them
8097
if (coll === alternateBands || !chart.hasRendered || !delay) {
8098
destroyInactiveItems();
8100
setTimeout(destroyInactiveItems, delay);
8104
// Static items. As the axis group is cleared on subsequent calls
8105
// to render, these items are added outside the group.
8108
linePath = axis.getLinePath(lineWidth);
8109
if (!axis.axisLine) {
8110
axis.axisLine = renderer.path(linePath)
8112
stroke: options.lineColor,
8113
'stroke-width': lineWidth,
8116
.add(axis.axisGroup);
8118
axis.axisLine.animate({ d: linePath });
8121
// show or hide the line depending on options.showEmpty
8122
axis.axisLine[showAxis ? 'show' : 'hide']();
8125
if (axisTitle && showAxis) {
8127
axisTitle[axisTitle.isNew ? 'attr' : 'animate'](
8128
axis.getTitlePosition()
8130
axisTitle.isNew = false;
8134
if (stackLabelOptions && stackLabelOptions.enabled) {
8135
axis.renderStackTotals();
8137
// End stacked totals
8139
axis.isDirty = false;
8143
* Redraw the axis to reflect changes in the data or axis extremes
8145
redraw: function () {
8148
pointer = chart.pointer;
8150
// hide tooltip and hover states
8152
pointer.reset(true);
8158
// move plot lines and bands
8159
each(axis.plotLinesAndBands, function (plotLine) {
8163
// mark associated series as dirty and ready for redraw
8164
each(axis.series, function (series) {
8165
series.isDirty = true;
8171
* Destroys an Axis instance.
8173
destroy: function (keepEvents) {
8175
stacks = axis.stacks,
8177
plotLinesAndBands = axis.plotLinesAndBands,
8180
// Remove the events
8185
// Destroy each stack total
8186
for (stackKey in stacks) {
8187
destroyObjectProperties(stacks[stackKey]);
8189
stacks[stackKey] = null;
8192
// Destroy collections
8193
each([axis.ticks, axis.minorTicks, axis.alternateBands], function (coll) {
8194
destroyObjectProperties(coll);
8196
i = plotLinesAndBands.length;
8197
while (i--) { // #1975
8198
plotLinesAndBands[i].destroy();
8201
// Destroy local variables
8202
each(['stackTotalGroup', 'axisLine', 'axisTitle', 'axisGroup', 'cross', 'gridGroup', 'labelGroup'], function (prop) {
8204
axis[prop] = axis[prop].destroy();
8208
// Destroy crosshair
8210
this.cross.destroy();
8215
* Draw the crosshair
8217
drawCrosshair: function (e, point) {
8218
if (!this.crosshair) { return; }// Do not draw crosshairs if you don't have too.
8220
if ((defined(point) || !pick(this.crosshair.snap, true)) === false) {
8221
this.hideCrosshair();
8226
options = this.crosshair,
8227
animation = options.animation,
8231
if (!pick(options.snap, true)) {
8232
pos = (this.horiz ? e.chartX - this.pos : this.len - e.chartY + this.pos);
8233
} else if (defined(point)) {
8234
/*jslint eqeq: true*/
8235
pos = (this.chart.inverted != this.horiz) ? point.plotX : this.len - point.plotY;
8236
/*jslint eqeq: false*/
8239
if (this.isRadial) {
8240
path = this.getPlotLinePath(this.isXAxis ? point.x : pick(point.stackY, point.y));
8242
path = this.getPlotLinePath(null, null, null, null, pos);
8245
if (path === null) {
8246
this.hideCrosshair();
8253
.attr({ visibility: VISIBLE })[animation ? 'animate' : 'attr']({ d: path }, animation);
8256
'stroke-width': options.width || 1,
8257
stroke: options.color || '#C0C0C0',
8258
zIndex: options.zIndex || 2
8260
if (options.dashStyle) {
8261
attribs.dashstyle = options.dashStyle;
8263
this.cross = this.chart.renderer.path(path).attr(attribs).add();
8268
* Hide the crosshair.
8270
hideCrosshair: function () {
8277
extend(Axis.prototype, AxisPlotLineOrBandExtension);
8280
* Set the tick positions to a time unit that makes sense, for example
8281
* on the first of each month or on every Monday. Return an array
8282
* with the time positions. Used in datetime axes as well as for grouping
8283
* data on a datetime axis.
8285
* @param {Object} normalizedInterval The interval in axis values (ms) and the count
8286
* @param {Number} min The minimum in axis values
8287
* @param {Number} max The maximum in axis values
8288
* @param {Number} startOfWeek
8290
Axis.prototype.getTimeTicks = function (normalizedInterval, min, max, startOfWeek) {
8291
var tickPositions = [],
8294
useUTC = defaultOptions.global.useUTC,
8295
minYear, // used in months and years as a basis for Date.UTC()
8296
minDate = new Date(min - timezoneOffset),
8297
interval = normalizedInterval.unitRange,
8298
count = normalizedInterval.count;
8300
if (defined(min)) { // #1300
8301
if (interval >= timeUnits[SECOND]) { // second
8302
minDate.setMilliseconds(0);
8303
minDate.setSeconds(interval >= timeUnits[MINUTE] ? 0 :
8304
count * mathFloor(minDate.getSeconds() / count));
8307
if (interval >= timeUnits[MINUTE]) { // minute
8308
minDate[setMinutes](interval >= timeUnits[HOUR] ? 0 :
8309
count * mathFloor(minDate[getMinutes]() / count));
8312
if (interval >= timeUnits[HOUR]) { // hour
8313
minDate[setHours](interval >= timeUnits[DAY] ? 0 :
8314
count * mathFloor(minDate[getHours]() / count));
8317
if (interval >= timeUnits[DAY]) { // day
8318
minDate[setDate](interval >= timeUnits[MONTH] ? 1 :
8319
count * mathFloor(minDate[getDate]() / count));
8322
if (interval >= timeUnits[MONTH]) { // month
8323
minDate[setMonth](interval >= timeUnits[YEAR] ? 0 :
8324
count * mathFloor(minDate[getMonth]() / count));
8325
minYear = minDate[getFullYear]();
8328
if (interval >= timeUnits[YEAR]) { // year
8329
minYear -= minYear % count;
8330
minDate[setFullYear](minYear);
8333
// week is a special case that runs outside the hierarchy
8334
if (interval === timeUnits[WEEK]) {
8335
// get start of current week, independent of count
8336
minDate[setDate](minDate[getDate]() - minDate[getDay]() +
8337
pick(startOfWeek, 1));
8341
// get tick positions
8343
if (timezoneOffset) {
8344
minDate = new Date(minDate.getTime() + timezoneOffset);
8346
minYear = minDate[getFullYear]();
8347
var time = minDate.getTime(),
8348
minMonth = minDate[getMonth](),
8349
minDateDate = minDate[getDate](),
8350
localTimezoneOffset = useUTC ?
8352
(24 * 3600 * 1000 + minDate.getTimezoneOffset() * 60 * 1000) % (24 * 3600 * 1000); // #950
8354
// iterate and add tick positions at appropriate values
8355
while (time < max) {
8356
tickPositions.push(time);
8358
// if the interval is years, use Date.UTC to increase years
8359
if (interval === timeUnits[YEAR]) {
8360
time = makeTime(minYear + i * count, 0);
8362
// if the interval is months, use Date.UTC to increase months
8363
} else if (interval === timeUnits[MONTH]) {
8364
time = makeTime(minYear, minMonth + i * count);
8366
// if we're using global time, the interval is not fixed as it jumps
8367
// one hour at the DST crossover
8368
} else if (!useUTC && (interval === timeUnits[DAY] || interval === timeUnits[WEEK])) {
8369
time = makeTime(minYear, minMonth, minDateDate +
8370
i * count * (interval === timeUnits[DAY] ? 1 : 7));
8372
// else, the interval is fixed and we use simple addition
8374
time += interval * count;
8380
// push the last time
8381
tickPositions.push(time);
8384
// mark new days if the time is dividible by day (#1649, #1760)
8385
each(grep(tickPositions, function (time) {
8386
return interval <= timeUnits[HOUR] && time % timeUnits[DAY] === localTimezoneOffset;
8387
}), function (time) {
8388
higherRanks[time] = DAY;
8393
// record information on the chosen unit - for dynamic label formatter
8394
tickPositions.info = extend(normalizedInterval, {
8395
higherRanks: higherRanks,
8396
totalRange: interval * count
8399
return tickPositions;
8403
* Get a normalized tick interval for dates. Returns a configuration object with
8404
* unit range (interval), count and name. Used to prepare data for getTimeTicks.
8405
* Previously this logic was part of getTimeTicks, but as getTimeTicks now runs
8406
* of segments in stock charts, the normalizing logic was extracted in order to
8407
* prevent it for running over again for each segment having the same interval.
8410
Axis.prototype.normalizeTimeTickInterval = function (tickInterval, unitsOption) {
8411
var units = unitsOption || [[
8412
MILLISECOND, // unit name
8413
[1, 2, 5, 10, 20, 25, 50, 100, 200, 500] // allowed multiples
8416
[1, 2, 5, 10, 15, 30]
8419
[1, 2, 5, 10, 15, 30]
8422
[1, 2, 3, 4, 6, 8, 12]
8436
unit = units[units.length - 1], // default unit is years
8437
interval = timeUnits[unit[0]],
8438
multiples = unit[1],
8442
// loop through the units to find the one that best fits the tickInterval
8443
for (i = 0; i < units.length; i++) {
8445
interval = timeUnits[unit[0]];
8446
multiples = unit[1];
8450
// lessThan is in the middle between the highest multiple and the next unit.
8451
var lessThan = (interval * multiples[multiples.length - 1] +
8452
timeUnits[units[i + 1][0]]) / 2;
8454
// break and keep the current unit
8455
if (tickInterval <= lessThan) {
8461
// prevent 2.5 years intervals, though 25, 250 etc. are allowed
8462
if (interval === timeUnits[YEAR] && tickInterval < 5 * interval) {
8463
multiples = [1, 2, 5];
8467
count = normalizeTickInterval(
8468
tickInterval / interval,
8470
unit[0] === YEAR ? mathMax(getMagnitude(tickInterval / interval), 1) : 1 // #1913, #2360
8474
unitRange: interval,
8479
* Methods defined on the Axis prototype
8483
* Set the tick positions of a logarithmic axis
8485
Axis.prototype.getLogTickPositions = function (interval, min, max, minor) {
8487
options = axis.options,
8488
axisLength = axis.len,
8489
// Since we use this method for both major and minor ticks,
8490
// use a local variable and return the result
8495
axis._minorAutoInterval = null;
8498
// First case: All ticks fall on whole logarithms: 1, 10, 100 etc.
8499
if (interval >= 0.5) {
8500
interval = mathRound(interval);
8501
positions = axis.getLinearTickPositions(interval, min, max);
8503
// Second case: We need intermediary ticks. For example
8504
// 1, 2, 4, 6, 8, 10, 20, 40 etc.
8505
} else if (interval >= 0.08) {
8506
var roundedMin = mathFloor(min),
8515
if (interval > 0.3) {
8516
intermediate = [1, 2, 4];
8517
} else if (interval > 0.15) { // 0.2 equals five minor ticks per 1, 10, 100 etc
8518
intermediate = [1, 2, 4, 6, 8];
8519
} else { // 0.1 equals ten minor ticks per 1, 10, 100 etc
8520
intermediate = [1, 2, 3, 4, 5, 6, 7, 8, 9];
8523
for (i = roundedMin; i < max + 1 && !break2; i++) {
8524
len = intermediate.length;
8525
for (j = 0; j < len && !break2; j++) {
8526
pos = log2lin(lin2log(i) * intermediate[j]);
8528
if (pos > min && (!minor || lastPos <= max)) { // #1670
8529
positions.push(lastPos);
8532
if (lastPos > max) {
8539
// Third case: We are so deep in between whole logarithmic values that
8540
// we might as well handle the tick positions like a linear axis. For
8541
// example 1.01, 1.02, 1.03, 1.04.
8543
var realMin = lin2log(min),
8544
realMax = lin2log(max),
8545
tickIntervalOption = options[minor ? 'minorTickInterval' : 'tickInterval'],
8546
filteredTickIntervalOption = tickIntervalOption === 'auto' ? null : tickIntervalOption,
8547
tickPixelIntervalOption = options.tickPixelInterval / (minor ? 5 : 1),
8548
totalPixelLength = minor ? axisLength / axis.tickPositions.length : axisLength;
8551
filteredTickIntervalOption,
8552
axis._minorAutoInterval,
8553
(realMax - realMin) * tickPixelIntervalOption / (totalPixelLength || 1)
8556
interval = normalizeTickInterval(
8559
getMagnitude(interval)
8562
positions = map(axis.getLinearTickPositions(
8569
axis._minorAutoInterval = interval / 5;
8573
// Set the axis-level tickInterval variable
8575
axis.tickInterval = interval;
8579
* The tooltip object
8580
* @param {Object} chart The chart instance
8581
* @param {Object} options Tooltip options
8583
var Tooltip = Highcharts.Tooltip = function () {
8584
this.init.apply(this, arguments);
8587
Tooltip.prototype = {
8589
init: function (chart, options) {
8591
var borderWidth = options.borderWidth,
8592
style = options.style,
8593
padding = pInt(style.padding);
8595
// Save the chart and options
8597
this.options = options;
8599
// Keep track of the current series
8600
//this.currentSeries = UNDEFINED;
8602
// List of crosshairs
8603
this.crosshairs = [];
8605
// Current values of x and y when animating
8606
this.now = { x: 0, y: 0 };
8608
// The tooltip is initially hidden
8609
this.isHidden = true;
8613
this.label = chart.renderer.label('', 0, 0, options.shape, null, null, options.useHTML, null, 'tooltip')
8616
fill: options.backgroundColor,
8617
'stroke-width': borderWidth,
8618
r: options.borderRadius,
8622
.css({ padding: 0 }) // Remove it from VML, the padding is applied as an attribute instead (#1117)
8624
.attr({ y: -9999 }); // #2301, #2657
8626
// When using canVG the shadow shows up as a gray circle
8627
// even if the tooltip is hidden.
8629
this.label.shadow(options.shadow);
8632
// Public property for getting the shared state.
8633
this.shared = options.shared;
8637
* Destroy the tooltip and its elements.
8639
destroy: function () {
8640
// Destroy and clear local variables
8642
this.label = this.label.destroy();
8644
clearTimeout(this.hideTimer);
8645
clearTimeout(this.tooltipTimeout);
8649
* Provide a soft movement for the tooltip
8655
move: function (x, y, anchorX, anchorY) {
8658
animate = tooltip.options.animation !== false && !tooltip.isHidden;
8660
// get intermediate values for animation
8662
x: animate ? (2 * now.x + x) / 3 : x,
8663
y: animate ? (now.y + y) / 2 : y,
8664
anchorX: animate ? (2 * now.anchorX + anchorX) / 3 : anchorX,
8665
anchorY: animate ? (now.anchorY + anchorY) / 2 : anchorY
8668
// move to the intermediate value
8669
tooltip.label.attr(now);
8672
// run on next tick of the mouse tracker
8673
if (animate && (mathAbs(x - now.x) > 1 || mathAbs(y - now.y) > 1)) {
8675
// never allow two timeouts
8676
clearTimeout(this.tooltipTimeout);
8678
// set the fixed interval ticking for the smooth tooltip
8679
this.tooltipTimeout = setTimeout(function () {
8680
// The interval function may still be running during destroy, so check that the chart is really there before calling.
8682
tooltip.move(x, y, anchorX, anchorY);
8696
clearTimeout(this.hideTimer); // disallow duplicate timers (#1728, #1766)
8697
if (!this.isHidden) {
8698
hoverPoints = this.chart.hoverPoints;
8700
this.hideTimer = setTimeout(function () {
8701
tooltip.label.fadeOut();
8702
tooltip.isHidden = true;
8703
}, pick(this.options.hideDelay, 500));
8705
// hide previous hoverPoints and set new
8707
each(hoverPoints, function (point) {
8712
this.chart.hoverPoints = null;
8717
* Extendable method to get the anchor position of the tooltip
8718
* from a point or set of points
8720
getAnchor: function (points, mouseEvent) {
8723
inverted = chart.inverted,
8724
plotTop = chart.plotTop,
8729
points = splat(points);
8731
// Pie uses a special tooltipPos
8732
ret = points[0].tooltipPos;
8734
// When tooltip follows mouse, relate the position to the mouse
8735
if (this.followPointer && mouseEvent) {
8736
if (mouseEvent.chartX === UNDEFINED) {
8737
mouseEvent = chart.pointer.normalize(mouseEvent);
8740
mouseEvent.chartX - chart.plotLeft,
8741
mouseEvent.chartY - plotTop
8744
// When shared, use the average position
8746
each(points, function (point) {
8747
yAxis = point.series.yAxis;
8748
plotX += point.plotX;
8749
plotY += (point.plotLow ? (point.plotLow + point.plotHigh) / 2 : point.plotY) +
8750
(!inverted && yAxis ? yAxis.top - plotTop : 0); // #1151
8753
plotX /= points.length;
8754
plotY /= points.length;
8757
inverted ? chart.plotWidth - plotY : plotX,
8758
this.shared && !inverted && points.length > 1 && mouseEvent ?
8759
mouseEvent.chartY - plotTop : // place shared tooltip next to the mouse (#424)
8760
inverted ? chart.plotHeight - plotX : plotY
8764
return map(ret, mathRound);
8768
* Place the tooltip in a chart without spilling over
8769
* and not covering the point it self.
8771
getPosition: function (boxWidth, boxHeight, point) {
8773
// Set up the variables
8774
var chart = this.chart,
8775
plotLeft = chart.plotLeft,
8776
plotTop = chart.plotTop,
8777
plotWidth = chart.plotWidth,
8778
plotHeight = chart.plotHeight,
8779
distance = pick(this.options.distance, 12),
8780
pointX = (isNaN(point.plotX) ? 0 : point.plotX), //#2599
8781
pointY = point.plotY,
8782
x = pointX + plotLeft + (chart.inverted ? distance : -boxWidth - distance),
8783
y = pointY - boxHeight + plotTop + 15, // 15 means the point is 15 pixels up from the bottom of the tooltip
8786
// It is too far to the left, adjust it
8788
x = plotLeft + mathMax(pointX, 0) + distance;
8791
// Test to see if the tooltip is too far to the right,
8792
// if it is, move it back to be inside and then up to not cover the point.
8793
if ((x + boxWidth) > (plotLeft + plotWidth)) {
8794
x -= (x + boxWidth) - (plotLeft + plotWidth);
8795
y = pointY - boxHeight + plotTop - distance;
8796
alignedRight = true;
8799
// If it is now above the plot area, align it to the top of the plot area
8800
if (y < plotTop + 5) {
8803
// If the tooltip is still covering the point, move it below instead
8804
if (alignedRight && pointY >= y && pointY <= (y + boxHeight)) {
8805
y = pointY + plotTop + distance; // below
8809
// Now if the tooltip is below the chart, move it up. It's better to cover the
8810
// point than to disappear outside the chart. #834.
8811
if (y + boxHeight > plotTop + plotHeight) {
8812
y = mathMax(plotTop, plotTop + plotHeight - boxHeight - distance); // below
8815
return {x: x, y: y};
8819
* In case no user defined formatter is given, this will be used. Note that the context
8820
* here is an object holding point, series, x, y etc.
8822
defaultFormatter: function (tooltip) {
8823
var items = this.points || splat(this),
8824
series = items[0].series,
8828
s = [tooltip.tooltipHeaderFormatter(items[0])];
8831
each(items, function (item) {
8832
series = item.series;
8833
s.push((series.tooltipFormatter && series.tooltipFormatter(item)) ||
8834
item.point.tooltipFormatter(series.tooltipOptions.pointFormat));
8838
s.push(tooltip.options.footerFormat || '');
8844
* Refresh the tooltip's text and position.
8845
* @param {Object} point
8847
refresh: function (point, mouseEvent) {
8849
chart = tooltip.chart,
8850
label = tooltip.label,
8851
options = tooltip.options,
8858
formatter = options.formatter || tooltip.defaultFormatter,
8859
hoverPoints = chart.hoverPoints,
8861
shared = tooltip.shared,
8864
clearTimeout(this.hideTimer);
8866
// get the reference point coordinates (pie charts use tooltipPos)
8867
tooltip.followPointer = splat(point)[0].series.tooltipOptions.followPointer;
8868
anchor = tooltip.getAnchor(point, mouseEvent);
8872
// shared tooltip, array is sent over
8873
if (shared && !(point.series && point.series.noSharedTooltip)) {
8875
// hide previous hoverPoints and set new
8877
chart.hoverPoints = point;
8879
each(hoverPoints, function (point) {
8884
each(point, function (item) {
8885
item.setState(HOVER_STATE);
8887
pointConfig.push(item.getLabelConfig());
8891
x: point[0].category,
8894
textConfig.points = pointConfig;
8897
// single point tooltip
8899
textConfig = point.getLabelConfig();
8901
text = formatter.call(textConfig, tooltip);
8903
// register the current series
8904
currentSeries = point.series;
8906
// update the inner HTML
8907
if (text === false) {
8912
if (tooltip.isHidden) {
8914
label.attr('opacity', 1).show();
8922
// set the stroke color of the box
8923
borderColor = options.borderColor || point.color || currentSeries.color || '#606060';
8928
tooltip.updatePosition({ plotX: x, plotY: y });
8930
this.isHidden = false;
8932
fireEvent(chart, 'tooltipRefresh', {
8934
x: x + chart.plotLeft,
8935
y: y + chart.plotTop,
8936
borderColor: borderColor
8941
* Find the new position and perform the move
8943
updatePosition: function (point) {
8944
var chart = this.chart,
8946
pos = (this.options.positioner || this.getPosition).call(
8957
point.plotX + chart.plotLeft,
8958
point.plotY + chart.plotTop
8964
* Format the header of the tooltip
8966
tooltipHeaderFormatter: function (point) {
8967
var series = point.series,
8968
tooltipOptions = series.tooltipOptions,
8969
dateTimeLabelFormats = tooltipOptions.dateTimeLabelFormats,
8970
xDateFormat = tooltipOptions.xDateFormat,
8971
xAxis = series.xAxis,
8972
isDateTime = xAxis && xAxis.options.type === 'datetime' && isNumber(point.key),
8973
headerFormat = tooltipOptions.headerFormat,
8974
closestPointRange = xAxis && xAxis.closestPointRange,
8977
// Guess the best date format based on the closest point distance (#568)
8978
if (isDateTime && !xDateFormat) {
8979
if (closestPointRange) {
8980
for (n in timeUnits) {
8981
if (timeUnits[n] >= closestPointRange ||
8982
// If the point is placed every day at 23:59, we need to show
8983
// the minutes as well. This logic only works for time units less than
8984
// a day, since all higher time units are dividable by those. #2637.
8985
(timeUnits[n] <= timeUnits[DAY] && point.key % timeUnits[n] > 0)) {
8986
xDateFormat = dateTimeLabelFormats[n];
8991
xDateFormat = dateTimeLabelFormats.day;
8994
xDateFormat = xDateFormat || dateTimeLabelFormats.year; // #2546, 2581
8998
// Insert the header date format if any
8999
if (isDateTime && xDateFormat) {
9000
headerFormat = headerFormat.replace('{point.key}', '{point.key:' + xDateFormat + '}');
9003
return format(headerFormat, {
9010
var hoverChartIndex;
9012
// Global flag for touch support
9013
hasTouch = doc.documentElement.ontouchstart !== UNDEFINED;
9016
* The mouse tracker object. All methods starting with "on" are primary DOM event handlers.
9017
* Subsequent methods should be named differently from what they are doing.
9018
* @param {Object} chart The Chart instance
9019
* @param {Object} options The root options object
9021
var Pointer = Highcharts.Pointer = function (chart, options) {
9022
this.init(chart, options);
9025
Pointer.prototype = {
9027
* Initialize Pointer
9029
init: function (chart, options) {
9031
var chartOptions = options.chart,
9032
chartEvents = chartOptions.events,
9033
zoomType = useCanVG ? '' : chartOptions.zoomType,
9034
inverted = chart.inverted,
9039
this.options = options;
9043
this.zoomX = zoomX = /x/.test(zoomType);
9044
this.zoomY = zoomY = /y/.test(zoomType);
9045
this.zoomHor = (zoomX && !inverted) || (zoomY && inverted);
9046
this.zoomVert = (zoomY && !inverted) || (zoomX && inverted);
9048
// Do we need to handle click on a touch device?
9049
this.runChartClick = chartEvents && !!chartEvents.click;
9051
this.pinchDown = [];
9052
this.lastValidTouch = {};
9054
if (Highcharts.Tooltip && options.tooltip.enabled) {
9055
chart.tooltip = new Tooltip(chart, options.tooltip);
9058
this.setDOMEvents();
9062
* Add crossbrowser support for chartX and chartY
9063
* @param {Object} e The event object in standard browsers
9065
normalize: function (e, chartPosition) {
9070
// common IE normalizing
9073
// Framework specific normalizing (#1165)
9074
e = washMouseEvent(e);
9076
// More IE normalizing, needs to go after washMouseEvent
9078
e.target = e.srcElement;
9082
ePos = e.touches ? e.touches.item(0) : e;
9084
// Get mouse position
9085
if (!chartPosition) {
9086
this.chartPosition = chartPosition = offset(this.chart.container);
9089
// chartX and chartY
9090
if (ePos.pageX === UNDEFINED) { // IE < 9. #886.
9091
chartX = mathMax(e.x, e.clientX - chartPosition.left); // #2005, #2129: the second case is
9092
// for IE10 quirks mode within framesets
9095
chartX = ePos.pageX - chartPosition.left;
9096
chartY = ePos.pageY - chartPosition.top;
9100
chartX: mathRound(chartX),
9101
chartY: mathRound(chartY)
9106
* Get the click position in terms of axis values.
9108
* @param {Object} e A pointer event
9110
getCoordinates: function (e) {
9116
each(this.chart.axes, function (axis) {
9117
coordinates[axis.isXAxis ? 'xAxis' : 'yAxis'].push({
9119
value: axis.toValue(e[axis.horiz ? 'chartX' : 'chartY'])
9126
* Return the index in the tooltipPoints array, corresponding to pixel position in
9129
getIndex: function (e) {
9130
var chart = this.chart;
9131
return chart.inverted ?
9132
chart.plotHeight + chart.plotTop - e.chartY :
9133
e.chartX - chart.plotLeft;
9137
* With line type charts with a single tracker, get the point closest to the mouse.
9138
* Run Point.onMouseOver and display tooltip for the point or points.
9140
runPointActions: function (e) {
9142
chart = pointer.chart,
9143
series = chart.series,
9144
tooltip = chart.tooltip,
9147
hoverPoint = chart.hoverPoint,
9148
hoverSeries = chart.hoverSeries,
9151
distance = chart.chartWidth,
9152
index = pointer.getIndex(e),
9156
if (tooltip && pointer.options.tooltip.shared && !(hoverSeries && hoverSeries.noSharedTooltip)) {
9159
// loop over all series and find the ones with points closest to the mouse
9161
for (j = 0; j < i; j++) {
9162
if (series[j].visible &&
9163
series[j].options.enableMouseTracking !== false &&
9164
!series[j].noSharedTooltip && series[j].singularTooltips !== true && series[j].tooltipPoints.length) {
9165
point = series[j].tooltipPoints[index];
9166
if (point && point.series) { // not a dummy point, #1544
9167
point._dist = mathAbs(index - point.clientX);
9168
distance = mathMin(distance, point._dist);
9173
// remove furthest points
9176
if (points[i]._dist > distance) {
9177
points.splice(i, 1);
9180
// refresh the tooltip if necessary
9181
if (points.length && (points[0].clientX !== pointer.hoverX)) {
9182
tooltip.refresh(points, e);
9183
pointer.hoverX = points[0].clientX;
9187
// separate tooltip and general mouse events
9188
if (hoverSeries && hoverSeries.tracker && (!tooltip || !tooltip.followPointer)) { // only use for line-type series with common tracker and while not following the pointer #2584
9191
point = hoverSeries.tooltipPoints[index];
9193
// a new point is hovered, refresh the tooltip
9194
if (point && point !== hoverPoint) {
9196
// trigger the events
9197
point.onMouseOver(e);
9201
} else if (tooltip && tooltip.followPointer && !tooltip.isHidden) {
9202
anchor = tooltip.getAnchor([{}], e);
9203
tooltip.updatePosition({ plotX: anchor[0], plotY: anchor[1] });
9206
// Start the event listener to pick up the tooltip
9207
if (tooltip && !pointer._onDocumentMouseMove) {
9208
pointer._onDocumentMouseMove = function (e) {
9209
if (defined(hoverChartIndex)) {
9210
charts[hoverChartIndex].pointer.onDocumentMouseMove(e);
9213
addEvent(doc, 'mousemove', pointer._onDocumentMouseMove);
9216
// Draw independent crosshairs
9217
each(chart.axes, function (axis) {
9218
axis.drawCrosshair(e, pick(point, hoverPoint));
9225
* Reset the tracking by hiding the tooltip, the hover series state and the hover point
9227
* @param allowMove {Boolean} Instead of destroying the tooltip altogether, allow moving it if possible
9229
reset: function (allowMove) {
9231
chart = pointer.chart,
9232
hoverSeries = chart.hoverSeries,
9233
hoverPoint = chart.hoverPoint,
9234
tooltip = chart.tooltip,
9235
tooltipPoints = tooltip && tooltip.shared ? chart.hoverPoints : hoverPoint;
9237
// Narrow in allowMove
9238
allowMove = allowMove && tooltip && tooltipPoints;
9240
// Check if the points have moved outside the plot area, #1003
9241
if (allowMove && splat(tooltipPoints)[0].plotX === UNDEFINED) {
9245
// Just move the tooltip, #349
9247
tooltip.refresh(tooltipPoints);
9248
if (hoverPoint) { // #2500
9249
hoverPoint.setState(hoverPoint.state, true);
9256
hoverPoint.onMouseOut();
9260
hoverSeries.onMouseOut();
9267
if (pointer._onDocumentMouseMove) {
9268
removeEvent(doc, 'mousemove', pointer._onDocumentMouseMove);
9269
pointer._onDocumentMouseMove = null;
9272
// Remove crosshairs
9273
each(chart.axes, function (axis) {
9274
axis.hideCrosshair();
9277
pointer.hoverX = null;
9283
* Scale series groups to a certain scale and translation
9285
scaleGroups: function (attribs, clip) {
9287
var chart = this.chart,
9290
// Scale each series
9291
each(chart.series, function (series) {
9292
seriesAttribs = attribs || series.getPlotBox(); // #1701
9293
if (series.xAxis && series.xAxis.zoomEnabled) {
9294
series.group.attr(seriesAttribs);
9295
if (series.markerGroup) {
9296
series.markerGroup.attr(seriesAttribs);
9297
series.markerGroup.clip(clip ? chart.clipRect : null);
9299
if (series.dataLabelsGroup) {
9300
series.dataLabelsGroup.attr(seriesAttribs);
9306
chart.clipRect.attr(clip || chart.clipBox);
9310
* Start a drag operation
9312
dragStart: function (e) {
9313
var chart = this.chart;
9315
// Record the start position
9316
chart.mouseIsDown = e.type;
9317
chart.cancelClick = false;
9318
chart.mouseDownX = this.mouseDownX = e.chartX;
9319
chart.mouseDownY = this.mouseDownY = e.chartY;
9323
* Perform a drag operation in response to a mousemove event while the mouse is down
9325
drag: function (e) {
9327
var chart = this.chart,
9328
chartOptions = chart.options.chart,
9331
zoomHor = this.zoomHor,
9332
zoomVert = this.zoomVert,
9333
plotLeft = chart.plotLeft,
9334
plotTop = chart.plotTop,
9335
plotWidth = chart.plotWidth,
9336
plotHeight = chart.plotHeight,
9339
mouseDownX = this.mouseDownX,
9340
mouseDownY = this.mouseDownY;
9342
// If the mouse is outside the plot area, adjust to cooordinates
9343
// inside to prevent the selection marker from going outside
9344
if (chartX < plotLeft) {
9346
} else if (chartX > plotLeft + plotWidth) {
9347
chartX = plotLeft + plotWidth;
9350
if (chartY < plotTop) {
9352
} else if (chartY > plotTop + plotHeight) {
9353
chartY = plotTop + plotHeight;
9356
// determine if the mouse has moved more than 10px
9357
this.hasDragged = Math.sqrt(
9358
Math.pow(mouseDownX - chartX, 2) +
9359
Math.pow(mouseDownY - chartY, 2)
9362
if (this.hasDragged > 10) {
9363
clickedInside = chart.isInsidePlot(mouseDownX - plotLeft, mouseDownY - plotTop);
9366
if (chart.hasCartesianSeries && (this.zoomX || this.zoomY) && clickedInside) {
9367
if (!this.selectionMarker) {
9368
this.selectionMarker = chart.renderer.rect(
9371
zoomHor ? 1 : plotWidth,
9372
zoomVert ? 1 : plotHeight,
9376
fill: chartOptions.selectionMarkerFill || 'rgba(69,114,167,0.25)',
9383
// adjust the width of the selection marker
9384
if (this.selectionMarker && zoomHor) {
9385
size = chartX - mouseDownX;
9386
this.selectionMarker.attr({
9387
width: mathAbs(size),
9388
x: (size > 0 ? 0 : size) + mouseDownX
9391
// adjust the height of the selection marker
9392
if (this.selectionMarker && zoomVert) {
9393
size = chartY - mouseDownY;
9394
this.selectionMarker.attr({
9395
height: mathAbs(size),
9396
y: (size > 0 ? 0 : size) + mouseDownY
9401
if (clickedInside && !this.selectionMarker && chartOptions.panning) {
9402
chart.pan(e, chartOptions.panning);
9408
* On mouse up or touch end across the entire document, drop the selection.
9410
drop: function (e) {
9411
var chart = this.chart,
9412
hasPinched = this.hasPinched;
9414
if (this.selectionMarker) {
9415
var selectionData = {
9418
originalEvent: e.originalEvent || e
9420
selectionBox = this.selectionMarker,
9421
selectionLeft = selectionBox.x,
9422
selectionTop = selectionBox.y,
9424
// a selection has been made
9425
if (this.hasDragged || hasPinched) {
9427
// record each axis' min and max
9428
each(chart.axes, function (axis) {
9429
if (axis.zoomEnabled) {
9430
var horiz = axis.horiz,
9431
selectionMin = axis.toValue((horiz ? selectionLeft : selectionTop)),
9432
selectionMax = axis.toValue((horiz ? selectionLeft + selectionBox.width : selectionTop + selectionBox.height));
9434
if (!isNaN(selectionMin) && !isNaN(selectionMax)) { // #859
9435
selectionData[axis.coll].push({
9437
min: mathMin(selectionMin, selectionMax), // for reversed axes,
9438
max: mathMax(selectionMin, selectionMax)
9445
fireEvent(chart, 'selection', selectionData, function (args) {
9446
chart.zoom(extend(args, hasPinched ? { animation: false } : null));
9451
this.selectionMarker = this.selectionMarker.destroy();
9453
// Reset scaling preview
9460
if (chart) { // it may be destroyed on mouse up - #877
9461
css(chart.container, { cursor: chart._cursor });
9462
chart.cancelClick = this.hasDragged > 10; // #370
9463
chart.mouseIsDown = this.hasDragged = this.hasPinched = false;
9464
this.pinchDown = [];
9468
onContainerMouseDown: function (e) {
9470
e = this.normalize(e);
9472
// issue #295, dragging not always working in Firefox
9473
if (e.preventDefault) {
9482
onDocumentMouseUp: function (e) {
9483
if (defined(hoverChartIndex)) {
9484
charts[hoverChartIndex].pointer.drop(e);
9489
* Special handler for mouse move that will hide the tooltip when the mouse leaves the plotarea.
9490
* Issue #149 workaround. The mouseleave event does not always fire.
9492
onDocumentMouseMove: function (e) {
9493
var chart = this.chart,
9494
chartPosition = this.chartPosition,
9495
hoverSeries = chart.hoverSeries;
9497
e = this.normalize(e, chartPosition);
9499
// If we're outside, hide the tooltip
9500
if (chartPosition && hoverSeries && !this.inClass(e.target, 'highcharts-tracker') &&
9501
!chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) {
9507
* When mouse leaves the container, hide the tooltip.
9509
onContainerMouseLeave: function () {
9510
var chart = charts[hoverChartIndex];
9512
chart.pointer.reset();
9513
chart.pointer.chartPosition = null; // also reset the chart position, used in #149 fix
9515
hoverChartIndex = null;
9518
// The mousemove, touchmove and touchstart event handler
9519
onContainerMouseMove: function (e) {
9521
var chart = this.chart;
9523
hoverChartIndex = chart.index;
9526
e = this.normalize(e);
9528
if (chart.mouseIsDown === 'mousedown') {
9532
// Show the tooltip and run mouse over events (#977)
9533
if ((this.inClass(e.target, 'highcharts-tracker') ||
9534
chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) && !chart.openMenu) {
9535
this.runPointActions(e);
9540
* Utility to detect whether an element has, or has a parent with, a specific
9541
* class name. Used on detection of tracker objects and on deciding whether
9542
* hovering the tooltip should cause the active series to mouse out.
9544
inClass: function (element, className) {
9547
elemClassName = attr(element, 'class');
9548
if (elemClassName) {
9549
if (elemClassName.indexOf(className) !== -1) {
9551
} else if (elemClassName.indexOf(PREFIX + 'container') !== -1) {
9555
element = element.parentNode;
9559
onTrackerMouseOut: function (e) {
9560
var series = this.chart.hoverSeries,
9561
relatedTarget = e.relatedTarget || e.toElement,
9562
relatedSeries = relatedTarget && relatedTarget.point && relatedTarget.point.series; // #2499
9564
if (series && !series.options.stickyTracking && !this.inClass(relatedTarget, PREFIX + 'tooltip') &&
9565
relatedSeries !== series) {
9566
series.onMouseOut();
9570
onContainerClick: function (e) {
9571
var chart = this.chart,
9572
hoverPoint = chart.hoverPoint,
9573
plotLeft = chart.plotLeft,
9574
plotTop = chart.plotTop,
9575
inverted = chart.inverted,
9580
e = this.normalize(e);
9581
e.cancelBubble = true; // IE specific
9583
if (!chart.cancelClick) {
9585
// On tracker click, fire the series and point events. #783, #1583
9586
if (hoverPoint && this.inClass(e.target, PREFIX + 'tracker')) {
9587
chartPosition = this.chartPosition;
9588
plotX = hoverPoint.plotX;
9589
plotY = hoverPoint.plotY;
9591
// add page position info
9592
extend(hoverPoint, {
9593
pageX: chartPosition.left + plotLeft +
9594
(inverted ? chart.plotWidth - plotY : plotX),
9595
pageY: chartPosition.top + plotTop +
9596
(inverted ? chart.plotHeight - plotX : plotY)
9599
// the series click event
9600
fireEvent(hoverPoint.series, 'click', extend(e, {
9604
// the point click event
9605
if (chart.hoverPoint) { // it may be destroyed (#1844)
9606
hoverPoint.firePointEvent('click', e);
9609
// When clicking outside a tracker, fire a chart event
9611
extend(e, this.getCoordinates(e));
9613
// fire a click event in the chart
9614
if (chart.isInsidePlot(e.chartX - plotLeft, e.chartY - plotTop)) {
9615
fireEvent(chart, 'click', e);
9624
* Set the JS DOM events on the container and document. This method should contain
9625
* a one-to-one assignment between methods and their handlers. Any advanced logic should
9626
* be moved to the handler reflecting the event's name.
9628
setDOMEvents: function () {
9631
container = pointer.chart.container;
9633
container.onmousedown = function (e) {
9634
pointer.onContainerMouseDown(e);
9636
container.onmousemove = function (e) {
9637
pointer.onContainerMouseMove(e);
9639
container.onclick = function (e) {
9640
pointer.onContainerClick(e);
9642
addEvent(container, 'mouseleave', pointer.onContainerMouseLeave);
9643
addEvent(doc, 'mouseup', pointer.onDocumentMouseUp);
9646
container.ontouchstart = function (e) {
9647
pointer.onContainerTouchStart(e);
9649
container.ontouchmove = function (e) {
9650
pointer.onContainerTouchMove(e);
9652
addEvent(doc, 'touchend', pointer.onDocumentTouchEnd);
9658
* Destroys the Pointer object and disconnects DOM events.
9660
destroy: function () {
9663
removeEvent(this.chart.container, 'mouseleave', this.onContainerMouseLeave);
9664
removeEvent(doc, 'mouseup', this.onDocumentMouseUp);
9665
removeEvent(doc, 'touchend', this.onDocumentTouchEnd);
9667
// memory and CPU leak
9668
clearInterval(this.tooltipTimeout);
9670
for (prop in this) {
9677
/* Support for touch devices */
9678
extend(Highcharts.Pointer.prototype, {
9681
* Run translation operations
9683
pinchTranslate: function (zoomHor, zoomVert, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch) {
9685
this.pinchTranslateDirection(true, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
9688
this.pinchTranslateDirection(false, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
9693
* Run translation operations for each direction (horizontal and vertical) independently
9695
pinchTranslateDirection: function (horiz, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch, forcedScale) {
9696
var chart = this.chart,
9697
xy = horiz ? 'x' : 'y',
9698
XY = horiz ? 'X' : 'Y',
9699
sChartXY = 'chart' + XY,
9700
wh = horiz ? 'width' : 'height',
9701
plotLeftTop = chart['plot' + (horiz ? 'Left' : 'Top')],
9705
scale = forcedScale || 1,
9706
inverted = chart.inverted,
9707
bounds = chart.bounds[horiz ? 'h' : 'v'],
9708
singleTouch = pinchDown.length === 1,
9709
touch0Start = pinchDown[0][sChartXY],
9710
touch0Now = touches[0][sChartXY],
9711
touch1Start = !singleTouch && pinchDown[1][sChartXY],
9712
touch1Now = !singleTouch && touches[1][sChartXY],
9716
setScale = function () {
9717
if (!singleTouch && mathAbs(touch0Start - touch1Start) > 20) { // Don't zoom if fingers are too close on this axis
9718
scale = forcedScale || mathAbs(touch0Now - touch1Now) / mathAbs(touch0Start - touch1Start);
9721
clipXY = ((plotLeftTop - touch0Now) / scale) + touch0Start;
9722
selectionWH = chart['plot' + (horiz ? 'Width' : 'Height')] / scale;
9725
// Set the scale, first pass
9728
selectionXY = clipXY; // the clip position (x or y) is altered if out of bounds, the selection position is not
9731
if (selectionXY < bounds.min) {
9732
selectionXY = bounds.min;
9734
} else if (selectionXY + selectionWH > bounds.max) {
9735
selectionXY = bounds.max - selectionWH;
9739
// Is the chart dragged off its bounds, determined by dataMin and dataMax?
9742
// Modify the touchNow position in order to create an elastic drag movement. This indicates
9743
// to the user that the chart is responsive but can't be dragged further.
9744
touch0Now -= 0.8 * (touch0Now - lastValidTouch[xy][0]);
9746
touch1Now -= 0.8 * (touch1Now - lastValidTouch[xy][1]);
9749
// Set the scale, second pass to adapt to the modified touchNow positions
9753
lastValidTouch[xy] = [touch0Now, touch1Now];
9756
// Set geometry for clipping, selection and transformation
9757
if (!inverted) { // TODO: implement clipping for inverted charts
9758
clip[xy] = clipXY - plotLeftTop;
9759
clip[wh] = selectionWH;
9761
scaleKey = inverted ? (horiz ? 'scaleY' : 'scaleX') : 'scale' + XY;
9762
transformScale = inverted ? 1 / scale : scale;
9764
selectionMarker[wh] = selectionWH;
9765
selectionMarker[xy] = selectionXY;
9766
transform[scaleKey] = scale;
9767
transform['translate' + XY] = (transformScale * plotLeftTop) + (touch0Now - (transformScale * touch0Start));
9771
* Handle touch events with two touches
9773
pinch: function (e) {
9777
pinchDown = self.pinchDown,
9778
followTouchMove = chart.tooltip && chart.tooltip.options.followTouchMove,
9779
touches = e.touches,
9780
touchesLength = touches.length,
9781
lastValidTouch = self.lastValidTouch,
9782
zoomHor = self.zoomHor || self.pinchHor,
9783
zoomVert = self.zoomVert || self.pinchVert,
9784
hasZoom = zoomHor || zoomVert,
9785
selectionMarker = self.selectionMarker,
9787
fireClickEvent = touchesLength === 1 && ((self.inClass(e.target, PREFIX + 'tracker') &&
9788
chart.runTrackerClick) || chart.runChartClick),
9791
// On touch devices, only proceed to trigger click if a handler is defined
9792
if ((hasZoom || followTouchMove) && !fireClickEvent) {
9796
// Normalize each touch
9797
map(touches, function (e) {
9798
return self.normalize(e);
9801
// Register the touch start position
9802
if (e.type === 'touchstart') {
9803
each(touches, function (e, i) {
9804
pinchDown[i] = { chartX: e.chartX, chartY: e.chartY };
9806
lastValidTouch.x = [pinchDown[0].chartX, pinchDown[1] && pinchDown[1].chartX];
9807
lastValidTouch.y = [pinchDown[0].chartY, pinchDown[1] && pinchDown[1].chartY];
9809
// Identify the data bounds in pixels
9810
each(chart.axes, function (axis) {
9811
if (axis.zoomEnabled) {
9812
var bounds = chart.bounds[axis.horiz ? 'h' : 'v'],
9813
minPixelPadding = axis.minPixelPadding,
9814
min = axis.toPixels(axis.dataMin),
9815
max = axis.toPixels(axis.dataMax),
9816
absMin = mathMin(min, max),
9817
absMax = mathMax(min, max);
9819
// Store the bounds for use in the touchmove handler
9820
bounds.min = mathMin(axis.pos, absMin - minPixelPadding);
9821
bounds.max = mathMax(axis.pos + axis.len, absMax + minPixelPadding);
9825
// Event type is touchmove, handle panning and pinching
9826
} else if (pinchDown.length) { // can be 0 when releasing, if touchend fires first
9830
if (!selectionMarker) {
9831
self.selectionMarker = selectionMarker = extend({
9836
self.pinchTranslate(zoomHor, zoomVert, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
9838
self.hasPinched = hasZoom;
9840
// Scale and translate the groups to provide visual feedback during pinching
9841
self.scaleGroups(transform, clip);
9843
// Optionally move the tooltip on touchmove
9844
if (!hasZoom && followTouchMove && touchesLength === 1) {
9845
this.runPointActions(self.normalize(e));
9850
onContainerTouchStart: function (e) {
9851
var chart = this.chart;
9853
hoverChartIndex = chart.index;
9855
if (e.touches.length === 1) {
9857
e = this.normalize(e);
9859
if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) {
9861
// Prevent the click pseudo event from firing unless it is set in the options
9862
/*if (!chart.runChartClick) {
9866
// Run mouse events and display tooltip etc
9867
this.runPointActions(e);
9872
// Hide the tooltip on touching outside the plot area (#1203)
9876
} else if (e.touches.length === 2) {
9881
onContainerTouchMove: function (e) {
9882
if (e.touches.length === 1 || e.touches.length === 2) {
9887
onDocumentTouchEnd: function (e) {
9888
if (defined(hoverChartIndex)) {
9889
charts[hoverChartIndex].pointer.drop(e);
9894
if (win.PointerEvent || win.MSPointerEvent) {
9896
// The touches object keeps track of the points being touched at all times
9898
hasPointerEvent = !!win.PointerEvent,
9899
getWebkitTouches = function () {
9901
fake.item = function (i) { return this[i]; };
9902
for (key in touches) {
9903
if (touches.hasOwnProperty(key)) {
9905
pageX: touches[key].pageX,
9906
pageY: touches[key].pageY,
9907
target: touches[key].target
9913
translateMSPointer = function (e, method, wktype, callback) {
9915
e = e.originalEvent || e;
9916
if ((e.pointerType === 'touch' || e.pointerType === e.MSPOINTER_TYPE_TOUCH) && charts[hoverChartIndex]) {
9918
p = charts[hoverChartIndex].pointer;
9921
target: e.currentTarget,
9922
preventDefault: noop,
9923
touches: getWebkitTouches()
9929
* Extend the Pointer prototype with methods for each event handler and more
9931
extend(Pointer.prototype, {
9932
onContainerPointerDown: function (e) {
9933
translateMSPointer(e, 'onContainerTouchStart', 'touchstart', function (e) {
9934
touches[e.pointerId] = { pageX: e.pageX, pageY: e.pageY, target: e.currentTarget };
9937
onContainerPointerMove: function (e) {
9938
translateMSPointer(e, 'onContainerTouchMove', 'touchmove', function (e) {
9939
touches[e.pointerId] = { pageX: e.pageX, pageY: e.pageY };
9940
if (!touches[e.pointerId].target) {
9941
touches[e.pointerId].target = e.currentTarget;
9945
onDocumentPointerUp: function (e) {
9946
translateMSPointer(e, 'onContainerTouchEnd', 'touchend', function (e) {
9947
delete touches[e.pointerId];
9952
* Add or remove the MS Pointer specific events
9954
batchMSEvents: function (fn) {
9955
fn(this.chart.container, hasPointerEvent ? 'pointerdown' : 'MSPointerDown', this.onContainerPointerDown);
9956
fn(this.chart.container, hasPointerEvent ? 'pointermove' : 'MSPointerMove', this.onContainerPointerMove);
9957
fn(doc, hasPointerEvent ? 'pointerup' : 'MSPointerUp', this.onDocumentPointerUp);
9961
// Disable default IE actions for pinch and such on chart element
9962
wrap(Pointer.prototype, 'init', function (proceed, chart, options) {
9963
css(chart.container, {
9964
'-ms-touch-action': NONE,
9965
'touch-action': NONE
9967
proceed.call(this, chart, options);
9970
// Add IE specific touch events to chart
9971
wrap(Pointer.prototype, 'setDOMEvents', function (proceed) {
9972
proceed.apply(this);
9973
this.batchMSEvents(addEvent);
9975
// Destroy MS events also
9976
wrap(Pointer.prototype, 'destroy', function (proceed) {
9977
this.batchMSEvents(removeEvent);
9982
* The overview of the chart's series
9984
var Legend = Highcharts.Legend = function (chart, options) {
9985
this.init(chart, options);
9988
Legend.prototype = {
9991
* Initialize the legend
9993
init: function (chart, options) {
9996
itemStyle = options.itemStyle,
9997
padding = pick(options.padding, 8),
9998
itemMarginTop = options.itemMarginTop || 0;
10000
this.options = options;
10002
if (!options.enabled) {
10006
legend.baseline = pInt(itemStyle.fontSize) + 3 + itemMarginTop; // used in Series prototype
10007
legend.itemStyle = itemStyle;
10008
legend.itemHiddenStyle = merge(itemStyle, options.itemHiddenStyle);
10009
legend.itemMarginTop = itemMarginTop;
10010
legend.padding = padding;
10011
legend.initialItemX = padding;
10012
legend.initialItemY = padding - 5; // 5 is the number of pixels above the text
10013
legend.maxItemWidth = 0;
10014
legend.chart = chart;
10015
legend.itemHeight = 0;
10016
legend.lastLineHeight = 0;
10017
legend.symbolWidth = pick(options.symbolWidth, 16);
10025
addEvent(legend.chart, 'endResize', function () {
10026
legend.positionCheckboxes();
10032
* Set the colors for the legend item
10033
* @param {Object} item A Series or Point instance
10034
* @param {Object} visible Dimmed or colored
10036
colorizeItem: function (item, visible) {
10038
options = legend.options,
10039
legendItem = item.legendItem,
10040
legendLine = item.legendLine,
10041
legendSymbol = item.legendSymbol,
10042
hiddenColor = legend.itemHiddenStyle.color,
10043
textColor = visible ? options.itemStyle.color : hiddenColor,
10044
symbolColor = visible ? (item.legendColor || item.color || '#CCC') : hiddenColor,
10045
markerOptions = item.options && item.options.marker,
10047
stroke: symbolColor,
10054
legendItem.css({ fill: textColor, color: textColor }); // color for #1553, oldIE
10057
legendLine.attr({ stroke: symbolColor });
10060
if (legendSymbol) {
10062
// Apply marker options
10063
if (markerOptions && legendSymbol.isMarker) { // #585
10064
markerOptions = item.convertAttribs(markerOptions);
10065
for (key in markerOptions) {
10066
val = markerOptions[key];
10067
if (val !== UNDEFINED) {
10068
symbolAttr[key] = val;
10073
legendSymbol.attr(symbolAttr);
10078
* Position the legend item
10079
* @param {Object} item A Series or Point instance
10081
positionItem: function (item) {
10083
options = legend.options,
10084
symbolPadding = options.symbolPadding,
10085
ltr = !options.rtl,
10086
legendItemPos = item._legendItemPos,
10087
itemX = legendItemPos[0],
10088
itemY = legendItemPos[1],
10089
checkbox = item.checkbox;
10091
if (item.legendGroup) {
10092
item.legendGroup.translate(
10093
ltr ? itemX : legend.legendWidth - itemX - 2 * symbolPadding - 4,
10099
checkbox.x = itemX;
10100
checkbox.y = itemY;
10105
* Destroy a single legend item
10106
* @param {Object} item The series or point
10108
destroyItem: function (item) {
10109
var checkbox = item.checkbox;
10111
// destroy SVG elements
10112
each(['legendItem', 'legendLine', 'legendSymbol', 'legendGroup'], function (key) {
10114
item[key] = item[key].destroy();
10119
discardElement(item.checkbox);
10124
* Destroys the legend.
10126
destroy: function () {
10128
legendGroup = legend.group,
10132
legend.box = box.destroy();
10136
legend.group = legendGroup.destroy();
10141
* Position the checkboxes after the width is determined
10143
positionCheckboxes: function (scrollOffset) {
10144
var alignAttr = this.group.alignAttr,
10146
clipHeight = this.clipHeight || this.legendHeight;
10149
translateY = alignAttr.translateY;
10150
each(this.allItems, function (item) {
10151
var checkbox = item.checkbox,
10155
top = (translateY + checkbox.y + (scrollOffset || 0) + 3);
10157
left: (alignAttr.translateX + item.legendItemWidth + checkbox.x - 20) + PX,
10159
display: top > translateY - 6 && top < translateY + clipHeight - 6 ? '' : NONE
10167
* Render the legend title on top of the legend
10169
renderTitle: function () {
10170
var options = this.options,
10171
padding = this.padding,
10172
titleOptions = options.title,
10176
if (titleOptions.text) {
10178
this.title = this.chart.renderer.label(titleOptions.text, padding - 3, padding - 4, null, null, null, null, null, 'legend-title')
10179
.attr({ zIndex: 1 })
10180
.css(titleOptions.style)
10183
bBox = this.title.getBBox();
10184
titleHeight = bBox.height;
10185
this.offsetWidth = bBox.width; // #1717
10186
this.contentGroup.attr({ translateY: titleHeight });
10188
this.titleHeight = titleHeight;
10192
* Render a single specific legend item
10193
* @param {Object} item A series or point
10195
renderItem: function (item) {
10197
chart = legend.chart,
10198
renderer = chart.renderer,
10199
options = legend.options,
10200
horizontal = options.layout === 'horizontal',
10201
symbolWidth = legend.symbolWidth,
10202
symbolPadding = options.symbolPadding,
10203
itemStyle = legend.itemStyle,
10204
itemHiddenStyle = legend.itemHiddenStyle,
10205
padding = legend.padding,
10206
itemDistance = horizontal ? pick(options.itemDistance, 8) : 0,
10207
ltr = !options.rtl,
10209
widthOption = options.width,
10210
itemMarginBottom = options.itemMarginBottom || 0,
10211
itemMarginTop = legend.itemMarginTop,
10212
initialItemX = legend.initialItemX,
10215
li = item.legendItem,
10216
series = item.series && item.series.drawLegendSymbol ? item.series : item,
10217
seriesOptions = series.options,
10218
showCheckbox = legend.createCheckboxForItem && seriesOptions && seriesOptions.showCheckbox,
10219
useHTML = options.useHTML;
10221
if (!li) { // generate it once, later move it
10223
// Generate the group box
10224
// A group to hold the symbol and text. Text is to be appended in Legend class.
10225
item.legendGroup = renderer.g('legend-item')
10226
.attr({ zIndex: 1 })
10227
.add(legend.scrollGroup);
10229
// Draw the legend symbol inside the group box
10230
series.drawLegendSymbol(legend, item);
10232
// Generate the list item text and add it to the group
10233
item.legendItem = li = renderer.text(
10234
options.labelFormat ? format(options.labelFormat, item) : options.labelFormatter.call(item),
10235
ltr ? symbolWidth + symbolPadding : -symbolPadding,
10239
.css(merge(item.visible ? itemStyle : itemHiddenStyle)) // merge to prevent modifying original (#1021)
10241
align: ltr ? 'left' : 'right',
10244
.add(item.legendGroup);
10246
if (legend.setItemEvents) {
10247
legend.setItemEvents(item, li, useHTML, itemStyle, itemHiddenStyle);
10250
// Colorize the items
10251
legend.colorizeItem(item, item.visible);
10253
// add the HTML checkbox on top
10254
if (showCheckbox) {
10255
legend.createCheckboxForItem(item);
10259
// calculate the positions for the next line
10260
bBox = li.getBBox();
10262
itemWidth = item.legendItemWidth =
10263
options.itemWidth || item.legendItemWidth || symbolWidth + symbolPadding + bBox.width + itemDistance +
10264
(showCheckbox ? 20 : 0);
10265
legend.itemHeight = itemHeight = mathRound(item.legendItemHeight || bBox.height);
10267
// if the item exceeds the width, start a new line
10268
if (horizontal && legend.itemX - initialItemX + itemWidth >
10269
(widthOption || (chart.chartWidth - 2 * padding - initialItemX - options.x))) {
10270
legend.itemX = initialItemX;
10271
legend.itemY += itemMarginTop + legend.lastLineHeight + itemMarginBottom;
10272
legend.lastLineHeight = 0; // reset for next line
10275
// If the item exceeds the height, start a new column
10276
/*if (!horizontal && legend.itemY + options.y + itemHeight > chart.chartHeight - spacingTop - spacingBottom) {
10277
legend.itemY = legend.initialItemY;
10278
legend.itemX += legend.maxItemWidth;
10279
legend.maxItemWidth = 0;
10282
// Set the edge positions
10283
legend.maxItemWidth = mathMax(legend.maxItemWidth, itemWidth);
10284
legend.lastItemY = itemMarginTop + legend.itemY + itemMarginBottom;
10285
legend.lastLineHeight = mathMax(itemHeight, legend.lastLineHeight); // #915
10287
// cache the position of the newly generated or reordered items
10288
item._legendItemPos = [legend.itemX, legend.itemY];
10292
legend.itemX += itemWidth;
10295
legend.itemY += itemMarginTop + itemHeight + itemMarginBottom;
10296
legend.lastLineHeight = itemHeight;
10299
// the width of the widest item
10300
legend.offsetWidth = widthOption || mathMax(
10301
(horizontal ? legend.itemX - initialItemX - itemDistance : itemWidth) + padding,
10307
* Get all items, which is one item per series for normal series and one item per point
10310
getAllItems: function () {
10312
each(this.chart.series, function (series) {
10313
var seriesOptions = series.options;
10315
// Handle showInLegend. If the series is linked to another series, defaults to false.
10316
if (!pick(seriesOptions.showInLegend, !defined(seriesOptions.linkedTo) ? UNDEFINED : false, true)) {
10320
// use points or series for the legend item depending on legendType
10321
allItems = allItems.concat(
10322
series.legendItems ||
10323
(seriesOptions.legendType === 'point' ?
10332
* Render the legend. This method can be called both before and after
10333
* chart.render. If called after, it will only rearrange items instead
10334
* of creating new ones.
10336
render: function () {
10338
chart = legend.chart,
10339
renderer = chart.renderer,
10340
legendGroup = legend.group,
10346
options = legend.options,
10347
padding = legend.padding,
10348
legendBorderWidth = options.borderWidth,
10349
legendBackgroundColor = options.backgroundColor;
10351
legend.itemX = legend.initialItemX;
10352
legend.itemY = legend.initialItemY;
10353
legend.offsetWidth = 0;
10354
legend.lastItemY = 0;
10356
if (!legendGroup) {
10357
legend.group = legendGroup = renderer.g('legend')
10358
.attr({ zIndex: 7 })
10360
legend.contentGroup = renderer.g()
10361
.attr({ zIndex: 1 }) // above background
10363
legend.scrollGroup = renderer.g()
10364
.add(legend.contentGroup);
10367
legend.renderTitle();
10369
// add each series or point
10370
allItems = legend.getAllItems();
10372
// sort by legendIndex
10373
stableSort(allItems, function (a, b) {
10374
return ((a.options && a.options.legendIndex) || 0) - ((b.options && b.options.legendIndex) || 0);
10378
if (options.reversed) {
10379
allItems.reverse();
10382
legend.allItems = allItems;
10383
legend.display = display = !!allItems.length;
10385
// render the items
10386
each(allItems, function (item) {
10387
legend.renderItem(item);
10391
legendWidth = options.width || legend.offsetWidth;
10392
legendHeight = legend.lastItemY + legend.lastLineHeight + legend.titleHeight;
10395
legendHeight = legend.handleOverflow(legendHeight);
10397
if (legendBorderWidth || legendBackgroundColor) {
10398
legendWidth += padding;
10399
legendHeight += padding;
10402
legend.box = box = renderer.rect(
10407
options.borderRadius,
10408
legendBorderWidth || 0
10410
stroke: options.borderColor,
10411
'stroke-width': legendBorderWidth || 0,
10412
fill: legendBackgroundColor || NONE
10415
.shadow(options.shadow);
10418
} else if (legendWidth > 0 && legendHeight > 0) {
10419
box[box.isNew ? 'attr' : 'animate'](
10420
box.crisp({ width: legendWidth, height: legendHeight })
10425
// hide the border if no items
10426
box[display ? 'show' : 'hide']();
10429
legend.legendWidth = legendWidth;
10430
legend.legendHeight = legendHeight;
10432
// Now that the legend width and height are established, put the items in the
10434
each(allItems, function (item) {
10435
legend.positionItem(item);
10438
// 1.x compatibility: positioning based on style
10439
/*var props = ['left', 'right', 'top', 'bottom'],
10444
if (options.style[prop] && options.style[prop] !== 'auto') {
10445
options[i < 2 ? 'align' : 'verticalAlign'] = prop;
10446
options[i < 2 ? 'x' : 'y'] = pInt(options.style[prop]) * (i % 2 ? -1 : 1);
10451
legendGroup.align(extend({
10452
width: legendWidth,
10453
height: legendHeight
10454
}, options), true, 'spacingBox');
10457
if (!chart.isResizing) {
10458
this.positionCheckboxes();
10463
* Set up the overflow handling by adding navigation with up and down arrows below the
10466
handleOverflow: function (legendHeight) {
10468
chart = this.chart,
10469
renderer = chart.renderer,
10470
options = this.options,
10471
optionsY = options.y,
10472
alignTop = options.verticalAlign === 'top',
10473
spaceHeight = chart.spacingBox.height + (alignTop ? -optionsY : optionsY) - this.padding,
10474
maxHeight = options.maxHeight,
10476
clipRect = this.clipRect,
10477
navOptions = options.navigation,
10478
animation = pick(navOptions.animation, true),
10479
arrowSize = navOptions.arrowSize || 12,
10481
pages = this.pages,
10483
allItems = this.allItems;
10485
// Adjust the height
10486
if (options.layout === 'horizontal') {
10490
spaceHeight = mathMin(spaceHeight, maxHeight);
10493
// Reset the legend height and adjust the clipping rectangle
10495
if (legendHeight > spaceHeight && !options.useHTML) {
10497
this.clipHeight = clipHeight = spaceHeight - 20 - this.titleHeight - this.padding;
10498
this.currentPage = pick(this.currentPage, 1);
10499
this.fullHeight = legendHeight;
10501
// Fill pages with Y positions so that the top of each a legend item defines
10502
// the scroll top for each page (#2098)
10503
each(allItems, function (item, i) {
10504
var y = item._legendItemPos[1],
10505
h = mathRound(item.legendItem.getBBox().height),
10506
len = pages.length;
10508
if (!len || (y - pages[len - 1] > clipHeight && (lastY || y) !== pages[len - 1])) {
10509
pages.push(lastY || y);
10513
if (i === allItems.length - 1 && y + h - pages[len - 1] > clipHeight) {
10521
// Only apply clipping if needed. Clipping causes blurred legend in PDF export (#1787)
10523
clipRect = legend.clipRect = renderer.clipRect(0, this.padding, 9999, 0);
10524
legend.contentGroup.clip(clipRect);
10530
// Add navigation elements
10532
this.nav = nav = renderer.g().attr({ zIndex: 1 }).add(this.group);
10533
this.up = renderer.symbol('triangle', 0, 0, arrowSize, arrowSize)
10534
.on('click', function () {
10535
legend.scroll(-1, animation);
10538
this.pager = renderer.text('', 15, 10)
10539
.css(navOptions.style)
10541
this.down = renderer.symbol('triangle-down', 0, 0, arrowSize, arrowSize)
10542
.on('click', function () {
10543
legend.scroll(1, animation);
10548
// Set initial position
10551
legendHeight = spaceHeight;
10555
height: chart.chartHeight
10558
this.scrollGroup.attr({
10561
this.clipHeight = 0; // #1379
10564
return legendHeight;
10568
* Scroll the legend by a number of pages
10569
* @param {Object} scrollBy
10570
* @param {Object} animation
10572
scroll: function (scrollBy, animation) {
10573
var pages = this.pages,
10574
pageCount = pages.length,
10575
currentPage = this.currentPage + scrollBy,
10576
clipHeight = this.clipHeight,
10577
navOptions = this.options.navigation,
10578
activeColor = navOptions.activeColor,
10579
inactiveColor = navOptions.inactiveColor,
10580
pager = this.pager,
10581
padding = this.padding,
10584
// When resizing while looking at the last page
10585
if (currentPage > pageCount) {
10586
currentPage = pageCount;
10589
if (currentPage > 0) {
10591
if (animation !== UNDEFINED) {
10592
setAnimation(animation, this.chart);
10596
translateX: padding,
10597
translateY: clipHeight + this.padding + 7 + this.titleHeight,
10598
visibility: VISIBLE
10601
fill: currentPage === 1 ? inactiveColor : activeColor
10604
cursor: currentPage === 1 ? 'default' : 'pointer'
10607
text: currentPage + '/' + pageCount
10610
x: 18 + this.pager.getBBox().width, // adjust to text width
10611
fill: currentPage === pageCount ? inactiveColor : activeColor
10614
cursor: currentPage === pageCount ? 'default' : 'pointer'
10617
scrollOffset = -pages[currentPage - 1] + this.initialItemY;
10619
this.scrollGroup.animate({
10620
translateY: scrollOffset
10623
this.currentPage = currentPage;
10624
this.positionCheckboxes(scrollOffset);
10632
* LegendSymbolMixin
10635
var LegendSymbolMixin = Highcharts.LegendSymbolMixin = {
10638
* Get the series' symbol in the legend
10640
* @param {Object} legend The legend object
10641
* @param {Object} item The series (this) or point
10643
drawRectangle: function (legend, item) {
10644
var symbolHeight = legend.options.symbolHeight || 12;
10646
item.legendSymbol = this.chart.renderer.rect(
10648
legend.baseline - 5 - (symbolHeight / 2),
10649
legend.symbolWidth,
10651
pick(legend.options.symbolRadius, 2)
10654
}).add(item.legendGroup);
10659
* Get the series' symbol in the legend. This method should be overridable to create custom
10660
* symbols through Highcharts.seriesTypes[type].prototype.drawLegendSymbols.
10662
* @param {Object} legend The legend object
10664
drawLineMarker: function (legend) {
10666
var options = this.options,
10667
markerOptions = options.marker,
10669
legendOptions = legend.options,
10671
symbolWidth = legend.symbolWidth,
10672
renderer = this.chart.renderer,
10673
legendItemGroup = this.legendGroup,
10674
verticalCenter = legend.baseline - mathRound(renderer.fontMetrics(legendOptions.itemStyle.fontSize).b * 0.3),
10678
if (options.lineWidth) {
10680
'stroke-width': options.lineWidth
10682
if (options.dashStyle) {
10683
attr.dashstyle = options.dashStyle;
10685
this.legendLine = renderer.path([
10694
.add(legendItemGroup);
10698
if (markerOptions && markerOptions.enabled) {
10699
radius = markerOptions.radius;
10700
this.legendSymbol = legendSymbol = renderer.symbol(
10702
(symbolWidth / 2) - radius,
10703
verticalCenter - radius,
10707
.add(legendItemGroup);
10708
legendSymbol.isMarker = true;
10713
// Workaround for #2030, horizontal legend items not displaying in IE11 Preview,
10714
// and for #2580, a similar drawing flaw in Firefox 26.
10715
// TODO: Explore if there's a general cause for this. The problem may be related
10716
// to nested group elements, as the legend item texts are within 4 group elements.
10717
if (/Trident\/7\.0/.test(userAgent) || isFirefox) {
10718
wrap(Legend.prototype, 'positionItem', function (proceed, item) {
10720
runPositionItem = function () { // If chart destroyed in sync, this is undefined (#2030)
10721
if (item._legendItemPos) {
10722
proceed.call(legend, item);
10726
if (legend.chart.renderer.forExport) {
10729
setTimeout(runPositionItem);
10735
* @param {Object} options
10736
* @param {Function} callback Function to run when the chart has loaded
10739
this.init.apply(this, arguments);
10742
Chart.prototype = {
10745
* Initialize the chart
10747
init: function (userOptions, callback) {
10749
// Handle regular options
10751
seriesOptions = userOptions.series; // skip merging data points to increase performance
10753
userOptions.series = null;
10754
options = merge(defaultOptions, userOptions); // do the merge
10755
options.series = userOptions.series = seriesOptions; // set back the series data
10756
this.userOptions = userOptions;
10758
var optionsChart = options.chart;
10760
// Create margin & spacing array
10761
this.margin = this.splashArray('margin', optionsChart);
10762
this.spacing = this.splashArray('spacing', optionsChart);
10764
var chartEvents = optionsChart.events;
10766
//this.runChartClick = chartEvents && !!chartEvents.click;
10767
this.bounds = { h: {}, v: {} }; // Pixel data bounds for touch zoom
10769
this.callback = callback;
10770
this.isResizing = 0;
10771
this.options = options;
10772
//chartTitleOptions = UNDEFINED;
10773
//chartSubtitleOptions = UNDEFINED;
10777
this.hasCartesianSeries = optionsChart.showAxes;
10778
//this.axisOffset = UNDEFINED;
10779
//this.maxTicks = UNDEFINED; // handle the greatest amount of ticks on grouped axes
10780
//this.inverted = UNDEFINED;
10781
//this.loadingShown = UNDEFINED;
10782
//this.container = UNDEFINED;
10783
//this.chartWidth = UNDEFINED;
10784
//this.chartHeight = UNDEFINED;
10785
//this.marginRight = UNDEFINED;
10786
//this.marginBottom = UNDEFINED;
10787
//this.containerWidth = UNDEFINED;
10788
//this.containerHeight = UNDEFINED;
10789
//this.oldChartWidth = UNDEFINED;
10790
//this.oldChartHeight = UNDEFINED;
10792
//this.renderTo = UNDEFINED;
10793
//this.renderToClone = UNDEFINED;
10795
//this.spacingBox = UNDEFINED
10797
//this.legend = UNDEFINED;
10800
//this.chartBackground = UNDEFINED;
10801
//this.plotBackground = UNDEFINED;
10802
//this.plotBGImage = UNDEFINED;
10803
//this.plotBorder = UNDEFINED;
10804
//this.loadingDiv = UNDEFINED;
10805
//this.loadingSpan = UNDEFINED;
10810
// Add the chart to the global lookup
10811
chart.index = charts.length;
10812
charts.push(chart);
10814
// Set up auto resize
10815
if (optionsChart.reflow !== false) {
10816
addEvent(chart, 'load', function () {
10817
chart.initReflow();
10821
// Chart event handlers
10823
for (eventType in chartEvents) {
10824
addEvent(chart, eventType, chartEvents[eventType]);
10831
// Expose methods and variables
10832
chart.animation = useCanVG ? false : pick(optionsChart.animation, true);
10833
chart.pointCount = 0;
10834
chart.counters = new ChartCounters();
10836
chart.firstRender();
10840
* Initialize an individual series, called internally before render time
10842
initSeries: function (options) {
10844
optionsChart = chart.options.chart,
10845
type = options.type || optionsChart.type || optionsChart.defaultSeriesType,
10847
constr = seriesTypes[type];
10849
// No such series type
10854
series = new constr();
10855
series.init(this, options);
10860
* Check whether a given point is within the plot area
10862
* @param {Number} plotX Pixel x relative to the plot area
10863
* @param {Number} plotY Pixel y relative to the plot area
10864
* @param {Boolean} inverted Whether the chart is inverted
10866
isInsidePlot: function (plotX, plotY, inverted) {
10867
var x = inverted ? plotY : plotX,
10868
y = inverted ? plotX : plotY;
10871
x <= this.plotWidth &&
10873
y <= this.plotHeight;
10877
* Adjust all axes tick amounts
10879
adjustTickAmounts: function () {
10880
if (this.options.chart.alignTicks !== false) {
10881
each(this.axes, function (axis) {
10882
axis.adjustTickAmount();
10885
this.maxTicks = null;
10889
* Redraw legend, axes or series based on updated data
10891
* @param {Boolean|Object} animation Whether to apply animation, and optionally animation
10894
redraw: function (animation) {
10897
series = chart.series,
10898
pointer = chart.pointer,
10899
legend = chart.legend,
10900
redrawLegend = chart.isDirtyLegend,
10903
isDirtyBox = chart.isDirtyBox, // todo: check if it has actually changed?
10904
seriesLength = series.length,
10907
renderer = chart.renderer,
10908
isHiddenChart = renderer.isHidden(),
10911
setAnimation(animation, chart);
10913
if (isHiddenChart) {
10914
chart.cloneRenderTo();
10917
// Adjust title layout (reflow multiline text)
10918
chart.layOutTitles();
10920
// link stacked series
10924
if (serie.options.stacking) {
10925
hasStackedSeries = true;
10927
if (serie.isDirty) {
10928
hasDirtyStacks = true;
10933
if (hasDirtyStacks) { // mark others as dirty
10937
if (serie.options.stacking) {
10938
serie.isDirty = true;
10943
// handle updated data in the series
10944
each(series, function (serie) {
10945
if (serie.isDirty) { // prepare the data so axis can read it
10946
if (serie.options.legendType === 'point') {
10947
redrawLegend = true;
10952
// handle added or removed series
10953
if (redrawLegend && legend.options.enabled) { // series or pie points are added or removed
10954
// draw legend graphics
10957
chart.isDirtyLegend = false;
10961
if (hasStackedSeries) {
10966
if (chart.hasCartesianSeries) {
10967
if (!chart.isResizing) {
10970
chart.maxTicks = null;
10973
each(axes, function (axis) {
10978
chart.adjustTickAmounts();
10979
chart.getMargins();
10981
// If one axis is dirty, all axes must be redrawn (#792, #2169)
10982
each(axes, function (axis) {
10983
if (axis.isDirty) {
10989
each(axes, function (axis) {
10991
// Fire 'afterSetExtremes' only if extremes are set
10992
if (axis.isDirtyExtremes) { // #821
10993
axis.isDirtyExtremes = false;
10994
afterRedraw.push(function () { // prevent a recursive call to chart.redraw() (#1119)
10995
fireEvent(axis, 'afterSetExtremes', extend(axis.eventArgs, axis.getExtremes())); // #747, #751
10996
delete axis.eventArgs;
11000
if (isDirtyBox || hasStackedSeries) {
11007
// the plot areas size has changed
11009
chart.drawChartBox();
11013
// redraw affected series
11014
each(series, function (serie) {
11015
if (serie.isDirty && serie.visible &&
11016
(!serie.isCartesian || serie.xAxis)) { // issue #153
11021
// move tooltip or reset
11023
pointer.reset(true);
11026
// redraw if canvas
11030
fireEvent(chart, 'redraw'); // jQuery breaks this when calling it from addEvent. Overwrites chart.redraw
11032
if (isHiddenChart) {
11033
chart.cloneRenderTo(true);
11036
// Fire callbacks that are put on hold until after the redraw
11037
each(afterRedraw, function (callback) {
11043
* Get an axis, series or point object by id.
11044
* @param id {String} The id as given in the configuration options
11046
get: function (id) {
11049
series = chart.series;
11056
for (i = 0; i < axes.length; i++) {
11057
if (axes[i].options.id === id) {
11063
for (i = 0; i < series.length; i++) {
11064
if (series[i].options.id === id) {
11070
for (i = 0; i < series.length; i++) {
11071
points = series[i].points || [];
11072
for (j = 0; j < points.length; j++) {
11073
if (points[j].id === id) {
11082
* Create the Axis instances based on the config options
11084
getAxes: function () {
11086
options = this.options,
11087
xAxisOptions = options.xAxis = splat(options.xAxis || {}),
11088
yAxisOptions = options.yAxis = splat(options.yAxis || {}),
11092
// make sure the options are arrays and add some members
11093
each(xAxisOptions, function (axis, i) {
11098
each(yAxisOptions, function (axis, i) {
11102
// concatenate all axis options into one array
11103
optionsArray = xAxisOptions.concat(yAxisOptions);
11105
each(optionsArray, function (axisOptions) {
11106
axis = new Axis(chart, axisOptions);
11109
chart.adjustTickAmounts();
11114
* Get the currently selected points from all series
11116
getSelectedPoints: function () {
11118
each(this.series, function (serie) {
11119
points = points.concat(grep(serie.points || [], function (point) {
11120
return point.selected;
11127
* Get the currently selected series
11129
getSelectedSeries: function () {
11130
return grep(this.series, function (serie) {
11131
return serie.selected;
11136
* Generate stacks for each series and calculate stacks total values
11138
getStacks: function () {
11141
// reset stacks for each yAxis
11142
each(chart.yAxis, function (axis) {
11143
if (axis.stacks && axis.hasVisibleSeries) {
11144
axis.oldStacks = axis.stacks;
11148
each(chart.series, function (series) {
11149
if (series.options.stacking && (series.visible === true || chart.options.chart.ignoreHiddenSeries === false)) {
11150
series.stackKey = series.type + pick(series.options.stack, '');
11156
* Show the title and subtitle of the chart
11158
* @param titleOptions {Object} New title options
11159
* @param subtitleOptions {Object} New subtitle options
11162
setTitle: function (titleOptions, subtitleOptions, redraw) {
11164
options = chart.options,
11166
chartSubtitleOptions;
11168
chartTitleOptions = options.title = merge(options.title, titleOptions);
11169
chartSubtitleOptions = options.subtitle = merge(options.subtitle, subtitleOptions);
11171
// add title and subtitle
11173
['title', titleOptions, chartTitleOptions],
11174
['subtitle', subtitleOptions, chartSubtitleOptions]
11175
], function (arr) {
11177
title = chart[name],
11178
titleOptions = arr[1],
11179
chartTitleOptions = arr[2];
11181
if (title && titleOptions) {
11182
chart[name] = title = title.destroy(); // remove old
11185
if (chartTitleOptions && chartTitleOptions.text && !title) {
11186
chart[name] = chart.renderer.text(
11187
chartTitleOptions.text,
11190
chartTitleOptions.useHTML
11193
align: chartTitleOptions.align,
11194
'class': PREFIX + name,
11195
zIndex: chartTitleOptions.zIndex || 4
11197
.css(chartTitleOptions.style)
11201
chart.layOutTitles(redraw);
11205
* Lay out the chart titles and cache the full offset height for use in getMargins
11207
layOutTitles: function (redraw) {
11208
var titleOffset = 0,
11209
title = this.title,
11210
subtitle = this.subtitle,
11211
options = this.options,
11212
titleOptions = options.title,
11213
subtitleOptions = options.subtitle,
11215
autoWidth = this.spacingBox.width - 44; // 44 makes room for default context button
11219
.css({ width: (titleOptions.width || autoWidth) + PX })
11220
.align(extend({ y: 15 }, titleOptions), false, 'spacingBox');
11222
if (!titleOptions.floating && !titleOptions.verticalAlign) {
11223
titleOffset = title.getBBox().height;
11225
// Adjust for browser consistency + backwards compat after #776 fix
11226
if (titleOffset >= 18 && titleOffset <= 25) {
11233
.css({ width: (subtitleOptions.width || autoWidth) + PX })
11234
.align(extend({ y: titleOffset + titleOptions.margin }, subtitleOptions), false, 'spacingBox');
11236
if (!subtitleOptions.floating && !subtitleOptions.verticalAlign) {
11237
titleOffset = mathCeil(titleOffset + subtitle.getBBox().height);
11241
requiresDirtyBox = this.titleOffset !== titleOffset;
11242
this.titleOffset = titleOffset; // used in getMargins
11244
if (!this.isDirtyBox && requiresDirtyBox) {
11245
this.isDirtyBox = requiresDirtyBox;
11246
// Redraw if necessary (#2719, #2744)
11247
if (this.hasRendered && pick(redraw, true) && this.isDirtyBox) {
11254
* Get chart width and height according to options and container size
11256
getChartSize: function () {
11258
optionsChart = chart.options.chart,
11259
widthOption = optionsChart.width,
11260
heightOption = optionsChart.height,
11261
renderTo = chart.renderToClone || chart.renderTo;
11263
// get inner width and height from jQuery (#824)
11264
if (!defined(widthOption)) {
11265
chart.containerWidth = adapterRun(renderTo, 'width');
11267
if (!defined(heightOption)) {
11268
chart.containerHeight = adapterRun(renderTo, 'height');
11271
chart.chartWidth = mathMax(0, widthOption || chart.containerWidth || 600); // #1393, 1460
11272
chart.chartHeight = mathMax(0, pick(heightOption,
11273
// the offsetHeight of an empty container is 0 in standard browsers, but 19 in IE7:
11274
chart.containerHeight > 19 ? chart.containerHeight : 400));
11278
* Create a clone of the chart's renderTo div and place it outside the viewport to allow
11279
* size computation on chart.render and chart.redraw
11281
cloneRenderTo: function (revert) {
11282
var clone = this.renderToClone,
11283
container = this.container;
11285
// Destroy the clone and bring the container back to the real renderTo div
11288
this.renderTo.appendChild(container);
11289
discardElement(clone);
11290
delete this.renderToClone;
11293
// Set up the clone
11295
if (container && container.parentNode === this.renderTo) {
11296
this.renderTo.removeChild(container); // do not clone this
11298
this.renderToClone = clone = this.renderTo.cloneNode(0);
11300
position: ABSOLUTE,
11302
display: 'block' // #833
11304
if (clone.style.setProperty) { // #2631
11305
clone.style.setProperty('display', 'block', 'important');
11307
doc.body.appendChild(clone);
11309
clone.appendChild(container);
11315
* Get the containing element, determine the size and create the inner container
11316
* div to hold the chart
11318
getContainer: function () {
11321
optionsChart = chart.options.chart,
11325
indexAttrName = 'data-highcharts-chart',
11329
chart.renderTo = renderTo = optionsChart.renderTo;
11330
containerId = PREFIX + idCounter++;
11332
if (isString(renderTo)) {
11333
chart.renderTo = renderTo = doc.getElementById(renderTo);
11336
// Display an error if the renderTo is wrong
11341
// If the container already holds a chart, destroy it. The check for hasRendered is there
11342
// because web pages that are saved to disk from the browser, will preserve the data-highcharts-chart
11343
// attribute and the SVG contents, but not an interactive chart. So in this case,
11344
// charts[oldChartIndex] will point to the wrong chart if any (#2609).
11345
oldChartIndex = pInt(attr(renderTo, indexAttrName));
11346
if (!isNaN(oldChartIndex) && charts[oldChartIndex] && charts[oldChartIndex].hasRendered) {
11347
charts[oldChartIndex].destroy();
11350
// Make a reference to the chart from the div
11351
attr(renderTo, indexAttrName, chart.index);
11353
// remove previous chart
11354
renderTo.innerHTML = '';
11356
// If the container doesn't have an offsetWidth, it has or is a child of a node
11357
// that has display:none. We need to temporarily move it out to a visible
11358
// state to determine the size, else the legend and tooltips won't render
11359
// properly. The allowClone option is used in sparklines as a micro optimization,
11360
// saving about 1-2 ms each chart.
11361
if (!optionsChart.skipClone && !renderTo.offsetWidth) {
11362
chart.cloneRenderTo();
11365
// get the width and height
11366
chart.getChartSize();
11367
chartWidth = chart.chartWidth;
11368
chartHeight = chart.chartHeight;
11370
// create the inner container
11371
chart.container = container = createElement(DIV, {
11372
className: PREFIX + 'container' +
11373
(optionsChart.className ? ' ' + optionsChart.className : ''),
11376
position: RELATIVE,
11377
overflow: HIDDEN, // needed for context menu (avoid scrollbars) and
11378
// content overflow in IE
11379
width: chartWidth + PX,
11380
height: chartHeight + PX,
11382
lineHeight: 'normal', // #427
11383
zIndex: 0, // #1072
11384
'-webkit-tap-highlight-color': 'rgba(0,0,0,0)'
11385
}, optionsChart.style),
11386
chart.renderToClone || renderTo
11389
// cache the cursor (#1650)
11390
chart._cursor = container.style.cursor;
11392
// Initialize the renderer
11394
optionsChart.forExport ? // force SVG, used for SVG export
11395
new SVGRenderer(container, chartWidth, chartHeight, optionsChart.style, true) :
11396
new Renderer(container, chartWidth, chartHeight, optionsChart.style);
11399
// If we need canvg library, extend and configure the renderer
11400
// to get the tracker for translating mouse events
11401
chart.renderer.create(chart, container, chartWidth, chartHeight);
11406
* Calculate margins by rendering axis labels in a preliminary position. Title,
11407
* subtitle and legend have already been rendered at this stage, but will be
11408
* moved into their final positions
11410
getMargins: function () {
11412
spacing = chart.spacing,
11414
legend = chart.legend,
11415
margin = chart.margin,
11416
legendOptions = chart.options.legend,
11417
legendMargin = pick(legendOptions.margin, 10),
11418
legendX = legendOptions.x,
11419
legendY = legendOptions.y,
11420
align = legendOptions.align,
11421
verticalAlign = legendOptions.verticalAlign,
11422
titleOffset = chart.titleOffset;
11424
chart.resetMargins();
11425
axisOffset = chart.axisOffset;
11427
// Adjust for title and subtitle
11428
if (titleOffset && !defined(margin[0])) {
11429
chart.plotTop = mathMax(chart.plotTop, titleOffset + chart.options.title.margin + spacing[0]);
11432
// Adjust for legend
11433
if (legend.display && !legendOptions.floating) {
11434
if (align === 'right') { // horizontal alignment handled first
11435
if (!defined(margin[1])) {
11436
chart.marginRight = mathMax(
11438
legend.legendWidth - legendX + legendMargin + spacing[1]
11441
} else if (align === 'left') {
11442
if (!defined(margin[3])) {
11443
chart.plotLeft = mathMax(
11445
legend.legendWidth + legendX + legendMargin + spacing[3]
11449
} else if (verticalAlign === 'top') {
11450
if (!defined(margin[0])) {
11451
chart.plotTop = mathMax(
11453
legend.legendHeight + legendY + legendMargin + spacing[0]
11457
} else if (verticalAlign === 'bottom') {
11458
if (!defined(margin[2])) {
11459
chart.marginBottom = mathMax(
11460
chart.marginBottom,
11461
legend.legendHeight - legendY + legendMargin + spacing[2]
11467
// adjust for scroller
11468
if (chart.extraBottomMargin) {
11469
chart.marginBottom += chart.extraBottomMargin;
11471
if (chart.extraTopMargin) {
11472
chart.plotTop += chart.extraTopMargin;
11475
// pre-render axes to get labels offset width
11476
if (chart.hasCartesianSeries) {
11477
each(chart.axes, function (axis) {
11482
if (!defined(margin[3])) {
11483
chart.plotLeft += axisOffset[3];
11485
if (!defined(margin[0])) {
11486
chart.plotTop += axisOffset[0];
11488
if (!defined(margin[2])) {
11489
chart.marginBottom += axisOffset[2];
11491
if (!defined(margin[1])) {
11492
chart.marginRight += axisOffset[1];
11495
chart.setChartSize();
11500
* Resize the chart to its container if size is not explicitly set
11502
reflow: function (e) {
11504
optionsChart = chart.options.chart,
11505
renderTo = chart.renderTo,
11506
width = optionsChart.width || adapterRun(renderTo, 'width'),
11507
height = optionsChart.height || adapterRun(renderTo, 'height'),
11508
target = e ? e.target : win, // #805 - MooTools doesn't supply e
11509
doReflow = function () {
11510
if (chart.container) { // It may have been destroyed in the meantime (#1257)
11511
chart.setSize(width, height, false);
11512
chart.hasUserSize = null;
11516
// Width and height checks for display:none. Target is doc in IE8 and Opera,
11517
// win in Firefox, Chrome and IE9.
11518
if (!chart.hasUserSize && width && height && (target === win || target === doc)) {
11519
if (width !== chart.containerWidth || height !== chart.containerHeight) {
11520
clearTimeout(chart.reflowTimeout);
11521
if (e) { // Called from window.resize
11522
chart.reflowTimeout = setTimeout(doReflow, 100);
11523
} else { // Called directly (#2224)
11527
chart.containerWidth = width;
11528
chart.containerHeight = height;
11533
* Add the event handlers necessary for auto resizing
11535
initReflow: function () {
11537
reflow = function (e) {
11542
addEvent(win, 'resize', reflow);
11543
addEvent(chart, 'destroy', function () {
11544
removeEvent(win, 'resize', reflow);
11549
* Resize the chart to a given width and height
11550
* @param {Number} width
11551
* @param {Number} height
11552
* @param {Object|Boolean} animation
11554
setSize: function (width, height, animation) {
11560
// Handle the isResizing counter
11561
chart.isResizing += 1;
11562
fireEndResize = function () {
11564
fireEvent(chart, 'endResize', null, function () {
11565
chart.isResizing -= 1;
11570
// set the animation for the current process
11571
setAnimation(animation, chart);
11573
chart.oldChartHeight = chart.chartHeight;
11574
chart.oldChartWidth = chart.chartWidth;
11575
if (defined(width)) {
11576
chart.chartWidth = chartWidth = mathMax(0, mathRound(width));
11577
chart.hasUserSize = !!chartWidth;
11579
if (defined(height)) {
11580
chart.chartHeight = chartHeight = mathMax(0, mathRound(height));
11583
// Resize the container with the global animation applied if enabled (#2503)
11584
(globalAnimation ? animate : css)(chart.container, {
11585
width: chartWidth + PX,
11586
height: chartHeight + PX
11587
}, globalAnimation);
11589
chart.setChartSize(true);
11590
chart.renderer.setSize(chartWidth, chartHeight, animation);
11593
chart.maxTicks = null;
11594
each(chart.axes, function (axis) {
11595
axis.isDirty = true;
11599
// make sure non-cartesian series are also handled
11600
each(chart.series, function (serie) {
11601
serie.isDirty = true;
11604
chart.isDirtyLegend = true; // force legend redraw
11605
chart.isDirtyBox = true; // force redraw of plot and chart border
11607
chart.getMargins();
11609
chart.redraw(animation);
11612
chart.oldChartHeight = null;
11613
fireEvent(chart, 'resize');
11615
// fire endResize and set isResizing back
11616
// If animation is disabled, fire without delay
11617
if (globalAnimation === false) {
11619
} else { // else set a timeout with the animation duration
11620
setTimeout(fireEndResize, (globalAnimation && globalAnimation.duration) || 500);
11625
* Set the public chart properties. This is done before and after the pre-render
11626
* to determine margin sizes
11628
setChartSize: function (skipAxes) {
11630
inverted = chart.inverted,
11631
renderer = chart.renderer,
11632
chartWidth = chart.chartWidth,
11633
chartHeight = chart.chartHeight,
11634
optionsChart = chart.options.chart,
11635
spacing = chart.spacing,
11636
clipOffset = chart.clipOffset,
11645
chart.plotLeft = plotLeft = mathRound(chart.plotLeft);
11646
chart.plotTop = plotTop = mathRound(chart.plotTop);
11647
chart.plotWidth = plotWidth = mathMax(0, mathRound(chartWidth - plotLeft - chart.marginRight));
11648
chart.plotHeight = plotHeight = mathMax(0, mathRound(chartHeight - plotTop - chart.marginBottom));
11650
chart.plotSizeX = inverted ? plotHeight : plotWidth;
11651
chart.plotSizeY = inverted ? plotWidth : plotHeight;
11653
chart.plotBorderWidth = optionsChart.plotBorderWidth || 0;
11655
// Set boxes used for alignment
11656
chart.spacingBox = renderer.spacingBox = {
11659
width: chartWidth - spacing[3] - spacing[1],
11660
height: chartHeight - spacing[0] - spacing[2]
11662
chart.plotBox = renderer.plotBox = {
11669
plotBorderWidth = 2 * mathFloor(chart.plotBorderWidth / 2);
11670
clipX = mathCeil(mathMax(plotBorderWidth, clipOffset[3]) / 2);
11671
clipY = mathCeil(mathMax(plotBorderWidth, clipOffset[0]) / 2);
11675
width: mathFloor(chart.plotSizeX - mathMax(plotBorderWidth, clipOffset[1]) / 2 - clipX),
11676
height: mathFloor(chart.plotSizeY - mathMax(plotBorderWidth, clipOffset[2]) / 2 - clipY)
11680
each(chart.axes, function (axis) {
11681
axis.setAxisSize();
11682
axis.setAxisTranslation();
11688
* Initial margins before auto size margins are applied
11690
resetMargins: function () {
11692
spacing = chart.spacing,
11693
margin = chart.margin;
11695
chart.plotTop = pick(margin[0], spacing[0]);
11696
chart.marginRight = pick(margin[1], spacing[1]);
11697
chart.marginBottom = pick(margin[2], spacing[2]);
11698
chart.plotLeft = pick(margin[3], spacing[3]);
11699
chart.axisOffset = [0, 0, 0, 0]; // top, right, bottom, left
11700
chart.clipOffset = [0, 0, 0, 0];
11704
* Draw the borders and backgrounds for chart and plot area
11706
drawChartBox: function () {
11708
optionsChart = chart.options.chart,
11709
renderer = chart.renderer,
11710
chartWidth = chart.chartWidth,
11711
chartHeight = chart.chartHeight,
11712
chartBackground = chart.chartBackground,
11713
plotBackground = chart.plotBackground,
11714
plotBorder = chart.plotBorder,
11715
plotBGImage = chart.plotBGImage,
11716
chartBorderWidth = optionsChart.borderWidth || 0,
11717
chartBackgroundColor = optionsChart.backgroundColor,
11718
plotBackgroundColor = optionsChart.plotBackgroundColor,
11719
plotBackgroundImage = optionsChart.plotBackgroundImage,
11720
plotBorderWidth = optionsChart.plotBorderWidth || 0,
11723
plotLeft = chart.plotLeft,
11724
plotTop = chart.plotTop,
11725
plotWidth = chart.plotWidth,
11726
plotHeight = chart.plotHeight,
11727
plotBox = chart.plotBox,
11728
clipRect = chart.clipRect,
11729
clipBox = chart.clipBox;
11732
mgn = chartBorderWidth + (optionsChart.shadow ? 8 : 0);
11734
if (chartBorderWidth || chartBackgroundColor) {
11735
if (!chartBackground) {
11738
fill: chartBackgroundColor || NONE
11740
if (chartBorderWidth) { // #980
11741
bgAttr.stroke = optionsChart.borderColor;
11742
bgAttr['stroke-width'] = chartBorderWidth;
11744
chart.chartBackground = renderer.rect(mgn / 2, mgn / 2, chartWidth - mgn, chartHeight - mgn,
11745
optionsChart.borderRadius, chartBorderWidth)
11747
.addClass(PREFIX + 'background')
11749
.shadow(optionsChart.shadow);
11752
chartBackground.animate(
11753
chartBackground.crisp({ width: chartWidth - mgn, height: chartHeight - mgn })
11760
if (plotBackgroundColor) {
11761
if (!plotBackground) {
11762
chart.plotBackground = renderer.rect(plotLeft, plotTop, plotWidth, plotHeight, 0)
11764
fill: plotBackgroundColor
11767
.shadow(optionsChart.plotShadow);
11769
plotBackground.animate(plotBox);
11772
if (plotBackgroundImage) {
11773
if (!plotBGImage) {
11774
chart.plotBGImage = renderer.image(plotBackgroundImage, plotLeft, plotTop, plotWidth, plotHeight)
11777
plotBGImage.animate(plotBox);
11783
chart.clipRect = renderer.clipRect(clipBox);
11786
width: clipBox.width,
11787
height: clipBox.height
11791
// Plot area border
11792
if (plotBorderWidth) {
11794
chart.plotBorder = renderer.rect(plotLeft, plotTop, plotWidth, plotHeight, 0, -plotBorderWidth)
11796
stroke: optionsChart.plotBorderColor,
11797
'stroke-width': plotBorderWidth,
11803
plotBorder.animate(
11804
plotBorder.crisp({ x: plotLeft, y: plotTop, width: plotWidth, height: plotHeight })
11810
chart.isDirtyBox = false;
11814
* Detect whether a certain chart property is needed based on inspecting its options
11815
* and series. This mainly applies to the chart.invert property, and in extensions to
11816
* the chart.angular and chart.polar properties.
11818
propFromSeries: function () {
11820
optionsChart = chart.options.chart,
11822
seriesOptions = chart.options.series,
11827
each(['inverted', 'angular', 'polar'], function (key) {
11829
// The default series type's class
11830
klass = seriesTypes[optionsChart.type || optionsChart.defaultSeriesType];
11832
// Get the value from available chart-wide properties
11834
chart[key] || // 1. it is set before
11835
optionsChart[key] || // 2. it is set in the options
11836
(klass && klass.prototype[key]) // 3. it's default series class requires it
11839
// 4. Check if any the chart's series require it
11840
i = seriesOptions && seriesOptions.length;
11841
while (!value && i--) {
11842
klass = seriesTypes[seriesOptions[i].type];
11843
if (klass && klass.prototype[key]) {
11848
// Set the chart property
11849
chart[key] = value;
11855
* Link two or more series together. This is done initially from Chart.render,
11856
* and after Chart.addSeries and Series.remove.
11858
linkSeries: function () {
11860
chartSeries = chart.series;
11863
each(chartSeries, function (series) {
11864
series.linkedSeries.length = 0;
11868
each(chartSeries, function (series) {
11869
var linkedTo = series.options.linkedTo;
11870
if (isString(linkedTo)) {
11871
if (linkedTo === ':previous') {
11872
linkedTo = chart.series[series.index - 1];
11874
linkedTo = chart.get(linkedTo);
11877
linkedTo.linkedSeries.push(series);
11878
series.linkedParent = linkedTo;
11885
* Render series for the chart
11887
renderSeries: function () {
11888
each(this.series, function (serie) {
11890
if (serie.setTooltipPoints) {
11891
serie.setTooltipPoints();
11898
* Render all graphics for the chart
11900
render: function () {
11903
renderer = chart.renderer,
11904
options = chart.options;
11906
var labels = options.labels,
11907
credits = options.credits,
11915
chart.legend = new Legend(chart, options.legend);
11917
chart.getStacks(); // render stacks
11919
// Get margins by pre-rendering axes
11921
each(axes, function (axis) {
11925
chart.getMargins();
11927
chart.maxTicks = null; // reset for second pass
11928
each(axes, function (axis) {
11929
axis.setTickPositions(true); // update to reflect the new margins
11930
axis.setMaxTicks();
11932
chart.adjustTickAmounts();
11933
chart.getMargins(); // second pass to check for new labels
11936
// Draw the borders and backgrounds
11937
chart.drawChartBox();
11941
if (chart.hasCartesianSeries) {
11942
each(axes, function (axis) {
11948
if (!chart.seriesGroup) {
11949
chart.seriesGroup = renderer.g('series-group')
11950
.attr({ zIndex: 3 })
11953
chart.renderSeries();
11956
if (labels.items) {
11957
each(labels.items, function (label) {
11958
var style = extend(labels.style, label.style),
11959
x = pInt(style.left) + chart.plotLeft,
11960
y = pInt(style.top) + chart.plotTop + 12;
11962
// delete to prevent rewriting in IE
11971
.attr({ zIndex: 2 })
11979
if (credits.enabled && !chart.credits) {
11980
creditsHref = credits.href;
11981
chart.credits = renderer.text(
11986
.on('click', function () {
11988
location.href = creditsHref;
11992
align: credits.position.align,
11995
.css(credits.style)
11997
.align(credits.position);
12001
chart.hasRendered = true;
12006
* Clean up memory usage
12008
destroy: function () {
12011
series = chart.series,
12012
container = chart.container,
12014
parentNode = container && container.parentNode;
12016
// fire the chart.destoy event
12017
fireEvent(chart, 'destroy');
12019
// Delete the chart from charts lookup array
12020
charts[chart.index] = UNDEFINED;
12021
chart.renderTo.removeAttribute('data-highcharts-chart');
12024
removeEvent(chart);
12026
// ==== Destroy collections:
12030
axes[i] = axes[i].destroy();
12033
// Destroy each series
12036
series[i] = series[i].destroy();
12039
// ==== Destroy chart properties:
12040
each(['title', 'subtitle', 'chartBackground', 'plotBackground', 'plotBGImage',
12041
'plotBorder', 'seriesGroup', 'clipRect', 'credits', 'pointer', 'scroller',
12042
'rangeSelector', 'legend', 'resetZoomButton', 'tooltip', 'renderer'], function (name) {
12043
var prop = chart[name];
12045
if (prop && prop.destroy) {
12046
chart[name] = prop.destroy();
12050
// remove container and all SVG
12051
if (container) { // can break in IE when destroyed before finished loading
12052
container.innerHTML = '';
12053
removeEvent(container);
12055
discardElement(container);
12069
* VML namespaces can't be added until after complete. Listening
12070
* for Perini's doScroll hack is not enough.
12072
isReadyToRender: function () {
12075
// Note: in spite of JSLint's complaints, win == win.top is required
12076
/*jslint eqeq: true*/
12077
if ((!hasSVG && (win == win.top && doc.readyState !== 'complete')) || (useCanVG && !win.canvg)) {
12078
/*jslint eqeq: false*/
12080
// Delay rendering until canvg library is downloaded and ready
12081
CanVGController.push(function () { chart.firstRender(); }, chart.options.global.canvasToolsURL);
12083
doc.attachEvent('onreadystatechange', function () {
12084
doc.detachEvent('onreadystatechange', chart.firstRender);
12085
if (doc.readyState === 'complete') {
12086
chart.firstRender();
12096
* Prepare for first rendering after all data are loaded
12098
firstRender: function () {
12100
options = chart.options,
12101
callback = chart.callback;
12103
// Check whether the chart is ready to render
12104
if (!chart.isReadyToRender()) {
12108
// Create the container
12109
chart.getContainer();
12111
// Run an early event after the container and renderer are established
12112
fireEvent(chart, 'init');
12115
chart.resetMargins();
12116
chart.setChartSize();
12118
// Set the common chart properties (mainly invert) from the given series
12119
chart.propFromSeries();
12124
// Initialize the series
12125
each(options.series || [], function (serieOptions) {
12126
chart.initSeries(serieOptions);
12129
chart.linkSeries();
12131
// Run an event after axes and series are initialized, but before render. At this stage,
12132
// the series data is indexed and cached in the xData and yData arrays, so we can access
12133
// those before rendering. Used in Highstock.
12134
fireEvent(chart, 'beforeRender');
12136
// depends on inverted and on margins being set
12137
if (Highcharts.Pointer) {
12138
chart.pointer = new Pointer(chart, options);
12144
chart.renderer.draw();
12147
callback.apply(chart, [chart]);
12149
each(chart.callbacks, function (fn) {
12150
fn.apply(chart, [chart]);
12154
// If the chart was rendered outside the top container, put it back in
12155
chart.cloneRenderTo(true);
12157
fireEvent(chart, 'load');
12162
* Creates arrays for spacing and margin from given options.
12164
splashArray: function (target, options) {
12165
var oVar = options[target],
12166
tArray = isObject(oVar) ? oVar : [oVar, oVar, oVar, oVar];
12168
return [pick(options[target + 'Top'], tArray[0]),
12169
pick(options[target + 'Right'], tArray[1]),
12170
pick(options[target + 'Bottom'], tArray[2]),
12171
pick(options[target + 'Left'], tArray[3])];
12175
// Hook for exporting module
12176
Chart.prototype.callbacks = [];
12178
var CenteredSeriesMixin = Highcharts.CenteredSeriesMixin = {
12180
* Get the center of the pie based on the size and center options relative to the
12181
* plot area. Borrowed by the polar and gauge series types.
12183
getCenter: function () {
12185
var options = this.options,
12186
chart = this.chart,
12187
slicingRoom = 2 * (options.slicedOffset || 0),
12189
plotWidth = chart.plotWidth - 2 * slicingRoom,
12190
plotHeight = chart.plotHeight - 2 * slicingRoom,
12191
centerOption = options.center,
12192
positions = [pick(centerOption[0], '50%'), pick(centerOption[1], '50%'), options.size || '100%', options.innerSize || 0],
12193
smallestSize = mathMin(plotWidth, plotHeight),
12196
return map(positions, function (length, i) {
12197
isPercent = /%$/.test(length);
12198
handleSlicingRoom = i < 2 || (i === 2 && isPercent);
12199
return (isPercent ?
12200
// i == 0: centerX, relative to width
12201
// i == 1: centerY, relative to height
12202
// i == 2: size, relative to smallestSize
12203
// i == 4: innerSize, relative to smallestSize
12204
[plotWidth, plotHeight, smallestSize, smallestSize][i] *
12205
pInt(length) / 100 :
12206
length) + (handleSlicingRoom ? slicingRoom : 0);
12212
* The Point object and prototype. Inheritable and used as base for PiePoint
12214
var Point = function () {};
12215
Point.prototype = {
12218
* Initialize the point
12219
* @param {Object} series The series object containing this point
12220
* @param {Object} options The data in either number, array or object format
12222
init: function (series, options, x) {
12226
point.series = series;
12227
point.applyOptions(options, x);
12228
point.pointAttr = {};
12230
if (series.options.colorByPoint) {
12231
colors = series.options.colors || series.chart.options.colors;
12232
point.color = point.color || colors[series.colorCounter++];
12233
// loop back to zero
12234
if (series.colorCounter === colors.length) {
12235
series.colorCounter = 0;
12239
series.chart.pointCount++;
12243
* Apply the options containing the x and y data and possible some extra properties.
12244
* This is called on point init or from point.update.
12246
* @param {Object} options
12248
applyOptions: function (options, x) {
12250
series = point.series,
12251
pointValKey = series.pointValKey;
12253
options = Point.prototype.optionsToObject.call(this, options);
12255
// copy options directly to point
12256
extend(point, options);
12257
point.options = point.options ? extend(point.options, options) : options;
12259
// For higher dimension series types. For instance, for ranges, point.y is mapped to point.low.
12261
point.y = point[pointValKey];
12264
// If no x is set by now, get auto incremented value. All points must have an
12265
// x value, however the y value can be null to create a gap in the series
12266
if (point.x === UNDEFINED && series) {
12267
point.x = x === UNDEFINED ? series.autoIncrement() : x;
12274
* Transform number or array configs into objects
12276
optionsToObject: function (options) {
12278
series = this.series,
12279
pointArrayMap = series.pointArrayMap || ['y'],
12280
valueCount = pointArrayMap.length,
12285
if (typeof options === 'number' || options === null) {
12286
ret[pointArrayMap[0]] = options;
12288
} else if (isArray(options)) {
12289
// with leading x value
12290
if (options.length > valueCount) {
12291
firstItemType = typeof options[0];
12292
if (firstItemType === 'string') {
12293
ret.name = options[0];
12294
} else if (firstItemType === 'number') {
12295
ret.x = options[0];
12299
while (j < valueCount) {
12300
ret[pointArrayMap[j++]] = options[i++];
12302
} else if (typeof options === 'object') {
12305
// This is the fastest way to detect if there are individual point dataLabels that need
12306
// to be considered in drawDataLabels. These can only occur in object configs.
12307
if (options.dataLabels) {
12308
series._hasPointLabels = true;
12311
// Same approach as above for markers
12312
if (options.marker) {
12313
series._hasPointMarkers = true;
12320
* Destroy a point to clear memory. Its reference still stays in series.data.
12322
destroy: function () {
12324
series = point.series,
12325
chart = series.chart,
12326
hoverPoints = chart.hoverPoints,
12329
chart.pointCount--;
12333
erase(hoverPoints, point);
12334
if (!hoverPoints.length) {
12335
chart.hoverPoints = null;
12339
if (point === chart.hoverPoint) {
12340
point.onMouseOut();
12343
// remove all events
12344
if (point.graphic || point.dataLabel) { // removeEvent and destroyElements are performance expensive
12345
removeEvent(point);
12346
point.destroyElements();
12349
if (point.legendItem) { // pies have legend items
12350
chart.legend.destroyItem(point);
12353
for (prop in point) {
12354
point[prop] = null;
12361
* Destroy SVG elements associated with the point
12363
destroyElements: function () {
12365
props = ['graphic', 'dataLabel', 'dataLabelUpper', 'group', 'connector', 'shadowGroup'],
12371
point[prop] = point[prop].destroy();
12377
* Return the configuration hash needed for the data label and tooltip formatters
12379
getLabelConfig: function () {
12384
key: point.name || point.category,
12385
series: point.series,
12387
percentage: point.percentage,
12388
total: point.total || point.stackTotal
12393
* Extendable method for formatting each point's tooltip line
12395
* @return {String} A string to be concatenated in to the common tooltip text
12397
tooltipFormatter: function (pointFormat) {
12399
// Insert options for valueDecimals, valuePrefix, and valueSuffix
12400
var series = this.series,
12401
seriesTooltipOptions = series.tooltipOptions,
12402
valueDecimals = pick(seriesTooltipOptions.valueDecimals, ''),
12403
valuePrefix = seriesTooltipOptions.valuePrefix || '',
12404
valueSuffix = seriesTooltipOptions.valueSuffix || '';
12406
// Loop over the point array map and replace unformatted values with sprintf formatting markup
12407
each(series.pointArrayMap || ['y'], function (key) {
12408
key = '{point.' + key; // without the closing bracket
12409
if (valuePrefix || valueSuffix) {
12410
pointFormat = pointFormat.replace(key + '}', valuePrefix + key + '}' + valueSuffix);
12412
pointFormat = pointFormat.replace(key + '}', key + ':,.' + valueDecimals + 'f}');
12415
return format(pointFormat, {
12417
series: this.series
12421
* @classDescription The base function which all other series types inherit from. The data in the series is stored
12422
* in various arrays.
12424
* - First, series.options.data contains all the original config options for
12425
* each point whether added by options or methods like series.addPoint.
12426
* - Next, series.data contains those values converted to points, but in case the series data length
12427
* exceeds the cropThreshold, or if the data is grouped, series.data doesn't contain all the points. It
12428
* only contains the points that have been created on demand.
12429
* - Then there's series.points that contains all currently visible point objects. In case of cropping,
12430
* the cropped-away points are not part of this array. The series.points array starts at series.cropStart
12431
* compared to series.data and series.options.data. If however the series data is grouped, these can't
12432
* be correlated one to one.
12433
* - series.xData and series.processedXData contain clean x values, equivalent to series.data and series.points.
12434
* - series.yData and series.processedYData contain clean x values, equivalent to series.data and series.points.
12436
* @param {Object} chart
12437
* @param {Object} options
12439
var Series = function () {};
12441
Series.prototype = {
12446
sorted: true, // requires the data to be sorted
12447
requireSorting: true,
12448
pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
12449
stroke: 'lineColor',
12450
'stroke-width': 'lineWidth',
12454
axisTypes: ['xAxis', 'yAxis'],
12456
parallelArrays: ['x', 'y'], // each point's x and y values are stored in this.xData and this.yData
12457
init: function (chart, options) {
12461
chartSeries = chart.series,
12462
sortByIndex = function (a, b) {
12463
return pick(a.options.index, a._i) - pick(b.options.index, b._i);
12466
series.chart = chart;
12467
series.options = options = series.setOptions(options); // merge with plotOptions
12468
series.linkedSeries = [];
12473
// set some variables
12475
name: options.name,
12476
state: NORMAL_STATE,
12478
visible: options.visible !== false, // true by default
12479
selected: options.selected === true // false by default
12484
options.animation = false;
12487
// register event listeners
12488
events = options.events;
12489
for (eventType in events) {
12490
addEvent(series, eventType, events[eventType]);
12493
(events && events.click) ||
12494
(options.point && options.point.events && options.point.events.click) ||
12495
options.allowPointSelect
12497
chart.runTrackerClick = true;
12501
series.getSymbol();
12504
each(series.parallelArrays, function (key) {
12505
series[key + 'Data'] = [];
12507
series.setData(options.data, false);
12510
if (series.isCartesian) {
12511
chart.hasCartesianSeries = true;
12514
// Register it in the chart
12515
chartSeries.push(series);
12516
series._i = chartSeries.length - 1;
12518
// Sort series according to index option (#248, #1123, #2456)
12519
stableSort(chartSeries, sortByIndex);
12521
stableSort(this.yAxis.series, sortByIndex);
12524
each(chartSeries, function (series, i) {
12526
series.name = series.name || 'Series ' + (i + 1);
12532
* Set the xAxis and yAxis properties of cartesian series, and register the series
12533
* in the axis.series array
12535
bindAxes: function () {
12537
seriesOptions = series.options,
12538
chart = series.chart,
12541
each(series.axisTypes || [], function (AXIS) { // repeat for xAxis and yAxis
12543
each(chart[AXIS], function (axis) { // loop through the chart's axis objects
12544
axisOptions = axis.options;
12546
// apply if the series xAxis or yAxis option mathches the number of the
12547
// axis, or if undefined, use the first axis
12548
if ((seriesOptions[AXIS] === axisOptions.index) ||
12549
(seriesOptions[AXIS] !== UNDEFINED && seriesOptions[AXIS] === axisOptions.id) ||
12550
(seriesOptions[AXIS] === UNDEFINED && axisOptions.index === 0)) {
12552
// register this series in the axis.series lookup
12553
axis.series.push(series);
12555
// set this series.xAxis or series.yAxis reference
12556
series[AXIS] = axis;
12558
// mark dirty for redraw
12559
axis.isDirty = true;
12563
// The series needs an X and an Y axis
12564
if (!series[AXIS] && series.optionalAxis !== AXIS) {
12572
* For simple series types like line and column, the data values are held in arrays like
12573
* xData and yData for quick lookup to find extremes and more. For multidimensional series
12574
* like bubble and map, this can be extended with arrays like zData and valueData by
12575
* adding to the series.parallelArrays array.
12577
updateParallelArrays: function (point, i) {
12578
var series = point.series,
12580
fn = typeof i === 'number' ?
12581
// Insert the value in the given position
12583
var val = key === 'y' && series.toYData ? series.toYData(point) : point[key];
12584
series[key + 'Data'][i] = val;
12586
// Apply the method specified in i with the following arguments as arguments
12588
Array.prototype[i].apply(series[key + 'Data'], Array.prototype.slice.call(args, 2));
12591
each(series.parallelArrays, fn);
12595
* Return an auto incremented x value based on the pointStart and pointInterval options.
12596
* This is only used if an x value is not given for the point that calls autoIncrement.
12598
autoIncrement: function () {
12600
options = series.options,
12601
xIncrement = series.xIncrement;
12603
xIncrement = pick(xIncrement, options.pointStart, 0);
12605
series.pointInterval = pick(series.pointInterval, options.pointInterval, 1);
12607
series.xIncrement = xIncrement + series.pointInterval;
12612
* Divide the series data into segments divided by null values.
12614
getSegments: function () {
12619
points = series.points,
12620
pointsLength = points.length;
12622
if (pointsLength) { // no action required for []
12624
// if connect nulls, just remove null points
12625
if (series.options.connectNulls) {
12628
if (points[i].y === null) {
12629
points.splice(i, 1);
12632
if (points.length) {
12633
segments = [points];
12636
// else, split on null points
12638
each(points, function (point, i) {
12639
if (point.y === null) {
12640
if (i > lastNull + 1) {
12641
segments.push(points.slice(lastNull + 1, i));
12644
} else if (i === pointsLength - 1) { // last value
12645
segments.push(points.slice(lastNull + 1, i + 1));
12652
series.segments = segments;
12656
* Set the series options by merging from the options tree
12657
* @param {Object} itemOptions
12659
setOptions: function (itemOptions) {
12660
var chart = this.chart,
12661
chartOptions = chart.options,
12662
plotOptions = chartOptions.plotOptions,
12663
userOptions = chart.userOptions || {},
12664
userPlotOptions = userOptions.plotOptions || {},
12665
typeOptions = plotOptions[this.type],
12668
this.userOptions = itemOptions;
12672
plotOptions.series,
12676
// The tooltip options are merged between global and series specific options
12677
this.tooltipOptions = merge(
12678
defaultOptions.tooltip,
12679
defaultOptions.plotOptions[this.type].tooltip,
12680
userOptions.tooltip,
12681
userPlotOptions.series && userPlotOptions.series.tooltip,
12682
userPlotOptions[this.type] && userPlotOptions[this.type].tooltip,
12683
itemOptions.tooltip
12686
// Delete marker object if not allowed (#1125)
12687
if (typeOptions.marker === null) {
12688
delete options.marker;
12695
* Get the series' color
12697
getColor: function () {
12698
var options = this.options,
12699
userOptions = this.userOptions,
12700
defaultColors = this.chart.options.colors,
12701
counters = this.chart.counters,
12705
color = options.color || defaultPlotOptions[this.type].color;
12707
if (!color && !options.colorByPoint) {
12708
if (defined(userOptions._colorIndex)) { // after Series.update()
12709
colorIndex = userOptions._colorIndex;
12711
userOptions._colorIndex = counters.color;
12712
colorIndex = counters.color++;
12714
color = defaultColors[colorIndex];
12717
this.color = color;
12718
counters.wrapColor(defaultColors.length);
12721
* Get the series' symbol
12723
getSymbol: function () {
12725
userOptions = series.userOptions,
12726
seriesMarkerOption = series.options.marker,
12727
chart = series.chart,
12728
defaultSymbols = chart.options.symbols,
12729
counters = chart.counters,
12732
series.symbol = seriesMarkerOption.symbol;
12733
if (!series.symbol) {
12734
if (defined(userOptions._symbolIndex)) { // after Series.update()
12735
symbolIndex = userOptions._symbolIndex;
12737
userOptions._symbolIndex = counters.symbol;
12738
symbolIndex = counters.symbol++;
12740
series.symbol = defaultSymbols[symbolIndex];
12743
// don't substract radius in image symbols (#604)
12744
if (/^url/.test(series.symbol)) {
12745
seriesMarkerOption.radius = 0;
12747
counters.wrapSymbol(defaultSymbols.length);
12750
drawLegendSymbol: LegendSymbolMixin.drawLineMarker,
12753
* Replace the series data with a new set of data
12754
* @param {Object} data
12755
* @param {Object} redraw
12757
setData: function (data, redraw, animation, updatePoints) {
12759
oldData = series.points,
12760
oldDataLength = (oldData && oldData.length) || 0,
12762
options = series.options,
12763
chart = series.chart,
12765
xAxis = series.xAxis,
12766
hasCategories = xAxis && !!xAxis.categories,
12767
tooltipPoints = series.tooltipPoints,
12769
turboThreshold = options.turboThreshold,
12771
xData = this.xData,
12772
yData = this.yData,
12773
pointArrayMap = series.pointArrayMap,
12774
valueCount = pointArrayMap && pointArrayMap.length;
12777
dataLength = data.length;
12778
redraw = pick(redraw, true);
12780
// If the point count is the same as is was, just run Point.update which is
12781
// cheaper, allows animation, and keeps references to points.
12782
if (updatePoints !== false && dataLength && oldDataLength === dataLength && !series.cropped && !series.hasGroupedData) {
12783
each(data, function (point, i) {
12784
oldData[i].update(point, false);
12789
// Reset properties
12790
series.xIncrement = null;
12791
series.pointRange = hasCategories ? 1 : options.pointRange;
12793
series.colorCounter = 0; // for series with colorByPoint (#1547)
12795
// Update parallel arrays
12796
each(this.parallelArrays, function (key) {
12797
series[key + 'Data'].length = 0;
12800
// In turbo mode, only one- or twodimensional arrays of numbers are allowed. The
12801
// first value is tested, and we assume that all the rest are defined the same
12802
// way. Although the 'for' loops are similar, they are repeated inside each
12803
// if-else conditional for max performance.
12804
if (turboThreshold && dataLength > turboThreshold) {
12806
// find the first non-null point
12808
while (firstPoint === null && i < dataLength) {
12809
firstPoint = data[i];
12814
if (isNumber(firstPoint)) { // assume all points are numbers
12815
var x = pick(options.pointStart, 0),
12816
pointInterval = pick(options.pointInterval, 1);
12818
for (i = 0; i < dataLength; i++) {
12820
yData[i] = data[i];
12821
x += pointInterval;
12823
series.xIncrement = x;
12824
} else if (isArray(firstPoint)) { // assume all points are arrays
12825
if (valueCount) { // [x, low, high] or [x, o, h, l, c]
12826
for (i = 0; i < dataLength; i++) {
12829
yData[i] = pt.slice(1, valueCount + 1);
12832
for (i = 0; i < dataLength; i++) {
12839
error(12); // Highcharts expects configs to be numbers or arrays in turbo mode
12842
for (i = 0; i < dataLength; i++) {
12843
if (data[i] !== UNDEFINED) { // stray commas in oldIE
12844
pt = { series: series };
12845
series.pointClass.prototype.applyOptions.apply(pt, [data[i]]);
12846
series.updateParallelArrays(pt, i);
12847
if (hasCategories && pt.name) {
12848
xAxis.names[pt.x] = pt.name; // #2046
12854
// Forgetting to cast strings to numbers is a common caveat when handling CSV or JSON
12855
if (isString(yData[0])) {
12860
series.options.data = data;
12861
//series.zData = zData;
12863
// destroy old points
12866
if (oldData[i] && oldData[i].destroy) {
12867
oldData[i].destroy();
12870
if (tooltipPoints) { // #2594
12871
tooltipPoints.length = 0;
12874
// reset minRange (#878)
12876
xAxis.minRange = xAxis.userMinRange;
12880
series.isDirty = series.isDirtyData = chart.isDirtyBox = true;
12885
chart.redraw(animation);
12890
* Process the data by cropping away unused data points if the series is longer
12891
* than the crop threshold. This saves computing time for lage series.
12893
processData: function (force) {
12895
processedXData = series.xData, // copied during slice operation below
12896
processedYData = series.yData,
12897
dataLength = processedXData.length,
12903
xAxis = series.xAxis,
12904
i, // loop variable
12905
options = series.options,
12906
cropThreshold = options.cropThreshold,
12907
isCartesian = series.isCartesian;
12909
// If the series data or axes haven't changed, don't go through this. Return false to pass
12910
// the message on to override methods like in data grouping.
12911
if (isCartesian && !series.isDirty && !xAxis.isDirty && !series.yAxis.isDirty && !force) {
12916
// optionally filter out points outside the plot area
12917
if (isCartesian && series.sorted && (!cropThreshold || dataLength > cropThreshold || series.forceCrop)) {
12918
var min = xAxis.min,
12921
// it's outside current extremes
12922
if (processedXData[dataLength - 1] < min || processedXData[0] > max) {
12923
processedXData = [];
12924
processedYData = [];
12926
// only crop if it's actually spilling out
12927
} else if (processedXData[0] < min || processedXData[dataLength - 1] > max) {
12928
croppedData = this.cropData(series.xData, series.yData, min, max);
12929
processedXData = croppedData.xData;
12930
processedYData = croppedData.yData;
12931
cropStart = croppedData.start;
12937
// Find the closest distance between processed points
12938
for (i = processedXData.length - 1; i >= 0; i--) {
12939
distance = processedXData[i] - processedXData[i - 1];
12940
if (distance > 0 && (closestPointRange === UNDEFINED || distance < closestPointRange)) {
12941
closestPointRange = distance;
12943
// Unsorted data is not supported by the line tooltip, as well as data grouping and
12944
// navigation in Stock charts (#725) and width calculation of columns (#1900)
12945
} else if (distance < 0 && series.requireSorting) {
12950
// Record the properties
12951
series.cropped = cropped; // undefined or true
12952
series.cropStart = cropStart;
12953
series.processedXData = processedXData;
12954
series.processedYData = processedYData;
12956
if (options.pointRange === null) { // null means auto, as for columns, candlesticks and OHLC
12957
series.pointRange = closestPointRange || 1;
12959
series.closestPointRange = closestPointRange;
12964
* Iterate over xData and crop values between min and max. Returns object containing crop start/end
12965
* cropped xData with corresponding part of yData, dataMin and dataMax within the cropped range
12967
cropData: function (xData, yData, min, max) {
12968
var dataLength = xData.length,
12970
cropEnd = dataLength,
12971
cropShoulder = pick(this.cropShoulder, 1), // line-type series need one point outside
12974
// iterate up to find slice start
12975
for (i = 0; i < dataLength; i++) {
12976
if (xData[i] >= min) {
12977
cropStart = mathMax(0, i - cropShoulder);
12982
// proceed to find slice end
12983
for (; i < dataLength; i++) {
12984
if (xData[i] > max) {
12985
cropEnd = i + cropShoulder;
12991
xData: xData.slice(cropStart, cropEnd),
12992
yData: yData.slice(cropStart, cropEnd),
13000
* Generate the data point after the data has been processed by cropping away
13001
* unused points and optionally grouped in Highcharts Stock.
13003
generatePoints: function () {
13005
options = series.options,
13006
dataOptions = options.data,
13007
data = series.data,
13009
processedXData = series.processedXData,
13010
processedYData = series.processedYData,
13011
pointClass = series.pointClass,
13012
processedDataLength = processedXData.length,
13013
cropStart = series.cropStart || 0,
13015
hasGroupedData = series.hasGroupedData,
13020
if (!data && !hasGroupedData) {
13022
arr.length = dataOptions.length;
13023
data = series.data = arr;
13026
for (i = 0; i < processedDataLength; i++) {
13027
cursor = cropStart + i;
13028
if (!hasGroupedData) {
13029
if (data[cursor]) {
13030
point = data[cursor];
13031
} else if (dataOptions[cursor] !== UNDEFINED) { // #970
13032
data[cursor] = point = (new pointClass()).init(series, dataOptions[cursor], processedXData[i]);
13036
// splat the y data in case of ohlc data array
13037
points[i] = (new pointClass()).init(series, [processedXData[i]].concat(splat(processedYData[i])));
13041
// Hide cropped-away points - this only runs when the number of points is above cropThreshold, or when
13042
// swithching view from non-grouped data to grouped data (#637)
13043
if (data && (processedDataLength !== (dataLength = data.length) || hasGroupedData)) {
13044
for (i = 0; i < dataLength; i++) {
13045
if (i === cropStart && !hasGroupedData) { // when has grouped data, clear all points
13046
i += processedDataLength;
13049
data[i].destroyElements();
13050
data[i].plotX = UNDEFINED; // #1003
13055
series.data = data;
13056
series.points = points;
13060
* Calculate Y extremes for visible data
13062
getExtremes: function (yData) {
13063
var xAxis = this.xAxis,
13064
yAxis = this.yAxis,
13065
xData = this.processedXData,
13069
xExtremes = xAxis.getExtremes(), // #2117, need to compensate for log X axis
13070
xMin = xExtremes.min,
13071
xMax = xExtremes.max,
13081
yData = yData || this.stackedYData || this.processedYData;
13082
yDataLength = yData.length;
13084
for (i = 0; i < yDataLength; i++) {
13089
// For points within the visible range, including the first point outside the
13090
// visible range, consider y extremes
13091
validValue = y !== null && y !== UNDEFINED && (!yAxis.isLog || (y.length || y > 0));
13092
withinRange = this.getExtremesFromAll || this.cropped || ((xData[i + 1] || x) >= xMin &&
13093
(xData[i - 1] || x) <= xMax);
13095
if (validValue && withinRange) {
13098
if (j) { // array, like ohlc or range data
13100
if (y[j] !== null) {
13101
activeYData[activeCounter++] = y[j];
13105
activeYData[activeCounter++] = y;
13109
this.dataMin = pick(dataMin, arrayMin(activeYData));
13110
this.dataMax = pick(dataMax, arrayMax(activeYData));
13114
* Translate data points from raw data values to chart specific positioning data
13115
* needed later in drawPoints, drawGraph and drawTracker.
13117
translate: function () {
13118
if (!this.processedXData) { // hidden series
13119
this.processData();
13121
this.generatePoints();
13123
options = series.options,
13124
stacking = options.stacking,
13125
xAxis = series.xAxis,
13126
categories = xAxis.categories,
13127
yAxis = series.yAxis,
13128
points = series.points,
13129
dataLength = points.length,
13130
hasModifyValue = !!series.modifyValue,
13132
pointPlacement = options.pointPlacement,
13133
dynamicallyPlaced = pointPlacement === 'between' || isNumber(pointPlacement),
13134
threshold = options.threshold;
13136
// Translate each point
13137
for (i = 0; i < dataLength; i++) {
13138
var point = points[i],
13141
yBottom = point.low,
13142
stack = stacking && yAxis.stacks[(series.negStacks && yValue < threshold ? '-' : '') + series.stackKey],
13146
// Discard disallowed y values for log axes
13147
if (yAxis.isLog && yValue <= 0) {
13148
point.y = yValue = null;
13151
// Get the plotX translation
13152
point.plotX = xAxis.translate(xValue, 0, 0, 0, 1, pointPlacement, this.type === 'flags'); // Math.round fixes #591
13155
// Calculate the bottom y value for stacked series
13156
if (stacking && series.visible && stack && stack[xValue]) {
13158
pointStack = stack[xValue];
13159
stackValues = pointStack.points[series.index];
13160
yBottom = stackValues[0];
13161
yValue = stackValues[1];
13163
if (yBottom === 0) {
13164
yBottom = pick(threshold, yAxis.min);
13166
if (yAxis.isLog && yBottom <= 0) { // #1200, #1232
13170
point.total = point.stackTotal = pointStack.total;
13171
point.percentage = pointStack.total && (point.y / pointStack.total * 100);
13172
point.stackY = yValue;
13174
// Place the stack label
13175
pointStack.setOffset(series.pointXOffset || 0, series.barW || 0);
13179
// Set translated yBottom or remove it
13180
point.yBottom = defined(yBottom) ?
13181
yAxis.translate(yBottom, 0, 1, 0, 1) :
13184
// general hook, used for Highstock compare mode
13185
if (hasModifyValue) {
13186
yValue = series.modifyValue(yValue, point);
13189
// Set the the plotY value, reset it for redraws
13190
point.plotY = (typeof yValue === 'number' && yValue !== Infinity) ?
13191
//mathRound(yAxis.translate(yValue, 0, 1, 0, 1) * 10) / 10 : // Math.round fixes #591
13192
yAxis.translate(yValue, 0, 1, 0, 1) :
13195
// Set client related positions for mouse tracking
13196
point.clientX = dynamicallyPlaced ? xAxis.translate(xValue, 0, 0, 0, 1) : point.plotX; // #1514
13198
point.negative = point.y < (threshold || 0);
13201
point.category = categories && categories[point.x] !== UNDEFINED ?
13202
categories[point.x] : point.x;
13207
// now that we have the cropped data, build the segments
13208
series.getSegments();
13212
* Animate in the series
13214
animate: function (init) {
13216
chart = series.chart,
13217
renderer = chart.renderer,
13220
animation = series.options.animation,
13221
clipBox = chart.clipBox,
13222
inverted = chart.inverted,
13225
// Animation option is set to true
13226
if (animation && !isObject(animation)) {
13227
animation = defaultPlotOptions[series.type].animation;
13229
sharedClipKey = '_sharedClip' + animation.duration + animation.easing;
13231
// Initialize the animation. Set up the clipping rectangle.
13234
// If a clipping rectangle with the same properties is currently present in the chart, use that.
13235
clipRect = chart[sharedClipKey];
13236
markerClipRect = chart[sharedClipKey + 'm'];
13238
chart[sharedClipKey] = clipRect = renderer.clipRect(
13239
extend(clipBox, { width: 0 })
13242
chart[sharedClipKey + 'm'] = markerClipRect = renderer.clipRect(
13243
-99, // include the width of the first marker
13244
inverted ? -chart.plotLeft : -chart.plotTop,
13246
inverted ? chart.chartWidth : chart.chartHeight
13249
series.group.clip(clipRect);
13250
series.markerGroup.clip(markerClipRect);
13251
series.sharedClipKey = sharedClipKey;
13253
// Run the animation
13255
clipRect = chart[sharedClipKey];
13258
width: chart.plotSizeX
13260
chart[sharedClipKey + 'm'].animate({
13261
width: chart.plotSizeX + 99
13265
// Delete this function to allow it only once
13266
series.animate = null;
13268
// Call the afterAnimate function on animation complete (but don't overwrite the animation.complete option
13269
// which should be available to the user).
13270
series.animationTimeout = setTimeout(function () {
13271
series.afterAnimate();
13272
}, animation.duration);
13277
* This runs after animation to land on the final plot clipping
13279
afterAnimate: function () {
13280
var chart = this.chart,
13281
sharedClipKey = this.sharedClipKey,
13282
group = this.group;
13284
if (group && this.options.clip !== false) {
13285
group.clip(chart.clipRect);
13286
this.markerGroup.clip(); // no clip
13289
// Remove the shared clipping rectancgle when all series are shown
13290
setTimeout(function () {
13291
if (sharedClipKey && chart[sharedClipKey]) {
13292
chart[sharedClipKey] = chart[sharedClipKey].destroy();
13293
chart[sharedClipKey + 'm'] = chart[sharedClipKey + 'm'].destroy();
13301
drawPoints: function () {
13304
points = series.points,
13305
chart = series.chart,
13314
options = series.options,
13315
seriesMarkerOptions = options.marker,
13316
seriesPointAttr = series.pointAttr[''],
13317
pointMarkerOptions,
13320
markerGroup = series.markerGroup;
13322
if (seriesMarkerOptions.enabled || series._hasPointMarkers) {
13327
plotX = mathFloor(point.plotX); // #1843
13328
plotY = point.plotY;
13329
graphic = point.graphic;
13330
pointMarkerOptions = point.marker || {};
13331
enabled = (seriesMarkerOptions.enabled && pointMarkerOptions.enabled === UNDEFINED) || pointMarkerOptions.enabled;
13332
isInside = chart.isInsidePlot(mathRound(plotX), plotY, chart.inverted); // #1858
13334
// only draw the point if y is defined
13335
if (enabled && plotY !== UNDEFINED && !isNaN(plotY) && point.y !== null) {
13338
pointAttr = point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE] || seriesPointAttr;
13339
radius = pointAttr.r;
13340
symbol = pick(pointMarkerOptions.symbol, series.symbol);
13341
isImage = symbol.indexOf('url') === 0;
13343
if (graphic) { // update
13345
.attr({ // Since the marker group isn't clipped, each individual marker must be toggled
13346
visibility: isInside ? 'inherit' : HIDDEN
13351
}, graphic.symbolName ? { // don't apply to image symbols #507
13355
} else if (isInside && (radius > 0 || isImage)) {
13356
point.graphic = graphic = chart.renderer.symbol(
13367
} else if (graphic) {
13368
point.graphic = graphic.destroy(); // #1269
13376
* Convert state properties from API naming conventions to SVG attributes
13378
* @param {Object} options API options object
13379
* @param {Object} base1 SVG attribute object to inherit from
13380
* @param {Object} base2 Second level SVG attribute object to inherit from
13382
convertAttribs: function (options, base1, base2, base3) {
13383
var conversion = this.pointAttrToOptions,
13388
options = options || {};
13389
base1 = base1 || {};
13390
base2 = base2 || {};
13391
base3 = base3 || {};
13393
for (attr in conversion) {
13394
option = conversion[attr];
13395
obj[attr] = pick(options[option], base1[attr], base2[attr], base3[attr]);
13401
* Get the state attributes. Each series type has its own set of attributes
13402
* that are allowed to change on a point's state change. Series wide attributes are stored for
13403
* all series, and additionally point specific attributes are stored for all
13404
* points with individual marker options. If such options are not defined for the point,
13405
* a reference to the series wide attributes is stored in point.pointAttr.
13407
getAttribs: function () {
13409
seriesOptions = series.options,
13410
normalOptions = defaultPlotOptions[series.type].marker ? seriesOptions.marker : seriesOptions,
13411
stateOptions = normalOptions.states,
13412
stateOptionsHover = stateOptions[HOVER_STATE],
13413
pointStateOptionsHover,
13414
seriesColor = series.color,
13416
stroke: seriesColor,
13419
points = series.points || [], // #927
13422
seriesPointAttr = [],
13424
pointAttrToOptions = series.pointAttrToOptions,
13425
hasPointSpecificOptions = series.hasPointSpecificOptions,
13426
negativeColor = seriesOptions.negativeColor,
13427
defaultLineColor = normalOptions.lineColor,
13428
defaultFillColor = normalOptions.fillColor,
13429
turboThreshold = seriesOptions.turboThreshold,
13433
// series type specific modifications
13434
if (seriesOptions.marker) { // line, spline, area, areaspline, scatter
13436
// if no hover radius is given, default to normal radius + 2
13437
stateOptionsHover.radius = stateOptionsHover.radius || normalOptions.radius + 2;
13438
stateOptionsHover.lineWidth = stateOptionsHover.lineWidth || normalOptions.lineWidth + 1;
13440
} else { // column, bar, pie
13442
// if no hover color is given, brighten the normal color
13443
stateOptionsHover.color = stateOptionsHover.color ||
13444
Color(stateOptionsHover.color || seriesColor)
13445
.brighten(stateOptionsHover.brightness).get();
13448
// general point attributes for the series normal state
13449
seriesPointAttr[NORMAL_STATE] = series.convertAttribs(normalOptions, normalDefaults);
13451
// HOVER_STATE and SELECT_STATE states inherit from normal state except the default radius
13452
each([HOVER_STATE, SELECT_STATE], function (state) {
13453
seriesPointAttr[state] =
13454
series.convertAttribs(stateOptions[state], seriesPointAttr[NORMAL_STATE]);
13458
series.pointAttr = seriesPointAttr;
13461
// Generate the point-specific attribute collections if specific point
13462
// options are given. If not, create a referance to the series wide point
13465
if (!turboThreshold || i < turboThreshold || hasPointSpecificOptions) {
13468
normalOptions = (point.options && point.options.marker) || point.options;
13469
if (normalOptions && normalOptions.enabled === false) {
13470
normalOptions.radius = 0;
13473
if (point.negative && negativeColor) {
13474
point.color = point.fillColor = negativeColor;
13477
hasPointSpecificOptions = seriesOptions.colorByPoint || point.color; // #868
13479
// check if the point has specific visual options
13480
if (point.options) {
13481
for (key in pointAttrToOptions) {
13482
if (defined(normalOptions[pointAttrToOptions[key]])) {
13483
hasPointSpecificOptions = true;
13488
// a specific marker config object is defined for the individual point:
13489
// create it's own attribute collection
13490
if (hasPointSpecificOptions) {
13491
normalOptions = normalOptions || {};
13493
stateOptions = normalOptions.states || {}; // reassign for individual point
13494
pointStateOptionsHover = stateOptions[HOVER_STATE] = stateOptions[HOVER_STATE] || {};
13496
// Handle colors for column and pies
13497
if (!seriesOptions.marker) { // column, bar, point
13498
// If no hover color is given, brighten the normal color. #1619, #2579
13499
pointStateOptionsHover.color = pointStateOptionsHover.color || (!point.options.color && stateOptionsHover.color) ||
13501
.brighten(pointStateOptionsHover.brightness || stateOptionsHover.brightness)
13505
// normal point state inherits series wide normal state
13506
attr = { color: point.color }; // #868
13507
if (!defaultFillColor) { // Individual point color or negative color markers (#2219)
13508
attr.fillColor = point.color;
13510
if (!defaultLineColor) {
13511
attr.lineColor = point.color; // Bubbles take point color, line markers use white
13513
pointAttr[NORMAL_STATE] = series.convertAttribs(extend(attr, normalOptions), seriesPointAttr[NORMAL_STATE]);
13515
// inherit from point normal and series hover
13516
pointAttr[HOVER_STATE] = series.convertAttribs(
13517
stateOptions[HOVER_STATE],
13518
seriesPointAttr[HOVER_STATE],
13519
pointAttr[NORMAL_STATE]
13522
// inherit from point normal and series hover
13523
pointAttr[SELECT_STATE] = series.convertAttribs(
13524
stateOptions[SELECT_STATE],
13525
seriesPointAttr[SELECT_STATE],
13526
pointAttr[NORMAL_STATE]
13530
// no marker config object is created: copy a reference to the series-wide
13531
// attribute collection
13533
pointAttr = seriesPointAttr;
13536
point.pointAttr = pointAttr;
13542
* Clear DOM objects and free up memory
13544
destroy: function () {
13546
chart = series.chart,
13547
issue134 = /AppleWebKit\/533/.test(userAgent),
13550
data = series.data || [],
13556
fireEvent(series, 'destroy');
13558
// remove all events
13559
removeEvent(series);
13562
each(series.axisTypes || [], function (AXIS) {
13563
axis = series[AXIS];
13565
erase(axis.series, series);
13566
axis.isDirty = axis.forceRedraw = true;
13570
// remove legend items
13571
if (series.legendItem) {
13572
series.chart.legend.destroyItem(series);
13575
// destroy all points with their elements
13579
if (point && point.destroy) {
13583
series.points = null;
13585
// Clear the animation timeout if we are destroying the series during initial animation
13586
clearTimeout(series.animationTimeout);
13588
// destroy all SVGElements associated to the series
13589
each(['area', 'graph', 'dataLabelsGroup', 'group', 'markerGroup', 'tracker',
13590
'graphNeg', 'areaNeg', 'posClip', 'negClip'], function (prop) {
13591
if (series[prop]) {
13593
// issue 134 workaround
13594
destroy = issue134 && prop === 'group' ?
13598
series[prop][destroy]();
13602
// remove from hoverSeries
13603
if (chart.hoverSeries === series) {
13604
chart.hoverSeries = null;
13606
erase(chart.series, series);
13608
// clear all members
13609
for (prop in series) {
13610
delete series[prop];
13615
* Return the graph path of a segment
13617
getSegmentPath: function (segment) {
13620
step = series.options.step;
13622
// build the segment line
13623
each(segment, function (point, i) {
13625
var plotX = point.plotX,
13626
plotY = point.plotY,
13629
if (series.getPointSpline) { // generate the spline as defined in the SplineSeries object
13630
segmentPath.push.apply(segmentPath, series.getPointSpline(segment, point, i));
13634
// moveTo or lineTo
13635
segmentPath.push(i ? L : M);
13639
lastPoint = segment[i - 1];
13640
if (step === 'right') {
13646
} else if (step === 'center') {
13648
(lastPoint.plotX + plotX) / 2,
13650
(lastPoint.plotX + plotX) / 2,
13662
// normal line to next point
13670
return segmentPath;
13674
* Get the graph path
13676
getGraphPath: function () {
13680
singlePoints = []; // used in drawTracker
13682
// Divide into segments and build graph and area paths
13683
each(series.segments, function (segment) {
13685
segmentPath = series.getSegmentPath(segment);
13687
// add the segment to the graph, or a single point for tracking
13688
if (segment.length > 1) {
13689
graphPath = graphPath.concat(segmentPath);
13691
singlePoints.push(segment[0]);
13695
// Record it for use in drawGraph and drawTracker, and return graphPath
13696
series.singlePoints = singlePoints;
13697
series.graphPath = graphPath;
13704
* Draw the actual graph
13706
drawGraph: function () {
13708
options = this.options,
13709
props = [['graph', options.lineColor || this.color]],
13710
lineWidth = options.lineWidth,
13711
dashStyle = options.dashStyle,
13712
roundCap = options.linecap !== 'square',
13713
graphPath = this.getGraphPath(),
13714
negativeColor = options.negativeColor;
13716
if (negativeColor) {
13717
props.push(['graphNeg', negativeColor]);
13721
each(props, function (prop, i) {
13722
var graphKey = prop[0],
13723
graph = series[graphKey],
13727
stop(graph); // cancel running animations, #459
13728
graph.animate({ d: graphPath });
13730
} else if (lineWidth && graphPath.length) { // #1487
13733
'stroke-width': lineWidth,
13738
attribs.dashstyle = dashStyle;
13739
} else if (roundCap) {
13740
attribs['stroke-linecap'] = attribs['stroke-linejoin'] = 'round';
13743
series[graphKey] = series.chart.renderer.path(graphPath)
13746
.shadow(!i && options.shadow);
13752
* Clip the graphs into the positive and negative coloured graphs
13754
clipNeg: function () {
13755
var options = this.options,
13756
chart = this.chart,
13757
renderer = chart.renderer,
13758
negativeColor = options.negativeColor || options.negativeFillColor,
13759
translatedThreshold,
13762
graph = this.graph,
13764
posClip = this.posClip,
13765
negClip = this.negClip,
13766
chartWidth = chart.chartWidth,
13767
chartHeight = chart.chartHeight,
13768
chartSizeMax = mathMax(chartWidth, chartHeight),
13769
yAxis = this.yAxis,
13773
if (negativeColor && (graph || area)) {
13774
translatedThreshold = mathRound(yAxis.toPixels(options.threshold || 0, true));
13775
if (translatedThreshold < 0) {
13776
chartSizeMax -= translatedThreshold; // #2534
13781
width: chartSizeMax,
13782
height: translatedThreshold
13786
y: translatedThreshold,
13787
width: chartSizeMax,
13788
height: chartSizeMax
13791
if (chart.inverted) {
13793
above.height = below.y = chart.plotWidth - translatedThreshold;
13794
if (renderer.isVML) {
13796
x: chart.plotWidth - translatedThreshold - chart.plotLeft,
13799
height: chartHeight
13802
x: translatedThreshold + chart.plotLeft - chartWidth,
13804
width: chart.plotLeft + translatedThreshold,
13810
if (yAxis.reversed) {
13818
if (posClip) { // update
13819
posClip.animate(posAttr);
13820
negClip.animate(negAttr);
13823
this.posClip = posClip = renderer.clipRect(posAttr);
13824
this.negClip = negClip = renderer.clipRect(negAttr);
13826
if (graph && this.graphNeg) {
13827
graph.clip(posClip);
13828
this.graphNeg.clip(negClip);
13832
area.clip(posClip);
13833
this.areaNeg.clip(negClip);
13840
* Initialize and perform group inversion on series.group and series.markerGroup
13842
invertGroups: function () {
13844
chart = series.chart;
13846
// Pie, go away (#1736)
13847
if (!series.xAxis) {
13851
// A fixed size is needed for inversion to work
13852
function setInvert() {
13854
width: series.yAxis.len,
13855
height: series.xAxis.len
13858
each(['group', 'markerGroup'], function (groupName) {
13859
if (series[groupName]) {
13860
series[groupName].attr(size).invert();
13865
addEvent(chart, 'resize', setInvert); // do it on resize
13866
addEvent(series, 'destroy', function () {
13867
removeEvent(chart, 'resize', setInvert);
13871
setInvert(); // do it now
13873
// On subsequent render and redraw, just do setInvert without setting up events again
13874
series.invertGroups = setInvert;
13878
* General abstraction for creating plot groups like series.group, series.dataLabelsGroup and
13879
* series.markerGroup. On subsequent calls, the group will only be adjusted to the updated plot size.
13881
plotGroup: function (prop, name, visibility, zIndex, parent) {
13882
var group = this[prop],
13885
// Generate it on first call
13887
this[prop] = group = this.chart.renderer.g(name)
13889
visibility: visibility,
13890
zIndex: zIndex || 0.1 // IE8 needs this
13894
// Place it on first and subsequent (redraw) calls
13895
group[isNew ? 'attr' : 'animate'](this.getPlotBox());
13900
* Get the translation and scale for the plot area of this series
13902
getPlotBox: function () {
13904
translateX: this.xAxis ? this.xAxis.left : this.chart.plotLeft,
13905
translateY: this.yAxis ? this.yAxis.top : this.chart.plotTop,
13906
scaleX: 1, // #1623
13912
* Render the graph and markers
13914
render: function () {
13916
chart = series.chart,
13918
options = series.options,
13919
animation = options.animation,
13920
doAnimation = animation && !!series.animate &&
13921
chart.renderer.isSVG, // this animation doesn't work in IE8 quirks when the group div is hidden,
13922
// and looks bad in other oldIE
13923
visibility = series.visible ? VISIBLE : HIDDEN,
13924
zIndex = options.zIndex,
13925
hasRendered = series.hasRendered,
13926
chartSeriesGroup = chart.seriesGroup;
13929
group = series.plotGroup(
13937
series.markerGroup = series.plotGroup(
13945
// initiate the animation
13947
series.animate(true);
13950
// cache attributes for shapes
13951
series.getAttribs();
13953
// SVGRenderer needs to know this before drawing elements (#1089, #1795)
13954
group.inverted = series.isCartesian ? chart.inverted : false;
13956
// draw the graph if any
13957
if (series.drawGraph) {
13958
series.drawGraph();
13962
// draw the data labels (inn pies they go before the points)
13963
if (series.drawDataLabels) {
13964
series.drawDataLabels();
13968
if (series.visible) {
13969
series.drawPoints();
13973
// draw the mouse tracking area
13974
if (series.drawTracker && series.options.enableMouseTracking !== false) {
13975
series.drawTracker();
13978
// Handle inverted series and tracker groups
13979
if (chart.inverted) {
13980
series.invertGroups();
13983
// Initial clipping, must be defined after inverting groups for VML
13984
if (options.clip !== false && !series.sharedClipKey && !hasRendered) {
13985
group.clip(chart.clipRect);
13988
// Run the animation
13991
} else if (!hasRendered) {
13992
series.afterAnimate();
13995
series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see
13996
// (See #322) series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see
13997
series.hasRendered = true;
14001
* Redraw the series after an update in the axes.
14003
redraw: function () {
14005
chart = series.chart,
14006
wasDirtyData = series.isDirtyData, // cache it here as it is set to false in render, but used after
14007
group = series.group,
14008
xAxis = series.xAxis,
14009
yAxis = series.yAxis;
14011
// reposition on resize
14013
if (chart.inverted) {
14015
width: chart.plotWidth,
14016
height: chart.plotHeight
14021
translateX: pick(xAxis && xAxis.left, chart.plotLeft),
14022
translateY: pick(yAxis && yAxis.top, chart.plotTop)
14026
series.translate();
14027
series.setTooltipPoints(true);
14030
if (wasDirtyData) {
14031
fireEvent(series, 'updatedData');
14034
}; // end Series prototype
14037
* The class for stack items
14039
function StackItem(axis, options, isNegative, x, stackOption, stacking) {
14041
var inverted = axis.chart.inverted;
14045
// Tells if the stack is negative
14046
this.isNegative = isNegative;
14048
// Save the options to be able to style the label
14049
this.options = options;
14051
// Save the x value to be able to position the label later
14054
// Initialize total value
14057
// This will keep each points' extremes stored by series.index
14060
// Save the stack option on the series configuration object, and whether to treat it as percent
14061
this.stack = stackOption;
14062
this.percent = stacking === 'percent';
14064
// The align options and text align varies on whether the stack is negative and
14065
// if the chart is inverted or not.
14066
// First test the user supplied value, then use the dynamic.
14067
this.alignOptions = {
14068
align: options.align || (inverted ? (isNegative ? 'left' : 'right') : 'center'),
14069
verticalAlign: options.verticalAlign || (inverted ? 'middle' : (isNegative ? 'bottom' : 'top')),
14070
y: pick(options.y, inverted ? 4 : (isNegative ? 14 : -6)),
14071
x: pick(options.x, inverted ? (isNegative ? -6 : 6) : 0)
14074
this.textAlign = options.textAlign || (inverted ? (isNegative ? 'right' : 'left') : 'center');
14077
StackItem.prototype = {
14078
destroy: function () {
14079
destroyObjectProperties(this, this.axis);
14083
* Renders the stack total label and adds it to the stack label group.
14085
render: function (group) {
14086
var options = this.options,
14087
formatOption = options.format,
14088
str = formatOption ?
14089
format(formatOption, this) :
14090
options.formatter.call(this); // format the text in the label
14092
// Change the text to reflect the new total and set visibility to hidden in case the serie is hidden
14094
this.label.attr({text: str, visibility: HIDDEN});
14095
// Create new label
14098
this.axis.chart.renderer.text(str, 0, 0, options.useHTML) // dummy positions, actual position updated with setOffset method in columnseries
14099
.css(options.style) // apply style
14101
align: this.textAlign, // fix the text-anchor
14102
rotation: options.rotation, // rotation
14103
visibility: HIDDEN // hidden until setOffset is called
14105
.add(group); // add to the labels-group
14110
* Sets the offset that the stack has from the x value and repositions the label.
14112
setOffset: function (xOffset, xWidth) {
14113
var stackItem = this,
14114
axis = stackItem.axis,
14115
chart = axis.chart,
14116
inverted = chart.inverted,
14117
neg = this.isNegative, // special treatment is needed for negative stacks
14118
y = axis.translate(this.percent ? 100 : this.total, 0, 0, 0, 1), // stack value translated mapped to chart coordinates
14119
yZero = axis.translate(0), // stack origin
14120
h = mathAbs(y - yZero), // stack height
14121
x = chart.xAxis[0].translate(this.x) + xOffset, // stack x position
14122
plotHeight = chart.plotHeight,
14123
stackBox = { // this is the box for the complete stack
14124
x: inverted ? (neg ? y : y - h) : x,
14125
y: inverted ? plotHeight - x - xWidth : (neg ? (plotHeight - y - h) : plotHeight - y),
14126
width: inverted ? h : xWidth,
14127
height: inverted ? xWidth : h
14129
label = this.label,
14133
label.align(this.alignOptions, null, stackBox); // align the label to the box
14135
// Set visibility (#678)
14136
alignAttr = label.alignAttr;
14137
label[this.options.crop === false || chart.isInsidePlot(alignAttr.x, alignAttr.y) ? 'show' : 'hide'](true);
14143
// Stacking methods defined on the Axis prototype
14146
* Build the stacks from top down
14148
Axis.prototype.buildStacks = function () {
14149
var series = this.series,
14150
reversedStacks = pick(this.options.reversedStacks, true),
14152
if (!this.isXAxis) {
14153
this.usePercentage = false;
14155
series[reversedStacks ? i : series.length - i - 1].setStackedPoints();
14157
// Loop up again to compute percent stack
14158
if (this.usePercentage) {
14159
for (i = 0; i < series.length; i++) {
14160
series[i].setPercentStacks();
14166
Axis.prototype.renderStackTotals = function () {
14168
chart = axis.chart,
14169
renderer = chart.renderer,
14170
stacks = axis.stacks,
14174
stackTotalGroup = axis.stackTotalGroup;
14176
// Create a separate group for the stack total labels
14177
if (!stackTotalGroup) {
14178
axis.stackTotalGroup = stackTotalGroup =
14179
renderer.g('stack-labels')
14181
visibility: VISIBLE,
14187
// plotLeft/Top will change when y axis gets wider so we need to translate the
14188
// stackTotalGroup at every render call. See bug #506 and #516
14189
stackTotalGroup.translate(chart.plotLeft, chart.plotTop);
14191
// Render each stack total
14192
for (stackKey in stacks) {
14193
oneStack = stacks[stackKey];
14194
for (stackCategory in oneStack) {
14195
oneStack[stackCategory].render(stackTotalGroup);
14201
// Stacking methods defnied for Series prototype
14204
* Adds series' points value to corresponding stack
14206
Series.prototype.setStackedPoints = function () {
14207
if (!this.options.stacking || (this.visible !== true && this.chart.options.chart.ignoreHiddenSeries !== false)) {
14212
xData = series.processedXData,
14213
yData = series.processedYData,
14215
yDataLength = yData.length,
14216
seriesOptions = series.options,
14217
threshold = seriesOptions.threshold,
14218
stackOption = seriesOptions.stack,
14219
stacking = seriesOptions.stacking,
14220
stackKey = series.stackKey,
14221
negKey = '-' + stackKey,
14222
negStacks = series.negStacks,
14223
yAxis = series.yAxis,
14224
stacks = yAxis.stacks,
14225
oldStacks = yAxis.oldStacks,
14234
// loop over the non-null y values and read them into a local array
14235
for (i = 0; i < yDataLength; i++) {
14239
// Read stacked values into a stack based on the x value,
14240
// the sign of y and the stack key. Stacking is also handled for null values (#739)
14241
isNegative = negStacks && y < threshold;
14242
key = isNegative ? negKey : stackKey;
14244
// Create empty object for this stack if it doesn't exist yet
14245
if (!stacks[key]) {
14249
// Initialize StackItem for this x
14250
if (!stacks[key][x]) {
14251
if (oldStacks[key] && oldStacks[key][x]) {
14252
stacks[key][x] = oldStacks[key][x];
14253
stacks[key][x].total = null;
14255
stacks[key][x] = new StackItem(yAxis, yAxis.options.stackLabels, isNegative, x, stackOption, stacking);
14259
// If the StackItem doesn't exist, create it first
14260
stack = stacks[key][x];
14261
stack.points[series.index] = [stack.cum || 0];
14263
// Add value to the stack total
14264
if (stacking === 'percent') {
14266
// Percent stacked column, totals are the same for the positive and negative stacks
14267
other = isNegative ? stackKey : negKey;
14268
if (negStacks && stacks[other] && stacks[other][x]) {
14269
other = stacks[other][x];
14270
stack.total = other.total = mathMax(other.total, stack.total) + mathAbs(y) || 0;
14272
// Percent stacked areas
14274
stack.total = correctFloat(stack.total + (mathAbs(y) || 0));
14277
stack.total = correctFloat(stack.total + (y || 0));
14280
stack.cum = (stack.cum || 0) + (y || 0);
14282
stack.points[series.index].push(stack.cum);
14283
stackedYData[i] = stack.cum;
14287
if (stacking === 'percent') {
14288
yAxis.usePercentage = true;
14291
this.stackedYData = stackedYData; // To be used in getExtremes
14293
// Reset old stacks
14294
yAxis.oldStacks = {};
14298
* Iterate over all stacks and compute the absolute values to percent
14300
Series.prototype.setPercentStacks = function () {
14302
stackKey = series.stackKey,
14303
stacks = series.yAxis.stacks,
14304
processedXData = series.processedXData;
14306
each([stackKey, '-' + stackKey], function (key) {
14307
var i = processedXData.length,
14314
x = processedXData[i];
14315
stack = stacks[key] && stacks[key][x];
14316
pointExtremes = stack && stack.points[series.index];
14317
if (pointExtremes) {
14318
totalFactor = stack.total ? 100 / stack.total : 0;
14319
pointExtremes[0] = correctFloat(pointExtremes[0] * totalFactor); // Y bottom value
14320
pointExtremes[1] = correctFloat(pointExtremes[1] * totalFactor); // Y value
14321
series.stackedYData[i] = pointExtremes[1];
14327
// Extend the Chart prototype for dynamic methods
14328
extend(Chart.prototype, {
14331
* Add a series dynamically after time
14333
* @param {Object} options The config options
14334
* @param {Boolean} redraw Whether to redraw the chart after adding. Defaults to true.
14335
* @param {Boolean|Object} animation Whether to apply animation, and optionally animation
14338
* @return {Object} series The newly created series object
14340
addSeries: function (options, redraw, animation) {
14345
redraw = pick(redraw, true); // defaults to true
14347
fireEvent(chart, 'addSeries', { options: options }, function () {
14348
series = chart.initSeries(options);
14350
chart.isDirtyLegend = true; // the series array is out of sync with the display
14351
chart.linkSeries();
14353
chart.redraw(animation);
14362
* Add an axis to the chart
14363
* @param {Object} options The axis option
14364
* @param {Boolean} isX Whether it is an X axis or a value axis
14366
addAxis: function (options, isX, redraw, animation) {
14367
var key = isX ? 'xAxis' : 'yAxis',
14368
chartOptions = this.options,
14371
/*jslint unused: false*/
14372
axis = new Axis(this, merge(options, {
14373
index: this[key].length,
14376
/*jslint unused: true*/
14378
// Push the new axis options to the chart options
14379
chartOptions[key] = splat(chartOptions[key] || {});
14380
chartOptions[key].push(options);
14382
if (pick(redraw, true)) {
14383
this.redraw(animation);
14388
* Dim the chart and show a loading text or symbol
14389
* @param {String} str An optional text to show in the loading label instead of the default one
14391
showLoading: function (str) {
14393
options = chart.options,
14394
loadingDiv = chart.loadingDiv;
14396
var loadingOptions = options.loading;
14398
// create the layer at the first call
14400
chart.loadingDiv = loadingDiv = createElement(DIV, {
14401
className: PREFIX + 'loading'
14402
}, extend(loadingOptions.style, {
14405
}), chart.container);
14407
chart.loadingSpan = createElement(
14410
loadingOptions.labelStyle,
14417
chart.loadingSpan.innerHTML = str || options.lang.loading;
14420
if (!chart.loadingShown) {
14424
left: chart.plotLeft + PX,
14425
top: chart.plotTop + PX,
14426
width: chart.plotWidth + PX,
14427
height: chart.plotHeight + PX
14429
animate(loadingDiv, {
14430
opacity: loadingOptions.style.opacity
14432
duration: loadingOptions.showDuration || 0
14434
chart.loadingShown = true;
14439
* Hide the loading layer
14441
hideLoading: function () {
14442
var options = this.options,
14443
loadingDiv = this.loadingDiv;
14446
animate(loadingDiv, {
14449
duration: options.loading.hideDuration || 100,
14450
complete: function () {
14451
css(loadingDiv, { display: NONE });
14455
this.loadingShown = false;
14459
// extend the Point prototype for dynamic methods
14460
extend(Point.prototype, {
14462
* Update the point with new options (typically x/y data) and optionally redraw the series.
14464
* @param {Object} options Point options as defined in the series.data array
14465
* @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
14466
* @param {Boolean|Object} animation Whether to apply animation, and optionally animation
14470
update: function (options, redraw, animation) {
14472
series = point.series,
14473
graphic = point.graphic,
14475
data = series.data,
14476
chart = series.chart,
14477
seriesOptions = series.options;
14479
redraw = pick(redraw, true);
14481
// fire the event with a default handler of doing the update
14482
point.firePointEvent('update', { options: options }, function () {
14484
point.applyOptions(options);
14487
if (isObject(options)) {
14488
series.getAttribs();
14490
if (options && options.marker && options.marker.symbol) {
14491
point.graphic = graphic.destroy();
14493
graphic.attr(point.pointAttr[point.state || '']);
14496
if (options && options.dataLabels && point.dataLabel) { // #2468
14497
point.dataLabel = point.dataLabel.destroy();
14501
// record changes in the parallel arrays
14502
i = inArray(point, data);
14503
series.updateParallelArrays(point, i);
14505
seriesOptions.data[i] = point.options;
14508
series.isDirty = series.isDirtyData = true;
14509
if (!series.fixedBox && series.hasCartesianSeries) { // #1906, #2320
14510
chart.isDirtyBox = true;
14513
if (seriesOptions.legendType === 'point') { // #1831, #1885
14514
chart.legend.destroyItem(point);
14517
chart.redraw(animation);
14523
* Remove a point and optionally redraw the series and if necessary the axes
14524
* @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
14525
* @param {Boolean|Object} animation Whether to apply animation, and optionally animation
14528
remove: function (redraw, animation) {
14530
series = point.series,
14531
points = series.points,
14532
chart = series.chart,
14534
data = series.data;
14536
setAnimation(animation, chart);
14537
redraw = pick(redraw, true);
14539
// fire the event with a default handler of removing the point
14540
point.firePointEvent('remove', null, function () {
14542
// splice all the parallel arrays
14543
i = inArray(point, data);
14544
if (data.length === points.length) {
14545
points.splice(i, 1);
14548
series.options.data.splice(i, 1);
14549
series.updateParallelArrays(point, 'splice', i, 1);
14554
series.isDirty = true;
14555
series.isDirtyData = true;
14563
// Extend the series prototype for dynamic methods
14564
extend(Series.prototype, {
14566
* Add a point dynamically after chart load time
14567
* @param {Object} options Point options as given in series.data
14568
* @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
14569
* @param {Boolean} shift If shift is true, a point is shifted off the start
14570
* of the series as one is appended to the end.
14571
* @param {Boolean|Object} animation Whether to apply animation, and optionally animation
14574
addPoint: function (options, redraw, shift, animation) {
14576
seriesOptions = series.options,
14577
data = series.data,
14578
graph = series.graph,
14579
area = series.area,
14580
chart = series.chart,
14581
names = series.xAxis && series.xAxis.names,
14582
currentShift = (graph && graph.shift) || 0,
14583
dataOptions = seriesOptions.data,
14586
xData = series.xData,
14590
setAnimation(animation, chart);
14592
// Make graph animate sideways
14594
each([graph, area, series.graphNeg, series.areaNeg], function (shape) {
14596
shape.shift = currentShift + 1;
14601
area.isArea = true; // needed in animation, both with and without shift
14604
// Optional redraw, defaults to true
14605
redraw = pick(redraw, true);
14607
// Get options and push the point to xData, yData and series.options. In series.generatePoints
14608
// the Point instance will be created on demand and pushed to the series.data array.
14609
point = { series: series };
14610
series.pointClass.prototype.applyOptions.apply(point, [options]);
14613
// Get the insertion point
14615
if (series.requireSorting && x < xData[i - 1]) {
14616
isInTheMiddle = true;
14617
while (i && xData[i - 1] > x) {
14622
series.updateParallelArrays(point, 'splice', i, 0, 0); // insert undefined item
14623
series.updateParallelArrays(point, i); // update it
14626
names[x] = point.name;
14628
dataOptions.splice(i, 0, options);
14630
if (isInTheMiddle) {
14631
series.data.splice(i, 0, null);
14632
series.processData();
14635
// Generate points to be added to the legend (#1329)
14636
if (seriesOptions.legendType === 'point') {
14637
series.generatePoints();
14640
// Shift the first point off the parallel arrays
14641
// todo: consider series.removePoint(i) method
14643
if (data[0] && data[0].remove) {
14644
data[0].remove(false);
14647
series.updateParallelArrays(point, 'shift');
14649
dataOptions.shift();
14654
series.isDirty = true;
14655
series.isDirtyData = true;
14657
series.getAttribs(); // #1937
14663
* Remove a series and optionally redraw the chart
14665
* @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
14666
* @param {Boolean|Object} animation Whether to apply animation, and optionally animation
14670
remove: function (redraw, animation) {
14672
chart = series.chart;
14673
redraw = pick(redraw, true);
14675
if (!series.isRemoving) { /* prevent triggering native event in jQuery
14676
(calling the remove function from the remove event) */
14677
series.isRemoving = true;
14679
// fire the event with a default handler of removing the point
14680
fireEvent(series, 'remove', null, function () {
14683
// destroy elements
14688
chart.isDirtyLegend = chart.isDirtyBox = true;
14689
chart.linkSeries();
14692
chart.redraw(animation);
14697
series.isRemoving = false;
14701
* Update the series with a new set of options
14703
update: function (newOptions, redraw) {
14704
var chart = this.chart,
14705
// must use user options when changing type because this.options is merged
14706
// in with type specific plotOptions
14707
oldOptions = this.userOptions,
14708
oldType = this.type,
14709
proto = seriesTypes[oldType].prototype,
14712
// Do the merge, with some forced options
14713
newOptions = merge(oldOptions, {
14716
pointStart: this.xData[0] // when updating after addPoint
14717
}, { data: this.options.data }, newOptions);
14719
// Destroy the series and reinsert methods from the type prototype
14720
this.remove(false);
14721
for (n in proto) { // Overwrite series-type specific methods (#2270)
14722
if (proto.hasOwnProperty(n)) {
14723
this[n] = UNDEFINED;
14726
extend(this, seriesTypes[newOptions.type || oldType].prototype);
14729
this.init(chart, newOptions);
14730
if (pick(redraw, true)) {
14731
chart.redraw(false);
14736
// Extend the Axis.prototype for dynamic methods
14737
extend(Axis.prototype, {
14740
* Update the axis with a new options structure
14742
update: function (newOptions, redraw) {
14743
var chart = this.chart;
14745
newOptions = chart.options[this.coll][this.options.index] = merge(this.userOptions, newOptions);
14747
this.destroy(true);
14748
this._addedPlotLB = this.userMin = this.userMax = UNDEFINED; // #1611, #2306
14750
this.init(chart, extend(newOptions, { events: UNDEFINED }));
14752
chart.isDirtyBox = true;
14753
if (pick(redraw, true)) {
14759
* Remove the axis from the chart
14761
remove: function (redraw) {
14762
var chart = this.chart,
14763
key = this.coll, // xAxis or yAxis
14764
axisSeries = this.series,
14765
i = axisSeries.length;
14767
// Remove associated series (#2687)
14769
if (axisSeries[i]) {
14770
axisSeries[i].remove(false);
14775
erase(chart.axes, this);
14776
erase(chart[key], this);
14777
chart.options[key].splice(this.options.index, 1);
14778
each(chart[key], function (axis, i) { // Re-index, #1706
14779
axis.options.index = i;
14782
chart.isDirtyBox = true;
14784
if (pick(redraw, true)) {
14790
* Update the axis title by options
14792
setTitle: function (newTitleOptions, redraw) {
14793
this.update({ title: newTitleOptions }, redraw);
14797
* Set new axis categories and optionally redraw
14798
* @param {Array} categories
14799
* @param {Boolean} redraw
14801
setCategories: function (categories, redraw) {
14802
this.update({ categories: categories }, redraw);
14809
* LineSeries object
14811
var LineSeries = extendClass(Series);
14812
seriesTypes.line = LineSeries;
14815
* Set the default options for area
14817
defaultPlotOptions.area = merge(defaultSeriesOptions, {
14819
// trackByArea: false,
14820
// lineColor: null, // overrides color, but lets fillColor be unaltered
14821
// fillOpacity: 0.75,
14826
* AreaSeries object
14828
var AreaSeries = extendClass(Series, {
14831
* For stacks, don't split segments on null values. Instead, draw null values with
14832
* no marker. Also insert dummy points for any X position that exists in other series
14835
getSegments: function () {
14839
xAxis = this.xAxis,
14840
yAxis = this.yAxis,
14841
stack = yAxis.stacks[this.stackKey],
14845
points = this.points,
14846
connectNulls = this.options.connectNulls,
14851
if (this.options.stacking && !this.cropped) { // cropped causes artefacts in Stock, and perf issue
14852
// Create a map where we can quickly look up the points by their X value.
14853
for (i = 0; i < points.length; i++) {
14854
pointMap[points[i].x] = points[i];
14857
// Sort the keys (#1651)
14859
if (stack[x].total !== null) { // nulled after switching between grouping and not (#1651, #2336)
14863
keys.sort(function (a, b) {
14867
each(keys, function (x) {
14868
if (connectNulls && (!pointMap[x] || pointMap[x].y === null)) { // #1836
14871
// The point exists, push it to the segment
14872
} else if (pointMap[x]) {
14873
segment.push(pointMap[x]);
14875
// There is no point for this X value in this series, so we
14876
// insert a dummy point in order for the areas to be drawn
14879
plotX = xAxis.translate(x);
14880
val = stack[x].percent ? (stack[x].total ? stack[x].cum * 100 / stack[x].total : 0) : stack[x].cum; // #1991
14881
plotY = yAxis.toPixels(val, true);
14893
if (segment.length) {
14894
segments.push(segment);
14898
Series.prototype.getSegments.call(this);
14899
segments = this.segments;
14902
this.segments = segments;
14906
* Extend the base Series getSegmentPath method by adding the path for the area.
14907
* This path is pushed to the series.areaPath property.
14909
getSegmentPath: function (segment) {
14911
var segmentPath = Series.prototype.getSegmentPath.call(this, segment), // call base method
14912
areaSegmentPath = [].concat(segmentPath), // work on a copy for the area path
14914
options = this.options,
14915
segLength = segmentPath.length,
14916
translatedThreshold = this.yAxis.getThreshold(options.threshold), // #2181
14919
if (segLength === 3) { // for animation from 1 to two points
14920
areaSegmentPath.push(L, segmentPath[1], segmentPath[2]);
14922
if (options.stacking && !this.closedStacks) {
14924
// Follow stack back. Todo: implement areaspline. A general solution could be to
14925
// reverse the entire graphPath of the previous series, though may be hard with
14926
// splines and with series with different extremes
14927
for (i = segment.length - 1; i >= 0; i--) {
14929
yBottom = pick(segment[i].yBottom, translatedThreshold);
14932
if (i < segment.length - 1 && options.step) {
14933
areaSegmentPath.push(segment[i + 1].plotX, yBottom);
14936
areaSegmentPath.push(segment[i].plotX, yBottom);
14939
} else { // follow zero line back
14940
this.closeSegment(areaSegmentPath, segment, translatedThreshold);
14942
this.areaPath = this.areaPath.concat(areaSegmentPath);
14943
return segmentPath;
14947
* Extendable method to close the segment path of an area. This is overridden in polar
14950
closeSegment: function (path, segment, translatedThreshold) {
14953
segment[segment.length - 1].plotX,
14954
translatedThreshold,
14957
translatedThreshold
14962
* Draw the graph and the underlying area. This method calls the Series base
14963
* function and adds the area. The areaPath is calculated in the getSegmentPath
14964
* method called from Series.prototype.drawGraph.
14966
drawGraph: function () {
14968
// Define or reset areaPath
14969
this.areaPath = [];
14971
// Call the base method
14972
Series.prototype.drawGraph.apply(this);
14974
// Define local variables
14976
areaPath = this.areaPath,
14977
options = this.options,
14978
negativeColor = options.negativeColor,
14979
negativeFillColor = options.negativeFillColor,
14980
props = [['area', this.color, options.fillColor]]; // area name, main color, fill color
14982
if (negativeColor || negativeFillColor) {
14983
props.push(['areaNeg', negativeColor, negativeFillColor]);
14986
each(props, function (prop) {
14987
var areaKey = prop[0],
14988
area = series[areaKey];
14990
// Create or update the area
14991
if (area) { // update
14992
area.animate({ d: areaPath });
14995
series[areaKey] = series.chart.renderer.path(areaPath)
14999
Color(prop[1]).setOpacity(pick(options.fillOpacity, 0.75)).get()
15002
}).add(series.group);
15007
drawLegendSymbol: LegendSymbolMixin.drawRectangle
15010
seriesTypes.area = AreaSeries;
15012
* Set the default options for spline
15014
defaultPlotOptions.spline = merge(defaultSeriesOptions);
15017
* SplineSeries object
15019
var SplineSeries = extendClass(Series, {
15023
* Get the spline segment from a given point's previous neighbour to the given point
15025
getPointSpline: function (segment, point, i) {
15026
var smoothing = 1.5, // 1 means control points midway between points, 2 means 1/3 from the point, 3 is 1/4 etc
15027
denom = smoothing + 1,
15028
plotX = point.plotX,
15029
plotY = point.plotY,
15030
lastPoint = segment[i - 1],
15031
nextPoint = segment[i + 1],
15038
// find control points
15039
if (lastPoint && nextPoint) {
15041
var lastX = lastPoint.plotX,
15042
lastY = lastPoint.plotY,
15043
nextX = nextPoint.plotX,
15044
nextY = nextPoint.plotY,
15047
leftContX = (smoothing * plotX + lastX) / denom;
15048
leftContY = (smoothing * plotY + lastY) / denom;
15049
rightContX = (smoothing * plotX + nextX) / denom;
15050
rightContY = (smoothing * plotY + nextY) / denom;
15052
// have the two control points make a straight line through main point
15053
correction = ((rightContY - leftContY) * (rightContX - plotX)) /
15054
(rightContX - leftContX) + plotY - rightContY;
15056
leftContY += correction;
15057
rightContY += correction;
15059
// to prevent false extremes, check that control points are between
15060
// neighbouring points' y values
15061
if (leftContY > lastY && leftContY > plotY) {
15062
leftContY = mathMax(lastY, plotY);
15063
rightContY = 2 * plotY - leftContY; // mirror of left control point
15064
} else if (leftContY < lastY && leftContY < plotY) {
15065
leftContY = mathMin(lastY, plotY);
15066
rightContY = 2 * plotY - leftContY;
15068
if (rightContY > nextY && rightContY > plotY) {
15069
rightContY = mathMax(nextY, plotY);
15070
leftContY = 2 * plotY - rightContY;
15071
} else if (rightContY < nextY && rightContY < plotY) {
15072
rightContY = mathMin(nextY, plotY);
15073
leftContY = 2 * plotY - rightContY;
15076
// record for drawing in next point
15077
point.rightContX = rightContX;
15078
point.rightContY = rightContY;
15082
// Visualize control points for debugging
15085
this.chart.renderer.circle(leftContX + this.chart.plotLeft, leftContY + this.chart.plotTop, 2)
15092
this.chart.renderer.path(['M', leftContX + this.chart.plotLeft, leftContY + this.chart.plotTop,
15093
'L', plotX + this.chart.plotLeft, plotY + this.chart.plotTop])
15099
this.chart.renderer.circle(rightContX + this.chart.plotLeft, rightContY + this.chart.plotTop, 2)
15106
this.chart.renderer.path(['M', rightContX + this.chart.plotLeft, rightContY + this.chart.plotTop,
15107
'L', plotX + this.chart.plotLeft, plotY + this.chart.plotTop])
15116
// moveTo or lineTo
15118
ret = [M, plotX, plotY];
15119
} else { // curve from last point to this
15122
lastPoint.rightContX || lastPoint.plotX,
15123
lastPoint.rightContY || lastPoint.plotY,
15124
leftContX || plotX,
15125
leftContY || plotY,
15129
lastPoint.rightContX = lastPoint.rightContY = null; // reset for updating series later
15134
seriesTypes.spline = SplineSeries;
15137
* Set the default options for areaspline
15139
defaultPlotOptions.areaspline = merge(defaultPlotOptions.area);
15142
* AreaSplineSeries object
15144
var areaProto = AreaSeries.prototype,
15145
AreaSplineSeries = extendClass(SplineSeries, {
15146
type: 'areaspline',
15147
closedStacks: true, // instead of following the previous graph back, follow the threshold back
15149
// Mix in methods from the area series
15150
getSegmentPath: areaProto.getSegmentPath,
15151
closeSegment: areaProto.closeSegment,
15152
drawGraph: areaProto.drawGraph,
15153
drawLegendSymbol: LegendSymbolMixin.drawRectangle
15156
seriesTypes.areaspline = AreaSplineSeries;
15159
* Set the default options for column
15161
defaultPlotOptions.column = merge(defaultSeriesOptions, {
15162
borderColor: '#FFFFFF',
15165
//colorByPoint: undefined,
15168
marker: null, // point options are specified in the base options
15170
//pointWidth: null,
15172
cropThreshold: 50, // when there are more points, they will not animate out of the chart on xAxis.setExtremes
15173
pointRange: null, // null means auto, meaning 1 in a categorized axis and least distance between points if not categories
15181
borderColor: '#000000',
15186
align: null, // auto
15187
verticalAlign: null, // auto
15190
stickyTracking: false,
15195
* ColumnSeries object
15197
var ColumnSeries = extendClass(Series, {
15199
pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
15200
stroke: 'borderColor',
15201
'stroke-width': 'borderWidth',
15206
trackerGroups: ['group', 'dataLabelsGroup'],
15207
negStacks: true, // use separate negative stacks, unlike area stacks where a negative
15208
// point is substracted from previous (#1910)
15211
* Initialize the series
15213
init: function () {
15214
Series.prototype.init.apply(this, arguments);
15217
chart = series.chart;
15219
// if the series is added dynamically, force redraw of other
15220
// series affected by a new column
15221
if (chart.hasRendered) {
15222
each(chart.series, function (otherSeries) {
15223
if (otherSeries.type === series.type) {
15224
otherSeries.isDirty = true;
15231
* Return the width and x offset of the columns adjusted for grouping, groupPadding, pointPadding,
15234
getColumnMetrics: function () {
15237
options = series.options,
15238
xAxis = series.xAxis,
15239
yAxis = series.yAxis,
15240
reversedXAxis = xAxis.reversed,
15246
// Get the total number of column type series.
15247
// This is called on every series. Consider moving this logic to a
15248
// chart.orderStacks() function and call it on init, addSeries and removeSeries
15249
if (options.grouping === false) {
15252
each(series.chart.series, function (otherSeries) {
15253
var otherOptions = otherSeries.options,
15254
otherYAxis = otherSeries.yAxis;
15255
if (otherSeries.type === series.type && otherSeries.visible &&
15256
yAxis.len === otherYAxis.len && yAxis.pos === otherYAxis.pos) { // #642, #2086
15257
if (otherOptions.stacking) {
15258
stackKey = otherSeries.stackKey;
15259
if (stackGroups[stackKey] === UNDEFINED) {
15260
stackGroups[stackKey] = columnCount++;
15262
columnIndex = stackGroups[stackKey];
15263
} else if (otherOptions.grouping !== false) { // #1162
15264
columnIndex = columnCount++;
15266
otherSeries.columnIndex = columnIndex;
15271
var categoryWidth = mathMin(
15272
mathAbs(xAxis.transA) * (xAxis.ordinalSlope || options.pointRange || xAxis.closestPointRange || xAxis.tickInterval || 1), // #2610
15275
groupPadding = categoryWidth * options.groupPadding,
15276
groupWidth = categoryWidth - 2 * groupPadding,
15277
pointOffsetWidth = groupWidth / columnCount,
15278
optionPointWidth = options.pointWidth,
15279
pointPadding = defined(optionPointWidth) ? (pointOffsetWidth - optionPointWidth) / 2 :
15280
pointOffsetWidth * options.pointPadding,
15281
pointWidth = pick(optionPointWidth, pointOffsetWidth - 2 * pointPadding), // exact point width, used in polar charts
15282
colIndex = (reversedXAxis ?
15283
columnCount - (series.columnIndex || 0) : // #1251
15284
series.columnIndex) || 0,
15285
pointXOffset = pointPadding + (groupPadding + colIndex *
15286
pointOffsetWidth - (categoryWidth / 2)) *
15287
(reversedXAxis ? -1 : 1);
15289
// Save it for reading in linked series (Error bars particularly)
15290
return (series.columnMetrics = {
15292
offset: pointXOffset
15298
* Translate each point to the plot area coordinate system and find shape positions
15300
translate: function () {
15302
chart = series.chart,
15303
options = series.options,
15304
borderWidth = options.borderWidth,
15305
yAxis = series.yAxis,
15306
threshold = options.threshold,
15307
translatedThreshold = series.translatedThreshold = yAxis.getThreshold(threshold),
15308
minPointLength = pick(options.minPointLength, 5),
15309
metrics = series.getColumnMetrics(),
15310
pointWidth = metrics.width,
15311
seriesBarW = series.barW = mathCeil(mathMax(pointWidth, 1 + 2 * borderWidth)), // rounded and postprocessed for border width
15312
pointXOffset = series.pointXOffset = metrics.offset,
15313
xCrisp = -(borderWidth % 2 ? 0.5 : 0),
15314
yCrisp = borderWidth % 2 ? 0.5 : 1;
15316
if (chart.renderer.isVML && chart.inverted) {
15320
Series.prototype.translate.apply(series);
15322
// record the new values
15323
each(series.points, function (point) {
15324
var yBottom = pick(point.yBottom, translatedThreshold),
15325
plotY = mathMin(mathMax(-999 - yBottom, point.plotY), yAxis.len + 999 + yBottom), // Don't draw too far outside plot area (#1303, #2241)
15326
barX = point.plotX + pointXOffset,
15328
barY = mathMin(plotY, yBottom),
15333
barH = mathMax(plotY, yBottom) - barY;
15335
// Handle options.minPointLength
15336
if (mathAbs(barH) < minPointLength) {
15337
if (minPointLength) {
15338
barH = minPointLength;
15340
mathRound(mathAbs(barY - translatedThreshold) > minPointLength ? // stacked
15341
yBottom - minPointLength : // keep position
15342
translatedThreshold - (yAxis.translate(point.y, 0, 1, 0, 1) <= translatedThreshold ? minPointLength : 0)); // use exact yAxis.translation (#1485)
15346
// Cache for access in polar
15348
point.pointWidth = pointWidth;
15350
// Round off to obtain crisp edges
15351
fromLeft = mathAbs(barX) < 0.5;
15352
right = mathRound(barX + barW) + xCrisp;
15353
barX = mathRound(barX) + xCrisp;
15354
barW = right - barX;
15356
fromTop = mathAbs(barY) < 0.5;
15357
bottom = mathRound(barY + barH) + yCrisp;
15358
barY = mathRound(barY) + yCrisp;
15359
barH = bottom - barY;
15361
// Top and left edges are exceptions
15371
// Register shape type and arguments to be used in drawPoints
15372
point.shapeType = 'rect';
15373
point.shapeArgs = {
15386
* Use a solid rectangle like the area series types
15388
drawLegendSymbol: LegendSymbolMixin.drawRectangle,
15392
* Columns have no graph
15397
* Draw the columns. For bars, the series.group is rotated, so the same coordinates
15398
* apply for columns and bars. This method is inherited by scatter series.
15401
drawPoints: function () {
15403
chart = this.chart,
15404
options = series.options,
15405
renderer = chart.renderer,
15406
animationLimit = options.animationLimit || 250,
15409
// draw the columns
15410
each(series.points, function (point) {
15411
var plotY = point.plotY,
15412
graphic = point.graphic;
15414
if (plotY !== UNDEFINED && !isNaN(plotY) && point.y !== null) {
15415
shapeArgs = point.shapeArgs;
15417
if (graphic) { // update
15419
graphic[series.points.length < animationLimit ? 'animate' : 'attr'](merge(shapeArgs));
15422
point.graphic = graphic = renderer[point.shapeType](shapeArgs)
15423
.attr(point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE])
15425
.shadow(options.shadow, null, options.stacking && !options.borderRadius);
15428
} else if (graphic) {
15429
point.graphic = graphic.destroy(); // #1269
15435
* Animate the column heights one by one from zero
15436
* @param {Boolean} init Whether to initialize the animation or run it
15438
animate: function (init) {
15440
yAxis = this.yAxis,
15441
options = series.options,
15442
inverted = this.chart.inverted,
15444
translatedThreshold;
15446
if (hasSVG) { // VML is too slow anyway
15448
attr.scaleY = 0.001;
15449
translatedThreshold = mathMin(yAxis.pos + yAxis.len, mathMax(yAxis.pos, yAxis.toPixels(options.threshold)));
15451
attr.translateX = translatedThreshold - yAxis.len;
15453
attr.translateY = translatedThreshold;
15455
series.group.attr(attr);
15457
} else { // run the animation
15460
attr[inverted ? 'translateX' : 'translateY'] = yAxis.pos;
15461
series.group.animate(attr, series.options.animation);
15463
// delete this function to allow it only once
15464
series.animate = null;
15470
* Remove this series from the chart
15472
remove: function () {
15474
chart = series.chart;
15476
// column and bar series affects other series of the same type
15477
// as they are either stacked or grouped
15478
if (chart.hasRendered) {
15479
each(chart.series, function (otherSeries) {
15480
if (otherSeries.type === series.type) {
15481
otherSeries.isDirty = true;
15486
Series.prototype.remove.apply(series, arguments);
15489
seriesTypes.column = ColumnSeries;
15491
* Set the default options for bar
15493
defaultPlotOptions.bar = merge(defaultPlotOptions.column);
15495
* The Bar series class
15497
var BarSeries = extendClass(ColumnSeries, {
15501
seriesTypes.bar = BarSeries;
15504
* Set the default options for scatter
15506
defaultPlotOptions.scatter = merge(defaultSeriesOptions, {
15509
headerFormat: '<span style="font-size: 10px; color:{series.color}">{series.name}</span><br/>',
15510
pointFormat: 'x: <b>{point.x}</b><br/>y: <b>{point.y}</b><br/>',
15511
followPointer: true
15513
stickyTracking: false
15517
* The scatter series class
15519
var ScatterSeries = extendClass(Series, {
15522
requireSorting: false,
15523
noSharedTooltip: true,
15524
trackerGroups: ['markerGroup'],
15525
takeOrdinalPosition: false, // #2342
15526
singularTooltips: true,
15527
drawGraph: function () {
15528
if (this.options.lineWidth) {
15529
Series.prototype.drawGraph.call(this);
15534
seriesTypes.scatter = ScatterSeries;
15537
* Set the default options for pie
15539
defaultPlotOptions.pie = merge(defaultSeriesOptions, {
15540
borderColor: '#FFFFFF',
15542
center: [null, null],
15544
colorByPoint: true, // always true for pies
15547
// connectorWidth: 1,
15548
// connectorColor: point.color,
15549
// connectorPadding: 5,
15552
formatter: function () {
15553
return this.point.name;
15555
// softConnector: true,
15558
ignoreHiddenPoint: true,
15560
legendType: 'point',
15561
marker: null, // point options are specified in the base options
15563
showInLegend: false,
15571
stickyTracking: false,
15573
followPointer: true
15578
* Extended point object for pies
15580
var PiePoint = extendClass(Point, {
15582
* Initiate the pie slice
15584
init: function () {
15586
Point.prototype.init.apply(this, arguments);
15591
// Disallow negative values (#1530)
15596
//visible: options.visible !== false,
15598
visible: point.visible !== false,
15599
name: pick(point.name, 'Slice')
15602
// add event listener for select
15603
toggleSlice = function (e) {
15604
point.slice(e.type === 'select');
15606
addEvent(point, 'select', toggleSlice);
15607
addEvent(point, 'unselect', toggleSlice);
15613
* Toggle the visibility of the pie slice
15614
* @param {Boolean} vis Whether to show the slice or not. If undefined, the
15615
* visibility is toggled
15617
setVisible: function (vis) {
15619
series = point.series,
15620
chart = series.chart;
15622
// if called without an argument, toggle visibility
15623
point.visible = point.options.visible = vis = vis === UNDEFINED ? !point.visible : vis;
15624
series.options.data[inArray(point, series.data)] = point.options; // update userOptions.data
15626
// Show and hide associated elements
15627
each(['graphic', 'dataLabel', 'connector', 'shadowGroup'], function (key) {
15629
point[key][vis ? 'show' : 'hide'](true);
15633
if (point.legendItem) {
15634
chart.legend.colorizeItem(point, vis);
15637
// Handle ignore hidden slices
15638
if (!series.isDirty && series.options.ignoreHiddenPoint) {
15639
series.isDirty = true;
15645
* Set or toggle whether the slice is cut out from the pie
15646
* @param {Boolean} sliced When undefined, the slice state is toggled
15647
* @param {Boolean} redraw Whether to redraw the chart. True by default.
15649
slice: function (sliced, redraw, animation) {
15651
series = point.series,
15652
chart = series.chart,
15655
setAnimation(animation, chart);
15657
// redraw is true by default
15658
redraw = pick(redraw, true);
15660
// if called without an argument, toggle
15661
point.sliced = point.options.sliced = sliced = defined(sliced) ? sliced : !point.sliced;
15662
series.options.data[inArray(point, series.data)] = point.options; // update userOptions.data
15664
translation = sliced ? point.slicedTranslation : {
15669
point.graphic.animate(translation);
15671
if (point.shadowGroup) {
15672
point.shadowGroup.animate(translation);
15679
* The Pie series class
15683
isCartesian: false,
15684
pointClass: PiePoint,
15685
requireSorting: false,
15686
noSharedTooltip: true,
15687
trackerGroups: ['group', 'dataLabelsGroup'],
15689
pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
15690
stroke: 'borderColor',
15691
'stroke-width': 'borderWidth',
15694
singularTooltips: true,
15697
* Pies have one color each point
15702
* Animate the pies in
15704
animate: function (init) {
15706
points = series.points,
15707
startAngleRad = series.startAngleRad;
15710
each(points, function (point) {
15711
var graphic = point.graphic,
15712
args = point.shapeArgs;
15717
r: series.center[3] / 2, // animate from inner radius (#779)
15718
start: startAngleRad,
15727
}, series.options.animation);
15731
// delete this function to allow it only once
15732
series.animate = null;
15737
* Extend the basic setData method by running processData and generatePoints immediately,
15738
* in order to access the points from the legend.
15740
setData: function (data, redraw, animation, updatePoints) {
15741
Series.prototype.setData.call(this, data, false, animation, updatePoints);
15742
this.processData();
15743
this.generatePoints();
15744
if (pick(redraw, true)) {
15745
this.chart.redraw(animation);
15750
* Extend the generatePoints method by adding total and percentage properties to each point
15752
generatePoints: function () {
15758
ignoreHiddenPoint = this.options.ignoreHiddenPoint;
15760
Series.prototype.generatePoints.call(this);
15762
// Populate local vars
15763
points = this.points;
15764
len = points.length;
15766
// Get the total sum
15767
for (i = 0; i < len; i++) {
15769
total += (ignoreHiddenPoint && !point.visible) ? 0 : point.y;
15771
this.total = total;
15773
// Set each point's properties
15774
for (i = 0; i < len; i++) {
15776
point.percentage = total > 0 ? (point.y / total) * 100 : 0;
15777
point.total = total;
15783
* Do translation for pie slices
15785
translate: function (positions) {
15786
this.generatePoints();
15790
precision = 1000, // issue #172
15791
options = series.options,
15792
slicedOffset = options.slicedOffset,
15793
connectorOffset = slicedOffset + options.borderWidth,
15797
startAngle = options.startAngle || 0,
15798
startAngleRad = series.startAngleRad = mathPI / 180 * (startAngle - 90),
15799
endAngleRad = series.endAngleRad = mathPI / 180 * ((pick(options.endAngle, startAngle + 360)) - 90),
15800
circ = endAngleRad - startAngleRad, //2 * mathPI,
15801
points = series.points,
15802
radiusX, // the x component of the radius vector for a given point
15804
labelDistance = options.dataLabels.distance,
15805
ignoreHiddenPoint = options.ignoreHiddenPoint,
15807
len = points.length,
15810
// Get positions - either an integer or a percentage string must be given.
15811
// If positions are passed as a parameter, we're in a recursive loop for adjusting
15812
// space for data labels.
15814
series.center = positions = series.getCenter();
15817
// utility for getting the x value from a given y, used for anticollision logic in data labels
15818
series.getX = function (y, left) {
15820
angle = math.asin(mathMin((y - positions[1]) / (positions[2] / 2 + labelDistance), 1));
15822
return positions[0] +
15824
(mathCos(angle) * (positions[2] / 2 + labelDistance));
15827
// Calculate the geometry for each point
15828
for (i = 0; i < len; i++) {
15832
// set start and end angle
15833
start = startAngleRad + (cumulative * circ);
15834
if (!ignoreHiddenPoint || point.visible) {
15835
cumulative += point.percentage / 100;
15837
end = startAngleRad + (cumulative * circ);
15840
point.shapeType = 'arc';
15841
point.shapeArgs = {
15844
r: positions[2] / 2,
15845
innerR: positions[3] / 2,
15846
start: mathRound(start * precision) / precision,
15847
end: mathRound(end * precision) / precision
15850
// The angle must stay within -90 and 270 (#2645)
15851
angle = (end + start) / 2;
15852
if (angle > 1.5 * mathPI) {
15853
angle -= 2 * mathPI;
15854
} else if (angle < -mathPI / 2) {
15855
angle += 2 * mathPI;
15858
// Center for the sliced out slice
15859
point.slicedTranslation = {
15860
translateX: mathRound(mathCos(angle) * slicedOffset),
15861
translateY: mathRound(mathSin(angle) * slicedOffset)
15864
// set the anchor point for tooltips
15865
radiusX = mathCos(angle) * positions[2] / 2;
15866
radiusY = mathSin(angle) * positions[2] / 2;
15867
point.tooltipPos = [
15868
positions[0] + radiusX * 0.7,
15869
positions[1] + radiusY * 0.7
15872
point.half = angle < -mathPI / 2 || angle > mathPI / 2 ? 1 : 0;
15873
point.angle = angle;
15875
// set the anchor point for data labels
15876
connectorOffset = mathMin(connectorOffset, labelDistance / 2); // #1678
15878
positions[0] + radiusX + mathCos(angle) * labelDistance, // first break of connector
15879
positions[1] + radiusY + mathSin(angle) * labelDistance, // a/a
15880
positions[0] + radiusX + mathCos(angle) * connectorOffset, // second break, right outside pie
15881
positions[1] + radiusY + mathSin(angle) * connectorOffset, // a/a
15882
positions[0] + radiusX, // landing point for connector
15883
positions[1] + radiusY, // a/a
15884
labelDistance < 0 ? // alignment
15886
point.half ? 'right' : 'left', // alignment
15887
angle // center angle
15896
* Draw the data points
15898
drawPoints: function () {
15900
chart = series.chart,
15901
renderer = chart.renderer,
15906
shadow = series.options.shadow,
15910
if (shadow && !series.shadowGroup) {
15911
series.shadowGroup = renderer.g('shadow')
15912
.add(series.group);
15916
each(series.points, function (point) {
15917
graphic = point.graphic;
15918
shapeArgs = point.shapeArgs;
15919
shadowGroup = point.shadowGroup;
15921
// put the shadow behind all points
15922
if (shadow && !shadowGroup) {
15923
shadowGroup = point.shadowGroup = renderer.g('shadow')
15924
.add(series.shadowGroup);
15927
// if the point is sliced, use special translation, else use plot area traslation
15928
groupTranslation = point.sliced ? point.slicedTranslation : {
15933
//group.translate(groupTranslation[0], groupTranslation[1]);
15935
shadowGroup.attr(groupTranslation);
15940
graphic.animate(extend(shapeArgs, groupTranslation));
15942
point.graphic = graphic = renderer[point.shapeType](shapeArgs)
15943
.setRadialReference(series.center)
15945
point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE]
15948
'stroke-linejoin': 'round'
15949
//zIndex: 1 // #2722 (reversed)
15951
.attr(groupTranslation)
15953
.shadow(shadow, shadowGroup);
15956
// detect point specific visibility (#2430)
15957
if (point.visible !== undefined) {
15958
point.setVisible(point.visible);
15966
* Utility for sorting data labels
15968
sortByAngle: function (points, sign) {
15969
points.sort(function (a, b) {
15970
return a.angle !== undefined && (b.angle - a.angle) * sign;
15975
* Use a simple symbol from LegendSymbolMixin
15977
drawLegendSymbol: LegendSymbolMixin.drawRectangle,
15980
* Use the getCenter method from drawLegendSymbol
15982
getCenter: CenteredSeriesMixin.getCenter,
15985
* Pies don't have point marker symbols
15990
PieSeries = extendClass(Series, PieSeries);
15991
seriesTypes.pie = PieSeries;
15994
* Draw the data labels
15996
Series.prototype.drawDataLabels = function () {
15999
seriesOptions = series.options,
16000
cursor = seriesOptions.cursor,
16001
options = seriesOptions.dataLabels,
16002
points = series.points,
16008
if (options.enabled || series._hasPointLabels) {
16010
// Process default alignment of data labels for columns
16011
if (series.dlProcessOptions) {
16012
series.dlProcessOptions(options);
16015
// Create a separate group for the data labels to avoid rotation
16016
dataLabelsGroup = series.plotGroup(
16019
series.visible ? VISIBLE : HIDDEN,
16020
options.zIndex || 6
16023
// Make the labels for each point
16024
generalOptions = options;
16025
each(points, function (point) {
16028
dataLabel = point.dataLabel,
16033
connector = point.connector,
16036
// Determine if each data label is enabled
16037
pointOptions = point.options && point.options.dataLabels;
16038
enabled = pick(pointOptions && pointOptions.enabled, generalOptions.enabled); // #2282
16041
// If the point is outside the plot area, destroy it. #678, #820
16042
if (dataLabel && !enabled) {
16043
point.dataLabel = dataLabel.destroy();
16045
// Individual labels are disabled if the are explicitly disabled
16046
// in the point options, or if they fall outside the plot area.
16047
} else if (enabled) {
16049
// Create individual options structure that can be extended without
16050
// affecting others
16051
options = merge(generalOptions, pointOptions);
16053
rotation = options.rotation;
16056
labelConfig = point.getLabelConfig();
16057
str = options.format ?
16058
format(options.format, labelConfig) :
16059
options.formatter.call(labelConfig, options);
16061
// Determine the color
16062
options.style.color = pick(options.color, options.style.color, series.color, 'black');
16065
// update existing label
16068
if (defined(str)) {
16075
} else { // #1437 - the label is shown conditionally
16076
point.dataLabel = dataLabel = dataLabel.destroy();
16078
point.connector = connector.destroy();
16082
// create new label
16083
} else if (defined(str)) {
16086
fill: options.backgroundColor,
16087
stroke: options.borderColor,
16088
'stroke-width': options.borderWidth,
16089
r: options.borderRadius || 0,
16090
rotation: rotation,
16091
padding: options.padding,
16094
// Remove unused attributes (#947)
16095
for (name in attr) {
16096
if (attr[name] === UNDEFINED) {
16101
dataLabel = point.dataLabel = series.chart.renderer[rotation ? 'text' : 'label']( // labels don't support rotation
16111
.css(extend(options.style, cursor && { cursor: cursor }))
16112
.add(dataLabelsGroup)
16113
.shadow(options.shadow);
16118
// Now the data label is created and placed at 0,0, so we need to align it
16119
series.alignDataLabel(point, dataLabel, options, null, isNew);
16127
* Align each individual data label
16129
Series.prototype.alignDataLabel = function (point, dataLabel, options, alignTo, isNew) {
16130
var chart = this.chart,
16131
inverted = chart.inverted,
16132
plotX = pick(point.plotX, -999),
16133
plotY = pick(point.plotY, -999),
16134
bBox = dataLabel.getBBox(),
16135
// Math.round for rounding errors (#2683), alignTo to allow column labels (#2700)
16136
visible = this.visible && (point.series.forceDL || chart.isInsidePlot(plotX, mathRound(plotY), inverted) ||
16137
(alignTo && chart.isInsidePlot(plotX, inverted ? alignTo.x + 1 : alignTo.y + alignTo.height - 1, inverted))),
16138
alignAttr; // the final position;
16142
// The alignment box is a singular point
16144
x: inverted ? chart.plotWidth - plotY : plotX,
16145
y: mathRound(inverted ? chart.plotHeight - plotX : plotY),
16150
// Add the text size for alignment calculation
16153
height: bBox.height
16156
// Allow a hook for changing alignment in the last moment, then do the alignment
16157
if (options.rotation) { // Fancy box alignment isn't supported for rotated text
16159
align: options.align,
16160
x: alignTo.x + options.x + alignTo.width / 2,
16161
y: alignTo.y + options.y + alignTo.height / 2
16163
dataLabel[isNew ? 'attr' : 'animate'](alignAttr);
16165
dataLabel.align(options, null, alignTo);
16166
alignAttr = dataLabel.alignAttr;
16168
// Handle justify or crop
16169
if (pick(options.overflow, 'justify') === 'justify') {
16170
this.justifyDataLabel(dataLabel, options, alignAttr, bBox, alignTo, isNew);
16172
} else if (pick(options.crop, true)) {
16173
// Now check that the data label is within the plot area
16174
visible = chart.isInsidePlot(alignAttr.x, alignAttr.y) && chart.isInsidePlot(alignAttr.x + bBox.width, alignAttr.y + bBox.height);
16180
// Show or hide based on the final aligned position
16182
dataLabel.attr({ y: -999 });
16183
dataLabel.placed = false; // don't animate back in
16189
* If data labels fall partly outside the plot area, align them back in, in a way that
16190
* doesn't hide the point.
16192
Series.prototype.justifyDataLabel = function (dataLabel, options, alignAttr, bBox, alignTo, isNew) {
16193
var chart = this.chart,
16194
align = options.align,
16195
verticalAlign = options.verticalAlign,
16202
if (align === 'right') {
16203
options.align = 'left';
16211
off = alignAttr.x + bBox.width;
16212
if (off > chart.plotWidth) {
16213
if (align === 'left') {
16214
options.align = 'right';
16216
options.x = chart.plotWidth - off;
16224
if (verticalAlign === 'bottom') {
16225
options.verticalAlign = 'top';
16233
off = alignAttr.y + bBox.height;
16234
if (off > chart.plotHeight) {
16235
if (verticalAlign === 'top') {
16236
options.verticalAlign = 'bottom';
16238
options.y = chart.plotHeight - off;
16244
dataLabel.placed = !isNew;
16245
dataLabel.align(options, null, alignTo);
16250
* Override the base drawDataLabels method by pie specific functionality
16252
if (seriesTypes.pie) {
16253
seriesTypes.pie.prototype.drawDataLabels = function () {
16255
data = series.data,
16257
chart = series.chart,
16258
options = series.options.dataLabels,
16259
connectorPadding = pick(options.connectorPadding, 10),
16260
connectorWidth = pick(options.connectorWidth, 1),
16261
plotWidth = chart.plotWidth,
16262
plotHeight = chart.plotHeight,
16265
softConnector = pick(options.softConnector, true),
16266
distanceOption = options.distance,
16267
seriesCenter = series.center,
16268
radius = seriesCenter[2] / 2,
16269
centerY = seriesCenter[1],
16270
outside = distanceOption > 0,
16275
halves = [// divide the points into right and left halves for anti collision
16285
overflow = [0, 0, 0, 0], // top, right, bottom, left
16286
sort = function (a, b) {
16290
// get out if not enabled
16291
if (!series.visible || (!options.enabled && !series._hasPointLabels)) {
16295
// run parent method
16296
Series.prototype.drawDataLabels.apply(series);
16298
// arrange points for detection collision
16299
each(data, function (point) {
16300
if (point.dataLabel && point.visible) { // #407, #2510
16301
halves[point.half].push(point);
16305
// assume equal label heights
16307
while (!labelHeight && data[i]) { // #1569
16308
labelHeight = data[i] && data[i].dataLabel && (data[i].dataLabel.getBBox().height || 21); // 21 is for #968
16312
/* Loop over the points in each half, starting from the top and bottom
16313
* of the pie to detect overlapping labels.
16321
points = halves[i],
16323
length = points.length,
16327
series.sortByAngle(points, i - 0.5);
16329
// Only do anti-collision when we are outside the pie and have connectors (#856)
16330
if (distanceOption > 0) {
16333
for (pos = centerY - radius - distanceOption; pos <= centerY + radius + distanceOption; pos += labelHeight) {
16336
// visualize the slot
16338
var slotX = series.getX(pos, i) + chart.plotLeft - (i ? 100 : 0),
16339
slotY = pos + chart.plotTop;
16340
if (!isNaN(slotX)) {
16341
chart.renderer.rect(slotX, slotY - 7, 100, labelHeight, 1)
16347
chart.renderer.text('Slot '+ (slots.length - 1), slotX, slotY + 4)
16354
slotsLength = slots.length;
16356
// if there are more values than available slots, remove lowest values
16357
if (length > slotsLength) {
16358
// create an array for sorting and ranking the points within each quarter
16359
rankArr = [].concat(points);
16360
rankArr.sort(sort);
16363
rankArr[j].rank = j;
16367
if (points[j].rank >= slotsLength) {
16368
points.splice(j, 1);
16371
length = points.length;
16374
// The label goes to the nearest open slot, but not closer to the edge than
16375
// the label's index.
16376
for (j = 0; j < length; j++) {
16379
labelPos = point.labelPos;
16381
var closest = 9999,
16385
// find the closest slot index
16386
for (slotI = 0; slotI < slotsLength; slotI++) {
16387
distance = mathAbs(slots[slotI] - labelPos[1]);
16388
if (distance < closest) {
16389
closest = distance;
16394
// if that slot index is closer to the edges of the slots, move it
16395
// to the closest appropriate slot
16396
if (slotIndex < j && slots[j] !== null) { // cluster at the top
16398
} else if (slotsLength < length - j + slotIndex && slots[j] !== null) { // cluster at the bottom
16399
slotIndex = slotsLength - length + j;
16400
while (slots[slotIndex] === null) { // make sure it is not taken
16404
// Slot is taken, find next free slot below. In the next run, the next slice will find the
16405
// slot above these, because it is the closest one
16406
while (slots[slotIndex] === null) { // make sure it is not taken
16411
usedSlots.push({ i: slotIndex, y: slots[slotIndex] });
16412
slots[slotIndex] = null; // mark as taken
16414
// sort them in order to fill in from the top
16415
usedSlots.sort(sort);
16418
// now the used slots are sorted, fill them up sequentially
16419
for (j = 0; j < length; j++) {
16421
var slot, naturalY;
16424
labelPos = point.labelPos;
16425
dataLabel = point.dataLabel;
16426
visibility = point.visible === false ? HIDDEN : VISIBLE;
16427
naturalY = labelPos[1];
16429
if (distanceOption > 0) {
16430
slot = usedSlots.pop();
16431
slotIndex = slot.i;
16433
// if the slot next to currrent slot is free, the y value is allowed
16434
// to fall back to the natural position
16436
if ((naturalY > y && slots[slotIndex + 1] !== null) ||
16437
(naturalY < y && slots[slotIndex - 1] !== null)) {
16445
// get the x - use the natural x position for first and last slot, to prevent the top
16446
// and botton slice connectors from touching each other on either side
16447
x = options.justify ?
16448
seriesCenter[0] + (i ? -1 : 1) * (radius + distanceOption) :
16449
series.getX(slotIndex === 0 || slotIndex === slots.length - 1 ? naturalY : y, i);
16452
// Record the placement and visibility
16453
dataLabel._attr = {
16454
visibility: visibility,
16459
({ left: connectorPadding, right: -connectorPadding }[labelPos[6]] || 0),
16460
y: y + options.y - 10 // 10 is for the baseline (label vs text)
16462
dataLabel.connX = x;
16463
dataLabel.connY = y;
16466
// Detect overflowing data labels
16467
if (this.options.size === null) {
16468
dataLabelWidth = dataLabel.width;
16470
if (x - dataLabelWidth < connectorPadding) {
16471
overflow[3] = mathMax(mathRound(dataLabelWidth - x + connectorPadding), overflow[3]);
16474
} else if (x + dataLabelWidth > plotWidth - connectorPadding) {
16475
overflow[1] = mathMax(mathRound(x + dataLabelWidth - plotWidth + connectorPadding), overflow[1]);
16479
if (y - labelHeight / 2 < 0) {
16480
overflow[0] = mathMax(mathRound(-y + labelHeight / 2), overflow[0]);
16483
} else if (y + labelHeight / 2 > plotHeight) {
16484
overflow[2] = mathMax(mathRound(y + labelHeight / 2 - plotHeight), overflow[2]);
16487
} // for each point
16490
// Do not apply the final placement and draw the connectors until we have verified
16491
// that labels are not spilling over.
16492
if (arrayMax(overflow) === 0 || this.verifyDataLabelOverflow(overflow)) {
16494
// Place the labels in the final position
16495
this.placeDataLabels();
16497
// Draw the connectors
16498
if (outside && connectorWidth) {
16499
each(this.points, function (point) {
16500
connector = point.connector;
16501
labelPos = point.labelPos;
16502
dataLabel = point.dataLabel;
16504
if (dataLabel && dataLabel._pos) {
16505
visibility = dataLabel._attr.visibility;
16506
x = dataLabel.connX;
16507
y = dataLabel.connY;
16508
connectorPath = softConnector ? [
16510
x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label
16512
x, y, // first break, next to the label
16513
2 * labelPos[2] - labelPos[4], 2 * labelPos[3] - labelPos[5],
16514
labelPos[2], labelPos[3], // second break
16516
labelPos[4], labelPos[5] // base
16519
x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label
16521
labelPos[2], labelPos[3], // second break
16523
labelPos[4], labelPos[5] // base
16527
connector.animate({ d: connectorPath });
16528
connector.attr('visibility', visibility);
16531
point.connector = connector = series.chart.renderer.path(connectorPath).attr({
16532
'stroke-width': connectorWidth,
16533
stroke: options.connectorColor || point.color || '#606060',
16534
visibility: visibility
16535
//zIndex: 0 // #2722 (reversed)
16537
.add(series.group);
16539
} else if (connector) {
16540
point.connector = connector.destroy();
16547
* Perform the final placement of the data labels after we have verified that they
16548
* fall within the plot area.
16550
seriesTypes.pie.prototype.placeDataLabels = function () {
16551
each(this.points, function (point) {
16552
var dataLabel = point.dataLabel,
16556
_pos = dataLabel._pos;
16558
dataLabel.attr(dataLabel._attr);
16559
dataLabel[dataLabel.moved ? 'animate' : 'attr'](_pos);
16560
dataLabel.moved = true;
16561
} else if (dataLabel) {
16562
dataLabel.attr({ y: -999 });
16568
seriesTypes.pie.prototype.alignDataLabel = noop;
16571
* Verify whether the data labels are allowed to draw, or we should run more translation and data
16572
* label positioning to keep them inside the plot area. Returns true when data labels are ready
16575
seriesTypes.pie.prototype.verifyDataLabelOverflow = function (overflow) {
16577
var center = this.center,
16578
options = this.options,
16579
centerOption = options.center,
16580
minSize = options.minSize || 80,
16584
// Handle horizontal size and center
16585
if (centerOption[0] !== null) { // Fixed center
16586
newSize = mathMax(center[2] - mathMax(overflow[1], overflow[3]), minSize);
16588
} else { // Auto center
16590
center[2] - overflow[1] - overflow[3], // horizontal overflow
16593
center[0] += (overflow[3] - overflow[1]) / 2; // horizontal center
16596
// Handle vertical size and center
16597
if (centerOption[1] !== null) { // Fixed center
16598
newSize = mathMax(mathMin(newSize, center[2] - mathMax(overflow[0], overflow[2])), minSize);
16600
} else { // Auto center
16604
center[2] - overflow[0] - overflow[2] // vertical overflow
16608
center[1] += (overflow[0] - overflow[2]) / 2; // vertical center
16611
// If the size must be decreased, we need to run translate and drawDataLabels again
16612
if (newSize < center[2]) {
16613
center[2] = newSize;
16614
this.translate(center);
16615
each(this.points, function (point) {
16616
if (point.dataLabel) {
16617
point.dataLabel._pos = null; // reset
16621
if (this.drawDataLabels) {
16622
this.drawDataLabels();
16624
// Else, return true to indicate that the pie and its labels is within the plot area
16632
if (seriesTypes.column) {
16635
* Override the basic data label alignment by adjusting for the position of the column
16637
seriesTypes.column.prototype.alignDataLabel = function (point, dataLabel, options, alignTo, isNew) {
16638
var chart = this.chart,
16639
inverted = chart.inverted,
16640
dlBox = point.dlBox || point.shapeArgs, // data label box for alignment
16641
below = point.below || (point.plotY > pick(this.translatedThreshold, chart.plotSizeY)),
16642
inside = pick(options.inside, !!this.options.stacking); // draw it inside the box?
16644
// Align to the column itself, or the top of it
16645
if (dlBox) { // Area range uses this method but not alignTo
16646
alignTo = merge(dlBox);
16650
x: chart.plotWidth - alignTo.y - alignTo.height,
16651
y: chart.plotHeight - alignTo.x - alignTo.width,
16652
width: alignTo.height,
16653
height: alignTo.width
16657
// Compute the alignment box
16660
alignTo.x += below ? 0 : alignTo.width;
16663
alignTo.y += below ? alignTo.height : 0;
16664
alignTo.height = 0;
16670
// When alignment is undefined (typically columns and bars), display the individual
16671
// point below or above the point depending on the threshold
16672
options.align = pick(
16674
!inverted || inside ? 'center' : below ? 'right' : 'left'
16676
options.verticalAlign = pick(
16677
options.verticalAlign,
16678
inverted || inside ? 'middle' : below ? 'top' : 'bottom'
16681
// Call the parent method
16682
Series.prototype.alignDataLabel.call(this, point, dataLabel, options, alignTo, isNew);
16689
* TrackerMixin for points and graphs
16692
var TrackerMixin = Highcharts.TrackerMixin = {
16694
drawTrackerPoint: function () {
16696
chart = series.chart,
16697
pointer = chart.pointer,
16698
cursor = series.options.cursor,
16699
css = cursor && { cursor: cursor },
16700
onMouseOver = function (e) {
16701
var target = e.target,
16704
if (chart.hoverSeries !== series) {
16705
series.onMouseOver();
16708
while (target && !point) {
16709
point = target.point;
16710
target = target.parentNode;
16713
if (point !== UNDEFINED && point !== chart.hoverPoint) { // undefined on graph in scatterchart
16714
point.onMouseOver(e);
16718
// Add reference to the point
16719
each(series.points, function (point) {
16720
if (point.graphic) {
16721
point.graphic.element.point = point;
16723
if (point.dataLabel) {
16724
point.dataLabel.element.point = point;
16728
// Add the event listeners, we need to do this only once
16729
if (!series._hasTracking) {
16730
each(series.trackerGroups, function (key) {
16731
if (series[key]) { // we don't always have dataLabelsGroup
16733
.addClass(PREFIX + 'tracker')
16734
.on('mouseover', onMouseOver)
16735
.on('mouseout', function (e) { pointer.onTrackerMouseOut(e); })
16738
series[key].on('touchstart', onMouseOver);
16742
series._hasTracking = true;
16747
* Draw the tracker object that sits above all data labels and markers to
16748
* track mouse events on the graph or points. For the line type charts
16749
* the tracker uses the same graphPath, but with a greater stroke width
16750
* for better control.
16752
drawTrackerGraph: function () {
16754
options = series.options,
16755
trackByArea = options.trackByArea,
16756
trackerPath = [].concat(trackByArea ? series.areaPath : series.graphPath),
16757
trackerPathLength = trackerPath.length,
16758
chart = series.chart,
16759
pointer = chart.pointer,
16760
renderer = chart.renderer,
16761
snap = chart.options.tooltip.snap,
16762
tracker = series.tracker,
16763
cursor = options.cursor,
16764
css = cursor && { cursor: cursor },
16765
singlePoints = series.singlePoints,
16768
onMouseOver = function () {
16769
if (chart.hoverSeries !== series) {
16770
series.onMouseOver();
16774
* Empirical lowest possible opacities for TRACKER_FILL for an element to stay invisible but clickable
16778
* IE9: 0.00000000001 (unlimited)
16779
* IE10: 0.0001 (exporting only)
16780
* FF: 0.00000000001 (unlimited)
16783
* Opera: 0.00000000001 (unlimited)
16785
TRACKER_FILL = 'rgba(192,192,192,' + (hasSVG ? 0.0001 : 0.002) + ')';
16787
// Extend end points. A better way would be to use round linecaps,
16788
// but those are not clickable in VML.
16789
if (trackerPathLength && !trackByArea) {
16790
i = trackerPathLength + 1;
16792
if (trackerPath[i] === M) { // extend left side
16793
trackerPath.splice(i + 1, 0, trackerPath[i + 1] - snap, trackerPath[i + 2], L);
16795
if ((i && trackerPath[i] === M) || i === trackerPathLength) { // extend right side
16796
trackerPath.splice(i, 0, L, trackerPath[i - 2] + snap, trackerPath[i - 1]);
16801
// handle single points
16802
for (i = 0; i < singlePoints.length; i++) {
16803
singlePoint = singlePoints[i];
16804
trackerPath.push(M, singlePoint.plotX - snap, singlePoint.plotY,
16805
L, singlePoint.plotX + snap, singlePoint.plotY);
16808
// draw the tracker
16810
tracker.attr({ d: trackerPath });
16813
series.tracker = renderer.path(trackerPath)
16815
'stroke-linejoin': 'round', // #1225
16816
visibility: series.visible ? VISIBLE : HIDDEN,
16817
stroke: TRACKER_FILL,
16818
fill: trackByArea ? TRACKER_FILL : NONE,
16819
'stroke-width' : options.lineWidth + (trackByArea ? 0 : 2 * snap),
16822
.add(series.group);
16824
// The tracker is added to the series group, which is clipped, but is covered
16825
// by the marker group. So the marker group also needs to capture events.
16826
each([series.tracker, series.markerGroup], function (tracker) {
16827
tracker.addClass(PREFIX + 'tracker')
16828
.on('mouseover', onMouseOver)
16829
.on('mouseout', function (e) { pointer.onTrackerMouseOut(e); })
16833
tracker.on('touchstart', onMouseOver);
16839
/* End TrackerMixin */
16843
* Add tracking event listener to the series group, so the point graphics
16844
* themselves act as trackers
16847
if (seriesTypes.column) {
16848
ColumnSeries.prototype.drawTracker = TrackerMixin.drawTrackerPoint;
16851
if (seriesTypes.pie) {
16852
seriesTypes.pie.prototype.drawTracker = TrackerMixin.drawTrackerPoint;
16855
if (seriesTypes.scatter) {
16856
ScatterSeries.prototype.drawTracker = TrackerMixin.drawTrackerPoint;
16860
* Extend Legend for item events
16862
extend(Legend.prototype, {
16864
setItemEvents: function (item, legendItem, useHTML, itemStyle, itemHiddenStyle) {
16866
// Set the events on the item group, or in case of useHTML, the item itself (#1249)
16867
(useHTML ? legendItem : item.legendGroup).on('mouseover', function () {
16868
item.setState(HOVER_STATE);
16869
legendItem.css(legend.options.itemHoverStyle);
16871
.on('mouseout', function () {
16872
legendItem.css(item.visible ? itemStyle : itemHiddenStyle);
16875
.on('click', function (event) {
16876
var strLegendItemClick = 'legendItemClick',
16877
fnLegendItemClick = function () {
16881
// Pass over the click/touch event. #4.
16883
browserEvent: event
16886
// click the name or symbol
16887
if (item.firePointEvent) { // point
16888
item.firePointEvent(strLegendItemClick, event, fnLegendItemClick);
16890
fireEvent(item, strLegendItemClick, event, fnLegendItemClick);
16895
createCheckboxForItem: function (item) {
16898
item.checkbox = createElement('input', {
16900
checked: item.selected,
16901
defaultChecked: item.selected // required by IE7
16902
}, legend.options.itemCheckboxStyle, legend.chart.container);
16904
addEvent(item.checkbox, 'click', function (event) {
16905
var target = event.target;
16906
fireEvent(item, 'checkboxClick', {
16907
checked: target.checked
16918
* Add pointer cursor to legend itemstyle in defaultOptions
16920
defaultOptions.legend.itemStyle.cursor = 'pointer';
16924
* Extend the Chart object with interaction
16927
extend(Chart.prototype, {
16929
* Display the zoom button
16931
showResetZoom: function () {
16933
lang = defaultOptions.lang,
16934
btnOptions = chart.options.chart.resetZoomButton,
16935
theme = btnOptions.theme,
16936
states = theme.states,
16937
alignTo = btnOptions.relativeTo === 'chart' ? null : 'plotBox';
16939
this.resetZoomButton = chart.renderer.button(lang.resetZoom, null, null, function () { chart.zoomOut(); }, theme, states && states.hover)
16941
align: btnOptions.position.align,
16942
title: lang.resetZoomTitle
16945
.align(btnOptions.position, false, alignTo);
16952
zoomOut: function () {
16954
fireEvent(chart, 'selection', { resetSelection: true }, function () {
16960
* Zoom into a given portion of the chart given by axis coordinates
16961
* @param {Object} event
16963
zoom: function (event) {
16966
pointer = chart.pointer,
16967
displayButton = false,
16970
// If zoom is called with no arguments, reset the axes
16971
if (!event || event.resetSelection) {
16972
each(chart.axes, function (axis) {
16973
hasZoomed = axis.zoom();
16975
} else { // else, zoom in on all axes
16976
each(event.xAxis.concat(event.yAxis), function (axisData) {
16977
var axis = axisData.axis,
16978
isXAxis = axis.isXAxis;
16980
// don't zoom more than minRange
16981
if (pointer[isXAxis ? 'zoomX' : 'zoomY'] || pointer[isXAxis ? 'pinchX' : 'pinchY']) {
16982
hasZoomed = axis.zoom(axisData.min, axisData.max);
16983
if (axis.displayBtn) {
16984
displayButton = true;
16990
// Show or hide the Reset zoom button
16991
resetZoomButton = chart.resetZoomButton;
16992
if (displayButton && !resetZoomButton) {
16993
chart.showResetZoom();
16994
} else if (!displayButton && isObject(resetZoomButton)) {
16995
chart.resetZoomButton = resetZoomButton.destroy();
17002
pick(chart.options.chart.animation, event && event.animation, chart.pointCount < 100) // animation
17008
* Pan the chart by dragging the mouse across the pane. This function is called
17009
* on mouse move, and the distance to pan is computed from chartX compared to
17010
* the first chartX position in the dragging operation.
17012
pan: function (e, panning) {
17015
hoverPoints = chart.hoverPoints,
17018
// remove active points for shared tooltip
17020
each(hoverPoints, function (point) {
17025
each(panning === 'xy' ? [1, 0] : [1], function (isX) { // xy is used in maps
17026
var mousePos = e[isX ? 'chartX' : 'chartY'],
17027
axis = chart[isX ? 'xAxis' : 'yAxis'][0],
17028
startPos = chart[isX ? 'mouseDownX' : 'mouseDownY'],
17029
halfPointRange = (axis.pointRange || 0) / 2,
17030
extremes = axis.getExtremes(),
17031
newMin = axis.toValue(startPos - mousePos, true) + halfPointRange,
17032
newMax = axis.toValue(startPos + chart[isX ? 'plotWidth' : 'plotHeight'] - mousePos, true) - halfPointRange;
17034
if (axis.series.length && newMin > mathMin(extremes.dataMin, extremes.min) && newMax < mathMax(extremes.dataMax, extremes.max)) {
17035
axis.setExtremes(newMin, newMax, false, false, { trigger: 'pan' });
17039
chart[isX ? 'mouseDownX' : 'mouseDownY'] = mousePos; // set new reference for next run
17043
chart.redraw(false);
17045
css(chart.container, { cursor: 'move' });
17050
* Extend the Point object with interaction
17052
extend(Point.prototype, {
17054
* Toggle the selection status of a point
17055
* @param {Boolean} selected Whether to select or unselect the point.
17056
* @param {Boolean} accumulate Whether to add to the previous selection. By default,
17057
* this happens if the control key (Cmd on Mac) was pressed during clicking.
17059
select: function (selected, accumulate) {
17061
series = point.series,
17062
chart = series.chart;
17064
selected = pick(selected, !point.selected);
17066
// fire the event with the defalut handler
17067
point.firePointEvent(selected ? 'select' : 'unselect', { accumulate: accumulate }, function () {
17068
point.selected = point.options.selected = selected;
17069
series.options.data[inArray(point, series.data)] = point.options;
17071
point.setState(selected && SELECT_STATE);
17073
// unselect all other points unless Ctrl or Cmd + click
17075
each(chart.getSelectedPoints(), function (loopPoint) {
17076
if (loopPoint.selected && loopPoint !== point) {
17077
loopPoint.selected = loopPoint.options.selected = false;
17078
series.options.data[inArray(loopPoint, series.data)] = loopPoint.options;
17079
loopPoint.setState(NORMAL_STATE);
17080
loopPoint.firePointEvent('unselect');
17088
* Runs on mouse over the point
17090
onMouseOver: function (e) {
17092
series = point.series,
17093
chart = series.chart,
17094
tooltip = chart.tooltip,
17095
hoverPoint = chart.hoverPoint;
17097
// set normal state to previous series
17098
if (hoverPoint && hoverPoint !== point) {
17099
hoverPoint.onMouseOut();
17102
// trigger the event
17103
point.firePointEvent('mouseOver');
17105
// update the tooltip
17106
if (tooltip && (!tooltip.shared || series.noSharedTooltip)) {
17107
tooltip.refresh(point, e);
17111
point.setState(HOVER_STATE);
17112
chart.hoverPoint = point;
17116
* Runs on mouse out from the point
17118
onMouseOut: function () {
17119
var chart = this.series.chart,
17120
hoverPoints = chart.hoverPoints;
17122
if (!hoverPoints || inArray(this, hoverPoints) === -1) { // #887
17123
this.firePointEvent('mouseOut');
17126
chart.hoverPoint = null;
17131
* Fire an event on the Point object. Must not be renamed to fireEvent, as this
17132
* causes a name clash in MooTools
17133
* @param {String} eventType
17134
* @param {Object} eventArgs Additional event arguments
17135
* @param {Function} defaultFunction Default event handler
17137
firePointEvent: function (eventType, eventArgs, defaultFunction) {
17139
series = this.series,
17140
seriesOptions = series.options;
17142
// load event handlers on demand to save time on mouseover/out
17143
if (seriesOptions.point.events[eventType] || (point.options && point.options.events && point.options.events[eventType])) {
17144
this.importEvents();
17147
// add default handler if in selection mode
17148
if (eventType === 'click' && seriesOptions.allowPointSelect) {
17149
defaultFunction = function (event) {
17150
// Control key is for Windows, meta (= Cmd key) for Mac, Shift for Opera
17151
point.select(null, event.ctrlKey || event.metaKey || event.shiftKey);
17155
fireEvent(this, eventType, eventArgs, defaultFunction);
17158
* Import events from the series' and point's options. Only do it on
17159
* demand, to save processing time on hovering.
17161
importEvents: function () {
17162
if (!this.hasImportedEvents) {
17164
options = merge(point.series.options.point, point.options),
17165
events = options.events,
17168
point.events = events;
17170
for (eventType in events) {
17171
addEvent(point, eventType, events[eventType]);
17173
this.hasImportedEvents = true;
17179
* Set the point's state
17180
* @param {String} state
17182
setState: function (state, move) {
17184
plotX = point.plotX,
17185
plotY = point.plotY,
17186
series = point.series,
17187
stateOptions = series.options.states,
17188
markerOptions = defaultPlotOptions[series.type].marker && series.options.marker,
17189
normalDisabled = markerOptions && !markerOptions.enabled,
17190
markerStateOptions = markerOptions && markerOptions.states[state],
17191
stateDisabled = markerStateOptions && markerStateOptions.enabled === false,
17192
stateMarkerGraphic = series.stateMarkerGraphic,
17193
pointMarker = point.marker || {},
17194
chart = series.chart,
17197
pointAttr = point.pointAttr;
17199
state = state || NORMAL_STATE; // empty string
17200
move = move && stateMarkerGraphic;
17203
// already has this state
17204
(state === point.state && !move) ||
17205
// selected points don't respond to hover
17206
(point.selected && state !== SELECT_STATE) ||
17207
// series' state options is disabled
17208
(stateOptions[state] && stateOptions[state].enabled === false) ||
17209
// general point marker's state options is disabled
17210
(state && (stateDisabled || (normalDisabled && !markerStateOptions.enabled))) ||
17211
// individual point marker's state options is disabled
17212
(state && pointMarker.states && pointMarker.states[state] && pointMarker.states[state].enabled === false) // #1610
17219
// apply hover styles to the existing point
17220
if (point.graphic) {
17221
radius = markerOptions && point.graphic.symbolName && pointAttr[state].r;
17222
point.graphic.attr(merge(
17224
radius ? { // new symbol attributes (#507, #612)
17232
// if a graphic is not applied to each point in the normal state, create a shared
17233
// graphic for the hover state
17234
if (state && markerStateOptions) {
17235
radius = markerStateOptions.radius;
17236
newSymbol = pointMarker.symbol || series.symbol;
17238
// If the point has another symbol than the previous one, throw away the
17239
// state marker graphic and force a new one (#1459)
17240
if (stateMarkerGraphic && stateMarkerGraphic.currentSymbol !== newSymbol) {
17241
stateMarkerGraphic = stateMarkerGraphic.destroy();
17244
// Add a new state marker graphic
17245
if (!stateMarkerGraphic) {
17246
series.stateMarkerGraphic = stateMarkerGraphic = chart.renderer.symbol(
17253
.attr(pointAttr[state])
17254
.add(series.markerGroup);
17255
stateMarkerGraphic.currentSymbol = newSymbol;
17257
// Move the existing graphic
17259
stateMarkerGraphic[move ? 'animate' : 'attr']({ // #1054
17266
if (stateMarkerGraphic) {
17267
stateMarkerGraphic[state && chart.isInsidePlot(plotX, plotY, chart.inverted) ? 'show' : 'hide'](); // #2450
17271
point.state = state;
17276
* Extend the Series object with interaction
17279
extend(Series.prototype, {
17281
* Series mouse over handler
17283
onMouseOver: function () {
17285
chart = series.chart,
17286
hoverSeries = chart.hoverSeries;
17288
// set normal state to previous series
17289
if (hoverSeries && hoverSeries !== series) {
17290
hoverSeries.onMouseOut();
17293
// trigger the event, but to save processing time,
17295
if (series.options.events.mouseOver) {
17296
fireEvent(series, 'mouseOver');
17300
series.setState(HOVER_STATE);
17301
chart.hoverSeries = series;
17305
* Series mouse out handler
17307
onMouseOut: function () {
17308
// trigger the event only if listeners exist
17310
options = series.options,
17311
chart = series.chart,
17312
tooltip = chart.tooltip,
17313
hoverPoint = chart.hoverPoint;
17315
// trigger mouse out on the point, which must be in this series
17317
hoverPoint.onMouseOut();
17320
// fire the mouse out event
17321
if (series && options.events.mouseOut) {
17322
fireEvent(series, 'mouseOut');
17326
// hide the tooltip
17327
if (tooltip && !options.stickyTracking && (!tooltip.shared || series.noSharedTooltip)) {
17331
// set normal state
17333
chart.hoverSeries = null;
17337
* Set the state of the graph
17339
setState: function (state) {
17341
options = series.options,
17342
graph = series.graph,
17343
graphNeg = series.graphNeg,
17344
stateOptions = options.states,
17345
lineWidth = options.lineWidth,
17348
state = state || NORMAL_STATE;
17350
if (series.state !== state) {
17351
series.state = state;
17353
if (stateOptions[state] && stateOptions[state].enabled === false) {
17358
lineWidth = stateOptions[state].lineWidth || lineWidth + 1;
17361
if (graph && !graph.dashstyle) { // hover is turned off for dashed lines in VML
17363
'stroke-width': lineWidth
17365
// use attr because animate will cause any other animation on the graph to stop
17366
graph.attr(attribs);
17368
graphNeg.attr(attribs);
17375
* Set the visibility of the graph
17377
* @param vis {Boolean} True to show the series, false to hide. If UNDEFINED,
17378
* the visibility is toggled.
17380
setVisible: function (vis, redraw) {
17382
chart = series.chart,
17383
legendItem = series.legendItem,
17385
ignoreHiddenSeries = chart.options.chart.ignoreHiddenSeries,
17386
oldVisibility = series.visible;
17388
// if called without an argument, toggle visibility
17389
series.visible = vis = series.userOptions.visible = vis === UNDEFINED ? !oldVisibility : vis;
17390
showOrHide = vis ? 'show' : 'hide';
17392
// show or hide elements
17393
each(['group', 'dataLabelsGroup', 'markerGroup', 'tracker'], function (key) {
17395
series[key][showOrHide]();
17400
// hide tooltip (#1361)
17401
if (chart.hoverSeries === series) {
17402
series.onMouseOut();
17407
chart.legend.colorizeItem(series, vis);
17411
// rescale or adapt to resized chart
17412
series.isDirty = true;
17413
// in a stack, all other series are affected
17414
if (series.options.stacking) {
17415
each(chart.series, function (otherSeries) {
17416
if (otherSeries.options.stacking && otherSeries.visible) {
17417
otherSeries.isDirty = true;
17422
// show or hide linked series
17423
each(series.linkedSeries, function (otherSeries) {
17424
otherSeries.setVisible(vis, false);
17427
if (ignoreHiddenSeries) {
17428
chart.isDirtyBox = true;
17430
if (redraw !== false) {
17434
fireEvent(series, showOrHide);
17438
* Memorize tooltip texts and positions
17440
setTooltipPoints: function (renew) {
17446
xAxis = series.xAxis,
17447
xExtremes = xAxis && xAxis.getExtremes(),
17448
axisLength = xAxis ? (xAxis.tooltipLen || xAxis.len) : series.chart.plotSizeX, // tooltipLen and tooltipPosName used in polar
17453
tooltipPoints = []; // a lookup array for each pixel in the x dimension
17455
// don't waste resources if tracker is disabled
17456
if (series.options.enableMouseTracking === false || series.singularTooltips) {
17462
series.tooltipPoints = null;
17465
// concat segments to overcome null values
17466
each(series.segments || series.points, function (segment) {
17467
points = points.concat(segment);
17470
// Reverse the points in case the X axis is reversed
17471
if (xAxis && xAxis.reversed) {
17472
points = points.reverse();
17475
// Polar needs additional shaping
17476
if (series.orderTooltipPoints) {
17477
series.orderTooltipPoints(points);
17480
// Assign each pixel position to the nearest point
17481
pointsLength = points.length;
17482
for (i = 0; i < pointsLength; i++) {
17485
if (pointX >= xExtremes.min && pointX <= xExtremes.max) { // #1149
17486
nextPoint = points[i + 1];
17488
// Set this range's low to the last range's high plus one
17489
low = high === UNDEFINED ? 0 : high + 1;
17490
// Now find the new high
17491
high = points[i + 1] ?
17492
mathMin(mathMax(0, mathFloor( // #2070
17493
(point.clientX + (nextPoint ? (nextPoint.wrappedClientX || nextPoint.clientX) : axisLength)) / 2
17497
while (low >= 0 && low <= high) {
17498
tooltipPoints[low++] = point;
17502
series.tooltipPoints = tooltipPoints;
17508
show: function () {
17509
this.setVisible(true);
17515
hide: function () {
17516
this.setVisible(false);
17521
* Set the selected state of the graph
17523
* @param selected {Boolean} True to select the series, false to unselect. If
17524
* UNDEFINED, the selection state is toggled.
17526
select: function (selected) {
17528
// if called without an argument, toggle
17529
series.selected = selected = (selected === UNDEFINED) ? !series.selected : selected;
17531
if (series.checkbox) {
17532
series.checkbox.checked = selected;
17535
fireEvent(series, selected ? 'select' : 'unselect');
17538
drawTracker: TrackerMixin.drawTrackerGraph
17539
});/* ****************************************************************************
17540
* Start ordinal axis logic *
17541
*****************************************************************************/
17544
wrap(Series.prototype, 'init', function (proceed) {
17548
// call the original function
17549
proceed.apply(this, Array.prototype.slice.call(arguments, 1));
17551
xAxis = series.xAxis;
17553
// Destroy the extended ordinal index on updated data
17554
if (xAxis && xAxis.options.ordinal) {
17555
addEvent(series, 'updatedData', function () {
17556
delete xAxis.ordinalIndex;
17562
* In an ordinal axis, there might be areas with dense consentrations of points, then large
17563
* gaps between some. Creating equally distributed ticks over this entire range
17564
* may lead to a huge number of ticks that will later be removed. So instead, break the
17565
* positions up in segments, find the tick positions for each segment then concatenize them.
17566
* This method is used from both data grouping logic and X axis tick position logic.
17568
wrap(Axis.prototype, 'getTimeTicks', function (proceed, normalizedInterval, min, max, startOfWeek, positions, closestDistance, findHigherRanks) {
17574
hasCrossedHigherRank,
17578
groupPositions = [],
17579
lastGroupPosition = -Number.MAX_VALUE,
17580
tickPixelIntervalOption = this.options.tickPixelInterval;
17582
// The positions are not always defined, for example for ordinal positions when data
17583
// has regular interval (#1557, #2090)
17584
if (!this.options.ordinal || !positions || positions.length < 3 || min === UNDEFINED) {
17585
return proceed.call(this, normalizedInterval, min, max, startOfWeek);
17588
// Analyze the positions array to split it into segments on gaps larger than 5 times
17589
// the closest distance. The closest distance is already found at this point, so
17590
// we reuse that instead of computing it again.
17591
posLength = positions.length;
17592
for (; end < posLength; end++) {
17594
outsideMax = end && positions[end - 1] > max;
17596
if (positions[end] < min) { // Set the last position before min
17600
if (end === posLength - 1 || positions[end + 1] - positions[end] > closestDistance * 5 || outsideMax) {
17602
// For each segment, calculate the tick positions from the getTimeTicks utility
17603
// function. The interval will be the same regardless of how long the segment is.
17604
if (positions[end] > lastGroupPosition) { // #1475
17606
segmentPositions = proceed.call(this, normalizedInterval, positions[start], positions[end], startOfWeek);
17608
// Prevent duplicate groups, for example for multiple segments within one larger time frame (#1475)
17609
while (segmentPositions.length && segmentPositions[0] <= lastGroupPosition) {
17610
segmentPositions.shift();
17612
if (segmentPositions.length) {
17613
lastGroupPosition = segmentPositions[segmentPositions.length - 1];
17616
groupPositions = groupPositions.concat(segmentPositions);
17618
// Set start of next segment
17627
// Get the grouping info from the last of the segments. The info is the same for
17629
info = segmentPositions.info;
17631
// Optionally identify ticks with higher rank, for example when the ticks
17632
// have crossed midnight.
17633
if (findHigherRanks && info.unitRange <= timeUnits[HOUR]) {
17634
end = groupPositions.length - 1;
17636
// Compare points two by two
17637
for (start = 1; start < end; start++) {
17638
if (new Date(groupPositions[start] - timezoneOffset)[getDate]() !== new Date(groupPositions[start - 1] - timezoneOffset)[getDate]()) {
17639
higherRanks[groupPositions[start]] = DAY;
17640
hasCrossedHigherRank = true;
17644
// If the complete array has crossed midnight, we want to mark the first
17645
// positions also as higher rank
17646
if (hasCrossedHigherRank) {
17647
higherRanks[groupPositions[0]] = DAY;
17649
info.higherRanks = higherRanks;
17653
groupPositions.info = info;
17657
// Don't show ticks within a gap in the ordinal axis, where the space between
17658
// two points is greater than a portion of the tick pixel interval
17659
if (findHigherRanks && defined(tickPixelIntervalOption)) { // check for squashed ticks
17661
var length = groupPositions.length,
17665
translatedArr = [],
17671
// Find median pixel distance in order to keep a reasonably even distance between
17674
translated = this.translate(groupPositions[i]);
17675
if (lastTranslated) {
17676
distances[i] = lastTranslated - translated;
17678
translatedArr[i] = lastTranslated = translated;
17681
medianDistance = distances[mathFloor(distances.length / 2)];
17682
if (medianDistance < tickPixelIntervalOption * 0.6) {
17683
medianDistance = null;
17686
// Now loop over again and remove ticks where needed
17687
i = groupPositions[length - 1] > max ? length - 1 : length; // #817
17688
lastTranslated = undefined;
17690
translated = translatedArr[i];
17691
distance = lastTranslated - translated;
17693
// Remove ticks that are closer than 0.6 times the pixel interval from the one to the right,
17694
// but not if it is close to the median distance (#748).
17695
if (lastTranslated && distance < tickPixelIntervalOption * 0.8 &&
17696
(medianDistance === null || distance < medianDistance * 0.8)) {
17698
// Is this a higher ranked position with a normal position to the right?
17699
if (higherRanks[groupPositions[i]] && !higherRanks[groupPositions[i + 1]]) {
17701
// Yes: remove the lower ranked neighbour to the right
17702
itemToRemove = i + 1;
17703
lastTranslated = translated; // #709
17707
// No: remove this one
17711
groupPositions.splice(itemToRemove, 1);
17714
lastTranslated = translated;
17718
return groupPositions;
17721
// Extend the Axis prototype
17722
extend(Axis.prototype, {
17725
* Calculate the ordinal positions before tick positions are calculated.
17727
beforeSetTickPositions: function () {
17730
ordinalPositions = [],
17731
useOrdinal = false,
17733
extremes = axis.getExtremes(),
17734
min = extremes.min,
17735
max = extremes.max,
17741
// apply the ordinal logic
17742
if (axis.options.ordinal) {
17744
each(axis.series, function (series, i) {
17746
if (series.visible !== false && series.takeOrdinalPosition !== false) {
17748
// concatenate the processed X data into the existing positions, or the empty array
17749
ordinalPositions = ordinalPositions.concat(series.processedXData);
17750
len = ordinalPositions.length;
17752
// remove duplicates (#1588)
17753
ordinalPositions.sort(function (a, b) {
17754
return a - b; // without a custom function it is sorted as strings
17760
if (ordinalPositions[i] === ordinalPositions[i + 1]) {
17761
ordinalPositions.splice(i, 1);
17769
// cache the length
17770
len = ordinalPositions.length;
17772
// Check if we really need the overhead of mapping axis data against the ordinal positions.
17773
// If the series consist of evenly spaced data any way, we don't need any ordinal logic.
17774
if (len > 2) { // two points have equal distance by default
17775
dist = ordinalPositions[1] - ordinalPositions[0];
17777
while (i-- && !useOrdinal) {
17778
if (ordinalPositions[i + 1] - ordinalPositions[i] !== dist) {
17783
// When zooming in on a week, prevent axis padding for weekends even though the data within
17784
// the week is evenly spaced.
17785
if (!axis.options.keepOrdinalPadding && (ordinalPositions[0] - min > dist || max - ordinalPositions[ordinalPositions.length - 1] > dist)) {
17790
// Record the slope and offset to compute the linear values from the array index.
17791
// Since the ordinal positions may exceed the current range, get the start and
17792
// end positions within it (#719, #665b)
17796
axis.ordinalPositions = ordinalPositions;
17798
// This relies on the ordinalPositions being set. Use mathMax and mathMin to prevent
17799
// padding on either sides of the data.
17800
minIndex = axis.val2lin(mathMax(min, ordinalPositions[0]), true);
17801
maxIndex = axis.val2lin(mathMin(max, ordinalPositions[ordinalPositions.length - 1]), true);
17803
// Set the slope and offset of the values compared to the indices in the ordinal positions
17804
axis.ordinalSlope = slope = (max - min) / (maxIndex - minIndex);
17805
axis.ordinalOffset = min - (minIndex * slope);
17808
axis.ordinalPositions = axis.ordinalSlope = axis.ordinalOffset = UNDEFINED;
17811
axis.groupIntervalFactor = null; // reset for next run
17814
* Translate from a linear axis value to the corresponding ordinal axis position. If there
17815
* are no gaps in the ordinal axis this will be the same. The translated value is the value
17816
* that the point would have if the axis were linear, using the same min and max.
17818
* @param Number val The axis value
17819
* @param Boolean toIndex Whether to return the index in the ordinalPositions or the new value
17821
val2lin: function (val, toIndex) {
17823
ordinalPositions = axis.ordinalPositions;
17825
if (!ordinalPositions) {
17830
var ordinalLength = ordinalPositions.length,
17835
// first look for an exact match in the ordinalpositions array
17838
if (ordinalPositions[i] === val) {
17844
// if that failed, find the intermediate position between the two nearest values
17845
i = ordinalLength - 1;
17847
if (val > ordinalPositions[i] || i === 0) { // interpolate
17848
distance = (val - ordinalPositions[i]) / (ordinalPositions[i + 1] - ordinalPositions[i]); // something between 0 and 1
17849
ordinalIndex = i + distance;
17855
axis.ordinalSlope * (ordinalIndex || 0) + axis.ordinalOffset;
17859
* Translate from linear (internal) to axis value
17861
* @param Number val The linear abstracted value
17862
* @param Boolean fromIndex Translate from an index in the ordinal positions rather than a value
17864
lin2val: function (val, fromIndex) {
17866
ordinalPositions = axis.ordinalPositions;
17868
if (!ordinalPositions) { // the visible range contains only equally spaced values
17873
var ordinalSlope = axis.ordinalSlope,
17874
ordinalOffset = axis.ordinalOffset,
17875
i = ordinalPositions.length - 1,
17876
linearEquivalentLeft,
17877
linearEquivalentRight,
17881
// Handle the case where we translate from the index directly, used only
17882
// when panning an ordinal axis
17885
if (val < 0) { // out of range, in effect panning to the left
17886
val = ordinalPositions[0];
17887
} else if (val > i) { // out of range, panning to the right
17888
val = ordinalPositions[i];
17889
} else { // split it up
17890
i = mathFloor(val);
17891
distance = val - i; // the decimal
17894
// Loop down along the ordinal positions. When the linear equivalent of i matches
17895
// an ordinal position, interpolate between the left and right values.
17898
linearEquivalentLeft = (ordinalSlope * i) + ordinalOffset;
17899
if (val >= linearEquivalentLeft) {
17900
linearEquivalentRight = (ordinalSlope * (i + 1)) + ordinalOffset;
17901
distance = (val - linearEquivalentLeft) / (linearEquivalentRight - linearEquivalentLeft); // something between 0 and 1
17907
// If the index is within the range of the ordinal positions, return the associated
17908
// or interpolated value. If not, just return the value
17909
return distance !== UNDEFINED && ordinalPositions[i] !== UNDEFINED ?
17910
ordinalPositions[i] + (distance ? distance * (ordinalPositions[i + 1] - ordinalPositions[i]) : 0) :
17915
* Get the ordinal positions for the entire data set. This is necessary in chart panning
17916
* because we need to find out what points or data groups are available outside the
17917
* visible range. When a panning operation starts, if an index for the given grouping
17918
* does not exists, it is created and cached. This index is deleted on updated data, so
17919
* it will be regenerated the next time a panning operation starts.
17921
getExtendedPositions: function () {
17923
chart = axis.chart,
17924
grouping = axis.series[0].currentDataGrouping,
17925
ordinalIndex = axis.ordinalIndex,
17926
key = grouping ? grouping.count + grouping.unitName : 'raw',
17927
extremes = axis.getExtremes(),
17931
// If this is the first time, or the ordinal index is deleted by updatedData,
17933
if (!ordinalIndex) {
17934
ordinalIndex = axis.ordinalIndex = {};
17938
if (!ordinalIndex[key]) {
17940
// Create a fake axis object where the extended ordinal positions are emulated
17943
getExtremes: function () {
17945
min: extremes.dataMin,
17946
max: extremes.dataMax
17952
val2lin: Axis.prototype.val2lin // #2590
17955
// Add the fake series to hold the full data, then apply processData to it
17956
each(axis.series, function (series) {
17959
xData: series.xData,
17961
destroyGroupedData: noop
17963
fakeSeries.options = {
17964
dataGrouping : grouping ? {
17967
approximation: 'open', // doesn't matter which, use the fastest
17968
units: [[grouping.unitName, [grouping.count]]]
17973
series.processData.apply(fakeSeries);
17975
fakeAxis.series.push(fakeSeries);
17978
// Run beforeSetTickPositions to compute the ordinalPositions
17979
axis.beforeSetTickPositions.apply(fakeAxis);
17982
ordinalIndex[key] = fakeAxis.ordinalPositions;
17984
return ordinalIndex[key];
17988
* Find the factor to estimate how wide the plot area would have been if ordinal
17989
* gaps were included. This value is used to compute an imagined plot width in order
17990
* to establish the data grouping interval.
17992
* A real world case is the intraday-candlestick
17993
* example. Without this logic, it would show the correct data grouping when viewing
17994
* a range within each day, but once moving the range to include the gap between two
17995
* days, the interval would include the cut-away night hours and the data grouping
17996
* would be wrong. So the below method tries to compensate by identifying the most
17997
* common point interval, in this case days.
17999
* An opposite case is presented in issue #718. We have a long array of daily data,
18000
* then one point is appended one hour after the last point. We expect the data grouping
18003
* In the future, if we find cases where this estimation doesn't work optimally, we
18004
* might need to add a second pass to the data grouping logic, where we do another run
18005
* with a greater interval if the number of data groups is more than a certain fraction
18006
* of the desired group count.
18008
getGroupIntervalFactor: function (xMin, xMax, series) {
18010
processedXData = series.processedXData,
18011
len = processedXData.length,
18014
groupIntervalFactor = this.groupIntervalFactor;
18016
// Only do this computation for the first series, let the other inherit it (#2416)
18017
if (!groupIntervalFactor) {
18019
// Register all the distances in an array
18020
for (; i < len - 1; i++) {
18021
distances[i] = processedXData[i + 1] - processedXData[i];
18024
// Sort them and find the median
18025
distances.sort(function (a, b) {
18028
median = distances[mathFloor(len / 2)];
18030
// Compensate for series that don't extend through the entire axis extent. #1675.
18031
xMin = mathMax(xMin, processedXData[0]);
18032
xMax = mathMin(xMax, processedXData[len - 1]);
18034
this.groupIntervalFactor = groupIntervalFactor = (len * median) / (xMax - xMin);
18037
// Return the factor needed for data grouping
18038
return groupIntervalFactor;
18042
* Make the tick intervals closer because the ordinal gaps make the ticks spread out or cluster
18044
postProcessTickInterval: function (tickInterval) {
18045
// TODO: http://jsfiddle.net/highcharts/FQm4E/1/
18046
// This is a case where this algorithm doesn't work optimally. In this case, the
18047
// tick labels are spread out per week, but all the gaps reside within weeks. So
18048
// we have a situation where the labels are courser than the ordinal gaps, and
18049
// thus the tick interval should not be altered
18050
var ordinalSlope = this.ordinalSlope;
18052
return ordinalSlope ?
18053
tickInterval / (ordinalSlope / this.closestPointRange) :
18058
// Extending the Chart.pan method for ordinal axes
18059
wrap(Chart.prototype, 'pan', function (proceed, e) {
18061
xAxis = chart.xAxis[0],
18065
if (xAxis.options.ordinal && xAxis.series.length) {
18067
var mouseDownX = chart.mouseDownX,
18068
extremes = xAxis.getExtremes(),
18069
dataMax = extremes.dataMax,
18070
min = extremes.min,
18071
max = extremes.max,
18073
hoverPoints = chart.hoverPoints,
18074
closestPointRange = xAxis.closestPointRange,
18075
pointPixelWidth = xAxis.translationSlope * (xAxis.ordinalSlope || closestPointRange),
18076
movedUnits = (mouseDownX - chartX) / pointPixelWidth, // how many ordinal units did we move?
18077
extendedAxis = { ordinalPositions: xAxis.getExtendedPositions() }, // get index of all the chart's points
18080
lin2val = xAxis.lin2val,
18081
val2lin = xAxis.val2lin,
18084
if (!extendedAxis.ordinalPositions) { // we have an ordinal axis, but the data is equally spaced
18087
} else if (mathAbs(movedUnits) > 1) {
18089
// Remove active points for shared tooltip
18091
each(hoverPoints, function (point) {
18096
if (movedUnits < 0) {
18097
searchAxisLeft = extendedAxis;
18098
searchAxisRight = xAxis.ordinalPositions ? xAxis : extendedAxis;
18100
searchAxisLeft = xAxis.ordinalPositions ? xAxis : extendedAxis;
18101
searchAxisRight = extendedAxis;
18104
// In grouped data series, the last ordinal position represents the grouped data, which is
18105
// to the left of the real data max. If we don't compensate for this, we will be allowed
18106
// to pan grouped data series passed the right of the plot area.
18107
ordinalPositions = searchAxisRight.ordinalPositions;
18108
if (dataMax > ordinalPositions[ordinalPositions.length - 1]) {
18109
ordinalPositions.push(dataMax);
18112
// Get the new min and max values by getting the ordinal index for the current extreme,
18113
// then add the moved units and translate back to values. This happens on the
18114
// extended ordinal positions if the new position is out of range, else it happens
18115
// on the current x axis which is smaller and faster.
18116
chart.fixedRange = max - min;
18117
trimmedRange = xAxis.toFixedRange(null, null,
18118
lin2val.apply(searchAxisLeft, [
18119
val2lin.apply(searchAxisLeft, [min, true]) + movedUnits, // the new index
18120
true // translate from index
18122
lin2val.apply(searchAxisRight, [
18123
val2lin.apply(searchAxisRight, [max, true]) + movedUnits, // the new index
18124
true // translate from index
18128
// Apply it if it is within the available data range
18129
if (trimmedRange.min >= mathMin(extremes.dataMin, min) && trimmedRange.max <= mathMax(dataMax, max)) {
18130
xAxis.setExtremes(trimmedRange.min, trimmedRange.max, true, false, { trigger: 'pan' });
18133
chart.mouseDownX = chartX; // set new reference for next run
18134
css(chart.container, { cursor: 'move' });
18141
// revert to the linear chart.pan version
18143
// call the original function
18144
proceed.apply(this, Array.prototype.slice.call(arguments, 1));
18151
* Extend getSegments by identifying gaps in the ordinal data so that we can draw a gap in the
18154
wrap(Series.prototype, 'getSegments', function (proceed) {
18158
gapSize = series.options.gapSize,
18159
xAxis = series.xAxis;
18161
// call base method
18162
proceed.apply(this, Array.prototype.slice.call(arguments, 1));
18167
segments = series.segments;
18169
// extension for ordinal breaks
18170
each(segments, function (segment, no) {
18171
var i = segment.length - 1;
18173
if (segment[i + 1].x - segment[i].x > xAxis.closestPointRange * gapSize) {
18174
segments.splice( // insert after this one
18177
segment.splice(i + 1, segment.length - i)
18185
/* ****************************************************************************
18186
* End ordinal axis logic *
18187
*****************************************************************************/
18188
/* ****************************************************************************
18189
* Start data grouping module *
18190
******************************************************************************/
18191
/*jslint white:true */
18192
var DATA_GROUPING = 'dataGrouping',
18193
seriesProto = Series.prototype,
18194
tooltipProto = Tooltip.prototype,
18195
baseProcessData = seriesProto.processData,
18196
baseGeneratePoints = seriesProto.generatePoints,
18197
baseDestroy = seriesProto.destroy,
18198
baseTooltipHeaderFormatter = tooltipProto.tooltipHeaderFormatter,
18202
approximation: 'average', // average, open, high, low, close, sum
18203
//enabled: null, // (true for stock charts, false for basic),
18204
//forced: undefined,
18205
groupPixelWidth: 2,
18206
// the first one is the point or start value, the second is the start value if we're dealing with range,
18207
// the third one is the end value if dealing with a range
18208
dateTimeLabelFormats: hash(
18209
MILLISECOND, ['%A, %b %e, %H:%M:%S.%L', '%A, %b %e, %H:%M:%S.%L', '-%H:%M:%S.%L'],
18210
SECOND, ['%A, %b %e, %H:%M:%S', '%A, %b %e, %H:%M:%S', '-%H:%M:%S'],
18211
MINUTE, ['%A, %b %e, %H:%M', '%A, %b %e, %H:%M', '-%H:%M'],
18212
HOUR, ['%A, %b %e, %H:%M', '%A, %b %e, %H:%M', '-%H:%M'],
18213
DAY, ['%A, %b %e, %Y', '%A, %b %e', '-%A, %b %e, %Y'],
18214
WEEK, ['Week from %A, %b %e, %Y', '%A, %b %e', '-%A, %b %e, %Y'],
18215
MONTH, ['%B %Y', '%B', '-%B %Y'],
18216
YEAR, ['%Y', '%Y', '-%Y']
18218
// smoothed = false, // enable this for navigator series only
18221
specificOptions = { // extends common options
18227
approximation: 'sum',
18228
groupPixelWidth: 10
18231
approximation: 'range'
18234
approximation: 'range'
18237
approximation: 'range',
18238
groupPixelWidth: 10
18241
approximation: 'ohlc',
18242
groupPixelWidth: 10
18245
approximation: 'ohlc',
18250
// units are defined in a separate array to allow complete overriding in case of a user option
18251
defaultDataGroupingUnits = [[
18252
MILLISECOND, // unit name
18253
[1, 2, 5, 10, 20, 25, 50, 100, 200, 500] // allowed multiples
18256
[1, 2, 5, 10, 15, 30]
18259
[1, 2, 5, 10, 15, 30]
18262
[1, 2, 3, 4, 6, 8, 12]
18280
* Define the available approximation types. The data grouping approximations takes an array
18281
* or numbers as the first parameter. In case of ohlc, four arrays are sent in as four parameters.
18282
* Each array consists only of numbers. In case null values belong to the group, the property
18283
* .hasNulls will be set to true on the array.
18286
sum: function (arr) {
18287
var len = arr.length,
18290
// 1. it consists of nulls exclusively
18291
if (!len && arr.hasNulls) {
18293
// 2. it has a length and real values
18300
// 3. it has zero length, so just return undefined
18305
average: function (arr) {
18306
var len = arr.length,
18307
ret = approximations.sum(arr);
18309
// If we have a number, return it divided by the length. If not, return
18310
// null or undefined based on what the sum method finds.
18311
if (typeof ret === NUMBER && len) {
18317
open: function (arr) {
18318
return arr.length ? arr[0] : (arr.hasNulls ? null : UNDEFINED);
18320
high: function (arr) {
18321
return arr.length ? arrayMax(arr) : (arr.hasNulls ? null : UNDEFINED);
18323
low: function (arr) {
18324
return arr.length ? arrayMin(arr) : (arr.hasNulls ? null : UNDEFINED);
18326
close: function (arr) {
18327
return arr.length ? arr[arr.length - 1] : (arr.hasNulls ? null : UNDEFINED);
18329
// ohlc and range are special cases where a multidimensional array is input and an array is output
18330
ohlc: function (open, high, low, close) {
18331
open = approximations.open(open);
18332
high = approximations.high(high);
18333
low = approximations.low(low);
18334
close = approximations.close(close);
18336
if (typeof open === NUMBER || typeof high === NUMBER || typeof low === NUMBER || typeof close === NUMBER) {
18337
return [open, high, low, close];
18339
// else, return is undefined
18341
range: function (low, high) {
18342
low = approximations.low(low);
18343
high = approximations.high(high);
18345
if (typeof low === NUMBER || typeof high === NUMBER) {
18346
return [low, high];
18348
// else, return is undefined
18352
/*jslint white:false */
18355
* Takes parallel arrays of x and y data and groups the data into intervals defined by groupPositions, a collection
18356
* of starting x values for each group.
18358
seriesProto.groupData = function (xData, yData, groupPositions, approximation) {
18360
data = series.data,
18361
dataOptions = series.options.data,
18364
dataLength = xData.length,
18368
handleYData = !!yData, // when grouping the fake extended axis for panning, we don't need to consider y
18369
values = [[], [], [], []],
18370
approximationFn = typeof approximation === 'function' ? approximation : approximations[approximation],
18371
pointArrayMap = series.pointArrayMap,
18372
pointArrayMapLength = pointArrayMap && pointArrayMap.length,
18375
// Start with the first point within the X axis range (#2696)
18376
for (i = 0; i <= dataLength; i++) {
18377
if (xData[i] >= groupPositions[0]) {
18382
for (; i <= dataLength; i++) {
18384
// when a new group is entered, summarize and initiate the previous group
18385
while ((groupPositions[1] !== UNDEFINED && xData[i] >= groupPositions[1]) ||
18386
i === dataLength) { // get the last group
18388
// get group x and y
18389
pointX = groupPositions.shift();
18390
groupedY = approximationFn.apply(0, values);
18392
// push the grouped data
18393
if (groupedY !== UNDEFINED) {
18394
groupedXData.push(pointX);
18395
groupedYData.push(groupedY);
18398
// reset the aggregate arrays
18404
// don't loop beyond the last group
18405
if (i === dataLength) {
18411
if (i === dataLength) {
18415
// for each raw data point, push it to an array that contains all values for this specific group
18416
if (pointArrayMap) {
18418
var index = series.cropStart + i,
18419
point = (data && data[index]) || series.pointClass.prototype.applyOptions.apply({ series: series }, [dataOptions[index]]),
18423
for (j = 0; j < pointArrayMapLength; j++) {
18424
val = point[pointArrayMap[j]];
18425
if (typeof val === NUMBER) {
18426
values[j].push(val);
18427
} else if (val === null) {
18428
values[j].hasNulls = true;
18433
pointY = handleYData ? yData[i] : null;
18435
if (typeof pointY === NUMBER) {
18436
values[0].push(pointY);
18437
} else if (pointY === null) {
18438
values[0].hasNulls = true;
18443
return [groupedXData, groupedYData];
18447
* Extend the basic processData method, that crops the data to the current zoom
18448
* range, with data grouping logic.
18450
seriesProto.processData = function () {
18452
chart = series.chart,
18453
options = series.options,
18454
dataGroupingOptions = options[DATA_GROUPING],
18455
groupingEnabled = dataGroupingOptions && pick(dataGroupingOptions.enabled, chart.options._stock),
18459
series.forceCrop = groupingEnabled; // #334
18460
series.groupPixelWidth = null; // #2110
18461
series.hasProcessed = true; // #2692
18463
// skip if processData returns false or if grouping is disabled (in that order)
18464
if (baseProcessData.apply(series, arguments) === false || !groupingEnabled) {
18468
series.destroyGroupedData();
18472
processedXData = series.processedXData,
18473
processedYData = series.processedYData,
18474
plotSizeX = chart.plotSizeX,
18475
xAxis = series.xAxis,
18476
ordinal = xAxis.options.ordinal,
18477
groupPixelWidth = series.groupPixelWidth = xAxis.getGroupPixelWidth && xAxis.getGroupPixelWidth(),
18478
nonGroupedPointRange = series.pointRange;
18480
// Execute grouping if the amount of points is greater than the limit defined in groupPixelWidth
18481
if (groupPixelWidth) {
18482
hasGroupedData = true;
18484
series.points = null; // force recreation of point instances in series.translate
18486
var extremes = xAxis.getExtremes(),
18487
xMin = extremes.min,
18488
xMax = extremes.max,
18489
groupIntervalFactor = (ordinal && xAxis.getGroupIntervalFactor(xMin, xMax, series)) || 1,
18490
interval = (groupPixelWidth * (xMax - xMin) / plotSizeX) * groupIntervalFactor,
18491
groupPositions = xAxis.getTimeTicks(
18492
xAxis.normalizeTimeTickInterval(interval, dataGroupingOptions.units || defaultDataGroupingUnits),
18497
series.closestPointRange
18499
groupedXandY = seriesProto.groupData.apply(series, [processedXData, processedYData, groupPositions, dataGroupingOptions.approximation]),
18500
groupedXData = groupedXandY[0],
18501
groupedYData = groupedXandY[1];
18503
// prevent the smoothed data to spill out left and right, and make
18504
// sure data is not shifted to the left
18505
if (dataGroupingOptions.smoothed) {
18506
i = groupedXData.length - 1;
18507
groupedXData[i] = xMax;
18508
while (i-- && i > 0) {
18509
groupedXData[i] += interval / 2;
18511
groupedXData[0] = xMin;
18514
// record what data grouping values were used
18515
series.currentDataGrouping = groupPositions.info;
18516
if (options.pointRange === null) { // null means auto, as for columns, candlesticks and OHLC
18517
series.pointRange = groupPositions.info.totalRange;
18519
series.closestPointRange = groupPositions.info.totalRange;
18521
// Make sure the X axis extends to show the first group (#2533)
18522
if (defined(groupedXData[0]) && groupedXData[0] < xAxis.dataMin) {
18523
xAxis.dataMin = groupedXData[0];
18526
// set series props
18527
series.processedXData = groupedXData;
18528
series.processedYData = groupedYData;
18530
series.currentDataGrouping = null;
18531
series.pointRange = nonGroupedPointRange;
18533
series.hasGroupedData = hasGroupedData;
18537
* Destroy the grouped data points. #622, #740
18539
seriesProto.destroyGroupedData = function () {
18541
var groupedData = this.groupedData;
18543
// clear previous groups
18544
each(groupedData || [], function (point, i) {
18546
groupedData[i] = point.destroy ? point.destroy() : null;
18549
this.groupedData = null;
18553
* Override the generatePoints method by adding a reference to grouped data
18555
seriesProto.generatePoints = function () {
18557
baseGeneratePoints.apply(this);
18559
// record grouped data in order to let it be destroyed the next time processData runs
18560
this.destroyGroupedData(); // #622
18561
this.groupedData = this.hasGroupedData ? this.points : null;
18565
* Extend the original method, make the tooltip's header reflect the grouped range
18567
tooltipProto.tooltipHeaderFormatter = function (point) {
18568
var tooltip = this,
18569
series = point.series,
18570
options = series.options,
18571
tooltipOptions = series.tooltipOptions,
18572
dataGroupingOptions = options.dataGrouping,
18573
xDateFormat = tooltipOptions.xDateFormat,
18575
xAxis = series.xAxis,
18576
currentDataGrouping,
18577
dateTimeLabelFormats,
18583
// apply only to grouped series
18584
if (xAxis && xAxis.options.type === 'datetime' && dataGroupingOptions && isNumber(point.key)) {
18587
currentDataGrouping = series.currentDataGrouping;
18588
dateTimeLabelFormats = dataGroupingOptions.dateTimeLabelFormats;
18590
// if we have grouped data, use the grouping information to get the right format
18591
if (currentDataGrouping) {
18592
labelFormats = dateTimeLabelFormats[currentDataGrouping.unitName];
18593
if (currentDataGrouping.count === 1) {
18594
xDateFormat = labelFormats[0];
18596
xDateFormat = labelFormats[1];
18597
xDateFormatEnd = labelFormats[2];
18599
// if not grouped, and we don't have set the xDateFormat option, get the best fit,
18600
// so if the least distance between points is one minute, show it, but if the
18601
// least distance is one day, skip hours and minutes etc.
18602
} else if (!xDateFormat && dateTimeLabelFormats) {
18603
for (n in timeUnits) {
18604
if (timeUnits[n] >= xAxis.closestPointRange ||
18605
// If the point is placed every day at 23:59, we need to show
18606
// the minutes as well. This logic only works for time units less than
18607
// a day, since all higher time units are dividable by those. #2637.
18608
(timeUnits[n] <= timeUnits[DAY] && point.key % timeUnits[n] > 0)) {
18610
xDateFormat = dateTimeLabelFormats[n][0];
18616
// now format the key
18617
formattedKey = dateFormat(xDateFormat, point.key);
18618
if (xDateFormatEnd) {
18619
formattedKey += dateFormat(xDateFormatEnd, point.key + currentDataGrouping.totalRange - 1);
18622
// return the replaced format
18623
ret = tooltipOptions.headerFormat.replace('{point.key}', formattedKey);
18625
// else, fall back to the regular formatter
18627
ret = baseTooltipHeaderFormatter.call(tooltip, point);
18634
* Extend the series destroyer
18636
seriesProto.destroy = function () {
18638
groupedData = series.groupedData || [],
18639
i = groupedData.length;
18642
if (groupedData[i]) {
18643
groupedData[i].destroy();
18646
baseDestroy.apply(series);
18650
// Handle default options for data grouping. This must be set at runtime because some series types are
18651
// defined after this.
18652
wrap(seriesProto, 'setOptions', function (proceed, itemOptions) {
18654
var options = proceed.call(this, itemOptions),
18656
plotOptions = this.chart.options.plotOptions,
18657
defaultOptions = defaultPlotOptions[type].dataGrouping;
18659
if (specificOptions[type]) { // #1284
18660
if (!defaultOptions) {
18661
defaultOptions = merge(commonOptions, specificOptions[type]);
18664
options.dataGrouping = merge(
18666
plotOptions.series && plotOptions.series.dataGrouping, // #1228
18667
plotOptions[type].dataGrouping, // Set by the StockChart constructor
18668
itemOptions.dataGrouping
18672
if (this.chart.options._stock) {
18673
this.requireSorting = true;
18681
* When resetting the scale reset the hasProccessed flag to avoid taking previous data grouping
18682
* of neighbour series into accound when determining group pixel width (#2692).
18684
wrap(Axis.prototype, 'setScale', function (proceed) {
18685
proceed.call(this);
18686
each(this.series, function (series) {
18687
series.hasProcessed = false;
18692
* Get the data grouping pixel width based on the greatest defined individual width
18693
* of the axis' series, and if whether one of the axes need grouping.
18695
Axis.prototype.getGroupPixelWidth = function () {
18697
var series = this.series,
18698
len = series.length,
18700
groupPixelWidth = 0,
18701
doGrouping = false,
18705
// If multiple series are compared on the same x axis, give them the same
18706
// group pixel width (#334)
18709
dgOptions = series[i].options.dataGrouping;
18711
groupPixelWidth = mathMax(groupPixelWidth, dgOptions.groupPixelWidth);
18716
// If one of the series needs grouping, apply it to all (#1634)
18719
dgOptions = series[i].options.dataGrouping;
18721
if (dgOptions && series[i].hasProcessed) { // #2692
18723
dataLength = (series[i].processedXData || series[i].data).length;
18725
// Execute grouping if the amount of points is greater than the limit defined in groupPixelWidth
18726
if (series[i].groupPixelWidth || dataLength > (this.chart.plotSizeX / groupPixelWidth) || (dataLength && dgOptions.forced)) {
18732
return doGrouping ? groupPixelWidth : 0;
18737
/* ****************************************************************************
18738
* End data grouping module *
18739
******************************************************************************//* ****************************************************************************
18740
* Start OHLC series code *
18741
*****************************************************************************/
18743
// 1 - Set default options
18744
defaultPlotOptions.ohlc = merge(defaultPlotOptions.column, {
18747
pointFormat: '<span style="color:{series.color};font-weight:bold">{series.name}</span><br/>' +
18748
'Open: {point.open}<br/>' +
18749
'High: {point.high}<br/>' +
18750
'Low: {point.low}<br/>' +
18751
'Close: {point.close}<br/>'
18759
//upColor: undefined
18762
// 2 - Create the OHLCSeries object
18763
var OHLCSeries = extendClass(seriesTypes.column, {
18765
pointArrayMap: ['open', 'high', 'low', 'close'], // array point configs are mapped to this
18766
toYData: function (point) { // return a plain array for speedy calculation
18767
return [point.open, point.high, point.low, point.close];
18769
pointValKey: 'high',
18771
pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
18773
'stroke-width': 'lineWidth'
18775
upColorProp: 'stroke',
18778
* Postprocess mapping between options and SVG attributes
18780
getAttribs: function () {
18781
seriesTypes.column.prototype.getAttribs.apply(this, arguments);
18783
options = series.options,
18784
stateOptions = options.states,
18785
upColor = options.upColor || series.color,
18786
seriesDownPointAttr = merge(series.pointAttr),
18787
upColorProp = series.upColorProp;
18789
seriesDownPointAttr[''][upColorProp] = upColor;
18790
seriesDownPointAttr.hover[upColorProp] = stateOptions.hover.upColor || upColor;
18791
seriesDownPointAttr.select[upColorProp] = stateOptions.select.upColor || upColor;
18793
each(series.points, function (point) {
18794
if (point.open < point.close) {
18795
point.pointAttr = seriesDownPointAttr;
18801
* Translate data points from raw values x and y to plotX and plotY
18803
translate: function () {
18805
yAxis = series.yAxis;
18807
seriesTypes.column.prototype.translate.apply(series);
18809
// do the translation
18810
each(series.points, function (point) {
18812
if (point.open !== null) {
18813
point.plotOpen = yAxis.translate(point.open, 0, 1, 0, 1);
18815
if (point.close !== null) {
18816
point.plotClose = yAxis.translate(point.close, 0, 1, 0, 1);
18823
* Draw the data points
18825
drawPoints: function () {
18827
points = series.points,
18828
chart = series.chart,
18839
each(points, function (point) {
18840
if (point.plotY !== UNDEFINED) {
18842
graphic = point.graphic;
18843
pointAttr = point.pointAttr[point.selected ? 'selected' : ''];
18845
// crisp vector coordinates
18846
crispCorr = (pointAttr['stroke-width'] % 2) / 2;
18847
crispX = mathRound(point.plotX) - crispCorr; // #2596
18848
halfWidth = mathRound(point.shapeArgs.width / 2);
18850
// the vertical stem
18853
crispX, mathRound(point.yBottom),
18855
crispX, mathRound(point.plotY)
18859
if (point.open !== null) {
18860
plotOpen = mathRound(point.plotOpen) + crispCorr;
18866
crispX - halfWidth,
18872
if (point.close !== null) {
18873
plotClose = mathRound(point.plotClose) + crispCorr;
18879
crispX + halfWidth,
18884
// create and/or update the graphic
18886
graphic.animate({ d: path });
18888
point.graphic = chart.renderer.path(path)
18890
.add(series.group);
18901
* Disable animation
18907
seriesTypes.ohlc = OHLCSeries;
18908
/* ****************************************************************************
18909
* End OHLC series code *
18910
*****************************************************************************/
18911
/* ****************************************************************************
18912
* Start Candlestick series code *
18913
*****************************************************************************/
18915
// 1 - set default options
18916
defaultPlotOptions.candlestick = merge(defaultPlotOptions.column, {
18917
lineColor: 'black',
18924
tooltip: defaultPlotOptions.ohlc.tooltip,
18927
// upLineColor: null
18930
// 2 - Create the CandlestickSeries object
18931
var CandlestickSeries = extendClass(OHLCSeries, {
18932
type: 'candlestick',
18935
* One-to-one mapping from options to SVG attributes
18937
pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
18939
stroke: 'lineColor',
18940
'stroke-width': 'lineWidth'
18942
upColorProp: 'fill',
18945
* Postprocess mapping between options and SVG attributes
18947
getAttribs: function () {
18948
seriesTypes.ohlc.prototype.getAttribs.apply(this, arguments);
18950
options = series.options,
18951
stateOptions = options.states,
18952
upLineColor = options.upLineColor || options.lineColor,
18953
hoverStroke = stateOptions.hover.upLineColor || upLineColor,
18954
selectStroke = stateOptions.select.upLineColor || upLineColor;
18956
// Add custom line color for points going up (close > open).
18957
// Fill is handled by OHLCSeries' getAttribs.
18958
each(series.points, function (point) {
18959
if (point.open < point.close) {
18960
point.pointAttr[''].stroke = upLineColor;
18961
point.pointAttr.hover.stroke = hoverStroke;
18962
point.pointAttr.select.stroke = selectStroke;
18968
* Draw the data points
18970
drawPoints: function () {
18971
var series = this, //state = series.state,
18972
points = series.points,
18973
chart = series.chart,
18975
seriesPointAttr = series.pointAttr[''],
18989
each(points, function (point) {
18991
graphic = point.graphic;
18992
if (point.plotY !== UNDEFINED) {
18994
pointAttr = point.pointAttr[point.selected ? 'selected' : ''] || seriesPointAttr;
18996
// crisp vector coordinates
18997
crispCorr = (pointAttr['stroke-width'] % 2) / 2;
18998
crispX = mathRound(point.plotX) - crispCorr; // #2596
18999
plotOpen = point.plotOpen;
19000
plotClose = point.plotClose;
19001
topBox = math.min(plotOpen, plotClose);
19002
bottomBox = math.max(plotOpen, plotClose);
19003
halfWidth = mathRound(point.shapeArgs.width / 2);
19004
hasTopWhisker = mathRound(topBox) !== mathRound(point.plotY);
19005
hasBottomWhisker = bottomBox !== point.yBottom;
19006
topBox = mathRound(topBox) + crispCorr;
19007
bottomBox = mathRound(bottomBox) + crispCorr;
19012
crispX - halfWidth, bottomBox,
19014
crispX - halfWidth, topBox,
19016
crispX + halfWidth, topBox,
19018
crispX + halfWidth, bottomBox,
19019
'Z', // Use a close statement to ensure a nice rectangle #2602
19023
crispX, hasTopWhisker ? mathRound(point.plotY) : topBox, // #460, #2094
19027
crispX, hasBottomWhisker ? mathRound(point.yBottom) : bottomBox, // #460, #2094
19032
graphic.animate({ d: path });
19034
point.graphic = chart.renderer.path(path)
19037
.shadow(series.options.shadow);
19048
seriesTypes.candlestick = CandlestickSeries;
19050
/* ****************************************************************************
19051
* End Candlestick series code *
19052
*****************************************************************************/
19053
/* ****************************************************************************
19054
* Start Flags series code *
19055
*****************************************************************************/
19057
var symbols = SVGRenderer.prototype.symbols;
19059
// 1 - set default options
19060
defaultPlotOptions.flags = merge(defaultPlotOptions.column, {
19061
dataGrouping: null,
19062
fillColor: 'white',
19064
pointRange: 0, // #673
19070
lineColor: 'black',
19071
fillColor: '#FCFFC5'
19076
fontWeight: 'bold',
19077
textAlign: 'center'
19080
pointFormat: '{point.text}<br/>'
19086
// 2 - Create the CandlestickSeries object
19087
seriesTypes.flags = extendClass(seriesTypes.column, {
19090
noSharedTooltip: true,
19091
takeOrdinalPosition: false, // #1074
19092
trackerGroups: ['markerGroup'],
19095
* Inherit the initialization from base Series
19097
init: Series.prototype.init,
19100
* One-to-one mapping from options to SVG attributes
19102
pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
19105
'stroke-width': 'lineWidth',
19110
* Extend the translate method by placing the point on the related series
19112
translate: function () {
19114
seriesTypes.column.prototype.translate.apply(this);
19117
options = series.options,
19118
chart = series.chart,
19119
points = series.points,
19120
cursor = points.length - 1,
19123
optionsOnSeries = options.onSeries,
19124
onSeries = optionsOnSeries && chart.get(optionsOnSeries),
19125
step = onSeries && onSeries.options.step,
19126
onData = onSeries && onSeries.points,
19127
i = onData && onData.length,
19128
xAxis = series.xAxis,
19129
xAxisExt = xAxis.getExtremes(),
19133
currentDataGrouping;
19135
// relate to a master series
19136
if (onSeries && onSeries.visible && i) {
19137
currentDataGrouping = onSeries.currentDataGrouping;
19138
lastX = onData[i - 1].x + (currentDataGrouping ? currentDataGrouping.totalRange : 0); // #2374
19140
// sort the data points
19141
points.sort(function (a, b) {
19142
return (a.x - b.x);
19145
while (i-- && points[cursor]) {
19146
point = points[cursor];
19147
leftPoint = onData[i];
19149
if (leftPoint.x <= point.x && leftPoint.plotY !== UNDEFINED) {
19150
if (point.x <= lastX) { // #803
19152
point.plotY = leftPoint.plotY;
19154
// interpolate between points, #666
19155
if (leftPoint.x < point.x && !step) {
19156
rightPoint = onData[i + 1];
19157
if (rightPoint && rightPoint.plotY !== UNDEFINED) {
19159
((point.x - leftPoint.x) / (rightPoint.x - leftPoint.x)) * // the distance ratio, between 0 and 1
19160
(rightPoint.plotY - leftPoint.plotY); // the y distance
19165
i++; // check again for points in the same x position
19173
// Add plotY position and handle stacking
19174
each(points, function (point, i) {
19176
// Undefined plotY means the point is either on axis, outside series range or hidden series.
19177
// If the series is outside the range of the x axis it should fall through with
19178
// an undefined plotY, but then we must remove the shapeArgs (#847).
19179
if (point.plotY === UNDEFINED) {
19180
if (point.x >= xAxisExt.min && point.x <= xAxisExt.max) { // we're inside xAxis range
19181
point.plotY = chart.chartHeight - xAxis.bottom - (xAxis.opposite ? xAxis.height : 0) + xAxis.offset - chart.plotTop;
19183
point.shapeArgs = {}; // 847
19186
// if multiple flags appear at the same x, order them into a stack
19187
lastPoint = points[i - 1];
19188
if (lastPoint && lastPoint.plotX === point.plotX) {
19189
if (lastPoint.stackIndex === UNDEFINED) {
19190
lastPoint.stackIndex = 0;
19192
point.stackIndex = lastPoint.stackIndex + 1;
19203
drawPoints: function () {
19206
seriesPointAttr = series.pointAttr[''],
19207
points = series.points,
19208
chart = series.chart,
19209
renderer = chart.renderer,
19212
options = series.options,
19213
optionsY = options.y,
19219
crisp = (options.lineWidth % 2 / 2),
19227
outsideRight = point.plotX > series.xAxis.len;
19228
plotX = point.plotX + (outsideRight ? crisp : -crisp);
19229
stackIndex = point.stackIndex;
19230
shape = point.options.shape || options.shape;
19231
plotY = point.plotY;
19232
if (plotY !== UNDEFINED) {
19233
plotY = point.plotY + optionsY + crisp - (stackIndex !== UNDEFINED && stackIndex * options.stackDistance);
19235
anchorX = stackIndex ? UNDEFINED : point.plotX + crisp; // skip connectors for higher level stacked points
19236
anchorY = stackIndex ? UNDEFINED : point.plotY;
19238
graphic = point.graphic;
19240
// only draw the point if y is defined and the flag is within the visible area
19241
if (plotY !== UNDEFINED && plotX >= 0 && !outsideRight) {
19243
pointAttr = point.pointAttr[point.selected ? 'select' : ''] || seriesPointAttr;
19244
if (graphic) { // update
19253
graphic = point.graphic = renderer.label(
19254
point.options.title || options.title || 'A',
19262
.css(merge(options.style, point.style))
19265
align: shape === 'flag' ? 'left' : 'center',
19266
width: options.width,
19267
height: options.height
19269
.add(series.markerGroup)
19270
.shadow(options.shadow);
19274
// Set the tooltip anchor position
19275
point.tooltipPos = [plotX, plotY];
19277
} else if (graphic) {
19278
point.graphic = graphic.destroy();
19286
* Extend the column trackers with listeners to expand and contract stacks
19288
drawTracker: function () {
19290
points = series.points;
19292
TrackerMixin.drawTrackerPoint.apply(this);
19294
// Bring each stacked flag up on mouse over, this allows readability of vertically
19295
// stacked elements as well as tight points on the x axis. #1924.
19296
each(points, function (point) {
19297
var graphic = point.graphic;
19299
addEvent(graphic.element, 'mouseover', function () {
19301
// Raise this point
19302
if (point.stackIndex > 0 && !point.raised) {
19303
point._y = graphic.y;
19307
point.raised = true;
19310
// Revert other raised points
19311
each(points, function (otherPoint) {
19312
if (otherPoint !== point && otherPoint.raised && otherPoint.graphic) {
19313
otherPoint.graphic.attr({
19316
otherPoint.raised = false;
19325
* Disable animation
19331
// create the flag icon with anchor
19332
symbols.flag = function (x, y, w, h, options) {
19333
var anchorX = (options && options.anchorX) || x,
19334
anchorY = (options && options.anchorY) || y;
19337
'M', anchorX, anchorY,
19343
'M', anchorX, anchorY,
19348
// create the circlepin and squarepin icons with anchor
19349
each(['circle', 'square'], function (shape) {
19350
symbols[shape + 'pin'] = function (x, y, w, h, options) {
19352
var anchorX = options && options.anchorX,
19353
anchorY = options && options.anchorY,
19354
path = symbols[shape](x, y, w, h),
19357
if (anchorX && anchorY) {
19358
// if the label is below the anchor, draw the connecting line from the top edge of the label
19359
// otherwise start drawing from the bottom edge
19360
labelTopOrBottomY = (y > anchorY) ? y : y + h;
19361
path.push('M', anchorX, labelTopOrBottomY, 'L', anchorX, anchorY);
19368
// The symbol callbacks are generated on the SVGRenderer object in all browsers. Even
19369
// VML browsers need this in order to generate shapes in export. Now share
19370
// them with the VMLRenderer.
19371
if (Renderer === Highcharts.VMLRenderer) {
19372
each(['flag', 'circlepin', 'squarepin'], function (shape) {
19373
VMLRenderer.prototype.symbols[shape] = symbols[shape];
19377
/* ****************************************************************************
19378
* End Flags series code *
19379
*****************************************************************************/
19380
/* ****************************************************************************
19381
* Start Scroller code *
19382
*****************************************************************************/
19383
var buttonGradient = {
19384
linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 },
19390
units = [].concat(defaultDataGroupingUnits), // copy
19392
// add more resolution to units
19393
units[4] = [DAY, [1, 2, 3, 4]]; // allow more days
19394
units[5] = [WEEK, [1, 2, 3]]; // allow more weeks
19396
defaultSeriesType = seriesTypes.areaspline === UNDEFINED ? 'line' : 'areaspline';
19398
extend(defaultOptions, {
19402
backgroundColor: '#FFF',
19403
borderColor: '#666'
19407
maskFill: 'rgba(255, 255, 255, 0.75)',
19408
outlineColor: '#444',
19411
type: defaultSeriesType,
19416
approximation: 'average',
19418
groupPixelWidth: 2,
19426
id: PREFIX + 'navigator-series',
19427
lineColor: '#4572A7',
19441
tickPixelInterval: 200,
19451
startOnTick: false,
19467
height: isTouchDevice ? 20 : 14,
19468
barBackgroundColor: buttonGradient,
19469
barBorderRadius: 2,
19471
barBorderColor: '#666',
19472
buttonArrowColor: '#666',
19473
buttonBackgroundColor: buttonGradient,
19474
buttonBorderColor: '#666',
19475
buttonBorderRadius: 2,
19476
buttonBorderWidth: 1,
19478
rifleColor: '#666',
19479
trackBackgroundColor: {
19480
linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 },
19486
trackBorderColor: '#CCC',
19487
trackBorderWidth: 1,
19488
// trackBorderRadius: 0
19489
liveRedraw: hasSVG && !isTouchDevice
19494
* The Scroller class
19495
* @param {Object} chart
19497
function Scroller(chart) {
19498
var chartOptions = chart.options,
19499
navigatorOptions = chartOptions.navigator,
19500
navigatorEnabled = navigatorOptions.enabled,
19501
scrollbarOptions = chartOptions.scrollbar,
19502
scrollbarEnabled = scrollbarOptions.enabled,
19503
height = navigatorEnabled ? navigatorOptions.height : 0,
19504
scrollbarHeight = scrollbarEnabled ? scrollbarOptions.height : 0;
19508
this.scrollbarButtons = [];
19509
this.elementsToDestroy = []; // Array containing the elements to destroy when Scroller is destroyed
19511
this.chart = chart;
19512
this.setBaseSeries();
19514
this.height = height;
19515
this.scrollbarHeight = scrollbarHeight;
19516
this.scrollbarEnabled = scrollbarEnabled;
19517
this.navigatorEnabled = navigatorEnabled;
19518
this.navigatorOptions = navigatorOptions;
19519
this.scrollbarOptions = scrollbarOptions;
19520
this.outlineHeight = height + scrollbarHeight;
19526
Scroller.prototype = {
19528
* Draw one of the handles on the side of the zoomed range in the navigator
19529
* @param {Number} x The x center for the handle
19530
* @param {Number} index 0 for left and 1 for right
19532
drawHandle: function (x, index) {
19533
var scroller = this,
19534
chart = scroller.chart,
19535
renderer = chart.renderer,
19536
elementsToDestroy = scroller.elementsToDestroy,
19537
handles = scroller.handles,
19538
handlesOptions = scroller.navigatorOptions.handles,
19540
fill: handlesOptions.backgroundColor,
19541
stroke: handlesOptions.borderColor,
19546
// create the elements
19547
if (!scroller.rendered) {
19549
handles[index] = renderer.g('navigator-handle-' + ['left', 'right'][index])
19550
.css({ cursor: 'e-resize' })
19551
.attr({ zIndex: 4 - index }) // zIndex = 3 for right handle, 4 for left
19555
tempElem = renderer.rect(-4.5, 0, 9, 16, 3, 1)
19557
.add(handles[index]);
19558
elementsToDestroy.push(tempElem);
19561
tempElem = renderer.path([
19571
.add(handles[index]);
19572
elementsToDestroy.push(tempElem);
19576
handles[index][chart.isResizing ? 'animate' : 'attr']({
19577
translateX: scroller.scrollerLeft + scroller.scrollbarHeight + parseInt(x, 10),
19578
translateY: scroller.top + scroller.height / 2 - 8
19583
* Draw the scrollbar buttons with arrows
19584
* @param {Number} index 0 is left, 1 is right
19586
drawScrollbarButton: function (index) {
19587
var scroller = this,
19588
chart = scroller.chart,
19589
renderer = chart.renderer,
19590
elementsToDestroy = scroller.elementsToDestroy,
19591
scrollbarButtons = scroller.scrollbarButtons,
19592
scrollbarHeight = scroller.scrollbarHeight,
19593
scrollbarOptions = scroller.scrollbarOptions,
19596
if (!scroller.rendered) {
19597
scrollbarButtons[index] = renderer.g().add(scroller.scrollbarGroup);
19599
tempElem = renderer.rect(
19602
scrollbarHeight + 1, // +1 to compensate for crispifying in rect method
19603
scrollbarHeight + 1,
19604
scrollbarOptions.buttonBorderRadius,
19605
scrollbarOptions.buttonBorderWidth
19607
stroke: scrollbarOptions.buttonBorderColor,
19608
'stroke-width': scrollbarOptions.buttonBorderWidth,
19609
fill: scrollbarOptions.buttonBackgroundColor
19610
}).add(scrollbarButtons[index]);
19611
elementsToDestroy.push(tempElem);
19613
tempElem = renderer.path([
19615
scrollbarHeight / 2 + (index ? -1 : 1), scrollbarHeight / 2 - 3,
19617
scrollbarHeight / 2 + (index ? -1 : 1), scrollbarHeight / 2 + 3,
19618
scrollbarHeight / 2 + (index ? 2 : -2), scrollbarHeight / 2
19620
fill: scrollbarOptions.buttonArrowColor
19621
}).add(scrollbarButtons[index]);
19622
elementsToDestroy.push(tempElem);
19625
// adjust the right side button to the varying length of the scroll track
19627
scrollbarButtons[index].attr({
19628
translateX: scroller.scrollerWidth - scrollbarHeight
19634
* Render the navigator and scroll bar
19635
* @param {Number} min X axis value minimum
19636
* @param {Number} max X axis value maximum
19637
* @param {Number} pxMin Pixel value minimum
19638
* @param {Number} pxMax Pixel value maximum
19640
render: function (min, max, pxMin, pxMax) {
19641
var scroller = this,
19642
chart = scroller.chart,
19643
renderer = chart.renderer,
19648
scrollbarGroup = scroller.scrollbarGroup,
19649
navigatorGroup = scroller.navigatorGroup,
19650
scrollbar = scroller.scrollbar,
19651
xAxis = scroller.xAxis,
19652
scrollbarTrack = scroller.scrollbarTrack,
19653
scrollbarHeight = scroller.scrollbarHeight,
19654
scrollbarEnabled = scroller.scrollbarEnabled,
19655
navigatorOptions = scroller.navigatorOptions,
19656
scrollbarOptions = scroller.scrollbarOptions,
19657
scrollbarMinWidth = scrollbarOptions.minWidth,
19658
height = scroller.height,
19659
top = scroller.top,
19660
navigatorEnabled = scroller.navigatorEnabled,
19661
outlineWidth = navigatorOptions.outlineWidth,
19662
halfOutline = outlineWidth / 2,
19669
outlineHeight = scroller.outlineHeight,
19670
barBorderRadius = scrollbarOptions.barBorderRadius,
19672
scrollbarStrokeWidth = scrollbarOptions.barBorderWidth,
19674
outlineTop = top + halfOutline,
19678
// don't render the navigator until we have data (#486)
19683
scroller.navigatorLeft = navigatorLeft = pick(
19685
chart.plotLeft + scrollbarHeight // in case of scrollbar only, without navigator
19687
scroller.navigatorWidth = navigatorWidth = pick(xAxis.len, chart.plotWidth - 2 * scrollbarHeight);
19688
scroller.scrollerLeft = scrollerLeft = navigatorLeft - scrollbarHeight;
19689
scroller.scrollerWidth = scrollerWidth = scrollerWidth = navigatorWidth + 2 * scrollbarHeight;
19691
// Set the scroller x axis extremes to reflect the total. The navigator extremes
19692
// should always be the extremes of the union of all series in the chart as
19693
// well as the navigator series.
19694
if (xAxis.getExtremes) {
19695
unionExtremes = scroller.getUnionExtremes(true);
19697
if (unionExtremes && (unionExtremes.dataMin !== xAxis.min || unionExtremes.dataMax !== xAxis.max)) {
19698
xAxis.setExtremes(unionExtremes.dataMin, unionExtremes.dataMax, true, false);
19702
// Get the pixel position of the handles
19703
pxMin = pick(pxMin, xAxis.translate(min));
19704
pxMax = pick(pxMax, xAxis.translate(max));
19705
if (isNaN(pxMin) || mathAbs(pxMin) === Infinity) { // Verify (#1851, #2238)
19707
pxMax = scrollerWidth;
19710
// Are we below the minRange? (#2618)
19711
if (xAxis.translate(pxMax, true) - xAxis.translate(pxMin, true) < chart.xAxis[0].minRange) {
19716
// handles are allowed to cross, but never exceed the plot area
19717
scroller.zoomedMax = mathMin(mathMax(pxMin, pxMax), navigatorWidth);
19718
scroller.zoomedMin =
19719
mathMax(scroller.fixedWidth ? scroller.zoomedMax - scroller.fixedWidth : mathMin(pxMin, pxMax), 0);
19720
scroller.range = scroller.zoomedMax - scroller.zoomedMin;
19721
zoomedMax = mathRound(scroller.zoomedMax);
19722
zoomedMin = mathRound(scroller.zoomedMin);
19723
range = zoomedMax - zoomedMin;
19727
// on first render, create all elements
19728
if (!scroller.rendered) {
19730
if (navigatorEnabled) {
19732
// draw the navigator group
19733
scroller.navigatorGroup = navigatorGroup = renderer.g('navigator')
19739
scroller.leftShade = renderer.rect()
19741
fill: navigatorOptions.maskFill
19742
}).add(navigatorGroup);
19743
scroller.rightShade = renderer.rect()
19745
fill: navigatorOptions.maskFill
19746
}).add(navigatorGroup);
19747
scroller.outline = renderer.path()
19749
'stroke-width': outlineWidth,
19750
stroke: navigatorOptions.outlineColor
19752
.add(navigatorGroup);
19755
if (scrollbarEnabled) {
19757
// draw the scrollbar group
19758
scroller.scrollbarGroup = scrollbarGroup = renderer.g('scrollbar').add();
19760
// the scrollbar track
19761
strokeWidth = scrollbarOptions.trackBorderWidth;
19762
scroller.scrollbarTrack = scrollbarTrack = renderer.rect().attr({
19764
y: -strokeWidth % 2 / 2,
19765
fill: scrollbarOptions.trackBackgroundColor,
19766
stroke: scrollbarOptions.trackBorderColor,
19767
'stroke-width': strokeWidth,
19768
r: scrollbarOptions.trackBorderRadius || 0,
19769
height: scrollbarHeight
19770
}).add(scrollbarGroup);
19772
// the scrollbar itself
19773
scroller.scrollbar = scrollbar = renderer.rect()
19775
y: -scrollbarStrokeWidth % 2 / 2,
19776
height: scrollbarHeight,
19777
fill: scrollbarOptions.barBackgroundColor,
19778
stroke: scrollbarOptions.barBorderColor,
19779
'stroke-width': scrollbarStrokeWidth,
19782
.add(scrollbarGroup);
19784
scroller.scrollbarRifles = renderer.path()
19786
stroke: scrollbarOptions.rifleColor,
19789
.add(scrollbarGroup);
19794
verb = chart.isResizing ? 'animate' : 'attr';
19796
if (navigatorEnabled) {
19797
scroller.leftShade[verb]({
19803
scroller.rightShade[verb]({
19804
x: navigatorLeft + zoomedMax,
19806
width: navigatorWidth - zoomedMax,
19809
scroller.outline[verb]({ d: [
19811
scrollerLeft, outlineTop, // left
19813
navigatorLeft + zoomedMin + halfOutline, outlineTop, // upper left of zoomed range
19814
navigatorLeft + zoomedMin + halfOutline, outlineTop + outlineHeight - scrollbarHeight, // lower left of z.r.
19816
navigatorLeft + zoomedMax - halfOutline, outlineTop + outlineHeight - scrollbarHeight, // lower right of z.r.
19818
navigatorLeft + zoomedMax - halfOutline, outlineTop, // upper right of z.r.
19819
scrollerLeft + scrollerWidth, outlineTop // right
19822
scroller.drawHandle(zoomedMin + halfOutline, 0);
19823
scroller.drawHandle(zoomedMax + halfOutline, 1);
19826
// draw the scrollbar
19827
if (scrollbarEnabled && scrollbarGroup) {
19829
// draw the buttons
19830
scroller.drawScrollbarButton(0);
19831
scroller.drawScrollbarButton(1);
19833
scrollbarGroup[verb]({
19834
translateX: scrollerLeft,
19835
translateY: mathRound(outlineTop + height)
19838
scrollbarTrack[verb]({
19839
width: scrollerWidth
19842
// prevent the scrollbar from drawing to small (#1246)
19843
scrX = scrollbarHeight + zoomedMin;
19844
scrWidth = range - scrollbarStrokeWidth;
19845
if (scrWidth < scrollbarMinWidth) {
19846
scrollbarPad = (scrollbarMinWidth - scrWidth) / 2;
19847
scrWidth = scrollbarMinWidth;
19848
scrX -= scrollbarPad;
19850
scroller.scrollbarPad = scrollbarPad;
19852
x: mathFloor(scrX) + (scrollbarStrokeWidth % 2 / 2),
19856
centerBarX = scrollbarHeight + zoomedMin + range / 2 - 0.5;
19858
scroller.scrollbarRifles
19860
visibility: range > 12 ? VISIBLE : HIDDEN
19864
centerBarX - 3, scrollbarHeight / 4,
19866
centerBarX - 3, 2 * scrollbarHeight / 3,
19868
centerBarX, scrollbarHeight / 4,
19870
centerBarX, 2 * scrollbarHeight / 3,
19872
centerBarX + 3, scrollbarHeight / 4,
19874
centerBarX + 3, 2 * scrollbarHeight / 3
19879
scroller.scrollbarPad = scrollbarPad;
19880
scroller.rendered = true;
19884
* Set up the mouse and touch events for the navigator and scrollbar
19886
addEvents: function () {
19887
var container = this.chart.container,
19888
mouseDownHandler = this.mouseDownHandler,
19889
mouseMoveHandler = this.mouseMoveHandler,
19890
mouseUpHandler = this.mouseUpHandler,
19895
[container, 'mousedown', mouseDownHandler],
19896
[container, 'mousemove', mouseMoveHandler],
19897
[document, 'mouseup', mouseUpHandler]
19903
[container, 'touchstart', mouseDownHandler],
19904
[container, 'touchmove', mouseMoveHandler],
19905
[document, 'touchend', mouseUpHandler]
19910
each(_events, function (args) {
19911
addEvent.apply(null, args);
19913
this._events = _events;
19917
* Removes the event handlers attached previously with addEvents.
19919
removeEvents: function () {
19921
each(this._events, function (args) {
19922
removeEvent.apply(null, args);
19924
this._events = UNDEFINED;
19925
if (this.navigatorEnabled && this.baseSeries) {
19926
removeEvent(this.baseSeries, 'updatedData', this.updatedDataHandler);
19931
* Initiate the Scroller object
19933
init: function () {
19934
var scroller = this,
19935
chart = scroller.chart,
19938
scrollbarHeight = scroller.scrollbarHeight,
19939
navigatorOptions = scroller.navigatorOptions,
19940
height = scroller.height,
19941
top = scroller.top,
19944
bodyStyle = document.body.style,
19946
baseSeries = scroller.baseSeries;
19949
* Event handler for the mouse down event.
19951
scroller.mouseDownHandler = function (e) {
19952
e = chart.pointer.normalize(e);
19954
var zoomedMin = scroller.zoomedMin,
19955
zoomedMax = scroller.zoomedMax,
19956
top = scroller.top,
19957
scrollbarHeight = scroller.scrollbarHeight,
19958
scrollerLeft = scroller.scrollerLeft,
19959
scrollerWidth = scroller.scrollerWidth,
19960
navigatorLeft = scroller.navigatorLeft,
19961
navigatorWidth = scroller.navigatorWidth,
19962
scrollbarPad = scroller.scrollbarPad,
19963
range = scroller.range,
19966
baseXAxis = chart.xAxis[0],
19969
handleSensitivity = isTouchDevice ? 10 : 7,
19973
if (chartY > top && chartY < top + height + scrollbarHeight) { // we're vertically inside the navigator
19974
isOnNavigator = !scroller.scrollbarEnabled || chartY < top + height;
19976
// grab the left handle
19977
if (isOnNavigator && math.abs(chartX - zoomedMin - navigatorLeft) < handleSensitivity) {
19978
scroller.grabbedLeft = true;
19979
scroller.otherHandlePos = zoomedMax;
19980
scroller.fixedExtreme = baseXAxis.max;
19981
chart.fixedRange = null;
19983
// grab the right handle
19984
} else if (isOnNavigator && math.abs(chartX - zoomedMax - navigatorLeft) < handleSensitivity) {
19985
scroller.grabbedRight = true;
19986
scroller.otherHandlePos = zoomedMin;
19987
scroller.fixedExtreme = baseXAxis.min;
19988
chart.fixedRange = null;
19990
// grab the zoomed range
19991
} else if (chartX > navigatorLeft + zoomedMin - scrollbarPad && chartX < navigatorLeft + zoomedMax + scrollbarPad) {
19992
scroller.grabbedCenter = chartX;
19993
scroller.fixedWidth = range;
19995
// In SVG browsers, change the cursor. IE6 & 7 produce an error on changing the cursor,
19996
// and IE8 isn't able to show it while dragging anyway.
19997
if (chart.renderer.isSVG) {
19998
defaultBodyCursor = bodyStyle.cursor;
19999
bodyStyle.cursor = 'ew-resize';
20002
dragOffset = chartX - zoomedMin;
20005
// shift the range by clicking on shaded areas, scrollbar track or scrollbar buttons
20006
} else if (chartX > scrollerLeft && chartX < scrollerLeft + scrollerWidth) {
20008
// Center around the clicked point
20009
if (isOnNavigator) {
20010
left = chartX - navigatorLeft - range / 2;
20012
// Click on scrollbar
20015
// Click left scrollbar button
20016
if (chartX < navigatorLeft) {
20017
left = zoomedMin - range * 0.2;
20019
// Click right scrollbar button
20020
} else if (chartX > scrollerLeft + scrollerWidth - scrollbarHeight) {
20021
left = zoomedMin + range * 0.2;
20023
// Click on scrollbar track, shift the scrollbar by one range
20025
left = chartX < navigatorLeft + zoomedMin ? // on the left
20026
zoomedMin - range :
20032
} else if (left + range >= navigatorWidth) {
20033
left = navigatorWidth - range;
20034
fixedMax = xAxis.dataMax; // #2293
20036
if (left !== zoomedMin) { // it has actually moved
20037
scroller.fixedWidth = range; // #1370
20039
ext = xAxis.toFixedRange(left, left + range, null, fixedMax);
20040
baseXAxis.setExtremes(
20045
{ trigger: 'navigator' }
20054
* Event handler for the mouse move event.
20056
scroller.mouseMoveHandler = function (e) {
20057
var scrollbarHeight = scroller.scrollbarHeight,
20058
navigatorLeft = scroller.navigatorLeft,
20059
navigatorWidth = scroller.navigatorWidth,
20060
scrollerLeft = scroller.scrollerLeft,
20061
scrollerWidth = scroller.scrollerWidth,
20062
range = scroller.range,
20065
// In iOS, a mousemove event with e.pageX === 0 is fired when holding the finger
20066
// down in the center of the scrollbar. This should be ignored.
20067
if (e.pageX !== 0) {
20069
e = chart.pointer.normalize(e);
20072
// validation for handle dragging
20073
if (chartX < navigatorLeft) {
20074
chartX = navigatorLeft;
20075
} else if (chartX > scrollerLeft + scrollerWidth - scrollbarHeight) {
20076
chartX = scrollerLeft + scrollerWidth - scrollbarHeight;
20079
// drag left handle
20080
if (scroller.grabbedLeft) {
20082
scroller.render(0, 0, chartX - navigatorLeft, scroller.otherHandlePos);
20084
// drag right handle
20085
} else if (scroller.grabbedRight) {
20087
scroller.render(0, 0, scroller.otherHandlePos, chartX - navigatorLeft);
20089
// drag scrollbar or open area in navigator
20090
} else if (scroller.grabbedCenter) {
20093
if (chartX < dragOffset) { // outside left
20094
chartX = dragOffset;
20095
} else if (chartX > navigatorWidth + dragOffset - range) { // outside right
20096
chartX = navigatorWidth + dragOffset - range;
20099
scroller.render(0, 0, chartX - dragOffset, chartX - dragOffset + range);
20102
if (hasDragged && scroller.scrollbarOptions.liveRedraw) {
20103
setTimeout(function () {
20104
scroller.mouseUpHandler(e);
20111
* Event handler for the mouse up event.
20113
scroller.mouseUpHandler = function (e) {
20119
// When dragging one handle, make sure the other one doesn't change
20120
if (scroller.zoomedMin === scroller.otherHandlePos) {
20121
fixedMin = scroller.fixedExtreme;
20122
} else if (scroller.zoomedMax === scroller.otherHandlePos) {
20123
fixedMax = scroller.fixedExtreme;
20126
ext = xAxis.toFixedRange(scroller.zoomedMin, scroller.zoomedMax, fixedMin, fixedMax);
20127
chart.xAxis[0].setExtremes(
20133
trigger: 'navigator',
20134
triggerOp: 'navigator-drag',
20135
DOMEvent: e // #1838
20140
if (e.type !== 'mousemove') {
20141
scroller.grabbedLeft = scroller.grabbedRight = scroller.grabbedCenter = scroller.fixedWidth =
20142
scroller.fixedExtreme = scroller.otherHandlePos = hasDragged = dragOffset = null;
20143
bodyStyle.cursor = defaultBodyCursor || '';
20150
var xAxisIndex = chart.xAxis.length,
20151
yAxisIndex = chart.yAxis.length;
20153
// make room below the chart
20154
chart.extraBottomMargin = scroller.outlineHeight + navigatorOptions.margin;
20156
if (scroller.navigatorEnabled) {
20157
// an x axis is required for scrollbar also
20158
scroller.xAxis = xAxis = new Axis(chart, merge({
20159
ordinal: baseSeries && baseSeries.xAxis.options.ordinal // inherit base xAxis' ordinal option
20160
}, navigatorOptions.xAxis, {
20161
id: 'navigator-x-axis',
20167
offsetLeft: scrollbarHeight,
20168
offsetRight: -scrollbarHeight,
20169
keepOrdinalPadding: true, // #2436
20170
startOnTick: false,
20177
scroller.yAxis = yAxis = new Axis(chart, merge(navigatorOptions.yAxis, {
20178
id: 'navigator-y-axis',
20186
// If we have a base series, initialize the navigator series
20187
if (baseSeries || navigatorOptions.series.data) {
20188
scroller.addBaseSeries();
20190
// If not, set up an event to listen for added series
20191
} else if (chart.series.length === 0) {
20193
wrap(chart, 'redraw', function (proceed, animation) {
20194
// We've got one, now add it as base and reset chart.redraw
20195
if (chart.series.length > 0 && !scroller.series) {
20196
scroller.setBaseSeries();
20197
chart.redraw = proceed; // reset
20199
proceed.call(chart, animation);
20204
// in case of scrollbar only, fake an x axis to get translation
20206
scroller.xAxis = xAxis = {
20207
translate: function (value, reverse) {
20208
var ext = chart.xAxis[0].getExtremes(),
20209
scrollTrackWidth = chart.plotWidth - 2 * scrollbarHeight,
20210
dataMin = ext.dataMin,
20211
valueRange = ext.dataMax - dataMin;
20214
// from pixel to value
20215
(value * valueRange / scrollTrackWidth) + dataMin :
20216
// from value to pixel
20217
scrollTrackWidth * (value - dataMin) / valueRange;
20219
toFixedRange: Axis.prototype.toFixedRange
20225
* For stock charts, extend the Chart.getMargins method so that we can set the final top position
20226
* of the navigator once the height of the chart, including the legend, is determined. #367.
20228
wrap(chart, 'getMargins', function (proceed) {
20230
var legend = this.legend,
20231
legendOptions = legend.options;
20233
proceed.call(this);
20235
// Compute the top position
20236
scroller.top = top = scroller.navigatorOptions.top ||
20237
this.chartHeight - scroller.height - scroller.scrollbarHeight - this.spacing[2] -
20238
(legendOptions.verticalAlign === 'bottom' && legendOptions.enabled && !legendOptions.floating ?
20239
legend.legendHeight + pick(legendOptions.margin, 10) : 0);
20241
if (xAxis && yAxis) { // false if navigator is disabled (#904)
20243
xAxis.options.top = yAxis.options.top = top;
20245
xAxis.setAxisSize();
20246
yAxis.setAxisSize();
20251
scroller.addEvents();
20255
* Get the union data extremes of the chart - the outer data extremes of the base
20256
* X axis and the navigator axis.
20258
getUnionExtremes: function (returnFalseOnNoBaseSeries) {
20259
var baseAxis = this.chart.xAxis[0],
20260
navAxis = this.xAxis,
20261
navAxisOptions = navAxis.options;
20263
if (!returnFalseOnNoBaseSeries || baseAxis.dataMin !== null) {
20266
navAxisOptions && navAxisOptions.min,
20267
((defined(baseAxis.dataMin) && defined(navAxis.dataMin)) ? mathMin : pick)(baseAxis.dataMin, navAxis.dataMin)
20270
navAxisOptions && navAxisOptions.max,
20271
((defined(baseAxis.dataMax) && defined(navAxis.dataMax)) ? mathMax : pick)(baseAxis.dataMax, navAxis.dataMax)
20279
* Set the base series. With a bit of modification we should be able to make
20280
* this an API method to be called from the outside
20282
setBaseSeries: function (baseSeriesOption) {
20283
var chart = this.chart;
20285
baseSeriesOption = baseSeriesOption || chart.options.navigator.baseSeries;
20287
// If we're resetting, remove the existing series
20289
this.series.remove();
20292
// Set the new base series
20293
this.baseSeries = chart.series[baseSeriesOption] ||
20294
(typeof baseSeriesOption === 'string' && chart.get(baseSeriesOption)) ||
20297
// When run after render, this.xAxis already exists
20299
this.addBaseSeries();
20303
addBaseSeries: function () {
20304
var baseSeries = this.baseSeries,
20305
baseOptions = baseSeries ? baseSeries.options : {},
20306
baseData = baseOptions.data,
20307
mergedNavSeriesOptions,
20308
navigatorSeriesOptions = this.navigatorOptions.series,
20311
// remove it to prevent merging one by one
20312
navigatorData = navigatorSeriesOptions.data;
20313
this.hasNavigatorData = !!navigatorData;
20315
// Merge the series options
20316
mergedNavSeriesOptions = merge(baseOptions, navigatorSeriesOptions, {
20318
enableMouseTracking: false,
20319
group: 'nav', // for columns
20321
xAxis: 'navigator-x-axis',
20322
yAxis: 'navigator-y-axis',
20324
showInLegend: false,
20329
// set the data back
20330
mergedNavSeriesOptions.data = navigatorData || baseData;
20333
this.series = this.chart.initSeries(mergedNavSeriesOptions);
20335
// Respond to updated data in the base series.
20336
// Abort if lazy-loading data from the server.
20337
if (baseSeries && this.navigatorOptions.adaptToUpdatedData !== false) {
20338
addEvent(baseSeries, 'updatedData', this.updatedDataHandler);
20339
// Survive Series.update()
20340
baseSeries.userOptions.events = extend(baseSeries.userOptions.event, { updatedData: this.updatedDataHandler });
20345
updatedDataHandler: function () {
20346
var scroller = this.chart.scroller,
20347
baseSeries = scroller.baseSeries,
20348
baseXAxis = baseSeries.xAxis,
20349
baseExtremes = baseXAxis.getExtremes(),
20350
baseMin = baseExtremes.min,
20351
baseMax = baseExtremes.max,
20352
baseDataMin = baseExtremes.dataMin,
20353
baseDataMax = baseExtremes.dataMax,
20354
range = baseMax - baseMin,
20360
navigatorSeries = scroller.series,
20361
navXData = navigatorSeries.xData,
20362
hasSetExtremes = !!baseXAxis.setExtremes;
20364
// detect whether to move the range
20365
stickToMax = baseMax >= navXData[navXData.length - 1] - (this.closestPointRange || 0); // #570
20366
stickToMin = baseMin <= baseDataMin;
20368
// set the navigator series data to the new data of the base series
20369
if (!scroller.hasNavigatorData) {
20370
navigatorSeries.options.pointStart = baseSeries.xData[0];
20371
navigatorSeries.setData(baseSeries.options.data, false);
20375
// if the zoomed range is already at the min, move it to the right as new data
20378
newMin = baseDataMin;
20379
newMax = newMin + range;
20382
// if the zoomed range is already at the max, move it to the right as new data
20385
newMax = baseDataMax;
20386
if (!stickToMin) { // if stickToMin is true, the new min value is set above
20387
newMin = mathMax(newMax - range, navigatorSeries.xData[0]);
20391
// update the extremes
20392
if (hasSetExtremes && (stickToMin || stickToMax)) {
20393
if (!isNaN(newMin)) {
20394
baseXAxis.setExtremes(newMin, newMax, true, false, { trigger: 'updatedData' });
20397
// if it is not at any edge, just move the scroller window to reflect the new series data
20400
this.chart.redraw(false);
20404
mathMax(baseMin, baseDataMin),
20405
mathMin(baseMax, baseDataMax)
20411
* Destroys allocated elements.
20413
destroy: function () {
20414
var scroller = this;
20416
// Disconnect events added in addEvents
20417
scroller.removeEvents();
20419
// Destroy properties
20420
each([scroller.xAxis, scroller.yAxis, scroller.leftShade, scroller.rightShade, scroller.outline, scroller.scrollbarTrack, scroller.scrollbarRifles, scroller.scrollbarGroup, scroller.scrollbar], function (prop) {
20421
if (prop && prop.destroy) {
20425
scroller.xAxis = scroller.yAxis = scroller.leftShade = scroller.rightShade = scroller.outline = scroller.scrollbarTrack = scroller.scrollbarRifles = scroller.scrollbarGroup = scroller.scrollbar = null;
20427
// Destroy elements in collection
20428
each([scroller.scrollbarButtons, scroller.handles, scroller.elementsToDestroy], function (coll) {
20429
destroyObjectProperties(coll);
20434
Highcharts.Scroller = Scroller;
20438
* For Stock charts, override selection zooming with some special features because
20439
* X axis zooming is already allowed by the Navigator and Range selector.
20441
wrap(Axis.prototype, 'zoom', function (proceed, newMin, newMax) {
20442
var chart = this.chart,
20443
chartOptions = chart.options,
20444
zoomType = chartOptions.chart.zoomType,
20446
navigator = chartOptions.navigator,
20447
rangeSelector = chartOptions.rangeSelector,
20450
if (this.isXAxis && ((navigator && navigator.enabled) ||
20451
(rangeSelector && rangeSelector.enabled))) {
20453
// For x only zooming, fool the chart.zoom method not to create the zoom button
20454
// because the property already exists
20455
if (zoomType === 'x') {
20456
chart.resetZoomButton = 'blocked';
20458
// For y only zooming, ignore the X axis completely
20459
} else if (zoomType === 'y') {
20462
// For xy zooming, record the state of the zoom before zoom selection, then when
20463
// the reset button is pressed, revert to this state
20464
} else if (zoomType === 'xy') {
20465
previousZoom = this.previousZoom;
20466
if (defined(newMin)) {
20467
this.previousZoom = [this.min, this.max];
20468
} else if (previousZoom) {
20469
newMin = previousZoom[0];
20470
newMax = previousZoom[1];
20471
delete this.previousZoom;
20476
return ret !== UNDEFINED ? ret : proceed.call(this, newMin, newMax);
20479
// Initialize scroller for stock charts
20480
wrap(Chart.prototype, 'init', function (proceed, options, callback) {
20482
addEvent(this, 'beforeRender', function () {
20483
var options = this.options;
20484
if (options.navigator.enabled || options.scrollbar.enabled) {
20485
this.scroller = new Scroller(this);
20489
proceed.call(this, options, callback);
20493
// Pick up badly formatted point options to addPoint
20494
wrap(Series.prototype, 'addPoint', function (proceed, options, redraw, shift, animation) {
20495
var turboThreshold = this.options.turboThreshold;
20496
if (turboThreshold && this.xData.length > turboThreshold && isObject(options) && !isArray(options) && this.chart.scroller) {
20499
proceed.call(this, options, redraw, shift, animation);
20502
/* ****************************************************************************
20503
* End Scroller code *
20504
*****************************************************************************/
20505
/* ****************************************************************************
20506
* Start Range Selector code *
20507
*****************************************************************************/
20508
extend(defaultOptions, {
20511
// buttons: {Object}
20512
// buttonSpacing: 0,
20519
zIndex: 7 // #484, #852
20528
// inputDateFormat: '%b %e, %Y',
20529
// inputEditDateFormat: '%Y-%m-%d',
20530
// inputEnabled: true,
20535
// selected: undefined
20538
defaultOptions.lang = merge(defaultOptions.lang, {
20539
rangeSelectorZoom: 'Zoom',
20540
rangeSelectorFrom: 'From',
20541
rangeSelectorTo: 'To'
20545
* The object constructor for the range selector
20546
* @param {Object} chart
20548
function RangeSelector(chart) {
20550
// Run RangeSelector
20554
RangeSelector.prototype = {
20556
* The method to run when one of the buttons in the range selectors is clicked
20557
* @param {Number} i The index of the button
20558
* @param {Object} rangeOptions
20559
* @param {Boolean} redraw
20561
clickButton: function (i, redraw) {
20562
var rangeSelector = this,
20563
selected = rangeSelector.selected,
20564
chart = rangeSelector.chart,
20565
buttons = rangeSelector.buttons,
20566
rangeOptions = rangeSelector.buttonOptions[i],
20567
baseAxis = chart.xAxis[0],
20568
unionExtremes = (chart.scroller && chart.scroller.getUnionExtremes()) || baseAxis || {},
20569
dataMin = unionExtremes.dataMin,
20570
dataMax = unionExtremes.dataMax,
20572
newMax = baseAxis && mathRound(mathMin(baseAxis.max, pick(dataMax, baseAxis.max))), // #1568
20574
date = new Date(newMax),
20575
type = rangeOptions.type,
20576
count = rangeOptions.count,
20578
range = rangeOptions._range,
20583
if (dataMin === null || dataMax === null || // chart has no data, base series is removed
20584
i === rangeSelector.selected) { // same button is clicked twice
20588
if (type === 'month' || type === 'year') {
20589
timeName = { month: 'Month', year: 'FullYear'}[type];
20590
date['set' + timeName](date['get' + timeName]() - count);
20592
newMin = date.getTime();
20593
dataMin = pick(dataMin, Number.MIN_VALUE);
20594
if (isNaN(newMin) || newMin < dataMin) {
20596
newMax = mathMin(newMin + range, dataMax);
20598
range = newMax - newMin;
20601
// Fixed times like minutes, hours, days
20602
} else if (range) {
20603
newMin = mathMax(newMax - range, dataMin);
20604
newMax = mathMin(newMin + range, dataMax);
20606
} else if (type === 'ytd') {
20608
// On user clicks on the buttons, or a delayed action running from the beforeRender
20609
// event (below), the baseAxis is defined.
20612
// When "ytd" is the pre-selected button for the initial view, its calculation
20613
// is delayed and rerun in the beforeRender event (below). When the series
20614
// are initialized, but before the chart is rendered, we have access to the xData
20616
if (dataMax === UNDEFINED) {
20617
dataMin = Number.MAX_VALUE;
20618
dataMax = Number.MIN_VALUE;
20619
each(chart.series, function (series) {
20620
var xData = series.xData; // reassign it to the last item
20621
dataMin = mathMin(xData[0], dataMin);
20622
dataMax = mathMax(xData[xData.length - 1], dataMax);
20626
now = new Date(dataMax);
20627
year = now.getFullYear();
20628
newMin = rangeMin = mathMax(dataMin || 0, Date.UTC(year, 0, 1));
20629
now = now.getTime();
20630
newMax = mathMin(dataMax || now, now);
20632
// "ytd" is pre-selected. We don't yet have access to processed point and extremes data
20633
// (things like pointStart and pointInterval are missing), so we delay the process (#942)
20635
addEvent(chart, 'beforeRender', function () {
20636
rangeSelector.clickButton(i);
20640
} else if (type === 'all' && baseAxis) {
20645
// Deselect previous button
20646
if (buttons[selected]) {
20647
buttons[selected].setState(0);
20649
// Select this button
20651
buttons[i].setState(2);
20654
chart.fixedRange = range;
20656
// update the chart
20657
if (!baseAxis) { // axis not yet instanciated
20658
baseXAxisOptions = chart.options.xAxis;
20659
baseXAxisOptions[0] = merge(
20660
baseXAxisOptions[0],
20666
rangeSelector.setSelected(i);
20667
} else { // existing axis object; after render time
20668
baseAxis.setExtremes(
20674
trigger: 'rangeSelectorButton',
20675
rangeSelectorButton: rangeOptions
20678
rangeSelector.setSelected(i);
20683
* Set the selected option. This method only sets the internal flag, it doesn't
20684
* update the buttons or the actual zoomed range.
20686
setSelected: function (selected) {
20687
this.selected = this.options.selected = selected;
20691
* The default buttons for pre-selecting time frames
20718
* Initialize the range selector
20720
init: function (chart) {
20722
var rangeSelector = this,
20723
options = chart.options.rangeSelector,
20724
buttonOptions = options.buttons || [].concat(rangeSelector.defaultButtons),
20725
selectedOption = options.selected,
20726
blurInputs = rangeSelector.blurInputs = function () {
20727
var minInput = rangeSelector.minInput,
20728
maxInput = rangeSelector.maxInput;
20737
rangeSelector.chart = chart;
20738
rangeSelector.options = options;
20739
rangeSelector.buttons = [];
20741
chart.extraTopMargin = 25;
20742
rangeSelector.buttonOptions = buttonOptions;
20744
addEvent(chart.container, 'mousedown', blurInputs);
20745
addEvent(chart, 'resize', blurInputs);
20747
// Extend the buttonOptions with actual range
20748
each(buttonOptions, rangeSelector.computeButtonRange);
20750
// zoomed range based on a pre-selected button index
20751
if (selectedOption !== UNDEFINED && buttonOptions[selectedOption]) {
20752
this.clickButton(selectedOption, false);
20755
// normalize the pressed button whenever a new range is selected
20756
addEvent(chart, 'load', function () {
20757
addEvent(chart.xAxis[0], 'afterSetExtremes', function () {
20758
rangeSelector.updateButtonStates(true);
20764
* Dynamically update the range selector buttons after a new range has been set
20766
updateButtonStates: function (updating) {
20767
var rangeSelector = this,
20768
chart = this.chart,
20769
baseAxis = chart.xAxis[0],
20770
unionExtremes = (chart.scroller && chart.scroller.getUnionExtremes()) || baseAxis,
20771
dataMin = unionExtremes.dataMin,
20772
dataMax = unionExtremes.dataMax,
20773
selected = rangeSelector.selected,
20774
buttons = rangeSelector.buttons;
20776
if (updating && chart.fixedRange !== mathRound(baseAxis.max - baseAxis.min)) {
20777
if (buttons[selected]) {
20778
buttons[selected].setState(0);
20780
rangeSelector.setSelected(null);
20783
each(rangeSelector.buttonOptions, function (rangeOptions, i) {
20784
var range = rangeOptions._range,
20785
// Disable buttons where the range exceeds what is allowed in the current view
20786
isTooGreatRange = range > dataMax - dataMin,
20787
// Disable buttons where the range is smaller than the minimum range
20788
isTooSmallRange = range < baseAxis.minRange,
20789
// Disable the All button if we're already showing all
20790
isAllButAlreadyShowingAll = rangeOptions.type === 'all' && baseAxis.max - baseAxis.min >= dataMax - dataMin &&
20791
buttons[i].state !== 2,
20792
// Disable the YTD button if the complete range is within the same year
20793
isYTDButNotAvailable = rangeOptions.type === 'ytd' && dateFormat('%Y', dataMin) === dateFormat('%Y', dataMax);
20795
// The new zoom area happens to match the range for a button - mark it selected.
20796
// This happens when scrolling across an ordinal gap. It can be seen in the intraday
20797
// demos when selecting 1h and scroll across the night gap.
20798
if (range === mathRound(baseAxis.max - baseAxis.min) && i !== selected) {
20799
rangeSelector.setSelected(i);
20800
buttons[i].setState(2);
20802
} else if (isTooGreatRange || isTooSmallRange || isAllButAlreadyShowingAll || isYTDButNotAvailable) {
20803
buttons[i].setState(3);
20805
} else if (buttons[i].state === 3) {
20806
buttons[i].setState(0);
20812
* Compute and cache the range for an individual button
20814
computeButtonRange: function (rangeOptions) {
20815
var type = rangeOptions.type,
20816
count = rangeOptions.count || 1,
20818
// these time intervals have a fixed number of milliseconds, as opposed
20819
// to month, ytd and year
20825
day: 24 * 3600 * 1000,
20826
week: 7 * 24 * 3600 * 1000
20829
// Store the range on the button object
20830
if (fixedTimes[type]) {
20831
rangeOptions._range = fixedTimes[type] * count;
20832
} else if (type === 'month' || type === 'year') {
20833
rangeOptions._range = { month: 30, year: 365 }[type] * 24 * 36e5 * count;
20838
* Set the internal and displayed value of a HTML input for the dates
20839
* @param {String} name
20840
* @param {Number} time
20842
setInputValue: function (name, time) {
20843
var options = this.chart.options.rangeSelector;
20845
if (defined(time)) {
20846
this[name + 'Input'].HCTime = time;
20849
this[name + 'Input'].value = dateFormat(options.inputEditDateFormat || '%Y-%m-%d', this[name + 'Input'].HCTime);
20850
this[name + 'DateBox'].attr({ text: dateFormat(options.inputDateFormat || '%b %e, %Y', this[name + 'Input'].HCTime) });
20854
* Draw either the 'from' or the 'to' HTML input box of the range selector
20855
* @param {Object} name
20857
drawInput: function (name) {
20858
var rangeSelector = this,
20859
chart = rangeSelector.chart,
20860
chartStyle = chart.renderer.style,
20861
renderer = chart.renderer,
20862
options = chart.options.rangeSelector,
20863
lang = defaultOptions.lang,
20864
div = rangeSelector.div,
20865
isMin = name === 'min',
20869
inputGroup = this.inputGroup;
20871
// Create the text label
20872
this[name + 'Label'] = label = renderer.label(lang[isMin ? 'rangeSelectorFrom' : 'rangeSelectorTo'], this.inputGroup.offset)
20876
.css(merge(chartStyle, options.labelStyle))
20878
inputGroup.offset += label.width + 5;
20880
// Create an SVG label that shows updated date ranges and and records click events that
20881
// bring in the HTML input.
20882
this[name + 'DateBox'] = dateBox = renderer.label('', inputGroup.offset)
20885
width: options.inputBoxWidth || 90,
20886
height: options.inputBoxHeight || 16,
20887
stroke: options.inputBoxBorderColor || 'silver',
20891
textAlign: 'center'
20892
}, chartStyle, options.inputStyle))
20893
.on('click', function () {
20894
rangeSelector[name + 'Input'].focus();
20897
inputGroup.offset += dateBox.width + (isMin ? 10 : 0);
20900
// Create the HTML input element. This is rendered as 1x1 pixel then set to the right size
20902
this[name + 'Input'] = input = createElement('input', {
20904
className: PREFIX + 'range-selector',
20907
position: ABSOLUTE,
20909
width: '1px', // Chrome needs a pixel to see it
20912
textAlign: 'center',
20913
fontSize: chartStyle.fontSize,
20914
fontFamily: chartStyle.fontFamily,
20915
top: chart.plotTop + PX // prevent jump on focus in Firefox
20916
}, options.inputStyle), div);
20918
// Blow up the input box
20919
input.onfocus = function () {
20921
left: (inputGroup.translateX + dateBox.x) + PX,
20922
top: inputGroup.translateY + PX,
20923
width: (dateBox.width - 2) + PX,
20924
height: (dateBox.height - 2) + PX,
20925
border: '2px solid silver'
20928
// Hide away the input box
20929
input.onblur = function () {
20935
rangeSelector.setInputValue(name);
20938
// handle changes in the input boxes
20939
input.onchange = function () {
20940
var inputValue = input.value,
20941
value = (options.inputDateParser || Date.parse)(inputValue),
20942
xAxis = chart.xAxis[0],
20943
dataMin = xAxis.dataMin,
20944
dataMax = xAxis.dataMax;
20946
// If the value isn't parsed directly to a value by the browser's Date.parse method,
20947
// like YYYY-MM-DD in IE, try parsing it a different way
20948
if (isNaN(value)) {
20949
value = inputValue.split('-');
20950
value = Date.UTC(pInt(value[0]), pInt(value[1]) - 1, pInt(value[2]));
20953
if (!isNaN(value)) {
20955
// Correct for timezone offset (#433)
20956
if (!defaultOptions.global.useUTC) {
20957
value = value + new Date().getTimezoneOffset() * 60 * 1000;
20960
// Validate the extremes. If it goes beyound the data min or max, use the
20961
// actual data extreme (#2438).
20963
if (value > rangeSelector.maxInput.HCTime) {
20965
} else if (value < dataMin) {
20969
if (value < rangeSelector.minInput.HCTime) {
20971
} else if (value > dataMax) {
20976
// Set the extremes
20977
if (value !== UNDEFINED) {
20978
chart.xAxis[0].setExtremes(
20979
isMin ? value : xAxis.min,
20980
isMin ? xAxis.max : value,
20983
{ trigger: 'rangeSelectorInput' }
20991
* Render the range selector including the buttons and the inputs. The first time render
20992
* is called, the elements are created and positioned. On subsequent calls, they are
20993
* moved and updated.
20994
* @param {Number} min X axis minimum
20995
* @param {Number} max X axis maximum
20997
render: function (min, max) {
20999
var rangeSelector = this,
21000
chart = rangeSelector.chart,
21001
renderer = chart.renderer,
21002
container = chart.container,
21003
chartOptions = chart.options,
21004
navButtonOptions = chartOptions.exporting && chartOptions.navigation && chartOptions.navigation.buttonOptions,
21005
options = chartOptions.rangeSelector,
21006
buttons = rangeSelector.buttons,
21007
lang = defaultOptions.lang,
21008
div = rangeSelector.div,
21009
inputGroup = rangeSelector.inputGroup,
21010
buttonTheme = options.buttonTheme,
21011
inputEnabled = options.inputEnabled !== false,
21012
states = buttonTheme && buttonTheme.states,
21013
plotLeft = chart.plotLeft,
21017
// create the elements
21018
if (!rangeSelector.rendered) {
21019
rangeSelector.zoomText = renderer.text(lang.rangeSelectorZoom, plotLeft, chart.plotTop - 10)
21020
.css(options.labelStyle)
21023
// button starting position
21024
buttonLeft = plotLeft + rangeSelector.zoomText.getBBox().width + 5;
21026
each(rangeSelector.buttonOptions, function (rangeOptions, i) {
21027
buttons[i] = renderer.button(
21030
chart.plotTop - 25,
21032
rangeSelector.clickButton(i);
21033
rangeSelector.isActive = true;
21036
states && states.hover,
21037
states && states.select
21040
textAlign: 'center'
21044
// increase button position for the next button
21045
buttonLeft += buttons[i].width + (options.buttonSpacing || 0);
21047
if (rangeSelector.selected === i) {
21048
buttons[i].setState(2);
21052
rangeSelector.updateButtonStates();
21054
// first create a wrapper outside the container in order to make
21055
// the inputs work and make export correct
21056
if (inputEnabled) {
21057
rangeSelector.div = div = createElement('div', null, {
21058
position: 'relative',
21060
zIndex: 1 // above container
21063
container.parentNode.insertBefore(div, container);
21065
// Create the group to keep the inputs
21066
rangeSelector.inputGroup = inputGroup = renderer.g('input-group')
21068
inputGroup.offset = 0;
21070
rangeSelector.drawInput('min');
21071
rangeSelector.drawInput('max');
21075
if (inputEnabled) {
21077
// Update the alignment to the updated spacing box
21078
yAlign = chart.plotTop - 35;
21079
inputGroup.align(extend({
21081
width: inputGroup.offset,
21082
// detect collision with the exporting buttons
21083
x: navButtonOptions && (yAlign < (navButtonOptions.y || 0) + navButtonOptions.height - chart.spacing[0]) ?
21085
}, options.inputPosition), true, chart.spacingBox);
21087
// Set or reset the input values
21088
rangeSelector.setInputValue('min', min);
21089
rangeSelector.setInputValue('max', max);
21092
rangeSelector.rendered = true;
21096
* Destroys allocated elements.
21098
destroy: function () {
21099
var minInput = this.minInput,
21100
maxInput = this.maxInput,
21101
chart = this.chart,
21102
blurInputs = this.blurInputs,
21105
removeEvent(chart.container, 'mousedown', blurInputs);
21106
removeEvent(chart, 'resize', blurInputs);
21108
// Destroy elements in collections
21109
destroyObjectProperties(this.buttons);
21111
// Clear input element events
21113
minInput.onfocus = minInput.onblur = minInput.onchange = null;
21116
maxInput.onfocus = maxInput.onblur = maxInput.onchange = null;
21119
// Destroy HTML and SVG elements
21120
for (key in this) {
21121
if (this[key] && key !== 'chart') {
21122
if (this[key].destroy) { // SVGElement
21123
this[key].destroy();
21124
} else if (this[key].nodeType) { // HTML element
21125
discardElement(this[key]);
21134
* Add logic to normalize the zoomed range in order to preserve the pressed state of range selector buttons
21136
Axis.prototype.toFixedRange = function (pxMin, pxMax, fixedMin, fixedMax) {
21137
var fixedRange = this.chart && this.chart.fixedRange,
21138
newMin = pick(fixedMin, this.translate(pxMin, true)),
21139
newMax = pick(fixedMax, this.translate(pxMax, true)),
21140
changeRatio = fixedRange && (newMax - newMin) / fixedRange;
21142
// If the difference between the fixed range and the actual requested range is
21143
// too great, the user is dragging across an ordinal gap, and we need to release
21144
// the range selector button.
21145
if (changeRatio > 0.7 && changeRatio < 1.3) {
21147
newMin = newMax - fixedRange;
21149
newMax = newMin + fixedRange;
21159
// Initialize scroller for stock charts
21160
wrap(Chart.prototype, 'init', function (proceed, options, callback) {
21162
addEvent(this, 'init', function () {
21163
if (this.options.rangeSelector.enabled) {
21164
this.rangeSelector = new RangeSelector(this);
21168
proceed.call(this, options, callback);
21173
Highcharts.RangeSelector = RangeSelector;
21175
/* ****************************************************************************
21176
* End Range Selector code *
21177
*****************************************************************************/
21181
Chart.prototype.callbacks.push(function (chart) {
21183
scroller = chart.scroller,
21184
rangeSelector = chart.rangeSelector;
21186
function renderScroller() {
21187
extremes = chart.xAxis[0].getExtremes();
21188
scroller.render(extremes.min, extremes.max);
21191
function renderRangeSelector() {
21192
extremes = chart.xAxis[0].getExtremes();
21193
if (!isNaN(extremes.min)) {
21194
rangeSelector.render(extremes.min, extremes.max);
21198
function afterSetExtremesHandlerScroller(e) {
21199
if (e.triggerOp !== 'navigator-drag') {
21200
scroller.render(e.min, e.max);
21204
function afterSetExtremesHandlerRangeSelector(e) {
21205
rangeSelector.render(e.min, e.max);
21208
function destroyEvents() {
21210
removeEvent(chart.xAxis[0], 'afterSetExtremes', afterSetExtremesHandlerScroller);
21212
if (rangeSelector) {
21213
removeEvent(chart, 'resize', renderRangeSelector);
21214
removeEvent(chart.xAxis[0], 'afterSetExtremes', afterSetExtremesHandlerRangeSelector);
21218
// initiate the scroller
21220
// redraw the scroller on setExtremes
21221
addEvent(chart.xAxis[0], 'afterSetExtremes', afterSetExtremesHandlerScroller);
21223
// redraw the scroller on chart resize or box resize
21224
wrap(chart, 'drawChartBox', function (proceed) {
21225
var isDirtyBox = this.isDirtyBox;
21226
proceed.call(this);
21235
if (rangeSelector) {
21236
// redraw the scroller on setExtremes
21237
addEvent(chart.xAxis[0], 'afterSetExtremes', afterSetExtremesHandlerRangeSelector);
21239
// redraw the scroller chart resize
21240
addEvent(chart, 'resize', renderRangeSelector);
21243
renderRangeSelector();
21246
// Remove resize/afterSetExtremes at chart destroy
21247
addEvent(chart, 'destroy', destroyEvents);
21250
* A wrapper for Chart with all the default values for a Stock chart
21252
Highcharts.StockChart = function (options, callback) {
21253
var seriesOptions = options.series, // to increase performance, don't merge the data
21256
// Always disable startOnTick:true on the main axis when the navigator is enabled (#1090)
21257
navigatorEnabled = pick(options.navigator && options.navigator.enabled, true),
21258
disableStartOnTick = navigatorEnabled ? {
21259
startOnTick: false,
21285
// apply X axis options to both single and multi y axes
21286
options.xAxis = map(splat(options.xAxis || {}), function (xAxisOptions) {
21287
return merge({ // defaults
21295
overflow: 'justify'
21297
showLastLabel: true
21298
}, xAxisOptions, // user options
21299
{ // forced options
21307
// apply Y axis options to both single and multi y axes
21308
options.yAxis = map(splat(options.yAxis || {}), function (yAxisOptions) {
21309
opposite = yAxisOptions.opposite;
21310
return merge({ // defaults
21312
align: opposite ? 'right' : 'left',
21313
x: opposite ? -2 : 2,
21316
showLastLabel: false,
21320
}, yAxisOptions // user options
21324
options.series = null;
21353
spline: lineOptions,
21355
areaspline: lineOptions,
21356
arearange: lineOptions,
21357
areasplinerange: lineOptions,
21358
column: columnOptions,
21359
columnrange: columnOptions,
21360
candlestick: columnOptions,
21361
ohlc: columnOptions
21365
options, // user's options
21367
{ // forced options
21368
_stock: true, // internal flag
21374
options.series = seriesOptions;
21377
return new Chart(options, callback);
21380
// Implement the pinchType option
21381
wrap(Pointer.prototype, 'init', function (proceed, chart, options) {
21383
var pinchType = options.chart.pinchType || '';
21385
proceed.call(this, chart, options);
21388
this.pinchX = this.pinchHor = pinchType.indexOf('x') !== -1;
21389
this.pinchY = this.pinchVert = pinchType.indexOf('y') !== -1;
21392
// Override getPlotLinePath to allow for multipane charts
21393
Axis.prototype.getPlotLinePath = function (value, lineWidth, old, force, translatedValue) {
21395
series = (this.isLinked ? this.linkedParent.series : this.series),
21396
renderer = axis.chart.renderer,
21397
axisLeft = axis.left,
21398
axisTop = axis.top,
21405
// Get the related axes.
21406
var axes = (this.isXAxis ?
21407
(defined(this.options.yAxis) ?
21408
[this.chart.yAxis[this.options.yAxis]] :
21409
map(series, function (S) { return S.yAxis; })
21411
(defined(this.options.xAxis) ?
21412
[this.chart.xAxis[this.options.xAxis]] :
21413
map(series, function (S) { return S.xAxis; })
21417
// remove duplicates in the axes array
21419
each(axes, function (axis2) {
21420
if (inArray(axis2, uAxes) === -1) {
21425
translatedValue = pick(translatedValue, axis.translate(value, null, null, old));
21427
if (!isNaN(translatedValue)) {
21429
each(uAxes, function (axis2) {
21431
y2 = y1 + axis2.len;
21432
x1 = x2 = mathRound(translatedValue + axis.transB);
21434
if ((x1 >= axisLeft && x1 <= axisLeft + axis.width) || force) {
21435
result.push('M', x1, y1, 'L', x2, y2);
21439
each(uAxes, function (axis2) {
21441
x2 = x1 + axis2.width;
21442
y1 = y2 = mathRound(axisTop + axis.height - translatedValue);
21444
if ((y1 >= axisTop && y1 <= axisTop + axis.height) || force) {
21445
result.push('M', x1, y1, 'L', x2, y2);
21450
if (result.length > 0) {
21451
return renderer.crispPolyLine(result, lineWidth || 1);
21457
// Function to crisp a line with multiple segments
21458
SVGRenderer.prototype.crispPolyLine = function (points, width) {
21459
// points format: [M, 0, 0, L, 100, 0]
21460
// normalize to a crisp line
21462
for (i = 0; i < points.length; i = i + 6) {
21463
if (points[i + 1] === points[i + 4]) {
21464
// Substract due to #1129. Now bottom and left axis gridlines behave the same.
21465
points[i + 1] = points[i + 4] = mathRound(points[i + 1]) - (width % 2 / 2);
21467
if (points[i + 2] === points[i + 5]) {
21468
points[i + 2] = points[i + 5] = mathRound(points[i + 2]) + (width % 2 / 2);
21473
if (Renderer === Highcharts.VMLRenderer) {
21474
VMLRenderer.prototype.crispPolyLine = SVGRenderer.prototype.crispPolyLine;
21478
// Wrapper to hide the label
21479
wrap(Axis.prototype, 'hideCrosshair', function (proceed, i) {
21480
proceed.call(this, i);
21482
if (!defined(this.crossLabelArray)) { return; }
21485
if (this.crossLabelArray[i]) { this.crossLabelArray[i].hide(); }
21487
each(this.crossLabelArray, function (crosslabel) {
21493
// Wrapper to draw the label
21494
wrap(Axis.prototype, 'drawCrosshair', function (proceed, e, point) {
21495
// Draw the crosshair
21496
proceed.call(this, e, point);
21498
// Check if the label has to be drawn
21499
if (!defined(this.crosshair.label) || !this.crosshair.label.enabled || !defined(point)) {
21503
var chart = this.chart,
21504
options = this.options.crosshair.label, // the label's options
21505
axis = this.isXAxis ? 'x' : 'y', // axis name
21506
horiz = this.horiz, // axis orientation
21507
opposite = this.opposite, // axis position
21508
left = this.left, // left position
21509
top = this.top, // top position
21510
crossLabel = this.crossLabel, // reference to the svgElement
21514
formatOption = options.format,
21518
// If the label does not exist yet, create it.
21520
crossLabel = this.crossLabel = chart.renderer.label()
21522
align: options.align || (horiz ? 'center' : opposite ? (this.labelAlign === 'right' ? 'right' : 'left') : (this.labelAlign === 'left' ? 'left' : 'center')),
21524
height: horiz ? 16 : UNDEFINED,
21525
fill: options.backgroundColor || (this.series[0] && this.series[0].color) || 'gray',
21526
padding: pick(options.padding, 2),
21527
stroke: options.borderColor || null,
21528
'stroke-width': options.borderWidth || 0
21532
fontWeight: 'normal',
21534
textAlign: 'center'
21540
posx = point.plotX + left;
21541
posy = top + (opposite ? 0 : this.height);
21543
posx = opposite ? this.width + left : 0;
21544
posy = point.plotY + top;
21547
// if the crosshair goes out of view (too high or too low, hide it and hide the label)
21548
if (posy < top || posy > top + this.height) {
21549
this.hideCrosshair();
21553
// TODO: Dynamic date formats like in Series.tooltipHeaderFormat.
21554
if (!formatOption && !options.formatter) {
21555
if (this.isDatetimeAxis) {
21556
formatFormat = '%b %d, %Y';
21558
formatOption = '{value' + (formatFormat ? ':' + formatFormat : '') + '}';
21565
text: formatOption ? format(formatOption, {value: point[axis]}) : options.formatter.call(this, point[axis]),
21566
visibility: VISIBLE
21568
crossBox = crossLabel.box;
21570
// now it is placed we can correct its position
21572
if (((this.options.tickPosition === 'inside') && !opposite) ||
21573
((this.options.tickPosition !== 'inside') && opposite)) {
21574
posy = crossLabel.y - crossBox.height;
21577
posy = crossLabel.y - (crossBox.height / 2);
21583
left: left - crossBox.x,
21584
right: left + this.width - crossBox.x
21588
left: this.labelAlign === 'left' ? left : 0,
21589
right: this.labelAlign === 'right' ? left + this.width : chart.chartWidth
21594
if (crossLabel.translateX < limit.left) {
21595
posx += limit.left - crossLabel.translateX;
21598
if (crossLabel.translateX + crossBox.width >= limit.right) {
21599
posx -= crossLabel.translateX + crossBox.width - limit.right;
21602
// show the crosslabel
21603
crossLabel.attr({x: posx, y: posy, visibility: VISIBLE});
21606
/* ****************************************************************************
21607
* Start value compare logic *
21608
*****************************************************************************/
21610
var seriesInit = seriesProto.init,
21611
seriesProcessData = seriesProto.processData,
21612
pointTooltipFormatter = Point.prototype.tooltipFormatter;
21615
* Extend series.init by adding a method to modify the y value used for plotting
21616
* on the y axis. This method is called both from the axis when finding dataMin
21617
* and dataMax, and from the series.translate method.
21619
seriesProto.init = function () {
21621
// Call base method
21622
seriesInit.apply(this, arguments);
21624
// Set comparison mode
21625
this.setCompare(this.options.compare);
21629
* The setCompare method can be called also from the outside after render time
21631
seriesProto.setCompare = function (compare) {
21633
// Set or unset the modifyValue method
21634
this.modifyValue = (compare === 'value' || compare === 'percent') ? function (value, point) {
21635
var compareValue = this.compareValue;
21637
if (value !== UNDEFINED) { // #2601
21639
// get the modified value
21640
value = compare === 'value' ?
21641
value - compareValue : // compare value
21642
value = 100 * (value / compareValue) - 100; // compare percent
21644
// record for tooltip etc.
21646
point.change = value;
21655
if (this.chart.hasRendered) {
21656
this.isDirty = true;
21662
* Extend series.processData by finding the first y value in the plot area,
21663
* used for comparing the following values
21665
seriesProto.processData = function () {
21672
// call base method
21673
seriesProcessData.apply(this, arguments);
21675
if (series.xAxis && series.processedYData) { // not pies
21678
processedXData = series.processedXData;
21679
processedYData = series.processedYData;
21680
length = processedYData.length;
21682
// find the first value for comparison
21683
for (; i < length; i++) {
21684
if (typeof processedYData[i] === NUMBER && processedXData[i] >= series.xAxis.min) {
21685
series.compareValue = processedYData[i];
21693
* Modify series extremes
21695
wrap(seriesProto, 'getExtremes', function (proceed) {
21696
proceed.call(this);
21698
if (this.modifyValue) {
21699
this.dataMax = this.modifyValue(this.dataMax);
21700
this.dataMin = this.modifyValue(this.dataMin);
21705
* Add a utility method, setCompare, to the Y axis
21707
Axis.prototype.setCompare = function (compare, redraw) {
21708
if (!this.isXAxis) {
21709
each(this.series, function (series) {
21710
series.setCompare(compare);
21712
if (pick(redraw, true)) {
21713
this.chart.redraw();
21719
* Extend the tooltip formatter by adding support for the point.change variable
21720
* as well as the changeDecimals option
21722
Point.prototype.tooltipFormatter = function (pointFormat) {
21725
pointFormat = pointFormat.replace(
21727
(point.change > 0 ? '+' : '') + numberFormat(point.change, pick(point.series.tooltipOptions.changeDecimals, 2))
21730
return pointTooltipFormatter.apply(this, [pointFormat]);
21733
/* ****************************************************************************
21734
* End value compare logic *
21735
*****************************************************************************/
21737
// global variables
21738
extend(Highcharts, {
21746
Renderer: Renderer,
21748
SVGElement: SVGElement,
21749
SVGRenderer: SVGRenderer,
21752
arrayMin: arrayMin,
21753
arrayMax: arrayMax,
21755
dateFormat: dateFormat,
21757
pathAnim: pathAnim,
21758
getOptions: getOptions,
21759
hasBidiBug: hasBidiBug,
21760
isTouchDevice: isTouchDevice,
21761
numberFormat: numberFormat,
21762
seriesTypes: seriesTypes,
21763
setOptions: setOptions,
21764
addEvent: addEvent,
21765
removeEvent: removeEvent,
21766
createElement: createElement,
21767
discardElement: discardElement,
21775
extendClass: extendClass,
21780
vml: !hasSVG && !useCanVG,