~jstys-z/helioviewer.org/timeline

« back to all changes in this revision

Viewing changes to timeline/Highstock-1.3.10/js/highstock.src.js

  • Committer: Jeff Stys
  • Date: 2014-04-21 12:46:26 UTC
  • Revision ID: jstys@sesda3.com-20140421124626-2332pb2dyjc33jxi
Proof-of-concept version of Data Coverage Timeline using Highchart/Highstock javascript library.  Changes to getDataCoverage API in order to feed the necessary data to the Timeline

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
// ==ClosureCompiler==
 
2
// @compilation_level SIMPLE_OPTIMIZATIONS
 
3
 
 
4
/**
 
5
 * @license Highstock JS v1.3.10 (2014-03-10)
 
6
 *
 
7
 * (c) 2009-2014 Torstein Honsi
 
8
 *
 
9
 * License: www.highcharts.com/license
 
10
 */
 
11
 
 
12
// JSLint options:
 
13
/*global Highcharts, document, window, navigator, setInterval, clearInterval, clearTimeout, setTimeout, location, jQuery, $, console, each, grep */
 
14
 
 
15
(function () {
 
16
// encapsulated variables
 
17
var UNDEFINED,
 
18
        doc = document,
 
19
        win = window,
 
20
        math = Math,
 
21
        mathRound = math.round,
 
22
        mathFloor = math.floor,
 
23
        mathCeil = math.ceil,
 
24
        mathMax = math.max,
 
25
        mathMin = math.min,
 
26
        mathAbs = math.abs,
 
27
        mathCos = math.cos,
 
28
        mathSin = math.sin,
 
29
        mathPI = math.PI,
 
30
        deg2rad = mathPI * 2 / 360,
 
31
 
 
32
 
 
33
        // some variables
 
34
        userAgent = navigator.userAgent,
 
35
        isOpera = win.opera,
 
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,
 
45
        Renderer,
 
46
        hasTouch,
 
47
        symbolSizes = {},
 
48
        idCounter = 0,
 
49
        garbageBin,
 
50
        defaultOptions,
 
51
        dateFormat, // function
 
52
        globalAnimation,
 
53
        pathAnim,
 
54
        timeUnits,
 
55
        noop = function () {},
 
56
        charts = [],
 
57
        PRODUCT = 'Highstock',
 
58
        VERSION = '1.3.10',
 
59
 
 
60
        // some constants for frequently used strings
 
61
        DIV = 'div',
 
62
        ABSOLUTE = 'absolute',
 
63
        RELATIVE = 'relative',
 
64
        HIDDEN = 'hidden',
 
65
        PREFIX = 'highcharts-',
 
66
        VISIBLE = 'visible',
 
67
        PX = 'px',
 
68
        NONE = 'none',
 
69
        M = 'M',
 
70
        L = 'L',
 
71
        numRegex = /^[0-9]+$/,
 
72
        NORMAL_STATE = '',
 
73
        HOVER_STATE = 'hover',
 
74
        SELECT_STATE = 'select',
 
75
        MILLISECOND = 'millisecond',
 
76
        SECOND = 'second',
 
77
        MINUTE = 'minute',
 
78
        HOUR = 'hour',
 
79
        DAY = 'day',
 
80
        WEEK = 'week',
 
81
        MONTH = 'month',
 
82
        YEAR = 'year',
 
83
        
 
84
        // Object for extending Axis
 
85
        AxisPlotLineOrBandExtension,
 
86
 
 
87
        // constants for attributes
 
88
        STROKE_WIDTH = 'stroke-width',
 
89
 
 
90
        // time methods, changed based on whether or not UTC is used
 
91
        makeTime,
 
92
        timezoneOffset,
 
93
        getMinutes,
 
94
        getHours,
 
95
        getDay,
 
96
        getDate,
 
97
        getMonth,
 
98
        getFullYear,
 
99
        setMinutes,
 
100
        setHours,
 
101
        setDate,
 
102
        setMonth,
 
103
        setFullYear,
 
104
 
 
105
 
 
106
        // lookup over the types and the associated classes
 
107
        seriesTypes = {};
 
108
 
 
109
// The Highcharts namespace
 
110
var Highcharts = win.Highcharts = win.Highcharts ? error(16, true) : {};
 
111
 
 
112
/**
 
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
 
116
 */
 
117
function extend(a, b) {
 
118
        var n;
 
119
        if (!a) {
 
120
                a = {};
 
121
        }
 
122
        for (n in b) {
 
123
                a[n] = b[n];
 
124
        }
 
125
        return a;
 
126
}
 
127
        
 
128
/**
 
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. 
 
134
 */
 
135
function merge() {
 
136
        var i,
 
137
                args = arguments,
 
138
                len,
 
139
                ret = {},
 
140
                doCopy = function (copy, original) {
 
141
                        var value, key;
 
142
 
 
143
                        // An object is replacing a primitive
 
144
                        if (typeof copy !== 'object') {
 
145
                                copy = {};
 
146
                        }
 
147
 
 
148
                        for (key in original) {
 
149
                                if (original.hasOwnProperty(key)) {
 
150
                                        value = original[key];
 
151
 
 
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);
 
156
                                
 
157
                                        // Primitives and arrays are copied over directly
 
158
                                        } else {
 
159
                                                copy[key] = original[key];
 
160
                                        }
 
161
                                }
 
162
                        }
 
163
                        return copy;
 
164
                };
 
165
 
 
166
        // If first argument is true, copy into the existing object. Used in setOptions.
 
167
        if (args[0] === true) {
 
168
                ret = args[1];
 
169
                args = Array.prototype.slice.call(args, 2);
 
170
        }
 
171
 
 
172
        // For each argument, extend the return
 
173
        len = args.length;
 
174
        for (i = 0; i < len; i++) {
 
175
                ret = doCopy(ret, args[i]);
 
176
        }
 
177
 
 
178
        return ret;
 
179
}
 
180
 
 
181
/**
 
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
 
185
 */
 
186
function hash() {
 
187
        var i = 0,
 
188
                args = arguments,
 
189
                length = args.length,
 
190
                obj = {};
 
191
        for (; i < length; i++) {
 
192
                obj[args[i++]] = args[i];
 
193
        }
 
194
        return obj;
 
195
}
 
196
 
 
197
/**
 
198
 * Shortcut for parseInt
 
199
 * @param {Object} s
 
200
 * @param {Number} mag Magnitude
 
201
 */
 
202
function pInt(s, mag) {
 
203
        return parseInt(s, mag || 10);
 
204
}
 
205
 
 
206
/**
 
207
 * Check for string
 
208
 * @param {Object} s
 
209
 */
 
210
function isString(s) {
 
211
        return typeof s === 'string';
 
212
}
 
213
 
 
214
/**
 
215
 * Check for object
 
216
 * @param {Object} obj
 
217
 */
 
218
function isObject(obj) {
 
219
        return typeof obj === 'object';
 
220
}
 
221
 
 
222
/**
 
223
 * Check for array
 
224
 * @param {Object} obj
 
225
 */
 
226
function isArray(obj) {
 
227
        return Object.prototype.toString.call(obj) === '[object Array]';
 
228
}
 
229
 
 
230
/**
 
231
 * Check for number
 
232
 * @param {Object} n
 
233
 */
 
234
function isNumber(n) {
 
235
        return typeof n === 'number';
 
236
}
 
237
 
 
238
function log2lin(num) {
 
239
        return math.log(num) / math.LN10;
 
240
}
 
241
function lin2log(num) {
 
242
        return math.pow(10, num);
 
243
}
 
244
 
 
245
/**
 
246
 * Remove last occurence of an item from an array
 
247
 * @param {Array} arr
 
248
 * @param {Mixed} item
 
249
 */
 
250
function erase(arr, item) {
 
251
        var i = arr.length;
 
252
        while (i--) {
 
253
                if (arr[i] === item) {
 
254
                        arr.splice(i, 1);
 
255
                        break;
 
256
                }
 
257
        }
 
258
        //return arr;
 
259
}
 
260
 
 
261
/**
 
262
 * Returns true if the object is not null or undefined. Like MooTools' $.defined.
 
263
 * @param {Object} obj
 
264
 */
 
265
function defined(obj) {
 
266
        return obj !== UNDEFINED && obj !== null;
 
267
}
 
268
 
 
269
/**
 
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.
 
272
 *
 
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
 
276
 */
 
277
function attr(elem, prop, value) {
 
278
        var key,
 
279
                setAttribute = 'setAttribute',
 
280
                ret;
 
281
 
 
282
        // if the prop is a string
 
283
        if (isString(prop)) {
 
284
                // set the value
 
285
                if (defined(value)) {
 
286
 
 
287
                        elem[setAttribute](prop, value);
 
288
 
 
289
                // get the value
 
290
                } else if (elem && elem.getAttribute) { // elem not defined when printing pie demo...
 
291
                        ret = elem.getAttribute(prop);
 
292
                }
 
293
 
 
294
        // else if prop is defined, it is a hash of key/value pairs
 
295
        } else if (defined(prop) && isObject(prop)) {
 
296
                for (key in prop) {
 
297
                        elem[setAttribute](key, prop[key]);
 
298
                }
 
299
        }
 
300
        return ret;
 
301
}
 
302
/**
 
303
 * Check if an element is an array, and if not, make it into an array. Like
 
304
 * MooTools' $.splat.
 
305
 */
 
306
function splat(obj) {
 
307
        return isArray(obj) ? obj : [obj];
 
308
}
 
309
 
 
310
 
 
311
/**
 
312
 * Return the first value that is defined. Like MooTools' $.pick.
 
313
 */
 
314
function pick() {
 
315
        var args = arguments,
 
316
                i,
 
317
                arg,
 
318
                length = args.length;
 
319
        for (i = 0; i < length; i++) {
 
320
                arg = args[i];
 
321
                if (typeof arg !== 'undefined' && arg !== null) {
 
322
                        return arg;
 
323
                }
 
324
        }
 
325
}
 
326
 
 
327
/**
 
328
 * Set CSS on a given element
 
329
 * @param {Object} el
 
330
 * @param {Object} styles Style object with camel case property names
 
331
 */
 
332
function css(el, styles) {
 
333
        if (isIE && !hasSVG) { // #2686
 
334
                if (styles && styles.opacity !== UNDEFINED) {
 
335
                        styles.filter = 'alpha(opacity=' + (styles.opacity * 100) + ')';
 
336
                }
 
337
        }
 
338
        extend(el.style, styles);
 
339
}
 
340
 
 
341
/**
 
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
 
348
 */
 
349
function createElement(tag, attribs, styles, parent, nopad) {
 
350
        var el = doc.createElement(tag);
 
351
        if (attribs) {
 
352
                extend(el, attribs);
 
353
        }
 
354
        if (nopad) {
 
355
                css(el, {padding: 0, border: NONE, margin: 0});
 
356
        }
 
357
        if (styles) {
 
358
                css(el, styles);
 
359
        }
 
360
        if (parent) {
 
361
                parent.appendChild(el);
 
362
        }
 
363
        return el;
 
364
}
 
365
 
 
366
/**
 
367
 * Extend a prototyped class by new members
 
368
 * @param {Object} parent
 
369
 * @param {Object} members
 
370
 */
 
371
function extendClass(parent, members) {
 
372
        var object = function () {};
 
373
        object.prototype = new parent();
 
374
        extend(object.prototype, members);
 
375
        return object;
 
376
}
 
377
 
 
378
/**
 
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
 
384
 */
 
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/
 
388
                n = +number || 0,
 
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;
 
397
 
 
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) : "");
 
400
}
 
401
 
 
402
/**
 
403
 * Pad a string to a given length by adding 0 to the beginning
 
404
 * @param {Number} number
 
405
 * @param {Number} length
 
406
 */
 
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;
 
410
}
 
411
 
 
412
/**
 
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 
 
418
 * argument. 
 
419
 */
 
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);
 
426
        };
 
427
}
 
428
 
 
429
/**
 
430
 * Based on http://www.php.net/manual/en/function.strftime.php
 
431
 * @param {String} format
 
432
 * @param {Number} timestamp
 
433
 * @param {Boolean} capitalize
 
434
 */
 
435
dateFormat = function (format, timestamp, capitalize) {
 
436
        if (!defined(timestamp) || isNaN(timestamp)) {
 
437
                return 'Invalid date';
 
438
        }
 
439
        format = pick(format, '%Y-%m-%d %H:%M:%S');
 
440
 
 
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,
 
451
 
 
452
                // List all format keys. Custom formats can be added from the outside. 
 
453
                replacements = extend({
 
454
 
 
455
                        // Day
 
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
 
460
 
 
461
                        // Week (none implemented)
 
462
                        //'W': weekNumber(),
 
463
 
 
464
                        // Month
 
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
 
468
 
 
469
                        // Year
 
470
                        'y': fullYear.toString().substr(2, 2), // Two digits year, like 09 for 2009
 
471
                        'Y': fullYear, // Four digits year, like 2009
 
472
 
 
473
                        // Time
 
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);
 
483
 
 
484
 
 
485
        // do the replaces
 
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]);
 
489
                }
 
490
        }
 
491
 
 
492
        // Optionally capitalize the string and return
 
493
        return capitalize ? format.substr(0, 1).toUpperCase() + format.substr(1) : format;
 
494
};
 
495
 
 
496
/** 
 
497
 * Format a single variable. Similar to sprintf, without the % prefix.
 
498
 */
 
499
function formatSingle(format, val) {
 
500
        var floatRegex = /f$/,
 
501
                decRegex = /\.([0-9])/,
 
502
                lang = defaultOptions.lang,
 
503
                decimals;
 
504
 
 
505
        if (floatRegex.test(format)) { // float
 
506
                decimals = format.match(decRegex);
 
507
                decimals = decimals ? decimals[1] : -1;
 
508
                val = numberFormat(
 
509
                        val,
 
510
                        decimals,
 
511
                        lang.decimalPoint,
 
512
                        format.indexOf(',') > -1 ? lang.thousandsSep : ''
 
513
                );
 
514
        } else {
 
515
                val = dateFormat(format, val);
 
516
        }
 
517
        return val;
 
518
}
 
519
 
 
520
/**
 
521
 * Format a string according to a subset of the rules of Python's String.format method.
 
522
 */
 
523
function format(str, ctx) {
 
524
        var splitter = '{',
 
525
                isInside = false,
 
526
                segment,
 
527
                valueAndFormat,
 
528
                path,
 
529
                i,
 
530
                len,
 
531
                ret = [],
 
532
                val,
 
533
                index;
 
534
        
 
535
        while ((index = str.indexOf(splitter)) !== -1) {
 
536
                
 
537
                segment = str.slice(0, index);
 
538
                if (isInside) { // we're on the closing bracket looking back
 
539
                        
 
540
                        valueAndFormat = segment.split(':');
 
541
                        path = valueAndFormat.shift().split('.'); // get first and leave format
 
542
                        len = path.length;
 
543
                        val = ctx;
 
544
 
 
545
                        // Assign deeper paths
 
546
                        for (i = 0; i < len; i++) {
 
547
                                val = val[path[i]];
 
548
                        }
 
549
 
 
550
                        // Format the replacement
 
551
                        if (valueAndFormat.length) {
 
552
                                val = formatSingle(valueAndFormat.join(':'), val);
 
553
                        }
 
554
 
 
555
                        // Push the result and advance the cursor
 
556
                        ret.push(val);
 
557
                        
 
558
                } else {
 
559
                        ret.push(segment);
 
560
                        
 
561
                }
 
562
                str = str.slice(index + 1); // the rest
 
563
                isInside = !isInside; // toggle
 
564
                splitter = isInside ? '}' : '{'; // now look for next matching bracket
 
565
        }
 
566
        ret.push(str);
 
567
        return ret.join('');
 
568
}
 
569
 
 
570
/**
 
571
 * Get the magnitude of a number
 
572
 */
 
573
function getMagnitude(num) {
 
574
        return math.pow(10, mathFloor(math.log(num) / math.LN10));
 
575
}
 
576
 
 
577
/**
 
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
 
583
 */
 
584
function normalizeTickInterval(interval, multiples, magnitude, options) {
 
585
        var normalized, i;
 
586
 
 
587
        // round to a tenfold of 1, 2, 2.5 or 5
 
588
        magnitude = pick(magnitude, 1);
 
589
        normalized = interval / magnitude;
 
590
 
 
591
        // multiples for a linear scale
 
592
        if (!multiples) {
 
593
                multiples = [1, 2, 2.5, 5, 10];
 
594
 
 
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];
 
601
                        }
 
602
                }
 
603
        }
 
604
 
 
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) {
 
609
                        break;
 
610
                }
 
611
        }
 
612
 
 
613
        // multiply back to the correct magnitude
 
614
        interval *= magnitude;
 
615
 
 
616
        return interval;
 
617
}
 
618
 
 
619
 
 
620
/**
 
621
 * Helper class that contains variuos counters that are local to the chart.
 
622
 */
 
623
function ChartCounters() {
 
624
        this.color = 0;
 
625
        this.symbol = 0;
 
626
}
 
627
 
 
628
ChartCounters.prototype =  {
 
629
        /**
 
630
         * Wraps the color counter if it reaches the specified length.
 
631
         */
 
632
        wrapColor: function (length) {
 
633
                if (this.color >= length) {
 
634
                        this.color = 0;
 
635
                }
 
636
        },
 
637
 
 
638
        /**
 
639
         * Wraps the symbol counter if it reaches the specified length.
 
640
         */
 
641
        wrapSymbol: function (length) {
 
642
                if (this.symbol >= length) {
 
643
                        this.symbol = 0;
 
644
                }
 
645
        }
 
646
};
 
647
 
 
648
 
 
649
/**
 
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.
 
652
 */
 
653
function stableSort(arr, sortFunction) {
 
654
        var length = arr.length,
 
655
                sortValue,
 
656
                i;
 
657
 
 
658
        // Add index to each item
 
659
        for (i = 0; i < length; i++) {
 
660
                arr[i].ss_i = i; // stable sort index
 
661
        }
 
662
 
 
663
        arr.sort(function (a, b) {
 
664
                sortValue = sortFunction(a, b);
 
665
                return sortValue === 0 ? a.ss_i - b.ss_i : sortValue;
 
666
        });
 
667
 
 
668
        // Remove index from items
 
669
        for (i = 0; i < length; i++) {
 
670
                delete arr[i].ss_i; // stable sort index
 
671
        }
 
672
}
 
673
 
 
674
/**
 
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.
 
678
 */
 
679
function arrayMin(data) {
 
680
        var i = data.length,
 
681
                min = data[0];
 
682
 
 
683
        while (i--) {
 
684
                if (data[i] < min) {
 
685
                        min = data[i];
 
686
                }
 
687
        }
 
688
        return min;
 
689
}
 
690
 
 
691
/**
 
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.
 
695
 */
 
696
function arrayMax(data) {
 
697
        var i = data.length,
 
698
                max = data[0];
 
699
 
 
700
        while (i--) {
 
701
                if (data[i] > max) {
 
702
                        max = data[i];
 
703
                }
 
704
        }
 
705
        return max;
 
706
}
 
707
 
 
708
/**
 
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
 
711
 * then delete'ed.
 
712
 * @param {Object} The object to destroy properties on
 
713
 * @param {Object} Exception, do not destroy this property, only delete it.
 
714
 */
 
715
function destroyObjectProperties(obj, except) {
 
716
        var n;
 
717
        for (n in obj) {
 
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
 
721
                        obj[n].destroy();
 
722
                }
 
723
 
 
724
                // Delete the property from the object.
 
725
                delete obj[n];
 
726
        }
 
727
}
 
728
 
 
729
 
 
730
/**
 
731
 * Discard an element by moving it to the bin and delete
 
732
 * @param {Object} The HTML node to discard
 
733
 */
 
734
function discardElement(element) {
 
735
        // create a garbage bin element, not part of the DOM
 
736
        if (!garbageBin) {
 
737
                garbageBin = createElement(DIV);
 
738
        }
 
739
 
 
740
        // move the node and empty bin
 
741
        if (element) {
 
742
                garbageBin.appendChild(element);
 
743
        }
 
744
        garbageBin.innerHTML = '';
 
745
}
 
746
 
 
747
/**
 
748
 * Provide error messages for debugging, with links to online explanation 
 
749
 */
 
750
function error(code, stop) {
 
751
        var msg = 'Highcharts error #' + code + ': www.highcharts.com/errors/' + code;
 
752
        if (stop) {
 
753
                throw msg;
 
754
        } else if (win.console) {
 
755
                console.log(msg);
 
756
        }
 
757
}
 
758
 
 
759
/**
 
760
 * Fix JS round off float errors
 
761
 * @param {Number} num
 
762
 */
 
763
function correctFloat(num) {
 
764
        return parseFloat(
 
765
                num.toPrecision(14)
 
766
        );
 
767
}
 
768
 
 
769
/**
 
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
 
774
 */
 
775
function setAnimation(animation, chart) {
 
776
        globalAnimation = pick(animation, chart.animation);
 
777
}
 
778
 
 
779
/**
 
780
 * The time unit lookup
 
781
 */
 
782
/*jslint white: true*/
 
783
timeUnits = hash(
 
784
        MILLISECOND, 1,
 
785
        SECOND, 1000,
 
786
        MINUTE, 60000,
 
787
        HOUR, 3600000,
 
788
        DAY, 24 * 3600000,
 
789
        WEEK, 7 * 24 * 3600000,
 
790
        MONTH, 31 * 24 * 3600000,
 
791
        YEAR, 31556952000
 
792
);
 
793
/*jslint white: false*/
 
794
/**
 
795
 * Path interpolation algorithm used across adapters
 
796
 */
 
797
pathAnim = {
 
798
        /**
 
799
         * Prepare start and end values so that the path can be animated one to one
 
800
         */
 
801
        init: function (elem, fromD, toD) {
 
802
                fromD = fromD || '';
 
803
                var shift = elem.shift,
 
804
                        bezier = fromD.indexOf('C') > -1,
 
805
                        numParams = bezier ? 7 : 3,
 
806
                        endLength,
 
807
                        slice,
 
808
                        i,
 
809
                        start = fromD.split(' '),
 
810
                        end = [].concat(toD), // copy
 
811
                        startBaseLine,
 
812
                        endBaseLine,
 
813
                        sixify = function (arr) { // in splines make move points have six parameters like bezier curves
 
814
                                i = arr.length;
 
815
                                while (i--) {
 
816
                                        if (arr[i] === M) {
 
817
                                                arr.splice(i + 1, 0, arr[i + 1], arr[i + 2], arr[i + 1], arr[i + 2]);
 
818
                                        }
 
819
                                }
 
820
                        };
 
821
 
 
822
                if (bezier) {
 
823
                        sixify(start);
 
824
                        sixify(end);
 
825
                }
 
826
 
 
827
                // pull out the base lines before padding
 
828
                if (elem.isArea) {
 
829
                        startBaseLine = start.splice(start.length - 6, 6);
 
830
                        endBaseLine = end.splice(end.length - 6, 6);
 
831
                }
 
832
 
 
833
                // if shifting points, prepend a dummy point to the end path
 
834
                if (shift <= end.length / numParams && start.length === end.length) {
 
835
                        while (shift--) {
 
836
                                end = [].concat(end).splice(0, numParams).concat(end);
 
837
                        }
 
838
                }
 
839
                elem.shift = 0; // reset for following animations
 
840
 
 
841
                // copy and append last point until the length matches the end length
 
842
                if (start.length) {
 
843
                        endLength = end.length;
 
844
                        while (start.length < endLength) {
 
845
 
 
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];
 
851
                                }
 
852
                                start = start.concat(slice);
 
853
                        }
 
854
                }
 
855
 
 
856
                if (startBaseLine) { // append the base lines for areas
 
857
                        start = start.concat(startBaseLine);
 
858
                        end = end.concat(endBaseLine);
 
859
                }
 
860
                return [start, end];
 
861
        },
 
862
 
 
863
        /**
 
864
         * Interpolate each value of the path and return the array
 
865
         */
 
866
        step: function (start, end, pos, complete) {
 
867
                var ret = [],
 
868
                        i = start.length,
 
869
                        startVal;
 
870
 
 
871
                if (pos === 1) { // land on the final path without adjustment points appended in the ends
 
872
                        ret = complete;
 
873
 
 
874
                } else if (i === end.length && pos < 1) {
 
875
                        while (i--) {
 
876
                                startVal = parseFloat(start[i]);
 
877
                                ret[i] =
 
878
                                        isNaN(startVal) ? // a letter instruction like M or L
 
879
                                                start[i] :
 
880
                                                pos * (parseFloat(end[i] - startVal)) + startVal;
 
881
 
 
882
                        }
 
883
                } else { // if animation is finished or length not matching, land on right value
 
884
                        ret = end;
 
885
                }
 
886
                return ret;
 
887
        }
 
888
};
 
889
 
 
890
(function ($) {
 
891
        /**
 
892
         * The default HighchartsAdapter for jQuery
 
893
         */
 
894
        win.HighchartsAdapter = win.HighchartsAdapter || ($ && {
 
895
                
 
896
                /**
 
897
                 * Initialize the adapter by applying some extensions to jQuery
 
898
                 */
 
899
                init: function (pathAnim) {
 
900
                        
 
901
                        // extend the animate function to allow SVG animations
 
902
                        var Fx = $.fx,
 
903
                                Step = Fx.step,
 
904
                                dSetter,
 
905
                                Tween = $.Tween,
 
906
                                propHooks = Tween && Tween.propHooks,
 
907
                                opacityHook = $.cssHooks.opacity;
 
908
                        
 
909
                        /*jslint unparam: true*//* allow unused param x in this function */
 
910
                        $.extend($.easing, {
 
911
                                easeOutQuad: function (x, t, b, c, d) {
 
912
                                        return -c * (t /= d) * (t - 2) + b;
 
913
                                }
 
914
                        });
 
915
                        /*jslint unparam: false*/
 
916
                
 
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) {
 
919
                                var obj = Step,
 
920
                                        base;
 
921
                                        
 
922
                                // Handle different parent objects
 
923
                                if (fn === 'cur') {
 
924
                                        obj = Fx.prototype; // 'cur', the getter, relates to Fx.prototype
 
925
                                
 
926
                                } else if (fn === '_default' && Tween) { // jQuery 1.8 model
 
927
                                        obj = propHooks[fn];
 
928
                                        fn = 'set';
 
929
                                }
 
930
                
 
931
                                // Overwrite the method
 
932
                                base = obj[fn];
 
933
                                if (base) { // step.width and step.height don't exist in jQuery < 1.7
 
934
                
 
935
                                        // create the extended function replacement
 
936
                                        obj[fn] = function (fx) {
 
937
 
 
938
                                                var elem;
 
939
                                                
 
940
                                                // Fx.prototype.cur does not use fx argument
 
941
                                                fx = i ? fx : this;
 
942
 
 
943
                                                // Don't run animations on textual properties like align (#1821)
 
944
                                                if (fx.prop === 'align') {
 
945
                                                        return;
 
946
                                                }
 
947
                
 
948
                                                // shortcut
 
949
                                                elem = fx.elem;
 
950
                
 
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
 
956
                                        };
 
957
                                }
 
958
                        });
 
959
 
 
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);
 
963
                        });
 
964
                        
 
965
                        
 
966
                        // Define the setter function for d (path definitions)
 
967
                        dSetter = function (fx) {
 
968
                                var elem = fx.elem,
 
969
                                        ends;
 
970
                
 
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
 
973
                                // in these cases
 
974
                                if (!fx.started) {
 
975
                                        ends = pathAnim.init(elem, elem.d, elem.toD);
 
976
                                        fx.start = ends[0];
 
977
                                        fx.end = ends[1];
 
978
                                        fx.started = true;
 
979
                                }
 
980
                
 
981
                
 
982
                                // interpolate each value of the path
 
983
                                elem.attr('d', pathAnim.step(fx.start, fx.end, fx.pos, elem.toD));
 
984
                        };
 
985
                        
 
986
                        // jQuery 1.8 style
 
987
                        if (Tween) {
 
988
                                propHooks.d = {
 
989
                                        set: dSetter
 
990
                                };
 
991
                        // pre 1.8
 
992
                        } else {
 
993
                                // animate paths
 
994
                                Step.d = dSetter;
 
995
                        }
 
996
                        
 
997
                        /**
 
998
                         * Utility for iterating over an array. Parameters are reversed compared to jQuery.
 
999
                         * @param {Array} arr
 
1000
                         * @param {Function} fn
 
1001
                         */
 
1002
                        this.each = Array.prototype.forEach ?
 
1003
                                function (arr, fn) { // modern browsers
 
1004
                                        return Array.prototype.forEach.call(arr, fn);
 
1005
                                        
 
1006
                                } : 
 
1007
                                function (arr, fn) { // legacy
 
1008
                                        var i = 0, 
 
1009
                                                len = arr.length;
 
1010
                                        for (; i < len; i++) {
 
1011
                                                if (fn.call(arr[i], arr[i], i, arr) === false) {
 
1012
                                                        return i;
 
1013
                                                }
 
1014
                                        }
 
1015
                                };
 
1016
                        
 
1017
                        /**
 
1018
                         * Register Highcharts as a plugin in the respective framework
 
1019
                         */
 
1020
                        $.fn.highcharts = function () {
 
1021
                                var constr = 'Chart', // default constructor
 
1022
                                        args = arguments,
 
1023
                                        options,
 
1024
                                        ret,
 
1025
                                        chart;
 
1026
 
 
1027
                                if (isString(args[0])) {
 
1028
                                        constr = args[0];
 
1029
                                        args = Array.prototype.slice.call(args, 1); 
 
1030
                                }
 
1031
                                options = args[0];
 
1032
 
 
1033
                                // Create the chart
 
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]);
 
1039
                                        ret = this;
 
1040
                                        /*jslint unused:true*/
 
1041
                                }
 
1042
 
 
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')];
 
1046
                                }       
 
1047
 
 
1048
                                return ret;
 
1049
                        };
 
1050
 
 
1051
                },
 
1052
 
 
1053
                
 
1054
                /**
 
1055
                 * Downloads a script and executes a callback when done.
 
1056
                 * @param {String} scriptLocation
 
1057
                 * @param {Function} callback
 
1058
                 */
 
1059
                getScript: $.getScript,
 
1060
                
 
1061
                /**
 
1062
                 * Return the index of an item in an array, or -1 if not found
 
1063
                 */
 
1064
                inArray: $.inArray,
 
1065
                
 
1066
                /**
 
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
 
1070
                 */
 
1071
                adapterRun: function (elem, method) {
 
1072
                        return $(elem)[method]();
 
1073
                },
 
1074
        
 
1075
                /**
 
1076
                 * Filter an array
 
1077
                 */
 
1078
                grep: $.grep,
 
1079
        
 
1080
                /**
 
1081
                 * Map an array
 
1082
                 * @param {Array} arr
 
1083
                 * @param {Function} fn
 
1084
                 */
 
1085
                map: function (arr, fn) {
 
1086
                        //return jQuery.map(arr, fn);
 
1087
                        var results = [],
 
1088
                                i = 0,
 
1089
                                len = arr.length;
 
1090
                        for (; i < len; i++) {
 
1091
                                results[i] = fn.call(arr[i], arr[i], i, arr);
 
1092
                        }
 
1093
                        return results;
 
1094
        
 
1095
                },
 
1096
        
 
1097
                /**
 
1098
                 * Get the position of an element relative to the top left of the page
 
1099
                 */
 
1100
                offset: function (el) {
 
1101
                        return $(el).offset();
 
1102
                },
 
1103
        
 
1104
                /**
 
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
 
1109
                 */
 
1110
                addEvent: function (el, event, fn) {
 
1111
                        $(el).bind(event, fn);
 
1112
                },
 
1113
        
 
1114
                /**
 
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
 
1119
                 */
 
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 () {};
 
1126
                        }
 
1127
        
 
1128
                        $(el).unbind(eventType, handler);
 
1129
                },
 
1130
        
 
1131
                /**
 
1132
                 * Fire an event on a custom object
 
1133
                 * @param {Object} el
 
1134
                 * @param {String} type
 
1135
                 * @param {Object} eventArguments
 
1136
                 * @param {Function} defaultFunction
 
1137
                 */
 
1138
                fireEvent: function (el, type, eventArguments, defaultFunction) {
 
1139
                        var event = $.Event(type),
 
1140
                                detachedType = 'detached' + type,
 
1141
                                defaultPrevented;
 
1142
        
 
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.
 
1146
                        //
 
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;
 
1152
                        }
 
1153
        
 
1154
                        extend(event, eventArguments);
 
1155
        
 
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.
 
1159
                        if (el[type]) {
 
1160
                                el[detachedType] = el[type];
 
1161
                                el[type] = null;
 
1162
                        }
 
1163
        
 
1164
                        // Wrap preventDefault and stopPropagation in try/catch blocks in
 
1165
                        // order to prevent JS errors when cancelling events on non-DOM
 
1166
                        // objects. #615.
 
1167
                        /*jslint unparam: true*/
 
1168
                        $.each(['preventDefault', 'stopPropagation'], function (i, fn) {
 
1169
                                var base = event[fn];
 
1170
                                event[fn] = function () {
 
1171
                                        try {
 
1172
                                                base.call(event);
 
1173
                                        } catch (e) {
 
1174
                                                if (fn === 'preventDefault') {
 
1175
                                                        defaultPrevented = true;
 
1176
                                                }
 
1177
                                        }
 
1178
                                };
 
1179
                        });
 
1180
                        /*jslint unparam: false*/
 
1181
        
 
1182
                        // trigger it
 
1183
                        $(el).trigger(event);
 
1184
        
 
1185
                        // attach the method
 
1186
                        if (el[detachedType]) {
 
1187
                                el[type] = el[detachedType];
 
1188
                                el[detachedType] = null;
 
1189
                        }
 
1190
        
 
1191
                        if (defaultFunction && !event.isDefaultPrevented() && !defaultPrevented) {
 
1192
                                defaultFunction(event);
 
1193
                        }
 
1194
                },
 
1195
                
 
1196
                /**
 
1197
                 * Extension method needed for MooTools
 
1198
                 */
 
1199
                washMouseEvent: function (e) {
 
1200
                        var ret = e.originalEvent || e;
 
1201
                        
 
1202
                        // computed by jQuery, needed by IE8
 
1203
                        if (ret.pageX === UNDEFINED) { // #1236
 
1204
                                ret.pageX = e.pageX;
 
1205
                                ret.pageY = e.pageY;
 
1206
                        }
 
1207
                        
 
1208
                        return ret;
 
1209
                },
 
1210
        
 
1211
                /**
 
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
 
1216
                 */
 
1217
                animate: function (el, params, options) {
 
1218
                        var $el = $(el);
 
1219
                        if (!el.style) {
 
1220
                                el.style = {}; // #1881
 
1221
                        }
 
1222
                        if (params.d) {
 
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
 
1225
                        }
 
1226
        
 
1227
                        $el.stop();
 
1228
                        if (params.opacity !== UNDEFINED && el.attr) {
 
1229
                                params.opacity += 'px'; // force jQuery to use same logic as width and height (#2161)
 
1230
                        }
 
1231
                        $el.animate(params, options);
 
1232
        
 
1233
                },
 
1234
                /**
 
1235
                 * Stop running animation
 
1236
                 */
 
1237
                stop: function (el) {
 
1238
                        $(el).stop();
 
1239
                }
 
1240
        });
 
1241
}(win.jQuery));
 
1242
 
 
1243
 
 
1244
// check for a custom HighchartsAdapter defined prior to this file
 
1245
var globalAdapter = win.HighchartsAdapter,
 
1246
        adapter = globalAdapter || {};
 
1247
        
 
1248
// Initialize the adapter
 
1249
if (globalAdapter) {
 
1250
        globalAdapter.init.call(globalAdapter, pathAnim);
 
1251
}
 
1252
 
 
1253
 
 
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,
 
1263
        map = adapter.map,
 
1264
        addEvent = adapter.addEvent,
 
1265
        removeEvent = adapter.removeEvent,
 
1266
        fireEvent = adapter.fireEvent,
 
1267
        washMouseEvent = adapter.washMouseEvent,
 
1268
        animate = adapter.animate,
 
1269
        stop = adapter.stop;
 
1270
 
 
1271
 
 
1272
 
 
1273
/* ****************************************************************************
 
1274
 * Handle the options                                                         *
 
1275
 *****************************************************************************/
 
1276
var
 
1277
 
 
1278
defaultLabelOptions = {
 
1279
        enabled: true,
 
1280
        // rotation: 0,
 
1281
        // align: 'center',
 
1282
        x: 0,
 
1283
        y: 15,
 
1284
        /*formatter: function () {
 
1285
                return this.value;
 
1286
        },*/
 
1287
        style: {
 
1288
                color: '#666',
 
1289
                cursor: 'default',
 
1290
                fontSize: '11px'
 
1291
        }
 
1292
};
 
1293
 
 
1294
defaultOptions = {
 
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'],
 
1299
        lang: {
 
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'],
 
1305
                decimalPoint: '.',
 
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',
 
1309
                thousandsSep: ','
 
1310
        },
 
1311
        global: {
 
1312
                useUTC: true,
 
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'
 
1316
        },
 
1317
        chart: {
 
1318
                //animation: true,
 
1319
                //alignTicks: false,
 
1320
                //reflow: true,
 
1321
                //className: null,
 
1322
                //events: { load, selection },
 
1323
                //margin: [null],
 
1324
                //marginTop: null,
 
1325
                //marginRight: null,
 
1326
                //marginBottom: null,
 
1327
                //marginLeft: null,
 
1328
                borderColor: '#4572A7',
 
1329
                //borderWidth: 0,
 
1330
                borderRadius: 5,
 
1331
                defaultSeriesType: 'line',
 
1332
                ignoreHiddenSeries: true,
 
1333
                //inverted: false,
 
1334
                //shadow: false,
 
1335
                spacing: [10, 10, 15, 10],
 
1336
                //spacingTop: 10,
 
1337
                //spacingRight: 10,
 
1338
                //spacingBottom: 15,
 
1339
                //spacingLeft: 10,
 
1340
                //style: {
 
1341
                //      fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif', // default font
 
1342
                //      fontSize: '12px'
 
1343
                //},
 
1344
                backgroundColor: '#FFFFFF',
 
1345
                //plotBackgroundColor: null,
 
1346
                plotBorderColor: '#C0C0C0',
 
1347
                //plotBorderWidth: 0,
 
1348
                //plotShadow: false,
 
1349
                //zoomType: ''
 
1350
                resetZoomButton: {
 
1351
                        theme: {
 
1352
                                zIndex: 20
 
1353
                        },
 
1354
                        position: {
 
1355
                                align: 'right',
 
1356
                                x: -10,
 
1357
                                //verticalAlign: 'top',
 
1358
                                y: 10
 
1359
                        }
 
1360
                        // relativeTo: 'plot'
 
1361
                }
 
1362
        },
 
1363
        title: {
 
1364
                text: 'Chart title',
 
1365
                align: 'center',
 
1366
                // floating: false,
 
1367
                margin: 15,
 
1368
                // x: 0,
 
1369
                // verticalAlign: 'top',
 
1370
                // y: null,
 
1371
                style: {
 
1372
                        color: '#274b6d',//#3E576F',
 
1373
                        fontSize: '16px'
 
1374
                }
 
1375
 
 
1376
        },
 
1377
        subtitle: {
 
1378
                text: '',
 
1379
                align: 'center',
 
1380
                // floating: false
 
1381
                // x: 0,
 
1382
                // verticalAlign: 'top',
 
1383
                // y: null,
 
1384
                style: {
 
1385
                        color: '#4d759e'
 
1386
                }
 
1387
        },
 
1388
 
 
1389
        plotOptions: {
 
1390
                line: { // base series options
 
1391
                        allowPointSelect: false,
 
1392
                        showCheckbox: false,
 
1393
                        animation: {
 
1394
                                duration: 1000
 
1395
                        },
 
1396
                        //connectNulls: false,
 
1397
                        //cursor: 'default',
 
1398
                        //clip: true,
 
1399
                        //dashStyle: null,
 
1400
                        //enableMouseTracking: true,
 
1401
                        events: {},
 
1402
                        //legendIndex: 0,
 
1403
                        //linecap: 'round',
 
1404
                        lineWidth: 2,
 
1405
                        //shadow: false,
 
1406
                        // stacking: null,
 
1407
                        marker: {
 
1408
                                enabled: true,
 
1409
                                //symbol: null,
 
1410
                                lineWidth: 0,
 
1411
                                radius: 4,
 
1412
                                lineColor: '#FFFFFF',
 
1413
                                //fillColor: null,
 
1414
                                states: { // states for a single point
 
1415
                                        hover: {
 
1416
                                                enabled: true
 
1417
                                                //radius: base + 2
 
1418
                                        },
 
1419
                                        select: {
 
1420
                                                fillColor: '#FFFFFF',
 
1421
                                                lineColor: '#000000',
 
1422
                                                lineWidth: 2
 
1423
                                        }
 
1424
                                }
 
1425
                        },
 
1426
                        point: {
 
1427
                                events: {}
 
1428
                        },
 
1429
                        dataLabels: merge(defaultLabelOptions, {
 
1430
                                align: 'center',
 
1431
                                enabled: false,
 
1432
                                formatter: function () {
 
1433
                                        return this.y === null ? '' : numberFormat(this.y, -1);
 
1434
                                },
 
1435
                                verticalAlign: 'bottom', // above singular point
 
1436
                                y: 0
 
1437
                                // backgroundColor: undefined,
 
1438
                                // borderColor: undefined,
 
1439
                                // borderRadius: undefined,
 
1440
                                // borderWidth: undefined,
 
1441
                                // padding: 3,
 
1442
                                // shadow: false
 
1443
                        }),
 
1444
                        cropThreshold: 300, // draw points outside the plot area when the number of points is less than this
 
1445
                        pointRange: 0,
 
1446
                        //pointStart: 0,
 
1447
                        //pointInterval: 1,
 
1448
                        //showInLegend: null, // auto: true for standalone series, false for linked series
 
1449
                        states: { // states for the entire series
 
1450
                                hover: {
 
1451
                                        //enabled: false,
 
1452
                                        //lineWidth: base + 1,
 
1453
                                        marker: {
 
1454
                                                // lineWidth: base + 1,
 
1455
                                                // radius: base + 1
 
1456
                                        }
 
1457
                                },
 
1458
                                select: {
 
1459
                                        marker: {}
 
1460
                                }
 
1461
                        },
 
1462
                        stickyTracking: true,
 
1463
                        //tooltip: {
 
1464
                                //pointFormat: '<span style="color:{series.color}">{series.name}</span>: <b>{point.y}</b>'
 
1465
                                //valueDecimals: null,
 
1466
                                //xDateFormat: '%A, %b %e, %Y',
 
1467
                                //valuePrefix: '',
 
1468
                                //ySuffix: ''                           
 
1469
                        //}
 
1470
                        turboThreshold: 1000
 
1471
                        // zIndex: null
 
1472
                }
 
1473
        },
 
1474
        labels: {
 
1475
                //items: [],
 
1476
                style: {
 
1477
                        //font: defaultFont,
 
1478
                        position: ABSOLUTE,
 
1479
                        color: '#3E576F'
 
1480
                }
 
1481
        },
 
1482
        legend: {
 
1483
                enabled: true,
 
1484
                align: 'center',
 
1485
                //floating: false,
 
1486
                layout: 'horizontal',
 
1487
                labelFormatter: function () {
 
1488
                        return this.name;
 
1489
                },
 
1490
                borderWidth: 1,
 
1491
                borderColor: '#909090',
 
1492
                borderRadius: 5,
 
1493
                navigation: {
 
1494
                        // animation: true,
 
1495
                        activeColor: '#274b6d',
 
1496
                        // arrowSize: 12
 
1497
                        inactiveColor: '#CCC'
 
1498
                        // style: {} // text styles
 
1499
                },
 
1500
                // margin: 10,
 
1501
                // reversed: false,
 
1502
                shadow: false,
 
1503
                // backgroundColor: null,
 
1504
                /*style: {
 
1505
                        padding: '5px'
 
1506
                },*/
 
1507
                itemStyle: {                    
 
1508
                        color: '#274b6d',
 
1509
                        fontSize: '12px'
 
1510
                },
 
1511
                itemHoverStyle: {
 
1512
                        //cursor: 'pointer', removed as of #601
 
1513
                        color: '#000'
 
1514
                },
 
1515
                itemHiddenStyle: {
 
1516
                        color: '#CCC'
 
1517
                },
 
1518
                itemCheckboxStyle: {
 
1519
                        position: ABSOLUTE,
 
1520
                        width: '13px', // for IE precision
 
1521
                        height: '13px'
 
1522
                },
 
1523
                // itemWidth: undefined,
 
1524
                // symbolWidth: 16,
 
1525
                symbolPadding: 5,
 
1526
                verticalAlign: 'bottom',
 
1527
                // width: undefined,
 
1528
                x: 0,
 
1529
                y: 0,
 
1530
                title: {
 
1531
                        //text: null,
 
1532
                        style: {
 
1533
                                fontWeight: 'bold'
 
1534
                        }
 
1535
                }                       
 
1536
        },
 
1537
 
 
1538
        loading: {
 
1539
                // hideDuration: 100,
 
1540
                labelStyle: {
 
1541
                        fontWeight: 'bold',
 
1542
                        position: RELATIVE,
 
1543
                        top: '1em'
 
1544
                },
 
1545
                // showDuration: 0,
 
1546
                style: {
 
1547
                        position: ABSOLUTE,
 
1548
                        backgroundColor: 'white',
 
1549
                        opacity: 0.5,
 
1550
                        textAlign: 'center'
 
1551
                }
 
1552
        },
 
1553
 
 
1554
        tooltip: {
 
1555
                enabled: true,
 
1556
                animation: hasSVG,
 
1557
                //crosshairs: null,
 
1558
                backgroundColor: 'rgba(255, 255, 255, .85)',
 
1559
                borderWidth: 1,
 
1560
                borderRadius: 3,
 
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',
 
1568
                        month: '%B %Y',
 
1569
                        year: '%Y'
 
1570
                },
 
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/>',
 
1574
                shadow: true,
 
1575
                //shared: false,
 
1576
                snap: isTouchDevice ? 25 : 10,
 
1577
                style: {
 
1578
                        color: '#333333',
 
1579
                        cursor: 'default',
 
1580
                        fontSize: '12px',
 
1581
                        padding: '8px',
 
1582
                        whiteSpace: 'nowrap'
 
1583
                }
 
1584
                //xDateFormat: '%A, %b %e, %Y',
 
1585
                //valueDecimals: null,
 
1586
                //valuePrefix: '',
 
1587
                //valueSuffix: ''
 
1588
        },
 
1589
 
 
1590
        credits: {
 
1591
                enabled: true,
 
1592
                text: 'Highcharts.com',
 
1593
                href: 'http://www.highcharts.com',
 
1594
                position: {
 
1595
                        align: 'right',
 
1596
                        x: -10,
 
1597
                        verticalAlign: 'bottom',
 
1598
                        y: -5
 
1599
                },
 
1600
                style: {
 
1601
                        cursor: 'pointer',
 
1602
                        color: '#909090',
 
1603
                        fontSize: '9px'
 
1604
                }
 
1605
        }
 
1606
};
 
1607
 
 
1608
 
 
1609
 
 
1610
 
 
1611
// Series defaults
 
1612
var defaultPlotOptions = defaultOptions.plotOptions,
 
1613
        defaultSeriesOptions = defaultPlotOptions.line;
 
1614
 
 
1615
// set the default time methods
 
1616
setTimeMethods();
 
1617
 
 
1618
 
 
1619
 
 
1620
/**
 
1621
 * Set the time methods globally based on the useUTC option. Time method can be either
 
1622
 * local time or UTC (default).
 
1623
 */
 
1624
function setTimeMethods() {
 
1625
        var useUTC = defaultOptions.global.useUTC,
 
1626
                GET = useUTC ? 'getUTC' : 'get',
 
1627
                SET = useUTC ? 'setUTC' : 'set';
 
1628
 
 
1629
 
 
1630
        timezoneOffset = ((useUTC && defaultOptions.global.timezoneOffset) || 0) * 60000;
 
1631
        makeTime = useUTC ? Date.UTC : function (year, month, date, hours, minutes, seconds) {
 
1632
                return new Date(
 
1633
                        year,
 
1634
                        month,
 
1635
                        pick(date, 1),
 
1636
                        pick(hours, 0),
 
1637
                        pick(minutes, 0),
 
1638
                        pick(seconds, 0)
 
1639
                ).getTime();
 
1640
        };
 
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';
 
1652
 
 
1653
}
 
1654
 
 
1655
/**
 
1656
 * Merge the default options with custom options and return the new options structure
 
1657
 * @param {Object} options The new custom options
 
1658
 */
 
1659
function setOptions(options) {
 
1660
        
 
1661
        // Copy in the default options
 
1662
        defaultOptions = merge(true, defaultOptions, options);
 
1663
        
 
1664
        // Apply UTC
 
1665
        setTimeMethods();
 
1666
 
 
1667
        return defaultOptions;
 
1668
}
 
1669
 
 
1670
/**
 
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.
 
1673
 */
 
1674
function getOptions() {
 
1675
        return defaultOptions;
 
1676
}
 
1677
 
 
1678
 
 
1679
/**
 
1680
 * Handle color operations. The object methods are chainable.
 
1681
 * @param {String} input The input color in either rbga or hex format
 
1682
 */
 
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*\)/;
 
1686
 
 
1687
var Color = function (input) {
 
1688
        // declare variables
 
1689
        var rgba = [], result, stops;
 
1690
 
 
1691
        /**
 
1692
         * Parse the input color to rgba array
 
1693
         * @param {String} input
 
1694
         */
 
1695
        function init(input) {
 
1696
 
 
1697
                // Gradients
 
1698
                if (input && input.stops) {
 
1699
                        stops = map(input.stops, function (stop) {
 
1700
                                return Color(stop[1]);
 
1701
                        });
 
1702
 
 
1703
                // Solid colors
 
1704
                } else {
 
1705
                        // rgba
 
1706
                        result = rgbaRegEx.exec(input);
 
1707
                        if (result) {
 
1708
                                rgba = [pInt(result[1]), pInt(result[2]), pInt(result[3]), parseFloat(result[4], 10)];
 
1709
                        } else { 
 
1710
                                // hex
 
1711
                                result = hexRegEx.exec(input);
 
1712
                                if (result) {
 
1713
                                        rgba = [pInt(result[1], 16), pInt(result[2], 16), pInt(result[3], 16), 1];
 
1714
                                } else {
 
1715
                                        // rgb
 
1716
                                        result = rgbRegEx.exec(input);
 
1717
                                        if (result) {
 
1718
                                                rgba = [pInt(result[1]), pInt(result[2]), pInt(result[3]), 1];
 
1719
                                        }
 
1720
                                }
 
1721
                        }
 
1722
                }               
 
1723
 
 
1724
        }
 
1725
        /**
 
1726
         * Return the color a specified format
 
1727
         * @param {String} format
 
1728
         */
 
1729
        function get(format) {
 
1730
                var ret;
 
1731
 
 
1732
                if (stops) {
 
1733
                        ret = merge(input);
 
1734
                        ret.stops = [].concat(ret.stops);
 
1735
                        each(stops, function (stop, i) {
 
1736
                                ret.stops[i] = [ret.stops[i][0], stop.get(format)];
 
1737
                        });
 
1738
 
 
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') {
 
1744
                                ret = rgba[3];
 
1745
                        } else {
 
1746
                                ret = 'rgba(' + rgba.join(',') + ')';
 
1747
                        }
 
1748
                } else {
 
1749
                        ret = input;
 
1750
                }
 
1751
                return ret;
 
1752
        }
 
1753
 
 
1754
        /**
 
1755
         * Brighten the color
 
1756
         * @param {Number} alpha
 
1757
         */
 
1758
        function brighten(alpha) {
 
1759
                if (stops) {
 
1760
                        each(stops, function (stop) {
 
1761
                                stop.brighten(alpha);
 
1762
                        });
 
1763
                
 
1764
                } else if (isNumber(alpha) && alpha !== 0) {
 
1765
                        var i;
 
1766
                        for (i = 0; i < 3; i++) {
 
1767
                                rgba[i] += pInt(alpha * 255);
 
1768
 
 
1769
                                if (rgba[i] < 0) {
 
1770
                                        rgba[i] = 0;
 
1771
                                }
 
1772
                                if (rgba[i] > 255) {
 
1773
                                        rgba[i] = 255;
 
1774
                                }
 
1775
                        }
 
1776
                }
 
1777
                return this;
 
1778
        }
 
1779
        /**
 
1780
         * Set the color's opacity to a given alpha value
 
1781
         * @param {Number} alpha
 
1782
         */
 
1783
        function setOpacity(alpha) {
 
1784
                rgba[3] = alpha;
 
1785
                return this;
 
1786
        }
 
1787
 
 
1788
        // initialize: parse the input
 
1789
        init(input);
 
1790
 
 
1791
        // public methods
 
1792
        return {
 
1793
                get: get,
 
1794
                brighten: brighten,
 
1795
                rgba: rgba,
 
1796
                setOpacity: setOpacity
 
1797
        };
 
1798
};
 
1799
 
 
1800
 
 
1801
/**
 
1802
 * A wrapper object for SVG elements
 
1803
 */
 
1804
function SVGElement() {}
 
1805
 
 
1806
SVGElement.prototype = {
 
1807
        /**
 
1808
         * Initialize the SVG renderer
 
1809
         * @param {Object} renderer
 
1810
         * @param {String} nodeName
 
1811
         */
 
1812
        init: function (renderer, nodeName) {
 
1813
                var wrapper = this;
 
1814
                wrapper.element = nodeName === 'span' ?
 
1815
                        createElement(nodeName) :
 
1816
                        doc.createElementNS(SVG_NS, nodeName);
 
1817
                wrapper.renderer = renderer;
 
1818
                /**
 
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
 
1822
                 * Renderer.label.
 
1823
                 */
 
1824
                wrapper.attrSetters = {};
 
1825
        },
 
1826
        /**
 
1827
         * Default base for animation
 
1828
         */
 
1829
        opacity: 1,
 
1830
        /**
 
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
 
1835
         */
 
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)
 
1839
                if (animOptions) {
 
1840
                        animOptions = merge(animOptions, {}); //#2625
 
1841
                        if (complete) { // allows using a callback with the global animation without overwriting it
 
1842
                                animOptions.complete = complete;
 
1843
                        }
 
1844
                        animate(this, params, animOptions);
 
1845
                } else {
 
1846
                        this.attr(params);
 
1847
                        if (complete) {
 
1848
                                complete();
 
1849
                        }
 
1850
                }
 
1851
        },
 
1852
        /**
 
1853
         * Set or get a given attribute
 
1854
         * @param {Object|String} hash
 
1855
         * @param {Mixed|Undefined} val
 
1856
         */
 
1857
        attr: function (hash, val) {
 
1858
                var wrapper = this,
 
1859
                        key,
 
1860
                        value,
 
1861
                        result,
 
1862
                        i,
 
1863
                        child,
 
1864
                        element = wrapper.element,
 
1865
                        nodeName = element.nodeName.toLowerCase(), // Android2 requires lower for "text"
 
1866
                        renderer = wrapper.renderer,
 
1867
                        skipAttr,
 
1868
                        titleNode,
 
1869
                        attrSetters = wrapper.attrSetters,
 
1870
                        shadows = wrapper.shadows,
 
1871
                        hasSetSymbolSize,
 
1872
                        doTransform,
 
1873
                        ret = wrapper;
 
1874
 
 
1875
                // single key-value pair
 
1876
                if (isString(hash) && defined(val)) {
 
1877
                        key = hash;
 
1878
                        hash = {};
 
1879
                        hash[key] = val;
 
1880
                }
 
1881
 
 
1882
                // used as a getter: first argument is a string, second is undefined
 
1883
                if (isString(hash)) {
 
1884
                        key = hash;
 
1885
                        if (nodeName === 'circle') {
 
1886
                                key = { x: 'cx', y: 'cy' }[key] || key;
 
1887
                        } else if (key === 'strokeWidth') {
 
1888
                                key = 'stroke-width';
 
1889
                        }
 
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);
 
1893
                        }
 
1894
 
 
1895
                // setter
 
1896
                } else {
 
1897
 
 
1898
                        for (key in hash) {
 
1899
                                skipAttr = false; // reset
 
1900
                                value = hash[key];
 
1901
 
 
1902
                                // check for a specific attribute setter
 
1903
                                result = attrSetters[key] && attrSetters[key].call(wrapper, value, key);
 
1904
 
 
1905
                                if (result !== false) {
 
1906
                                        if (result !== UNDEFINED) {
 
1907
                                                value = result; // the attribute setter has returned a new value to set
 
1908
                                        }
 
1909
 
 
1910
 
 
1911
                                        // paths
 
1912
                                        if (key === 'd') {
 
1913
                                                if (value && value.join) { // join path
 
1914
                                                        value = value.join(' ');
 
1915
                                                }
 
1916
                                                if (/(NaN| {2}|^$)/.test(value)) {
 
1917
                                                        value = 'M 0 0';
 
1918
                                                }
 
1919
                                                //wrapper.d = value; // shortcut for animations
 
1920
 
 
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);
 
1929
                                                        }
 
1930
                                                }
 
1931
 
 
1932
                                        } else if (wrapper.rotation && (key === 'x' || key === 'y')) {
 
1933
                                                doTransform = true;
 
1934
 
 
1935
                                        // apply gradients
 
1936
                                        } else if (key === 'fill') {
 
1937
                                                value = renderer.color(value, element, key);
 
1938
 
 
1939
                                        // circle x and y
 
1940
                                        } else if (nodeName === 'circle' && (key === 'x' || key === 'y')) {
 
1941
                                                key = { x: 'cx', y: 'cy' }[key] || key;
 
1942
 
 
1943
                                        // rectangle border radius
 
1944
                                        } else if (nodeName === 'rect' && key === 'r') {
 
1945
                                                attr(element, {
 
1946
                                                        rx: value,
 
1947
                                                        ry: value
 
1948
                                                });
 
1949
                                                skipAttr = true;
 
1950
 
 
1951
                                        // translation and text rotation
 
1952
                                        } else if (key === 'translateX' || key === 'translateY' || key === 'rotation' ||
 
1953
                                                        key === 'verticalAlign' || key === 'scaleX' || key === 'scaleY') {
 
1954
                                                doTransform = true;
 
1955
                                                skipAttr = true;
 
1956
 
 
1957
                                        // apply opacity as subnode (required by legacy WebKit and Batik)
 
1958
                                        } else if (key === 'stroke') {
 
1959
                                                value = renderer.color(value, element, key);
 
1960
 
 
1961
                                        // emulate VML's dashstyle implementation
 
1962
                                        } else if (key === 'dashstyle') {
 
1963
                                                key = 'stroke-dasharray';
 
1964
                                                value = value && value.toLowerCase();
 
1965
                                                if (value === 'solid') {
 
1966
                                                        value = NONE;
 
1967
                                                } else if (value) {
 
1968
                                                        value = value
 
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,')
 
1976
                                                                .replace(/,$/, '')
 
1977
                                                                .split(','); // ending comma
 
1978
 
 
1979
                                                        i = value.length;
 
1980
                                                        while (i--) {
 
1981
                                                                value[i] = pInt(value[i]) * pick(hash['stroke-width'], wrapper['stroke-width']);
 
1982
                                                        }
 
1983
                                                        value = value.join(',');
 
1984
                                                }
 
1985
 
 
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);
 
1990
 
 
1991
                                        // Text alignment
 
1992
                                        } else if (key === 'align') {
 
1993
                                                key = 'text-anchor';
 
1994
                                                value = { left: 'start', center: 'middle', right: 'end' }[value];
 
1995
 
 
1996
                                        // Title requires a subnode, #431
 
1997
                                        } else if (key === 'title') {
 
1998
                                                titleNode = element.getElementsByTagName('title')[0];
 
1999
                                                if (!titleNode) {
 
2000
                                                        titleNode = doc.createElementNS(SVG_NS, 'title');
 
2001
                                                        element.appendChild(titleNode);
 
2002
                                                }
 
2003
                                                titleNode.textContent = value;
 
2004
                                        }
 
2005
 
 
2006
                                        // jQuery animate changes case
 
2007
                                        if (key === 'strokeWidth') {
 
2008
                                                key = 'stroke-width';
 
2009
                                        }
 
2010
 
 
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;
 
2023
                                                }
 
2024
                                                skipAttr = true;
 
2025
                                        }
 
2026
 
 
2027
                                        // symbols
 
2028
                                        if (wrapper.symbolName && /^(x|y|width|height|r|start|end|innerR|anchorX|anchorY)/.test(key)) {
 
2029
 
 
2030
 
 
2031
                                                if (!hasSetSymbolSize) {
 
2032
                                                        wrapper.symbolAttr(hash);
 
2033
                                                        hasSetSymbolSize = true;
 
2034
                                                }
 
2035
                                                skipAttr = true;
 
2036
                                        }
 
2037
 
 
2038
                                        // let the shadow follow the main element
 
2039
                                        if (shadows && /^(width|height|visibility|x|y|d|transform|cx|cy|r)$/.test(key)) {
 
2040
                                                i = shadows.length;
 
2041
                                                while (i--) {
 
2042
                                                        attr(
 
2043
                                                                shadows[i],
 
2044
                                                                key,
 
2045
                                                                key === 'height' ?
 
2046
                                                                        mathMax(value - (shadows[i].cutHeight || 0), 0) :
 
2047
                                                                        value
 
2048
                                                        );
 
2049
                                                }
 
2050
                                        }
 
2051
 
 
2052
                                        // validate heights
 
2053
                                        if ((key === 'width' || key === 'height') && nodeName === 'rect' && value < 0) {
 
2054
                                                value = 0;
 
2055
                                        }
 
2056
 
 
2057
                                        // Record for animation and quick access without polling the DOM
 
2058
                                        wrapper[key] = value;
 
2059
 
 
2060
 
 
2061
                                        if (key === 'text') {
 
2062
                                                if (value !== wrapper.textStr) {
 
2063
                                                        
 
2064
                                                        // Delete bBox memo when the text changes
 
2065
                                                        delete wrapper.bBox;
 
2066
                                                
 
2067
                                                        wrapper.textStr = value;
 
2068
                                                        if (wrapper.added) {
 
2069
                                                                renderer.buildText(wrapper);
 
2070
                                                        }
 
2071
                                                }
 
2072
                                        } else if (!skipAttr) {
 
2073
                                                //attr(element, key, value);
 
2074
                                                if (value !== undefined) {
 
2075
                                                        element.setAttribute(key, value);
 
2076
                                                }
 
2077
                                        }
 
2078
 
 
2079
                                }
 
2080
 
 
2081
                        }
 
2082
 
 
2083
                        // Update transform. Do this outside the loop to prevent redundant updating for batch setting
 
2084
                        // of attributes.
 
2085
                        if (doTransform) {
 
2086
                                wrapper.updateTransform();
 
2087
                        }
 
2088
 
 
2089
                }
 
2090
 
 
2091
                return ret;
 
2092
        },
 
2093
 
 
2094
 
 
2095
        /**
 
2096
         * Add a class name to an element
 
2097
         */
 
2098
        addClass: function (className) {
 
2099
                var element = this.element,
 
2100
                        currentClassName = attr(element, 'class') || '';
 
2101
 
 
2102
                if (currentClassName.indexOf(className) === -1) {
 
2103
                        attr(element, 'class', currentClassName + ' ' + className);
 
2104
                }
 
2105
                return this;
 
2106
        },
 
2107
        /* hasClass and removeClass are not (yet) needed
 
2108
        hasClass: function (className) {
 
2109
                return attr(this.element, 'class').indexOf(className) !== -1;
 
2110
        },
 
2111
        removeClass: function (className) {
 
2112
                attr(this.element, 'class', attr(this.element, 'class').replace(className, ''));
 
2113
                return this;
 
2114
        },
 
2115
        */
 
2116
 
 
2117
        /**
 
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
 
2120
         * .attr() method
 
2121
         * @param {Object} hash
 
2122
         */
 
2123
        symbolAttr: function (hash) {
 
2124
                var wrapper = this;
 
2125
 
 
2126
                each(['x', 'y', 'r', 'start', 'end', 'width', 'height', 'innerR', 'anchorX', 'anchorY'], function (key) {
 
2127
                        wrapper[key] = pick(hash[key], wrapper[key]);
 
2128
                });
 
2129
 
 
2130
                wrapper.attr({
 
2131
                        d: wrapper.renderer.symbols[wrapper.symbolName](
 
2132
                                wrapper.x,
 
2133
                                wrapper.y,
 
2134
                                wrapper.width,
 
2135
                                wrapper.height,
 
2136
                                wrapper
 
2137
                        )
 
2138
                });
 
2139
        },
 
2140
 
 
2141
        /**
 
2142
         * Apply a clipping path to this object
 
2143
         * @param {String} id
 
2144
         */
 
2145
        clip: function (clipRect) {
 
2146
                return this.attr('clip-path', clipRect ? 'url(' + this.renderer.url + '#' + clipRect.id + ')' : NONE);
 
2147
        },
 
2148
 
 
2149
        /**
 
2150
         * Calculate the coordinates needed for drawing a rectangle crisply and return the
 
2151
         * calculated attributes
 
2152
         * @param {Number} strokeWidth
 
2153
         * @param {Number} x
 
2154
         * @param {Number} y
 
2155
         * @param {Number} width
 
2156
         * @param {Number} height
 
2157
         */
 
2158
        crisp: function (rect) {
 
2159
 
 
2160
                var wrapper = this,
 
2161
                        key,
 
2162
                        attribs = {},
 
2163
                        normalizer,
 
2164
                        strokeWidth = rect.strokeWidth || wrapper.strokeWidth || (wrapper.attr && wrapper.attr('stroke-width')) || 0;
 
2165
 
 
2166
                normalizer = mathRound(strokeWidth) % 2 / 2; // mathRound because strokeWidth can sometimes have roundoff errors
 
2167
 
 
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;
 
2174
 
 
2175
                for (key in rect) {
 
2176
                        if (wrapper[key] !== rect[key]) { // only set attribute if changed
 
2177
                                wrapper[key] = attribs[key] = rect[key];
 
2178
                        }
 
2179
                }
 
2180
 
 
2181
                return attribs;
 
2182
        },
 
2183
 
 
2184
        /**
 
2185
         * Set styles for the element
 
2186
         * @param {Object} styles
 
2187
         */
 
2188
        css: function (styles) {
 
2189
                var elemWrapper = this,
 
2190
                        oldStyles = elemWrapper.styles,
 
2191
                        newStyles = {},
 
2192
                        elem = elemWrapper.element,
 
2193
                        textWidth,
 
2194
                        n,
 
2195
                        serializedCss = '',
 
2196
                        hyphenate,
 
2197
                        hasNew = !oldStyles;
 
2198
 
 
2199
                // convert legacy
 
2200
                if (styles && styles.color) {
 
2201
                        styles.fill = styles.color;
 
2202
                }
 
2203
 
 
2204
                // Filter out existing styles to increase performance (#2640)
 
2205
                if (oldStyles) {
 
2206
                        for (n in styles) {
 
2207
                                if (styles[n] !== oldStyles[n]) {
 
2208
                                        newStyles[n] = styles[n];
 
2209
                                        hasNew = true;
 
2210
                                }
 
2211
                        }
 
2212
                }
 
2213
                if (hasNew) {
 
2214
                        textWidth = elemWrapper.textWidth = styles && styles.width && elem.nodeName.toLowerCase() === 'text' && pInt(styles.width);
 
2215
 
 
2216
                        // Merge the new styles with the old ones
 
2217
                        if (oldStyles) {
 
2218
                                styles = extend(
 
2219
                                        oldStyles,
 
2220
                                        newStyles
 
2221
                                );
 
2222
                        }               
 
2223
 
 
2224
                        // store object
 
2225
                        elemWrapper.styles = styles;
 
2226
 
 
2227
                        if (textWidth && (useCanVG || (!hasSVG && elemWrapper.renderer.forExport))) {
 
2228
                                delete styles.width;
 
2229
                        }
 
2230
 
 
2231
                        // serialize and set style attribute
 
2232
                        if (isIE && !hasSVG) {
 
2233
                                css(elemWrapper.element, styles);
 
2234
                        } else {
 
2235
                                /*jslint unparam: true*/
 
2236
                                hyphenate = function (a, b) { return '-' + b.toLowerCase(); };
 
2237
                                /*jslint unparam: false*/
 
2238
                                for (n in styles) {
 
2239
                                        serializedCss += n.replace(/([A-Z])/g, hyphenate) + ':' + styles[n] + ';';
 
2240
                                }
 
2241
                                attr(elem, 'style', serializedCss); // #1881
 
2242
                        }
 
2243
 
 
2244
 
 
2245
                        // re-build text
 
2246
                        if (textWidth && elemWrapper.added) {
 
2247
                                elemWrapper.renderer.buildText(elemWrapper);
 
2248
                        }
 
2249
                }
 
2250
 
 
2251
                return elemWrapper;
 
2252
        },
 
2253
 
 
2254
        /**
 
2255
         * Add an event listener
 
2256
         * @param {String} eventType
 
2257
         * @param {Function} handler
 
2258
         */
 
2259
        on: function (eventType, handler) {
 
2260
                var svgElement = this,
 
2261
                        element = svgElement.element;
 
2262
                
 
2263
                // touch
 
2264
                if (hasTouch && eventType === 'click') {
 
2265
                        element.ontouchstart = function (e) {                   
 
2266
                                svgElement.touchEventFired = Date.now();                                
 
2267
                                e.preventDefault();
 
2268
                                handler.call(element, e);
 
2269
                        };
 
2270
                        element.onclick = function (e) {                                                                                                
 
2271
                                if (userAgent.indexOf('Android') === -1 || Date.now() - (svgElement.touchEventFired || 0) > 1100) { // #2269
 
2272
                                        handler.call(element, e);
 
2273
                                }
 
2274
                        };                      
 
2275
                } else {
 
2276
                        // simplest possible event model for internal use
 
2277
                        element['on' + eventType] = handler;
 
2278
                }
 
2279
                return this;
 
2280
        },
 
2281
 
 
2282
        /**
 
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.
 
2286
         */
 
2287
        setRadialReference: function (coordinates) {
 
2288
                this.element.radialReference = coordinates;
 
2289
                return this;
 
2290
        },
 
2291
 
 
2292
        /**
 
2293
         * Move an object and its children by x and y values
 
2294
         * @param {Number} x
 
2295
         * @param {Number} y
 
2296
         */
 
2297
        translate: function (x, y) {
 
2298
                return this.attr({
 
2299
                        translateX: x,
 
2300
                        translateY: y
 
2301
                });
 
2302
        },
 
2303
 
 
2304
        /**
 
2305
         * Invert a group, rotate and flip
 
2306
         */
 
2307
        invert: function () {
 
2308
                var wrapper = this;
 
2309
                wrapper.inverted = true;
 
2310
                wrapper.updateTransform();
 
2311
                return wrapper;
 
2312
        },
 
2313
 
 
2314
        /**
 
2315
         * Private method to update the transform attribute based on internal
 
2316
         * properties
 
2317
         */
 
2318
        updateTransform: function () {
 
2319
                var wrapper = this,
 
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,
 
2326
                        transform;
 
2327
 
 
2328
                // flipping affects translate as adjustment for flipping around the group's axis
 
2329
                if (inverted) {
 
2330
                        translateX += wrapper.attr('width');
 
2331
                        translateY += wrapper.attr('height');
 
2332
                }
 
2333
 
 
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 + ')'];
 
2337
 
 
2338
                // apply rotation
 
2339
                if (inverted) {
 
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) + ')');
 
2343
                }
 
2344
 
 
2345
                // apply scale
 
2346
                if (defined(scaleX) || defined(scaleY)) {
 
2347
                        transform.push('scale(' + pick(scaleX, 1) + ' ' + pick(scaleY, 1) + ')');
 
2348
                }
 
2349
 
 
2350
                if (transform.length) {
 
2351
                        attr(wrapper.element, 'transform', transform.join(' '));
 
2352
                }
 
2353
        },
 
2354
        /**
 
2355
         * Bring the element to the front
 
2356
         */
 
2357
        toFront: function () {
 
2358
                var element = this.element;
 
2359
                element.parentNode.appendChild(element);
 
2360
                return this;
 
2361
        },
 
2362
 
 
2363
 
 
2364
        /**
 
2365
         * Break down alignment options like align, verticalAlign, x and y
 
2366
         * to x and y relative to the chart.
 
2367
         *
 
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.
 
2374
         *
 
2375
         */
 
2376
        align: function (alignOptions, alignByTranslate, box) {
 
2377
                var align,
 
2378
                        vAlign,
 
2379
                        x,
 
2380
                        y,
 
2381
                        attribs = {},
 
2382
                        alignTo,
 
2383
                        renderer = this.renderer,
 
2384
                        alignedObjects = renderer.alignedObjects;
 
2385
 
 
2386
                // First call on instanciate
 
2387
                if (alignOptions) {
 
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
 
2395
                        }
 
2396
 
 
2397
                // When called on resize, no arguments are supplied
 
2398
                } else {
 
2399
                        alignOptions = this.alignOptions;
 
2400
                        alignByTranslate = this.alignByTranslate;
 
2401
                        alignTo = this.alignTo;
 
2402
                }
 
2403
 
 
2404
                box = pick(box, renderer[alignTo], renderer);
 
2405
 
 
2406
                // Assign variables
 
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
 
2411
 
 
2412
                // Align
 
2413
                if (align === 'right' || align === 'center') {
 
2414
                        x += (box.width - (alignOptions.width || 0)) /
 
2415
                                        { right: 1, center: 2 }[align];
 
2416
                }
 
2417
                attribs[alignByTranslate ? 'translateX' : 'x'] = mathRound(x);
 
2418
 
 
2419
 
 
2420
                // Vertical align
 
2421
                if (vAlign === 'bottom' || vAlign === 'middle') {
 
2422
                        y += (box.height - (alignOptions.height || 0)) /
 
2423
                                        ({ bottom: 1, middle: 2 }[vAlign] || 1);
 
2424
 
 
2425
                }
 
2426
                attribs[alignByTranslate ? 'translateY' : 'y'] = mathRound(y);
 
2427
 
 
2428
                // Animate only if already placed
 
2429
                this[this.placed ? 'animate' : 'attr'](attribs);
 
2430
                this.placed = true;
 
2431
                this.alignAttr = attribs;
 
2432
 
 
2433
                return this;
 
2434
        },
 
2435
 
 
2436
        /**
 
2437
         * Get the bounding box (width, height, x and y) for the element
 
2438
         */
 
2439
        getBBox: function () {
 
2440
                var wrapper = this,
 
2441
                        bBox = wrapper.bBox,
 
2442
                        renderer = wrapper.renderer,
 
2443
                        width,
 
2444
                        height,
 
2445
                        rotation = wrapper.rotation,
 
2446
                        element = wrapper.element,
 
2447
                        styles = wrapper.styles,
 
2448
                        rad = rotation * deg2rad,
 
2449
                        textStr = wrapper.textStr,
 
2450
                        numKey;
 
2451
 
 
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];
 
2458
                }
 
2459
 
 
2460
                // No cache found
 
2461
                if (!bBox) {
 
2462
 
 
2463
                        // SVG elements
 
2464
                        if (element.namespaceURI === SVG_NS || renderer.forExport) {
 
2465
                                try { // Fails in Firefox if the container has display: none.
 
2466
 
 
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
 
2472
                                                {
 
2473
                                                        width: element.offsetWidth,
 
2474
                                                        height: element.offsetHeight
 
2475
                                                };
 
2476
                                } catch (e) {}
 
2477
 
 
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 };
 
2482
                                }
 
2483
 
 
2484
 
 
2485
                        // VML Renderer or useHTML within SVG
 
2486
                        } else {
 
2487
 
 
2488
                                bBox = wrapper.htmlGetBBox();
 
2489
 
 
2490
                        }
 
2491
 
 
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) {
 
2495
                                width = bBox.width;
 
2496
                                height = bBox.height;
 
2497
 
 
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;
 
2501
                                }
 
2502
 
 
2503
                                // Adjust for rotated text
 
2504
                                if (rotation) {
 
2505
                                        bBox.width = mathAbs(height * mathSin(rad)) + mathAbs(width * mathCos(rad));
 
2506
                                        bBox.height = mathAbs(height * mathCos(rad)) + mathAbs(width * mathSin(rad));
 
2507
                                }
 
2508
                        }
 
2509
 
 
2510
                        // Cache it
 
2511
                        wrapper.bBox = bBox;
 
2512
                        if (numKey) {
 
2513
                                renderer.cache[numKey] = bBox;
 
2514
                        }
 
2515
                }
 
2516
                return bBox;
 
2517
        },
 
2518
 
 
2519
        /**
 
2520
         * Show the element
 
2521
         */
 
2522
        show: function (inherit) {
 
2523
                return this.attr({ visibility: inherit ? 'inherit' : VISIBLE });
 
2524
        },
 
2525
 
 
2526
        /**
 
2527
         * Hide the element
 
2528
         */
 
2529
        hide: function () {
 
2530
                return this.attr({ visibility: HIDDEN });
 
2531
        },
 
2532
 
 
2533
        fadeOut: function (duration) {
 
2534
                var elemWrapper = this;
 
2535
                elemWrapper.animate({
 
2536
                        opacity: 0
 
2537
                }, {
 
2538
                        duration: duration || 150,
 
2539
                        complete: function () {
 
2540
                                elemWrapper.hide();
 
2541
                        }
 
2542
                });
 
2543
        },
 
2544
 
 
2545
        /**
 
2546
         * Add the element
 
2547
         * @param {Object|Undefined} parent Can be an element, an element wrapper or undefined
 
2548
         *    to append the element to the renderer.box.
 
2549
         */
 
2550
        add: function (parent) {
 
2551
 
 
2552
                var renderer = this.renderer,
 
2553
                        parentWrapper = parent || renderer,
 
2554
                        parentNode = parentWrapper.element || renderer.box,
 
2555
                        childNodes,
 
2556
                        element = this.element,
 
2557
                        zIndex = this.zIndex,
 
2558
                        otherElement,
 
2559
                        otherZIndex,
 
2560
                        i,
 
2561
                        inserted;
 
2562
 
 
2563
                if (parent) {
 
2564
                        this.parentGroup = parent;
 
2565
                }
 
2566
 
 
2567
                // mark as inverted
 
2568
                this.parentInverted = parent && parent.inverted;
 
2569
 
 
2570
                // build formatted text
 
2571
                if (this.textStr !== undefined) {
 
2572
                        renderer.buildText(this);
 
2573
                }
 
2574
 
 
2575
                // mark the container as having z indexed children
 
2576
                if (zIndex) {
 
2577
                        parentWrapper.handleZ = true;
 
2578
                        zIndex = pInt(zIndex);
 
2579
                }
 
2580
 
 
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))
 
2592
 
 
2593
                                                )) {
 
2594
                                        parentNode.insertBefore(element, otherElement);
 
2595
                                        inserted = true;
 
2596
                                        break;
 
2597
                                }
 
2598
                        }
 
2599
                }
 
2600
 
 
2601
                // default: append at the end
 
2602
                if (!inserted) {
 
2603
                        parentNode.appendChild(element);
 
2604
                }
 
2605
 
 
2606
                // mark as added
 
2607
                this.added = true;
 
2608
 
 
2609
                // fire an event for internal hooks
 
2610
                if (this.onAdd) {
 
2611
                        this.onAdd();
 
2612
                }
 
2613
 
 
2614
                return this;
 
2615
        },
 
2616
 
 
2617
        /**
 
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.
 
2620
         */
 
2621
        safeRemoveChild: function (element) {
 
2622
                var parentNode = element.parentNode;
 
2623
                if (parentNode) {
 
2624
                        parentNode.removeChild(element);
 
2625
                }
 
2626
        },
 
2627
 
 
2628
        /**
 
2629
         * Destroy the element and element wrapper
 
2630
         */
 
2631
        destroy: function () {
 
2632
                var wrapper = this,
 
2633
                        element = wrapper.element || {},
 
2634
                        shadows = wrapper.shadows,
 
2635
                        parentToClean = wrapper.renderer.isSVG && element.nodeName === 'SPAN' && wrapper.parentGroup,
 
2636
                        grandParent,
 
2637
                        key,
 
2638
                        i;
 
2639
 
 
2640
                // remove events
 
2641
                element.onclick = element.onmouseout = element.onmouseover = element.onmousemove = element.point = null;
 
2642
                stop(wrapper); // stop running animations
 
2643
 
 
2644
                if (wrapper.clipPath) {
 
2645
                        wrapper.clipPath = wrapper.clipPath.destroy();
 
2646
                }
 
2647
 
 
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();
 
2652
                        }
 
2653
                        wrapper.stops = null;
 
2654
                }
 
2655
 
 
2656
                // remove element
 
2657
                wrapper.safeRemoveChild(element);
 
2658
 
 
2659
                // destroy shadows
 
2660
                if (shadows) {
 
2661
                        each(shadows, function (shadow) {
 
2662
                                wrapper.safeRemoveChild(shadow);
 
2663
                        });
 
2664
                }
 
2665
 
 
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;
 
2672
                }
 
2673
 
 
2674
                // remove from alignObjects
 
2675
                if (wrapper.alignTo) {
 
2676
                        erase(wrapper.renderer.alignedObjects, wrapper);
 
2677
                }
 
2678
 
 
2679
                for (key in wrapper) {
 
2680
                        delete wrapper[key];
 
2681
                }
 
2682
 
 
2683
                return null;
 
2684
        },
 
2685
 
 
2686
        /**
 
2687
         * Add a shadow to the element. Must be done after the element is added to the DOM
 
2688
         * @param {Boolean|Object} shadowOptions
 
2689
         */
 
2690
        shadow: function (shadowOptions, group, cutOff) {
 
2691
                var shadows = [],
 
2692
                        i,
 
2693
                        shadow,
 
2694
                        element = this.element,
 
2695
                        strokeWidth,
 
2696
                        shadowWidth,
 
2697
                        shadowElementOpacity,
 
2698
 
 
2699
                        // compensate for inverted plot area
 
2700
                        transform;
 
2701
 
 
2702
 
 
2703
                if (shadowOptions) {
 
2704
                        shadowWidth = pick(shadowOptions.width, 3);
 
2705
                        shadowElementOpacity = (shadowOptions.opacity || 0.15) / shadowWidth;
 
2706
                        transform = this.parentInverted ?
 
2707
                                '(-1,-1)' :
 
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);
 
2712
                                attr(shadow, {
 
2713
                                        'isShadow': 'true',
 
2714
                                        'stroke': shadowOptions.color || 'black',
 
2715
                                        'stroke-opacity': shadowElementOpacity * i,
 
2716
                                        'stroke-width': strokeWidth,
 
2717
                                        'transform': 'translate' + transform,
 
2718
                                        'fill': NONE
 
2719
                                });
 
2720
                                if (cutOff) {
 
2721
                                        attr(shadow, 'height', mathMax(attr(shadow, 'height') - strokeWidth, 0));
 
2722
                                        shadow.cutHeight = strokeWidth;
 
2723
                                }
 
2724
 
 
2725
                                if (group) {
 
2726
                                        group.element.appendChild(shadow);
 
2727
                                } else {
 
2728
                                        element.parentNode.insertBefore(shadow, element);
 
2729
                                }
 
2730
 
 
2731
                                shadows.push(shadow);
 
2732
                        }
 
2733
 
 
2734
                        this.shadows = shadows;
 
2735
                }
 
2736
                return this;
 
2737
 
 
2738
        }
 
2739
};
 
2740
 
 
2741
 
 
2742
/**
 
2743
 * The default SVG renderer
 
2744
 */
 
2745
var SVGRenderer = function () {
 
2746
        this.init.apply(this, arguments);
 
2747
};
 
2748
SVGRenderer.prototype = {
 
2749
        Element: SVGElement,
 
2750
 
 
2751
        /**
 
2752
         * Initialize the SVGRenderer
 
2753
         * @param {Object} container
 
2754
         * @param {Number} width
 
2755
         * @param {Number} height
 
2756
         * @param {Boolean} forExport
 
2757
         */
 
2758
        init: function (container, width, height, style, forExport) {
 
2759
                var renderer = this,
 
2760
                        loc = location,
 
2761
                        boxWrapper,
 
2762
                        element,
 
2763
                        desc;
 
2764
 
 
2765
                boxWrapper = renderer.createElement('svg')
 
2766
                        .attr({
 
2767
                                version: '1.1'
 
2768
                        })
 
2769
                        .css(this.getStyle(style));
 
2770
                element = boxWrapper.element;
 
2771
                container.appendChild(element);
 
2772
 
 
2773
                // For browsers other than IE, add the namespace attribute (#1978)
 
2774
                if (container.innerHTML.indexOf('xmlns') === -1) {
 
2775
                        attr(element, 'xmlns', SVG_NS);
 
2776
                }
 
2777
 
 
2778
                // object properties
 
2779
                renderer.isSVG = true;
 
2780
                renderer.box = element;
 
2781
                renderer.boxWrapper = boxWrapper;
 
2782
                renderer.alignedObjects = [];
 
2783
 
 
2784
                // Page url used for internal references. #24, #672, #1070
 
2785
                renderer.url = (isFirefox || isWebKit) && doc.getElementsByTagName('base').length ?
 
2786
                        loc.href
 
2787
                                .replace(/#.*?$/, '') // remove the hash
 
2788
                                .replace(/([\('\)])/g, '\\$1') // escape parantheses and quotes
 
2789
                                .replace(/ /g, '%20') : // replace spaces (needed for Safari only)
 
2790
                        '';
 
2791
 
 
2792
                // Add description
 
2793
                desc = this.createElement('desc').add();
 
2794
                desc.element.appendChild(doc.createTextNode('Created with ' + PRODUCT + ' ' + VERSION));
 
2795
 
 
2796
 
 
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
 
2801
 
 
2802
                renderer.setSize(width, height, false);
 
2803
 
 
2804
 
 
2805
 
 
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();
 
2817
                                css(container, {
 
2818
                                        left: (mathCeil(rect.left) - rect.left) + PX,
 
2819
                                        top: (mathCeil(rect.top) - rect.top) + PX
 
2820
                                });
 
2821
                        };
 
2822
 
 
2823
                        // run the fix now
 
2824
                        subPixelFix();
 
2825
 
 
2826
                        // run it on resize
 
2827
                        addEvent(win, 'resize', subPixelFix);
 
2828
                }
 
2829
        },
 
2830
 
 
2831
        getStyle: function (style) {
 
2832
                return (this.style = extend({
 
2833
                        fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif', // default font
 
2834
                        fontSize: '12px'
 
2835
                }, style));
 
2836
        },
 
2837
 
 
2838
        /**
 
2839
         * Detect whether the renderer is hidden. This happens when one of the parent elements
 
2840
         * has display: none. #608.
 
2841
         */
 
2842
        isHidden: function () {
 
2843
                return !this.boxWrapper.getBBox().width;
 
2844
        },
 
2845
 
 
2846
        /**
 
2847
         * Destroys the renderer and its allocated members.
 
2848
         */
 
2849
        destroy: function () {
 
2850
                var renderer = this,
 
2851
                        rendererDefs = renderer.defs;
 
2852
                renderer.box = null;
 
2853
                renderer.boxWrapper = renderer.boxWrapper.destroy();
 
2854
 
 
2855
                // Call destroy on all gradient elements
 
2856
                destroyObjectProperties(renderer.gradients || {});
 
2857
                renderer.gradients = null;
 
2858
 
 
2859
                // Defs are null in VMLRenderer
 
2860
                // Otherwise, destroy them here.
 
2861
                if (rendererDefs) {
 
2862
                        renderer.defs = rendererDefs.destroy();
 
2863
                }
 
2864
 
 
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
 
2867
                // See issue #982
 
2868
                if (renderer.subPixelFix) {
 
2869
                        removeEvent(win, 'resize', renderer.subPixelFix);
 
2870
                }
 
2871
 
 
2872
                renderer.alignedObjects = null;
 
2873
 
 
2874
                return null;
 
2875
        },
 
2876
 
 
2877
        /**
 
2878
         * Create a wrapper for an SVG element
 
2879
         * @param {Object} nodeName
 
2880
         */
 
2881
        createElement: function (nodeName) {
 
2882
                var wrapper = new this.Element();
 
2883
                wrapper.init(this, nodeName);
 
2884
                return wrapper;
 
2885
        },
 
2886
 
 
2887
        /**
 
2888
         * Dummy function for use in canvas renderer
 
2889
         */
 
2890
        draw: function () {},
 
2891
 
 
2892
        /**
 
2893
         * Parse a simple HTML string into SVG tspans
 
2894
         *
 
2895
         * @param {Object} textNode The parent text SVG node
 
2896
         */
 
2897
        buildText: function (wrapper) {
 
2898
                var textNode = wrapper.element,
 
2899
                        renderer = this,
 
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>')
 
2906
                                .split(/<br.*?>/g),
 
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)
 
2922
                                        ).h;
 
2923
                        };
 
2924
 
 
2925
                /// remove old text
 
2926
                while (i--) {
 
2927
                        textNode.removeChild(childNodes[i]);
 
2928
                }
 
2929
 
 
2930
                if (width && !wrapper.added) {
 
2931
                        this.box.appendChild(textNode); // attach it to the DOM to read offset width
 
2932
                }
 
2933
 
 
2934
                // remove empty line at end
 
2935
                if (lines[lines.length - 1] === '') {
 
2936
                        lines.pop();
 
2937
                }
 
2938
 
 
2939
                // build the lines
 
2940
                each(lines, function (line, lineNo) {
 
2941
                        var spans, spanNo = 0;
 
2942
 
 
2943
                        line = line.replace(/<span/g, '|||<span').replace(/<\/span>/g, '</span>|||');
 
2944
                        spans = line.split('|||');
 
2945
 
 
2946
                        each(spans, function (span) {
 
2947
                                if (span !== '' || spans.length === 1) {
 
2948
                                        var attributes = {},
 
2949
                                                tspan = doc.createElementNS(SVG_NS, 'tspan'),
 
2950
                                                spanStyle; // #390
 
2951
                                        if (styleRegex.test(span)) {
 
2952
                                                spanStyle = span.match(styleRegex)[1].replace(/(;| |^)color([ :])/, '$1fill$2');
 
2953
                                                attr(tspan, 'style', spanStyle);
 
2954
                                        }
 
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' });
 
2958
                                        }
 
2959
 
 
2960
                                        span = (span.replace(/<(.|\n)*?>/g, '') || ' ')
 
2961
                                                .replace(/&lt;/g, '<')
 
2962
                                                .replace(/&gt;/g, '>');
 
2963
 
 
2964
                                        // Nested tags aren't supported, and cause crash in Safari (#1596)
 
2965
                                        if (span !== ' ') {
 
2966
 
 
2967
                                                // add the text node
 
2968
                                                tspan.appendChild(doc.createTextNode(span));
 
2969
 
 
2970
                                                if (!spanNo) { // first span in a line, align it to the left
 
2971
                                                        attributes.x = parentX;
 
2972
                                                } else {
 
2973
                                                        attributes.dx = 0; // #16
 
2974
                                                }
 
2975
 
 
2976
                                                // add attributes
 
2977
                                                attr(tspan, attributes);
 
2978
 
 
2979
                                                // first span on subsequent line, add the line height
 
2980
                                                if (!spanNo && lineNo) {
 
2981
 
 
2982
                                                        // allow getting the right offset height in exporting in IE
 
2983
                                                        if (!hasSVG && forExport) {
 
2984
                                                                css(tspan, { display: 'block' });
 
2985
                                                        }
 
2986
 
 
2987
                                                        // Set the line height based on the font size of either
 
2988
                                                        // the text element or the tspan element
 
2989
                                                        attr(
 
2990
                                                                tspan,
 
2991
                                                                'dy',
 
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
 
2996
                                                        );
 
2997
                                                }
 
2998
 
 
2999
                                                // Append it
 
3000
                                                textNode.appendChild(tspan);
 
3001
 
 
3002
                                                spanNo++;
 
3003
 
 
3004
                                                // check width and apply soft breaks
 
3005
                                                if (width) {
 
3006
                                                        var words = span.replace(/([^\^])-/g, '$1- ').split(' '), // #1273
 
3007
                                                                hasWhiteSpace = words.length > 1 && textStyles.whiteSpace !== 'nowrap',
 
3008
                                                                tooLong,
 
3009
                                                                actualWidth,
 
3010
                                                                clipHeight = wrapper._clipHeight,
 
3011
                                                                rest = [],
 
3012
                                                                dy = getLineHeight(),
 
3013
                                                                softLineNo = 1,
 
3014
                                                                bBox;
 
3015
 
 
3016
                                                        while (hasWhiteSpace && (words.length || rest.length)) {
 
3017
                                                                delete wrapper.bBox; // delete cache
 
3018
                                                                bBox = wrapper.getBBox();
 
3019
                                                                actualWidth = bBox.width;
 
3020
 
 
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);
 
3024
                                                                }
 
3025
 
 
3026
                                                                tooLong = actualWidth > width;
 
3027
                                                                if (!tooLong || words.length === 1) { // new line needed
 
3028
                                                                        words = rest;
 
3029
                                                                        rest = [];
 
3030
                                                                        if (words.length) {
 
3031
                                                                                softLineNo++;
 
3032
 
 
3033
                                                                                if (clipHeight && softLineNo * dy > clipHeight) {
 
3034
                                                                                        words = ['...'];
 
3035
                                                                                        wrapper.attr('title', wrapper.textStr);
 
3036
                                                                                } else {
 
3037
 
 
3038
                                                                                        tspan = doc.createElementNS(SVG_NS, 'tspan');
 
3039
                                                                                        attr(tspan, {
 
3040
                                                                                                dy: dy,
 
3041
                                                                                                x: parentX
 
3042
                                                                                        });
 
3043
                                                                                        if (spanStyle) { // #390
 
3044
                                                                                                attr(tspan, 'style', spanStyle);
 
3045
                                                                                        }
 
3046
                                                                                        textNode.appendChild(tspan);
 
3047
 
 
3048
                                                                                        if (actualWidth > width) { // a single word is pressing it out
 
3049
                                                                                                width = actualWidth;
 
3050
                                                                                        }
 
3051
                                                                                }
 
3052
                                                                        }
 
3053
                                                                } else { // append to existing line tspan
 
3054
                                                                        tspan.removeChild(tspan.firstChild);
 
3055
                                                                        rest.unshift(words.pop());
 
3056
                                                                }
 
3057
                                                                if (words.length) {
 
3058
                                                                        tspan.appendChild(doc.createTextNode(words.join(' ').replace(/- /g, '-')));
 
3059
                                                                }
 
3060
                                                        }
 
3061
                                                }
 
3062
                                        }
 
3063
                                }
 
3064
                        });
 
3065
                });
 
3066
        },
 
3067
 
 
3068
        /**
 
3069
         * Create a button with preset states
 
3070
         * @param {String} text
 
3071
         * @param {Number} x
 
3072
         * @param {Number} y
 
3073
         * @param {Function} callback
 
3074
         * @param {Object} normalState
 
3075
         * @param {Object} hoverState
 
3076
         * @param {Object} pressedState
 
3077
         */
 
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'),
 
3080
                        curState = 0,
 
3081
                        stateOptions,
 
3082
                        stateStyle,
 
3083
                        normalStyle,
 
3084
                        hoverStyle,
 
3085
                        pressedStyle,
 
3086
                        disabledStyle,
 
3087
                        STYLE = 'style',
 
3088
                        verticalGradient = { x1: 0, y1: 0, x2: 0, y2: 1 };
 
3089
 
 
3090
                // Normal state - prepare the attributes
 
3091
                normalState = merge({
 
3092
                        'stroke-width': 1,
 
3093
                        stroke: '#CCCCCC',
 
3094
                        fill: {
 
3095
                                linearGradient: verticalGradient,
 
3096
                                stops: [
 
3097
                                        [0, '#FEFEFE'],
 
3098
                                        [1, '#F6F6F6']
 
3099
                                ]
 
3100
                        },
 
3101
                        r: 2,
 
3102
                        padding: 5,
 
3103
                        style: {
 
3104
                                color: 'black'
 
3105
                        }
 
3106
                }, normalState);
 
3107
                normalStyle = normalState[STYLE];
 
3108
                delete normalState[STYLE];
 
3109
 
 
3110
                // Hover state
 
3111
                hoverState = merge(normalState, {
 
3112
                        stroke: '#68A',
 
3113
                        fill: {
 
3114
                                linearGradient: verticalGradient,
 
3115
                                stops: [
 
3116
                                        [0, '#FFF'],
 
3117
                                        [1, '#ACF']
 
3118
                                ]
 
3119
                        }
 
3120
                }, hoverState);
 
3121
                hoverStyle = hoverState[STYLE];
 
3122
                delete hoverState[STYLE];
 
3123
 
 
3124
                // Pressed state
 
3125
                pressedState = merge(normalState, {
 
3126
                        stroke: '#68A',
 
3127
                        fill: {
 
3128
                                linearGradient: verticalGradient,
 
3129
                                stops: [
 
3130
                                        [0, '#9BD'],
 
3131
                                        [1, '#CDF']
 
3132
                                ]
 
3133
                        }
 
3134
                }, pressedState);
 
3135
                pressedStyle = pressedState[STYLE];
 
3136
                delete pressedState[STYLE];
 
3137
 
 
3138
                // Disabled state
 
3139
                disabledState = merge(normalState, {
 
3140
                        style: {
 
3141
                                color: '#CCC'
 
3142
                        }
 
3143
                }, disabledState);
 
3144
                disabledStyle = disabledState[STYLE];
 
3145
                delete disabledState[STYLE];
 
3146
 
 
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)
 
3151
                                        .css(hoverStyle);
 
3152
                        }
 
3153
                });
 
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)
 
3159
                                        .css(stateStyle);
 
3160
                        }
 
3161
                });
 
3162
 
 
3163
                label.setState = function (state) {
 
3164
                        label.state = curState = state;
 
3165
                        if (!state) {
 
3166
                                label.attr(normalState)
 
3167
                                        .css(normalStyle);
 
3168
                        } else if (state === 2) {
 
3169
                                label.attr(pressedState)
 
3170
                                        .css(pressedStyle);
 
3171
                        } else if (state === 3) {
 
3172
                                label.attr(disabledState)
 
3173
                                        .css(disabledStyle);
 
3174
                        }
 
3175
                };
 
3176
 
 
3177
                return label
 
3178
                        .on('click', function () {
 
3179
                                if (curState !== 3) {
 
3180
                                        callback.call(label);
 
3181
                                }
 
3182
                        })
 
3183
                        .attr(normalState)
 
3184
                        .css(extend({ cursor: 'default' }, normalStyle));
 
3185
        },
 
3186
 
 
3187
        /**
 
3188
         * Make a straight line crisper by not spilling out to neighbour pixels
 
3189
         * @param {Array} points
 
3190
         * @param {Number} width
 
3191
         */
 
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);
 
3198
                }
 
3199
                if (points[2] === points[5]) {
 
3200
                        points[2] = points[5] = mathRound(points[2]) + (width % 2 / 2);
 
3201
                }
 
3202
                return points;
 
3203
        },
 
3204
 
 
3205
 
 
3206
        /**
 
3207
         * Draw a path
 
3208
         * @param {Array} path An SVG path in array form
 
3209
         */
 
3210
        path: function (path) {
 
3211
                var attr = {
 
3212
                        fill: NONE
 
3213
                };
 
3214
                if (isArray(path)) {
 
3215
                        attr.d = path;
 
3216
                } else if (isObject(path)) { // attributes
 
3217
                        extend(attr, path);
 
3218
                }
 
3219
                return this.createElement('path').attr(attr);
 
3220
        },
 
3221
 
 
3222
        /**
 
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
 
3227
         */
 
3228
        circle: function (x, y, r) {
 
3229
                var attr = isObject(x) ?
 
3230
                        x :
 
3231
                        {
 
3232
                                x: x,
 
3233
                                y: y,
 
3234
                                r: r
 
3235
                        };
 
3236
 
 
3237
                return this.createElement('circle').attr(attr);
 
3238
        },
 
3239
 
 
3240
        /**
 
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
 
3248
         */
 
3249
        arc: function (x, y, r, innerR, start, end) {
 
3250
                var arc;
 
3251
 
 
3252
                if (isObject(x)) {
 
3253
                        y = x.y;
 
3254
                        r = x.r;
 
3255
                        innerR = x.innerR;
 
3256
                        start = x.start;
 
3257
                        end = x.end;
 
3258
                        x = x.x;
 
3259
                }
 
3260
 
 
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,
 
3265
                        start: start || 0,
 
3266
                        end: end || 0
 
3267
                });
 
3268
                arc.r = r; // #959
 
3269
                return arc;
 
3270
        },
 
3271
 
 
3272
        /**
 
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
 
3280
         */
 
3281
        rect: function (x, y, width, height, r, strokeWidth) {
 
3282
 
 
3283
                r = isObject(x) ? x.r : r;
 
3284
 
 
3285
                var wrapper = this.createElement('rect'),
 
3286
                        attr = isObject(x) ? x : x === UNDEFINED ? {} : {
 
3287
                                x: x,
 
3288
                                y: y,
 
3289
                                width: mathMax(width, 0),
 
3290
                                height: mathMax(height, 0)
 
3291
                        };
 
3292
 
 
3293
                if (strokeWidth !== UNDEFINED) {
 
3294
                        attr.strokeWidth = strokeWidth;
 
3295
                        attr = wrapper.crisp(attr);
 
3296
                }
 
3297
 
 
3298
                if (r) {
 
3299
                        attr.r = r;
 
3300
                }               
 
3301
                
 
3302
                return wrapper.attr(attr);
 
3303
        },
 
3304
 
 
3305
        /**
 
3306
         * Resize the box and re-align all aligned elements
 
3307
         * @param {Object} width
 
3308
         * @param {Object} height
 
3309
         * @param {Boolean} animate
 
3310
         *
 
3311
         */
 
3312
        setSize: function (width, height, animate) {
 
3313
                var renderer = this,
 
3314
                        alignedObjects = renderer.alignedObjects,
 
3315
                        i = alignedObjects.length;
 
3316
 
 
3317
                renderer.width = width;
 
3318
                renderer.height = height;
 
3319
 
 
3320
                renderer.boxWrapper[pick(animate, true) ? 'animate' : 'attr']({
 
3321
                        width: width,
 
3322
                        height: height
 
3323
                });
 
3324
 
 
3325
                while (i--) {
 
3326
                        alignedObjects[i].align();
 
3327
                }
 
3328
        },
 
3329
 
 
3330
        /**
 
3331
         * Create a group
 
3332
         * @param {String} name The group will be given a class name of 'highcharts-{name}'.
 
3333
         *     This can be used for styling and scripting.
 
3334
         */
 
3335
        g: function (name) {
 
3336
                var elem = this.createElement('g');
 
3337
                return defined(name) ? elem.attr({ 'class': PREFIX + name }) : elem;
 
3338
        },
 
3339
 
 
3340
        /**
 
3341
         * Display an image
 
3342
         * @param {String} src
 
3343
         * @param {Number} x
 
3344
         * @param {Number} y
 
3345
         * @param {Number} width
 
3346
         * @param {Number} height
 
3347
         */
 
3348
        image: function (src, x, y, width, height) {
 
3349
                var attribs = {
 
3350
                                preserveAspectRatio: NONE
 
3351
                        },
 
3352
                        elemWrapper;
 
3353
 
 
3354
                // optional properties
 
3355
                if (arguments.length > 1) {
 
3356
                        extend(attribs, {
 
3357
                                x: x,
 
3358
                                y: y,
 
3359
                                width: width,
 
3360
                                height: height
 
3361
                        });
 
3362
                }
 
3363
 
 
3364
                elemWrapper = this.createElement('image').attr(attribs);
 
3365
 
 
3366
                // set the href in the xlink namespace
 
3367
                if (elemWrapper.element.setAttributeNS) {
 
3368
                        elemWrapper.element.setAttributeNS('http://www.w3.org/1999/xlink',
 
3369
                                'href', src);
 
3370
                } else {
 
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);
 
3374
        }
 
3375
 
 
3376
                return elemWrapper;
 
3377
        },
 
3378
 
 
3379
        /**
 
3380
         * Draw a symbol out of pre-defined shape paths from the namespace 'symbol' object.
 
3381
         *
 
3382
         * @param {Object} symbol
 
3383
         * @param {Object} x
 
3384
         * @param {Object} y
 
3385
         * @param {Object} radius
 
3386
         * @param {Object} options
 
3387
         */
 
3388
        symbol: function (symbol, x, y, width, height, options) {
 
3389
 
 
3390
                var obj,
 
3391
 
 
3392
                        // get the symbol definition function
 
3393
                        symbolFn = this.symbols[symbol],
 
3394
 
 
3395
                        // check if there's a path defined for this symbol
 
3396
                        path = symbolFn && symbolFn(
 
3397
                                mathRound(x),
 
3398
                                mathRound(y),
 
3399
                                width,
 
3400
                                height,
 
3401
                                options
 
3402
                        ),
 
3403
 
 
3404
                        imageElement,
 
3405
                        imageRegex = /^url\((.*?)\)$/,
 
3406
                        imageSrc,
 
3407
                        imageSize,
 
3408
                        centerImage;
 
3409
 
 
3410
                if (path) {
 
3411
 
 
3412
                        obj = this.path(path);
 
3413
                        // expando properties for use in animate and attr
 
3414
                        extend(obj, {
 
3415
                                symbolName: symbol,
 
3416
                                x: x,
 
3417
                                y: y,
 
3418
                                width: width,
 
3419
                                height: height
 
3420
                        });
 
3421
                        if (options) {
 
3422
                                extend(obj, options);
 
3423
                        }
 
3424
 
 
3425
 
 
3426
                // image symbols
 
3427
                } else if (imageRegex.test(symbol)) {
 
3428
 
 
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)
 
3432
                                        img.attr({
 
3433
                                                width: size[0],
 
3434
                                                height: size[1]
 
3435
                                        });
 
3436
 
 
3437
                                        if (!img.alignByTranslate) { // #185
 
3438
                                                img.translate(
 
3439
                                                        mathRound((width - size[0]) / 2), // #1378
 
3440
                                                        mathRound((height - size[1]) / 2)
 
3441
                                                );
 
3442
                                        }
 
3443
                                }
 
3444
                        };
 
3445
 
 
3446
                        imageSrc = symbol.match(imageRegex)[1];
 
3447
                        imageSize = symbolSizes[imageSrc];
 
3448
 
 
3449
                        // Ireate the image synchronously, add attribs async
 
3450
                        obj = this.image(imageSrc)
 
3451
                                .attr({
 
3452
                                        x: x,
 
3453
                                        y: y
 
3454
                                });
 
3455
                        obj.isImg = true;
 
3456
 
 
3457
                        if (imageSize) {
 
3458
                                centerImage(obj, imageSize);
 
3459
                        } else {
 
3460
                                // Initialize image to be 0 size so export will still function if there's no cached sizes.
 
3461
                                //
 
3462
                                obj.attr({ width: 0, height: 0 });
 
3463
 
 
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]);
 
3469
                                        },
 
3470
                                        src: imageSrc
 
3471
                                });
 
3472
                        }
 
3473
                }
 
3474
 
 
3475
                return obj;
 
3476
        },
 
3477
 
 
3478
        /**
 
3479
         * An extendable collection of functions for defining symbol paths.
 
3480
         */
 
3481
        symbols: {
 
3482
                'circle': function (x, y, w, h) {
 
3483
                        var cpw = 0.166 * w;
 
3484
                        return [
 
3485
                                M, x + w / 2, y,
 
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,
 
3488
                                'Z'
 
3489
                        ];
 
3490
                },
 
3491
 
 
3492
                'square': function (x, y, w, h) {
 
3493
                        return [
 
3494
                                M, x, y,
 
3495
                                L, x + w, y,
 
3496
                                x + w, y + h,
 
3497
                                x, y + h,
 
3498
                                'Z'
 
3499
                        ];
 
3500
                },
 
3501
 
 
3502
                'triangle': function (x, y, w, h) {
 
3503
                        return [
 
3504
                                M, x + w / 2, y,
 
3505
                                L, x + w, y + h,
 
3506
                                x, y + h,
 
3507
                                'Z'
 
3508
                        ];
 
3509
                },
 
3510
 
 
3511
                'triangle-down': function (x, y, w, h) {
 
3512
                        return [
 
3513
                                M, x, y,
 
3514
                                L, x + w, y,
 
3515
                                x + w / 2, y + h,
 
3516
                                'Z'
 
3517
                        ];
 
3518
                },
 
3519
                'diamond': function (x, y, w, h) {
 
3520
                        return [
 
3521
                                M, x + w / 2, y,
 
3522
                                L, x + w, y + h / 2,
 
3523
                                x + w / 2, y + h,
 
3524
                                x, y + h / 2,
 
3525
                                'Z'
 
3526
                        ];
 
3527
                },
 
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;
 
3539
 
 
3540
                        return [
 
3541
                                M,
 
3542
                                x + radius * cosStart,
 
3543
                                y + radius * sinStart,
 
3544
                                'A', // arcTo
 
3545
                                radius, // x radius
 
3546
                                radius, // y radius
 
3547
                                0, // slanting
 
3548
                                longArc, // long or short arc
 
3549
                                1, // clockwise
 
3550
                                x + radius * cosEnd,
 
3551
                                y + radius * sinEnd,
 
3552
                                open ? M : L,
 
3553
                                x + innerRadius * cosEnd,
 
3554
                                y + innerRadius * sinEnd,
 
3555
                                'A', // arcTo
 
3556
                                innerRadius, // x radius
 
3557
                                innerRadius, // y radius
 
3558
                                0, // slanting
 
3559
                                longArc, // long or short arc
 
3560
                                0, // clockwise
 
3561
                                x + innerRadius * cosStart,
 
3562
                                y + innerRadius * sinStart,
 
3563
 
 
3564
                                open ? '' : 'Z' // close
 
3565
                        ];
 
3566
                }
 
3567
        },
 
3568
 
 
3569
        /**
 
3570
         * Define a clipping rectangle
 
3571
         * @param {String} id
 
3572
         * @param {Number} x
 
3573
         * @param {Number} y
 
3574
         * @param {Number} width
 
3575
         * @param {Number} height
 
3576
         */
 
3577
        clipRect: function (x, y, width, height) {
 
3578
                var wrapper,
 
3579
                        id = PREFIX + idCounter++,
 
3580
 
 
3581
                        clipPath = this.createElement('clipPath').attr({
 
3582
                                id: id
 
3583
                        }).add(this.defs);
 
3584
 
 
3585
                wrapper = this.rect(x, y, width, height, 0).add(clipPath);
 
3586
                wrapper.id = id;
 
3587
                wrapper.clipPath = clipPath;
 
3588
 
 
3589
                return wrapper;
 
3590
        },
 
3591
 
 
3592
 
 
3593
        /**
 
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.
 
3600
         *
 
3601
         * @param {Object} color The color or config object
 
3602
         */
 
3603
        color: function (color, elem, prop) {
 
3604
                var renderer = this,
 
3605
                        colorObject,
 
3606
                        regexRgba = /^rgba/,
 
3607
                        gradName,
 
3608
                        gradAttr,
 
3609
                        gradients,
 
3610
                        gradientObject,
 
3611
                        stops,
 
3612
                        stopColor,
 
3613
                        stopOpacity,
 
3614
                        radialReference,
 
3615
                        n,
 
3616
                        id,
 
3617
                        key = [];
 
3618
 
 
3619
                // Apply linear or radial gradients
 
3620
                if (color && color.linearGradient) {
 
3621
                        gradName = 'linearGradient';
 
3622
                } else if (color && color.radialGradient) {
 
3623
                        gradName = 'radialGradient';
 
3624
                }
 
3625
 
 
3626
                if (gradName) {
 
3627
                        gradAttr = color[gradName];
 
3628
                        gradients = renderer.gradients;
 
3629
                        stops = color.stops;
 
3630
                        radialReference = elem.radialReference;
 
3631
 
 
3632
                        // Keep < 2.2 kompatibility
 
3633
                        if (isArray(gradAttr)) {
 
3634
                                color[gradName] = gradAttr = {
 
3635
                                        x1: gradAttr[0],
 
3636
                                        y1: gradAttr[1],
 
3637
                                        x2: gradAttr[2],
 
3638
                                        y2: gradAttr[3],
 
3639
                                        gradientUnits: 'userSpaceOnUse'
 
3640
                                };
 
3641
                        }
 
3642
 
 
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'
 
3650
                                });
 
3651
                        }
 
3652
 
 
3653
                        // Build the unique key to detect whether we need to create a new element (#1282)
 
3654
                        for (n in gradAttr) {
 
3655
                                if (n !== 'id') {
 
3656
                                        key.push(n, gradAttr[n]);
 
3657
                                }
 
3658
                        }
 
3659
                        for (n in stops) {
 
3660
                                key.push(stops[n]);
 
3661
                        }
 
3662
                        key = key.join(',');
 
3663
 
 
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;
 
3667
 
 
3668
                        } else {
 
3669
 
 
3670
                                // Set the id and create the element
 
3671
                                gradAttr.id = id = PREFIX + idCounter++;
 
3672
                                gradients[key] = gradientObject = renderer.createElement(gradName)
 
3673
                                        .attr(gradAttr)
 
3674
                                        .add(renderer.defs);
 
3675
 
 
3676
 
 
3677
                                // The gradient needs to keep a list of stops to be able to destroy them
 
3678
                                gradientObject.stops = [];
 
3679
                                each(stops, function (stop) {
 
3680
                                        var stopObject;
 
3681
                                        if (regexRgba.test(stop[1])) {
 
3682
                                                colorObject = Color(stop[1]);
 
3683
                                                stopColor = colorObject.get('rgb');
 
3684
                                                stopOpacity = colorObject.get('a');
 
3685
                                        } else {
 
3686
                                                stopColor = stop[1];
 
3687
                                                stopOpacity = 1;
 
3688
                                        }
 
3689
                                        stopObject = renderer.createElement('stop').attr({
 
3690
                                                offset: stop[0],
 
3691
                                                'stop-color': stopColor,
 
3692
                                                'stop-opacity': stopOpacity
 
3693
                                        }).add(gradientObject);
 
3694
 
 
3695
                                        // Add the stop element to the gradient
 
3696
                                        gradientObject.stops.push(stopObject);
 
3697
                                });
 
3698
                        }
 
3699
 
 
3700
                        // Return the reference to the gradient object
 
3701
                        return 'url(' + renderer.url + '#' + id + ')';
 
3702
 
 
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'));
 
3707
 
 
3708
                        return colorObject.get('rgb');
 
3709
 
 
3710
 
 
3711
                } else {
 
3712
                        // Remove the opacity attribute added above. Does not throw if the attribute is not there.
 
3713
                        elem.removeAttribute(prop + '-opacity');
 
3714
 
 
3715
                        return color;
 
3716
                }
 
3717
 
 
3718
        },
 
3719
 
 
3720
 
 
3721
        /**
 
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
 
3727
         */
 
3728
        text: function (str, x, y, useHTML) {
 
3729
 
 
3730
                // declare variables
 
3731
                var renderer = this,
 
3732
                        fakeSVG = useCanVG || (!hasSVG && renderer.forExport),
 
3733
                        wrapper;
 
3734
 
 
3735
                if (useHTML && !renderer.forExport) {
 
3736
                        return renderer.html(str, x, y);
 
3737
                }
 
3738
 
 
3739
                x = mathRound(pick(x, 0));
 
3740
                y = mathRound(pick(y, 0));
 
3741
 
 
3742
                wrapper = renderer.createElement('text')
 
3743
                        .attr({
 
3744
                                x: x,
 
3745
                                y: y,
 
3746
                                text: str
 
3747
                        });
 
3748
 
 
3749
                // Prevent wrapping from creating false offsetWidths in export in legacy IE (#1079, #1063)
 
3750
                if (fakeSVG) {
 
3751
                        wrapper.css({
 
3752
                                position: ABSOLUTE
 
3753
                        });
 
3754
                }
 
3755
 
 
3756
                wrapper.x = x;
 
3757
                wrapper.y = y;
 
3758
                return wrapper;
 
3759
        },
 
3760
 
 
3761
        /**
 
3762
         * Utility to return the baseline offset and total line height from the font size
 
3763
         */
 
3764
        fontMetrics: function (fontSize) {
 
3765
                fontSize = fontSize || this.style.fontSize;
 
3766
                fontSize = /px/.test(fontSize) ? pInt(fontSize) : /em/.test(fontSize) ? parseFloat(fontSize) * 12 : 12;
 
3767
 
 
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);
 
3772
 
 
3773
                return {
 
3774
                        h: lineHeight,
 
3775
                        b: baseline
 
3776
                };
 
3777
        },
 
3778
 
 
3779
        /**
 
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
 
3783
         * @param {Number} x
 
3784
         * @param {Number} y
 
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
 
3792
         */
 
3793
        label: function (str, x, y, shape, anchorX, anchorY, useHTML, baseline, className) {
 
3794
 
 
3795
                var renderer = this,
 
3796
                        wrapper = renderer.g(className),
 
3797
                        text = renderer.text('', 0, 0, useHTML)
 
3798
                                .attr({
 
3799
                                        zIndex: 1
 
3800
                                }),
 
3801
                                //.add(wrapper),
 
3802
                        box,
 
3803
                        bBox,
 
3804
                        alignFactor = 0,
 
3805
                        padding = 3,
 
3806
                        paddingLeft = 0,
 
3807
                        width,
 
3808
                        height,
 
3809
                        wrapperX,
 
3810
                        wrapperY,
 
3811
                        crispAdjust = 0,
 
3812
                        deferredAttr = {},
 
3813
                        baselineOffset,
 
3814
                        attrSetters = wrapper.attrSetters,
 
3815
                        needsBox;
 
3816
 
 
3817
                /**
 
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.
 
3821
                 */
 
3822
                function updateBoxSize() {
 
3823
                        var boxX,
 
3824
                                boxY,
 
3825
                                style = text.element.style;
 
3826
 
 
3827
                        bBox = (width === undefined || height === undefined || wrapper.styles.textAlign) && text.textStr && 
 
3828
                                text.getBBox();
 
3829
                        wrapper.width = (width || bBox.width || 0) + 2 * padding + paddingLeft;
 
3830
                        wrapper.height = (height || bBox.height || 0) + 2 * padding;
 
3831
 
 
3832
                        // update the label-scoped y offset
 
3833
                        baselineOffset = padding + renderer.fontMetrics(style && style.fontSize).b;
 
3834
 
 
3835
                        if (needsBox) {
 
3836
 
 
3837
                                // create the border box if it is not already present
 
3838
                                if (!box) {
 
3839
                                        boxX = mathRound(-alignFactor * padding);
 
3840
                                        boxY = baseline ? -baselineOffset : 0;
 
3841
 
 
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);
 
3846
                                }
 
3847
 
 
3848
                                // apply the box attributes
 
3849
                                if (!box.isImg) { // #1630
 
3850
                                        box.attr(merge({
 
3851
                                                width: wrapper.width,
 
3852
                                                height: wrapper.height
 
3853
                                        }, deferredAttr));
 
3854
                                }
 
3855
                                deferredAttr = null;
 
3856
                        }
 
3857
                }
 
3858
 
 
3859
                /**
 
3860
                 * This function runs after setting text or padding, but only if padding is changed
 
3861
                 */
 
3862
                function updateTextPadding() {
 
3863
                        var styles = wrapper.styles,
 
3864
                                textAlign = styles && styles.textAlign,
 
3865
                                x = paddingLeft + padding * (1 - alignFactor),
 
3866
                                y;
 
3867
 
 
3868
                        // determin y based on the baseline
 
3869
                        y = baseline ? 0 : baselineOffset;
 
3870
 
 
3871
                        // compensate for alignment
 
3872
                        if (defined(width) && bBox && (textAlign === 'center' || textAlign === 'right')) {
 
3873
                                x += { center: 0.5, right: 1 }[textAlign] * (width - bBox.width);
 
3874
                        }
 
3875
 
 
3876
                        // update if anything changed
 
3877
                        if (x !== text.x || y !== text.y) {
 
3878
                                text.attr({
 
3879
                                        x: x,
 
3880
                                        y: y
 
3881
                                });
 
3882
                        }
 
3883
 
 
3884
                        // record current values
 
3885
                        text.x = x;
 
3886
                        text.y = y;
 
3887
                }
 
3888
 
 
3889
                /**
 
3890
                 * Set a box attribute, or defer it if the box is not yet created
 
3891
                 * @param {Object} key
 
3892
                 * @param {Object} value
 
3893
                 */
 
3894
                function boxAttr(key, value) {
 
3895
                        if (box) {
 
3896
                                box.attr(key, value);
 
3897
                        } else {
 
3898
                                deferredAttr[key] = value;
 
3899
                        }
 
3900
                }
 
3901
 
 
3902
                /**
 
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.
 
3905
                 */
 
3906
                wrapper.onAdd = function () {
 
3907
                        text.add(wrapper);
 
3908
                        wrapper.attr({
 
3909
                                text: str, // alignment is available now
 
3910
                                x: x,
 
3911
                                y: y
 
3912
                        });
 
3913
 
 
3914
                        if (box && defined(anchorX)) {
 
3915
                                wrapper.attr({
 
3916
                                        anchorX: anchorX,
 
3917
                                        anchorY: anchorY
 
3918
                                });
 
3919
                        }
 
3920
                };
 
3921
 
 
3922
                /*
 
3923
                 * Add specific attribute setters.
 
3924
                 */
 
3925
 
 
3926
                // only change local variables
 
3927
                attrSetters.width = function (value) {
 
3928
                        width = value;
 
3929
                        return false;
 
3930
                };
 
3931
                attrSetters.height = function (value) {
 
3932
                        height = value;
 
3933
                        return false;
 
3934
                };
 
3935
                attrSetters.padding =  function (value) {
 
3936
                        if (defined(value) && value !== padding) {
 
3937
                                padding = value;
 
3938
                                updateTextPadding();
 
3939
                        }
 
3940
                        return false;
 
3941
                };
 
3942
                attrSetters.paddingLeft =  function (value) {
 
3943
                        if (defined(value) && value !== paddingLeft) {
 
3944
                                paddingLeft = value;
 
3945
                                updateTextPadding();
 
3946
                        }
 
3947
                        return false;
 
3948
                };
 
3949
 
 
3950
 
 
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
 
3955
                };
 
3956
 
 
3957
                // apply these to the box and the text alike
 
3958
                attrSetters.text = function (value, key) {
 
3959
                        text.attr(key, value);
 
3960
                        updateBoxSize();
 
3961
                        updateTextPadding();
 
3962
                        return false;
 
3963
                };
 
3964
 
 
3965
                // apply these to the box but not to the text
 
3966
                attrSetters[STROKE_WIDTH] = function (value, key) {
 
3967
                        if (value) {
 
3968
                                needsBox = true;
 
3969
                        }
 
3970
                        crispAdjust = value % 2 / 2;
 
3971
                        boxAttr(key, value);
 
3972
                        return false;
 
3973
                };
 
3974
                attrSetters.stroke = attrSetters.fill = attrSetters.r = function (value, key) {
 
3975
                        if (key === 'fill' && value) {
 
3976
                                needsBox = true;
 
3977
                        }
 
3978
                        boxAttr(key, value);
 
3979
                        return false;
 
3980
                };
 
3981
                attrSetters.anchorX = function (value, key) {
 
3982
                        anchorX = value;
 
3983
                        boxAttr(key, value + crispAdjust - wrapperX);
 
3984
                        return false;
 
3985
                };
 
3986
                attrSetters.anchorY = function (value, key) {
 
3987
                        anchorY = value;
 
3988
                        boxAttr(key, value - wrapperY);
 
3989
                        return false;
 
3990
                };
 
3991
 
 
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);
 
3997
 
 
3998
                        wrapper.attr('translateX', wrapperX);
 
3999
                        return false;
 
4000
                };
 
4001
                attrSetters.y = function (value) {
 
4002
                        wrapperY = wrapper.y = mathRound(value);
 
4003
                        wrapper.attr('translateY', wrapperY);
 
4004
                        return false;
 
4005
                };
 
4006
 
 
4007
                // Redirect certain methods to either the box or the text
 
4008
                var baseCss = wrapper.css;
 
4009
                return extend(wrapper, {
 
4010
                        /**
 
4011
                         * Pick up some properties and apply them to the text instead of the wrapper
 
4012
                         */
 
4013
                        css: function (styles) {
 
4014
                                if (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];
 
4021
                                                }
 
4022
                                        });
 
4023
                                        text.css(textStyles);
 
4024
                                }
 
4025
                                return baseCss.call(wrapper, styles);
 
4026
                        },
 
4027
                        /**
 
4028
                         * Return the bounding box of the box, not the group
 
4029
                         */
 
4030
                        getBBox: function () {
 
4031
                                return {
 
4032
                                        width: bBox.width + 2 * padding,
 
4033
                                        height: bBox.height + 2 * padding,
 
4034
                                        x: bBox.x - padding,
 
4035
                                        y: bBox.y - padding
 
4036
                                };
 
4037
                        },
 
4038
                        /**
 
4039
                         * Apply the shadow to the box
 
4040
                         */
 
4041
                        shadow: function (b) {
 
4042
                                if (box) {
 
4043
                                        box.shadow(b);
 
4044
                                }
 
4045
                                return wrapper;
 
4046
                        },
 
4047
                        /**
 
4048
                         * Destroy and release memory.
 
4049
                         */
 
4050
                        destroy: function () {
 
4051
 
 
4052
                                // Added by button implementation
 
4053
                                removeEvent(wrapper.element, 'mouseenter');
 
4054
                                removeEvent(wrapper.element, 'mouseleave');
 
4055
 
 
4056
                                if (text) {
 
4057
                                        text = text.destroy();
 
4058
                                }
 
4059
                                if (box) {
 
4060
                                        box = box.destroy();
 
4061
                                }
 
4062
                                // Call base implementation to destroy the rest
 
4063
                                SVGElement.prototype.destroy.call(wrapper);
 
4064
 
 
4065
                                // Release local pointers (#1298)
 
4066
                                wrapper = renderer = updateBoxSize = updateTextPadding = boxAttr = null;
 
4067
                        }
 
4068
                });
 
4069
        }
 
4070
}; // end SVGRenderer
 
4071
 
 
4072
 
 
4073
// general renderer
 
4074
Renderer = SVGRenderer;
 
4075
// extend SvgElement for useHTML option
 
4076
extend(SVGElement.prototype, {
 
4077
        /**
 
4078
         * Apply CSS to HTML elements. This is used in text within SVG rendering and
 
4079
         * by the VML renderer
 
4080
         */
 
4081
        htmlCss: function (styles) {
 
4082
                var wrapper = this,
 
4083
                        element = wrapper.element,
 
4084
                        textWidth = styles && element.tagName === 'SPAN' && styles.width;
 
4085
 
 
4086
                if (textWidth) {
 
4087
                        delete styles.width;
 
4088
                        wrapper.textWidth = textWidth;
 
4089
                        wrapper.updateTransform();
 
4090
                }
 
4091
 
 
4092
                wrapper.styles = extend(wrapper.styles, styles);
 
4093
                css(wrapper.element, styles);
 
4094
 
 
4095
                return wrapper;
 
4096
        },
 
4097
 
 
4098
        /**
 
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
 
4102
         *
 
4103
         * @return {Object} A hash containing values for x, y, width and height
 
4104
         */
 
4105
 
 
4106
        htmlGetBBox: function () {
 
4107
                var wrapper = this,
 
4108
                        element = wrapper.element,
 
4109
                        bBox = wrapper.bBox;
 
4110
 
 
4111
                // faking getBBox in exported SVG in legacy IE
 
4112
                if (!bBox) {
 
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;
 
4116
                        }
 
4117
 
 
4118
                        bBox = wrapper.bBox = {
 
4119
                                x: element.offsetLeft,
 
4120
                                y: element.offsetTop,
 
4121
                                width: element.offsetWidth,
 
4122
                                height: element.offsetHeight
 
4123
                        };
 
4124
                }
 
4125
 
 
4126
                return bBox;
 
4127
        },
 
4128
 
 
4129
        /**
 
4130
         * VML override private method to update elements based on internal
 
4131
         * properties based on SVG transform
 
4132
         */
 
4133
        htmlUpdateTransform: function () {
 
4134
                // aligning non added elements is expensive
 
4135
                if (!this.added) {
 
4136
                        this.alignOnAdd = true;
 
4137
                        return;
 
4138
                }
 
4139
 
 
4140
                var wrapper = this,
 
4141
                        renderer = wrapper.renderer,
 
4142
                        elem = wrapper.element,
 
4143
                        translateX = wrapper.translateX || 0,
 
4144
                        translateY = wrapper.translateY || 0,
 
4145
                        x = wrapper.x || 0,
 
4146
                        y = wrapper.y || 0,
 
4147
                        align = wrapper.textAlign || 'left',
 
4148
                        alignCorrection = { left: 0, center: 0.5, right: 1 }[align],
 
4149
                        shadows = wrapper.shadows;
 
4150
 
 
4151
                // apply translate
 
4152
                css(elem, {
 
4153
                        marginLeft: translateX,
 
4154
                        marginTop: translateY
 
4155
                });
 
4156
                if (shadows) { // used in labels/tooltip
 
4157
                        each(shadows, function (shadow) {
 
4158
                                css(shadow, {
 
4159
                                        marginLeft: translateX + 1,
 
4160
                                        marginTop: translateY + 1
 
4161
                                });
 
4162
                        });
 
4163
                }
 
4164
 
 
4165
                // apply inversion
 
4166
                if (wrapper.inverted) { // wrapper is a group
 
4167
                        each(elem.childNodes, function (child) {
 
4168
                                renderer.invertChild(child, elem);
 
4169
                        });
 
4170
                }
 
4171
 
 
4172
                if (elem.tagName === 'SPAN') {
 
4173
 
 
4174
                        var width,
 
4175
                                rotation = wrapper.rotation,
 
4176
                                baseline,
 
4177
                                textWidth = pInt(wrapper.textWidth),
 
4178
                                currentTextTransform = [rotation, align, elem.innerHTML, wrapper.textWidth].join(',');
 
4179
 
 
4180
                        if (currentTextTransform !== wrapper.cTT) { // do the calculations and DOM access only if properties changed
 
4181
 
 
4182
 
 
4183
                                baseline = renderer.fontMetrics(elem.style.fontSize).b;
 
4184
 
 
4185
                                // Renderer specific handling of span rotation
 
4186
                                if (defined(rotation)) {
 
4187
                                        wrapper.setSpanRotation(rotation, alignCorrection, baseline);
 
4188
                                }
 
4189
 
 
4190
                                width = pick(wrapper.elemWidth, elem.offsetWidth);
 
4191
 
 
4192
                                // Update textWidth
 
4193
                                if (width > textWidth && /[ \-]/.test(elem.textContent || elem.innerText)) { // #983, #1254
 
4194
                                        css(elem, {
 
4195
                                                width: textWidth + PX,
 
4196
                                                display: 'block',
 
4197
                                                whiteSpace: 'normal'
 
4198
                                        });
 
4199
                                        width = textWidth;
 
4200
                                }
 
4201
 
 
4202
                                wrapper.getSpanCorrection(width, baseline, alignCorrection, rotation, align);
 
4203
                        }
 
4204
 
 
4205
                        // apply position with correction
 
4206
                        css(elem, {
 
4207
                                left: (x + (wrapper.xCorr || 0)) + PX,
 
4208
                                top: (y + (wrapper.yCorr || 0)) + PX
 
4209
                        });
 
4210
 
 
4211
                        // force reflow in webkit to apply the left and top on useHTML element (#1249)
 
4212
                        if (isWebKit) {
 
4213
                                baseline = elem.offsetHeight; // assigned to baseline for JSLint purpose
 
4214
                        }
 
4215
 
 
4216
                        // record current text transform
 
4217
                        wrapper.cTT = currentTextTransform;
 
4218
                }
 
4219
        },
 
4220
 
 
4221
        /**
 
4222
         * Set the rotation of an individual HTML span
 
4223
         */
 
4224
        setSpanRotation: function (rotation, alignCorrection, baseline) {
 
4225
                var rotationStyle = {},
 
4226
                        cssTransformKey = isIE ? '-ms-transform' : isWebKit ? '-webkit-transform' : isFirefox ? 'MozTransform' : isOpera ? '-o-transform' : '';
 
4227
 
 
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);
 
4231
        },
 
4232
 
 
4233
        /**
 
4234
         * Get the correction in X and Y positioning as the element is rotated.
 
4235
         */
 
4236
        getSpanCorrection: function (width, baseline, alignCorrection) {
 
4237
                this.xCorr = -width * alignCorrection;
 
4238
                this.yCorr = -baseline;
 
4239
        }
 
4240
});
 
4241
 
 
4242
// Extend SvgRenderer for useHTML option.
 
4243
extend(SVGRenderer.prototype, {
 
4244
        /**
 
4245
         * Create HTML text node. This is used by the VML renderer as well as the SVG
 
4246
         * renderer through the useHTML option.
 
4247
         *
 
4248
         * @param {String} str
 
4249
         * @param {Number} x
 
4250
         * @param {Number} y
 
4251
         */
 
4252
        html: function (str, x, y) {
 
4253
                var wrapper = this.createElement('span'),
 
4254
                        attrSetters = wrapper.attrSetters,
 
4255
                        element = wrapper.element,
 
4256
                        renderer = wrapper.renderer;
 
4257
 
 
4258
                // Text setter
 
4259
                attrSetters.text = function (value) {
 
4260
                        if (value !== element.innerHTML) {
 
4261
                                delete this.bBox;
 
4262
                        }
 
4263
                        element.innerHTML = this.textStr = value;
 
4264
                        return false;
 
4265
                };
 
4266
 
 
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.
 
4271
                        }
 
4272
                        wrapper[key] = value;
 
4273
                        wrapper.htmlUpdateTransform();
 
4274
                        
 
4275
                        return false;
 
4276
                };
 
4277
 
 
4278
                // Set the default attributes
 
4279
                wrapper.attr({
 
4280
                                text: str,
 
4281
                                x: mathRound(x),
 
4282
                                y: mathRound(y)
 
4283
                        })
 
4284
                        .css({
 
4285
                                position: ABSOLUTE,
 
4286
                                whiteSpace: 'nowrap',
 
4287
                                fontFamily: this.style.fontFamily,
 
4288
                                fontSize: this.style.fontSize
 
4289
                        });
 
4290
 
 
4291
                // Use the HTML specific .css method
 
4292
                wrapper.css = wrapper.htmlCss;
 
4293
 
 
4294
                // This is specific for HTML within SVG
 
4295
                if (renderer.isSVG) {
 
4296
                        wrapper.add = function (svgGroupWrapper) {
 
4297
 
 
4298
                                var htmlGroup,
 
4299
                                        container = renderer.box.parentNode,
 
4300
                                        parentGroup,
 
4301
                                        parents = [];
 
4302
 
 
4303
                                this.parentGroup = svgGroupWrapper;
 
4304
 
 
4305
                                // Create a mock group to hold the HTML elements
 
4306
                                if (svgGroupWrapper) {
 
4307
                                        htmlGroup = svgGroupWrapper.div;
 
4308
                                        if (!htmlGroup) {
 
4309
 
 
4310
                                                // Read the parent chain into an array and read from top down
 
4311
                                                parentGroup = svgGroupWrapper;
 
4312
                                                while (parentGroup) {
 
4313
 
 
4314
                                                        parents.push(parentGroup);
 
4315
 
 
4316
                                                        // Move up to the next parent group
 
4317
                                                        parentGroup = parentGroup.parentGroup;
 
4318
                                                }
 
4319
 
 
4320
                                                // Ensure dynamically updating position when any parent is translated
 
4321
                                                each(parents.reverse(), function (parentGroup) {
 
4322
                                                        var htmlGroupStyle;
 
4323
 
 
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')
 
4328
                                                        }, {
 
4329
                                                                position: ABSOLUTE,
 
4330
                                                                left: (parentGroup.translateX || 0) + PX,
 
4331
                                                                top: (parentGroup.translateY || 0) + PX
 
4332
                                                        }, htmlGroup || container); // the top group is appended to container
 
4333
 
 
4334
                                                        // Shortcut
 
4335
                                                        htmlGroupStyle = htmlGroup.style;
 
4336
 
 
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;
 
4342
                                                                },
 
4343
                                                                translateY: function (value) {
 
4344
                                                                        htmlGroupStyle.top = value + PX;
 
4345
                                                                },
 
4346
                                                                visibility: function (value, key) {
 
4347
                                                                        htmlGroupStyle[key] = value;
 
4348
                                                                }
 
4349
                                                        });
 
4350
                                                });
 
4351
 
 
4352
                                        }
 
4353
                                } else {
 
4354
                                        htmlGroup = container;
 
4355
                                }
 
4356
 
 
4357
                                htmlGroup.appendChild(element);
 
4358
 
 
4359
                                // Shared with VML:
 
4360
                                wrapper.added = true;
 
4361
                                if (wrapper.alignOnAdd) {
 
4362
                                        wrapper.htmlUpdateTransform();
 
4363
                                }
 
4364
 
 
4365
                                return wrapper;
 
4366
                        };
 
4367
                }
 
4368
                return wrapper;
 
4369
        }
 
4370
});
 
4371
 
 
4372
/* ****************************************************************************
 
4373
 *                                                                            *
 
4374
 * START OF INTERNET EXPLORER <= 8 SPECIFIC CODE                              *
 
4375
 *                                                                            *
 
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.               *
 
4378
 *                                                                            *
 
4379
 *****************************************************************************/
 
4380
 
 
4381
/**
 
4382
 * @constructor
 
4383
 */
 
4384
var VMLRenderer, VMLElement;
 
4385
if (!hasSVG && !useCanVG) {
 
4386
 
 
4387
/**
 
4388
 * The VML element wrapper.
 
4389
 */
 
4390
Highcharts.VMLElement = VMLElement = {
 
4391
 
 
4392
        /**
 
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
 
4397
         */
 
4398
        init: function (renderer, nodeName) {
 
4399
                var wrapper = this,
 
4400
                        markup =  ['<', nodeName, ' filled="f" stroked="f"'],
 
4401
                        style = ['position: ', ABSOLUTE, ';'],
 
4402
                        isDiv = nodeName === DIV;
 
4403
 
 
4404
                // divs and shapes need size
 
4405
                if (nodeName === 'shape' || isDiv) {
 
4406
                        style.push('left:0;top:0;width:1px;height:1px;');
 
4407
                }
 
4408
                style.push('visibility: ', isDiv ? HIDDEN : VISIBLE);
 
4409
 
 
4410
                markup.push(' style="', style.join(''), '"/>');
 
4411
 
 
4412
                // create element with default attributes and style
 
4413
                if (nodeName) {
 
4414
                        markup = isDiv || nodeName === 'span' || nodeName === 'img' ?
 
4415
                                markup.join('')
 
4416
                                : renderer.prepVML(markup);
 
4417
                        wrapper.element = createElement(markup);
 
4418
                }
 
4419
 
 
4420
                wrapper.renderer = renderer;
 
4421
                wrapper.attrSetters = {};
 
4422
        },
 
4423
 
 
4424
        /**
 
4425
         * Add the node to the given parent
 
4426
         * @param {Object} parent
 
4427
         */
 
4428
        add: function (parent) {
 
4429
                var wrapper = this,
 
4430
                        renderer = wrapper.renderer,
 
4431
                        element = wrapper.element,
 
4432
                        box = renderer.box,
 
4433
                        inverted = parent && parent.inverted,
 
4434
 
 
4435
                        // get the parent node
 
4436
                        parentNode = parent ?
 
4437
                                parent.element || parent :
 
4438
                                box;
 
4439
 
 
4440
 
 
4441
                // if the parent group is inverted, apply inversion on all children
 
4442
                if (inverted) { // only on groups
 
4443
                        renderer.invertChild(element, parentNode);
 
4444
                }
 
4445
 
 
4446
                // append it
 
4447
                parentNode.appendChild(element);
 
4448
 
 
4449
                // align text after adding to be able to read offset
 
4450
                wrapper.added = true;
 
4451
                if (wrapper.alignOnAdd && !wrapper.deferUpdateTransform) {
 
4452
                        wrapper.updateTransform();
 
4453
                }
 
4454
 
 
4455
                // fire an event for internal hooks
 
4456
                if (wrapper.onAdd) {
 
4457
                        wrapper.onAdd();
 
4458
                }
 
4459
 
 
4460
                return wrapper;
 
4461
        },
 
4462
 
 
4463
        /**
 
4464
         * VML always uses htmlUpdateTransform
 
4465
         */
 
4466
        updateTransform: SVGElement.prototype.htmlUpdateTransform,
 
4467
 
 
4468
        /**
 
4469
         * Set the rotation of a span with oldIE's filter
 
4470
         */
 
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/
 
4477
 
 
4478
                var rotation = this.rotation,
 
4479
                        costheta = mathCos(rotation * deg2rad),
 
4480
                        sintheta = mathSin(rotation * deg2rad);
 
4481
                                        
 
4482
                css(this.element, {
 
4483
                        filter: rotation ? ['progid:DXImageTransform.Microsoft.Matrix(M11=', costheta,
 
4484
                                ', M12=', -sintheta, ', M21=', sintheta, ', M22=', costheta,
 
4485
                                ', sizingMethod=\'auto expand\')'].join('') : NONE
 
4486
                });
 
4487
        },
 
4488
 
 
4489
        /**
 
4490
         * Get the positioning correction for the span after rotating. 
 
4491
         */
 
4492
        getSpanCorrection: function (width, baseline, alignCorrection, rotation, align) {
 
4493
 
 
4494
                var costheta = rotation ? mathCos(rotation * deg2rad) : 1,
 
4495
                        sintheta = rotation ? mathSin(rotation * deg2rad) : 0,
 
4496
                        height = pick(this.elemHeight, this.element.offsetHeight),
 
4497
                        quad,
 
4498
                        nonLeft = align && align !== 'left';
 
4499
 
 
4500
                // correct x and y
 
4501
                this.xCorr = costheta < 0 && -width;
 
4502
                this.yCorr = sintheta < 0 && -height;
 
4503
 
 
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
 
4509
                if (nonLeft) {
 
4510
                        this.xCorr -= width * alignCorrection * (costheta < 0 ? -1 : 1);
 
4511
                        if (rotation) {
 
4512
                                this.yCorr -= height * alignCorrection * (sintheta < 0 ? -1 : 1);
 
4513
                        }
 
4514
                        css(this.element, {
 
4515
                                textAlign: align
 
4516
                        });
 
4517
                }
 
4518
        },
 
4519
 
 
4520
        /**
 
4521
         * Converts a subset of an SVG path definition to its VML counterpart. Takes an array
 
4522
         * as the parameter and returns a string.
 
4523
         */
 
4524
        pathToVML: function (value) {
 
4525
                // convert paths
 
4526
                var i = value.length,
 
4527
                        path = [];
 
4528
 
 
4529
                while (i--) {
 
4530
 
 
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
 
4537
                                path[i] = 'x';
 
4538
                        } else {
 
4539
                                path[i] = value[i];
 
4540
 
 
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')) {
 
4545
                                        // Start and end X
 
4546
                                        if (path[i + 5] === path[i + 7]) {
 
4547
                                                path[i + 7] += value[i + 7] > value[i + 5] ? 1 : -1;
 
4548
                                        }
 
4549
                                        // Start and end Y
 
4550
                                        if (path[i + 6] === path[i + 8]) {
 
4551
                                                path[i + 8] += value[i + 8] > value[i + 6] ? 1 : -1;
 
4552
                                        }
 
4553
                                }
 
4554
                        }
 
4555
                }
 
4556
 
 
4557
                
 
4558
                // Loop up again to handle path shortcuts (#2132)
 
4559
                /*while (i++ < path.length) {
 
4560
                        if (path[i] === 'H') { // horizontal line to
 
4561
                                path[i] = 'L';
 
4562
                                path.splice(i + 2, 0, path[i - 1]);
 
4563
                        } else if (path[i] === 'V') { // vertical line to
 
4564
                                path[i] = 'L';
 
4565
                                path.splice(i + 1, 0, path[i - 2]);
 
4566
                        }
 
4567
                }*/
 
4568
                return path.join(' ') || 'x';
 
4569
        },
 
4570
 
 
4571
        /**
 
4572
         * Get or set attributes
 
4573
         */
 
4574
        attr: function (hash, val) {
 
4575
                var wrapper = this,
 
4576
                        key,
 
4577
                        value,
 
4578
                        i,
 
4579
                        result,
 
4580
                        element = wrapper.element || {},
 
4581
                        elemStyle = element.style,
 
4582
                        nodeName = element.nodeName,
 
4583
                        renderer = wrapper.renderer,
 
4584
                        symbolName = wrapper.symbolName,
 
4585
                        hasSetSymbolSize,
 
4586
                        shadows = wrapper.shadows,
 
4587
                        skipAttr,
 
4588
                        attrSetters = wrapper.attrSetters,
 
4589
                        ret = wrapper;
 
4590
 
 
4591
                // single key-value pair
 
4592
                if (isString(hash) && defined(val)) {
 
4593
                        key = hash;
 
4594
                        hash = {};
 
4595
                        hash[key] = val;
 
4596
                }
 
4597
 
 
4598
                // used as a getter, val is undefined
 
4599
                if (isString(hash)) {
 
4600
                        key = hash;
 
4601
                        if (key === 'strokeWidth' || key === 'stroke-width') {
 
4602
                                ret = wrapper.strokeweight;
 
4603
                        } else {
 
4604
                                ret = wrapper[key];
 
4605
                        }
 
4606
 
 
4607
                // setter
 
4608
                } else {
 
4609
                        for (key in hash) {
 
4610
                                value = hash[key];
 
4611
                                skipAttr = false;
 
4612
 
 
4613
                                // check for a specific attribute setter
 
4614
                                result = attrSetters[key] && attrSetters[key].call(wrapper, value, key);
 
4615
 
 
4616
                                if (result !== false && value !== null) { // #620
 
4617
 
 
4618
                                        if (result !== UNDEFINED) {
 
4619
                                                value = result; // the attribute setter has returned a new value to set
 
4620
                                        }
 
4621
 
 
4622
 
 
4623
                                        // prepare paths
 
4624
                                        // symbols
 
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
 
4628
                                                // .attr() method
 
4629
                                                if (!hasSetSymbolSize) {
 
4630
                                                        wrapper.symbolAttr(hash);
 
4631
 
 
4632
                                                        hasSetSymbolSize = true;
 
4633
                                                }
 
4634
                                                skipAttr = true;
 
4635
 
 
4636
                                        } else if (key === 'd') {
 
4637
                                                value = value || [];
 
4638
                                                wrapper.d = value.join(' '); // used in getter for animation
 
4639
 
 
4640
                                                element.path = value = wrapper.pathToVML(value);
 
4641
 
 
4642
                                                // update shadows
 
4643
                                                if (shadows) {
 
4644
                                                        i = shadows.length;
 
4645
                                                        while (i--) {
 
4646
                                                                shadows[i].path = shadows[i].cutOff ? this.cutOffPath(value, shadows[i].cutOff) : value;
 
4647
                                                        }
 
4648
                                                }
 
4649
                                                skipAttr = true;
 
4650
 
 
4651
                                        // handle visibility
 
4652
                                        } else if (key === 'visibility') {
 
4653
 
 
4654
                                                // Handle inherited visibility
 
4655
                                                if (value === 'inherit') {
 
4656
                                                        value = VISIBLE;
 
4657
                                                }
 
4658
                                                
 
4659
                                                // Let the shadow follow the main element
 
4660
                                                if (shadows) {
 
4661
                                                        i = shadows.length;
 
4662
                                                        while (i--) {
 
4663
                                                                shadows[i].style[key] = value;
 
4664
                                                        }
 
4665
                                                }
 
4666
 
 
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;
 
4671
 
 
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.
 
4675
                                                        if (!docMode8) {
 
4676
                                                                elemStyle[key] = value ? VISIBLE : HIDDEN;
 
4677
                                                        }
 
4678
                                                        key = 'top';
 
4679
                                                }
 
4680
                                                elemStyle[key] = value;
 
4681
                                                skipAttr = true;
 
4682
 
 
4683
                                        // directly mapped to css
 
4684
                                        } else if (key === 'zIndex') {
 
4685
 
 
4686
                                                if (value) {
 
4687
                                                        elemStyle[key] = value;
 
4688
                                                }
 
4689
                                                skipAttr = true;
 
4690
 
 
4691
                                        // x, y, width, height
 
4692
                                        } else if (inArray(key, ['x', 'y', 'width', 'height']) !== -1) {
 
4693
 
 
4694
                                                wrapper[key] = value; // used in getter
 
4695
 
 
4696
                                                if (key === 'x' || key === 'y') {
 
4697
                                                        key = { x: 'left', y: 'top' }[key];
 
4698
                                                } else {
 
4699
                                                        value = mathMax(0, value); // don't set width or height below zero (#311)
 
4700
                                                }
 
4701
 
 
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();
 
4706
                                                } else {
 
4707
                                                        // normal
 
4708
                                                        elemStyle[key] = value;
 
4709
                                                }
 
4710
 
 
4711
                                                skipAttr = true;
 
4712
 
 
4713
                                        // class name
 
4714
                                        } else if (key === 'class' && nodeName === 'DIV') {
 
4715
                                                // IE8 Standards mode has problems retrieving the className
 
4716
                                                element.className = value;
 
4717
 
 
4718
                                        // stroke
 
4719
                                        } else if (key === 'stroke') {
 
4720
 
 
4721
                                                value = renderer.color(value, element, key);
 
4722
 
 
4723
                                                key = 'strokecolor';
 
4724
 
 
4725
                                        // stroke width
 
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)) {
 
4731
                                                        value += PX;
 
4732
                                                }
 
4733
 
 
4734
                                        // dashStyle
 
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 */
 
4741
                                                skipAttr = true;
 
4742
 
 
4743
                                        // fill
 
4744
                                        } else if (key === 'fill') {
 
4745
 
 
4746
                                                if (nodeName === 'SPAN') { // text color
 
4747
                                                        elemStyle.color = value;
 
4748
                                                } else if (nodeName !== 'IMG') { // #1336
 
4749
                                                        element.filled = value !== NONE ? true : false;
 
4750
 
 
4751
                                                        value = renderer.color(value, element, key, wrapper);
 
4752
 
 
4753
                                                        key = 'fillcolor';
 
4754
                                                }
 
4755
 
 
4756
                                        // opacity: don't bother - animation is too slow and filters introduce artifacts
 
4757
                                        } else if (key === 'opacity') {
 
4758
                                                /*css(element, {
 
4759
                                                        opacity: value
 
4760
                                                });*/
 
4761
                                                skipAttr = true;
 
4762
 
 
4763
                                        // rotation on VML elements
 
4764
                                        } else if (nodeName === 'shape' && key === 'rotation') {
 
4765
 
 
4766
                                                wrapper[key] = element.style[key] = value; // style is for #1873
 
4767
 
 
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;
 
4771
 
 
4772
                                        // translation for animation
 
4773
                                        } else if (key === 'translateX' || key === 'translateY' || key === 'rotation') {
 
4774
                                                wrapper[key] = value;
 
4775
                                                wrapper.updateTransform();
 
4776
 
 
4777
                                                skipAttr = true;
 
4778
 
 
4779
                                        }
 
4780
 
 
4781
 
 
4782
                                        if (!skipAttr) {
 
4783
                                                if (docMode8) { // IE8 setAttribute bug
 
4784
                                                        element[key] = value;
 
4785
                                                } else {
 
4786
                                                        attr(element, key, value);
 
4787
                                                }
 
4788
                                        }
 
4789
 
 
4790
                                }
 
4791
                        }
 
4792
                }
 
4793
                return ret;
 
4794
        },
 
4795
 
 
4796
        /**
 
4797
         * Set the element's clipping to a predefined rectangle
 
4798
         *
 
4799
         * @param {String} id The id of the clip rectangle
 
4800
         */
 
4801
        clip: function (clipRect) {
 
4802
                var wrapper = this,
 
4803
                        clipMembers,
 
4804
                        cssRet;
 
4805
 
 
4806
                if (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);
 
4812
                        };
 
4813
                        cssRet = clipRect.getCSS(wrapper);
 
4814
 
 
4815
                } else {
 
4816
                        if (wrapper.destroyClip) {
 
4817
                                wrapper.destroyClip();
 
4818
                        }
 
4819
                        cssRet = { clip: docMode8 ? 'inherit' : 'rect(auto)' }; // #1214
 
4820
                }
 
4821
 
 
4822
                return wrapper.css(cssRet);
 
4823
 
 
4824
        },
 
4825
 
 
4826
        /**
 
4827
         * Set styles for the element
 
4828
         * @param {Object} styles
 
4829
         */
 
4830
        css: SVGElement.prototype.htmlCss,
 
4831
 
 
4832
        /**
 
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.
 
4835
         */
 
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);
 
4841
                }
 
4842
        },
 
4843
 
 
4844
        /**
 
4845
         * Extend element.destroy by removing it from the clip members array
 
4846
         */
 
4847
        destroy: function () {
 
4848
                if (this.destroyClip) {
 
4849
                        this.destroyClip();
 
4850
                }
 
4851
 
 
4852
                return SVGElement.prototype.destroy.apply(this);
 
4853
        },
 
4854
 
 
4855
        /**
 
4856
         * Add an event listener. VML override for normalizing event parameters.
 
4857
         * @param {String} eventType
 
4858
         * @param {Function} handler
 
4859
         */
 
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;
 
4865
                        handler(evt);
 
4866
                };
 
4867
                return this;
 
4868
        },
 
4869
 
 
4870
        /**
 
4871
         * In stacked columns, cut off the shadows so that they don't overlap
 
4872
         */
 
4873
        cutOffPath: function (path, length) {
 
4874
 
 
4875
                var len;
 
4876
 
 
4877
                path = path.split(/[ ,]/);
 
4878
                len = path.length;
 
4879
 
 
4880
                if (len === 9 || len === 11) {
 
4881
                        path[len - 4] = path[len - 2] = pInt(path[len - 2]) - 10 * length;
 
4882
                }
 
4883
                return path.join(' ');
 
4884
        },
 
4885
 
 
4886
        /**
 
4887
         * Apply a drop shadow by copying elements and giving them different strokes
 
4888
         * @param {Boolean|Object} shadowOptions
 
4889
         */
 
4890
        shadow: function (shadowOptions, group, cutOff) {
 
4891
                var shadows = [],
 
4892
                        i,
 
4893
                        element = this.element,
 
4894
                        renderer = this.renderer,
 
4895
                        shadow,
 
4896
                        elemStyle = element.style,
 
4897
                        markup,
 
4898
                        path = element.path,
 
4899
                        strokeWidth,
 
4900
                        modifiedPath,
 
4901
                        shadowWidth,
 
4902
                        shadowElementOpacity;
 
4903
 
 
4904
                // some times empty paths are not strings
 
4905
                if (path && typeof path.value !== 'string') {
 
4906
                        path = 'x';
 
4907
                }
 
4908
                modifiedPath = path;
 
4909
 
 
4910
                if (shadowOptions) {
 
4911
                        shadowWidth = pick(shadowOptions.width, 3);
 
4912
                        shadowElementOpacity = (shadowOptions.opacity || 0.15) / shadowWidth;
 
4913
                        for (i = 1; i <= 3; i++) {
 
4914
 
 
4915
                                strokeWidth = (shadowWidth * 2) + 1 - (2 * i);
 
4916
 
 
4917
                                // Cut off shadows for stacked column items
 
4918
                                if (cutOff) {
 
4919
                                        modifiedPath = this.cutOffPath(path.value, strokeWidth + 0.5);
 
4920
                                }
 
4921
 
 
4922
                                markup = ['<shape isShadow="true" strokeweight="', strokeWidth,
 
4923
                                        '" filled="false" path="', modifiedPath,
 
4924
                                        '" coordsize="10 10" style="', element.style.cssText, '" />'];
 
4925
 
 
4926
                                shadow = createElement(renderer.prepVML(markup),
 
4927
                                        null, {
 
4928
                                                left: pInt(elemStyle.left) + pick(shadowOptions.offsetX, 1),
 
4929
                                                top: pInt(elemStyle.top) + pick(shadowOptions.offsetY, 1)
 
4930
                                        }
 
4931
                                );
 
4932
                                if (cutOff) {
 
4933
                                        shadow.cutOff = strokeWidth + 1;
 
4934
                                }
 
4935
 
 
4936
                                // apply the opacity
 
4937
                                markup = ['<stroke color="', shadowOptions.color || 'black', '" opacity="', shadowElementOpacity * i, '"/>'];
 
4938
                                createElement(renderer.prepVML(markup), null, null, shadow);
 
4939
 
 
4940
 
 
4941
                                // insert it
 
4942
                                if (group) {
 
4943
                                        group.element.appendChild(shadow);
 
4944
                                } else {
 
4945
                                        element.parentNode.insertBefore(shadow, element);
 
4946
                                }
 
4947
 
 
4948
                                // record it
 
4949
                                shadows.push(shadow);
 
4950
 
 
4951
                        }
 
4952
 
 
4953
                        this.shadows = shadows;
 
4954
                }
 
4955
                return this;
 
4956
 
 
4957
        }
 
4958
};
 
4959
VMLElement = extendClass(SVGElement, VMLElement);
 
4960
 
 
4961
/**
 
4962
 * The VML renderer
 
4963
 */
 
4964
var VMLRendererExtension = { // inherit SVGRenderer
 
4965
 
 
4966
        Element: VMLElement,
 
4967
        isIE8: userAgent.indexOf('MSIE 8.0') > -1,
 
4968
 
 
4969
 
 
4970
        /**
 
4971
         * Initialize the VMLRenderer
 
4972
         * @param {Object} container
 
4973
         * @param {Number} width
 
4974
         * @param {Number} height
 
4975
         */
 
4976
        init: function (container, width, height, style) {
 
4977
                var renderer = this,
 
4978
                        boxWrapper,
 
4979
                        box,
 
4980
                        css;
 
4981
 
 
4982
                renderer.alignedObjects = [];
 
4983
 
 
4984
                boxWrapper = renderer.createElement(DIV)
 
4985
                        .css(extend(this.getStyle(style), { position: RELATIVE}));
 
4986
                box = boxWrapper.element;
 
4987
                container.appendChild(boxWrapper.element);
 
4988
 
 
4989
 
 
4990
                // generate the containing box
 
4991
                renderer.isVML = true;
 
4992
                renderer.box = box;
 
4993
                renderer.boxWrapper = boxWrapper;
 
4994
                renderer.cache = {};
 
4995
 
 
4996
 
 
4997
                renderer.setSize(width, height, false);
 
4998
 
 
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) {
 
5003
 
 
5004
                        doc.namespaces.add('hcv', 'urn:schemas-microsoft-com:vml');
 
5005
 
 
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; } ';
 
5009
                        try {
 
5010
                                doc.createStyleSheet().cssText = css;
 
5011
                        } catch (e) {
 
5012
                                doc.styleSheets[0].cssText += css;
 
5013
                        }
 
5014
 
 
5015
                }
 
5016
        },
 
5017
 
 
5018
 
 
5019
        /**
 
5020
         * Detect whether the renderer is hidden. This happens when one of the parent elements
 
5021
         * has display: none
 
5022
         */
 
5023
        isHidden: function () {
 
5024
                return !this.box.offsetWidth;
 
5025
        },
 
5026
 
 
5027
        /**
 
5028
         * Define a clipping rectangle. In VML it is accomplished by storing the values
 
5029
         * for setting the CSS style to all associated members.
 
5030
         *
 
5031
         * @param {Number} x
 
5032
         * @param {Number} y
 
5033
         * @param {Number} width
 
5034
         * @param {Number} height
 
5035
         */
 
5036
        clipRect: function (x, y, width, height) {
 
5037
 
 
5038
                // create a dummy element
 
5039
                var clipRect = this.createElement(),
 
5040
                        isObj = isObject(x);
 
5041
 
 
5042
                // mimic a rectangle with its style object for automatic updating in attr
 
5043
                return extend(clipRect, {
 
5044
                        members: [],
 
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,
 
5054
                                        rect = this,
 
5055
                                        top = rect.top - (isShape ? element.offsetTop : 0),
 
5056
                                        left = rect.left,
 
5057
                                        right = left + rect.width,
 
5058
                                        bottom = top + rect.height,
 
5059
                                        ret = {
 
5060
                                                clip: 'rect(' +
 
5061
                                                        mathRound(inverted ? left : top) + 'px,' +
 
5062
                                                        mathRound(inverted ? bottom : right) + 'px,' +
 
5063
                                                        mathRound(inverted ? right : bottom) + 'px,' +
 
5064
                                                        mathRound(inverted ? top : left) + 'px)'
 
5065
                                        };
 
5066
 
 
5067
                                // issue 74 workaround
 
5068
                                if (!inverted && docMode8 && nodeName === 'DIV') {
 
5069
                                        extend(ret, {
 
5070
                                                width: right + PX,
 
5071
                                                height: bottom + PX
 
5072
                                        });
 
5073
                                }
 
5074
                                return ret;
 
5075
                        },
 
5076
 
 
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));
 
5081
                                });
 
5082
                        }
 
5083
                });
 
5084
 
 
5085
        },
 
5086
 
 
5087
 
 
5088
        /**
 
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.
 
5091
         *
 
5092
         * @param {Object} color The color or config object
 
5093
         */
 
5094
        color: function (color, elem, prop, wrapper) {
 
5095
                var renderer = this,
 
5096
                        colorObject,
 
5097
                        regexRgba = /^rgba/,
 
5098
                        markup,
 
5099
                        fillType,
 
5100
                        ret = NONE;
 
5101
 
 
5102
                // Check for linear or radial gradient
 
5103
                if (color && color.linearGradient) {
 
5104
                        fillType = 'gradient';
 
5105
                } else if (color && color.radialGradient) {
 
5106
                        fillType = 'pattern';
 
5107
                }
 
5108
 
 
5109
 
 
5110
                if (fillType) {
 
5111
 
 
5112
                        var stopColor,
 
5113
                                stopOpacity,
 
5114
                                gradient = color.linearGradient || color.radialGradient,
 
5115
                                x1,
 
5116
                                y1,
 
5117
                                x2,
 
5118
                                y2,
 
5119
                                opacity1,
 
5120
                                opacity2,
 
5121
                                color1,
 
5122
                                color2,
 
5123
                                fillAttr = '',
 
5124
                                stops = color.stops,
 
5125
                                firstStop,
 
5126
                                lastStop,
 
5127
                                colors = [],
 
5128
                                addFillNode = function () {
 
5129
                                        // Add the fill subnode. When colors attribute is used, the meanings of opacity and o:opacity2
 
5130
                                        // are reversed.
 
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);
 
5134
                                };
 
5135
 
 
5136
                        // Extend from 0 to 1
 
5137
                        firstStop = stops[0];
 
5138
                        lastStop = stops[stops.length - 1];
 
5139
                        if (firstStop[0] > 0) {
 
5140
                                stops.unshift([
 
5141
                                        0,
 
5142
                                        firstStop[1]
 
5143
                                ]);
 
5144
                        }
 
5145
                        if (lastStop[0] < 1) {
 
5146
                                stops.push([
 
5147
                                        1,
 
5148
                                        lastStop[1]
 
5149
                                ]);
 
5150
                        }
 
5151
 
 
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');
 
5158
                                } else {
 
5159
                                        stopColor = stop[1];
 
5160
                                        stopOpacity = 1;
 
5161
                                }
 
5162
 
 
5163
                                // Build the color attribute
 
5164
                                colors.push((stop[0] * 100) + '% ' + stopColor);
 
5165
 
 
5166
                                // Only start and end opacities are allowed, so we use the first and the last
 
5167
                                if (!i) {
 
5168
                                        opacity1 = stopOpacity;
 
5169
                                        color2 = stopColor;
 
5170
                                } else {
 
5171
                                        opacity2 = stopOpacity;
 
5172
                                        color1 = stopColor;
 
5173
                                }
 
5174
                        });
 
5175
 
 
5176
                        // Apply the gradient to fills only.
 
5177
                        if (prop === 'fill') {
 
5178
 
 
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) + '"';
 
5189
 
 
5190
                                        addFillNode();
 
5191
 
 
5192
                                // Radial (circular) gradient
 
5193
                                } else {
 
5194
 
 
5195
                                        var r = gradient.r,
 
5196
                                                sizex = r * 2,
 
5197
                                                sizey = r * 2,
 
5198
                                                cx = gradient.cx,
 
5199
                                                cy = gradient.cy,
 
5200
                                                radialReference = elem.radialReference,
 
5201
                                                bBox,
 
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;
 
5209
                                                        }
 
5210
                                                        fillAttr = 'src="' + defaultOptions.global.VMLRadialGradientURL + '" ' +
 
5211
                                                                'size="' + sizex + ',' + sizey + '" ' +
 
5212
                                                                'origin="0.5,0.5" ' +
 
5213
                                                                'position="' + cx + ',' + cy + '" ' +
 
5214
                                                                'color2="' + color2 + '" ';
 
5215
 
 
5216
                                                        addFillNode();
 
5217
                                                };
 
5218
 
 
5219
                                        // Apply radial gradient
 
5220
                                        if (wrapper.added) {
 
5221
                                                applyRadialGradient();
 
5222
                                        } else {
 
5223
                                                // We need to know the bounding box to get the size and position right
 
5224
                                                wrapper.onAdd = applyRadialGradient;
 
5225
                                        }
 
5226
 
 
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.
 
5229
                                        ret = color1;
 
5230
                                }
 
5231
 
 
5232
                        // Gradients are not supported for VML stroke, return the first color. #722.
 
5233
                        } else {
 
5234
                                ret = stopColor;
 
5235
                        }
 
5236
 
 
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') {
 
5240
 
 
5241
                        colorObject = Color(color);
 
5242
 
 
5243
                        markup = ['<', prop, ' opacity="', colorObject.get('a'), '"/>'];
 
5244
                        createElement(this.prepVML(markup), null, null, elem);
 
5245
 
 
5246
                        ret = colorObject.get('rgb');
 
5247
 
 
5248
 
 
5249
                } else {
 
5250
                        var propNodes = elem.getElementsByTagName(prop); // 'stroke' or 'fill' node
 
5251
                        if (propNodes.length) {
 
5252
                                propNodes[0].opacity = 1;
 
5253
                                propNodes[0].type = 'solid';
 
5254
                        }
 
5255
                        ret = color;
 
5256
                }
 
5257
 
 
5258
                return ret;
 
5259
        },
 
5260
 
 
5261
        /**
 
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
 
5264
         */
 
5265
        prepVML: function (markup) {
 
5266
                var vmlStyle = 'display:inline-block;behavior:url(#default#VML);',
 
5267
                        isIE8 = this.isIE8;
 
5268
 
 
5269
                markup = markup.join('');
 
5270
 
 
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 + '" />');
 
5275
                        } else {
 
5276
                                markup = markup.replace('style="', 'style="' + vmlStyle);
 
5277
                        }
 
5278
 
 
5279
                } else { // add namespace
 
5280
                        markup = markup.replace('<', '<hcv:');
 
5281
                }
 
5282
 
 
5283
                return markup;
 
5284
        },
 
5285
 
 
5286
        /**
 
5287
         * Create rotated and aligned text
 
5288
         * @param {String} str
 
5289
         * @param {Number} x
 
5290
         * @param {Number} y
 
5291
         */
 
5292
        text: SVGRenderer.prototype.html,
 
5293
 
 
5294
        /**
 
5295
         * Create and return a path element
 
5296
         * @param {Array} path
 
5297
         */
 
5298
        path: function (path) {
 
5299
                var attr = {
 
5300
                        // subpixel precision down to 0.1 (width and height = 1px)
 
5301
                        coordsize: '10 10'
 
5302
                };
 
5303
                if (isArray(path)) {
 
5304
                        attr.d = path;
 
5305
                } else if (isObject(path)) { // attributes
 
5306
                        extend(attr, path);
 
5307
                }
 
5308
                // create the shape
 
5309
                return this.createElement('shape').attr(attr);
 
5310
        },
 
5311
 
 
5312
        /**
 
5313
         * Create and return a circle element. In VML circles are implemented as
 
5314
         * shapes, which is faster than v:oval
 
5315
         * @param {Number} x
 
5316
         * @param {Number} y
 
5317
         * @param {Number} r
 
5318
         */
 
5319
        circle: function (x, y, r) {
 
5320
                var circle = this.symbol('circle');
 
5321
                if (isObject(x)) {
 
5322
                        r = x.r;
 
5323
                        y = x.y;
 
5324
                        x = x.x;
 
5325
                }
 
5326
                circle.isCircle = true; // Causes x and y to mean center (#1682)
 
5327
                circle.r = r;
 
5328
                return circle.attr({ x: x, y: y });
 
5329
        },
 
5330
 
 
5331
        /**
 
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.
 
5335
         *
 
5336
         * @param {String} name The name of the group
 
5337
         */
 
5338
        g: function (name) {
 
5339
                var wrapper,
 
5340
                        attribs;
 
5341
 
 
5342
                // set the class name
 
5343
                if (name) {
 
5344
                        attribs = { 'className': PREFIX + name, 'class': PREFIX + name };
 
5345
                }
 
5346
 
 
5347
                // the div to hold HTML and clipping
 
5348
                wrapper = this.createElement(DIV).attr(attribs);
 
5349
 
 
5350
                return wrapper;
 
5351
        },
 
5352
 
 
5353
        /**
 
5354
         * VML override to create a regular HTML image
 
5355
         * @param {String} src
 
5356
         * @param {Number} x
 
5357
         * @param {Number} y
 
5358
         * @param {Number} width
 
5359
         * @param {Number} height
 
5360
         */
 
5361
        image: function (src, x, y, width, height) {
 
5362
                var obj = this.createElement('img')
 
5363
                        .attr({ src: src });
 
5364
 
 
5365
                if (arguments.length > 1) {
 
5366
                        obj.attr({
 
5367
                                x: x,
 
5368
                                y: y,
 
5369
                                width: width,
 
5370
                                height: height
 
5371
                        });
 
5372
                }
 
5373
                return obj;
 
5374
        },
 
5375
 
 
5376
        /**
 
5377
         * For rectangles, VML uses a shape for rect to overcome bugs and rotation problems
 
5378
         */
 
5379
        createElement: function (nodeName) {
 
5380
                return nodeName === 'rect' ? this.symbol(nodeName) : SVGRenderer.prototype.createElement.call(this, nodeName);  
 
5381
        },
 
5382
 
 
5383
        /**
 
5384
         * In the VML renderer, each child of an inverted div (group) is inverted
 
5385
         * @param {Object} element
 
5386
         * @param {Object} parentNode
 
5387
         */
 
5388
        invertChild: function (element, parentNode) {
 
5389
                var ren = this,
 
5390
                        parentStyle = parentNode.style,
 
5391
                        imgStyle = element.tagName === 'IMG' && element.style; // #1111
 
5392
 
 
5393
                css(element, {
 
5394
                        flip: 'x',
 
5395
                        left: pInt(parentStyle.width) - (imgStyle ? pInt(imgStyle.top) : 1),
 
5396
                        top: pInt(parentStyle.height) - (imgStyle ? pInt(imgStyle.left) : 1),
 
5397
                        rotation: -90
 
5398
                });
 
5399
 
 
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);
 
5403
                });
 
5404
        },
 
5405
 
 
5406
        /**
 
5407
         * Symbol definitions that override the parent SVG renderer's symbols
 
5408
         *
 
5409
         */
 
5410
        symbols: {
 
5411
                // VML specific arc function
 
5412
                arc: function (x, y, w, h, options) {
 
5413
                        var start = options.start,
 
5414
                                end = options.end,
 
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),
 
5421
                                ret;
 
5422
 
 
5423
                        if (end - start === 0) { // no angle, don't show it.
 
5424
                                return ['x'];
 
5425
                        }
 
5426
 
 
5427
                        ret = [
 
5428
                                'wa', // clockwise arc to
 
5429
                                x - radius, // left
 
5430
                                y - radius, // top
 
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
 
5437
                        ];
 
5438
 
 
5439
                        if (options.open && !innerRadius) {
 
5440
                                ret.push(
 
5441
                                        'e',
 
5442
                                        M,
 
5443
                                        x,// - innerRadius,
 
5444
                                        y// - innerRadius
 
5445
                                );
 
5446
                        }
 
5447
 
 
5448
                        ret.push(
 
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
 
5458
                                'x', // finish path
 
5459
                                'e' // close
 
5460
                        );
 
5461
 
 
5462
                        ret.isArc = true;
 
5463
                        return ret;
 
5464
 
 
5465
                },
 
5466
                // Add circle symbol path. This performs significantly faster than v:oval.
 
5467
                circle: function (x, y, w, h, wrapper) {
 
5468
 
 
5469
                        if (wrapper) {
 
5470
                                w = h = 2 * wrapper.r;
 
5471
                        }
 
5472
 
 
5473
                        // Center correction, #1682
 
5474
                        if (wrapper && wrapper.isCircle) {
 
5475
                                x -= w / 2;
 
5476
                                y -= h / 2;
 
5477
                        }
 
5478
 
 
5479
                        // Return the path
 
5480
                        return [
 
5481
                                'wa', // clockwisearcto
 
5482
                                x, // left
 
5483
                                y, // top
 
5484
                                x + w, // right
 
5485
                                y + h, // bottom
 
5486
                                x + w, // start x
 
5487
                                y + h / 2,     // start y
 
5488
                                x + w, // end x
 
5489
                                y + h / 2,     // end y
 
5490
                                //'x', // finish path
 
5491
                                'e' // close
 
5492
                        ];
 
5493
                },
 
5494
                /**
 
5495
                 * Add rectangle symbol path which eases rotation and omits arcsize problems
 
5496
                 * compared to the built-in VML roundrect shape
 
5497
                 *
 
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
 
5502
                 */
 
5503
 
 
5504
                rect: function (left, top, width, height, options) {
 
5505
 
 
5506
                        var right = left + width,
 
5507
                                bottom = top + height,
 
5508
                                ret,
 
5509
                                r;
 
5510
 
 
5511
                        // No radius, return the more lightweight square
 
5512
                        if (!defined(options) || !options.r) {
 
5513
                                ret = SVGRenderer.prototype.symbols.square.apply(0, arguments);
 
5514
 
 
5515
                        // Has radius add arcs for the corners
 
5516
                        } else {
 
5517
 
 
5518
                                r = mathMin(options.r, width, height);
 
5519
                                ret = [
 
5520
                                        M,
 
5521
                                        left + r, top,
 
5522
 
 
5523
                                        L,
 
5524
                                        right - r, top,
 
5525
                                        'wa',
 
5526
                                        right - 2 * r, top,
 
5527
                                        right, top + 2 * r,
 
5528
                                        right - r, top,
 
5529
                                        right, top + r,
 
5530
 
 
5531
                                        L,
 
5532
                                        right, bottom - r,
 
5533
                                        'wa',
 
5534
                                        right - 2 * r, bottom - 2 * r,
 
5535
                                        right, bottom,
 
5536
                                        right, bottom - r,
 
5537
                                        right - r, bottom,
 
5538
 
 
5539
                                        L,
 
5540
                                        left + r, bottom,
 
5541
                                        'wa',
 
5542
                                        left, bottom - 2 * r,
 
5543
                                        left + 2 * r, bottom,
 
5544
                                        left + r, bottom,
 
5545
                                        left, bottom - r,
 
5546
 
 
5547
                                        L,
 
5548
                                        left, top + r,
 
5549
                                        'wa',
 
5550
                                        left, top,
 
5551
                                        left + 2 * r, top + 2 * r,
 
5552
                                        left, top + r,
 
5553
                                        left + r, top,
 
5554
 
 
5555
 
 
5556
                                        'x',
 
5557
                                        'e'
 
5558
                                ];
 
5559
                        }
 
5560
                        return ret;
 
5561
                }
 
5562
        }
 
5563
};
 
5564
Highcharts.VMLRenderer = VMLRenderer = function () {
 
5565
        this.init.apply(this, arguments);
 
5566
};
 
5567
VMLRenderer.prototype = merge(SVGRenderer.prototype, VMLRendererExtension);
 
5568
 
 
5569
        // general renderer
 
5570
        Renderer = VMLRenderer;
 
5571
}
 
5572
 
 
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'),
 
5576
                offsetWidth,
 
5577
        textNode = doc.createTextNode(text);
 
5578
 
 
5579
        measuringSpan.appendChild(textNode);
 
5580
        css(measuringSpan, styles);
 
5581
        this.box.appendChild(measuringSpan);
 
5582
        offsetWidth = measuringSpan.offsetWidth;
 
5583
        discardElement(measuringSpan); // #2463
 
5584
        return offsetWidth;
 
5585
};
 
5586
 
 
5587
 
 
5588
/* ****************************************************************************
 
5589
 *                                                                            *
 
5590
 * END OF INTERNET EXPLORER <= 8 SPECIFIC CODE                                *
 
5591
 *                                                                            *
 
5592
 *****************************************************************************/
 
5593
/* ****************************************************************************
 
5594
 *                                                                            *
 
5595
 * START OF ANDROID < 3 SPECIFIC CODE. THIS CAN BE REMOVED IF YOU'RE NOT      *
 
5596
 * TARGETING THAT SYSTEM.                                                     *
 
5597
 *                                                                            *
 
5598
 *****************************************************************************/
 
5599
var CanVGRenderer,
 
5600
        CanVGController;
 
5601
 
 
5602
if (useCanVG) {
 
5603
        /**
 
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.
 
5607
         */
 
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';
 
5611
        };
 
5612
 
 
5613
        /**
 
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.
 
5616
         */
 
5617
        CanVGRenderer.prototype.symbols = {};
 
5618
 
 
5619
        /**
 
5620
         * Handles on demand download of canvg rendering support.
 
5621
         */
 
5622
        CanVGController = (function () {
 
5623
                // List of renderering calls
 
5624
                var deferredRenderCalls = [];
 
5625
 
 
5626
                /**
 
5627
                 * When downloaded, we are ready to draw deferred charts.
 
5628
                 */
 
5629
                function drawDeferred() {
 
5630
                        var callLength = deferredRenderCalls.length,
 
5631
                                callIndex;
 
5632
 
 
5633
                        // Draw all pending render calls
 
5634
                        for (callIndex = 0; callIndex < callLength; callIndex++) {
 
5635
                                deferredRenderCalls[callIndex]();
 
5636
                        }
 
5637
                        // Clear the list
 
5638
                        deferredRenderCalls = [];
 
5639
                }
 
5640
 
 
5641
                return {
 
5642
                        push: function (func, scriptLocation) {
 
5643
                                // Only get the script once
 
5644
                                if (deferredRenderCalls.length === 0) {
 
5645
                                        getScript(scriptLocation, drawDeferred);
 
5646
                                }
 
5647
                                // Register render call
 
5648
                                deferredRenderCalls.push(func);
 
5649
                        }
 
5650
                };
 
5651
        }());
 
5652
 
 
5653
        Renderer = CanVGRenderer;
 
5654
} // end CanVGRenderer
 
5655
 
 
5656
/* ****************************************************************************
 
5657
 *                                                                            *
 
5658
 * END OF ANDROID < 3 SPECIFIC CODE                                           *
 
5659
 *                                                                            *
 
5660
 *****************************************************************************/
 
5661
 
 
5662
/**
 
5663
 * The Tick class
 
5664
 */
 
5665
function Tick(axis, pos, type, noLabel) {
 
5666
        this.axis = axis;
 
5667
        this.pos = pos;
 
5668
        this.type = type || '';
 
5669
        this.isNew = true;
 
5670
 
 
5671
        if (!type && !noLabel) {
 
5672
                this.addLabel();
 
5673
        }
 
5674
}
 
5675
 
 
5676
Tick.prototype = {
 
5677
        /**
 
5678
         * Write the tick label
 
5679
         */
 
5680
        addLabel: function () {
 
5681
                var tick = this,
 
5682
                        axis = tick.axis,
 
5683
                        options = axis.options,
 
5684
                        chart = axis.chart,
 
5685
                        horiz = axis.horiz,
 
5686
                        categories = axis.categories,
 
5687
                        names = axis.names,
 
5688
                        pos = tick.pos,
 
5689
                        labelOptions = options.labels,
 
5690
                        str,
 
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],
 
5699
                        css,
 
5700
                        attr,
 
5701
                        value = categories ?
 
5702
                                pick(categories[pos], names[pos], pos) :
 
5703
                                pos,
 
5704
                        label = tick.label,
 
5705
                        tickPositionInfo = tickPositions.info,
 
5706
                        dateTimeLabelFormat;
 
5707
 
 
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];
 
5712
                }
 
5713
                // set properties for access in render method
 
5714
                tick.isFirst = isFirst;
 
5715
                tick.isLast = isLast;
 
5716
 
 
5717
                // get the string
 
5718
                str = axis.labelFormatter.call({
 
5719
                        axis: axis,
 
5720
                        chart: chart,
 
5721
                        isFirst: isFirst,
 
5722
                        isLast: isLast,
 
5723
                        dateTimeLabelFormat: dateTimeLabelFormat,
 
5724
                        value: axis.isLog ? correctFloat(lin2log(value)) : value
 
5725
                });
 
5726
 
 
5727
                // prepare CSS
 
5728
                css = width && { width: mathMax(1, mathRound(width - 2 * (labelOptions.padding || 10))) + PX };
 
5729
                css = extend(css, labelOptions.style);
 
5730
 
 
5731
                // first call
 
5732
                if (!defined(label)) {
 
5733
                        attr = {
 
5734
                                align: axis.labelAlign
 
5735
                        };
 
5736
                        if (isNumber(labelOptions.rotation)) {
 
5737
                                attr.rotation = labelOptions.rotation;
 
5738
                        }
 
5739
                        if (width && labelOptions.ellipsis) {
 
5740
                                attr._clipHeight = axis.len / tickPositions.length;
 
5741
                        }
 
5742
 
 
5743
                        tick.label =
 
5744
                                defined(str) && labelOptions.enabled ?
 
5745
                                        chart.renderer.text(
 
5746
                                                        str,
 
5747
                                                        0,
 
5748
                                                        0,
 
5749
                                                        labelOptions.useHTML
 
5750
                                                )
 
5751
                                                .attr(attr)
 
5752
                                                // without position absolute, IE export sometimes is wrong
 
5753
                                                .css(css)
 
5754
                                                .add(axis.labelGroup) :
 
5755
                                        null;
 
5756
 
 
5757
                // update
 
5758
                } else if (label) {
 
5759
                        label.attr({
 
5760
                                        text: str
 
5761
                                })
 
5762
                                .css(css);
 
5763
                }
 
5764
        },
 
5765
 
 
5766
        /**
 
5767
         * Get the offset height or width of the label
 
5768
         */
 
5769
        getLabelSize: function () {
 
5770
                var label = this.label,
 
5771
                        axis = this.axis;
 
5772
                return label ?
 
5773
                        label.getBBox()[axis.horiz ? 'height' : 'width'] :
 
5774
                        0;
 
5775
        },
 
5776
 
 
5777
        /**
 
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.
 
5780
         */
 
5781
        getLabelSides: function () {
 
5782
                var bBox = this.label.getBBox(),
 
5783
                        axis = this.axis,
 
5784
                        horiz = axis.horiz,
 
5785
                        options = axis.options,
 
5786
                        labelOptions = options.labels,
 
5787
                        size = horiz ? bBox.width : bBox.height,
 
5788
                        leftSide = horiz ?
 
5789
                                labelOptions.x - size * { left: 0, center: 0.5, right: 1 }[axis.labelAlign] :
 
5790
                                0,
 
5791
                        rightSide = horiz ?
 
5792
                                size + leftSide :
 
5793
                                size;
 
5794
 
 
5795
                return [leftSide, rightSide];
 
5796
        },
 
5797
 
 
5798
        /**
 
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.
 
5801
         */
 
5802
        handleOverflow: function (index, xy) {
 
5803
                var show = true,
 
5804
                        axis = this.axis,
 
5805
                        isFirst = this.isFirst,
 
5806
                        isLast = this.isLast,
 
5807
                        horiz = axis.horiz,
 
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],
 
5814
                        axisLeft,
 
5815
                        axisRight,
 
5816
                        neighbour,
 
5817
                        neighbourEdge,
 
5818
                        line = this.label.line || 0,
 
5819
                        labelEdge = axis.labelEdge,
 
5820
                        justifyLabel = axis.justifyLabels && (isFirst || isLast),
 
5821
                        justifyToPlot;
 
5822
 
 
5823
                // Hide it if it now overlaps the neighbour label
 
5824
                if (labelEdge[line] === UNDEFINED || pxPos + leftSide > labelEdge[line]) {
 
5825
                        labelEdge[line] = pxPos + rightSide;
 
5826
 
 
5827
                } else if (!justifyLabel) {
 
5828
                        show = false;
 
5829
                }
 
5830
 
 
5831
                if (justifyLabel) {
 
5832
                        justifyToPlot = axis.justifyToPlot;
 
5833
                        axisLeft = justifyToPlot ? axis.pos : 0;
 
5834
                        axisRight = justifyToPlot ? axisLeft + axis.len : axis.chart.chartWidth;
 
5835
 
 
5836
                        // Find the firsth neighbour on the same line
 
5837
                        do {
 
5838
                                index += (isFirst ? 1 : -1);
 
5839
                                neighbour = axis.ticks[tickPositions[index]];
 
5840
                        } while (tickPositions[index] && (!neighbour || neighbour.label.line !== line));
 
5841
 
 
5842
                        neighbourEdge = neighbour && neighbour.label.xy && neighbour.label.xy.x + neighbour.getLabelSides()[isFirst ? 0 : 1];
 
5843
 
 
5844
                        if ((isFirst && !reversed) || (isLast && reversed)) {
 
5845
                                // Is the label spilling out to the left of the plot area?
 
5846
                                if (pxPos + leftSide < axisLeft) {
 
5847
 
 
5848
                                        // Align it to plot left
 
5849
                                        pxPos = axisLeft - leftSide;
 
5850
 
 
5851
                                        // Hide it if it now overlaps the neighbour label
 
5852
                                        if (neighbour && pxPos + rightSide > neighbourEdge) {
 
5853
                                                show = false;
 
5854
                                        }
 
5855
                                }
 
5856
 
 
5857
                        } else {
 
5858
                                // Is the label spilling out to the right of the plot area?
 
5859
                                if (pxPos + rightSide > axisRight) {
 
5860
 
 
5861
                                        // Align it to plot right
 
5862
                                        pxPos = axisRight - rightSide;
 
5863
 
 
5864
                                        // Hide it if it now overlaps the neighbour label
 
5865
                                        if (neighbour && pxPos + leftSide < neighbourEdge) {
 
5866
                                                show = false;
 
5867
                                        }
 
5868
 
 
5869
                                }
 
5870
                        }
 
5871
 
 
5872
                        // Set the modified x position of the label
 
5873
                        xy.x = pxPos;
 
5874
                }
 
5875
                return show;
 
5876
        },
 
5877
 
 
5878
        /**
 
5879
         * Get the x and y position for ticks and labels
 
5880
         */
 
5881
        getPosition: function (horiz, pos, tickmarkOffset, old) {
 
5882
                var axis = this.axis,
 
5883
                        chart = axis.chart,
 
5884
                        cHeight = (old && chart.oldChartHeight) || chart.chartHeight;
 
5885
 
 
5886
                return {
 
5887
                        x: horiz ?
 
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),
 
5890
 
 
5891
                        y: horiz ?
 
5892
                                cHeight - axis.bottom + axis.offset - (axis.opposite ? axis.height : 0) :
 
5893
                                cHeight - axis.translate(pos + tickmarkOffset, null, null, old) - axis.transB
 
5894
                };
 
5895
 
 
5896
        },
 
5897
 
 
5898
        /**
 
5899
         * Get the x, y position of the tick label
 
5900
         */
 
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;
 
5908
 
 
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);
 
5913
 
 
5914
                // Correct for rotation (#1764)
 
5915
                if (rotation && axis.side === 2) {
 
5916
                        y -= baseline - baseline * mathCos(rotation * deg2rad);
 
5917
                }
 
5918
 
 
5919
                // Vertically centered
 
5920
                if (!defined(labelOptions.y) && !rotation) { // #1951
 
5921
                        y += baseline - label.getBBox().height / 2;
 
5922
                }
 
5923
 
 
5924
                // Correct for staggered labels
 
5925
                if (staggerLines) {
 
5926
                        label.line = (index / (step || 1) % staggerLines);
 
5927
                        y += label.line * (axis.labelOffset / staggerLines);
 
5928
                }
 
5929
 
 
5930
                return {
 
5931
                        x: x,
 
5932
                        y: y
 
5933
                };
 
5934
        },
 
5935
 
 
5936
        /**
 
5937
         * Extendible method to return the path of the marker
 
5938
         */
 
5939
        getMarkPath: function (x, y, tickLength, tickWidth, horiz, renderer) {
 
5940
                return renderer.crispLine([
 
5941
                                M,
 
5942
                                x,
 
5943
                                y,
 
5944
                                L,
 
5945
                                x + (horiz ? 0 : -tickLength),
 
5946
                                y + (horiz ? tickLength : 0)
 
5947
                        ], tickWidth);
 
5948
        },
 
5949
 
 
5950
        /**
 
5951
         * Put everything in place
 
5952
         *
 
5953
         * @param index {Number}
 
5954
         * @param old {Boolean} Use old coordinates to prepare an animation into new position
 
5955
         */
 
5956
        render: function (index, old, opacity) {
 
5957
                var tick = this,
 
5958
                        axis = tick.axis,
 
5959
                        options = axis.options,
 
5960
                        chart = axis.chart,
 
5961
                        renderer = chart.renderer,
 
5962
                        horiz = axis.horiz,
 
5963
                        type = tick.type,
 
5964
                        label = tick.label,
 
5965
                        pos = tick.pos,
 
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'],
 
5977
                        gridLinePath,
 
5978
                        mark = tick.mark,
 
5979
                        markPath,
 
5980
                        step = labelOptions.step,
 
5981
                        attribs,
 
5982
                        show = true,
 
5983
                        tickmarkOffset = axis.tickmarkOffset,
 
5984
                        xy = tick.getPosition(horiz, pos, tickmarkOffset, old),
 
5985
                        x = xy.x,
 
5986
                        y = xy.y,
 
5987
                        reverseCrisp = ((horiz && x === axis.pos + axis.len) || (!horiz && y === axis.pos)) ? -1 : 1; // #1480, #1687
 
5988
 
 
5989
                this.isActive = true;
 
5990
 
 
5991
                // create the grid line
 
5992
                if (gridLineWidth) {
 
5993
                        gridLinePath = axis.getPlotLinePath(pos + tickmarkOffset, gridLineWidth * reverseCrisp, old, true);
 
5994
 
 
5995
                        if (gridLine === UNDEFINED) {
 
5996
                                attribs = {
 
5997
                                        stroke: gridLineColor,
 
5998
                                        'stroke-width': gridLineWidth
 
5999
                                };
 
6000
                                if (dashStyle) {
 
6001
                                        attribs.dashstyle = dashStyle;
 
6002
                                }
 
6003
                                if (!type) {
 
6004
                                        attribs.zIndex = 1;
 
6005
                                }
 
6006
                                if (old) {
 
6007
                                        attribs.opacity = 0;
 
6008
                                }
 
6009
                                tick.gridLine = gridLine =
 
6010
                                        gridLineWidth ?
 
6011
                                                renderer.path(gridLinePath)
 
6012
                                                        .attr(attribs).add(axis.gridGroup) :
 
6013
                                                null;
 
6014
                        }
 
6015
 
 
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']({
 
6020
                                        d: gridLinePath,
 
6021
                                        opacity: opacity
 
6022
                                });
 
6023
                        }
 
6024
                }
 
6025
 
 
6026
                // create the tick mark
 
6027
                if (tickWidth && tickLength) {
 
6028
 
 
6029
                        // negate the length
 
6030
                        if (tickPosition === 'inside') {
 
6031
                                tickLength = -tickLength;
 
6032
                        }
 
6033
                        if (axis.opposite) {
 
6034
                                tickLength = -tickLength;
 
6035
                        }
 
6036
 
 
6037
                        markPath = tick.getMarkPath(x, y, tickLength, tickWidth * reverseCrisp, horiz, renderer);
 
6038
                        if (mark) { // updating
 
6039
                                mark.animate({
 
6040
                                        d: markPath,
 
6041
                                        opacity: opacity
 
6042
                                });
 
6043
                        } else { // first time
 
6044
                                tick.mark = renderer.path(
 
6045
                                        markPath
 
6046
                                ).attr({
 
6047
                                        stroke: tickColor,
 
6048
                                        'stroke-width': tickWidth,
 
6049
                                        opacity: opacity
 
6050
                                }).add(axis.axisGroup);
 
6051
                        }
 
6052
                }
 
6053
 
 
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);
 
6057
 
 
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))) {
 
6062
                                show = false;
 
6063
 
 
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);
 
6067
                        }
 
6068
 
 
6069
                        // apply step
 
6070
                        if (step && index % step) {
 
6071
                                // show those indices dividable by step
 
6072
                                show = false;
 
6073
                        }
 
6074
 
 
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);
 
6079
                                tick.isNew = false;
 
6080
                        } else {
 
6081
                                label.attr('y', -9999); // #1338
 
6082
                        }
 
6083
                }
 
6084
        },
 
6085
 
 
6086
        /**
 
6087
         * Destructor for the tick prototype
 
6088
         */
 
6089
        destroy: function () {
 
6090
                destroyObjectProperties(this, this.axis);
 
6091
        }
 
6092
};
 
6093
 
 
6094
/**
 
6095
 * The object wrapper for plot lines and plot bands
 
6096
 * @param {Object} options
 
6097
 */
 
6098
Highcharts.PlotLineOrBand = function (axis, options) {
 
6099
        this.axis = axis;
 
6100
 
 
6101
        if (options) {
 
6102
                this.options = options;
 
6103
                this.id = options.id;
 
6104
        }
 
6105
};
 
6106
 
 
6107
Highcharts.PlotLineOrBand.prototype = {
 
6108
        
 
6109
        /**
 
6110
         * Render the plot line or plot band. If it is already existing,
 
6111
         * move it.
 
6112
         */
 
6113
        render: function () {
 
6114
                var plotLine = this,
 
6115
                        axis = plotLine.axis,
 
6116
                        horiz = axis.horiz,
 
6117
                        halfPointRange = (axis.pointRange || 0) / 2,
 
6118
                        options = plotLine.options,
 
6119
                        optionsLabel = options.label,
 
6120
                        label = plotLine.label,
 
6121
                        width = options.width,
 
6122
                        to = options.to,
 
6123
                        from = options.from,
 
6124
                        isBand = defined(from) && defined(to),
 
6125
                        value = options.value,
 
6126
                        dashStyle = options.dashStyle,
 
6127
                        svgElem = plotLine.svgElem,
 
6128
                        path = [],
 
6129
                        addEvent,
 
6130
                        eventType,
 
6131
                        xs,
 
6132
                        ys,
 
6133
                        x,
 
6134
                        y,
 
6135
                        color = options.color,
 
6136
                        zIndex = options.zIndex,
 
6137
                        events = options.events,
 
6138
                        attribs,
 
6139
                        renderer = axis.chart.renderer;
 
6140
 
 
6141
                // logarithmic conversion
 
6142
                if (axis.isLog) {
 
6143
                        from = log2lin(from);
 
6144
                        to = log2lin(to);
 
6145
                        value = log2lin(value);
 
6146
                }
 
6147
 
 
6148
                // plot line
 
6149
                if (width) {
 
6150
                        path = axis.getPlotLinePath(value, width);
 
6151
                        attribs = {
 
6152
                                stroke: color,
 
6153
                                'stroke-width': width
 
6154
                        };
 
6155
                        if (dashStyle) {
 
6156
                                attribs.dashstyle = dashStyle;
 
6157
                        }
 
6158
                } else if (isBand) { // plot band
 
6159
                        
 
6160
                        // keep within plot area
 
6161
                        from = mathMax(from, axis.min - halfPointRange);
 
6162
                        to = mathMin(to, axis.max + halfPointRange);
 
6163
                        
 
6164
                        path = axis.getPlotBandPath(from, to, options);
 
6165
                        attribs = {
 
6166
                                fill: color
 
6167
                        };
 
6168
                        if (options.borderWidth) {
 
6169
                                attribs.stroke = options.borderColor;
 
6170
                                attribs['stroke-width'] = options.borderWidth;
 
6171
                        }
 
6172
                } else {
 
6173
                        return;
 
6174
                }
 
6175
                // zIndex
 
6176
                if (defined(zIndex)) {
 
6177
                        attribs.zIndex = zIndex;
 
6178
                }
 
6179
 
 
6180
                // common for lines and bands
 
6181
                if (svgElem) {
 
6182
                        if (path) {
 
6183
                                svgElem.animate({
 
6184
                                        d: path
 
6185
                                }, null, svgElem.onGetPath);
 
6186
                        } else {
 
6187
                                svgElem.hide();
 
6188
                                svgElem.onGetPath = function () {
 
6189
                                        svgElem.show();
 
6190
                                };
 
6191
                                if (label) {
 
6192
                                        plotLine.label = label = label.destroy();
 
6193
                                }
 
6194
                        }
 
6195
                } else if (path && path.length) {
 
6196
                        plotLine.svgElem = svgElem = renderer.path(path)
 
6197
                                .attr(attribs).add();
 
6198
 
 
6199
                        // events
 
6200
                        if (events) {
 
6201
                                addEvent = function (eventType) {
 
6202
                                        svgElem.on(eventType, function (e) {
 
6203
                                                events[eventType].apply(plotLine, [e]);
 
6204
                                        });
 
6205
                                };
 
6206
                                for (eventType in events) {
 
6207
                                        addEvent(eventType);
 
6208
                                }
 
6209
                        }
 
6210
                }
 
6211
 
 
6212
                // the plot band/line label
 
6213
                if (optionsLabel && defined(optionsLabel.text) && path && path.length && axis.width > 0 && axis.height > 0) {
 
6214
                        // apply defaults
 
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
 
6221
                        }, optionsLabel);
 
6222
 
 
6223
                        // add the SVG element
 
6224
                        if (!label) {
 
6225
                                plotLine.label = label = renderer.text(
 
6226
                                                optionsLabel.text,
 
6227
                                                0,
 
6228
                                                0,
 
6229
                                                optionsLabel.useHTML
 
6230
                                        )
 
6231
                                        .attr({
 
6232
                                                align: optionsLabel.textAlign || optionsLabel.align,
 
6233
                                                rotation: optionsLabel.rotation,
 
6234
                                                zIndex: zIndex
 
6235
                                        })
 
6236
                                        .css(optionsLabel.style)
 
6237
                                        .add();
 
6238
                        }
 
6239
 
 
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])];
 
6243
                        x = arrayMin(xs);
 
6244
                        y = arrayMin(ys);
 
6245
 
 
6246
                        label.align(optionsLabel, false, {
 
6247
                                x: x,
 
6248
                                y: y,
 
6249
                                width: arrayMax(xs) - x,
 
6250
                                height: arrayMax(ys) - y
 
6251
                        });
 
6252
                        label.show();
 
6253
 
 
6254
                } else if (label) { // move out of sight
 
6255
                        label.hide();
 
6256
                }
 
6257
 
 
6258
                // chainable
 
6259
                return plotLine;
 
6260
        },
 
6261
 
 
6262
        /**
 
6263
         * Remove the plot line or band
 
6264
         */
 
6265
        destroy: function () {
 
6266
                // remove it from the lookup
 
6267
                erase(this.axis.plotLinesAndBands, this);
 
6268
                
 
6269
                delete this.axis;
 
6270
                destroyObjectProperties(this);
 
6271
        }
 
6272
};
 
6273
 
 
6274
/**
 
6275
 * Object with members for extending the Axis prototype
 
6276
 */
 
6277
 
 
6278
AxisPlotLineOrBandExtension = {
 
6279
 
 
6280
        /**
 
6281
         * Create the path for a plot band
 
6282
         */ 
 
6283
        getPlotBandPath: function (from, to) {
 
6284
                var toPath = this.getPlotLinePath(to),
 
6285
                        path = this.getPlotLinePath(from);
 
6286
 
 
6287
                if (path && toPath) {
 
6288
                        path.push(
 
6289
                                toPath[4],
 
6290
                                toPath[5],
 
6291
                                toPath[1],
 
6292
                                toPath[2]
 
6293
                        );
 
6294
                } else { // outside the axis area
 
6295
                        path = null;
 
6296
                }
 
6297
                
 
6298
                return path;
 
6299
        },
 
6300
 
 
6301
        addPlotBand: function (options) {
 
6302
                this.addPlotBandOrLine(options, 'plotBands');
 
6303
        },
 
6304
        
 
6305
        addPlotLine: function (options) {
 
6306
                        this.addPlotBandOrLine(options, 'plotLines');
 
6307
        },
 
6308
 
 
6309
        /**
 
6310
         * Add a plot band or plot line after render time
 
6311
         *
 
6312
         * @param options {Object} The plotBand or plotLine configuration object
 
6313
         */
 
6314
        addPlotBandOrLine: function (options, coll) {
 
6315
                var obj = new Highcharts.PlotLineOrBand(this, options).render(),
 
6316
                        userOptions = this.userOptions;
 
6317
 
 
6318
                if (obj) { // #2189
 
6319
                        // Add it to the user options for exporting and Axis.update
 
6320
                        if (coll) {
 
6321
                                userOptions[coll] = userOptions[coll] || [];
 
6322
                                userOptions[coll].push(options); 
 
6323
                        }
 
6324
                        this.plotLinesAndBands.push(obj); 
 
6325
                }
 
6326
                
 
6327
                return obj;
 
6328
        },
 
6329
 
 
6330
        /**
 
6331
         * Remove a plot band or plot line from the chart by id
 
6332
         * @param {Object} id
 
6333
         */
 
6334
        removePlotBandOrLine: function (id) {
 
6335
                var plotLinesAndBands = this.plotLinesAndBands,
 
6336
                        options = this.options,
 
6337
                        userOptions = this.userOptions,
 
6338
                        i = plotLinesAndBands.length;
 
6339
                while (i--) {
 
6340
                        if (plotLinesAndBands[i].id === id) {
 
6341
                                plotLinesAndBands[i].destroy();
 
6342
                        }
 
6343
                }
 
6344
                each([options.plotLines || [], userOptions.plotLines || [], options.plotBands || [], userOptions.plotBands || []], function (arr) {
 
6345
                        i = arr.length;
 
6346
                        while (i--) {
 
6347
                                if (arr[i].id === id) {
 
6348
                                        erase(arr, arr[i]);
 
6349
                                }
 
6350
                        }
 
6351
                });
 
6352
        }
 
6353
};
 
6354
 
 
6355
/**
 
6356
 * Create a new axis object
 
6357
 * @param {Object} chart
 
6358
 * @param {Object} options
 
6359
 */
 
6360
function Axis() {
 
6361
        this.init.apply(this, arguments);
 
6362
}
 
6363
 
 
6364
Axis.prototype = {
 
6365
 
 
6366
        /**
 
6367
         * Default options for the X axis - the Y axis has extended defaults
 
6368
         */
 
6369
        defaultOptions: {
 
6370
                // allowDecimals: null,
 
6371
                // alternateGridColor: null,
 
6372
                // categories: [],
 
6373
                dateTimeLabelFormats: {
 
6374
                        millisecond: '%H:%M:%S.%L',
 
6375
                        second: '%H:%M:%S',
 
6376
                        minute: '%H:%M',
 
6377
                        hour: '%H:%M',
 
6378
                        day: '%e. %b',
 
6379
                        week: '%e. %b',
 
6380
                        month: '%b \'%y',
 
6381
                        year: '%Y'
 
6382
                },
 
6383
                endOnTick: false,
 
6384
                gridLineColor: '#C0C0C0',
 
6385
                // gridLineDashStyle: 'solid',
 
6386
                // gridLineWidth: 0,
 
6387
                // reversed: false,
 
6388
 
 
6389
                labels: defaultLabelOptions,
 
6390
                        // { step: null },
 
6391
                lineColor: '#C0D0E0',
 
6392
                lineWidth: 1,
 
6393
                //linkedTo: null,
 
6394
                //max: undefined,
 
6395
                //min: undefined,
 
6396
                minPadding: 0.01,
 
6397
                maxPadding: 0.01,
 
6398
                //minRange: null,
 
6399
                minorGridLineColor: '#E0E0E0',
 
6400
                // minorGridLineDashStyle: null,
 
6401
                minorGridLineWidth: 1,
 
6402
                minorTickColor: '#A0A0A0',
 
6403
                //minorTickInterval: null,
 
6404
                minorTickLength: 2,
 
6405
                minorTickPosition: 'outside', // inside or outside
 
6406
                //minorTickWidth: 0,
 
6407
                //opposite: false,
 
6408
                //offset: 0,
 
6409
                //plotBands: [{
 
6410
                //      events: {},
 
6411
                //      zIndex: 1,
 
6412
                //      labels: { align, x, verticalAlign, y, style, rotation, textAlign }
 
6413
                //}],
 
6414
                //plotLines: [{
 
6415
                //      events: {}
 
6416
                //  dashStyle: {}
 
6417
                //      zIndex:
 
6418
                //      labels: { align, x, verticalAlign, y, style, rotation, textAlign }
 
6419
                //}],
 
6420
                //reversed: false,
 
6421
                // showFirstLabel: true,
 
6422
                // showLastLabel: true,
 
6423
                startOfWeek: 1,
 
6424
                startOnTick: false,
 
6425
                tickColor: '#C0D0E0',
 
6426
                //tickInterval: null,
 
6427
                tickLength: 5,
 
6428
                tickmarkPlacement: 'between', // on or between
 
6429
                tickPixelInterval: 100,
 
6430
                tickPosition: 'outside',
 
6431
                tickWidth: 1,
 
6432
                title: {
 
6433
                        //text: null,
 
6434
                        align: 'middle', // low, middle or high
 
6435
                        //margin: 0 for horizontal, 10 for vertical axes,
 
6436
                        //rotation: 0,
 
6437
                        //side: 'outside',
 
6438
                        style: {
 
6439
                                color: '#4d759e',
 
6440
                                //font: defaultFont.replace('normal', 'bold')
 
6441
                                fontWeight: 'bold'
 
6442
                        }
 
6443
                        //x: 0,
 
6444
                        //y: 0
 
6445
                },
 
6446
                type: 'linear' // linear, logarithmic or datetime
 
6447
        },
 
6448
 
 
6449
        /**
 
6450
         * This options set extends the defaultOptions for Y axes
 
6451
         */
 
6452
        defaultYAxisOptions: {
 
6453
                endOnTick: true,
 
6454
                gridLineWidth: 1,
 
6455
                tickPixelInterval: 72,
 
6456
                showLastLabel: true,
 
6457
                labels: {
 
6458
                        x: -8,
 
6459
                        y: 3
 
6460
                },
 
6461
                lineWidth: 0,
 
6462
                maxPadding: 0.05,
 
6463
                minPadding: 0.05,
 
6464
                startOnTick: true,
 
6465
                tickWidth: 0,
 
6466
                title: {
 
6467
                        rotation: 270,
 
6468
                        text: 'Values'
 
6469
                },
 
6470
                stackLabels: {
 
6471
                        enabled: false,
 
6472
                        //align: dynamic,
 
6473
                        //y: dynamic,
 
6474
                        //x: dynamic,
 
6475
                        //verticalAlign: dynamic,
 
6476
                        //textAlign: dynamic,
 
6477
                        //rotation: 0,
 
6478
                        formatter: function () {
 
6479
                                return numberFormat(this.total, -1);
 
6480
                        },
 
6481
                        style: defaultLabelOptions.style
 
6482
                }
 
6483
        },
 
6484
 
 
6485
        /**
 
6486
         * These options extend the defaultOptions for left axes
 
6487
         */
 
6488
        defaultLeftAxisOptions: {
 
6489
                labels: {
 
6490
                        x: -8,
 
6491
                        y: null
 
6492
                },
 
6493
                title: {
 
6494
                        rotation: 270
 
6495
                }
 
6496
        },
 
6497
 
 
6498
        /**
 
6499
         * These options extend the defaultOptions for right axes
 
6500
         */
 
6501
        defaultRightAxisOptions: {
 
6502
                labels: {
 
6503
                        x: 8,
 
6504
                        y: null
 
6505
                },
 
6506
                title: {
 
6507
                        rotation: 90
 
6508
                }
 
6509
        },
 
6510
 
 
6511
        /**
 
6512
         * These options extend the defaultOptions for bottom axes
 
6513
         */
 
6514
        defaultBottomAxisOptions: {
 
6515
                labels: {
 
6516
                        x: 0,
 
6517
                        y: 14
 
6518
                        // overflow: undefined,
 
6519
                        // staggerLines: null
 
6520
                },
 
6521
                title: {
 
6522
                        rotation: 0
 
6523
                }
 
6524
        },
 
6525
        /**
 
6526
         * These options extend the defaultOptions for left axes
 
6527
         */
 
6528
        defaultTopAxisOptions: {
 
6529
                labels: {
 
6530
                        x: 0,
 
6531
                        y: -5
 
6532
                        // overflow: undefined
 
6533
                        // staggerLines: null
 
6534
                },
 
6535
                title: {
 
6536
                        rotation: 0
 
6537
                }
 
6538
        },
 
6539
 
 
6540
        /**
 
6541
         * Initialize the axis
 
6542
         */
 
6543
        init: function (chart, userOptions) {
 
6544
 
 
6545
 
 
6546
                var isXAxis = userOptions.isX,
 
6547
                        axis = this;
 
6548
 
 
6549
                // Flag, is the axis horizontal
 
6550
                axis.horiz = chart.inverted ? !isXAxis : isXAxis;
 
6551
 
 
6552
                // Flag, isXAxis
 
6553
                axis.isXAxis = isXAxis;
 
6554
                axis.coll = isXAxis ? 'xAxis' : 'yAxis';
 
6555
 
 
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
 
6560
 
 
6561
                axis.setOptions(userOptions);
 
6562
 
 
6563
 
 
6564
                var options = this.options,
 
6565
                        type = options.type,
 
6566
                        isDatetimeAxis = type === 'datetime';
 
6567
 
 
6568
                axis.labelFormatter = options.labels.formatter || axis.defaultLabelFormatter; // can be overwritten by dynamic format
 
6569
 
 
6570
 
 
6571
                // Flag, stagger lines or not
 
6572
                axis.userOptions = userOptions;
 
6573
 
 
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;
 
6578
 
 
6579
                axis.chart = chart;
 
6580
                axis.reversed = options.reversed;
 
6581
                axis.zoomEnabled = options.zoomEnabled !== false;
 
6582
 
 
6583
                // Initial categories
 
6584
                axis.categories = options.categories || type === 'category';
 
6585
                axis.names = [];
 
6586
 
 
6587
                // Elements
 
6588
                //axis.axisGroup = UNDEFINED;
 
6589
                //axis.gridGroup = UNDEFINED;
 
6590
                //axis.axisTitle = UNDEFINED;
 
6591
                //axis.axisLine = UNDEFINED;
 
6592
 
 
6593
                // Shorthand types
 
6594
                axis.isLog = type === 'logarithmic';
 
6595
                axis.isDatetimeAxis = isDatetimeAxis;
 
6596
 
 
6597
                // Flag, if axis is linked to another axis
 
6598
                axis.isLinked = defined(options.linkedTo);
 
6599
                // Linked axis.
 
6600
                //axis.linkedParent = UNDEFINED;
 
6601
 
 
6602
                // Tick positions
 
6603
                //axis.tickPositions = UNDEFINED; // array containing predefined positions
 
6604
                // Tick intervals
 
6605
                //axis.tickInterval = UNDEFINED;
 
6606
                //axis.minorTickInterval = UNDEFINED;
 
6607
 
 
6608
                axis.tickmarkOffset = (axis.categories && options.tickmarkPlacement === 'between') ? 0.5 : 0;
 
6609
 
 
6610
                // Major ticks
 
6611
                axis.ticks = {};
 
6612
                axis.labelEdge = [];
 
6613
                // Minor ticks
 
6614
                axis.minorTicks = {};
 
6615
                //axis.tickAmount = UNDEFINED;
 
6616
 
 
6617
                // List of plotLines/Bands
 
6618
                axis.plotLinesAndBands = [];
 
6619
 
 
6620
                // Alternate bands
 
6621
                axis.alternateBands = {};
 
6622
 
 
6623
                // Axis metrics
 
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;
 
6633
                axis.len = 0;
 
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;
 
6642
 
 
6643
 
 
6644
                // Dictionary for stacks
 
6645
                axis.stacks = {};
 
6646
                axis.oldStacks = {};
 
6647
                
 
6648
                // Min and max in the data
 
6649
                //axis.dataMin = UNDEFINED,
 
6650
                //axis.dataMax = UNDEFINED,
 
6651
 
 
6652
                // The axis range
 
6653
                axis.max = null;
 
6654
                axis.min = null;
 
6655
 
 
6656
                // User set min and max
 
6657
                //axis.userMin = UNDEFINED,
 
6658
                //axis.userMax = UNDEFINED,
 
6659
 
 
6660
                // Crosshair options
 
6661
                axis.crosshair = pick(options.crosshair, splat(chart.options.tooltip.crosshairs)[isXAxis ? 0 : 1], false);
 
6662
                // Run Axis
 
6663
 
 
6664
                var eventType,
 
6665
                        events = axis.options.events;
 
6666
 
 
6667
                // Register
 
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);
 
6671
                        } else {
 
6672
                                chart.axes.push(axis);
 
6673
                        }
 
6674
 
 
6675
                        chart[axis.coll].push(axis);
 
6676
                }
 
6677
 
 
6678
                axis.series = axis.series || []; // populated by Series
 
6679
 
 
6680
                // inverted charts have reversed xAxes as default
 
6681
                if (chart.inverted && isXAxis && axis.reversed === UNDEFINED) {
 
6682
                        axis.reversed = true;
 
6683
                }
 
6684
 
 
6685
                axis.removePlotBand = axis.removePlotBandOrLine;
 
6686
                axis.removePlotLine = axis.removePlotBandOrLine;
 
6687
 
 
6688
 
 
6689
                // register event listeners
 
6690
                for (eventType in events) {
 
6691
                        addEvent(axis, eventType, events[eventType]);
 
6692
                }
 
6693
 
 
6694
                // extend logarithmic axis
 
6695
                if (axis.isLog) {
 
6696
                        axis.val2lin = log2lin;
 
6697
                        axis.lin2val = lin2log;
 
6698
                }
 
6699
        },
 
6700
 
 
6701
        /**
 
6702
         * Merge and set options
 
6703
         */
 
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],
 
6710
                        merge(
 
6711
                                defaultOptions[this.coll], // if set in setOptions (#1053)
 
6712
                                userOptions
 
6713
                        )
 
6714
                );
 
6715
        },
 
6716
 
 
6717
        /**
 
6718
         * The default label formatter. The context is a special config object for the label.
 
6719
         */
 
6720
        defaultLabelFormatter: function () {
 
6721
                var axis = this.axis,
 
6722
                        value = this.value,
 
6723
                        categories = axis.categories,
 
6724
                        dateTimeLabelFormat = this.dateTimeLabelFormat,
 
6725
                        numericSymbols = defaultOptions.lang.numericSymbols,
 
6726
                        i = numericSymbols && numericSymbols.length,
 
6727
                        multi,
 
6728
                        ret,
 
6729
                        formatOption = axis.options.labels.format,
 
6730
 
 
6731
                        // make sure the same symbol is added for all labels on a linear axis
 
6732
                        numericSymbolDetector = axis.isLog ? value : axis.tickInterval;
 
6733
 
 
6734
                if (formatOption) {
 
6735
                        ret = format(formatOption, this);
 
6736
 
 
6737
                } else if (categories) {
 
6738
                        ret = value;
 
6739
 
 
6740
                } else if (dateTimeLabelFormat) { // datetime axis
 
6741
                        ret = dateFormat(dateTimeLabelFormat, value);
 
6742
 
 
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];
 
6751
                                }
 
6752
                        }
 
6753
                }
 
6754
 
 
6755
                if (ret === UNDEFINED) {
 
6756
                        if (value >= 10000) { // add thousands separators
 
6757
                                ret = numberFormat(value, 0);
 
6758
 
 
6759
                        } else { // small numbers
 
6760
                                ret = numberFormat(value, -1, UNDEFINED, ''); // #2466
 
6761
                        }
 
6762
                }
 
6763
 
 
6764
                return ret;
 
6765
        },
 
6766
 
 
6767
        /**
 
6768
         * Get the minimum and maximum for the series of each axis
 
6769
         */
 
6770
        getSeriesExtremes: function () {
 
6771
                var axis = this,
 
6772
                        chart = axis.chart;
 
6773
 
 
6774
                axis.hasVisibleSeries = false;
 
6775
 
 
6776
                // reset dataMin and dataMax in case we're redrawing
 
6777
                axis.dataMin = axis.dataMax = null;
 
6778
                
 
6779
                if (axis.buildStacks) {
 
6780
                        axis.buildStacks();
 
6781
                }
 
6782
 
 
6783
                // loop through this axis' series
 
6784
                each(axis.series, function (series) {
 
6785
 
 
6786
                        if (series.visible || !chart.options.chart.ignoreHiddenSeries) {
 
6787
 
 
6788
                                var seriesOptions = series.options,
 
6789
                                        xData,
 
6790
                                        threshold = seriesOptions.threshold,
 
6791
                                        seriesDataMin,
 
6792
                                        seriesDataMax;
 
6793
 
 
6794
                                axis.hasVisibleSeries = true;
 
6795
 
 
6796
                                // Validate threshold in logarithmic axes
 
6797
                                if (axis.isLog && threshold <= 0) {
 
6798
                                        threshold = null;
 
6799
                                }
 
6800
 
 
6801
                                // Get dataMin and dataMax for X axes
 
6802
                                if (axis.isXAxis) {
 
6803
                                        xData = series.xData;
 
6804
                                        if (xData.length) {
 
6805
                                                axis.dataMin = mathMin(pick(axis.dataMin, xData[0]), arrayMin(xData));
 
6806
                                                axis.dataMax = mathMax(pick(axis.dataMax, xData[0]), arrayMax(xData));
 
6807
                                        }
 
6808
 
 
6809
                                // Get dataMin and dataMax for Y axes, as well as handle stacking and processed data
 
6810
                                } else {
 
6811
 
 
6812
                                        // Get this particular series extremes
 
6813
                                        series.getExtremes();
 
6814
                                        seriesDataMax = series.dataMax;
 
6815
                                        seriesDataMin = series.dataMin;
 
6816
 
 
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);
 
6823
                                        }
 
6824
 
 
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;
 
6833
                                                }
 
6834
                                        }
 
6835
                                }
 
6836
                        }
 
6837
                });
 
6838
        },
 
6839
 
 
6840
        /**
 
6841
         * Translate from axis value to pixel position on the chart, or back
 
6842
         *
 
6843
         */
 
6844
        translate: function (val, backwards, cvsCoord, old, handleLog, pointPlacement) {
 
6845
                var axis = this,
 
6846
                        sign = 1,
 
6847
                        cvsOffset = 0,
 
6848
                        localA = old ? axis.oldTransA : axis.transA,
 
6849
                        localMin = old ? axis.oldMin : axis.min,
 
6850
                        returnValue,
 
6851
                        minPixelPadding = axis.minPixelPadding,
 
6852
                        postTranslate = (axis.options.ordinal || (axis.isLog && handleLog)) && axis.lin2val;
 
6853
 
 
6854
                if (!localA) {
 
6855
                        localA = axis.transA;
 
6856
                }
 
6857
 
 
6858
                // In vertical axes, the canvas coordinates start from 0 at the top like in
 
6859
                // SVG.
 
6860
                if (cvsCoord) {
 
6861
                        sign *= -1; // canvas coordinates inverts the value
 
6862
                        cvsOffset = axis.len;
 
6863
                }
 
6864
 
 
6865
                // Handle reversed axis
 
6866
                if (axis.reversed) {
 
6867
                        sign *= -1;
 
6868
                        cvsOffset -= sign * (axis.sector || axis.len);
 
6869
                }
 
6870
 
 
6871
                // From pixels to value
 
6872
                if (backwards) { // reverse translation
 
6873
 
 
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);
 
6879
                        }
 
6880
 
 
6881
                // From value to pixels
 
6882
                } else {
 
6883
                        if (postTranslate) { // log and ordinal axes
 
6884
                                val = axis.val2lin(val);
 
6885
                        }
 
6886
                        if (pointPlacement === 'between') {
 
6887
                                pointPlacement = 0.5;
 
6888
                        }
 
6889
                        returnValue = sign * (val - localMin) * localA + cvsOffset + (sign * minPixelPadding) +
 
6890
                                (isNumber(pointPlacement) ? localA * pointPlacement * axis.pointRange : 0);
 
6891
                }
 
6892
 
 
6893
                return returnValue;
 
6894
        },
 
6895
 
 
6896
        /**
 
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.
 
6901
         */
 
6902
        toPixels: function (value, paneCoordinates) {
 
6903
                return this.translate(value, false, !this.horiz, null, true) + (paneCoordinates ? 0 : this.pos);
 
6904
        },
 
6905
 
 
6906
        /*
 
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
 
6910
         *        axis/pane itself.
 
6911
         */
 
6912
        toValue: function (pixel, paneCoordinates) {
 
6913
                return this.translate(pixel - (paneCoordinates ? 0 : this.pos), true, !this.horiz, null, true);
 
6914
        },
 
6915
 
 
6916
        /**
 
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)
 
6922
         */
 
6923
        getPlotLinePath: function (value, lineWidth, old, force, translatedValue) {
 
6924
                var axis = this,
 
6925
                        chart = axis.chart,
 
6926
                        axisLeft = axis.left,
 
6927
                        axisTop = axis.top,
 
6928
                        x1,
 
6929
                        y1,
 
6930
                        x2,
 
6931
                        y2,
 
6932
                        cHeight = (old && chart.oldChartHeight) || chart.chartHeight,
 
6933
                        cWidth = (old && chart.oldChartWidth) || chart.chartWidth,
 
6934
                        skip,
 
6935
                        transB = axis.transB;
 
6936
 
 
6937
                translatedValue = pick(translatedValue, axis.translate(value, null, null, old));
 
6938
                x1 = x2 = mathRound(translatedValue + transB);
 
6939
                y1 = y2 = mathRound(cHeight - translatedValue - transB);
 
6940
 
 
6941
                if (isNaN(translatedValue)) { // no min or max
 
6942
                        skip = true;
 
6943
 
 
6944
                } else if (axis.horiz) {
 
6945
                        y1 = axisTop;
 
6946
                        y2 = cHeight - axis.bottom;
 
6947
                        if (x1 < axisLeft || x1 > axisLeft + axis.width) {
 
6948
                                skip = true;
 
6949
                        }
 
6950
                } else {
 
6951
                        x1 = axisLeft;
 
6952
                        x2 = cWidth - axis.right;
 
6953
 
 
6954
                        if (y1 < axisTop || y1 > axisTop + axis.height) {
 
6955
                                skip = true;
 
6956
                        }
 
6957
                }
 
6958
                return skip && !force ?
 
6959
                        null :
 
6960
                        chart.renderer.crispLine([M, x1, y1, L, x2, y2], lineWidth || 1);
 
6961
        },
 
6962
 
 
6963
        /**
 
6964
         * Set the tick positions of a linear axis to round values like whole tens or every five.
 
6965
         */
 
6966
        getLinearTickPositions: function (tickInterval, min, max) {
 
6967
                var pos,
 
6968
                        lastPos,
 
6969
                        roundedMin = correctFloat(mathFloor(min / tickInterval) * tickInterval),
 
6970
                        roundedMax = correctFloat(mathCeil(max / tickInterval) * tickInterval),
 
6971
                        tickPositions = [];
 
6972
 
 
6973
                // Populate the intermediate values
 
6974
                pos = roundedMin;
 
6975
                while (pos <= roundedMax) {
 
6976
 
 
6977
                        // Place the tick on the rounded value
 
6978
                        tickPositions.push(pos);
 
6979
 
 
6980
                        // Always add the raw tickInterval, not the corrected one.
 
6981
                        pos = correctFloat(pos + tickInterval);
 
6982
 
 
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) {
 
6986
                                break;
 
6987
                        }
 
6988
 
 
6989
                        // Record the last value
 
6990
                        lastPos = pos;
 
6991
                }
 
6992
                return tickPositions;
 
6993
        },
 
6994
 
 
6995
        /**
 
6996
         * Return the minor tick positions. For logarithmic axes, reuse the same logic
 
6997
         * as for major ticks.
 
6998
         */
 
6999
        getMinorTickPositions: function () {
 
7000
                var axis = this,
 
7001
                        options = axis.options,
 
7002
                        tickPositions = axis.tickPositions,
 
7003
                        minorTickInterval = axis.minorTickInterval,
 
7004
                        minorTickPositions = [],
 
7005
                        pos,
 
7006
                        i,
 
7007
                        len;
 
7008
 
 
7009
                if (axis.isLog) {
 
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)
 
7014
                                );
 
7015
                        }
 
7016
                } else if (axis.isDatetimeAxis && options.minorTickInterval === 'auto') { // #1314
 
7017
                        minorTickPositions = minorTickPositions.concat(
 
7018
                                axis.getTimeTicks(
 
7019
                                        axis.normalizeTimeTickInterval(minorTickInterval),
 
7020
                                        axis.min,
 
7021
                                        axis.max,
 
7022
                                        options.startOfWeek
 
7023
                                )
 
7024
                        );
 
7025
                        if (minorTickPositions[0] < axis.min) {
 
7026
                                minorTickPositions.shift();
 
7027
                        }
 
7028
                } else {
 
7029
                        for (pos = axis.min + (tickPositions[0] - axis.min) % minorTickInterval; pos <= axis.max; pos += minorTickInterval) {
 
7030
                                minorTickPositions.push(pos);
 
7031
                        }
 
7032
                }
 
7033
                return minorTickPositions;
 
7034
        },
 
7035
 
 
7036
        /**
 
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.
 
7041
         */
 
7042
        adjustForMinRange: function () {
 
7043
                var axis = this,
 
7044
                        options = axis.options,
 
7045
                        min = axis.min,
 
7046
                        max = axis.max,
 
7047
                        zoomOffset,
 
7048
                        spaceAvailable = axis.dataMax - axis.dataMin >= axis.minRange,
 
7049
                        closestDataRange,
 
7050
                        i,
 
7051
                        distance,
 
7052
                        xData,
 
7053
                        loopLength,
 
7054
                        minArgs,
 
7055
                        maxArgs;
 
7056
 
 
7057
                // Set the automatic minimum range based on the closest point distance
 
7058
                if (axis.isXAxis && axis.minRange === UNDEFINED && !axis.isLog) {
 
7059
 
 
7060
                        if (defined(options.min) || defined(options.max)) {
 
7061
                                axis.minRange = null; // don't do this again
 
7062
 
 
7063
                        } else {
 
7064
 
 
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;
 
7074
                                                }
 
7075
                                        }
 
7076
                                });
 
7077
                                axis.minRange = mathMin(closestDataRange * 5, axis.dataMax - axis.dataMin);
 
7078
                        }
 
7079
                }
 
7080
 
 
7081
                // if minRange is exceeded, adjust
 
7082
                if (max - min < axis.minRange) {
 
7083
                        var minRange = axis.minRange;
 
7084
                        zoomOffset = (minRange - max + min) / 2;
 
7085
 
 
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;
 
7090
                        }
 
7091
                        min = arrayMax(minArgs);
 
7092
 
 
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;
 
7096
                        }
 
7097
 
 
7098
                        max = arrayMin(maxArgs);
 
7099
 
 
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);
 
7105
                        }
 
7106
                }
 
7107
 
 
7108
                // Record modified extremes
 
7109
                axis.min = min;
 
7110
                axis.max = max;
 
7111
        },
 
7112
 
 
7113
        /**
 
7114
         * Update translation information
 
7115
         */
 
7116
        setAxisTranslation: function (saveOld) {
 
7117
                var axis = this,
 
7118
                        range = axis.max - axis.min,
 
7119
                        pointRange = axis.axisPointRange || 0,
 
7120
                        closestPointRange,
 
7121
                        minPointOffset = 0,
 
7122
                        pointRangePadding = 0,
 
7123
                        linkedParent = axis.linkedParent,
 
7124
                        ordinalCorrection,
 
7125
                        hasCategories = !!axis.categories,
 
7126
                        transA = axis.transA;
 
7127
 
 
7128
                // Adjust translation for padding. Y axis with categories need to go through the same (#1784).
 
7129
                if (axis.isXAxis || hasCategories || pointRange) {
 
7130
                        if (linkedParent) {
 
7131
                                minPointOffset = linkedParent.minPointOffset;
 
7132
                                pointRangePadding = linkedParent.pointRangePadding;
 
7133
 
 
7134
                        } else {
 
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;
 
7139
 
 
7140
                                        if (seriesPointRange > range) { // #1446
 
7141
                                                seriesPointRange = 0;
 
7142
                                        }
 
7143
                                        pointRange = mathMax(pointRange, seriesPointRange);
 
7144
 
 
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(
 
7149
                                                minPointOffset,
 
7150
                                                isString(pointPlacement) ? 0 : seriesPointRange / 2
 
7151
                                        );
 
7152
 
 
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(
 
7156
                                                pointRangePadding,
 
7157
                                                pointPlacement === 'on' ? 0 : seriesPointRange
 
7158
                                        );
 
7159
 
 
7160
                                        // Set the closestPointRange
 
7161
                                        if (!series.noSharedTooltip && defined(seriesClosestPointRange)) {
 
7162
                                                closestPointRange = defined(closestPointRange) ?
 
7163
                                                        mathMin(closestPointRange, seriesClosestPointRange) :
 
7164
                                                        seriesClosestPointRange;
 
7165
                                        }
 
7166
                                });
 
7167
                        }
 
7168
 
 
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;
 
7173
 
 
7174
                        // pointRange means the width reserved for each point, like in a column chart
 
7175
                        axis.pointRange = mathMin(pointRange, range);
 
7176
 
 
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;
 
7181
                }
 
7182
 
 
7183
                // Secondary values
 
7184
                if (saveOld) {
 
7185
                        axis.oldTransA = transA;
 
7186
                }
 
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;
 
7190
        },
 
7191
 
 
7192
        /**
 
7193
         * Set the tick positions to round values and optionally extend the extremes
 
7194
         * to the nearest tick
 
7195
         */
 
7196
        setTickPositions: function (secondPass) {
 
7197
                var axis = this,
 
7198
                        chart = axis.chart,
 
7199
                        options = axis.options,
 
7200
                        isLog = axis.isLog,
 
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,
 
7207
                        length,
 
7208
                        linkedParentExtremes,
 
7209
                        tickIntervalOption = options.tickInterval,
 
7210
                        minTickIntervalOption = options.minTickInterval,
 
7211
                        tickPixelIntervalOption = options.tickPixelInterval,
 
7212
                        tickPositions,
 
7213
                        keepTwoTicksOnly,
 
7214
                        categories = axis.categories;
 
7215
 
 
7216
                // linked axis gets the extremes from the parent axis
 
7217
                if (isLinked) {
 
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
 
7224
                        }
 
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);
 
7228
                }
 
7229
 
 
7230
                if (isLog) {
 
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
 
7233
                        }
 
7234
                        axis.min = correctFloat(log2lin(axis.min)); // correctFloat cures #934
 
7235
                        axis.max = correctFloat(log2lin(axis.max));
 
7236
                }
 
7237
 
 
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;
 
7242
 
 
7243
                        axis.range = null;  // don't use it when running setExtremes
 
7244
                }
 
7245
 
 
7246
                // Hook for adjusting this.min and this.max. Used by bubble series.
 
7247
                if (axis.beforePadding) {
 
7248
                        axis.beforePadding();
 
7249
                }
 
7250
 
 
7251
                // adjust min and max for the minimum range
 
7252
                axis.adjustForMinRange();
 
7253
 
 
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;
 
7258
                        if (length) {
 
7259
                                if (!defined(options.min) && !defined(axis.userMin) && minPadding && (axis.dataMin < 0 || !axis.ignoreMinPadding)) {
 
7260
                                        axis.min -= length * minPadding;
 
7261
                                }
 
7262
                                if (!defined(options.max) && !defined(axis.userMax)  && maxPadding && (axis.dataMax > 0 || !axis.ignoreMaxPadding)) {
 
7263
                                        axis.max += length * maxPadding;
 
7264
                                }
 
7265
                        }
 
7266
                }
 
7267
 
 
7268
                // get tickInterval
 
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;
 
7274
                } else {
 
7275
                        axis.tickInterval = pick(
 
7276
                                tickIntervalOption,
 
7277
                                categories ? // for categoried axis, 1 is default, for linear axis use tickPix
 
7278
                                        1 :
 
7279
                                        // don't let it be more than the data range
 
7280
                                        (axis.max - axis.min) * tickPixelIntervalOption / mathMax(axis.len, tickPixelIntervalOption)
 
7281
                        );
 
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
 
7287
                        }
 
7288
                }
 
7289
 
 
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);
 
7295
                        });
 
7296
                }
 
7297
 
 
7298
                // set the translation factor used in translate function
 
7299
                axis.setAxisTranslation(true);
 
7300
 
 
7301
                // hook for ordinal axes and radial axes
 
7302
                if (axis.beforeSetTickPositions) {
 
7303
                        axis.beforeSetTickPositions();
 
7304
                }
 
7305
 
 
7306
                // hook for extensions, used in Highstock ordinal axes
 
7307
                if (axis.postProcessTickInterval) {
 
7308
                        axis.tickInterval = axis.postProcessTickInterval(axis.tickInterval);
 
7309
                }
 
7310
 
 
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);
 
7314
                }
 
7315
 
 
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;
 
7319
                }
 
7320
 
 
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);
 
7325
                        }
 
7326
                }
 
7327
 
 
7328
                // get minorTickInterval
 
7329
                axis.minorTickInterval = options.minorTickInterval === 'auto' && axis.tickInterval ?
 
7330
                                axis.tickInterval / 5 : options.minorTickInterval;
 
7331
 
 
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) {
 
7337
 
 
7338
                        // Too many ticks
 
7339
                        if (!axis.ordinalPositions && (axis.max - axis.min) / axis.tickInterval > mathMax(2 * axis.len, 200)) {
 
7340
                                error(19, true);
 
7341
                        }
 
7342
 
 
7343
                        if (isDatetimeAxis) {
 
7344
                                tickPositions = axis.getTimeTicks(
 
7345
                                        axis.normalizeTimeTickInterval(axis.tickInterval, options.units),
 
7346
                                        axis.min,
 
7347
                                        axis.max,
 
7348
                                        options.startOfWeek,
 
7349
                                        axis.ordinalPositions,
 
7350
                                        axis.closestPointRange,
 
7351
                                        true
 
7352
                                );
 
7353
                        } else if (isLog) {
 
7354
                                tickPositions = axis.getLogTickPositions(axis.tickInterval, axis.min, axis.max);
 
7355
                        } else {
 
7356
                                tickPositions = axis.getLinearTickPositions(axis.tickInterval, axis.min, axis.max);
 
7357
                        }
 
7358
 
 
7359
                        if (keepTwoTicksOnly) {
 
7360
                                tickPositions.splice(1, tickPositions.length - 2);
 
7361
                        }
 
7362
 
 
7363
                        axis.tickPositions = tickPositions;
 
7364
                }
 
7365
 
 
7366
                if (!isLinked) {
 
7367
 
 
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,
 
7372
                                singlePad;
 
7373
 
 
7374
                        if (options.startOnTick) {
 
7375
                                axis.min = roundedMin;
 
7376
                        } else if (axis.min - minPointOffset > roundedMin) {
 
7377
                                tickPositions.shift();
 
7378
                        }
 
7379
 
 
7380
                        if (options.endOnTick) {
 
7381
                                axis.max = roundedMax;
 
7382
                        } else if (axis.max + minPointOffset < roundedMax) {
 
7383
                                tickPositions.pop();
 
7384
                        }
 
7385
 
 
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;
 
7393
                        }
 
7394
                }
 
7395
        },
 
7396
 
 
7397
        /**
 
7398
         * Set the max ticks of either the x and y axis collection
 
7399
         */
 
7400
        setMaxTicks: function () {
 
7401
 
 
7402
                var chart = this.chart,
 
7403
                        maxTicks = chart.maxTicks || {},
 
7404
                        tickPositions = this.tickPositions,
 
7405
                        key = this._maxTicksKey = [this.coll, this.pos, this.len].join('-');
 
7406
 
 
7407
                if (!this.isLinked && !this.isDatetimeAxis && tickPositions && tickPositions.length > (maxTicks[key] || 0) && this.options.alignTicks !== false) {
 
7408
                        maxTicks[key] = tickPositions.length;
 
7409
                }
 
7410
                chart.maxTicks = maxTicks;
 
7411
        },
 
7412
 
 
7413
        /**
 
7414
         * When using multiple axes, adjust the number of ticks to match the highest
 
7415
         * number of ticks in that group
 
7416
         */
 
7417
        adjustTickAmount: function () {
 
7418
                var axis = this,
 
7419
                        chart = axis.chart,
 
7420
                        key = axis._maxTicksKey,
 
7421
                        tickPositions = axis.tickPositions,
 
7422
                        maxTicks = chart.maxTicks;
 
7423
 
 
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,
 
7428
                                tickAmount;
 
7429
 
 
7430
                        // set the axis-level tickAmount to use below
 
7431
                        axis.tickAmount = tickAmount = maxTicks[key];
 
7432
 
 
7433
                        if (calculatedTickAmount < tickAmount) {
 
7434
                                while (tickPositions.length < tickAmount) {
 
7435
                                        tickPositions.push(correctFloat(
 
7436
                                                tickPositions[tickPositions.length - 1] + axis.tickInterval
 
7437
                                        ));
 
7438
                                }
 
7439
                                axis.transA *= (calculatedTickAmount - 1) / (tickAmount - 1);
 
7440
                                axis.max = tickPositions[tickPositions.length - 1];
 
7441
 
 
7442
                        }
 
7443
                        if (defined(oldTickAmount) && tickAmount !== oldTickAmount) {
 
7444
                                axis.isDirty = true;
 
7445
                        }
 
7446
                }
 
7447
        },
 
7448
 
 
7449
        /**
 
7450
         * Set the scale based on data min and max, user set min and max or options
 
7451
         *
 
7452
         */
 
7453
        setScale: function () {
 
7454
                var axis = this,
 
7455
                        stacks = axis.stacks,
 
7456
                        type,
 
7457
                        i,
 
7458
                        isDirtyData,
 
7459
                        isDirtyAxisLength;
 
7460
 
 
7461
                axis.oldMin = axis.min;
 
7462
                axis.oldMax = axis.max;
 
7463
                axis.oldAxisLength = axis.len;
 
7464
 
 
7465
                // set the new axisLength
 
7466
                axis.setAxisSize();
 
7467
                //axisLength = horiz ? axisWidth : axisHeight;
 
7468
                isDirtyAxisLength = axis.len !== axis.oldAxisLength;
 
7469
 
 
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
 
7474
                                isDirtyData = true;
 
7475
                        }
 
7476
                });
 
7477
 
 
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) {
 
7481
 
 
7482
                        // reset stacks
 
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;
 
7488
                                        }
 
7489
                                }
 
7490
                        }
 
7491
 
 
7492
                        axis.forceRedraw = false;
 
7493
 
 
7494
                        // get data extremes if needed
 
7495
                        axis.getSeriesExtremes();
 
7496
 
 
7497
                        // get fixed positions based on tickInterval
 
7498
                        axis.setTickPositions();
 
7499
 
 
7500
                        // record old values to decide whether a rescale is necessary later on (#540)
 
7501
                        axis.oldUserMin = axis.userMin;
 
7502
                        axis.oldUserMax = axis.userMax;
 
7503
 
 
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;
 
7507
                        }
 
7508
                } else if (!axis.isXAxis) {
 
7509
                        if (axis.oldStacks) {
 
7510
                                stacks = axis.stacks = axis.oldStacks;
 
7511
                        }
 
7512
 
 
7513
                        // reset stacks
 
7514
                        for (type in stacks) {
 
7515
                                for (i in stacks[type]) {
 
7516
                                        stacks[type][i].cum = stacks[type][i].total;
 
7517
                                }
 
7518
                        }
 
7519
                }
 
7520
 
 
7521
                // Set the maximum tick amount
 
7522
                axis.setMaxTicks();
 
7523
        },
 
7524
 
 
7525
        /**
 
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
 
7531
         *    configuration
 
7532
         * @param {Object} eventArguments
 
7533
         *
 
7534
         */
 
7535
        setExtremes: function (newMin, newMax, redraw, animation, eventArguments) {
 
7536
                var axis = this,
 
7537
                        chart = axis.chart;
 
7538
 
 
7539
                redraw = pick(redraw, true); // defaults to true
 
7540
 
 
7541
                // Extend the arguments with min and max
 
7542
                eventArguments = extend(eventArguments, {
 
7543
                        min: newMin,
 
7544
                        max: newMax
 
7545
                });
 
7546
 
 
7547
                // Fire the event
 
7548
                fireEvent(axis, 'setExtremes', eventArguments, function () { // the default event handler
 
7549
 
 
7550
                        axis.userMin = newMin;
 
7551
                        axis.userMax = newMax;
 
7552
                        axis.eventArgs = eventArguments;
 
7553
 
 
7554
                        // Mark for running afterSetExtremes
 
7555
                        axis.isDirtyExtremes = true;
 
7556
 
 
7557
                        // redraw
 
7558
                        if (redraw) {
 
7559
                                chart.redraw(animation);
 
7560
                        }
 
7561
                });
 
7562
        },
 
7563
 
 
7564
        /**
 
7565
         * Overridable method for zooming chart. Pulled out in a separate method to allow overriding
 
7566
         * in stock charts.
 
7567
         */
 
7568
        zoom: function (newMin, newMax) {
 
7569
                var dataMin = this.dataMin,
 
7570
                        dataMax = this.dataMax,
 
7571
                        options = this.options;
 
7572
 
 
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))) {
 
7576
                                newMin = UNDEFINED;
 
7577
                        }
 
7578
                        if (defined(dataMax) && newMax >= mathMax(dataMax, pick(options.max, dataMax))) {
 
7579
                                newMax = UNDEFINED;
 
7580
                        }
 
7581
                }
 
7582
 
 
7583
                // In full view, displaying the reset zoom button is not required
 
7584
                this.displayBtn = newMin !== UNDEFINED || newMax !== UNDEFINED;
 
7585
 
 
7586
                // Do it
 
7587
                this.setExtremes(
 
7588
                        newMin,
 
7589
                        newMax,
 
7590
                        false,
 
7591
                        UNDEFINED,
 
7592
                        { trigger: 'zoom' }
 
7593
                );
 
7594
                return true;
 
7595
        },
 
7596
 
 
7597
        /**
 
7598
         * Update the axis metrics
 
7599
         */
 
7600
        setAxisSize: function () {
 
7601
                var chart = this.chart,
 
7602
                        options = this.options,
 
7603
                        offsetLeft = options.offsetLeft || 0,
 
7604
                        offsetRight = options.offsetRight || 0,
 
7605
                        horiz = this.horiz,
 
7606
                        width,
 
7607
                        height,
 
7608
                        top,
 
7609
                        left;
 
7610
 
 
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;
 
7618
 
 
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
 
7622
        },
 
7623
 
 
7624
        /**
 
7625
         * Get the actual axis extremes
 
7626
         */
 
7627
        getExtremes: function () {
 
7628
                var axis = this,
 
7629
                        isLog = axis.isLog;
 
7630
 
 
7631
                return {
 
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
 
7638
                };
 
7639
        },
 
7640
 
 
7641
        /**
 
7642
         * Get the zero plane either based on zero or on the min or max value.
 
7643
         * Used in bar and area plots
 
7644
         */
 
7645
        getThreshold: function (threshold) {
 
7646
                var axis = this,
 
7647
                        isLog = axis.isLog;
 
7648
 
 
7649
                var realMin = isLog ? lin2log(axis.min) : axis.min,
 
7650
                        realMax = isLog ? lin2log(axis.max) : axis.max;
 
7651
 
 
7652
                if (realMin > threshold || threshold === null) {
 
7653
                        threshold = realMin;
 
7654
                } else if (realMax < threshold) {
 
7655
                        threshold = realMax;
 
7656
                }
 
7657
 
 
7658
                return axis.translate(threshold, 0, 1, 0, 1);
 
7659
        },
 
7660
 
 
7661
        /**
 
7662
         * Compute auto alignment for the axis label based on which side the axis is on
 
7663
         * and the given rotation for the label
 
7664
         */
 
7665
        autoLabelAlign: function (rotation) {
 
7666
                var ret,
 
7667
                        angle = (pick(rotation, 0) - (this.side * 90) + 720) % 360;
 
7668
 
 
7669
                if (angle > 15 && angle < 165) {
 
7670
                        ret = 'right';
 
7671
                } else if (angle > 195 && angle < 345) {
 
7672
                        ret = 'left';
 
7673
                } else {
 
7674
                        ret = 'center';
 
7675
                }
 
7676
                return ret;
 
7677
        },
 
7678
 
 
7679
        /**
 
7680
         * Render the tick labels to a preliminary position to get their sizes
 
7681
         */
 
7682
        getOffset: function () {
 
7683
                var axis = this,
 
7684
                        chart = axis.chart,
 
7685
                        renderer = chart.renderer,
 
7686
                        options = axis.options,
 
7687
                        tickPositions = axis.tickPositions,
 
7688
                        ticks = axis.ticks,
 
7689
                        horiz = axis.horiz,
 
7690
                        side = axis.side,
 
7691
                        invertedSide = chart.inverted ? [1, 0, 3, 2][side] : side,
 
7692
                        hasData,
 
7693
                        showAxis,
 
7694
                        titleOffset = 0,
 
7695
                        titleOffsetOption,
 
7696
                        titleMargin = 0,
 
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],
 
7703
                        n,
 
7704
                        i,
 
7705
                        autoStaggerLines = 1,
 
7706
                        maxStaggerLines = pick(labelOptions.maxStaggerLines, 5),
 
7707
                        sortedPositions,
 
7708
                        lastRight,
 
7709
                        overlap,
 
7710
                        pos,
 
7711
                        bBox,
 
7712
                        x,
 
7713
                        w,
 
7714
                        lineNo;
 
7715
 
 
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);
 
7719
 
 
7720
                // Set/reset staggerLines
 
7721
                axis.staggerLines = axis.horiz && labelOptions.staggerLines;
 
7722
 
 
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 })
 
7727
                                .add();
 
7728
                        axis.axisGroup = renderer.g('axis')
 
7729
                                .attr({ zIndex: options.zIndex || 2 })
 
7730
                                .add();
 
7731
                        axis.labelGroup = renderer.g('axis-labels')
 
7732
                                .attr({ zIndex: labelOptions.zIndex || 7 })
 
7733
                                .addClass(PREFIX + axis.coll.toLowerCase() + '-labels')
 
7734
                                .add();
 
7735
                }
 
7736
 
 
7737
                if (hasData || axis.isLinked) {
 
7738
 
 
7739
                        // Set the explicit or automatic label alignment
 
7740
                        axis.labelAlign = pick(labelOptions.align || axis.autoLabelAlign(labelOptions.rotation));
 
7741
 
 
7742
                        // Generate ticks
 
7743
                        each(tickPositions, function (pos) {
 
7744
                                if (!ticks[pos]) {
 
7745
                                        ticks[pos] = new Tick(axis, pos);
 
7746
                                } else {
 
7747
                                        ticks[pos].addLabel(); // update labels depending on tick interval
 
7748
                                }
 
7749
                        });
 
7750
 
 
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) {
 
7755
                                        lastRight = [];
 
7756
                                        overlap = false;
 
7757
 
 
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;
 
7763
 
 
7764
                                                if (w) {
 
7765
                                                        x = axis.translate(pos); // don't handle log
 
7766
                                                        if (lastRight[lineNo] !== UNDEFINED && x < lastRight[lineNo]) {
 
7767
                                                                overlap = true;
 
7768
                                                        }
 
7769
                                                        lastRight[lineNo] = x + w;
 
7770
                                                }
 
7771
                                        }
 
7772
                                        if (overlap) {
 
7773
                                                autoStaggerLines++;
 
7774
                                        } else {
 
7775
                                                break;
 
7776
                                        }
 
7777
                                }
 
7778
 
 
7779
                                if (autoStaggerLines > 1) {
 
7780
                                        axis.staggerLines = autoStaggerLines;
 
7781
                                }
 
7782
                        }
 
7783
 
 
7784
 
 
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) {
 
7788
 
 
7789
                                        // get the highest offset
 
7790
                                        labelOffset = mathMax(
 
7791
                                                ticks[pos].getLabelSize(),
 
7792
                                                labelOffset
 
7793
                                        );
 
7794
                                }
 
7795
 
 
7796
                        });
 
7797
                        if (axis.staggerLines) {
 
7798
                                labelOffset *= axis.staggerLines;
 
7799
                                axis.labelOffset = labelOffset;
 
7800
                        }
 
7801
 
 
7802
 
 
7803
                } else { // doesn't have data
 
7804
                        for (n in ticks) {
 
7805
                                ticks[n].destroy();
 
7806
                                delete ticks[n];
 
7807
                        }
 
7808
                }
 
7809
 
 
7810
                if (axisTitleOptions && axisTitleOptions.text && axisTitleOptions.enabled !== false) {
 
7811
                        if (!axis.axisTitle) {
 
7812
                                axis.axisTitle = renderer.text(
 
7813
                                        axisTitleOptions.text,
 
7814
                                        0,
 
7815
                                        0,
 
7816
                                        axisTitleOptions.useHTML
 
7817
                                )
 
7818
                                .attr({
 
7819
                                        zIndex: 7,
 
7820
                                        rotation: axisTitleOptions.rotation || 0,
 
7821
                                        align:
 
7822
                                                axisTitleOptions.textAlign ||
 
7823
                                                { low: 'left', middle: 'center', high: 'right' }[axisTitleOptions.align]
 
7824
                                })
 
7825
                                .addClass(PREFIX + this.coll.toLowerCase() + '-title')
 
7826
                                .css(axisTitleOptions.style)
 
7827
                                .add(axis.axisGroup);
 
7828
                                axis.axisTitle.isNew = true;
 
7829
                        }
 
7830
 
 
7831
                        if (showAxis) {
 
7832
                                titleOffset = axis.axisTitle.getBBox()[horiz ? 'height' : 'width'];
 
7833
                                titleMargin = pick(axisTitleOptions.margin, horiz ? 5 : 10);
 
7834
                                titleOffsetOption = axisTitleOptions.offset;
 
7835
                        }
 
7836
 
 
7837
                        // hide or show the title depending on whether showEmpty is set
 
7838
                        axis.axisTitle[showAxis ? 'show' : 'hide']();
 
7839
                }
 
7840
 
 
7841
                // handle automatic or user set offset
 
7842
                axis.offset = directionFactor * pick(options.offset, axisOffset[side]);
 
7843
 
 
7844
                axis.axisTitleMargin =
 
7845
                        pick(titleOffsetOption,
 
7846
                                labelOffset + titleMargin +
 
7847
                                (side !== 2 && labelOffset && directionFactor * options.labels[horiz ? 'y' : 'x'])
 
7848
                        );
 
7849
 
 
7850
                axisOffset[side] = mathMax(
 
7851
                        axisOffset[side],
 
7852
                        axis.axisTitleMargin + titleOffset + directionFactor * axis.offset
 
7853
                );
 
7854
                clipOffset[invertedSide] = mathMax(clipOffset[invertedSide], mathFloor(options.lineWidth / 2) * 2);
 
7855
        },
 
7856
 
 
7857
        /**
 
7858
         * Get the path for the axis line
 
7859
         */
 
7860
        getLinePath: function (lineWidth) {
 
7861
                var chart = this.chart,
 
7862
                        opposite = this.opposite,
 
7863
                        offset = this.offset,
 
7864
                        horiz = this.horiz,
 
7865
                        lineLeft = this.left + (opposite ? this.width : 0) + offset,
 
7866
                        lineTop = chart.chartHeight - this.bottom - (opposite ? this.height : 0) + offset;
 
7867
 
 
7868
                if (opposite) {
 
7869
                        lineWidth *= -1; // crispify the other way - #1480, #1687
 
7870
                }
 
7871
 
 
7872
                return chart.renderer.crispLine([
 
7873
                                M,
 
7874
                                horiz ?
 
7875
                                        this.left :
 
7876
                                        lineLeft,
 
7877
                                horiz ?
 
7878
                                        lineTop :
 
7879
                                        this.top,
 
7880
                                L,
 
7881
                                horiz ?
 
7882
                                        chart.chartWidth - this.right :
 
7883
                                        lineLeft,
 
7884
                                horiz ?
 
7885
                                        lineTop :
 
7886
                                        chart.chartHeight - this.bottom
 
7887
                        ], lineWidth);
 
7888
        },
 
7889
 
 
7890
        /**
 
7891
         * Position the title
 
7892
         */
 
7893
        getTitlePosition: function () {
 
7894
                // compute anchor points for each of the title align options
 
7895
                var horiz = this.horiz,
 
7896
                        axisLeft = this.left,
 
7897
                        axisTop = this.top,
 
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),
 
7904
 
 
7905
                        // the position in the length direction of the axis
 
7906
                        alongAxis = {
 
7907
                                low: margin + (horiz ? 0 : axisLength),
 
7908
                                middle: margin + axisLength / 2,
 
7909
                                high: margin + (horiz ? axisLength : 0)
 
7910
                        }[axisTitleOptions.align],
 
7911
 
 
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);
 
7918
 
 
7919
                return {
 
7920
                        x: horiz ?
 
7921
                                alongAxis :
 
7922
                                offAxis + (opposite ? this.width : 0) + offset +
 
7923
                                        (axisTitleOptions.x || 0), // x
 
7924
                        y: horiz ?
 
7925
                                offAxis - (opposite ? this.height : 0) + offset :
 
7926
                                alongAxis + (axisTitleOptions.y || 0) // y
 
7927
                };
 
7928
        },
 
7929
 
 
7930
        /**
 
7931
         * Render the axis
 
7932
         */
 
7933
        render: function () {
 
7934
                var axis = this,
 
7935
                        horiz = axis.horiz,
 
7936
                        reversed = axis.reversed,
 
7937
                        chart = axis.chart,
 
7938
                        renderer = chart.renderer,
 
7939
                        options = axis.options,
 
7940
                        isLog = axis.isLog,
 
7941
                        isLinked = axis.isLinked,
 
7942
                        tickPositions = axis.tickPositions,
 
7943
                        sortedPositions,
 
7944
                        axisTitle = axis.axisTitle,                     
 
7945
                        ticks = axis.ticks,
 
7946
                        minorTicks = axis.minorTicks,
 
7947
                        alternateBands = axis.alternateBands,
 
7948
                        stackLabelOptions = options.stackLabels,
 
7949
                        alternateGridColor = options.alternateGridColor,
 
7950
                        tickmarkOffset = axis.tickmarkOffset,
 
7951
                        lineWidth = options.lineWidth,
 
7952
                        linePath,
 
7953
                        hasRendered = chart.hasRendered,
 
7954
                        slideInTicks = hasRendered && defined(axis.oldMin) && !isNaN(axis.oldMin),
 
7955
                        hasData = axis.hasData,
 
7956
                        showAxis = axis.showAxis,
 
7957
                        from,
 
7958
                        overflow = options.labels.overflow,
 
7959
                        justifyLabels = axis.justifyLabels = horiz && overflow !== false,
 
7960
                        to;
 
7961
 
 
7962
                // Reset
 
7963
                axis.labelEdge.length = 0;
 
7964
                axis.justifyToPlot = overflow === 'justify';
 
7965
 
 
7966
                // Mark all elements inActive before we go over and mark the active ones
 
7967
                each([ticks, minorTicks, alternateBands], function (coll) {
 
7968
                        var pos;
 
7969
                        for (pos in coll) {
 
7970
                                coll[pos].isActive = false;
 
7971
                        }
 
7972
                });
 
7973
 
 
7974
                // If the series has data draw the ticks. Else only the line and title
 
7975
                if (hasData || isLinked) {
 
7976
 
 
7977
                        // minor ticks
 
7978
                        if (axis.minorTickInterval && !axis.categories) {
 
7979
                                each(axis.getMinorTickPositions(), function (pos) {
 
7980
                                        if (!minorTicks[pos]) {
 
7981
                                                minorTicks[pos] = new Tick(axis, pos, 'minor');
 
7982
                                        }
 
7983
 
 
7984
                                        // render new ticks in old position
 
7985
                                        if (slideInTicks && minorTicks[pos].isNew) {
 
7986
                                                minorTicks[pos].render(null, true);
 
7987
                                        }
 
7988
 
 
7989
                                        minorTicks[pos].render(null, false, 1);
 
7990
                                });
 
7991
                        }
 
7992
 
 
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();
 
7999
                                }
 
8000
                                if (justifyLabels) {
 
8001
                                        sortedPositions = sortedPositions.slice(1).concat([sortedPositions[0]]);
 
8002
                                }
 
8003
                                each(sortedPositions, function (pos, i) {
 
8004
 
 
8005
                                        // Reorganize the indices
 
8006
                                        if (justifyLabels) {
 
8007
                                                i = (i === sortedPositions.length - 1) ? 0 : i + 1;
 
8008
                                        }
 
8009
 
 
8010
                                        // linked axes need an extra check to find out if
 
8011
                                        if (!isLinked || (pos >= axis.min && pos <= axis.max)) {
 
8012
 
 
8013
                                                if (!ticks[pos]) {
 
8014
                                                        ticks[pos] = new Tick(axis, pos);
 
8015
                                                }
 
8016
 
 
8017
                                                // render new ticks in old position
 
8018
                                                if (slideInTicks && ticks[pos].isNew) {
 
8019
                                                        ticks[pos].render(i, true, 0.1);
 
8020
                                                }
 
8021
 
 
8022
                                                ticks[pos].render(i, false, 1);
 
8023
                                        }
 
8024
 
 
8025
                                });
 
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) {
 
8029
                                        if (!ticks[-1]) {
 
8030
                                                ticks[-1] = new Tick(axis, -1, null, true);
 
8031
                                        }
 
8032
                                        ticks[-1].render(-1);
 
8033
                                }
 
8034
 
 
8035
                        }
 
8036
 
 
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);
 
8043
                                                }
 
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
 
8050
                                                };
 
8051
                                                alternateBands[pos].render();
 
8052
                                                alternateBands[pos].isActive = true;
 
8053
                                        }
 
8054
                                });
 
8055
                        }
 
8056
 
 
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);
 
8061
                                });
 
8062
                                axis._addedPlotLB = true;
 
8063
                        }
 
8064
 
 
8065
                } // end if hasData
 
8066
 
 
8067
                // Remove inactive ticks
 
8068
                each([ticks, minorTicks, alternateBands], function (coll) {
 
8069
                        var pos,
 
8070
                                i,
 
8071
                                forDestruction = [],
 
8072
                                delay = globalAnimation ? globalAnimation.duration || 500 : 0,
 
8073
                                destroyInactiveItems = function () {
 
8074
                                        i = forDestruction.length;
 
8075
                                        while (i--) {
 
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]];
 
8081
                                                }
 
8082
                                        }
 
8083
 
 
8084
                                };
 
8085
 
 
8086
                        for (pos in coll) {
 
8087
 
 
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);
 
8093
                                }
 
8094
                        }
 
8095
 
 
8096
                        // When the objects are finished fading out, destroy them
 
8097
                        if (coll === alternateBands || !chart.hasRendered || !delay) {
 
8098
                                destroyInactiveItems();
 
8099
                        } else if (delay) {
 
8100
                                setTimeout(destroyInactiveItems, delay);
 
8101
                        }
 
8102
                });
 
8103
 
 
8104
                // Static items. As the axis group is cleared on subsequent calls
 
8105
                // to render, these items are added outside the group.
 
8106
                // axis line
 
8107
                if (lineWidth) {
 
8108
                        linePath = axis.getLinePath(lineWidth);
 
8109
                        if (!axis.axisLine) {
 
8110
                                axis.axisLine = renderer.path(linePath)
 
8111
                                        .attr({
 
8112
                                                stroke: options.lineColor,
 
8113
                                                'stroke-width': lineWidth,
 
8114
                                                zIndex: 7
 
8115
                                        })
 
8116
                                        .add(axis.axisGroup);
 
8117
                        } else {
 
8118
                                axis.axisLine.animate({ d: linePath });
 
8119
                        }
 
8120
 
 
8121
                        // show or hide the line depending on options.showEmpty
 
8122
                        axis.axisLine[showAxis ? 'show' : 'hide']();
 
8123
                }
 
8124
 
 
8125
                if (axisTitle && showAxis) {
 
8126
 
 
8127
                        axisTitle[axisTitle.isNew ? 'attr' : 'animate'](
 
8128
                                axis.getTitlePosition()
 
8129
                        );
 
8130
                        axisTitle.isNew = false;
 
8131
                }
 
8132
 
 
8133
                // Stacked totals:
 
8134
                if (stackLabelOptions && stackLabelOptions.enabled) {
 
8135
                        axis.renderStackTotals();
 
8136
                }
 
8137
                // End stacked totals
 
8138
 
 
8139
                axis.isDirty = false;
 
8140
        },
 
8141
 
 
8142
        /**
 
8143
         * Redraw the axis to reflect changes in the data or axis extremes
 
8144
         */
 
8145
        redraw: function () {
 
8146
                var axis = this,
 
8147
                        chart = axis.chart,
 
8148
                        pointer = chart.pointer;
 
8149
 
 
8150
                // hide tooltip and hover states
 
8151
                if (pointer) {
 
8152
                        pointer.reset(true);
 
8153
                }
 
8154
 
 
8155
                // render the axis
 
8156
                axis.render();
 
8157
 
 
8158
                // move plot lines and bands
 
8159
                each(axis.plotLinesAndBands, function (plotLine) {
 
8160
                        plotLine.render();
 
8161
                });
 
8162
 
 
8163
                // mark associated series as dirty and ready for redraw
 
8164
                each(axis.series, function (series) {
 
8165
                        series.isDirty = true;
 
8166
                });
 
8167
 
 
8168
        },
 
8169
 
 
8170
        /**
 
8171
         * Destroys an Axis instance.
 
8172
         */
 
8173
        destroy: function (keepEvents) {
 
8174
                var axis = this,
 
8175
                        stacks = axis.stacks,
 
8176
                        stackKey,
 
8177
                        plotLinesAndBands = axis.plotLinesAndBands,
 
8178
                        i;
 
8179
 
 
8180
                // Remove the events
 
8181
                if (!keepEvents) {
 
8182
                        removeEvent(axis);
 
8183
                }
 
8184
 
 
8185
                // Destroy each stack total
 
8186
                for (stackKey in stacks) {
 
8187
                        destroyObjectProperties(stacks[stackKey]);
 
8188
 
 
8189
                        stacks[stackKey] = null;
 
8190
                }
 
8191
 
 
8192
                // Destroy collections
 
8193
                each([axis.ticks, axis.minorTicks, axis.alternateBands], function (coll) {
 
8194
                        destroyObjectProperties(coll);
 
8195
                });
 
8196
                i = plotLinesAndBands.length;
 
8197
                while (i--) { // #1975
 
8198
                        plotLinesAndBands[i].destroy();
 
8199
                }
 
8200
 
 
8201
                // Destroy local variables
 
8202
                each(['stackTotalGroup', 'axisLine', 'axisTitle', 'axisGroup', 'cross', 'gridGroup', 'labelGroup'], function (prop) {
 
8203
                        if (axis[prop]) {
 
8204
                                axis[prop] = axis[prop].destroy();
 
8205
                        }
 
8206
                });
 
8207
 
 
8208
                // Destroy crosshair
 
8209
                if (this.cross) {
 
8210
                        this.cross.destroy();
 
8211
                }
 
8212
        },
 
8213
 
 
8214
        /**
 
8215
         * Draw the crosshair
 
8216
         */
 
8217
        drawCrosshair: function (e, point) {
 
8218
                if (!this.crosshair) { return; }// Do not draw crosshairs if you don't have too.
 
8219
 
 
8220
                if ((defined(point) || !pick(this.crosshair.snap, true)) === false) {
 
8221
                        this.hideCrosshair();
 
8222
                        return;
 
8223
                }
 
8224
 
 
8225
                var path,
 
8226
                        options = this.crosshair,
 
8227
                        animation = options.animation,
 
8228
                        pos;
 
8229
 
 
8230
                // Get the path
 
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*/
 
8237
                }
 
8238
 
 
8239
                if (this.isRadial) {
 
8240
                        path = this.getPlotLinePath(this.isXAxis ? point.x : pick(point.stackY, point.y));
 
8241
                } else {
 
8242
                        path = this.getPlotLinePath(null, null, null, null, pos);
 
8243
                }
 
8244
 
 
8245
                if (path === null) {
 
8246
                        this.hideCrosshair();
 
8247
                        return;
 
8248
                }
 
8249
 
 
8250
                // Draw the cross
 
8251
                if (this.cross) {
 
8252
                        this.cross
 
8253
                                .attr({ visibility: VISIBLE })[animation ? 'animate' : 'attr']({ d: path }, animation);
 
8254
                } else {
 
8255
                        var attribs = {
 
8256
                                'stroke-width': options.width || 1,
 
8257
                                stroke: options.color || '#C0C0C0',
 
8258
                                zIndex: options.zIndex || 2
 
8259
                        };
 
8260
                        if (options.dashStyle) {
 
8261
                                attribs.dashstyle = options.dashStyle;
 
8262
                        }
 
8263
                        this.cross = this.chart.renderer.path(path).attr(attribs).add();
 
8264
                }
 
8265
        },
 
8266
 
 
8267
        /**
 
8268
         *      Hide the crosshair.
 
8269
         */
 
8270
        hideCrosshair: function () {
 
8271
                if (this.cross) {
 
8272
                        this.cross.hide();
 
8273
                }
 
8274
        }
 
8275
}; // end Axis
 
8276
 
 
8277
extend(Axis.prototype, AxisPlotLineOrBandExtension);
 
8278
 
 
8279
/**
 
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.
 
8284
 *
 
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
 
8289
 */
 
8290
Axis.prototype.getTimeTicks = function (normalizedInterval, min, max, startOfWeek) {
 
8291
        var tickPositions = [],
 
8292
                i,
 
8293
                higherRanks = {},
 
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;
 
8299
 
 
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));
 
8305
                }
 
8306
        
 
8307
                if (interval >= timeUnits[MINUTE]) { // minute
 
8308
                        minDate[setMinutes](interval >= timeUnits[HOUR] ? 0 :
 
8309
                                count * mathFloor(minDate[getMinutes]() / count));
 
8310
                }
 
8311
        
 
8312
                if (interval >= timeUnits[HOUR]) { // hour
 
8313
                        minDate[setHours](interval >= timeUnits[DAY] ? 0 :
 
8314
                                count * mathFloor(minDate[getHours]() / count));
 
8315
                }
 
8316
        
 
8317
                if (interval >= timeUnits[DAY]) { // day
 
8318
                        minDate[setDate](interval >= timeUnits[MONTH] ? 1 :
 
8319
                                count * mathFloor(minDate[getDate]() / count));
 
8320
                }
 
8321
        
 
8322
                if (interval >= timeUnits[MONTH]) { // month
 
8323
                        minDate[setMonth](interval >= timeUnits[YEAR] ? 0 :
 
8324
                                count * mathFloor(minDate[getMonth]() / count));
 
8325
                        minYear = minDate[getFullYear]();
 
8326
                }
 
8327
        
 
8328
                if (interval >= timeUnits[YEAR]) { // year
 
8329
                        minYear -= minYear % count;
 
8330
                        minDate[setFullYear](minYear);
 
8331
                }
 
8332
        
 
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));
 
8338
                }
 
8339
        
 
8340
        
 
8341
                // get tick positions
 
8342
                i = 1;
 
8343
                if (timezoneOffset) {
 
8344
                        minDate = new Date(minDate.getTime() + timezoneOffset);
 
8345
                }
 
8346
                minYear = minDate[getFullYear]();
 
8347
                var time = minDate.getTime(),
 
8348
                        minMonth = minDate[getMonth](),
 
8349
                        minDateDate = minDate[getDate](),
 
8350
                        localTimezoneOffset = useUTC ? 
 
8351
                                timezoneOffset : 
 
8352
                                (24 * 3600 * 1000 + minDate.getTimezoneOffset() * 60 * 1000) % (24 * 3600 * 1000); // #950
 
8353
        
 
8354
                // iterate and add tick positions at appropriate values
 
8355
                while (time < max) {
 
8356
                        tickPositions.push(time);
 
8357
        
 
8358
                        // if the interval is years, use Date.UTC to increase years
 
8359
                        if (interval === timeUnits[YEAR]) {
 
8360
                                time = makeTime(minYear + i * count, 0);
 
8361
        
 
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);
 
8365
        
 
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));
 
8371
        
 
8372
                        // else, the interval is fixed and we use simple addition
 
8373
                        } else {
 
8374
                                time += interval * count;
 
8375
                        }
 
8376
        
 
8377
                        i++;
 
8378
                }
 
8379
        
 
8380
                // push the last time
 
8381
                tickPositions.push(time);
 
8382
 
 
8383
 
 
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;
 
8389
                });
 
8390
        }
 
8391
 
 
8392
 
 
8393
        // record information on the chosen unit - for dynamic label formatter
 
8394
        tickPositions.info = extend(normalizedInterval, {
 
8395
                higherRanks: higherRanks,
 
8396
                totalRange: interval * count
 
8397
        });
 
8398
 
 
8399
        return tickPositions;
 
8400
};
 
8401
 
 
8402
/**
 
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. 
 
8408
 * #662, #697.
 
8409
 */
 
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
 
8414
                        ], [
 
8415
                                SECOND,
 
8416
                                [1, 2, 5, 10, 15, 30]
 
8417
                        ], [
 
8418
                                MINUTE,
 
8419
                                [1, 2, 5, 10, 15, 30]
 
8420
                        ], [
 
8421
                                HOUR,
 
8422
                                [1, 2, 3, 4, 6, 8, 12]
 
8423
                        ], [
 
8424
                                DAY,
 
8425
                                [1, 2]
 
8426
                        ], [
 
8427
                                WEEK,
 
8428
                                [1, 2]
 
8429
                        ], [
 
8430
                                MONTH,
 
8431
                                [1, 2, 3, 4, 6]
 
8432
                        ], [
 
8433
                                YEAR,
 
8434
                                null
 
8435
                        ]],
 
8436
                unit = units[units.length - 1], // default unit is years
 
8437
                interval = timeUnits[unit[0]],
 
8438
                multiples = unit[1],
 
8439
                count,
 
8440
                i;
 
8441
                
 
8442
        // loop through the units to find the one that best fits the tickInterval
 
8443
        for (i = 0; i < units.length; i++) {
 
8444
                unit = units[i];
 
8445
                interval = timeUnits[unit[0]];
 
8446
                multiples = unit[1];
 
8447
 
 
8448
 
 
8449
                if (units[i + 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;
 
8453
 
 
8454
                        // break and keep the current unit
 
8455
                        if (tickInterval <= lessThan) {
 
8456
                                break;
 
8457
                        }
 
8458
                }
 
8459
        }
 
8460
 
 
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];
 
8464
        }
 
8465
 
 
8466
        // get the count
 
8467
        count = normalizeTickInterval(
 
8468
                tickInterval / interval, 
 
8469
                multiples,
 
8470
                unit[0] === YEAR ? mathMax(getMagnitude(tickInterval / interval), 1) : 1 // #1913, #2360
 
8471
        );
 
8472
        
 
8473
        return {
 
8474
                unitRange: interval,
 
8475
                count: count,
 
8476
                unitName: unit[0]
 
8477
        };
 
8478
};/**
 
8479
 * Methods defined on the Axis prototype
 
8480
 */
 
8481
 
 
8482
/**
 
8483
 * Set the tick positions of a logarithmic axis
 
8484
 */
 
8485
Axis.prototype.getLogTickPositions = function (interval, min, max, minor) {
 
8486
        var axis = this,
 
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
 
8491
                positions = []; 
 
8492
        
 
8493
        // Reset
 
8494
        if (!minor) {
 
8495
                axis._minorAutoInterval = null;
 
8496
        }
 
8497
        
 
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);
 
8502
                
 
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),
 
8507
                        intermediate,
 
8508
                        i,
 
8509
                        j,
 
8510
                        len,
 
8511
                        pos,
 
8512
                        lastPos,
 
8513
                        break2;
 
8514
                        
 
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];
 
8521
                }
 
8522
                
 
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]);
 
8527
                                
 
8528
                                if (pos > min && (!minor || lastPos <= max)) { // #1670
 
8529
                                        positions.push(lastPos);
 
8530
                                }
 
8531
                                
 
8532
                                if (lastPos > max) {
 
8533
                                        break2 = true;
 
8534
                                }
 
8535
                                lastPos = pos;
 
8536
                        }
 
8537
                }
 
8538
                
 
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.
 
8542
        } else {
 
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;
 
8549
                
 
8550
                interval = pick(
 
8551
                        filteredTickIntervalOption,
 
8552
                        axis._minorAutoInterval,
 
8553
                        (realMax - realMin) * tickPixelIntervalOption / (totalPixelLength || 1)
 
8554
                );
 
8555
                
 
8556
                interval = normalizeTickInterval(
 
8557
                        interval, 
 
8558
                        null, 
 
8559
                        getMagnitude(interval)
 
8560
                );
 
8561
                
 
8562
                positions = map(axis.getLinearTickPositions(
 
8563
                        interval, 
 
8564
                        realMin,
 
8565
                        realMax 
 
8566
                ), log2lin);
 
8567
                
 
8568
                if (!minor) {
 
8569
                        axis._minorAutoInterval = interval / 5;
 
8570
                }
 
8571
        }
 
8572
        
 
8573
        // Set the axis-level tickInterval variable 
 
8574
        if (!minor) {
 
8575
                axis.tickInterval = interval;
 
8576
        }
 
8577
        return positions;
 
8578
};/**
 
8579
 * The tooltip object
 
8580
 * @param {Object} chart The chart instance
 
8581
 * @param {Object} options Tooltip options
 
8582
 */
 
8583
var Tooltip = Highcharts.Tooltip = function () {
 
8584
        this.init.apply(this, arguments);
 
8585
};
 
8586
 
 
8587
Tooltip.prototype = {
 
8588
 
 
8589
        init: function (chart, options) {
 
8590
 
 
8591
                var borderWidth = options.borderWidth,
 
8592
                        style = options.style,
 
8593
                        padding = pInt(style.padding);
 
8594
 
 
8595
                // Save the chart and options
 
8596
                this.chart = chart;
 
8597
                this.options = options;
 
8598
 
 
8599
                // Keep track of the current series
 
8600
                //this.currentSeries = UNDEFINED;
 
8601
 
 
8602
                // List of crosshairs
 
8603
                this.crosshairs = [];
 
8604
 
 
8605
                // Current values of x and y when animating
 
8606
                this.now = { x: 0, y: 0 };
 
8607
 
 
8608
                // The tooltip is initially hidden
 
8609
                this.isHidden = true;
 
8610
 
 
8611
 
 
8612
                // create the label
 
8613
                this.label = chart.renderer.label('', 0, 0, options.shape, null, null, options.useHTML, null, 'tooltip')
 
8614
                        .attr({
 
8615
                                padding: padding,
 
8616
                                fill: options.backgroundColor,
 
8617
                                'stroke-width': borderWidth,
 
8618
                                r: options.borderRadius,
 
8619
                                zIndex: 8
 
8620
                        })
 
8621
                        .css(style)
 
8622
                        .css({ padding: 0 }) // Remove it from VML, the padding is applied as an attribute instead (#1117)
 
8623
                        .add()
 
8624
                        .attr({ y: -9999 }); // #2301, #2657
 
8625
 
 
8626
                // When using canVG the shadow shows up as a gray circle
 
8627
                // even if the tooltip is hidden.
 
8628
                if (!useCanVG) {
 
8629
                        this.label.shadow(options.shadow);
 
8630
                }
 
8631
 
 
8632
                // Public property for getting the shared state.
 
8633
                this.shared = options.shared;
 
8634
        },
 
8635
 
 
8636
        /**
 
8637
         * Destroy the tooltip and its elements.
 
8638
         */
 
8639
        destroy: function () {
 
8640
                // Destroy and clear local variables
 
8641
                if (this.label) {
 
8642
                        this.label = this.label.destroy();
 
8643
                }
 
8644
                clearTimeout(this.hideTimer);
 
8645
                clearTimeout(this.tooltipTimeout);
 
8646
        },
 
8647
 
 
8648
        /**
 
8649
         * Provide a soft movement for the tooltip
 
8650
         *
 
8651
         * @param {Number} x
 
8652
         * @param {Number} y
 
8653
         * @private
 
8654
         */
 
8655
        move: function (x, y, anchorX, anchorY) {
 
8656
                var tooltip = this,
 
8657
                        now = tooltip.now,
 
8658
                        animate = tooltip.options.animation !== false && !tooltip.isHidden;
 
8659
 
 
8660
                // get intermediate values for animation
 
8661
                extend(now, {
 
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
 
8666
                });
 
8667
 
 
8668
                // move to the intermediate value
 
8669
                tooltip.label.attr(now);
 
8670
 
 
8671
                
 
8672
                // run on next tick of the mouse tracker
 
8673
                if (animate && (mathAbs(x - now.x) > 1 || mathAbs(y - now.y) > 1)) {
 
8674
                
 
8675
                        // never allow two timeouts
 
8676
                        clearTimeout(this.tooltipTimeout);
 
8677
                        
 
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.
 
8681
                                if (tooltip) {
 
8682
                                        tooltip.move(x, y, anchorX, anchorY);
 
8683
                                }
 
8684
                        }, 32);
 
8685
                        
 
8686
                }
 
8687
        },
 
8688
 
 
8689
        /**
 
8690
         * Hide the tooltip
 
8691
         */
 
8692
        hide: function () {
 
8693
                var tooltip = this,
 
8694
                        hoverPoints;
 
8695
                
 
8696
                clearTimeout(this.hideTimer); // disallow duplicate timers (#1728, #1766)
 
8697
                if (!this.isHidden) {
 
8698
                        hoverPoints = this.chart.hoverPoints;
 
8699
 
 
8700
                        this.hideTimer = setTimeout(function () {
 
8701
                                tooltip.label.fadeOut();
 
8702
                                tooltip.isHidden = true;
 
8703
                        }, pick(this.options.hideDelay, 500));
 
8704
 
 
8705
                        // hide previous hoverPoints and set new
 
8706
                        if (hoverPoints) {
 
8707
                                each(hoverPoints, function (point) {
 
8708
                                        point.setState();
 
8709
                                });
 
8710
                        }
 
8711
 
 
8712
                        this.chart.hoverPoints = null;
 
8713
                }
 
8714
        },
 
8715
        
 
8716
        /** 
 
8717
         * Extendable method to get the anchor position of the tooltip
 
8718
         * from a point or set of points
 
8719
         */
 
8720
        getAnchor: function (points, mouseEvent) {
 
8721
                var ret,
 
8722
                        chart = this.chart,
 
8723
                        inverted = chart.inverted,
 
8724
                        plotTop = chart.plotTop,
 
8725
                        plotX = 0,
 
8726
                        plotY = 0,
 
8727
                        yAxis;
 
8728
                
 
8729
                points = splat(points);
 
8730
                
 
8731
                // Pie uses a special tooltipPos
 
8732
                ret = points[0].tooltipPos;
 
8733
                
 
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);
 
8738
                        }
 
8739
                        ret = [
 
8740
                                mouseEvent.chartX - chart.plotLeft,
 
8741
                                mouseEvent.chartY - plotTop
 
8742
                        ];
 
8743
                }
 
8744
                // When shared, use the average position
 
8745
                if (!ret) {
 
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
 
8751
                        });
 
8752
                        
 
8753
                        plotX /= points.length;
 
8754
                        plotY /= points.length;
 
8755
                        
 
8756
                        ret = [
 
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
 
8761
                        ];
 
8762
                }
 
8763
 
 
8764
                return map(ret, mathRound);
 
8765
        },
 
8766
        
 
8767
        /**
 
8768
         * Place the tooltip in a chart without spilling over
 
8769
         * and not covering the point it self.
 
8770
         */
 
8771
        getPosition: function (boxWidth, boxHeight, point) {
 
8772
                
 
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
 
8784
                        alignedRight;
 
8785
 
 
8786
                // It is too far to the left, adjust it
 
8787
                if (x < 7) {
 
8788
                        x = plotLeft + mathMax(pointX, 0) + distance;
 
8789
                }
 
8790
        
 
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;
 
8797
                }
 
8798
        
 
8799
                // If it is now above the plot area, align it to the top of the plot area
 
8800
                if (y < plotTop + 5) {
 
8801
                        y = plotTop + 5;
 
8802
        
 
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
 
8806
                        }
 
8807
                } 
 
8808
        
 
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
 
8813
                }
 
8814
        
 
8815
                return {x: x, y: y};
 
8816
        },
 
8817
 
 
8818
        /**
 
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.
 
8821
         */
 
8822
        defaultFormatter: function (tooltip) {
 
8823
                var items = this.points || splat(this),
 
8824
                        series = items[0].series,
 
8825
                        s;
 
8826
 
 
8827
                // build the header
 
8828
                s = [tooltip.tooltipHeaderFormatter(items[0])];
 
8829
 
 
8830
                // build the values
 
8831
                each(items, function (item) {
 
8832
                        series = item.series;
 
8833
                        s.push((series.tooltipFormatter && series.tooltipFormatter(item)) ||
 
8834
                                item.point.tooltipFormatter(series.tooltipOptions.pointFormat));
 
8835
                });
 
8836
 
 
8837
                // footer
 
8838
                s.push(tooltip.options.footerFormat || '');
 
8839
 
 
8840
                return s.join('');
 
8841
        },
 
8842
 
 
8843
        /**
 
8844
         * Refresh the tooltip's text and position.
 
8845
         * @param {Object} point
 
8846
         */
 
8847
        refresh: function (point, mouseEvent) {
 
8848
                var tooltip = this,
 
8849
                        chart = tooltip.chart,
 
8850
                        label = tooltip.label,
 
8851
                        options = tooltip.options,
 
8852
                        x,
 
8853
                        y,
 
8854
                        anchor,
 
8855
                        textConfig = {},
 
8856
                        text,
 
8857
                        pointConfig = [],
 
8858
                        formatter = options.formatter || tooltip.defaultFormatter,
 
8859
                        hoverPoints = chart.hoverPoints,
 
8860
                        borderColor,
 
8861
                        shared = tooltip.shared,
 
8862
                        currentSeries;
 
8863
                        
 
8864
                clearTimeout(this.hideTimer);
 
8865
                
 
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);
 
8869
                x = anchor[0];
 
8870
                y = anchor[1];
 
8871
 
 
8872
                // shared tooltip, array is sent over
 
8873
                if (shared && !(point.series && point.series.noSharedTooltip)) {
 
8874
                        
 
8875
                        // hide previous hoverPoints and set new
 
8876
                        
 
8877
                        chart.hoverPoints = point;
 
8878
                        if (hoverPoints) {
 
8879
                                each(hoverPoints, function (point) {
 
8880
                                        point.setState();
 
8881
                                });
 
8882
                        }
 
8883
 
 
8884
                        each(point, function (item) {
 
8885
                                item.setState(HOVER_STATE);
 
8886
 
 
8887
                                pointConfig.push(item.getLabelConfig());
 
8888
                        });
 
8889
 
 
8890
                        textConfig = {
 
8891
                                x: point[0].category,
 
8892
                                y: point[0].y
 
8893
                        };
 
8894
                        textConfig.points = pointConfig;
 
8895
                        point = point[0];
 
8896
 
 
8897
                // single point tooltip
 
8898
                } else {
 
8899
                        textConfig = point.getLabelConfig();
 
8900
                }
 
8901
                text = formatter.call(textConfig, tooltip);
 
8902
 
 
8903
                // register the current series
 
8904
                currentSeries = point.series;
 
8905
 
 
8906
                // update the inner HTML
 
8907
                if (text === false) {
 
8908
                        this.hide();
 
8909
                } else {
 
8910
 
 
8911
                        // show it
 
8912
                        if (tooltip.isHidden) {
 
8913
                                stop(label);
 
8914
                                label.attr('opacity', 1).show();
 
8915
                        }
 
8916
 
 
8917
                        // update text
 
8918
                        label.attr({
 
8919
                                text: text
 
8920
                        });
 
8921
 
 
8922
                        // set the stroke color of the box
 
8923
                        borderColor = options.borderColor || point.color || currentSeries.color || '#606060';
 
8924
                        label.attr({
 
8925
                                stroke: borderColor
 
8926
                        });
 
8927
                        
 
8928
                        tooltip.updatePosition({ plotX: x, plotY: y });
 
8929
                
 
8930
                        this.isHidden = false;
 
8931
                }
 
8932
                fireEvent(chart, 'tooltipRefresh', {
 
8933
                                text: text,
 
8934
                                x: x + chart.plotLeft,
 
8935
                                y: y + chart.plotTop,
 
8936
                                borderColor: borderColor
 
8937
                        });
 
8938
        },
 
8939
        
 
8940
        /**
 
8941
         * Find the new position and perform the move
 
8942
         */
 
8943
        updatePosition: function (point) {
 
8944
                var chart = this.chart,
 
8945
                        label = this.label, 
 
8946
                        pos = (this.options.positioner || this.getPosition).call(
 
8947
                                this,
 
8948
                                label.width,
 
8949
                                label.height,
 
8950
                                point
 
8951
                        );
 
8952
 
 
8953
                // do the move
 
8954
                this.move(
 
8955
                        mathRound(pos.x), 
 
8956
                        mathRound(pos.y), 
 
8957
                        point.plotX + chart.plotLeft, 
 
8958
                        point.plotY + chart.plotTop
 
8959
                );
 
8960
        },
 
8961
 
 
8962
 
 
8963
        /**
 
8964
         * Format the header of the tooltip
 
8965
         */
 
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,
 
8975
                        n;
 
8976
 
 
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];
 
8987
                                                break;
 
8988
                                        }
 
8989
                                }
 
8990
                        } else {
 
8991
                                xDateFormat = dateTimeLabelFormats.day;
 
8992
                        }
 
8993
 
 
8994
                        xDateFormat = xDateFormat || dateTimeLabelFormats.year; // #2546, 2581
 
8995
 
 
8996
                }
 
8997
 
 
8998
                // Insert the header date format if any
 
8999
                if (isDateTime && xDateFormat) {
 
9000
                        headerFormat = headerFormat.replace('{point.key}', '{point.key:' + xDateFormat + '}');
 
9001
                }
 
9002
 
 
9003
                return format(headerFormat, {
 
9004
                        point: point,
 
9005
                        series: series
 
9006
                });
 
9007
        }
 
9008
};
 
9009
 
 
9010
var hoverChartIndex;
 
9011
 
 
9012
// Global flag for touch support
 
9013
hasTouch = doc.documentElement.ontouchstart !== UNDEFINED;
 
9014
 
 
9015
/**
 
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
 
9020
 */
 
9021
var Pointer = Highcharts.Pointer = function (chart, options) {
 
9022
        this.init(chart, options);
 
9023
};
 
9024
 
 
9025
Pointer.prototype = {
 
9026
        /**
 
9027
         * Initialize Pointer
 
9028
         */
 
9029
        init: function (chart, options) {
 
9030
                
 
9031
                var chartOptions = options.chart,
 
9032
                        chartEvents = chartOptions.events,
 
9033
                        zoomType = useCanVG ? '' : chartOptions.zoomType,
 
9034
                        inverted = chart.inverted,
 
9035
                        zoomX,
 
9036
                        zoomY;
 
9037
 
 
9038
                // Store references
 
9039
                this.options = options;
 
9040
                this.chart = chart;
 
9041
                
 
9042
                // Zoom status
 
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);
 
9047
 
 
9048
                // Do we need to handle click on a touch device?
 
9049
                this.runChartClick = chartEvents && !!chartEvents.click;
 
9050
 
 
9051
                this.pinchDown = [];
 
9052
                this.lastValidTouch = {};
 
9053
 
 
9054
                if (Highcharts.Tooltip && options.tooltip.enabled) {
 
9055
                        chart.tooltip = new Tooltip(chart, options.tooltip);
 
9056
                }
 
9057
 
 
9058
                this.setDOMEvents();
 
9059
        }, 
 
9060
 
 
9061
        /**
 
9062
         * Add crossbrowser support for chartX and chartY
 
9063
         * @param {Object} e The event object in standard browsers
 
9064
         */
 
9065
        normalize: function (e, chartPosition) {
 
9066
                var chartX,
 
9067
                        chartY,
 
9068
                        ePos;
 
9069
 
 
9070
                // common IE normalizing
 
9071
                e = e || win.event;
 
9072
 
 
9073
                // Framework specific normalizing (#1165)
 
9074
                e = washMouseEvent(e);
 
9075
 
 
9076
                // More IE normalizing, needs to go after washMouseEvent
 
9077
                if (!e.target) {
 
9078
                        e.target = e.srcElement;
 
9079
                }
 
9080
                
 
9081
                // iOS
 
9082
                ePos = e.touches ? e.touches.item(0) : e;
 
9083
 
 
9084
                // Get mouse position
 
9085
                if (!chartPosition) {
 
9086
                        this.chartPosition = chartPosition = offset(this.chart.container);
 
9087
                }
 
9088
 
 
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
 
9093
                        chartY = e.y;
 
9094
                } else {
 
9095
                        chartX = ePos.pageX - chartPosition.left;
 
9096
                        chartY = ePos.pageY - chartPosition.top;
 
9097
                }
 
9098
 
 
9099
                return extend(e, {
 
9100
                        chartX: mathRound(chartX),
 
9101
                        chartY: mathRound(chartY)
 
9102
                });
 
9103
        },
 
9104
 
 
9105
        /**
 
9106
         * Get the click position in terms of axis values.
 
9107
         *
 
9108
         * @param {Object} e A pointer event
 
9109
         */
 
9110
        getCoordinates: function (e) {
 
9111
                var coordinates = {
 
9112
                                xAxis: [],
 
9113
                                yAxis: []
 
9114
                        };
 
9115
 
 
9116
                each(this.chart.axes, function (axis) {
 
9117
                        coordinates[axis.isXAxis ? 'xAxis' : 'yAxis'].push({
 
9118
                                axis: axis,
 
9119
                                value: axis.toValue(e[axis.horiz ? 'chartX' : 'chartY'])
 
9120
                        });
 
9121
                });
 
9122
                return coordinates;
 
9123
        },
 
9124
        
 
9125
        /**
 
9126
         * Return the index in the tooltipPoints array, corresponding to pixel position in 
 
9127
         * the plot area.
 
9128
         */
 
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;
 
9134
        },
 
9135
 
 
9136
        /**
 
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.
 
9139
         */
 
9140
        runPointActions: function (e) {
 
9141
                var pointer = this,
 
9142
                        chart = pointer.chart,
 
9143
                        series = chart.series,
 
9144
                        tooltip = chart.tooltip,
 
9145
                        point,
 
9146
                        points,
 
9147
                        hoverPoint = chart.hoverPoint,
 
9148
                        hoverSeries = chart.hoverSeries,
 
9149
                        i,
 
9150
                        j,
 
9151
                        distance = chart.chartWidth,
 
9152
                        index = pointer.getIndex(e),
 
9153
                        anchor;
 
9154
 
 
9155
                // shared tooltip
 
9156
                if (tooltip && pointer.options.tooltip.shared && !(hoverSeries && hoverSeries.noSharedTooltip)) {
 
9157
                        points = [];
 
9158
 
 
9159
                        // loop over all series and find the ones with points closest to the mouse
 
9160
                        i = series.length;
 
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);
 
9169
                                                points.push(point);
 
9170
                                        }
 
9171
                                }
 
9172
                        }
 
9173
                        // remove furthest points
 
9174
                        i = points.length;
 
9175
                        while (i--) {
 
9176
                                if (points[i]._dist > distance) {
 
9177
                                        points.splice(i, 1);
 
9178
                                }
 
9179
                        }
 
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;
 
9184
                        }
 
9185
                }
 
9186
 
 
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
 
9189
 
 
9190
                        // get the point
 
9191
                        point = hoverSeries.tooltipPoints[index];
 
9192
 
 
9193
                        // a new point is hovered, refresh the tooltip
 
9194
                        if (point && point !== hoverPoint) {
 
9195
 
 
9196
                                // trigger the events
 
9197
                                point.onMouseOver(e);
 
9198
 
 
9199
                        }
 
9200
                        
 
9201
                } else if (tooltip && tooltip.followPointer && !tooltip.isHidden) {
 
9202
                        anchor = tooltip.getAnchor([{}], e);
 
9203
                        tooltip.updatePosition({ plotX: anchor[0], plotY: anchor[1] });
 
9204
                }
 
9205
 
 
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);
 
9211
                                }
 
9212
                        };
 
9213
                        addEvent(doc, 'mousemove', pointer._onDocumentMouseMove);
 
9214
                }
 
9215
 
 
9216
                // Draw independent crosshairs
 
9217
                each(chart.axes, function (axis) {
 
9218
                        axis.drawCrosshair(e, pick(point, hoverPoint));
 
9219
                });
 
9220
        },
 
9221
 
 
9222
 
 
9223
 
 
9224
        /**
 
9225
         * Reset the tracking by hiding the tooltip, the hover series state and the hover point
 
9226
         * 
 
9227
         * @param allowMove {Boolean} Instead of destroying the tooltip altogether, allow moving it if possible
 
9228
         */
 
9229
        reset: function (allowMove) {
 
9230
                var pointer = this,
 
9231
                        chart = pointer.chart,
 
9232
                        hoverSeries = chart.hoverSeries,
 
9233
                        hoverPoint = chart.hoverPoint,
 
9234
                        tooltip = chart.tooltip,
 
9235
                        tooltipPoints = tooltip && tooltip.shared ? chart.hoverPoints : hoverPoint;
 
9236
                        
 
9237
                // Narrow in allowMove
 
9238
                allowMove = allowMove && tooltip && tooltipPoints;
 
9239
                        
 
9240
                // Check if the points have moved outside the plot area, #1003
 
9241
                if (allowMove && splat(tooltipPoints)[0].plotX === UNDEFINED) {
 
9242
                        allowMove = false;
 
9243
                }       
 
9244
 
 
9245
                // Just move the tooltip, #349
 
9246
                if (allowMove) {
 
9247
                        tooltip.refresh(tooltipPoints);
 
9248
                        if (hoverPoint) { // #2500
 
9249
                                hoverPoint.setState(hoverPoint.state, true);
 
9250
                        }
 
9251
 
 
9252
                // Full reset
 
9253
                } else {
 
9254
 
 
9255
                        if (hoverPoint) {
 
9256
                                hoverPoint.onMouseOut();
 
9257
                        }
 
9258
 
 
9259
                        if (hoverSeries) {
 
9260
                                hoverSeries.onMouseOut();
 
9261
                        }
 
9262
 
 
9263
                        if (tooltip) {
 
9264
                                tooltip.hide();
 
9265
                        }
 
9266
 
 
9267
                        if (pointer._onDocumentMouseMove) {
 
9268
                                removeEvent(doc, 'mousemove', pointer._onDocumentMouseMove);
 
9269
                                pointer._onDocumentMouseMove = null;
 
9270
                        }
 
9271
 
 
9272
                        // Remove crosshairs
 
9273
                        each(chart.axes, function (axis) {
 
9274
                                axis.hideCrosshair();
 
9275
                        });
 
9276
                        
 
9277
                        pointer.hoverX = null;
 
9278
 
 
9279
                }
 
9280
        },
 
9281
 
 
9282
        /**
 
9283
         * Scale series groups to a certain scale and translation
 
9284
         */
 
9285
        scaleGroups: function (attribs, clip) {
 
9286
 
 
9287
                var chart = this.chart,
 
9288
                        seriesAttribs;
 
9289
 
 
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);
 
9298
                                }
 
9299
                                if (series.dataLabelsGroup) {
 
9300
                                        series.dataLabelsGroup.attr(seriesAttribs);
 
9301
                                }
 
9302
                        }
 
9303
                });
 
9304
                
 
9305
                // Clip
 
9306
                chart.clipRect.attr(clip || chart.clipBox);
 
9307
        },
 
9308
 
 
9309
        /**
 
9310
         * Start a drag operation
 
9311
         */
 
9312
        dragStart: function (e) {
 
9313
                var chart = this.chart;
 
9314
 
 
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;
 
9320
        },
 
9321
 
 
9322
        /**
 
9323
         * Perform a drag operation in response to a mousemove event while the mouse is down
 
9324
         */
 
9325
        drag: function (e) {
 
9326
 
 
9327
                var chart = this.chart,
 
9328
                        chartOptions = chart.options.chart,
 
9329
                        chartX = e.chartX,
 
9330
                        chartY = e.chartY,
 
9331
                        zoomHor = this.zoomHor,
 
9332
                        zoomVert = this.zoomVert,
 
9333
                        plotLeft = chart.plotLeft,
 
9334
                        plotTop = chart.plotTop,
 
9335
                        plotWidth = chart.plotWidth,
 
9336
                        plotHeight = chart.plotHeight,
 
9337
                        clickedInside,
 
9338
                        size,
 
9339
                        mouseDownX = this.mouseDownX,
 
9340
                        mouseDownY = this.mouseDownY;
 
9341
 
 
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) {
 
9345
                        chartX = plotLeft;
 
9346
                } else if (chartX > plotLeft + plotWidth) {
 
9347
                        chartX = plotLeft + plotWidth;
 
9348
                }
 
9349
 
 
9350
                if (chartY < plotTop) {
 
9351
                        chartY = plotTop;
 
9352
                } else if (chartY > plotTop + plotHeight) {
 
9353
                        chartY = plotTop + plotHeight;
 
9354
                }
 
9355
                
 
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)
 
9360
                );
 
9361
                
 
9362
                if (this.hasDragged > 10) {
 
9363
                        clickedInside = chart.isInsidePlot(mouseDownX - plotLeft, mouseDownY - plotTop);
 
9364
 
 
9365
                        // make a selection
 
9366
                        if (chart.hasCartesianSeries && (this.zoomX || this.zoomY) && clickedInside) {
 
9367
                                if (!this.selectionMarker) {
 
9368
                                        this.selectionMarker = chart.renderer.rect(
 
9369
                                                plotLeft,
 
9370
                                                plotTop,
 
9371
                                                zoomHor ? 1 : plotWidth,
 
9372
                                                zoomVert ? 1 : plotHeight,
 
9373
                                                0
 
9374
                                        )
 
9375
                                        .attr({
 
9376
                                                fill: chartOptions.selectionMarkerFill || 'rgba(69,114,167,0.25)',
 
9377
                                                zIndex: 7
 
9378
                                        })
 
9379
                                        .add();
 
9380
                                }
 
9381
                        }
 
9382
 
 
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
 
9389
                                });
 
9390
                        }
 
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
 
9397
                                });
 
9398
                        }
 
9399
 
 
9400
                        // panning
 
9401
                        if (clickedInside && !this.selectionMarker && chartOptions.panning) {
 
9402
                                chart.pan(e, chartOptions.panning);
 
9403
                        }
 
9404
                }
 
9405
        },
 
9406
 
 
9407
        /**
 
9408
         * On mouse up or touch end across the entire document, drop the selection.
 
9409
         */
 
9410
        drop: function (e) {
 
9411
                var chart = this.chart,
 
9412
                        hasPinched = this.hasPinched;
 
9413
 
 
9414
                if (this.selectionMarker) {
 
9415
                        var selectionData = {
 
9416
                                        xAxis: [],
 
9417
                                        yAxis: [],
 
9418
                                        originalEvent: e.originalEvent || e
 
9419
                                },
 
9420
                                selectionBox = this.selectionMarker,
 
9421
                                selectionLeft = selectionBox.x,
 
9422
                                selectionTop = selectionBox.y,
 
9423
                                runZoom;
 
9424
                        // a selection has been made
 
9425
                        if (this.hasDragged || hasPinched) {
 
9426
 
 
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));
 
9433
 
 
9434
                                                if (!isNaN(selectionMin) && !isNaN(selectionMax)) { // #859
 
9435
                                                        selectionData[axis.coll].push({
 
9436
                                                                axis: axis,
 
9437
                                                                min: mathMin(selectionMin, selectionMax), // for reversed axes,
 
9438
                                                                max: mathMax(selectionMin, selectionMax)
 
9439
                                                        });
 
9440
                                                        runZoom = true;
 
9441
                                                }
 
9442
                                        }
 
9443
                                });
 
9444
                                if (runZoom) {
 
9445
                                        fireEvent(chart, 'selection', selectionData, function (args) { 
 
9446
                                                chart.zoom(extend(args, hasPinched ? { animation: false } : null)); 
 
9447
                                        });
 
9448
                                }
 
9449
 
 
9450
                        }
 
9451
                        this.selectionMarker = this.selectionMarker.destroy();
 
9452
 
 
9453
                        // Reset scaling preview
 
9454
                        if (hasPinched) {
 
9455
                                this.scaleGroups();
 
9456
                        }
 
9457
                }
 
9458
 
 
9459
                // Reset all
 
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 = [];
 
9465
                }
 
9466
        },
 
9467
 
 
9468
        onContainerMouseDown: function (e) {
 
9469
 
 
9470
                e = this.normalize(e);
 
9471
 
 
9472
                // issue #295, dragging not always working in Firefox
 
9473
                if (e.preventDefault) {
 
9474
                        e.preventDefault();
 
9475
                }
 
9476
                
 
9477
                this.dragStart(e);
 
9478
        },
 
9479
 
 
9480
        
 
9481
 
 
9482
        onDocumentMouseUp: function (e) {
 
9483
                if (defined(hoverChartIndex)) {
 
9484
                        charts[hoverChartIndex].pointer.drop(e);
 
9485
                }
 
9486
        },
 
9487
 
 
9488
        /**
 
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. 
 
9491
         */
 
9492
        onDocumentMouseMove: function (e) {
 
9493
                var chart = this.chart,
 
9494
                        chartPosition = this.chartPosition,
 
9495
                        hoverSeries = chart.hoverSeries;
 
9496
 
 
9497
                e = this.normalize(e, chartPosition);
 
9498
 
 
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)) {
 
9502
                        this.reset();
 
9503
                }
 
9504
        },
 
9505
 
 
9506
        /**
 
9507
         * When mouse leaves the container, hide the tooltip.
 
9508
         */
 
9509
        onContainerMouseLeave: function () {
 
9510
                var chart = charts[hoverChartIndex];
 
9511
                if (chart) {
 
9512
                        chart.pointer.reset();
 
9513
                        chart.pointer.chartPosition = null; // also reset the chart position, used in #149 fix
 
9514
                }
 
9515
                hoverChartIndex = null;
 
9516
        },
 
9517
 
 
9518
        // The mousemove, touchmove and touchstart event handler
 
9519
        onContainerMouseMove: function (e) {
 
9520
 
 
9521
                var chart = this.chart;
 
9522
 
 
9523
                hoverChartIndex = chart.index;
 
9524
 
 
9525
                // normalize
 
9526
                e = this.normalize(e);          
 
9527
                
 
9528
                if (chart.mouseIsDown === 'mousedown') {
 
9529
                        this.drag(e);
 
9530
                } 
 
9531
                
 
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);
 
9536
                }
 
9537
        },
 
9538
 
 
9539
        /**
 
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.
 
9543
         */
 
9544
        inClass: function (element, className) {
 
9545
                var elemClassName;
 
9546
                while (element) {
 
9547
                        elemClassName = attr(element, 'class');
 
9548
                        if (elemClassName) {
 
9549
                                if (elemClassName.indexOf(className) !== -1) {
 
9550
                                        return true;
 
9551
                                } else if (elemClassName.indexOf(PREFIX + 'container') !== -1) {
 
9552
                                        return false;
 
9553
                                }
 
9554
                        }
 
9555
                        element = element.parentNode;
 
9556
                }               
 
9557
        },
 
9558
 
 
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
 
9563
                
 
9564
                if (series && !series.options.stickyTracking && !this.inClass(relatedTarget, PREFIX + 'tooltip') &&
 
9565
                                relatedSeries !== series) {
 
9566
                        series.onMouseOut();
 
9567
                }
 
9568
        },
 
9569
 
 
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,
 
9576
                        chartPosition,
 
9577
                        plotX,
 
9578
                        plotY;
 
9579
                
 
9580
                e = this.normalize(e);
 
9581
                e.cancelBubble = true; // IE specific
 
9582
 
 
9583
                if (!chart.cancelClick) {
 
9584
                        
 
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;
 
9590
 
 
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)
 
9597
                                });
 
9598
                        
 
9599
                                // the series click event
 
9600
                                fireEvent(hoverPoint.series, 'click', extend(e, {
 
9601
                                        point: hoverPoint
 
9602
                                }));
 
9603
 
 
9604
                                // the point click event
 
9605
                                if (chart.hoverPoint) { // it may be destroyed (#1844)
 
9606
                                        hoverPoint.firePointEvent('click', e);
 
9607
                                }
 
9608
 
 
9609
                        // When clicking outside a tracker, fire a chart event
 
9610
                        } else {
 
9611
                                extend(e, this.getCoordinates(e));
 
9612
 
 
9613
                                // fire a click event in the chart
 
9614
                                if (chart.isInsidePlot(e.chartX - plotLeft, e.chartY - plotTop)) {
 
9615
                                        fireEvent(chart, 'click', e);
 
9616
                                }
 
9617
                        }
 
9618
 
 
9619
 
 
9620
                }
 
9621
        },
 
9622
 
 
9623
        /**
 
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.
 
9627
         */
 
9628
        setDOMEvents: function () {
 
9629
 
 
9630
                var pointer = this,
 
9631
                        container = pointer.chart.container;
 
9632
 
 
9633
                container.onmousedown = function (e) {
 
9634
                        pointer.onContainerMouseDown(e);
 
9635
                };
 
9636
                container.onmousemove = function (e) {
 
9637
                        pointer.onContainerMouseMove(e);
 
9638
                };
 
9639
                container.onclick = function (e) {
 
9640
                        pointer.onContainerClick(e);
 
9641
                };
 
9642
                addEvent(container, 'mouseleave', pointer.onContainerMouseLeave);
 
9643
                addEvent(doc, 'mouseup', pointer.onDocumentMouseUp);
 
9644
 
 
9645
                if (hasTouch) {
 
9646
                        container.ontouchstart = function (e) {
 
9647
                                pointer.onContainerTouchStart(e);
 
9648
                        };
 
9649
                        container.ontouchmove = function (e) {
 
9650
                                pointer.onContainerTouchMove(e);
 
9651
                        };
 
9652
                        addEvent(doc, 'touchend', pointer.onDocumentTouchEnd);
 
9653
                }
 
9654
                
 
9655
        },
 
9656
 
 
9657
        /**
 
9658
         * Destroys the Pointer object and disconnects DOM events.
 
9659
         */
 
9660
        destroy: function () {
 
9661
                var prop;
 
9662
 
 
9663
                removeEvent(this.chart.container, 'mouseleave', this.onContainerMouseLeave);
 
9664
                removeEvent(doc, 'mouseup', this.onDocumentMouseUp);
 
9665
                removeEvent(doc, 'touchend', this.onDocumentTouchEnd);
 
9666
                
 
9667
                // memory and CPU leak
 
9668
                clearInterval(this.tooltipTimeout);
 
9669
 
 
9670
                for (prop in this) {
 
9671
                        this[prop] = null;
 
9672
                }
 
9673
        }
 
9674
};
 
9675
 
 
9676
 
 
9677
/* Support for touch devices */
 
9678
extend(Highcharts.Pointer.prototype, {
 
9679
 
 
9680
        /**
 
9681
         * Run translation operations
 
9682
         */
 
9683
        pinchTranslate: function (zoomHor, zoomVert, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch) {
 
9684
                if (zoomHor) {
 
9685
                        this.pinchTranslateDirection(true, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
 
9686
                }
 
9687
                if (zoomVert) {
 
9688
                        this.pinchTranslateDirection(false, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
 
9689
                }
 
9690
        },
 
9691
 
 
9692
        /**
 
9693
         * Run translation operations for each direction (horizontal and vertical) independently
 
9694
         */
 
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')],
 
9702
                        selectionWH,
 
9703
                        selectionXY,
 
9704
                        clipXY,
 
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],
 
9713
                        outOfBounds,
 
9714
                        transformScale,
 
9715
                        scaleKey,
 
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); 
 
9719
                                }
 
9720
                                
 
9721
                                clipXY = ((plotLeftTop - touch0Now) / scale) + touch0Start;
 
9722
                                selectionWH = chart['plot' + (horiz ? 'Width' : 'Height')] / scale;
 
9723
                        };
 
9724
 
 
9725
                // Set the scale, first pass
 
9726
                setScale();
 
9727
 
 
9728
                selectionXY = clipXY; // the clip position (x or y) is altered if out of bounds, the selection position is not
 
9729
 
 
9730
                // Out of bounds
 
9731
                if (selectionXY < bounds.min) {
 
9732
                        selectionXY = bounds.min;
 
9733
                        outOfBounds = true;
 
9734
                } else if (selectionXY + selectionWH > bounds.max) {
 
9735
                        selectionXY = bounds.max - selectionWH;
 
9736
                        outOfBounds = true;
 
9737
                }
 
9738
                
 
9739
                // Is the chart dragged off its bounds, determined by dataMin and dataMax?
 
9740
                if (outOfBounds) {
 
9741
 
 
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]);
 
9745
                        if (!singleTouch) {
 
9746
                                touch1Now -= 0.8 * (touch1Now - lastValidTouch[xy][1]);
 
9747
                        }
 
9748
 
 
9749
                        // Set the scale, second pass to adapt to the modified touchNow positions
 
9750
                        setScale();
 
9751
 
 
9752
                } else {
 
9753
                        lastValidTouch[xy] = [touch0Now, touch1Now];
 
9754
                }
 
9755
 
 
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;
 
9760
                }
 
9761
                scaleKey = inverted ? (horiz ? 'scaleY' : 'scaleX') : 'scale' + XY;
 
9762
                transformScale = inverted ? 1 / scale : scale;
 
9763
 
 
9764
                selectionMarker[wh] = selectionWH;
 
9765
                selectionMarker[xy] = selectionXY;
 
9766
                transform[scaleKey] = scale;
 
9767
                transform['translate' + XY] = (transformScale * plotLeftTop) + (touch0Now - (transformScale * touch0Start));
 
9768
        },
 
9769
        
 
9770
        /**
 
9771
         * Handle touch events with two touches
 
9772
         */
 
9773
        pinch: function (e) {
 
9774
 
 
9775
                var self = this,
 
9776
                        chart = self.chart,
 
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,
 
9786
                        transform = {},
 
9787
                        fireClickEvent = touchesLength === 1 && ((self.inClass(e.target, PREFIX + 'tracker') && 
 
9788
                                chart.runTrackerClick) || chart.runChartClick),
 
9789
                        clip = {};
 
9790
 
 
9791
                // On touch devices, only proceed to trigger click if a handler is defined
 
9792
                if ((hasZoom || followTouchMove) && !fireClickEvent) {
 
9793
                        e.preventDefault();
 
9794
                }
 
9795
                
 
9796
                // Normalize each touch
 
9797
                map(touches, function (e) {
 
9798
                        return self.normalize(e);
 
9799
                });
 
9800
                
 
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 };
 
9805
                        });
 
9806
                        lastValidTouch.x = [pinchDown[0].chartX, pinchDown[1] && pinchDown[1].chartX];
 
9807
                        lastValidTouch.y = [pinchDown[0].chartY, pinchDown[1] && pinchDown[1].chartY];
 
9808
 
 
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);
 
9818
 
 
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);
 
9822
                                }
 
9823
                        });
 
9824
                
 
9825
                // Event type is touchmove, handle panning and pinching
 
9826
                } else if (pinchDown.length) { // can be 0 when releasing, if touchend fires first
 
9827
                        
 
9828
 
 
9829
                        // Set the marker
 
9830
                        if (!selectionMarker) {
 
9831
                                self.selectionMarker = selectionMarker = extend({
 
9832
                                        destroy: noop
 
9833
                                }, chart.plotBox);
 
9834
                        }
 
9835
                        
 
9836
                        self.pinchTranslate(zoomHor, zoomVert, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
 
9837
 
 
9838
                        self.hasPinched = hasZoom;
 
9839
 
 
9840
                        // Scale and translate the groups to provide visual feedback during pinching
 
9841
                        self.scaleGroups(transform, clip);
 
9842
                        
 
9843
                        // Optionally move the tooltip on touchmove
 
9844
                        if (!hasZoom && followTouchMove && touchesLength === 1) {
 
9845
                                this.runPointActions(self.normalize(e));
 
9846
                        }
 
9847
                }
 
9848
        },
 
9849
 
 
9850
        onContainerTouchStart: function (e) {
 
9851
                var chart = this.chart;
 
9852
 
 
9853
                hoverChartIndex = chart.index;
 
9854
 
 
9855
                if (e.touches.length === 1) {
 
9856
 
 
9857
                        e = this.normalize(e);
 
9858
 
 
9859
                        if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) {
 
9860
 
 
9861
                                // Prevent the click pseudo event from firing unless it is set in the options
 
9862
                                /*if (!chart.runChartClick) {
 
9863
                                        e.preventDefault();
 
9864
                                }*/
 
9865
                        
 
9866
                                // Run mouse events and display tooltip etc
 
9867
                                this.runPointActions(e);
 
9868
 
 
9869
                                this.pinch(e);
 
9870
 
 
9871
                        } else {
 
9872
                                // Hide the tooltip on touching outside the plot area (#1203)
 
9873
                                this.reset();
 
9874
                        }
 
9875
 
 
9876
                } else if (e.touches.length === 2) {
 
9877
                        this.pinch(e);
 
9878
                }   
 
9879
        },
 
9880
 
 
9881
        onContainerTouchMove: function (e) {
 
9882
                if (e.touches.length === 1 || e.touches.length === 2) {
 
9883
                        this.pinch(e);
 
9884
                }
 
9885
        },
 
9886
 
 
9887
        onDocumentTouchEnd: function (e) {
 
9888
                if (defined(hoverChartIndex)) {
 
9889
                        charts[hoverChartIndex].pointer.drop(e);
 
9890
                }
 
9891
        }
 
9892
 
 
9893
});
 
9894
if (win.PointerEvent || win.MSPointerEvent) {
 
9895
        
 
9896
        // The touches object keeps track of the points being touched at all times
 
9897
        var touches = {},
 
9898
                hasPointerEvent = !!win.PointerEvent,
 
9899
                getWebkitTouches = function () {
 
9900
                        var key, fake = [];
 
9901
                        fake.item = function (i) { return this[i]; };
 
9902
                        for (key in touches) {
 
9903
                                if (touches.hasOwnProperty(key)) {
 
9904
                                        fake.push({
 
9905
                                                pageX: touches[key].pageX,
 
9906
                                                pageY: touches[key].pageY,
 
9907
                                                target: touches[key].target
 
9908
                                        });
 
9909
                                }
 
9910
                        }
 
9911
                        return fake;
 
9912
                },
 
9913
                translateMSPointer = function (e, method, wktype, callback) {
 
9914
                        var p;
 
9915
                        e = e.originalEvent || e;
 
9916
                        if ((e.pointerType === 'touch' || e.pointerType === e.MSPOINTER_TYPE_TOUCH) && charts[hoverChartIndex]) {
 
9917
                                callback(e);
 
9918
                                p = charts[hoverChartIndex].pointer;
 
9919
                                p[method]({
 
9920
                                        type: wktype,
 
9921
                                        target: e.currentTarget,
 
9922
                                        preventDefault: noop,
 
9923
                                        touches: getWebkitTouches()
 
9924
                                });                             
 
9925
                        }
 
9926
                };
 
9927
 
 
9928
        /**
 
9929
         * Extend the Pointer prototype with methods for each event handler and more
 
9930
         */
 
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 };
 
9935
                        });
 
9936
                },
 
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;
 
9942
                                }
 
9943
                        });
 
9944
                },
 
9945
                onDocumentPointerUp: function (e) {
 
9946
                        translateMSPointer(e, 'onContainerTouchEnd', 'touchend', function (e) {
 
9947
                                delete touches[e.pointerId];
 
9948
                        });
 
9949
                },
 
9950
 
 
9951
                /**
 
9952
                 * Add or remove the MS Pointer specific events
 
9953
                 */
 
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);
 
9958
                }
 
9959
        });
 
9960
 
 
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
 
9966
                });
 
9967
                proceed.call(this, chart, options);
 
9968
        });
 
9969
 
 
9970
        // Add IE specific touch events to chart
 
9971
        wrap(Pointer.prototype, 'setDOMEvents', function (proceed) {
 
9972
                proceed.apply(this);
 
9973
                this.batchMSEvents(addEvent);
 
9974
        });
 
9975
        // Destroy MS events also
 
9976
        wrap(Pointer.prototype, 'destroy', function (proceed) {
 
9977
                this.batchMSEvents(removeEvent);
 
9978
                proceed.call(this);
 
9979
        });
 
9980
}
 
9981
/**
 
9982
 * The overview of the chart's series
 
9983
 */
 
9984
var Legend = Highcharts.Legend = function (chart, options) {
 
9985
        this.init(chart, options);
 
9986
};
 
9987
 
 
9988
Legend.prototype = {
 
9989
        
 
9990
        /**
 
9991
         * Initialize the legend
 
9992
         */
 
9993
        init: function (chart, options) {
 
9994
                
 
9995
                var legend = this,
 
9996
                        itemStyle = options.itemStyle,
 
9997
                        padding = pick(options.padding, 8),
 
9998
                        itemMarginTop = options.itemMarginTop || 0;
 
9999
        
 
10000
                this.options = options;
 
10001
 
 
10002
                if (!options.enabled) {
 
10003
                        return;
 
10004
                }
 
10005
        
 
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);
 
10018
                legend.pages = [];
 
10019
 
 
10020
 
 
10021
                // Render it
 
10022
                legend.render();
 
10023
 
 
10024
                // move checkboxes
 
10025
                addEvent(legend.chart, 'endResize', function () { 
 
10026
                        legend.positionCheckboxes();
 
10027
                });
 
10028
 
 
10029
        },
 
10030
 
 
10031
        /**
 
10032
         * Set the colors for the legend item
 
10033
         * @param {Object} item A Series or Point instance
 
10034
         * @param {Object} visible Dimmed or colored
 
10035
         */
 
10036
        colorizeItem: function (item, visible) {
 
10037
                var legend = this,
 
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,
 
10046
                        symbolAttr = {
 
10047
                                stroke: symbolColor,
 
10048
                                fill: symbolColor
 
10049
                        },
 
10050
                        key,
 
10051
                        val;
 
10052
                
 
10053
                if (legendItem) {
 
10054
                        legendItem.css({ fill: textColor, color: textColor }); // color for #1553, oldIE
 
10055
                }
 
10056
                if (legendLine) {
 
10057
                        legendLine.attr({ stroke: symbolColor });
 
10058
                }
 
10059
                
 
10060
                if (legendSymbol) {
 
10061
                        
 
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;
 
10069
                                        }
 
10070
                                }
 
10071
                        }
 
10072
 
 
10073
                        legendSymbol.attr(symbolAttr);
 
10074
                }
 
10075
        },
 
10076
 
 
10077
        /**
 
10078
         * Position the legend item
 
10079
         * @param {Object} item A Series or Point instance
 
10080
         */
 
10081
        positionItem: function (item) {
 
10082
                var legend = this,
 
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;
 
10090
 
 
10091
                if (item.legendGroup) {
 
10092
                        item.legendGroup.translate(
 
10093
                                ltr ? itemX : legend.legendWidth - itemX - 2 * symbolPadding - 4,
 
10094
                                itemY
 
10095
                        );
 
10096
                }
 
10097
 
 
10098
                if (checkbox) {
 
10099
                        checkbox.x = itemX;
 
10100
                        checkbox.y = itemY;
 
10101
                }
 
10102
        },
 
10103
 
 
10104
        /**
 
10105
         * Destroy a single legend item
 
10106
         * @param {Object} item The series or point
 
10107
         */
 
10108
        destroyItem: function (item) {
 
10109
                var checkbox = item.checkbox;
 
10110
 
 
10111
                // destroy SVG elements
 
10112
                each(['legendItem', 'legendLine', 'legendSymbol', 'legendGroup'], function (key) {
 
10113
                        if (item[key]) {
 
10114
                                item[key] = item[key].destroy();
 
10115
                        }
 
10116
                });
 
10117
 
 
10118
                if (checkbox) {
 
10119
                        discardElement(item.checkbox);
 
10120
                }
 
10121
        },
 
10122
 
 
10123
        /**
 
10124
         * Destroys the legend.
 
10125
         */
 
10126
        destroy: function () {
 
10127
                var legend = this,
 
10128
                        legendGroup = legend.group,
 
10129
                        box = legend.box;
 
10130
 
 
10131
                if (box) {
 
10132
                        legend.box = box.destroy();
 
10133
                }
 
10134
 
 
10135
                if (legendGroup) {
 
10136
                        legend.group = legendGroup.destroy();
 
10137
                }
 
10138
        },
 
10139
 
 
10140
        /**
 
10141
         * Position the checkboxes after the width is determined
 
10142
         */
 
10143
        positionCheckboxes: function (scrollOffset) {
 
10144
                var alignAttr = this.group.alignAttr,
 
10145
                        translateY,
 
10146
                        clipHeight = this.clipHeight || this.legendHeight;
 
10147
 
 
10148
                if (alignAttr) {
 
10149
                        translateY = alignAttr.translateY;
 
10150
                        each(this.allItems, function (item) {
 
10151
                                var checkbox = item.checkbox,
 
10152
                                        top;
 
10153
                                
 
10154
                                if (checkbox) {
 
10155
                                        top = (translateY + checkbox.y + (scrollOffset || 0) + 3);
 
10156
                                        css(checkbox, {
 
10157
                                                left: (alignAttr.translateX + item.legendItemWidth + checkbox.x - 20) + PX,
 
10158
                                                top: top + PX,
 
10159
                                                display: top > translateY - 6 && top < translateY + clipHeight - 6 ? '' : NONE
 
10160
                                        });
 
10161
                                }
 
10162
                        });
 
10163
                }
 
10164
        },
 
10165
        
 
10166
        /**
 
10167
         * Render the legend title on top of the legend
 
10168
         */
 
10169
        renderTitle: function () {
 
10170
                var options = this.options,
 
10171
                        padding = this.padding,
 
10172
                        titleOptions = options.title,
 
10173
                        titleHeight = 0,
 
10174
                        bBox;
 
10175
                
 
10176
                if (titleOptions.text) {
 
10177
                        if (!this.title) {
 
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)
 
10181
                                        .add(this.group);
 
10182
                        }
 
10183
                        bBox = this.title.getBBox();
 
10184
                        titleHeight = bBox.height;
 
10185
                        this.offsetWidth = bBox.width; // #1717
 
10186
                        this.contentGroup.attr({ translateY: titleHeight });
 
10187
                }
 
10188
                this.titleHeight = titleHeight;
 
10189
        },
 
10190
 
 
10191
        /**
 
10192
         * Render a single specific legend item
 
10193
         * @param {Object} item A series or point
 
10194
         */
 
10195
        renderItem: function (item) {
 
10196
                var legend = this,
 
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,
 
10208
                        itemHeight,
 
10209
                        widthOption = options.width,
 
10210
                        itemMarginBottom = options.itemMarginBottom || 0,
 
10211
                        itemMarginTop = legend.itemMarginTop,
 
10212
                        initialItemX = legend.initialItemX,
 
10213
                        bBox,
 
10214
                        itemWidth,
 
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;
 
10220
 
 
10221
                if (!li) { // generate it once, later move it
 
10222
 
 
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);
 
10228
 
 
10229
                        // Draw the legend symbol inside the group box
 
10230
                        series.drawLegendSymbol(legend, item);
 
10231
 
 
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,
 
10236
                                        legend.baseline,
 
10237
                                        useHTML
 
10238
                                )
 
10239
                                .css(merge(item.visible ? itemStyle : itemHiddenStyle)) // merge to prevent modifying original (#1021)
 
10240
                                .attr({
 
10241
                                        align: ltr ? 'left' : 'right',
 
10242
                                        zIndex: 2
 
10243
                                })
 
10244
                                .add(item.legendGroup);
 
10245
 
 
10246
                        if (legend.setItemEvents) {
 
10247
                                legend.setItemEvents(item, li, useHTML, itemStyle, itemHiddenStyle);
 
10248
                        }                       
 
10249
 
 
10250
                        // Colorize the items
 
10251
                        legend.colorizeItem(item, item.visible);
 
10252
 
 
10253
                        // add the HTML checkbox on top
 
10254
                        if (showCheckbox) {
 
10255
                                legend.createCheckboxForItem(item);                             
 
10256
                        }
 
10257
                }
 
10258
 
 
10259
                // calculate the positions for the next line
 
10260
                bBox = li.getBBox();
 
10261
 
 
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);
 
10266
 
 
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
 
10273
                }
 
10274
 
 
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;
 
10280
                }*/
 
10281
 
 
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
 
10286
 
 
10287
                // cache the position of the newly generated or reordered items
 
10288
                item._legendItemPos = [legend.itemX, legend.itemY];
 
10289
 
 
10290
                // advance
 
10291
                if (horizontal) {
 
10292
                        legend.itemX += itemWidth;
 
10293
 
 
10294
                } else {
 
10295
                        legend.itemY += itemMarginTop + itemHeight + itemMarginBottom;
 
10296
                        legend.lastLineHeight = itemHeight;
 
10297
                }
 
10298
 
 
10299
                // the width of the widest item
 
10300
                legend.offsetWidth = widthOption || mathMax(
 
10301
                        (horizontal ? legend.itemX - initialItemX - itemDistance : itemWidth) + padding,
 
10302
                        legend.offsetWidth
 
10303
                );
 
10304
        },
 
10305
 
 
10306
        /**
 
10307
         * Get all items, which is one item per series for normal series and one item per point
 
10308
         * for pie series.
 
10309
         */
 
10310
        getAllItems: function () {
 
10311
                var allItems = [];
 
10312
                each(this.chart.series, function (series) {
 
10313
                        var seriesOptions = series.options;
 
10314
 
 
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)) {
 
10317
                                return;
 
10318
                        }
 
10319
 
 
10320
                        // use points or series for the legend item depending on legendType
 
10321
                        allItems = allItems.concat(
 
10322
                                        series.legendItems ||
 
10323
                                        (seriesOptions.legendType === 'point' ?
 
10324
                                                        series.data :
 
10325
                                                        series)
 
10326
                        );
 
10327
                });
 
10328
                return allItems;
 
10329
        },
 
10330
 
 
10331
        /**
 
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.
 
10335
         */
 
10336
        render: function () {
 
10337
                var legend = this,
 
10338
                        chart = legend.chart,
 
10339
                        renderer = chart.renderer,
 
10340
                        legendGroup = legend.group,
 
10341
                        allItems,
 
10342
                        display,
 
10343
                        legendWidth,
 
10344
                        legendHeight,
 
10345
                        box = legend.box,
 
10346
                        options = legend.options,
 
10347
                        padding = legend.padding,
 
10348
                        legendBorderWidth = options.borderWidth,
 
10349
                        legendBackgroundColor = options.backgroundColor;
 
10350
 
 
10351
                legend.itemX = legend.initialItemX;
 
10352
                legend.itemY = legend.initialItemY;
 
10353
                legend.offsetWidth = 0;
 
10354
                legend.lastItemY = 0;
 
10355
 
 
10356
                if (!legendGroup) {
 
10357
                        legend.group = legendGroup = renderer.g('legend')
 
10358
                                .attr({ zIndex: 7 }) 
 
10359
                                .add();
 
10360
                        legend.contentGroup = renderer.g()
 
10361
                                .attr({ zIndex: 1 }) // above background
 
10362
                                .add(legendGroup);
 
10363
                        legend.scrollGroup = renderer.g()
 
10364
                                .add(legend.contentGroup);
 
10365
                }
 
10366
                
 
10367
                legend.renderTitle();
 
10368
 
 
10369
                // add each series or point
 
10370
                allItems = legend.getAllItems();
 
10371
 
 
10372
                // sort by legendIndex
 
10373
                stableSort(allItems, function (a, b) {
 
10374
                        return ((a.options && a.options.legendIndex) || 0) - ((b.options && b.options.legendIndex) || 0);
 
10375
                });
 
10376
 
 
10377
                // reversed legend
 
10378
                if (options.reversed) {
 
10379
                        allItems.reverse();
 
10380
                }
 
10381
 
 
10382
                legend.allItems = allItems;
 
10383
                legend.display = display = !!allItems.length;
 
10384
 
 
10385
                // render the items
 
10386
                each(allItems, function (item) {
 
10387
                        legend.renderItem(item); 
 
10388
                });
 
10389
 
 
10390
                // Draw the border
 
10391
                legendWidth = options.width || legend.offsetWidth;
 
10392
                legendHeight = legend.lastItemY + legend.lastLineHeight + legend.titleHeight;
 
10393
                
 
10394
                
 
10395
                legendHeight = legend.handleOverflow(legendHeight);
 
10396
 
 
10397
                if (legendBorderWidth || legendBackgroundColor) {
 
10398
                        legendWidth += padding;
 
10399
                        legendHeight += padding;
 
10400
 
 
10401
                        if (!box) {
 
10402
                                legend.box = box = renderer.rect(
 
10403
                                        0,
 
10404
                                        0,
 
10405
                                        legendWidth,
 
10406
                                        legendHeight,
 
10407
                                        options.borderRadius,
 
10408
                                        legendBorderWidth || 0
 
10409
                                ).attr({
 
10410
                                        stroke: options.borderColor,
 
10411
                                        'stroke-width': legendBorderWidth || 0,
 
10412
                                        fill: legendBackgroundColor || NONE
 
10413
                                })
 
10414
                                .add(legendGroup)
 
10415
                                .shadow(options.shadow);
 
10416
                                box.isNew = true;
 
10417
 
 
10418
                        } else if (legendWidth > 0 && legendHeight > 0) {
 
10419
                                box[box.isNew ? 'attr' : 'animate'](
 
10420
                                        box.crisp({ width: legendWidth, height: legendHeight })
 
10421
                                );
 
10422
                                box.isNew = false;
 
10423
                        }
 
10424
 
 
10425
                        // hide the border if no items
 
10426
                        box[display ? 'show' : 'hide']();
 
10427
                }
 
10428
                
 
10429
                legend.legendWidth = legendWidth;
 
10430
                legend.legendHeight = legendHeight;
 
10431
 
 
10432
                // Now that the legend width and height are established, put the items in the 
 
10433
                // final position
 
10434
                each(allItems, function (item) {
 
10435
                        legend.positionItem(item);
 
10436
                });
 
10437
 
 
10438
                // 1.x compatibility: positioning based on style
 
10439
                /*var props = ['left', 'right', 'top', 'bottom'],
 
10440
                        prop,
 
10441
                        i = 4;
 
10442
                while (i--) {
 
10443
                        prop = props[i];
 
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);
 
10447
                        }
 
10448
                }*/
 
10449
 
 
10450
                if (display) {
 
10451
                        legendGroup.align(extend({
 
10452
                                width: legendWidth,
 
10453
                                height: legendHeight
 
10454
                        }, options), true, 'spacingBox');
 
10455
                }
 
10456
 
 
10457
                if (!chart.isResizing) {
 
10458
                        this.positionCheckboxes();
 
10459
                }
 
10460
        },
 
10461
        
 
10462
        /**
 
10463
         * Set up the overflow handling by adding navigation with up and down arrows below the
 
10464
         * legend.
 
10465
         */
 
10466
        handleOverflow: function (legendHeight) {
 
10467
                var legend = this,
 
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,
 
10475
                        clipHeight,
 
10476
                        clipRect = this.clipRect,
 
10477
                        navOptions = options.navigation,
 
10478
                        animation = pick(navOptions.animation, true),
 
10479
                        arrowSize = navOptions.arrowSize || 12,
 
10480
                        nav = this.nav,
 
10481
                        pages = this.pages,
 
10482
                        lastY,
 
10483
                        allItems = this.allItems;
 
10484
                        
 
10485
                // Adjust the height
 
10486
                if (options.layout === 'horizontal') {
 
10487
                        spaceHeight /= 2;
 
10488
                }
 
10489
                if (maxHeight) {
 
10490
                        spaceHeight = mathMin(spaceHeight, maxHeight);
 
10491
                }
 
10492
                
 
10493
                // Reset the legend height and adjust the clipping rectangle
 
10494
                pages.length = 0;
 
10495
                if (legendHeight > spaceHeight && !options.useHTML) {
 
10496
 
 
10497
                        this.clipHeight = clipHeight = spaceHeight - 20 - this.titleHeight - this.padding;
 
10498
                        this.currentPage = pick(this.currentPage, 1);
 
10499
                        this.fullHeight = legendHeight;
 
10500
                        
 
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;
 
10507
                                
 
10508
                                if (!len || (y - pages[len - 1] > clipHeight && (lastY || y) !== pages[len - 1])) {
 
10509
                                        pages.push(lastY || y);
 
10510
                                        len++;
 
10511
                                }
 
10512
                                
 
10513
                                if (i === allItems.length - 1 && y + h - pages[len - 1] > clipHeight) {
 
10514
                                        pages.push(y);
 
10515
                                }
 
10516
                                if (y !== lastY) {
 
10517
                                        lastY = y;
 
10518
                                }
 
10519
                        });
 
10520
 
 
10521
                        // Only apply clipping if needed. Clipping causes blurred legend in PDF export (#1787)
 
10522
                        if (!clipRect) {
 
10523
                                clipRect = legend.clipRect = renderer.clipRect(0, this.padding, 9999, 0);
 
10524
                                legend.contentGroup.clip(clipRect);
 
10525
                        }
 
10526
                        clipRect.attr({
 
10527
                                height: clipHeight
 
10528
                        });
 
10529
                        
 
10530
                        // Add navigation elements
 
10531
                        if (!nav) {
 
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);
 
10536
                                        })
 
10537
                                        .add(nav);
 
10538
                                this.pager = renderer.text('', 15, 10)
 
10539
                                        .css(navOptions.style)
 
10540
                                        .add(nav);
 
10541
                                this.down = renderer.symbol('triangle-down', 0, 0, arrowSize, arrowSize)
 
10542
                                        .on('click', function () {
 
10543
                                                legend.scroll(1, animation);
 
10544
                                        })
 
10545
                                        .add(nav);
 
10546
                        }
 
10547
                        
 
10548
                        // Set initial position
 
10549
                        legend.scroll(0);
 
10550
                        
 
10551
                        legendHeight = spaceHeight;
 
10552
                        
 
10553
                } else if (nav) {
 
10554
                        clipRect.attr({
 
10555
                                height: chart.chartHeight
 
10556
                        });
 
10557
                        nav.hide();
 
10558
                        this.scrollGroup.attr({
 
10559
                                translateY: 1
 
10560
                        });
 
10561
                        this.clipHeight = 0; // #1379
 
10562
                }
 
10563
                
 
10564
                return legendHeight;
 
10565
        },
 
10566
        
 
10567
        /**
 
10568
         * Scroll the legend by a number of pages
 
10569
         * @param {Object} scrollBy
 
10570
         * @param {Object} animation
 
10571
         */
 
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,
 
10582
                        scrollOffset;
 
10583
                
 
10584
                // When resizing while looking at the last page
 
10585
                if (currentPage > pageCount) {
 
10586
                        currentPage = pageCount;
 
10587
                }
 
10588
                
 
10589
                if (currentPage > 0) {
 
10590
                        
 
10591
                        if (animation !== UNDEFINED) {
 
10592
                                setAnimation(animation, this.chart);
 
10593
                        }
 
10594
                        
 
10595
                        this.nav.attr({
 
10596
                                translateX: padding,
 
10597
                                translateY: clipHeight + this.padding + 7 + this.titleHeight,
 
10598
                                visibility: VISIBLE
 
10599
                        });
 
10600
                        this.up.attr({
 
10601
                                        fill: currentPage === 1 ? inactiveColor : activeColor
 
10602
                                })
 
10603
                                .css({
 
10604
                                        cursor: currentPage === 1 ? 'default' : 'pointer'
 
10605
                                });
 
10606
                        pager.attr({
 
10607
                                text: currentPage + '/' + pageCount
 
10608
                        });
 
10609
                        this.down.attr({
 
10610
                                        x: 18 + this.pager.getBBox().width, // adjust to text width
 
10611
                                        fill: currentPage === pageCount ? inactiveColor : activeColor
 
10612
                                })
 
10613
                                .css({
 
10614
                                        cursor: currentPage === pageCount ? 'default' : 'pointer'
 
10615
                                });
 
10616
                        
 
10617
                        scrollOffset = -pages[currentPage - 1] + this.initialItemY;
 
10618
 
 
10619
                        this.scrollGroup.animate({
 
10620
                                translateY: scrollOffset
 
10621
                        });                     
 
10622
                        
 
10623
                        this.currentPage = currentPage;
 
10624
                        this.positionCheckboxes(scrollOffset);
 
10625
                }
 
10626
                        
 
10627
        }
 
10628
        
 
10629
};
 
10630
 
 
10631
/*
 
10632
 * LegendSymbolMixin
 
10633
 */ 
 
10634
 
 
10635
var LegendSymbolMixin = Highcharts.LegendSymbolMixin = {
 
10636
 
 
10637
        /**
 
10638
         * Get the series' symbol in the legend
 
10639
         * 
 
10640
         * @param {Object} legend The legend object
 
10641
         * @param {Object} item The series (this) or point
 
10642
         */
 
10643
        drawRectangle: function (legend, item) {
 
10644
                var symbolHeight = legend.options.symbolHeight || 12;
 
10645
                
 
10646
                item.legendSymbol = this.chart.renderer.rect(
 
10647
                        0,
 
10648
                        legend.baseline - 5 - (symbolHeight / 2),
 
10649
                        legend.symbolWidth,
 
10650
                        symbolHeight,
 
10651
                        pick(legend.options.symbolRadius, 2)
 
10652
                ).attr({
 
10653
                        zIndex: 3
 
10654
                }).add(item.legendGroup);               
 
10655
                
 
10656
        },
 
10657
 
 
10658
        /**
 
10659
         * Get the series' symbol in the legend. This method should be overridable to create custom 
 
10660
         * symbols through Highcharts.seriesTypes[type].prototype.drawLegendSymbols.
 
10661
         * 
 
10662
         * @param {Object} legend The legend object
 
10663
         */
 
10664
        drawLineMarker: function (legend) {
 
10665
 
 
10666
                var options = this.options,
 
10667
                        markerOptions = options.marker,
 
10668
                        radius,
 
10669
                        legendOptions = legend.options,
 
10670
                        legendSymbol,
 
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),
 
10675
                        attr;
 
10676
                        
 
10677
                // Draw the line
 
10678
                if (options.lineWidth) {
 
10679
                        attr = {
 
10680
                                'stroke-width': options.lineWidth
 
10681
                        };
 
10682
                        if (options.dashStyle) {
 
10683
                                attr.dashstyle = options.dashStyle;
 
10684
                        }
 
10685
                        this.legendLine = renderer.path([
 
10686
                                M,
 
10687
                                0,
 
10688
                                verticalCenter,
 
10689
                                L,
 
10690
                                symbolWidth,
 
10691
                                verticalCenter
 
10692
                        ])
 
10693
                        .attr(attr)
 
10694
                        .add(legendItemGroup);
 
10695
                }
 
10696
                
 
10697
                // Draw the marker
 
10698
                if (markerOptions && markerOptions.enabled) {
 
10699
                        radius = markerOptions.radius;
 
10700
                        this.legendSymbol = legendSymbol = renderer.symbol(
 
10701
                                this.symbol,
 
10702
                                (symbolWidth / 2) - radius,
 
10703
                                verticalCenter - radius,
 
10704
                                2 * radius,
 
10705
                                2 * radius
 
10706
                        )
 
10707
                        .add(legendItemGroup);
 
10708
                        legendSymbol.isMarker = true;
 
10709
                }
 
10710
        }
 
10711
};
 
10712
 
 
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) {
 
10719
                var legend = this,
 
10720
                        runPositionItem = function () { // If chart destroyed in sync, this is undefined (#2030)
 
10721
                                if (item._legendItemPos) {
 
10722
                                        proceed.call(legend, item);
 
10723
                                }
 
10724
                        };
 
10725
 
 
10726
                if (legend.chart.renderer.forExport) {
 
10727
                        runPositionItem();
 
10728
                } else {
 
10729
                        setTimeout(runPositionItem);
 
10730
                }
 
10731
        });
 
10732
}
 
10733
/**
 
10734
 * The chart class
 
10735
 * @param {Object} options
 
10736
 * @param {Function} callback Function to run when the chart has loaded
 
10737
 */
 
10738
function Chart() {
 
10739
        this.init.apply(this, arguments);
 
10740
}
 
10741
 
 
10742
Chart.prototype = {
 
10743
 
 
10744
        /**
 
10745
         * Initialize the chart
 
10746
         */
 
10747
        init: function (userOptions, callback) {
 
10748
 
 
10749
                // Handle regular options
 
10750
                var options,
 
10751
                        seriesOptions = userOptions.series; // skip merging data points to increase performance
 
10752
 
 
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;
 
10757
 
 
10758
                var optionsChart = options.chart;
 
10759
                
 
10760
                // Create margin & spacing array
 
10761
                this.margin = this.splashArray('margin', optionsChart);
 
10762
                this.spacing = this.splashArray('spacing', optionsChart);
 
10763
 
 
10764
                var chartEvents = optionsChart.events;
 
10765
 
 
10766
                //this.runChartClick = chartEvents && !!chartEvents.click;
 
10767
                this.bounds = { h: {}, v: {} }; // Pixel data bounds for touch zoom
 
10768
 
 
10769
                this.callback = callback;
 
10770
                this.isResizing = 0;
 
10771
                this.options = options;
 
10772
                //chartTitleOptions = UNDEFINED;
 
10773
                //chartSubtitleOptions = UNDEFINED;
 
10774
 
 
10775
                this.axes = [];
 
10776
                this.series = [];
 
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;
 
10791
 
 
10792
                //this.renderTo = UNDEFINED;
 
10793
                //this.renderToClone = UNDEFINED;
 
10794
 
 
10795
                //this.spacingBox = UNDEFINED
 
10796
 
 
10797
                //this.legend = UNDEFINED;
 
10798
 
 
10799
                // Elements
 
10800
                //this.chartBackground = UNDEFINED;
 
10801
                //this.plotBackground = UNDEFINED;
 
10802
                //this.plotBGImage = UNDEFINED;
 
10803
                //this.plotBorder = UNDEFINED;
 
10804
                //this.loadingDiv = UNDEFINED;
 
10805
                //this.loadingSpan = UNDEFINED;
 
10806
 
 
10807
                var chart = this,
 
10808
                        eventType;
 
10809
 
 
10810
                // Add the chart to the global lookup
 
10811
                chart.index = charts.length;
 
10812
                charts.push(chart);
 
10813
 
 
10814
                // Set up auto resize
 
10815
                if (optionsChart.reflow !== false) {
 
10816
                        addEvent(chart, 'load', function () {
 
10817
                                chart.initReflow();
 
10818
                        });
 
10819
                }
 
10820
 
 
10821
                // Chart event handlers
 
10822
                if (chartEvents) {
 
10823
                        for (eventType in chartEvents) {
 
10824
                                addEvent(chart, eventType, chartEvents[eventType]);
 
10825
                        }
 
10826
                }
 
10827
 
 
10828
                chart.xAxis = [];
 
10829
                chart.yAxis = [];
 
10830
 
 
10831
                // Expose methods and variables
 
10832
                chart.animation = useCanVG ? false : pick(optionsChart.animation, true);
 
10833
                chart.pointCount = 0;
 
10834
                chart.counters = new ChartCounters();
 
10835
 
 
10836
                chart.firstRender();
 
10837
        },
 
10838
 
 
10839
        /**
 
10840
         * Initialize an individual series, called internally before render time
 
10841
         */
 
10842
        initSeries: function (options) {
 
10843
                var chart = this,
 
10844
                        optionsChart = chart.options.chart,
 
10845
                        type = options.type || optionsChart.type || optionsChart.defaultSeriesType,
 
10846
                        series,
 
10847
                        constr = seriesTypes[type];
 
10848
 
 
10849
                // No such series type
 
10850
                if (!constr) {
 
10851
                        error(17, true);
 
10852
                }
 
10853
 
 
10854
                series = new constr();
 
10855
                series.init(this, options);
 
10856
                return series;
 
10857
        },
 
10858
 
 
10859
        /**
 
10860
         * Check whether a given point is within the plot area
 
10861
         *
 
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
 
10865
         */
 
10866
        isInsidePlot: function (plotX, plotY, inverted) {
 
10867
                var x = inverted ? plotY : plotX,
 
10868
                        y = inverted ? plotX : plotY;
 
10869
                        
 
10870
                return x >= 0 &&
 
10871
                        x <= this.plotWidth &&
 
10872
                        y >= 0 &&
 
10873
                        y <= this.plotHeight;
 
10874
        },
 
10875
 
 
10876
        /**
 
10877
         * Adjust all axes tick amounts
 
10878
         */
 
10879
        adjustTickAmounts: function () {
 
10880
                if (this.options.chart.alignTicks !== false) {
 
10881
                        each(this.axes, function (axis) {
 
10882
                                axis.adjustTickAmount();
 
10883
                        });
 
10884
                }
 
10885
                this.maxTicks = null;
 
10886
        },
 
10887
 
 
10888
        /**
 
10889
         * Redraw legend, axes or series based on updated data
 
10890
         *
 
10891
         * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
 
10892
         *    configuration
 
10893
         */
 
10894
        redraw: function (animation) {
 
10895
                var chart = this,
 
10896
                        axes = chart.axes,
 
10897
                        series = chart.series,
 
10898
                        pointer = chart.pointer,
 
10899
                        legend = chart.legend,
 
10900
                        redrawLegend = chart.isDirtyLegend,
 
10901
                        hasStackedSeries,
 
10902
                        hasDirtyStacks,
 
10903
                        isDirtyBox = chart.isDirtyBox, // todo: check if it has actually changed?
 
10904
                        seriesLength = series.length,
 
10905
                        i = seriesLength,
 
10906
                        serie,
 
10907
                        renderer = chart.renderer,
 
10908
                        isHiddenChart = renderer.isHidden(),
 
10909
                        afterRedraw = [];
 
10910
                        
 
10911
                setAnimation(animation, chart);
 
10912
                
 
10913
                if (isHiddenChart) {
 
10914
                        chart.cloneRenderTo();
 
10915
                }
 
10916
 
 
10917
                // Adjust title layout (reflow multiline text)
 
10918
                chart.layOutTitles();
 
10919
 
 
10920
                // link stacked series
 
10921
                while (i--) {
 
10922
                        serie = series[i];
 
10923
 
 
10924
                        if (serie.options.stacking) {
 
10925
                                hasStackedSeries = true;
 
10926
                                
 
10927
                                if (serie.isDirty) {
 
10928
                                        hasDirtyStacks = true;
 
10929
                                        break;
 
10930
                                }
 
10931
                        }
 
10932
                }
 
10933
                if (hasDirtyStacks) { // mark others as dirty
 
10934
                        i = seriesLength;
 
10935
                        while (i--) {
 
10936
                                serie = series[i];
 
10937
                                if (serie.options.stacking) {
 
10938
                                        serie.isDirty = true;
 
10939
                                }
 
10940
                        }
 
10941
                }
 
10942
 
 
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;
 
10948
                                }
 
10949
                        }
 
10950
                });
 
10951
 
 
10952
                // handle added or removed series
 
10953
                if (redrawLegend && legend.options.enabled) { // series or pie points are added or removed
 
10954
                        // draw legend graphics
 
10955
                        legend.render();
 
10956
 
 
10957
                        chart.isDirtyLegend = false;
 
10958
                }
 
10959
 
 
10960
                // reset stacks
 
10961
                if (hasStackedSeries) {
 
10962
                        chart.getStacks();
 
10963
                }
 
10964
 
 
10965
 
 
10966
                if (chart.hasCartesianSeries) {
 
10967
                        if (!chart.isResizing) {
 
10968
 
 
10969
                                // reset maxTicks
 
10970
                                chart.maxTicks = null;
 
10971
 
 
10972
                                // set axes scales
 
10973
                                each(axes, function (axis) {
 
10974
                                        axis.setScale();
 
10975
                                });
 
10976
                        }
 
10977
 
 
10978
                        chart.adjustTickAmounts();
 
10979
                        chart.getMargins();
 
10980
 
 
10981
                        // If one axis is dirty, all axes must be redrawn (#792, #2169)
 
10982
                        each(axes, function (axis) {
 
10983
                                if (axis.isDirty) {
 
10984
                                        isDirtyBox = true;
 
10985
                                }
 
10986
                        });
 
10987
 
 
10988
                        // redraw axes
 
10989
                        each(axes, function (axis) {
 
10990
                                
 
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;
 
10997
                                        });
 
10998
                                }
 
10999
                                
 
11000
                                if (isDirtyBox || hasStackedSeries) {
 
11001
                                        axis.redraw();
 
11002
                                }
 
11003
                        });
 
11004
 
 
11005
 
 
11006
                }
 
11007
                // the plot areas size has changed
 
11008
                if (isDirtyBox) {
 
11009
                        chart.drawChartBox();
 
11010
                }
 
11011
 
 
11012
 
 
11013
                // redraw affected series
 
11014
                each(series, function (serie) {
 
11015
                        if (serie.isDirty && serie.visible &&
 
11016
                                        (!serie.isCartesian || serie.xAxis)) { // issue #153
 
11017
                                serie.redraw();
 
11018
                        }
 
11019
                });
 
11020
 
 
11021
                // move tooltip or reset
 
11022
                if (pointer) {
 
11023
                        pointer.reset(true);
 
11024
                }
 
11025
 
 
11026
                // redraw if canvas
 
11027
                renderer.draw();
 
11028
 
 
11029
                // fire the event
 
11030
                fireEvent(chart, 'redraw'); // jQuery breaks this when calling it from addEvent. Overwrites chart.redraw
 
11031
                
 
11032
                if (isHiddenChart) {
 
11033
                        chart.cloneRenderTo(true);
 
11034
                }
 
11035
                
 
11036
                // Fire callbacks that are put on hold until after the redraw
 
11037
                each(afterRedraw, function (callback) {
 
11038
                        callback.call();
 
11039
                });
 
11040
        },
 
11041
 
 
11042
        /**
 
11043
         * Get an axis, series or point object by id.
 
11044
         * @param id {String} The id as given in the configuration options
 
11045
         */
 
11046
        get: function (id) {
 
11047
                var chart = this,
 
11048
                        axes = chart.axes,
 
11049
                        series = chart.series;
 
11050
 
 
11051
                var i,
 
11052
                        j,
 
11053
                        points;
 
11054
 
 
11055
                // search axes
 
11056
                for (i = 0; i < axes.length; i++) {
 
11057
                        if (axes[i].options.id === id) {
 
11058
                                return axes[i];
 
11059
                        }
 
11060
                }
 
11061
 
 
11062
                // search series
 
11063
                for (i = 0; i < series.length; i++) {
 
11064
                        if (series[i].options.id === id) {
 
11065
                                return series[i];
 
11066
                        }
 
11067
                }
 
11068
 
 
11069
                // search points
 
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) {
 
11074
                                        return points[j];
 
11075
                                }
 
11076
                        }
 
11077
                }
 
11078
                return null;
 
11079
        },
 
11080
 
 
11081
        /**
 
11082
         * Create the Axis instances based on the config options
 
11083
         */
 
11084
        getAxes: function () {
 
11085
                var chart = this,
 
11086
                        options = this.options,
 
11087
                        xAxisOptions = options.xAxis = splat(options.xAxis || {}),
 
11088
                        yAxisOptions = options.yAxis = splat(options.yAxis || {}),
 
11089
                        optionsArray,
 
11090
                        axis;
 
11091
 
 
11092
                // make sure the options are arrays and add some members
 
11093
                each(xAxisOptions, function (axis, i) {
 
11094
                        axis.index = i;
 
11095
                        axis.isX = true;
 
11096
                });
 
11097
 
 
11098
                each(yAxisOptions, function (axis, i) {
 
11099
                        axis.index = i;
 
11100
                });
 
11101
 
 
11102
                // concatenate all axis options into one array
 
11103
                optionsArray = xAxisOptions.concat(yAxisOptions);
 
11104
 
 
11105
                each(optionsArray, function (axisOptions) {
 
11106
                        axis = new Axis(chart, axisOptions);
 
11107
                });
 
11108
 
 
11109
                chart.adjustTickAmounts();
 
11110
        },
 
11111
 
 
11112
 
 
11113
        /**
 
11114
         * Get the currently selected points from all series
 
11115
         */
 
11116
        getSelectedPoints: function () {
 
11117
                var points = [];
 
11118
                each(this.series, function (serie) {
 
11119
                        points = points.concat(grep(serie.points || [], function (point) {
 
11120
                                return point.selected;
 
11121
                        }));
 
11122
                });
 
11123
                return points;
 
11124
        },
 
11125
 
 
11126
        /**
 
11127
         * Get the currently selected series
 
11128
         */
 
11129
        getSelectedSeries: function () {
 
11130
                return grep(this.series, function (serie) {
 
11131
                        return serie.selected;
 
11132
                });
 
11133
        },
 
11134
 
 
11135
        /**
 
11136
         * Generate stacks for each series and calculate stacks total values
 
11137
         */
 
11138
        getStacks: function () {
 
11139
                var chart = this;
 
11140
 
 
11141
                // reset stacks for each yAxis
 
11142
                each(chart.yAxis, function (axis) {
 
11143
                        if (axis.stacks && axis.hasVisibleSeries) {
 
11144
                                axis.oldStacks = axis.stacks;
 
11145
                        }
 
11146
                });
 
11147
 
 
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, '');
 
11151
                        }
 
11152
                });
 
11153
        },      
 
11154
 
 
11155
        /**
 
11156
         * Show the title and subtitle of the chart
 
11157
         *
 
11158
         * @param titleOptions {Object} New title options
 
11159
         * @param subtitleOptions {Object} New subtitle options
 
11160
         *
 
11161
         */
 
11162
        setTitle: function (titleOptions, subtitleOptions, redraw) {
 
11163
                var chart = this,
 
11164
                        options = chart.options,
 
11165
                        chartTitleOptions,
 
11166
                        chartSubtitleOptions;
 
11167
 
 
11168
                chartTitleOptions = options.title = merge(options.title, titleOptions);
 
11169
                chartSubtitleOptions = options.subtitle = merge(options.subtitle, subtitleOptions);
 
11170
 
 
11171
                // add title and subtitle
 
11172
                each([
 
11173
                        ['title', titleOptions, chartTitleOptions],
 
11174
                        ['subtitle', subtitleOptions, chartSubtitleOptions]
 
11175
                ], function (arr) {
 
11176
                        var name = arr[0],
 
11177
                                title = chart[name],
 
11178
                                titleOptions = arr[1],
 
11179
                                chartTitleOptions = arr[2];
 
11180
 
 
11181
                        if (title && titleOptions) {
 
11182
                                chart[name] = title = title.destroy(); // remove old
 
11183
                        }
 
11184
                        
 
11185
                        if (chartTitleOptions && chartTitleOptions.text && !title) {
 
11186
                                chart[name] = chart.renderer.text(
 
11187
                                        chartTitleOptions.text,
 
11188
                                        0,
 
11189
                                        0,
 
11190
                                        chartTitleOptions.useHTML
 
11191
                                )
 
11192
                                .attr({
 
11193
                                        align: chartTitleOptions.align,
 
11194
                                        'class': PREFIX + name,
 
11195
                                        zIndex: chartTitleOptions.zIndex || 4
 
11196
                                })
 
11197
                                .css(chartTitleOptions.style)
 
11198
                                .add();
 
11199
                        }       
 
11200
                });
 
11201
                chart.layOutTitles(redraw);
 
11202
        },
 
11203
 
 
11204
        /**
 
11205
         * Lay out the chart titles and cache the full offset height for use in getMargins
 
11206
         */
 
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,
 
11214
                        requiresDirtyBox,
 
11215
                        autoWidth = this.spacingBox.width - 44; // 44 makes room for default context button
 
11216
 
 
11217
                if (title) {
 
11218
                        title
 
11219
                                .css({ width: (titleOptions.width || autoWidth) + PX })
 
11220
                                .align(extend({ y: 15 }, titleOptions), false, 'spacingBox');
 
11221
                        
 
11222
                        if (!titleOptions.floating && !titleOptions.verticalAlign) {
 
11223
                                titleOffset = title.getBBox().height;
 
11224
 
 
11225
                                // Adjust for browser consistency + backwards compat after #776 fix
 
11226
                                if (titleOffset >= 18 && titleOffset <= 25) {
 
11227
                                        titleOffset = 15; 
 
11228
                                }
 
11229
                        }
 
11230
                }
 
11231
                if (subtitle) {
 
11232
                        subtitle
 
11233
                                .css({ width: (subtitleOptions.width || autoWidth) + PX })
 
11234
                                .align(extend({ y: titleOffset + titleOptions.margin }, subtitleOptions), false, 'spacingBox');
 
11235
                        
 
11236
                        if (!subtitleOptions.floating && !subtitleOptions.verticalAlign) {
 
11237
                                titleOffset = mathCeil(titleOffset + subtitle.getBBox().height);
 
11238
                        }
 
11239
                }
 
11240
 
 
11241
                requiresDirtyBox = this.titleOffset !== titleOffset;                            
 
11242
                this.titleOffset = titleOffset; // used in getMargins
 
11243
 
 
11244
                if (!this.isDirtyBox && requiresDirtyBox) {
 
11245
                        this.isDirtyBox = requiresDirtyBox;
 
11246
                        // Redraw if necessary (#2719, #2744)           
 
11247
                        if (this.hasRendered && pick(redraw, true) && this.isDirtyBox) {
 
11248
                                this.redraw();
 
11249
                        }
 
11250
                }
 
11251
        },
 
11252
 
 
11253
        /**
 
11254
         * Get chart width and height according to options and container size
 
11255
         */
 
11256
        getChartSize: function () {
 
11257
                var chart = this,
 
11258
                        optionsChart = chart.options.chart,
 
11259
                        widthOption = optionsChart.width,
 
11260
                        heightOption = optionsChart.height,
 
11261
                        renderTo = chart.renderToClone || chart.renderTo;
 
11262
 
 
11263
                // get inner width and height from jQuery (#824)
 
11264
                if (!defined(widthOption)) {
 
11265
                        chart.containerWidth = adapterRun(renderTo, 'width');
 
11266
                }
 
11267
                if (!defined(heightOption)) {
 
11268
                        chart.containerHeight = adapterRun(renderTo, 'height');
 
11269
                }
 
11270
                
 
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));
 
11275
        },
 
11276
 
 
11277
        /**
 
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
 
11280
         */
 
11281
        cloneRenderTo: function (revert) {
 
11282
                var clone = this.renderToClone,
 
11283
                        container = this.container;
 
11284
                
 
11285
                // Destroy the clone and bring the container back to the real renderTo div
 
11286
                if (revert) {
 
11287
                        if (clone) {
 
11288
                                this.renderTo.appendChild(container);
 
11289
                                discardElement(clone);
 
11290
                                delete this.renderToClone;
 
11291
                        }
 
11292
                
 
11293
                // Set up the clone
 
11294
                } else {
 
11295
                        if (container && container.parentNode === this.renderTo) {
 
11296
                                this.renderTo.removeChild(container); // do not clone this
 
11297
                        }
 
11298
                        this.renderToClone = clone = this.renderTo.cloneNode(0);
 
11299
                        css(clone, {
 
11300
                                position: ABSOLUTE,
 
11301
                                top: '-9999px',
 
11302
                                display: 'block' // #833
 
11303
                        });
 
11304
                        if (clone.style.setProperty) { // #2631
 
11305
                                clone.style.setProperty('display', 'block', 'important');
 
11306
                        }
 
11307
                        doc.body.appendChild(clone);
 
11308
                        if (container) {
 
11309
                                clone.appendChild(container);
 
11310
                        }
 
11311
                }
 
11312
        },
 
11313
 
 
11314
        /**
 
11315
         * Get the containing element, determine the size and create the inner container
 
11316
         * div to hold the chart
 
11317
         */
 
11318
        getContainer: function () {
 
11319
                var chart = this,
 
11320
                        container,
 
11321
                        optionsChart = chart.options.chart,
 
11322
                        chartWidth,
 
11323
                        chartHeight,
 
11324
                        renderTo,
 
11325
                        indexAttrName = 'data-highcharts-chart',
 
11326
                        oldChartIndex,
 
11327
                        containerId;
 
11328
 
 
11329
                chart.renderTo = renderTo = optionsChart.renderTo;
 
11330
                containerId = PREFIX + idCounter++;
 
11331
 
 
11332
                if (isString(renderTo)) {
 
11333
                        chart.renderTo = renderTo = doc.getElementById(renderTo);
 
11334
                }
 
11335
                
 
11336
                // Display an error if the renderTo is wrong
 
11337
                if (!renderTo) {
 
11338
                        error(13, true);
 
11339
                }
 
11340
                
 
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();
 
11348
                }               
 
11349
                
 
11350
                // Make a reference to the chart from the div
 
11351
                attr(renderTo, indexAttrName, chart.index);
 
11352
 
 
11353
                // remove previous chart
 
11354
                renderTo.innerHTML = '';
 
11355
 
 
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();
 
11363
                }
 
11364
 
 
11365
                // get the width and height
 
11366
                chart.getChartSize();
 
11367
                chartWidth = chart.chartWidth;
 
11368
                chartHeight = chart.chartHeight;
 
11369
 
 
11370
                // create the inner container
 
11371
                chart.container = container = createElement(DIV, {
 
11372
                                className: PREFIX + 'container' +
 
11373
                                        (optionsChart.className ? ' ' + optionsChart.className : ''),
 
11374
                                id: containerId
 
11375
                        }, extend({
 
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,
 
11381
                                textAlign: 'left',
 
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
 
11387
                );
 
11388
 
 
11389
                // cache the cursor (#1650)
 
11390
                chart._cursor = container.style.cursor;
 
11391
 
 
11392
                // Initialize the renderer
 
11393
                chart.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);
 
11397
 
 
11398
                if (useCanVG) {
 
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);
 
11402
                }
 
11403
        },
 
11404
 
 
11405
        /**
 
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
 
11409
         */
 
11410
        getMargins: function () {
 
11411
                var chart = this,
 
11412
                        spacing = chart.spacing,
 
11413
                        axisOffset,
 
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;
 
11423
 
 
11424
                chart.resetMargins();
 
11425
                axisOffset = chart.axisOffset;
 
11426
 
 
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]);
 
11430
                }
 
11431
                
 
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(
 
11437
                                                chart.marginRight,
 
11438
                                                legend.legendWidth - legendX + legendMargin + spacing[1]
 
11439
                                        );
 
11440
                                }
 
11441
                        } else if (align === 'left') {
 
11442
                                if (!defined(margin[3])) {
 
11443
                                        chart.plotLeft = mathMax(
 
11444
                                                chart.plotLeft,
 
11445
                                                legend.legendWidth + legendX + legendMargin + spacing[3]
 
11446
                                        );
 
11447
                                }
 
11448
 
 
11449
                        } else if (verticalAlign === 'top') {
 
11450
                                if (!defined(margin[0])) {
 
11451
                                        chart.plotTop = mathMax(
 
11452
                                                chart.plotTop,
 
11453
                                                legend.legendHeight + legendY + legendMargin + spacing[0]
 
11454
                                        );
 
11455
                                }
 
11456
 
 
11457
                        } else if (verticalAlign === 'bottom') {
 
11458
                                if (!defined(margin[2])) {
 
11459
                                        chart.marginBottom = mathMax(
 
11460
                                                chart.marginBottom,
 
11461
                                                legend.legendHeight - legendY + legendMargin + spacing[2]
 
11462
                                        );
 
11463
                                }
 
11464
                        }
 
11465
                }
 
11466
 
 
11467
                // adjust for scroller
 
11468
                if (chart.extraBottomMargin) {
 
11469
                        chart.marginBottom += chart.extraBottomMargin;
 
11470
                }
 
11471
                if (chart.extraTopMargin) {
 
11472
                        chart.plotTop += chart.extraTopMargin;
 
11473
                }
 
11474
 
 
11475
                // pre-render axes to get labels offset width
 
11476
                if (chart.hasCartesianSeries) {
 
11477
                        each(chart.axes, function (axis) {
 
11478
                                axis.getOffset();
 
11479
                        });
 
11480
                }
 
11481
                
 
11482
                if (!defined(margin[3])) {
 
11483
                        chart.plotLeft += axisOffset[3];
 
11484
                }
 
11485
                if (!defined(margin[0])) {
 
11486
                        chart.plotTop += axisOffset[0];
 
11487
                }
 
11488
                if (!defined(margin[2])) {
 
11489
                        chart.marginBottom += axisOffset[2];
 
11490
                }
 
11491
                if (!defined(margin[1])) {
 
11492
                        chart.marginRight += axisOffset[1];
 
11493
                }
 
11494
 
 
11495
                chart.setChartSize();
 
11496
 
 
11497
        },
 
11498
 
 
11499
        /**
 
11500
         * Resize the chart to its container if size is not explicitly set
 
11501
         */
 
11502
        reflow: function (e) {
 
11503
                var chart = this,
 
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;
 
11513
                                }
 
11514
                        };
 
11515
                        
 
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)
 
11524
                                        doReflow();
 
11525
                                }
 
11526
                        }
 
11527
                        chart.containerWidth = width;
 
11528
                        chart.containerHeight = height;
 
11529
                }
 
11530
        },
 
11531
 
 
11532
        /**
 
11533
         * Add the event handlers necessary for auto resizing
 
11534
         */
 
11535
        initReflow: function () {
 
11536
                var chart = this,
 
11537
                        reflow = function (e) {
 
11538
                                chart.reflow(e);
 
11539
                        };
 
11540
                        
 
11541
                
 
11542
                addEvent(win, 'resize', reflow);
 
11543
                addEvent(chart, 'destroy', function () {
 
11544
                        removeEvent(win, 'resize', reflow);
 
11545
                });
 
11546
        },
 
11547
 
 
11548
        /**
 
11549
         * Resize the chart to a given width and height
 
11550
         * @param {Number} width
 
11551
         * @param {Number} height
 
11552
         * @param {Object|Boolean} animation
 
11553
         */
 
11554
        setSize: function (width, height, animation) {
 
11555
                var chart = this,
 
11556
                        chartWidth,
 
11557
                        chartHeight,
 
11558
                        fireEndResize;
 
11559
 
 
11560
                // Handle the isResizing counter
 
11561
                chart.isResizing += 1;
 
11562
                fireEndResize = function () {
 
11563
                        if (chart) {
 
11564
                                fireEvent(chart, 'endResize', null, function () {
 
11565
                                        chart.isResizing -= 1;
 
11566
                                });
 
11567
                        }
 
11568
                };
 
11569
 
 
11570
                // set the animation for the current process
 
11571
                setAnimation(animation, chart);
 
11572
 
 
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;
 
11578
                }
 
11579
                if (defined(height)) {
 
11580
                        chart.chartHeight = chartHeight = mathMax(0, mathRound(height));
 
11581
                }
 
11582
 
 
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);
 
11588
 
 
11589
                chart.setChartSize(true);
 
11590
                chart.renderer.setSize(chartWidth, chartHeight, animation);
 
11591
 
 
11592
                // handle axes
 
11593
                chart.maxTicks = null;
 
11594
                each(chart.axes, function (axis) {
 
11595
                        axis.isDirty = true;
 
11596
                        axis.setScale();
 
11597
                });
 
11598
 
 
11599
                // make sure non-cartesian series are also handled
 
11600
                each(chart.series, function (serie) {
 
11601
                        serie.isDirty = true;
 
11602
                });
 
11603
 
 
11604
                chart.isDirtyLegend = true; // force legend redraw
 
11605
                chart.isDirtyBox = true; // force redraw of plot and chart border
 
11606
 
 
11607
                chart.getMargins();
 
11608
 
 
11609
                chart.redraw(animation);
 
11610
 
 
11611
 
 
11612
                chart.oldChartHeight = null;
 
11613
                fireEvent(chart, 'resize');
 
11614
 
 
11615
                // fire endResize and set isResizing back
 
11616
                // If animation is disabled, fire without delay
 
11617
                if (globalAnimation === false) {
 
11618
                        fireEndResize();
 
11619
                } else { // else set a timeout with the animation duration
 
11620
                        setTimeout(fireEndResize, (globalAnimation && globalAnimation.duration) || 500);
 
11621
                }
 
11622
        },
 
11623
 
 
11624
        /**
 
11625
         * Set the public chart properties. This is done before and after the pre-render
 
11626
         * to determine margin sizes
 
11627
         */
 
11628
        setChartSize: function (skipAxes) {
 
11629
                var chart = this,
 
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,
 
11637
                        clipX,
 
11638
                        clipY,
 
11639
                        plotLeft,
 
11640
                        plotTop,
 
11641
                        plotWidth,
 
11642
                        plotHeight,
 
11643
                        plotBorderWidth;
 
11644
 
 
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));
 
11649
 
 
11650
                chart.plotSizeX = inverted ? plotHeight : plotWidth;
 
11651
                chart.plotSizeY = inverted ? plotWidth : plotHeight;
 
11652
                
 
11653
                chart.plotBorderWidth = optionsChart.plotBorderWidth || 0;
 
11654
 
 
11655
                // Set boxes used for alignment
 
11656
                chart.spacingBox = renderer.spacingBox = {
 
11657
                        x: spacing[3],
 
11658
                        y: spacing[0],
 
11659
                        width: chartWidth - spacing[3] - spacing[1],
 
11660
                        height: chartHeight - spacing[0] - spacing[2]
 
11661
                };
 
11662
                chart.plotBox = renderer.plotBox = {
 
11663
                        x: plotLeft,
 
11664
                        y: plotTop,
 
11665
                        width: plotWidth,
 
11666
                        height: plotHeight
 
11667
                };
 
11668
 
 
11669
                plotBorderWidth = 2 * mathFloor(chart.plotBorderWidth / 2);
 
11670
                clipX = mathCeil(mathMax(plotBorderWidth, clipOffset[3]) / 2);
 
11671
                clipY = mathCeil(mathMax(plotBorderWidth, clipOffset[0]) / 2);
 
11672
                chart.clipBox = {
 
11673
                        x: clipX, 
 
11674
                        y: clipY, 
 
11675
                        width: mathFloor(chart.plotSizeX - mathMax(plotBorderWidth, clipOffset[1]) / 2 - clipX), 
 
11676
                        height: mathFloor(chart.plotSizeY - mathMax(plotBorderWidth, clipOffset[2]) / 2 - clipY)
 
11677
                };
 
11678
 
 
11679
                if (!skipAxes) {
 
11680
                        each(chart.axes, function (axis) {
 
11681
                                axis.setAxisSize();
 
11682
                                axis.setAxisTranslation();
 
11683
                        });
 
11684
                }
 
11685
        },
 
11686
 
 
11687
        /**
 
11688
         * Initial margins before auto size margins are applied
 
11689
         */
 
11690
        resetMargins: function () {
 
11691
                var chart = this,
 
11692
                        spacing = chart.spacing,
 
11693
                        margin = chart.margin;
 
11694
 
 
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];
 
11701
        },
 
11702
 
 
11703
        /**
 
11704
         * Draw the borders and backgrounds for chart and plot area
 
11705
         */
 
11706
        drawChartBox: function () {
 
11707
                var chart = this,
 
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,
 
11721
                        mgn,
 
11722
                        bgAttr,
 
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;
 
11730
 
 
11731
                // Chart area
 
11732
                mgn = chartBorderWidth + (optionsChart.shadow ? 8 : 0);
 
11733
 
 
11734
                if (chartBorderWidth || chartBackgroundColor) {
 
11735
                        if (!chartBackground) {
 
11736
                                
 
11737
                                bgAttr = {
 
11738
                                        fill: chartBackgroundColor || NONE
 
11739
                                };
 
11740
                                if (chartBorderWidth) { // #980
 
11741
                                        bgAttr.stroke = optionsChart.borderColor;
 
11742
                                        bgAttr['stroke-width'] = chartBorderWidth;
 
11743
                                }
 
11744
                                chart.chartBackground = renderer.rect(mgn / 2, mgn / 2, chartWidth - mgn, chartHeight - mgn,
 
11745
                                                optionsChart.borderRadius, chartBorderWidth)
 
11746
                                        .attr(bgAttr)
 
11747
                                        .addClass(PREFIX + 'background')
 
11748
                                        .add()
 
11749
                                        .shadow(optionsChart.shadow);
 
11750
 
 
11751
                        } else { // resize
 
11752
                                chartBackground.animate(
 
11753
                                        chartBackground.crisp({ width: chartWidth - mgn, height: chartHeight - mgn })
 
11754
                                );
 
11755
                        }
 
11756
                }
 
11757
 
 
11758
 
 
11759
                // Plot background
 
11760
                if (plotBackgroundColor) {
 
11761
                        if (!plotBackground) {
 
11762
                                chart.plotBackground = renderer.rect(plotLeft, plotTop, plotWidth, plotHeight, 0)
 
11763
                                        .attr({
 
11764
                                                fill: plotBackgroundColor
 
11765
                                        })
 
11766
                                        .add()
 
11767
                                        .shadow(optionsChart.plotShadow);
 
11768
                        } else {
 
11769
                                plotBackground.animate(plotBox);
 
11770
                        }
 
11771
                }
 
11772
                if (plotBackgroundImage) {
 
11773
                        if (!plotBGImage) {
 
11774
                                chart.plotBGImage = renderer.image(plotBackgroundImage, plotLeft, plotTop, plotWidth, plotHeight)
 
11775
                                        .add();
 
11776
                        } else {
 
11777
                                plotBGImage.animate(plotBox);
 
11778
                        }
 
11779
                }
 
11780
                
 
11781
                // Plot clip
 
11782
                if (!clipRect) {
 
11783
                        chart.clipRect = renderer.clipRect(clipBox);
 
11784
                } else {
 
11785
                        clipRect.animate({
 
11786
                                width: clipBox.width,
 
11787
                                height: clipBox.height
 
11788
                        });
 
11789
                }
 
11790
 
 
11791
                // Plot area border
 
11792
                if (plotBorderWidth) {
 
11793
                        if (!plotBorder) {
 
11794
                                chart.plotBorder = renderer.rect(plotLeft, plotTop, plotWidth, plotHeight, 0, -plotBorderWidth)
 
11795
                                        .attr({
 
11796
                                                stroke: optionsChart.plotBorderColor,
 
11797
                                                'stroke-width': plotBorderWidth,
 
11798
                                                fill: NONE,
 
11799
                                                zIndex: 1
 
11800
                                        })
 
11801
                                        .add();
 
11802
                        } else {
 
11803
                                plotBorder.animate(
 
11804
                                        plotBorder.crisp({ x: plotLeft, y: plotTop, width: plotWidth, height: plotHeight })
 
11805
                                );
 
11806
                        }
 
11807
                }
 
11808
 
 
11809
                // reset
 
11810
                chart.isDirtyBox = false;
 
11811
        },
 
11812
 
 
11813
        /**
 
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.
 
11817
         */
 
11818
        propFromSeries: function () {
 
11819
                var chart = this,
 
11820
                        optionsChart = chart.options.chart,
 
11821
                        klass,
 
11822
                        seriesOptions = chart.options.series,
 
11823
                        i,
 
11824
                        value;
 
11825
                        
 
11826
                        
 
11827
                each(['inverted', 'angular', 'polar'], function (key) {
 
11828
                        
 
11829
                        // The default series type's class
 
11830
                        klass = seriesTypes[optionsChart.type || optionsChart.defaultSeriesType];
 
11831
                        
 
11832
                        // Get the value from available chart-wide properties
 
11833
                        value = (
 
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
 
11837
                        );
 
11838
        
 
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]) {
 
11844
                                        value = true;
 
11845
                                }
 
11846
                        }
 
11847
        
 
11848
                        // Set the chart property
 
11849
                        chart[key] = value;     
 
11850
                });
 
11851
                
 
11852
        },
 
11853
 
 
11854
        /**
 
11855
         * Link two or more series together. This is done initially from Chart.render,
 
11856
         * and after Chart.addSeries and Series.remove.
 
11857
         */
 
11858
        linkSeries: function () {
 
11859
                var chart = this,
 
11860
                        chartSeries = chart.series;
 
11861
 
 
11862
                // Reset links
 
11863
                each(chartSeries, function (series) {
 
11864
                        series.linkedSeries.length = 0;
 
11865
                });
 
11866
 
 
11867
                // Apply new links
 
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];
 
11873
                                } else {
 
11874
                                        linkedTo = chart.get(linkedTo);
 
11875
                                }
 
11876
                                if (linkedTo) {
 
11877
                                        linkedTo.linkedSeries.push(series);
 
11878
                                        series.linkedParent = linkedTo;
 
11879
                                }
 
11880
                        }
 
11881
                });
 
11882
        },
 
11883
 
 
11884
        /**
 
11885
         * Render series for the chart
 
11886
         */
 
11887
        renderSeries: function () {
 
11888
                each(this.series, function (serie) {
 
11889
                        serie.translate();
 
11890
                        if (serie.setTooltipPoints) {
 
11891
                                serie.setTooltipPoints();
 
11892
                        }
 
11893
                        serie.render();
 
11894
                });
 
11895
        },
 
11896
 
 
11897
        /**
 
11898
         * Render all graphics for the chart
 
11899
         */
 
11900
        render: function () {
 
11901
                var chart = this,
 
11902
                        axes = chart.axes,
 
11903
                        renderer = chart.renderer,
 
11904
                        options = chart.options;
 
11905
 
 
11906
                var labels = options.labels,
 
11907
                        credits = options.credits,
 
11908
                        creditsHref;
 
11909
 
 
11910
                // Title
 
11911
                chart.setTitle();
 
11912
 
 
11913
 
 
11914
                // Legend
 
11915
                chart.legend = new Legend(chart, options.legend);
 
11916
 
 
11917
                chart.getStacks(); // render stacks
 
11918
 
 
11919
                // Get margins by pre-rendering axes
 
11920
                // set axes scales
 
11921
                each(axes, function (axis) {
 
11922
                        axis.setScale();
 
11923
                });
 
11924
 
 
11925
                chart.getMargins();
 
11926
 
 
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();
 
11931
                });
 
11932
                chart.adjustTickAmounts();
 
11933
                chart.getMargins(); // second pass to check for new labels
 
11934
 
 
11935
 
 
11936
                // Draw the borders and backgrounds
 
11937
                chart.drawChartBox();           
 
11938
 
 
11939
 
 
11940
                // Axes
 
11941
                if (chart.hasCartesianSeries) {
 
11942
                        each(axes, function (axis) {
 
11943
                                axis.render();
 
11944
                        });
 
11945
                }
 
11946
 
 
11947
                // The series
 
11948
                if (!chart.seriesGroup) {
 
11949
                        chart.seriesGroup = renderer.g('series-group')
 
11950
                                .attr({ zIndex: 3 })
 
11951
                                .add();
 
11952
                }
 
11953
                chart.renderSeries();
 
11954
 
 
11955
                // Labels
 
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;
 
11961
 
 
11962
                                // delete to prevent rewriting in IE
 
11963
                                delete style.left;
 
11964
                                delete style.top;
 
11965
 
 
11966
                                renderer.text(
 
11967
                                        label.html,
 
11968
                                        x,
 
11969
                                        y
 
11970
                                )
 
11971
                                .attr({ zIndex: 2 })
 
11972
                                .css(style)
 
11973
                                .add();
 
11974
 
 
11975
                        });
 
11976
                }
 
11977
 
 
11978
                // Credits
 
11979
                if (credits.enabled && !chart.credits) {
 
11980
                        creditsHref = credits.href;
 
11981
                        chart.credits = renderer.text(
 
11982
                                credits.text,
 
11983
                                0,
 
11984
                                0
 
11985
                        )
 
11986
                        .on('click', function () {
 
11987
                                if (creditsHref) {
 
11988
                                        location.href = creditsHref;
 
11989
                                }
 
11990
                        })
 
11991
                        .attr({
 
11992
                                align: credits.position.align,
 
11993
                                zIndex: 8
 
11994
                        })
 
11995
                        .css(credits.style)
 
11996
                        .add()
 
11997
                        .align(credits.position);
 
11998
                }
 
11999
 
 
12000
                // Set flag
 
12001
                chart.hasRendered = true;
 
12002
 
 
12003
        },
 
12004
 
 
12005
        /**
 
12006
         * Clean up memory usage
 
12007
         */
 
12008
        destroy: function () {
 
12009
                var chart = this,
 
12010
                        axes = chart.axes,
 
12011
                        series = chart.series,
 
12012
                        container = chart.container,
 
12013
                        i,
 
12014
                        parentNode = container && container.parentNode;
 
12015
                        
 
12016
                // fire the chart.destoy event
 
12017
                fireEvent(chart, 'destroy');
 
12018
                
 
12019
                // Delete the chart from charts lookup array
 
12020
                charts[chart.index] = UNDEFINED;
 
12021
                chart.renderTo.removeAttribute('data-highcharts-chart');
 
12022
 
 
12023
                // remove events
 
12024
                removeEvent(chart);
 
12025
 
 
12026
                // ==== Destroy collections:
 
12027
                // Destroy axes
 
12028
                i = axes.length;
 
12029
                while (i--) {
 
12030
                        axes[i] = axes[i].destroy();
 
12031
                }
 
12032
 
 
12033
                // Destroy each series
 
12034
                i = series.length;
 
12035
                while (i--) {
 
12036
                        series[i] = series[i].destroy();
 
12037
                }
 
12038
 
 
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];
 
12044
 
 
12045
                        if (prop && prop.destroy) {
 
12046
                                chart[name] = prop.destroy();
 
12047
                        }
 
12048
                });
 
12049
 
 
12050
                // remove container and all SVG
 
12051
                if (container) { // can break in IE when destroyed before finished loading
 
12052
                        container.innerHTML = '';
 
12053
                        removeEvent(container);
 
12054
                        if (parentNode) {
 
12055
                                discardElement(container);
 
12056
                        }
 
12057
 
 
12058
                }
 
12059
 
 
12060
                // clean it all up
 
12061
                for (i in chart) {
 
12062
                        delete chart[i];
 
12063
                }
 
12064
 
 
12065
        },
 
12066
 
 
12067
 
 
12068
        /**
 
12069
         * VML namespaces can't be added until after complete. Listening
 
12070
         * for Perini's doScroll hack is not enough.
 
12071
         */
 
12072
        isReadyToRender: function () {
 
12073
                var chart = this;
 
12074
 
 
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*/
 
12079
                        if (useCanVG) {
 
12080
                                // Delay rendering until canvg library is downloaded and ready
 
12081
                                CanVGController.push(function () { chart.firstRender(); }, chart.options.global.canvasToolsURL);
 
12082
                        } else {
 
12083
                                doc.attachEvent('onreadystatechange', function () {
 
12084
                                        doc.detachEvent('onreadystatechange', chart.firstRender);
 
12085
                                        if (doc.readyState === 'complete') {
 
12086
                                                chart.firstRender();
 
12087
                                        }
 
12088
                                });
 
12089
                        }
 
12090
                        return false;
 
12091
                }
 
12092
                return true;
 
12093
        },
 
12094
 
 
12095
        /**
 
12096
         * Prepare for first rendering after all data are loaded
 
12097
         */
 
12098
        firstRender: function () {
 
12099
                var chart = this,
 
12100
                        options = chart.options,
 
12101
                        callback = chart.callback;
 
12102
 
 
12103
                // Check whether the chart is ready to render
 
12104
                if (!chart.isReadyToRender()) {
 
12105
                        return;
 
12106
                }
 
12107
 
 
12108
                // Create the container
 
12109
                chart.getContainer();
 
12110
 
 
12111
                // Run an early event after the container and renderer are established
 
12112
                fireEvent(chart, 'init');
 
12113
 
 
12114
                
 
12115
                chart.resetMargins();
 
12116
                chart.setChartSize();
 
12117
 
 
12118
                // Set the common chart properties (mainly invert) from the given series
 
12119
                chart.propFromSeries();
 
12120
 
 
12121
                // get axes
 
12122
                chart.getAxes();
 
12123
 
 
12124
                // Initialize the series
 
12125
                each(options.series || [], function (serieOptions) {
 
12126
                        chart.initSeries(serieOptions);
 
12127
                });
 
12128
 
 
12129
                chart.linkSeries();
 
12130
 
 
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'); 
 
12135
 
 
12136
                // depends on inverted and on margins being set
 
12137
                if (Highcharts.Pointer) {
 
12138
                        chart.pointer = new Pointer(chart, options);
 
12139
                }
 
12140
 
 
12141
                chart.render();
 
12142
 
 
12143
                // add canvas
 
12144
                chart.renderer.draw();
 
12145
                // run callbacks
 
12146
                if (callback) {
 
12147
                        callback.apply(chart, [chart]);
 
12148
                }
 
12149
                each(chart.callbacks, function (fn) {
 
12150
                        fn.apply(chart, [chart]);
 
12151
                });
 
12152
                
 
12153
                
 
12154
                // If the chart was rendered outside the top container, put it back in
 
12155
                chart.cloneRenderTo(true);
 
12156
                
 
12157
                fireEvent(chart, 'load');
 
12158
 
 
12159
        },
 
12160
 
 
12161
        /**
 
12162
        * Creates arrays for spacing and margin from given options.
 
12163
        */
 
12164
        splashArray: function (target, options) {
 
12165
                var oVar = options[target],
 
12166
                        tArray = isObject(oVar) ? oVar : [oVar, oVar, oVar, oVar];
 
12167
 
 
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])];
 
12172
        }
 
12173
}; // end Chart
 
12174
 
 
12175
// Hook for exporting module
 
12176
Chart.prototype.callbacks = [];
 
12177
 
 
12178
var CenteredSeriesMixin = Highcharts.CenteredSeriesMixin = {
 
12179
        /**
 
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.
 
12182
         */
 
12183
        getCenter: function () {
 
12184
                
 
12185
                var options = this.options,
 
12186
                        chart = this.chart,
 
12187
                        slicingRoom = 2 * (options.slicedOffset || 0),
 
12188
                        handleSlicingRoom,
 
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),
 
12194
                        isPercent;
 
12195
                
 
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);
 
12207
                });
 
12208
        }
 
12209
};
 
12210
 
 
12211
/**
 
12212
 * The Point object and prototype. Inheritable and used as base for PiePoint
 
12213
 */
 
12214
var Point = function () {};
 
12215
Point.prototype = {
 
12216
 
 
12217
        /**
 
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
 
12221
         */
 
12222
        init: function (series, options, x) {
 
12223
 
 
12224
                var point = this,
 
12225
                        colors;
 
12226
                point.series = series;
 
12227
                point.applyOptions(options, x);
 
12228
                point.pointAttr = {};
 
12229
 
 
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;
 
12236
                        }
 
12237
                }
 
12238
 
 
12239
                series.chart.pointCount++;
 
12240
                return point;
 
12241
        },
 
12242
        /**
 
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.
 
12245
         *
 
12246
         * @param {Object} options
 
12247
         */
 
12248
        applyOptions: function (options, x) {
 
12249
                var point = this,
 
12250
                        series = point.series,
 
12251
                        pointValKey = series.pointValKey;
 
12252
 
 
12253
                options = Point.prototype.optionsToObject.call(this, options);
 
12254
 
 
12255
                // copy options directly to point
 
12256
                extend(point, options);
 
12257
                point.options = point.options ? extend(point.options, options) : options;
 
12258
 
 
12259
                // For higher dimension series types. For instance, for ranges, point.y is mapped to point.low.
 
12260
                if (pointValKey) {
 
12261
                        point.y = point[pointValKey];
 
12262
                }
 
12263
 
 
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;
 
12268
                }
 
12269
 
 
12270
                return point;
 
12271
        },
 
12272
 
 
12273
        /**
 
12274
         * Transform number or array configs into objects
 
12275
         */
 
12276
        optionsToObject: function (options) {
 
12277
                var ret = {},
 
12278
                        series = this.series,
 
12279
                        pointArrayMap = series.pointArrayMap || ['y'],
 
12280
                        valueCount = pointArrayMap.length,
 
12281
                        firstItemType,
 
12282
                        i = 0,
 
12283
                        j = 0;
 
12284
 
 
12285
                if (typeof options === 'number' || options === null) {
 
12286
                        ret[pointArrayMap[0]] = options;
 
12287
 
 
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];
 
12296
                                }
 
12297
                                i++;
 
12298
                        }
 
12299
                        while (j < valueCount) {
 
12300
                                ret[pointArrayMap[j++]] = options[i++];
 
12301
                        }
 
12302
                } else if (typeof options === 'object') {
 
12303
                        ret = options;
 
12304
 
 
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;
 
12309
                        }
 
12310
 
 
12311
                        // Same approach as above for markers
 
12312
                        if (options.marker) {
 
12313
                                series._hasPointMarkers = true;
 
12314
                        }
 
12315
                }
 
12316
                return ret;
 
12317
        },
 
12318
 
 
12319
        /**
 
12320
         * Destroy a point to clear memory. Its reference still stays in series.data.
 
12321
         */
 
12322
        destroy: function () {
 
12323
                var point = this,
 
12324
                        series = point.series,
 
12325
                        chart = series.chart,
 
12326
                        hoverPoints = chart.hoverPoints,
 
12327
                        prop;
 
12328
 
 
12329
                chart.pointCount--;
 
12330
 
 
12331
                if (hoverPoints) {
 
12332
                        point.setState();
 
12333
                        erase(hoverPoints, point);
 
12334
                        if (!hoverPoints.length) {
 
12335
                                chart.hoverPoints = null;
 
12336
                        }
 
12337
 
 
12338
                }
 
12339
                if (point === chart.hoverPoint) {
 
12340
                        point.onMouseOut();
 
12341
                }
 
12342
 
 
12343
                // remove all events
 
12344
                if (point.graphic || point.dataLabel) { // removeEvent and destroyElements are performance expensive
 
12345
                        removeEvent(point);
 
12346
                        point.destroyElements();
 
12347
                }
 
12348
 
 
12349
                if (point.legendItem) { // pies have legend items
 
12350
                        chart.legend.destroyItem(point);
 
12351
                }
 
12352
 
 
12353
                for (prop in point) {
 
12354
                        point[prop] = null;
 
12355
                }
 
12356
 
 
12357
 
 
12358
        },
 
12359
 
 
12360
        /**
 
12361
         * Destroy SVG elements associated with the point
 
12362
         */
 
12363
        destroyElements: function () {
 
12364
                var point = this,
 
12365
                        props = ['graphic', 'dataLabel', 'dataLabelUpper', 'group', 'connector', 'shadowGroup'],
 
12366
                        prop,
 
12367
                        i = 6;
 
12368
                while (i--) {
 
12369
                        prop = props[i];
 
12370
                        if (point[prop]) {
 
12371
                                point[prop] = point[prop].destroy();
 
12372
                        }
 
12373
                }
 
12374
        },
 
12375
 
 
12376
        /**
 
12377
         * Return the configuration hash needed for the data label and tooltip formatters
 
12378
         */
 
12379
        getLabelConfig: function () {
 
12380
                var point = this;
 
12381
                return {
 
12382
                        x: point.category,
 
12383
                        y: point.y,
 
12384
                        key: point.name || point.category,
 
12385
                        series: point.series,
 
12386
                        point: point,
 
12387
                        percentage: point.percentage,
 
12388
                        total: point.total || point.stackTotal
 
12389
                };
 
12390
        },      
 
12391
 
 
12392
        /**
 
12393
         * Extendable method for formatting each point's tooltip line
 
12394
         *
 
12395
         * @return {String} A string to be concatenated in to the common tooltip text
 
12396
         */
 
12397
        tooltipFormatter: function (pointFormat) {
 
12398
 
 
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 || '';
 
12405
 
 
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);
 
12411
                        }
 
12412
                        pointFormat = pointFormat.replace(key + '}', key + ':,.' + valueDecimals + 'f}');
 
12413
                });
 
12414
 
 
12415
                return format(pointFormat, {
 
12416
                        point: this,
 
12417
                        series: this.series
 
12418
                });
 
12419
        }
 
12420
};/**
 
12421
 * @classDescription The base function which all other series types inherit from. The data in the series is stored
 
12422
 * in various arrays.
 
12423
 *
 
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.
 
12435
 *
 
12436
 * @param {Object} chart
 
12437
 * @param {Object} options
 
12438
 */
 
12439
var Series = function () {};
 
12440
 
 
12441
Series.prototype = {
 
12442
 
 
12443
        isCartesian: true,
 
12444
        type: 'line',
 
12445
        pointClass: Point,
 
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',
 
12451
                fill: 'fillColor',
 
12452
                r: 'radius'
 
12453
        },
 
12454
        axisTypes: ['xAxis', 'yAxis'],
 
12455
        colorCounter: 0,
 
12456
        parallelArrays: ['x', 'y'], // each point's x and y values are stored in this.xData and this.yData
 
12457
        init: function (chart, options) {
 
12458
                var series = this,
 
12459
                        eventType,
 
12460
                        events,
 
12461
                        chartSeries = chart.series,
 
12462
                        sortByIndex = function (a, b) {
 
12463
                                return pick(a.options.index, a._i) - pick(b.options.index, b._i);
 
12464
                        };
 
12465
 
 
12466
                series.chart = chart;
 
12467
                series.options = options = series.setOptions(options); // merge with plotOptions
 
12468
                series.linkedSeries = [];
 
12469
 
 
12470
                // bind the axes
 
12471
                series.bindAxes();
 
12472
 
 
12473
                // set some variables
 
12474
                extend(series, {
 
12475
                        name: options.name,
 
12476
                        state: NORMAL_STATE,
 
12477
                        pointAttr: {},
 
12478
                        visible: options.visible !== false, // true by default
 
12479
                        selected: options.selected === true // false by default
 
12480
                });
 
12481
 
 
12482
                // special
 
12483
                if (useCanVG) {
 
12484
                        options.animation = false;
 
12485
                }
 
12486
 
 
12487
                // register event listeners
 
12488
                events = options.events;
 
12489
                for (eventType in events) {
 
12490
                        addEvent(series, eventType, events[eventType]);
 
12491
                }
 
12492
                if (
 
12493
                        (events && events.click) ||
 
12494
                        (options.point && options.point.events && options.point.events.click) ||
 
12495
                        options.allowPointSelect
 
12496
                ) {
 
12497
                        chart.runTrackerClick = true;
 
12498
                }
 
12499
 
 
12500
                series.getColor();
 
12501
                series.getSymbol();
 
12502
 
 
12503
                // Set the data
 
12504
                each(series.parallelArrays, function (key) {
 
12505
                        series[key + 'Data'] = [];
 
12506
                });
 
12507
                series.setData(options.data, false);
 
12508
 
 
12509
                // Mark cartesian
 
12510
                if (series.isCartesian) {
 
12511
                        chart.hasCartesianSeries = true;
 
12512
                }
 
12513
 
 
12514
                // Register it in the chart
 
12515
                chartSeries.push(series);
 
12516
                series._i = chartSeries.length - 1;
 
12517
 
 
12518
                // Sort series according to index option (#248, #1123, #2456)
 
12519
                stableSort(chartSeries, sortByIndex);
 
12520
                if (this.yAxis) {
 
12521
                        stableSort(this.yAxis.series, sortByIndex);
 
12522
                }
 
12523
 
 
12524
                each(chartSeries, function (series, i) {
 
12525
                        series.index = i;
 
12526
                        series.name = series.name || 'Series ' + (i + 1);
 
12527
                });
 
12528
 
 
12529
        },
 
12530
 
 
12531
        /**
 
12532
         * Set the xAxis and yAxis properties of cartesian series, and register the series
 
12533
         * in the axis.series array
 
12534
         */
 
12535
        bindAxes: function () {
 
12536
                var series = this,
 
12537
                        seriesOptions = series.options,
 
12538
                        chart = series.chart,
 
12539
                        axisOptions;
 
12540
 
 
12541
                each(series.axisTypes || [], function (AXIS) { // repeat for xAxis and yAxis
 
12542
 
 
12543
                        each(chart[AXIS], function (axis) { // loop through the chart's axis objects
 
12544
                                axisOptions = axis.options;
 
12545
 
 
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)) {
 
12551
 
 
12552
                                        // register this series in the axis.series lookup
 
12553
                                        axis.series.push(series);
 
12554
 
 
12555
                                        // set this series.xAxis or series.yAxis reference
 
12556
                                        series[AXIS] = axis;
 
12557
 
 
12558
                                        // mark dirty for redraw
 
12559
                                        axis.isDirty = true;
 
12560
                                }
 
12561
                        });
 
12562
 
 
12563
                        // The series needs an X and an Y axis
 
12564
                        if (!series[AXIS] && series.optionalAxis !== AXIS) {
 
12565
                                error(18, true);
 
12566
                        }
 
12567
 
 
12568
                });
 
12569
        },
 
12570
 
 
12571
        /**
 
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.
 
12576
         */
 
12577
        updateParallelArrays: function (point, i) {
 
12578
                var series = point.series,
 
12579
                        args = arguments,
 
12580
                        fn = typeof i === 'number' ?
 
12581
                                 // Insert the value in the given position
 
12582
                                function (key) {
 
12583
                                        var val = key === 'y' && series.toYData ? series.toYData(point) : point[key];
 
12584
                                        series[key + 'Data'][i] = val;
 
12585
                                } :
 
12586
                                // Apply the method specified in i with the following arguments as arguments
 
12587
                                function (key) {
 
12588
                                        Array.prototype[i].apply(series[key + 'Data'], Array.prototype.slice.call(args, 2));
 
12589
                                };
 
12590
 
 
12591
                each(series.parallelArrays, fn);
 
12592
        },
 
12593
 
 
12594
        /**
 
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.
 
12597
         */
 
12598
        autoIncrement: function () {
 
12599
                var series = this,
 
12600
                        options = series.options,
 
12601
                        xIncrement = series.xIncrement;
 
12602
 
 
12603
                xIncrement = pick(xIncrement, options.pointStart, 0);
 
12604
 
 
12605
                series.pointInterval = pick(series.pointInterval, options.pointInterval, 1);
 
12606
 
 
12607
                series.xIncrement = xIncrement + series.pointInterval;
 
12608
                return xIncrement;
 
12609
        },
 
12610
 
 
12611
        /**
 
12612
         * Divide the series data into segments divided by null values.
 
12613
         */
 
12614
        getSegments: function () {
 
12615
                var series = this,
 
12616
                        lastNull = -1,
 
12617
                        segments = [],
 
12618
                        i,
 
12619
                        points = series.points,
 
12620
                        pointsLength = points.length;
 
12621
 
 
12622
                if (pointsLength) { // no action required for []
 
12623
 
 
12624
                        // if connect nulls, just remove null points
 
12625
                        if (series.options.connectNulls) {
 
12626
                                i = pointsLength;
 
12627
                                while (i--) {
 
12628
                                        if (points[i].y === null) {
 
12629
                                                points.splice(i, 1);
 
12630
                                        }
 
12631
                                }
 
12632
                                if (points.length) {
 
12633
                                        segments = [points];
 
12634
                                }
 
12635
 
 
12636
                        // else, split on null points
 
12637
                        } else {
 
12638
                                each(points, function (point, i) {
 
12639
                                        if (point.y === null) {
 
12640
                                                if (i > lastNull + 1) {
 
12641
                                                        segments.push(points.slice(lastNull + 1, i));
 
12642
                                                }
 
12643
                                                lastNull = i;
 
12644
                                        } else if (i === pointsLength - 1) { // last value
 
12645
                                                segments.push(points.slice(lastNull + 1, i + 1));
 
12646
                                        }
 
12647
                                });
 
12648
                        }
 
12649
                }
 
12650
 
 
12651
                // register it
 
12652
                series.segments = segments;
 
12653
        },
 
12654
 
 
12655
        /**
 
12656
         * Set the series options by merging from the options tree
 
12657
         * @param {Object} itemOptions
 
12658
         */
 
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],
 
12666
                        options;
 
12667
 
 
12668
                this.userOptions = itemOptions;
 
12669
 
 
12670
                options = merge(
 
12671
                        typeOptions,
 
12672
                        plotOptions.series,
 
12673
                        itemOptions
 
12674
                );
 
12675
 
 
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
 
12684
                );
 
12685
 
 
12686
                // Delete marker object if not allowed (#1125)
 
12687
                if (typeOptions.marker === null) {
 
12688
                        delete options.marker;
 
12689
                }
 
12690
 
 
12691
                return options;
 
12692
 
 
12693
        },
 
12694
        /**
 
12695
         * Get the series' color
 
12696
         */
 
12697
        getColor: function () {
 
12698
                var options = this.options,
 
12699
                        userOptions = this.userOptions,
 
12700
                        defaultColors = this.chart.options.colors,
 
12701
                        counters = this.chart.counters,
 
12702
                        color,
 
12703
                        colorIndex;
 
12704
 
 
12705
                color = options.color || defaultPlotOptions[this.type].color;
 
12706
 
 
12707
                if (!color && !options.colorByPoint) {
 
12708
                        if (defined(userOptions._colorIndex)) { // after Series.update()
 
12709
                                colorIndex = userOptions._colorIndex;
 
12710
                        } else {
 
12711
                                userOptions._colorIndex = counters.color;
 
12712
                                colorIndex = counters.color++;
 
12713
                        }
 
12714
                        color = defaultColors[colorIndex];
 
12715
                }
 
12716
 
 
12717
                this.color = color;
 
12718
                counters.wrapColor(defaultColors.length);
 
12719
        },
 
12720
        /**
 
12721
         * Get the series' symbol
 
12722
         */
 
12723
        getSymbol: function () {
 
12724
                var series = this,
 
12725
                        userOptions = series.userOptions,
 
12726
                        seriesMarkerOption = series.options.marker,
 
12727
                        chart = series.chart,
 
12728
                        defaultSymbols = chart.options.symbols,
 
12729
                        counters = chart.counters,
 
12730
                        symbolIndex;
 
12731
 
 
12732
                series.symbol = seriesMarkerOption.symbol;
 
12733
                if (!series.symbol) {
 
12734
                        if (defined(userOptions._symbolIndex)) { // after Series.update()
 
12735
                                symbolIndex = userOptions._symbolIndex;
 
12736
                        } else {
 
12737
                                userOptions._symbolIndex = counters.symbol;
 
12738
                                symbolIndex = counters.symbol++;
 
12739
                        }
 
12740
                        series.symbol = defaultSymbols[symbolIndex];
 
12741
                }
 
12742
 
 
12743
                // don't substract radius in image symbols (#604)
 
12744
                if (/^url/.test(series.symbol)) {
 
12745
                        seriesMarkerOption.radius = 0;
 
12746
                }
 
12747
                counters.wrapSymbol(defaultSymbols.length);
 
12748
        },
 
12749
 
 
12750
        drawLegendSymbol: LegendSymbolMixin.drawLineMarker,
 
12751
 
 
12752
        /**
 
12753
         * Replace the series data with a new set of data
 
12754
         * @param {Object} data
 
12755
         * @param {Object} redraw
 
12756
         */
 
12757
        setData: function (data, redraw, animation, updatePoints) {
 
12758
                var series = this,
 
12759
                        oldData = series.points,
 
12760
                        oldDataLength = (oldData && oldData.length) || 0,
 
12761
                        dataLength,
 
12762
                        options = series.options,
 
12763
                        chart = series.chart,
 
12764
                        firstPoint = null,
 
12765
                        xAxis = series.xAxis,
 
12766
                        hasCategories = xAxis && !!xAxis.categories,
 
12767
                        tooltipPoints = series.tooltipPoints,
 
12768
                        i,
 
12769
                        turboThreshold = options.turboThreshold,
 
12770
                        pt,
 
12771
                        xData = this.xData,
 
12772
                        yData = this.yData,
 
12773
                        pointArrayMap = series.pointArrayMap,
 
12774
                        valueCount = pointArrayMap && pointArrayMap.length;
 
12775
 
 
12776
                data = data || [];
 
12777
                dataLength = data.length;
 
12778
                redraw = pick(redraw, true);
 
12779
 
 
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);
 
12785
                        });
 
12786
 
 
12787
                } else {
 
12788
 
 
12789
                        // Reset properties
 
12790
                        series.xIncrement = null;
 
12791
                        series.pointRange = hasCategories ? 1 : options.pointRange;
 
12792
 
 
12793
                        series.colorCounter = 0; // for series with colorByPoint (#1547)
 
12794
                        
 
12795
                        // Update parallel arrays
 
12796
                        each(this.parallelArrays, function (key) {
 
12797
                                series[key + 'Data'].length = 0;
 
12798
                        });
 
12799
 
 
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) {
 
12805
 
 
12806
                                // find the first non-null point
 
12807
                                i = 0;
 
12808
                                while (firstPoint === null && i < dataLength) {
 
12809
                                        firstPoint = data[i];
 
12810
                                        i++;
 
12811
                                }
 
12812
 
 
12813
 
 
12814
                                if (isNumber(firstPoint)) { // assume all points are numbers
 
12815
                                        var x = pick(options.pointStart, 0),
 
12816
                                                pointInterval = pick(options.pointInterval, 1);
 
12817
 
 
12818
                                        for (i = 0; i < dataLength; i++) {
 
12819
                                                xData[i] = x;
 
12820
                                                yData[i] = data[i];
 
12821
                                                x += pointInterval;
 
12822
                                        }
 
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++) {
 
12827
                                                        pt = data[i];
 
12828
                                                        xData[i] = pt[0];
 
12829
                                                        yData[i] = pt.slice(1, valueCount + 1);
 
12830
                                                }
 
12831
                                        } else { // [x, y]
 
12832
                                                for (i = 0; i < dataLength; i++) {
 
12833
                                                        pt = data[i];
 
12834
                                                        xData[i] = pt[0];
 
12835
                                                        yData[i] = pt[1];
 
12836
                                                }
 
12837
                                        }
 
12838
                                } else {
 
12839
                                        error(12); // Highcharts expects configs to be numbers or arrays in turbo mode
 
12840
                                }
 
12841
                        } else {
 
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
 
12849
                                                }
 
12850
                                        }
 
12851
                                }
 
12852
                        }
 
12853
 
 
12854
                        // Forgetting to cast strings to numbers is a common caveat when handling CSV or JSON
 
12855
                        if (isString(yData[0])) {
 
12856
                                error(14, true);
 
12857
                        }
 
12858
 
 
12859
                        series.data = [];
 
12860
                        series.options.data = data;
 
12861
                        //series.zData = zData;
 
12862
 
 
12863
                        // destroy old points
 
12864
                        i = oldDataLength;
 
12865
                        while (i--) {
 
12866
                                if (oldData[i] && oldData[i].destroy) {
 
12867
                                        oldData[i].destroy();
 
12868
                                }
 
12869
                        }
 
12870
                        if (tooltipPoints) { // #2594
 
12871
                                tooltipPoints.length = 0;
 
12872
                        }
 
12873
 
 
12874
                        // reset minRange (#878)
 
12875
                        if (xAxis) {
 
12876
                                xAxis.minRange = xAxis.userMinRange;
 
12877
                        }
 
12878
 
 
12879
                        // redraw
 
12880
                        series.isDirty = series.isDirtyData = chart.isDirtyBox = true;
 
12881
                        animation = false;
 
12882
                }
 
12883
 
 
12884
                if (redraw) {
 
12885
                        chart.redraw(animation);
 
12886
                }
 
12887
        },
 
12888
 
 
12889
        /**
 
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.
 
12892
         */
 
12893
        processData: function (force) {
 
12894
                var series = this,
 
12895
                        processedXData = series.xData, // copied during slice operation below
 
12896
                        processedYData = series.yData,
 
12897
                        dataLength = processedXData.length,
 
12898
                        croppedData,
 
12899
                        cropStart = 0,
 
12900
                        cropped,
 
12901
                        distance,
 
12902
                        closestPointRange,
 
12903
                        xAxis = series.xAxis,
 
12904
                        i, // loop variable
 
12905
                        options = series.options,
 
12906
                        cropThreshold = options.cropThreshold,
 
12907
                        isCartesian = series.isCartesian;
 
12908
 
 
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) {
 
12912
                        return false;
 
12913
                }
 
12914
 
 
12915
 
 
12916
                // optionally filter out points outside the plot area
 
12917
                if (isCartesian && series.sorted && (!cropThreshold || dataLength > cropThreshold || series.forceCrop)) {
 
12918
                        var min = xAxis.min,
 
12919
                                max = xAxis.max;
 
12920
 
 
12921
                        // it's outside current extremes
 
12922
                        if (processedXData[dataLength - 1] < min || processedXData[0] > max) {
 
12923
                                processedXData = [];
 
12924
                                processedYData = [];
 
12925
 
 
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;
 
12932
                                cropped = true;
 
12933
                        }
 
12934
                }
 
12935
 
 
12936
 
 
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;
 
12942
 
 
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) {
 
12946
                                error(15);
 
12947
                        }
 
12948
                }
 
12949
 
 
12950
                // Record the properties
 
12951
                series.cropped = cropped; // undefined or true
 
12952
                series.cropStart = cropStart;
 
12953
                series.processedXData = processedXData;
 
12954
                series.processedYData = processedYData;
 
12955
 
 
12956
                if (options.pointRange === null) { // null means auto, as for columns, candlesticks and OHLC
 
12957
                        series.pointRange = closestPointRange || 1;
 
12958
                }
 
12959
                series.closestPointRange = closestPointRange;
 
12960
 
 
12961
        },
 
12962
 
 
12963
        /**
 
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
 
12966
         */
 
12967
        cropData: function (xData, yData, min, max) {
 
12968
                var dataLength = xData.length,
 
12969
                        cropStart = 0,
 
12970
                        cropEnd = dataLength,
 
12971
                        cropShoulder = pick(this.cropShoulder, 1), // line-type series need one point outside
 
12972
                        i;
 
12973
 
 
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);
 
12978
                                break;
 
12979
                        }
 
12980
                }
 
12981
 
 
12982
                // proceed to find slice end
 
12983
                for (; i < dataLength; i++) {
 
12984
                        if (xData[i] > max) {
 
12985
                                cropEnd = i + cropShoulder;
 
12986
                                break;
 
12987
                        }
 
12988
                }
 
12989
 
 
12990
                return {
 
12991
                        xData: xData.slice(cropStart, cropEnd),
 
12992
                        yData: yData.slice(cropStart, cropEnd),
 
12993
                        start: cropStart,
 
12994
                        end: cropEnd
 
12995
                };
 
12996
        },
 
12997
 
 
12998
 
 
12999
        /**
 
13000
         * Generate the data point after the data has been processed by cropping away
 
13001
         * unused points and optionally grouped in Highcharts Stock.
 
13002
         */
 
13003
        generatePoints: function () {
 
13004
                var series = this,
 
13005
                        options = series.options,
 
13006
                        dataOptions = options.data,
 
13007
                        data = series.data,
 
13008
                        dataLength,
 
13009
                        processedXData = series.processedXData,
 
13010
                        processedYData = series.processedYData,
 
13011
                        pointClass = series.pointClass,
 
13012
                        processedDataLength = processedXData.length,
 
13013
                        cropStart = series.cropStart || 0,
 
13014
                        cursor,
 
13015
                        hasGroupedData = series.hasGroupedData,
 
13016
                        point,
 
13017
                        points = [],
 
13018
                        i;
 
13019
 
 
13020
                if (!data && !hasGroupedData) {
 
13021
                        var arr = [];
 
13022
                        arr.length = dataOptions.length;
 
13023
                        data = series.data = arr;
 
13024
                }
 
13025
 
 
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]);
 
13033
                                }
 
13034
                                points[i] = point;
 
13035
                        } else {
 
13036
                                // splat the y data in case of ohlc data array
 
13037
                                points[i] = (new pointClass()).init(series, [processedXData[i]].concat(splat(processedYData[i])));
 
13038
                        }
 
13039
                }
 
13040
 
 
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;
 
13047
                                }
 
13048
                                if (data[i]) {
 
13049
                                        data[i].destroyElements();
 
13050
                                        data[i].plotX = UNDEFINED; // #1003
 
13051
                                }
 
13052
                        }
 
13053
                }
 
13054
 
 
13055
                series.data = data;
 
13056
                series.points = points;
 
13057
        },
 
13058
 
 
13059
        /**
 
13060
         * Calculate Y extremes for visible data
 
13061
         */
 
13062
        getExtremes: function (yData) {
 
13063
                var xAxis = this.xAxis,
 
13064
                        yAxis = this.yAxis,
 
13065
                        xData = this.processedXData,
 
13066
                        yDataLength,
 
13067
                        activeYData = [],
 
13068
                        activeCounter = 0,
 
13069
                        xExtremes = xAxis.getExtremes(), // #2117, need to compensate for log X axis
 
13070
                        xMin = xExtremes.min,
 
13071
                        xMax = xExtremes.max,
 
13072
                        validValue,
 
13073
                        withinRange,
 
13074
                        dataMin,
 
13075
                        dataMax,
 
13076
                        x,
 
13077
                        y,
 
13078
                        i,
 
13079
                        j;
 
13080
 
 
13081
                yData = yData || this.stackedYData || this.processedYData;
 
13082
                yDataLength = yData.length;
 
13083
 
 
13084
                for (i = 0; i < yDataLength; i++) {
 
13085
 
 
13086
                        x = xData[i];
 
13087
                        y = yData[i];
 
13088
 
 
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);
 
13094
 
 
13095
                        if (validValue && withinRange) {
 
13096
 
 
13097
                                j = y.length;
 
13098
                                if (j) { // array, like ohlc or range data
 
13099
                                        while (j--) {
 
13100
                                                if (y[j] !== null) {
 
13101
                                                        activeYData[activeCounter++] = y[j];
 
13102
                                                }
 
13103
                                        }
 
13104
                                } else {
 
13105
                                        activeYData[activeCounter++] = y;
 
13106
                                }
 
13107
                        }
 
13108
                }
 
13109
                this.dataMin = pick(dataMin, arrayMin(activeYData));
 
13110
                this.dataMax = pick(dataMax, arrayMax(activeYData));
 
13111
        },
 
13112
 
 
13113
        /**
 
13114
         * Translate data points from raw data values to chart specific positioning data
 
13115
         * needed later in drawPoints, drawGraph and drawTracker.
 
13116
         */
 
13117
        translate: function () {
 
13118
                if (!this.processedXData) { // hidden series
 
13119
                        this.processData();
 
13120
                }
 
13121
                this.generatePoints();
 
13122
                var series = this,
 
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,
 
13131
                        i,
 
13132
                        pointPlacement = options.pointPlacement,
 
13133
                        dynamicallyPlaced = pointPlacement === 'between' || isNumber(pointPlacement),
 
13134
                        threshold = options.threshold;
 
13135
 
 
13136
                // Translate each point
 
13137
                for (i = 0; i < dataLength; i++) {
 
13138
                        var point = points[i],
 
13139
                                xValue = point.x,
 
13140
                                yValue = point.y,
 
13141
                                yBottom = point.low,
 
13142
                                stack = stacking && yAxis.stacks[(series.negStacks && yValue < threshold ? '-' : '') + series.stackKey],
 
13143
                                pointStack,
 
13144
                                stackValues;
 
13145
 
 
13146
                        // Discard disallowed y values for log axes
 
13147
                        if (yAxis.isLog && yValue <= 0) {
 
13148
                                point.y = yValue = null;
 
13149
                        }
 
13150
 
 
13151
                        // Get the plotX translation
 
13152
                        point.plotX = xAxis.translate(xValue, 0, 0, 0, 1, pointPlacement, this.type === 'flags'); // Math.round fixes #591
 
13153
 
 
13154
 
 
13155
                        // Calculate the bottom y value for stacked series
 
13156
                        if (stacking && series.visible && stack && stack[xValue]) {
 
13157
 
 
13158
                                pointStack = stack[xValue];
 
13159
                                stackValues = pointStack.points[series.index];
 
13160
                                yBottom = stackValues[0];
 
13161
                                yValue = stackValues[1];
 
13162
 
 
13163
                                if (yBottom === 0) {
 
13164
                                        yBottom = pick(threshold, yAxis.min);
 
13165
                                }
 
13166
                                if (yAxis.isLog && yBottom <= 0) { // #1200, #1232
 
13167
                                        yBottom = null;
 
13168
                                }
 
13169
 
 
13170
                                point.total = point.stackTotal = pointStack.total;
 
13171
                                point.percentage = pointStack.total && (point.y / pointStack.total * 100);
 
13172
                                point.stackY = yValue;
 
13173
 
 
13174
                                // Place the stack label
 
13175
                                pointStack.setOffset(series.pointXOffset || 0, series.barW || 0);
 
13176
 
 
13177
                        }
 
13178
 
 
13179
                        // Set translated yBottom or remove it
 
13180
                        point.yBottom = defined(yBottom) ?
 
13181
                                yAxis.translate(yBottom, 0, 1, 0, 1) :
 
13182
                                null;
 
13183
 
 
13184
                        // general hook, used for Highstock compare mode
 
13185
                        if (hasModifyValue) {
 
13186
                                yValue = series.modifyValue(yValue, point);
 
13187
                        }
 
13188
 
 
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) :
 
13193
                                UNDEFINED;
 
13194
 
 
13195
                        // Set client related positions for mouse tracking
 
13196
                        point.clientX = dynamicallyPlaced ? xAxis.translate(xValue, 0, 0, 0, 1) : point.plotX; // #1514
 
13197
 
 
13198
                        point.negative = point.y < (threshold || 0);
 
13199
 
 
13200
                        // some API data
 
13201
                        point.category = categories && categories[point.x] !== UNDEFINED ?
 
13202
                                categories[point.x] : point.x;
 
13203
 
 
13204
 
 
13205
                }
 
13206
 
 
13207
                // now that we have the cropped data, build the segments
 
13208
                series.getSegments();
 
13209
        },
 
13210
 
 
13211
        /**
 
13212
         * Animate in the series
 
13213
         */
 
13214
        animate: function (init) {
 
13215
                var series = this,
 
13216
                        chart = series.chart,
 
13217
                        renderer = chart.renderer,
 
13218
                        clipRect,
 
13219
                        markerClipRect,
 
13220
                        animation = series.options.animation,
 
13221
                        clipBox = chart.clipBox,
 
13222
                        inverted = chart.inverted,
 
13223
                        sharedClipKey;
 
13224
 
 
13225
                // Animation option is set to true
 
13226
                if (animation && !isObject(animation)) {
 
13227
                        animation = defaultPlotOptions[series.type].animation;
 
13228
                }
 
13229
                sharedClipKey = '_sharedClip' + animation.duration + animation.easing;
 
13230
 
 
13231
                // Initialize the animation. Set up the clipping rectangle.
 
13232
                if (init) {
 
13233
 
 
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'];
 
13237
                        if (!clipRect) {
 
13238
                                chart[sharedClipKey] = clipRect = renderer.clipRect(
 
13239
                                        extend(clipBox, { width: 0 })
 
13240
                                );
 
13241
 
 
13242
                                chart[sharedClipKey + 'm'] = markerClipRect = renderer.clipRect(
 
13243
                                        -99, // include the width of the first marker
 
13244
                                        inverted ? -chart.plotLeft : -chart.plotTop,
 
13245
                                        99,
 
13246
                                        inverted ? chart.chartWidth : chart.chartHeight
 
13247
                                );
 
13248
                        }
 
13249
                        series.group.clip(clipRect);
 
13250
                        series.markerGroup.clip(markerClipRect);
 
13251
                        series.sharedClipKey = sharedClipKey;
 
13252
 
 
13253
                // Run the animation
 
13254
                } else {
 
13255
                        clipRect = chart[sharedClipKey];
 
13256
                        if (clipRect) {
 
13257
                                clipRect.animate({
 
13258
                                        width: chart.plotSizeX
 
13259
                                }, animation);
 
13260
                                chart[sharedClipKey + 'm'].animate({
 
13261
                                        width: chart.plotSizeX + 99
 
13262
                                }, animation);
 
13263
                        }
 
13264
 
 
13265
                        // Delete this function to allow it only once
 
13266
                        series.animate = null;
 
13267
 
 
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);
 
13273
                }
 
13274
        },
 
13275
 
 
13276
        /**
 
13277
         * This runs after animation to land on the final plot clipping
 
13278
         */
 
13279
        afterAnimate: function () {
 
13280
                var chart = this.chart,
 
13281
                        sharedClipKey = this.sharedClipKey,
 
13282
                        group = this.group;
 
13283
 
 
13284
                if (group && this.options.clip !== false) {
 
13285
                        group.clip(chart.clipRect);
 
13286
                        this.markerGroup.clip(); // no clip
 
13287
                }
 
13288
 
 
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();
 
13294
                        }
 
13295
                }, 100);
 
13296
        },
 
13297
 
 
13298
        /**
 
13299
         * Draw the markers
 
13300
         */
 
13301
        drawPoints: function () {
 
13302
                var series = this,
 
13303
                        pointAttr,
 
13304
                        points = series.points,
 
13305
                        chart = series.chart,
 
13306
                        plotX,
 
13307
                        plotY,
 
13308
                        i,
 
13309
                        point,
 
13310
                        radius,
 
13311
                        symbol,
 
13312
                        isImage,
 
13313
                        graphic,
 
13314
                        options = series.options,
 
13315
                        seriesMarkerOptions = options.marker,
 
13316
                        seriesPointAttr = series.pointAttr[''],
 
13317
                        pointMarkerOptions,
 
13318
                        enabled,
 
13319
                        isInside,
 
13320
                        markerGroup = series.markerGroup;
 
13321
 
 
13322
                if (seriesMarkerOptions.enabled || series._hasPointMarkers) {
 
13323
 
 
13324
                        i = points.length;
 
13325
                        while (i--) {
 
13326
                                point = points[i];
 
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
 
13333
 
 
13334
                                // only draw the point if y is defined
 
13335
                                if (enabled && plotY !== UNDEFINED && !isNaN(plotY) && point.y !== null) {
 
13336
 
 
13337
                                        // shortcuts
 
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;
 
13342
 
 
13343
                                        if (graphic) { // update
 
13344
                                                graphic
 
13345
                                                        .attr({ // Since the marker group isn't clipped, each individual marker must be toggled
 
13346
                                                                visibility: isInside ? 'inherit' : HIDDEN
 
13347
                                                        })
 
13348
                                                        .animate(extend({
 
13349
                                                                x: plotX - radius,
 
13350
                                                                y: plotY - radius
 
13351
                                                        }, graphic.symbolName ? { // don't apply to image symbols #507
 
13352
                                                                width: 2 * radius,
 
13353
                                                                height: 2 * radius
 
13354
                                                        } : {}));
 
13355
                                        } else if (isInside && (radius > 0 || isImage)) {
 
13356
                                                point.graphic = graphic = chart.renderer.symbol(
 
13357
                                                        symbol,
 
13358
                                                        plotX - radius,
 
13359
                                                        plotY - radius,
 
13360
                                                        2 * radius,
 
13361
                                                        2 * radius
 
13362
                                                )
 
13363
                                                .attr(pointAttr)
 
13364
                                                .add(markerGroup);
 
13365
                                        }
 
13366
 
 
13367
                                } else if (graphic) {
 
13368
                                        point.graphic = graphic.destroy(); // #1269
 
13369
                                }
 
13370
                        }
 
13371
                }
 
13372
 
 
13373
        },
 
13374
 
 
13375
        /**
 
13376
         * Convert state properties from API naming conventions to SVG attributes
 
13377
         *
 
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
 
13381
         */
 
13382
        convertAttribs: function (options, base1, base2, base3) {
 
13383
                var conversion = this.pointAttrToOptions,
 
13384
                        attr,
 
13385
                        option,
 
13386
                        obj = {};
 
13387
 
 
13388
                options = options || {};
 
13389
                base1 = base1 || {};
 
13390
                base2 = base2 || {};
 
13391
                base3 = base3 || {};
 
13392
 
 
13393
                for (attr in conversion) {
 
13394
                        option = conversion[attr];
 
13395
                        obj[attr] = pick(options[option], base1[attr], base2[attr], base3[attr]);
 
13396
                }
 
13397
                return obj;
 
13398
        },
 
13399
 
 
13400
        /**
 
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.
 
13406
         */
 
13407
        getAttribs: function () {
 
13408
                var series = this,
 
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,
 
13415
                        normalDefaults = {
 
13416
                                stroke: seriesColor,
 
13417
                                fill: seriesColor
 
13418
                        },
 
13419
                        points = series.points || [], // #927
 
13420
                        i,
 
13421
                        point,
 
13422
                        seriesPointAttr = [],
 
13423
                        pointAttr,
 
13424
                        pointAttrToOptions = series.pointAttrToOptions,
 
13425
                        hasPointSpecificOptions = series.hasPointSpecificOptions,
 
13426
                        negativeColor = seriesOptions.negativeColor,
 
13427
                        defaultLineColor = normalOptions.lineColor,
 
13428
                        defaultFillColor = normalOptions.fillColor,
 
13429
                        turboThreshold = seriesOptions.turboThreshold,
 
13430
                        attr,
 
13431
                        key;
 
13432
 
 
13433
                // series type specific modifications
 
13434
                if (seriesOptions.marker) { // line, spline, area, areaspline, scatter
 
13435
 
 
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;
 
13439
 
 
13440
                } else { // column, bar, pie
 
13441
 
 
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();
 
13446
                }
 
13447
 
 
13448
                // general point attributes for the series normal state
 
13449
                seriesPointAttr[NORMAL_STATE] = series.convertAttribs(normalOptions, normalDefaults);
 
13450
 
 
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]);
 
13455
                });
 
13456
 
 
13457
                // set it
 
13458
                series.pointAttr = seriesPointAttr;
 
13459
 
 
13460
 
 
13461
                // Generate the point-specific attribute collections if specific point
 
13462
                // options are given. If not, create a referance to the series wide point
 
13463
                // attributes
 
13464
                i = points.length;
 
13465
                if (!turboThreshold || i < turboThreshold || hasPointSpecificOptions) {
 
13466
                        while (i--) {
 
13467
                                point = points[i];
 
13468
                                normalOptions = (point.options && point.options.marker) || point.options;
 
13469
                                if (normalOptions && normalOptions.enabled === false) {
 
13470
                                        normalOptions.radius = 0;
 
13471
                                }
 
13472
 
 
13473
                                if (point.negative && negativeColor) {
 
13474
                                        point.color = point.fillColor = negativeColor;
 
13475
                                }
 
13476
 
 
13477
                                hasPointSpecificOptions = seriesOptions.colorByPoint || point.color; // #868
 
13478
 
 
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;
 
13484
                                                }
 
13485
                                        }
 
13486
                                }
 
13487
 
 
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 || {};
 
13492
                                        pointAttr = [];
 
13493
                                        stateOptions = normalOptions.states || {}; // reassign for individual point
 
13494
                                        pointStateOptionsHover = stateOptions[HOVER_STATE] = stateOptions[HOVER_STATE] || {};
 
13495
 
 
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) ||
 
13500
                                                        Color(point.color)
 
13501
                                                                .brighten(pointStateOptionsHover.brightness || stateOptionsHover.brightness)
 
13502
                                                                .get();
 
13503
                                        }
 
13504
 
 
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;
 
13509
                                        }
 
13510
                                        if (!defaultLineColor) {
 
13511
                                                attr.lineColor = point.color; // Bubbles take point color, line markers use white
 
13512
                                        }
 
13513
                                        pointAttr[NORMAL_STATE] = series.convertAttribs(extend(attr, normalOptions), seriesPointAttr[NORMAL_STATE]);
 
13514
 
 
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]
 
13520
                                        );
 
13521
 
 
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]
 
13527
                                        );
 
13528
 
 
13529
 
 
13530
                                // no marker config object is created: copy a reference to the series-wide
 
13531
                                // attribute collection
 
13532
                                } else {
 
13533
                                        pointAttr = seriesPointAttr;
 
13534
                                }
 
13535
 
 
13536
                                point.pointAttr = pointAttr;
 
13537
                        }
 
13538
                }
 
13539
        },
 
13540
 
 
13541
        /**
 
13542
         * Clear DOM objects and free up memory
 
13543
         */
 
13544
        destroy: function () {
 
13545
                var series = this,
 
13546
                        chart = series.chart,
 
13547
                        issue134 = /AppleWebKit\/533/.test(userAgent),
 
13548
                        destroy,
 
13549
                        i,
 
13550
                        data = series.data || [],
 
13551
                        point,
 
13552
                        prop,
 
13553
                        axis;
 
13554
 
 
13555
                // add event hook
 
13556
                fireEvent(series, 'destroy');
 
13557
 
 
13558
                // remove all events
 
13559
                removeEvent(series);
 
13560
 
 
13561
                // erase from axes
 
13562
                each(series.axisTypes || [], function (AXIS) {
 
13563
                        axis = series[AXIS];
 
13564
                        if (axis) {
 
13565
                                erase(axis.series, series);
 
13566
                                axis.isDirty = axis.forceRedraw = true;
 
13567
                        }
 
13568
                });
 
13569
 
 
13570
                // remove legend items
 
13571
                if (series.legendItem) {
 
13572
                        series.chart.legend.destroyItem(series);
 
13573
                }
 
13574
 
 
13575
                // destroy all points with their elements
 
13576
                i = data.length;
 
13577
                while (i--) {
 
13578
                        point = data[i];
 
13579
                        if (point && point.destroy) {
 
13580
                                point.destroy();
 
13581
                        }
 
13582
                }
 
13583
                series.points = null;
 
13584
 
 
13585
                // Clear the animation timeout if we are destroying the series during initial animation
 
13586
                clearTimeout(series.animationTimeout);
 
13587
 
 
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]) {
 
13592
 
 
13593
                                // issue 134 workaround
 
13594
                                destroy = issue134 && prop === 'group' ?
 
13595
                                        'hide' :
 
13596
                                        'destroy';
 
13597
 
 
13598
                                series[prop][destroy]();
 
13599
                        }
 
13600
                });
 
13601
 
 
13602
                // remove from hoverSeries
 
13603
                if (chart.hoverSeries === series) {
 
13604
                        chart.hoverSeries = null;
 
13605
                }
 
13606
                erase(chart.series, series);
 
13607
 
 
13608
                // clear all members
 
13609
                for (prop in series) {
 
13610
                        delete series[prop];
 
13611
                }
 
13612
        },
 
13613
 
 
13614
        /**
 
13615
         * Return the graph path of a segment
 
13616
         */
 
13617
        getSegmentPath: function (segment) {
 
13618
                var series = this,
 
13619
                        segmentPath = [],
 
13620
                        step = series.options.step;
 
13621
 
 
13622
                // build the segment line
 
13623
                each(segment, function (point, i) {
 
13624
 
 
13625
                        var plotX = point.plotX,
 
13626
                                plotY = point.plotY,
 
13627
                                lastPoint;
 
13628
 
 
13629
                        if (series.getPointSpline) { // generate the spline as defined in the SplineSeries object
 
13630
                                segmentPath.push.apply(segmentPath, series.getPointSpline(segment, point, i));
 
13631
 
 
13632
                        } else {
 
13633
 
 
13634
                                // moveTo or lineTo
 
13635
                                segmentPath.push(i ? L : M);
 
13636
 
 
13637
                                // step line?
 
13638
                                if (step && i) {
 
13639
                                        lastPoint = segment[i - 1];
 
13640
                                        if (step === 'right') {
 
13641
                                                segmentPath.push(
 
13642
                                                        lastPoint.plotX,
 
13643
                                                        plotY
 
13644
                                                );
 
13645
 
 
13646
                                        } else if (step === 'center') {
 
13647
                                                segmentPath.push(
 
13648
                                                        (lastPoint.plotX + plotX) / 2,
 
13649
                                                        lastPoint.plotY,
 
13650
                                                        (lastPoint.plotX + plotX) / 2,
 
13651
                                                        plotY
 
13652
                                                );
 
13653
 
 
13654
                                        } else {
 
13655
                                                segmentPath.push(
 
13656
                                                        plotX,
 
13657
                                                        lastPoint.plotY
 
13658
                                                );
 
13659
                                        }
 
13660
                                }
 
13661
 
 
13662
                                // normal line to next point
 
13663
                                segmentPath.push(
 
13664
                                        point.plotX,
 
13665
                                        point.plotY
 
13666
                                );
 
13667
                        }
 
13668
                });
 
13669
 
 
13670
                return segmentPath;
 
13671
        },
 
13672
 
 
13673
        /**
 
13674
         * Get the graph path
 
13675
         */
 
13676
        getGraphPath: function () {
 
13677
                var series = this,
 
13678
                        graphPath = [],
 
13679
                        segmentPath,
 
13680
                        singlePoints = []; // used in drawTracker
 
13681
 
 
13682
                // Divide into segments and build graph and area paths
 
13683
                each(series.segments, function (segment) {
 
13684
 
 
13685
                        segmentPath = series.getSegmentPath(segment);
 
13686
 
 
13687
                        // add the segment to the graph, or a single point for tracking
 
13688
                        if (segment.length > 1) {
 
13689
                                graphPath = graphPath.concat(segmentPath);
 
13690
                        } else {
 
13691
                                singlePoints.push(segment[0]);
 
13692
                        }
 
13693
                });
 
13694
 
 
13695
                // Record it for use in drawGraph and drawTracker, and return graphPath
 
13696
                series.singlePoints = singlePoints;
 
13697
                series.graphPath = graphPath;
 
13698
 
 
13699
                return graphPath;
 
13700
 
 
13701
        },
 
13702
 
 
13703
        /**
 
13704
         * Draw the actual graph
 
13705
         */
 
13706
        drawGraph: function () {
 
13707
                var series = this,
 
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;
 
13715
 
 
13716
                if (negativeColor) {
 
13717
                        props.push(['graphNeg', negativeColor]);
 
13718
                }
 
13719
 
 
13720
                // draw the graph
 
13721
                each(props, function (prop, i) {
 
13722
                        var graphKey = prop[0],
 
13723
                                graph = series[graphKey],
 
13724
                                attribs;
 
13725
 
 
13726
                        if (graph) {
 
13727
                                stop(graph); // cancel running animations, #459
 
13728
                                graph.animate({ d: graphPath });
 
13729
 
 
13730
                        } else if (lineWidth && graphPath.length) { // #1487
 
13731
                                attribs = {
 
13732
                                        stroke: prop[1],
 
13733
                                        'stroke-width': lineWidth,
 
13734
                                        fill: NONE,
 
13735
                                        zIndex: 1 // #1069
 
13736
                                };
 
13737
                                if (dashStyle) {
 
13738
                                        attribs.dashstyle = dashStyle;
 
13739
                                } else if (roundCap) {
 
13740
                                        attribs['stroke-linecap'] = attribs['stroke-linejoin'] = 'round';
 
13741
                                }
 
13742
 
 
13743
                                series[graphKey] = series.chart.renderer.path(graphPath)
 
13744
                                        .attr(attribs)
 
13745
                                        .add(series.group)
 
13746
                                        .shadow(!i && options.shadow);
 
13747
                        }
 
13748
                });
 
13749
        },
 
13750
 
 
13751
        /**
 
13752
         * Clip the graphs into the positive and negative coloured graphs
 
13753
         */
 
13754
        clipNeg: function () {
 
13755
                var options = this.options,
 
13756
                        chart = this.chart,
 
13757
                        renderer = chart.renderer,
 
13758
                        negativeColor = options.negativeColor || options.negativeFillColor,
 
13759
                        translatedThreshold,
 
13760
                        posAttr,
 
13761
                        negAttr,
 
13762
                        graph = this.graph,
 
13763
                        area = this.area,
 
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,
 
13770
                        above,
 
13771
                        below;
 
13772
 
 
13773
                if (negativeColor && (graph || area)) {
 
13774
                        translatedThreshold = mathRound(yAxis.toPixels(options.threshold || 0, true));
 
13775
                        if (translatedThreshold < 0) {
 
13776
                                chartSizeMax -= translatedThreshold; // #2534
 
13777
                        }
 
13778
                        above = {
 
13779
                                x: 0,
 
13780
                                y: 0,
 
13781
                                width: chartSizeMax,
 
13782
                                height: translatedThreshold
 
13783
                        };
 
13784
                        below = {
 
13785
                                x: 0,
 
13786
                                y: translatedThreshold,
 
13787
                                width: chartSizeMax,
 
13788
                                height: chartSizeMax
 
13789
                        };
 
13790
 
 
13791
                        if (chart.inverted) {
 
13792
 
 
13793
                                above.height = below.y = chart.plotWidth - translatedThreshold;
 
13794
                                if (renderer.isVML) {
 
13795
                                        above = {
 
13796
                                                x: chart.plotWidth - translatedThreshold - chart.plotLeft,
 
13797
                                                y: 0,
 
13798
                                                width: chartWidth,
 
13799
                                                height: chartHeight
 
13800
                                        };
 
13801
                                        below = {
 
13802
                                                x: translatedThreshold + chart.plotLeft - chartWidth,
 
13803
                                                y: 0,
 
13804
                                                width: chart.plotLeft + translatedThreshold,
 
13805
                                                height: chartWidth
 
13806
                                        };
 
13807
                                }
 
13808
                        }
 
13809
 
 
13810
                        if (yAxis.reversed) {
 
13811
                                posAttr = below;
 
13812
                                negAttr = above;
 
13813
                        } else {
 
13814
                                posAttr = above;
 
13815
                                negAttr = below;
 
13816
                        }
 
13817
 
 
13818
                        if (posClip) { // update
 
13819
                                posClip.animate(posAttr);
 
13820
                                negClip.animate(negAttr);
 
13821
                        } else {
 
13822
 
 
13823
                                this.posClip = posClip = renderer.clipRect(posAttr);
 
13824
                                this.negClip = negClip = renderer.clipRect(negAttr);
 
13825
 
 
13826
                                if (graph && this.graphNeg) {
 
13827
                                        graph.clip(posClip);
 
13828
                                        this.graphNeg.clip(negClip);
 
13829
                                }
 
13830
 
 
13831
                                if (area) {
 
13832
                                        area.clip(posClip);
 
13833
                                        this.areaNeg.clip(negClip);
 
13834
                                }
 
13835
                        }
 
13836
                }
 
13837
        },
 
13838
 
 
13839
        /**
 
13840
         * Initialize and perform group inversion on series.group and series.markerGroup
 
13841
         */
 
13842
        invertGroups: function () {
 
13843
                var series = this,
 
13844
                        chart = series.chart;
 
13845
 
 
13846
                // Pie, go away (#1736)
 
13847
                if (!series.xAxis) {
 
13848
                        return;
 
13849
                }
 
13850
 
 
13851
                // A fixed size is needed for inversion to work
 
13852
                function setInvert() {
 
13853
                        var size = {
 
13854
                                width: series.yAxis.len,
 
13855
                                height: series.xAxis.len
 
13856
                        };
 
13857
 
 
13858
                        each(['group', 'markerGroup'], function (groupName) {
 
13859
                                if (series[groupName]) {
 
13860
                                        series[groupName].attr(size).invert();
 
13861
                                }
 
13862
                        });
 
13863
                }
 
13864
 
 
13865
                addEvent(chart, 'resize', setInvert); // do it on resize
 
13866
                addEvent(series, 'destroy', function () {
 
13867
                        removeEvent(chart, 'resize', setInvert);
 
13868
                });
 
13869
 
 
13870
                // Do it now
 
13871
                setInvert(); // do it now
 
13872
 
 
13873
                // On subsequent render and redraw, just do setInvert without setting up events again
 
13874
                series.invertGroups = setInvert;
 
13875
        },
 
13876
 
 
13877
        /**
 
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.
 
13880
         */
 
13881
        plotGroup: function (prop, name, visibility, zIndex, parent) {
 
13882
                var group = this[prop],
 
13883
                        isNew = !group;
 
13884
 
 
13885
                // Generate it on first call
 
13886
                if (isNew) {
 
13887
                        this[prop] = group = this.chart.renderer.g(name)
 
13888
                                .attr({
 
13889
                                        visibility: visibility,
 
13890
                                        zIndex: zIndex || 0.1 // IE8 needs this
 
13891
                                })
 
13892
                                .add(parent);
 
13893
                }
 
13894
                // Place it on first and subsequent (redraw) calls
 
13895
                group[isNew ? 'attr' : 'animate'](this.getPlotBox());
 
13896
                return group;
 
13897
        },
 
13898
 
 
13899
        /**
 
13900
         * Get the translation and scale for the plot area of this series
 
13901
         */
 
13902
        getPlotBox: function () {
 
13903
                return {
 
13904
                        translateX: this.xAxis ? this.xAxis.left : this.chart.plotLeft,
 
13905
                        translateY: this.yAxis ? this.yAxis.top : this.chart.plotTop,
 
13906
                        scaleX: 1, // #1623
 
13907
                        scaleY: 1
 
13908
                };
 
13909
        },
 
13910
 
 
13911
        /**
 
13912
         * Render the graph and markers
 
13913
         */
 
13914
        render: function () {
 
13915
                var series = this,
 
13916
                        chart = series.chart,
 
13917
                        group,
 
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;
 
13927
 
 
13928
                // the group
 
13929
                group = series.plotGroup(
 
13930
                        'group',
 
13931
                        'series',
 
13932
                        visibility,
 
13933
                        zIndex,
 
13934
                        chartSeriesGroup
 
13935
                );
 
13936
 
 
13937
                series.markerGroup = series.plotGroup(
 
13938
                        'markerGroup',
 
13939
                        'markers',
 
13940
                        visibility,
 
13941
                        zIndex,
 
13942
                        chartSeriesGroup
 
13943
                );
 
13944
 
 
13945
                // initiate the animation
 
13946
                if (doAnimation) {
 
13947
                        series.animate(true);
 
13948
                }
 
13949
 
 
13950
                // cache attributes for shapes
 
13951
                series.getAttribs();
 
13952
 
 
13953
                // SVGRenderer needs to know this before drawing elements (#1089, #1795)
 
13954
                group.inverted = series.isCartesian ? chart.inverted : false;
 
13955
 
 
13956
                // draw the graph if any
 
13957
                if (series.drawGraph) {
 
13958
                        series.drawGraph();
 
13959
                        series.clipNeg();
 
13960
                }
 
13961
 
 
13962
                // draw the data labels (inn pies they go before the points)
 
13963
                if (series.drawDataLabels) {
 
13964
                        series.drawDataLabels();
 
13965
                }
 
13966
 
 
13967
                // draw the points
 
13968
                if (series.visible) {
 
13969
                        series.drawPoints();
 
13970
                }
 
13971
 
 
13972
 
 
13973
                // draw the mouse tracking area
 
13974
                if (series.drawTracker && series.options.enableMouseTracking !== false) {
 
13975
                        series.drawTracker();
 
13976
                }
 
13977
 
 
13978
                // Handle inverted series and tracker groups
 
13979
                if (chart.inverted) {
 
13980
                        series.invertGroups();
 
13981
                }
 
13982
 
 
13983
                // Initial clipping, must be defined after inverting groups for VML
 
13984
                if (options.clip !== false && !series.sharedClipKey && !hasRendered) {
 
13985
                        group.clip(chart.clipRect);
 
13986
                }
 
13987
 
 
13988
                // Run the animation
 
13989
                if (doAnimation) {
 
13990
                        series.animate();
 
13991
                } else if (!hasRendered) {
 
13992
                        series.afterAnimate();
 
13993
                }
 
13994
 
 
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;
 
13998
        },
 
13999
 
 
14000
        /**
 
14001
         * Redraw the series after an update in the axes.
 
14002
         */
 
14003
        redraw: function () {
 
14004
                var series = this,
 
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;
 
14010
 
 
14011
                // reposition on resize
 
14012
                if (group) {
 
14013
                        if (chart.inverted) {
 
14014
                                group.attr({
 
14015
                                        width: chart.plotWidth,
 
14016
                                        height: chart.plotHeight
 
14017
                                });
 
14018
                        }
 
14019
 
 
14020
                        group.animate({
 
14021
                                translateX: pick(xAxis && xAxis.left, chart.plotLeft),
 
14022
                                translateY: pick(yAxis && yAxis.top, chart.plotTop)
 
14023
                        });
 
14024
                }
 
14025
 
 
14026
                series.translate();
 
14027
                series.setTooltipPoints(true);
 
14028
                series.render();
 
14029
 
 
14030
                if (wasDirtyData) {
 
14031
                        fireEvent(series, 'updatedData');
 
14032
                }
 
14033
        }
 
14034
}; // end Series prototype
 
14035
 
 
14036
/**
 
14037
 * The class for stack items
 
14038
 */
 
14039
function StackItem(axis, options, isNegative, x, stackOption, stacking) {
 
14040
        
 
14041
        var inverted = axis.chart.inverted;
 
14042
 
 
14043
        this.axis = axis;
 
14044
 
 
14045
        // Tells if the stack is negative
 
14046
        this.isNegative = isNegative;
 
14047
 
 
14048
        // Save the options to be able to style the label
 
14049
        this.options = options;
 
14050
 
 
14051
        // Save the x value to be able to position the label later
 
14052
        this.x = x;
 
14053
 
 
14054
        // Initialize total value
 
14055
        this.total = null;
 
14056
 
 
14057
        // This will keep each points' extremes stored by series.index
 
14058
        this.points = {};
 
14059
 
 
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';
 
14063
 
 
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)
 
14072
        };
 
14073
 
 
14074
        this.textAlign = options.textAlign || (inverted ? (isNegative ? 'right' : 'left') : 'center');
 
14075
}
 
14076
 
 
14077
StackItem.prototype = {
 
14078
        destroy: function () {
 
14079
                destroyObjectProperties(this, this.axis);
 
14080
        },
 
14081
 
 
14082
        /**
 
14083
         * Renders the stack total label and adds it to the stack label group.
 
14084
         */
 
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
 
14091
 
 
14092
                // Change the text to reflect the new total and set visibility to hidden in case the serie is hidden
 
14093
                if (this.label) {
 
14094
                        this.label.attr({text: str, visibility: HIDDEN});
 
14095
                // Create new label
 
14096
                } else {
 
14097
                        this.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
 
14100
                                        .attr({
 
14101
                                                align: this.textAlign,                          // fix the text-anchor
 
14102
                                                rotation: options.rotation,     // rotation
 
14103
                                                visibility: HIDDEN                                      // hidden until setOffset is called
 
14104
                                        })                              
 
14105
                                        .add(group);                                                    // add to the labels-group
 
14106
                }
 
14107
        },
 
14108
 
 
14109
        /**
 
14110
         * Sets the offset that the stack has from the x value and repositions the label.
 
14111
         */
 
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
 
14128
                        },
 
14129
                        label = this.label,
 
14130
                        alignAttr;
 
14131
                
 
14132
                if (label) {
 
14133
                        label.align(this.alignOptions, null, stackBox); // align the label to the box
 
14134
                                
 
14135
                        // Set visibility (#678)
 
14136
                        alignAttr = label.alignAttr;
 
14137
                        label[this.options.crop === false || chart.isInsidePlot(alignAttr.x, alignAttr.y) ? 'show' : 'hide'](true);
 
14138
                }
 
14139
        }
 
14140
};
 
14141
 
 
14142
 
 
14143
// Stacking methods defined on the Axis prototype
 
14144
 
 
14145
/**
 
14146
 * Build the stacks from top down
 
14147
 */
 
14148
Axis.prototype.buildStacks = function () {
 
14149
        var series = this.series,
 
14150
                reversedStacks = pick(this.options.reversedStacks, true),
 
14151
                i = series.length;
 
14152
        if (!this.isXAxis) {
 
14153
                this.usePercentage = false;
 
14154
                while (i--) {
 
14155
                        series[reversedStacks ? i : series.length - i - 1].setStackedPoints();
 
14156
                }
 
14157
                // Loop up again to compute percent stack
 
14158
                if (this.usePercentage) {
 
14159
                        for (i = 0; i < series.length; i++) {
 
14160
                                series[i].setPercentStacks();
 
14161
                        }
 
14162
                }
 
14163
        }
 
14164
};
 
14165
 
 
14166
Axis.prototype.renderStackTotals = function () {
 
14167
        var axis = this,
 
14168
                chart = axis.chart,
 
14169
                renderer = chart.renderer,
 
14170
                stacks = axis.stacks,
 
14171
                stackKey, 
 
14172
                oneStack, 
 
14173
                stackCategory,
 
14174
                stackTotalGroup = axis.stackTotalGroup;
 
14175
 
 
14176
        // Create a separate group for the stack total labels
 
14177
        if (!stackTotalGroup) {
 
14178
                axis.stackTotalGroup = stackTotalGroup =
 
14179
                        renderer.g('stack-labels')
 
14180
                                .attr({
 
14181
                                        visibility: VISIBLE,
 
14182
                                        zIndex: 6
 
14183
                                })
 
14184
                                .add();
 
14185
        }
 
14186
 
 
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);
 
14190
 
 
14191
        // Render each stack total
 
14192
        for (stackKey in stacks) {
 
14193
                oneStack = stacks[stackKey];
 
14194
                for (stackCategory in oneStack) {
 
14195
                        oneStack[stackCategory].render(stackTotalGroup);
 
14196
                }
 
14197
        }
 
14198
};
 
14199
 
 
14200
 
 
14201
// Stacking methods defnied for Series prototype
 
14202
 
 
14203
/**
 
14204
 * Adds series' points value to corresponding stack
 
14205
 */
 
14206
Series.prototype.setStackedPoints = function () {
 
14207
        if (!this.options.stacking || (this.visible !== true && this.chart.options.chart.ignoreHiddenSeries !== false)) {
 
14208
                return;
 
14209
        }
 
14210
 
 
14211
        var series = this,
 
14212
                xData = series.processedXData,
 
14213
                yData = series.processedYData,
 
14214
                stackedYData = [],
 
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,
 
14226
                isNegative,
 
14227
                stack,
 
14228
                other,
 
14229
                key,
 
14230
                i,
 
14231
                x,
 
14232
                y;
 
14233
 
 
14234
        // loop over the non-null y values and read them into a local array
 
14235
        for (i = 0; i < yDataLength; i++) {
 
14236
                x = xData[i];
 
14237
                y = yData[i];
 
14238
 
 
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;
 
14243
 
 
14244
                // Create empty object for this stack if it doesn't exist yet
 
14245
                if (!stacks[key]) {
 
14246
                        stacks[key] = {};
 
14247
                }
 
14248
 
 
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;
 
14254
                        } else {
 
14255
                                stacks[key][x] = new StackItem(yAxis, yAxis.options.stackLabels, isNegative, x, stackOption, stacking);
 
14256
                        }
 
14257
                }
 
14258
 
 
14259
                // If the StackItem doesn't exist, create it first
 
14260
                stack = stacks[key][x];
 
14261
                stack.points[series.index] = [stack.cum || 0];
 
14262
 
 
14263
                // Add value to the stack total
 
14264
                if (stacking === 'percent') {
 
14265
 
 
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;
 
14271
 
 
14272
                        // Percent stacked areas
 
14273
                        } else {
 
14274
                                stack.total = correctFloat(stack.total + (mathAbs(y) || 0));
 
14275
                        }
 
14276
                } else {
 
14277
                        stack.total = correctFloat(stack.total + (y || 0));
 
14278
                }
 
14279
 
 
14280
                stack.cum = (stack.cum || 0) + (y || 0);
 
14281
 
 
14282
                stack.points[series.index].push(stack.cum);
 
14283
                stackedYData[i] = stack.cum;
 
14284
 
 
14285
        }
 
14286
 
 
14287
        if (stacking === 'percent') {
 
14288
                yAxis.usePercentage = true;
 
14289
        }
 
14290
 
 
14291
        this.stackedYData = stackedYData; // To be used in getExtremes
 
14292
 
 
14293
        // Reset old stacks
 
14294
        yAxis.oldStacks = {};
 
14295
};
 
14296
 
 
14297
/**
 
14298
 * Iterate over all stacks and compute the absolute values to percent
 
14299
 */
 
14300
Series.prototype.setPercentStacks = function () {
 
14301
        var series = this,
 
14302
                stackKey = series.stackKey,
 
14303
                stacks = series.yAxis.stacks,
 
14304
                processedXData = series.processedXData;
 
14305
 
 
14306
        each([stackKey, '-' + stackKey], function (key) {
 
14307
                var i = processedXData.length,
 
14308
                        x,
 
14309
                        stack,
 
14310
                        pointExtremes,
 
14311
                        totalFactor;
 
14312
 
 
14313
                while (i--) {
 
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];
 
14322
                        }
 
14323
                }
 
14324
        });
 
14325
};
 
14326
 
 
14327
// Extend the Chart prototype for dynamic methods
 
14328
extend(Chart.prototype, {
 
14329
 
 
14330
        /**
 
14331
         * Add a series dynamically after  time
 
14332
         *
 
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
 
14336
         *    configuration
 
14337
         *
 
14338
         * @return {Object} series The newly created series object
 
14339
         */
 
14340
        addSeries: function (options, redraw, animation) {
 
14341
                var series,
 
14342
                        chart = this;
 
14343
 
 
14344
                if (options) {
 
14345
                        redraw = pick(redraw, true); // defaults to true
 
14346
 
 
14347
                        fireEvent(chart, 'addSeries', { options: options }, function () {
 
14348
                                series = chart.initSeries(options);
 
14349
 
 
14350
                                chart.isDirtyLegend = true; // the series array is out of sync with the display
 
14351
                                chart.linkSeries();
 
14352
                                if (redraw) {
 
14353
                                        chart.redraw(animation);
 
14354
                                }
 
14355
                        });
 
14356
                }
 
14357
 
 
14358
                return series;
 
14359
        },
 
14360
 
 
14361
        /**
 
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
 
14365
     */
 
14366
        addAxis: function (options, isX, redraw, animation) {
 
14367
                var key = isX ? 'xAxis' : 'yAxis',
 
14368
                        chartOptions = this.options,
 
14369
                        axis;
 
14370
 
 
14371
                /*jslint unused: false*/
 
14372
                axis = new Axis(this, merge(options, {
 
14373
                        index: this[key].length,
 
14374
                        isX: isX
 
14375
                }));
 
14376
                /*jslint unused: true*/
 
14377
 
 
14378
                // Push the new axis options to the chart options
 
14379
                chartOptions[key] = splat(chartOptions[key] || {});
 
14380
                chartOptions[key].push(options);
 
14381
 
 
14382
                if (pick(redraw, true)) {
 
14383
                        this.redraw(animation);
 
14384
                }
 
14385
        },
 
14386
 
 
14387
        /**
 
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
 
14390
         */
 
14391
        showLoading: function (str) {
 
14392
                var chart = this,
 
14393
                        options = chart.options,
 
14394
                        loadingDiv = chart.loadingDiv;
 
14395
 
 
14396
                var loadingOptions = options.loading;
 
14397
 
 
14398
                // create the layer at the first call
 
14399
                if (!loadingDiv) {
 
14400
                        chart.loadingDiv = loadingDiv = createElement(DIV, {
 
14401
                                className: PREFIX + 'loading'
 
14402
                        }, extend(loadingOptions.style, {
 
14403
                                zIndex: 10,
 
14404
                                display: NONE
 
14405
                        }), chart.container);
 
14406
 
 
14407
                        chart.loadingSpan = createElement(
 
14408
                                'span',
 
14409
                                null,
 
14410
                                loadingOptions.labelStyle,
 
14411
                                loadingDiv
 
14412
                        );
 
14413
 
 
14414
                }
 
14415
 
 
14416
                // update text
 
14417
                chart.loadingSpan.innerHTML = str || options.lang.loading;
 
14418
 
 
14419
                // show it
 
14420
                if (!chart.loadingShown) {
 
14421
                        css(loadingDiv, {
 
14422
                                opacity: 0,
 
14423
                                display: '',
 
14424
                                left: chart.plotLeft + PX,
 
14425
                                top: chart.plotTop + PX,
 
14426
                                width: chart.plotWidth + PX,
 
14427
                                height: chart.plotHeight + PX
 
14428
                        });
 
14429
                        animate(loadingDiv, {
 
14430
                                opacity: loadingOptions.style.opacity
 
14431
                        }, {
 
14432
                                duration: loadingOptions.showDuration || 0
 
14433
                        });
 
14434
                        chart.loadingShown = true;
 
14435
                }
 
14436
        },
 
14437
 
 
14438
        /**
 
14439
         * Hide the loading layer
 
14440
         */
 
14441
        hideLoading: function () {
 
14442
                var options = this.options,
 
14443
                        loadingDiv = this.loadingDiv;
 
14444
 
 
14445
                if (loadingDiv) {
 
14446
                        animate(loadingDiv, {
 
14447
                                opacity: 0
 
14448
                        }, {
 
14449
                                duration: options.loading.hideDuration || 100,
 
14450
                                complete: function () {
 
14451
                                        css(loadingDiv, { display: NONE });
 
14452
                                }
 
14453
                        });
 
14454
                }
 
14455
                this.loadingShown = false;
 
14456
        }
 
14457
});
 
14458
 
 
14459
// extend the Point prototype for dynamic methods
 
14460
extend(Point.prototype, {
 
14461
        /**
 
14462
         * Update the point with new options (typically x/y data) and optionally redraw the series.
 
14463
         *
 
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
 
14467
         *    configuration
 
14468
         *
 
14469
         */
 
14470
        update: function (options, redraw, animation) {
 
14471
                var point = this,
 
14472
                        series = point.series,
 
14473
                        graphic = point.graphic,
 
14474
                        i,
 
14475
                        data = series.data,
 
14476
                        chart = series.chart,
 
14477
                        seriesOptions = series.options;
 
14478
 
 
14479
                redraw = pick(redraw, true);
 
14480
 
 
14481
                // fire the event with a default handler of doing the update
 
14482
                point.firePointEvent('update', { options: options }, function () {
 
14483
 
 
14484
                        point.applyOptions(options);
 
14485
 
 
14486
                        // update visuals
 
14487
                        if (isObject(options)) {
 
14488
                                series.getAttribs();
 
14489
                                if (graphic) {
 
14490
                                        if (options && options.marker && options.marker.symbol) {
 
14491
                                                point.graphic = graphic.destroy();
 
14492
                                        } else {
 
14493
                                                graphic.attr(point.pointAttr[point.state || '']);
 
14494
                                        }
 
14495
                                }
 
14496
                                if (options && options.dataLabels && point.dataLabel) { // #2468
 
14497
                                        point.dataLabel = point.dataLabel.destroy();
 
14498
                                }
 
14499
                        }
 
14500
 
 
14501
                        // record changes in the parallel arrays
 
14502
                        i = inArray(point, data);
 
14503
                        series.updateParallelArrays(point, i);
 
14504
 
 
14505
                        seriesOptions.data[i] = point.options;
 
14506
 
 
14507
                        // redraw
 
14508
                        series.isDirty = series.isDirtyData = true;
 
14509
                        if (!series.fixedBox && series.hasCartesianSeries) { // #1906, #2320
 
14510
                                chart.isDirtyBox = true;
 
14511
                        }
 
14512
 
 
14513
                        if (seriesOptions.legendType === 'point') { // #1831, #1885
 
14514
                                chart.legend.destroyItem(point);
 
14515
                        }
 
14516
                        if (redraw) {
 
14517
                                chart.redraw(animation);
 
14518
                        }
 
14519
                });
 
14520
        },
 
14521
 
 
14522
        /**
 
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
 
14526
         *    configuration
 
14527
         */
 
14528
        remove: function (redraw, animation) {
 
14529
                var point = this,
 
14530
                        series = point.series,
 
14531
                        points = series.points,
 
14532
                        chart = series.chart,
 
14533
                        i,
 
14534
                        data = series.data;
 
14535
 
 
14536
                setAnimation(animation, chart);
 
14537
                redraw = pick(redraw, true);
 
14538
 
 
14539
                // fire the event with a default handler of removing the point
 
14540
                point.firePointEvent('remove', null, function () {
 
14541
 
 
14542
                        // splice all the parallel arrays
 
14543
                        i = inArray(point, data);
 
14544
                        if (data.length === points.length) {
 
14545
                                points.splice(i, 1);
 
14546
                        }
 
14547
                        data.splice(i, 1);
 
14548
                        series.options.data.splice(i, 1);
 
14549
                        series.updateParallelArrays(point, 'splice', i, 1);
 
14550
 
 
14551
                        point.destroy();
 
14552
 
 
14553
                        // redraw
 
14554
                        series.isDirty = true;
 
14555
                        series.isDirtyData = true;
 
14556
                        if (redraw) {
 
14557
                                chart.redraw();
 
14558
                        }
 
14559
                });
 
14560
        }
 
14561
});
 
14562
 
 
14563
// Extend the series prototype for dynamic methods
 
14564
extend(Series.prototype, {
 
14565
        /**
 
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
 
14572
         *    configuration
 
14573
         */
 
14574
        addPoint: function (options, redraw, shift, animation) {
 
14575
                var series = this,
 
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,
 
14584
                        point,
 
14585
                        isInTheMiddle,
 
14586
                        xData = series.xData,
 
14587
                        x,
 
14588
                        i;
 
14589
 
 
14590
                setAnimation(animation, chart);
 
14591
 
 
14592
                // Make graph animate sideways
 
14593
                if (shift) {
 
14594
                        each([graph, area, series.graphNeg, series.areaNeg], function (shape) {
 
14595
                                if (shape) {
 
14596
                                        shape.shift = currentShift + 1;
 
14597
                                }
 
14598
                        });
 
14599
                }
 
14600
                if (area) {
 
14601
                        area.isArea = true; // needed in animation, both with and without shift
 
14602
                }
 
14603
 
 
14604
                // Optional redraw, defaults to true
 
14605
                redraw = pick(redraw, true);
 
14606
 
 
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]);
 
14611
                x = point.x;
 
14612
 
 
14613
                // Get the insertion point
 
14614
                i = xData.length;
 
14615
                if (series.requireSorting && x < xData[i - 1]) {
 
14616
                        isInTheMiddle = true;
 
14617
                        while (i && xData[i - 1] > x) {
 
14618
                                i--;
 
14619
                        }
 
14620
                }
 
14621
 
 
14622
                series.updateParallelArrays(point, 'splice', i, 0, 0); // insert undefined item
 
14623
                series.updateParallelArrays(point, i); // update it
 
14624
 
 
14625
                if (names) {
 
14626
                        names[x] = point.name;
 
14627
                }
 
14628
                dataOptions.splice(i, 0, options);
 
14629
 
 
14630
                if (isInTheMiddle) {
 
14631
                        series.data.splice(i, 0, null);
 
14632
                        series.processData();
 
14633
                }
 
14634
 
 
14635
                // Generate points to be added to the legend (#1329)
 
14636
                if (seriesOptions.legendType === 'point') {
 
14637
                        series.generatePoints();
 
14638
                }
 
14639
 
 
14640
                // Shift the first point off the parallel arrays
 
14641
                // todo: consider series.removePoint(i) method
 
14642
                if (shift) {
 
14643
                        if (data[0] && data[0].remove) {
 
14644
                                data[0].remove(false);
 
14645
                        } else {
 
14646
                                data.shift();
 
14647
                                series.updateParallelArrays(point, 'shift');
 
14648
 
 
14649
                                dataOptions.shift();
 
14650
                        }
 
14651
                }
 
14652
 
 
14653
                // redraw
 
14654
                series.isDirty = true;
 
14655
                series.isDirtyData = true;
 
14656
                if (redraw) {
 
14657
                        series.getAttribs(); // #1937
 
14658
                        chart.redraw();
 
14659
                }
 
14660
        },
 
14661
 
 
14662
        /**
 
14663
         * Remove a series and optionally redraw the chart
 
14664
         *
 
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
 
14667
         *    configuration
 
14668
         */
 
14669
 
 
14670
        remove: function (redraw, animation) {
 
14671
                var series = this,
 
14672
                        chart = series.chart;
 
14673
                redraw = pick(redraw, true);
 
14674
 
 
14675
                if (!series.isRemoving) {  /* prevent triggering native event in jQuery
 
14676
                                (calling the remove function from the remove event) */
 
14677
                        series.isRemoving = true;
 
14678
 
 
14679
                        // fire the event with a default handler of removing the point
 
14680
                        fireEvent(series, 'remove', null, function () {
 
14681
 
 
14682
 
 
14683
                                // destroy elements
 
14684
                                series.destroy();
 
14685
 
 
14686
 
 
14687
                                // redraw
 
14688
                                chart.isDirtyLegend = chart.isDirtyBox = true;
 
14689
                                chart.linkSeries();
 
14690
 
 
14691
                                if (redraw) {
 
14692
                                        chart.redraw(animation);
 
14693
                                }
 
14694
                        });
 
14695
 
 
14696
                }
 
14697
                series.isRemoving = false;
 
14698
        },
 
14699
 
 
14700
        /**
 
14701
         * Update the series with a new set of options
 
14702
         */
 
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,
 
14710
                        n;
 
14711
 
 
14712
                // Do the merge, with some forced options
 
14713
                newOptions = merge(oldOptions, {
 
14714
                        animation: false,
 
14715
                        index: this.index,
 
14716
                        pointStart: this.xData[0] // when updating after addPoint
 
14717
                }, { data: this.options.data }, newOptions);
 
14718
 
 
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;
 
14724
                        }
 
14725
                }
 
14726
                extend(this, seriesTypes[newOptions.type || oldType].prototype);
 
14727
 
 
14728
 
 
14729
                this.init(chart, newOptions);
 
14730
                if (pick(redraw, true)) {
 
14731
                        chart.redraw(false);
 
14732
                }
 
14733
        }
 
14734
});
 
14735
 
 
14736
// Extend the Axis.prototype for dynamic methods
 
14737
extend(Axis.prototype, {
 
14738
 
 
14739
        /**
 
14740
         * Update the axis with a new options structure
 
14741
         */
 
14742
        update: function (newOptions, redraw) {
 
14743
                var chart = this.chart;
 
14744
 
 
14745
                newOptions = chart.options[this.coll][this.options.index] = merge(this.userOptions, newOptions);
 
14746
 
 
14747
                this.destroy(true);
 
14748
                this._addedPlotLB = this.userMin = this.userMax = UNDEFINED; // #1611, #2306
 
14749
 
 
14750
                this.init(chart, extend(newOptions, { events: UNDEFINED }));
 
14751
 
 
14752
                chart.isDirtyBox = true;
 
14753
                if (pick(redraw, true)) {
 
14754
                        chart.redraw();
 
14755
                }
 
14756
        },
 
14757
 
 
14758
        /**
 
14759
     * Remove the axis from the chart
 
14760
     */
 
14761
        remove: function (redraw) {
 
14762
                var chart = this.chart,
 
14763
                        key = this.coll, // xAxis or yAxis
 
14764
                        axisSeries = this.series,
 
14765
                        i = axisSeries.length;
 
14766
 
 
14767
                // Remove associated series (#2687)
 
14768
                while (i--) {
 
14769
                        if (axisSeries[i]) {
 
14770
                                axisSeries[i].remove(false);
 
14771
                        }
 
14772
                }
 
14773
 
 
14774
                // Remove the axis
 
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;
 
14780
                });
 
14781
                this.destroy();
 
14782
                chart.isDirtyBox = true;
 
14783
 
 
14784
                if (pick(redraw, true)) {
 
14785
                        chart.redraw();
 
14786
                }
 
14787
        },
 
14788
 
 
14789
        /**
 
14790
         * Update the axis title by options
 
14791
         */
 
14792
        setTitle: function (newTitleOptions, redraw) {
 
14793
                this.update({ title: newTitleOptions }, redraw);
 
14794
        },
 
14795
 
 
14796
        /**
 
14797
         * Set new axis categories and optionally redraw
 
14798
         * @param {Array} categories
 
14799
         * @param {Boolean} redraw
 
14800
         */
 
14801
        setCategories: function (categories, redraw) {
 
14802
                this.update({ categories: categories }, redraw);
 
14803
        }
 
14804
 
 
14805
});
 
14806
 
 
14807
 
 
14808
/**
 
14809
 * LineSeries object
 
14810
 */
 
14811
var LineSeries = extendClass(Series);
 
14812
seriesTypes.line = LineSeries;
 
14813
 
 
14814
/**
 
14815
 * Set the default options for area
 
14816
 */
 
14817
defaultPlotOptions.area = merge(defaultSeriesOptions, {
 
14818
        threshold: 0
 
14819
        // trackByArea: false,
 
14820
        // lineColor: null, // overrides color, but lets fillColor be unaltered
 
14821
        // fillOpacity: 0.75,
 
14822
        // fillColor: null
 
14823
});
 
14824
 
 
14825
/**
 
14826
 * AreaSeries object
 
14827
 */
 
14828
var AreaSeries = extendClass(Series, {
 
14829
        type: 'area',
 
14830
        /**
 
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
 
14833
         * in the stack.
 
14834
         */ 
 
14835
        getSegments: function () {
 
14836
                var segments = [],
 
14837
                        segment = [],
 
14838
                        keys = [],
 
14839
                        xAxis = this.xAxis,
 
14840
                        yAxis = this.yAxis,
 
14841
                        stack = yAxis.stacks[this.stackKey],
 
14842
                        pointMap = {},
 
14843
                        plotX,
 
14844
                        plotY,
 
14845
                        points = this.points,
 
14846
                        connectNulls = this.options.connectNulls,
 
14847
                        val,
 
14848
                        i,
 
14849
                        x;
 
14850
 
 
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];
 
14855
                        }
 
14856
 
 
14857
                        // Sort the keys (#1651)
 
14858
                        for (x in stack) {
 
14859
                                if (stack[x].total !== null) { // nulled after switching between grouping and not (#1651, #2336)
 
14860
                                        keys.push(+x);
 
14861
                                }
 
14862
                        }
 
14863
                        keys.sort(function (a, b) {
 
14864
                                return a - b;
 
14865
                        });
 
14866
 
 
14867
                        each(keys, function (x) {
 
14868
                                if (connectNulls && (!pointMap[x] || pointMap[x].y === null)) { // #1836
 
14869
                                        return;
 
14870
 
 
14871
                                // The point exists, push it to the segment
 
14872
                                } else if (pointMap[x]) {
 
14873
                                        segment.push(pointMap[x]);
 
14874
 
 
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
 
14877
                                // correctly.
 
14878
                                } else {
 
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);
 
14882
                                        segment.push({ 
 
14883
                                                y: null, 
 
14884
                                                plotX: plotX,
 
14885
                                                clientX: plotX, 
 
14886
                                                plotY: plotY, 
 
14887
                                                yBottom: plotY,
 
14888
                                                onMouseOver: noop
 
14889
                                        });
 
14890
                                }
 
14891
                        });
 
14892
 
 
14893
                        if (segment.length) {
 
14894
                                segments.push(segment);
 
14895
                        }
 
14896
 
 
14897
                } else {
 
14898
                        Series.prototype.getSegments.call(this);
 
14899
                        segments = this.segments;
 
14900
                }
 
14901
 
 
14902
                this.segments = segments;
 
14903
        },
 
14904
        
 
14905
        /**
 
14906
         * Extend the base Series getSegmentPath method by adding the path for the area.
 
14907
         * This path is pushed to the series.areaPath property.
 
14908
         */
 
14909
        getSegmentPath: function (segment) {
 
14910
                
 
14911
                var segmentPath = Series.prototype.getSegmentPath.call(this, segment), // call base method
 
14912
                        areaSegmentPath = [].concat(segmentPath), // work on a copy for the area path
 
14913
                        i,
 
14914
                        options = this.options,
 
14915
                        segLength = segmentPath.length,
 
14916
                        translatedThreshold = this.yAxis.getThreshold(options.threshold), // #2181
 
14917
                        yBottom;
 
14918
                
 
14919
                if (segLength === 3) { // for animation from 1 to two points
 
14920
                        areaSegmentPath.push(L, segmentPath[1], segmentPath[2]);
 
14921
                }
 
14922
                if (options.stacking && !this.closedStacks) {
 
14923
                        
 
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--) {
 
14928
 
 
14929
                                yBottom = pick(segment[i].yBottom, translatedThreshold);
 
14930
                        
 
14931
                                // step line?
 
14932
                                if (i < segment.length - 1 && options.step) {
 
14933
                                        areaSegmentPath.push(segment[i + 1].plotX, yBottom);
 
14934
                                }
 
14935
                                
 
14936
                                areaSegmentPath.push(segment[i].plotX, yBottom);
 
14937
                        }
 
14938
 
 
14939
                } else { // follow zero line back
 
14940
                        this.closeSegment(areaSegmentPath, segment, translatedThreshold);
 
14941
                }
 
14942
                this.areaPath = this.areaPath.concat(areaSegmentPath);
 
14943
                return segmentPath;
 
14944
        },
 
14945
        
 
14946
        /**
 
14947
         * Extendable method to close the segment path of an area. This is overridden in polar 
 
14948
         * charts.
 
14949
         */
 
14950
        closeSegment: function (path, segment, translatedThreshold) {
 
14951
                path.push(
 
14952
                        L,
 
14953
                        segment[segment.length - 1].plotX,
 
14954
                        translatedThreshold,
 
14955
                        L,
 
14956
                        segment[0].plotX,
 
14957
                        translatedThreshold
 
14958
                );
 
14959
        },
 
14960
        
 
14961
        /**
 
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.
 
14965
         */
 
14966
        drawGraph: function () {
 
14967
                
 
14968
                // Define or reset areaPath
 
14969
                this.areaPath = [];
 
14970
                
 
14971
                // Call the base method
 
14972
                Series.prototype.drawGraph.apply(this);
 
14973
                
 
14974
                // Define local variables
 
14975
                var series = this,
 
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
 
14981
                
 
14982
                if (negativeColor || negativeFillColor) {
 
14983
                        props.push(['areaNeg', negativeColor, negativeFillColor]);
 
14984
                }
 
14985
                
 
14986
                each(props, function (prop) {
 
14987
                        var areaKey = prop[0],
 
14988
                                area = series[areaKey];
 
14989
                                
 
14990
                        // Create or update the area
 
14991
                        if (area) { // update
 
14992
                                area.animate({ d: areaPath });
 
14993
        
 
14994
                        } else { // create
 
14995
                                series[areaKey] = series.chart.renderer.path(areaPath)
 
14996
                                        .attr({
 
14997
                                                fill: pick(
 
14998
                                                        prop[2],
 
14999
                                                        Color(prop[1]).setOpacity(pick(options.fillOpacity, 0.75)).get()
 
15000
                                                ),
 
15001
                                                zIndex: 0 // #1069
 
15002
                                        }).add(series.group);
 
15003
                        }
 
15004
                });
 
15005
        },
 
15006
 
 
15007
        drawLegendSymbol: LegendSymbolMixin.drawRectangle
 
15008
});
 
15009
 
 
15010
seriesTypes.area = AreaSeries;
 
15011
/**
 
15012
 * Set the default options for spline
 
15013
 */
 
15014
defaultPlotOptions.spline = merge(defaultSeriesOptions);
 
15015
 
 
15016
/**
 
15017
 * SplineSeries object
 
15018
 */
 
15019
var SplineSeries = extendClass(Series, {
 
15020
        type: 'spline',
 
15021
 
 
15022
        /**
 
15023
         * Get the spline segment from a given point's previous neighbour to the given point
 
15024
         */
 
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],
 
15032
                        leftContX,
 
15033
                        leftContY,
 
15034
                        rightContX,
 
15035
                        rightContY,
 
15036
                        ret;
 
15037
 
 
15038
                // find control points
 
15039
                if (lastPoint && nextPoint) {
 
15040
                
 
15041
                        var lastX = lastPoint.plotX,
 
15042
                                lastY = lastPoint.plotY,
 
15043
                                nextX = nextPoint.plotX,
 
15044
                                nextY = nextPoint.plotY,
 
15045
                                correction;
 
15046
 
 
15047
                        leftContX = (smoothing * plotX + lastX) / denom;
 
15048
                        leftContY = (smoothing * plotY + lastY) / denom;
 
15049
                        rightContX = (smoothing * plotX + nextX) / denom;
 
15050
                        rightContY = (smoothing * plotY + nextY) / denom;
 
15051
 
 
15052
                        // have the two control points make a straight line through main point
 
15053
                        correction = ((rightContY - leftContY) * (rightContX - plotX)) /
 
15054
                                (rightContX - leftContX) + plotY - rightContY;
 
15055
 
 
15056
                        leftContY += correction;
 
15057
                        rightContY += correction;
 
15058
 
 
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;
 
15067
                        }
 
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;
 
15074
                        }
 
15075
 
 
15076
                        // record for drawing in next point
 
15077
                        point.rightContX = rightContX;
 
15078
                        point.rightContY = rightContY;
 
15079
 
 
15080
                }
 
15081
                
 
15082
                // Visualize control points for debugging
 
15083
                /*
 
15084
                if (leftContX) {
 
15085
                        this.chart.renderer.circle(leftContX + this.chart.plotLeft, leftContY + this.chart.plotTop, 2)
 
15086
                                .attr({
 
15087
                                        stroke: 'red',
 
15088
                                        'stroke-width': 1,
 
15089
                                        fill: 'none'
 
15090
                                })
 
15091
                                .add();
 
15092
                        this.chart.renderer.path(['M', leftContX + this.chart.plotLeft, leftContY + this.chart.plotTop,
 
15093
                                'L', plotX + this.chart.plotLeft, plotY + this.chart.plotTop])
 
15094
                                .attr({
 
15095
                                        stroke: 'red',
 
15096
                                        'stroke-width': 1
 
15097
                                })
 
15098
                                .add();
 
15099
                        this.chart.renderer.circle(rightContX + this.chart.plotLeft, rightContY + this.chart.plotTop, 2)
 
15100
                                .attr({
 
15101
                                        stroke: 'green',
 
15102
                                        'stroke-width': 1,
 
15103
                                        fill: 'none'
 
15104
                                })
 
15105
                                .add();
 
15106
                        this.chart.renderer.path(['M', rightContX + this.chart.plotLeft, rightContY + this.chart.plotTop,
 
15107
                                'L', plotX + this.chart.plotLeft, plotY + this.chart.plotTop])
 
15108
                                .attr({
 
15109
                                        stroke: 'green',
 
15110
                                        'stroke-width': 1
 
15111
                                })
 
15112
                                .add();
 
15113
                }
 
15114
                */
 
15115
 
 
15116
                // moveTo or lineTo
 
15117
                if (!i) {
 
15118
                        ret = [M, plotX, plotY];
 
15119
                } else { // curve from last point to this
 
15120
                        ret = [
 
15121
                                'C',
 
15122
                                lastPoint.rightContX || lastPoint.plotX,
 
15123
                                lastPoint.rightContY || lastPoint.plotY,
 
15124
                                leftContX || plotX,
 
15125
                                leftContY || plotY,
 
15126
                                plotX,
 
15127
                                plotY
 
15128
                        ];
 
15129
                        lastPoint.rightContX = lastPoint.rightContY = null; // reset for updating series later
 
15130
                }
 
15131
                return ret;
 
15132
        }
 
15133
});
 
15134
seriesTypes.spline = SplineSeries;
 
15135
 
 
15136
/**
 
15137
 * Set the default options for areaspline
 
15138
 */
 
15139
defaultPlotOptions.areaspline = merge(defaultPlotOptions.area);
 
15140
 
 
15141
/**
 
15142
 * AreaSplineSeries object
 
15143
 */
 
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
 
15148
                
 
15149
                // Mix in methods from the area series
 
15150
                getSegmentPath: areaProto.getSegmentPath,
 
15151
                closeSegment: areaProto.closeSegment,
 
15152
                drawGraph: areaProto.drawGraph,
 
15153
                drawLegendSymbol: LegendSymbolMixin.drawRectangle
 
15154
        });
 
15155
 
 
15156
seriesTypes.areaspline = AreaSplineSeries;
 
15157
 
 
15158
/**
 
15159
 * Set the default options for column
 
15160
 */
 
15161
defaultPlotOptions.column = merge(defaultSeriesOptions, {
 
15162
        borderColor: '#FFFFFF',
 
15163
        borderWidth: 1,
 
15164
        borderRadius: 0,
 
15165
        //colorByPoint: undefined,
 
15166
        groupPadding: 0.2,
 
15167
        //grouping: true,
 
15168
        marker: null, // point options are specified in the base options
 
15169
        pointPadding: 0.1,
 
15170
        //pointWidth: null,
 
15171
        minPointLength: 0,
 
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
 
15174
        states: {
 
15175
                hover: {
 
15176
                        brightness: 0.1,
 
15177
                        shadow: false
 
15178
                },
 
15179
                select: {
 
15180
                        color: '#C0C0C0',
 
15181
                        borderColor: '#000000',
 
15182
                        shadow: false
 
15183
                }
 
15184
        },
 
15185
        dataLabels: {
 
15186
                align: null, // auto
 
15187
                verticalAlign: null, // auto
 
15188
                y: null
 
15189
        },
 
15190
        stickyTracking: false,
 
15191
        threshold: 0
 
15192
});
 
15193
 
 
15194
/**
 
15195
 * ColumnSeries object
 
15196
 */
 
15197
var ColumnSeries = extendClass(Series, {
 
15198
        type: 'column',
 
15199
        pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
 
15200
                stroke: 'borderColor',
 
15201
                'stroke-width': 'borderWidth',
 
15202
                fill: 'color',
 
15203
                r: 'borderRadius'
 
15204
        },
 
15205
        cropShoulder: 0,
 
15206
        trackerGroups: ['group', 'dataLabelsGroup'],
 
15207
        negStacks: true, // use separate negative stacks, unlike area stacks where a negative 
 
15208
                // point is substracted from previous (#1910)
 
15209
        
 
15210
        /**
 
15211
         * Initialize the series
 
15212
         */
 
15213
        init: function () {
 
15214
                Series.prototype.init.apply(this, arguments);
 
15215
 
 
15216
                var series = this,
 
15217
                        chart = series.chart;
 
15218
 
 
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;
 
15225
                                }
 
15226
                        });
 
15227
                }
 
15228
        },
 
15229
 
 
15230
        /**
 
15231
         * Return the width and x offset of the columns adjusted for grouping, groupPadding, pointPadding,
 
15232
         * pointWidth etc. 
 
15233
         */
 
15234
        getColumnMetrics: function () {
 
15235
 
 
15236
                var series = this,
 
15237
                        options = series.options,
 
15238
                        xAxis = series.xAxis,
 
15239
                        yAxis = series.yAxis,
 
15240
                        reversedXAxis = xAxis.reversed,
 
15241
                        stackKey,
 
15242
                        stackGroups = {},
 
15243
                        columnIndex,
 
15244
                        columnCount = 0;
 
15245
 
 
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) {
 
15250
                        columnCount = 1;
 
15251
                } else {
 
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++;
 
15261
                                                }
 
15262
                                                columnIndex = stackGroups[stackKey];
 
15263
                                        } else if (otherOptions.grouping !== false) { // #1162
 
15264
                                                columnIndex = columnCount++;
 
15265
                                        }
 
15266
                                        otherSeries.columnIndex = columnIndex;
 
15267
                                }
 
15268
                        });
 
15269
                }
 
15270
 
 
15271
                var categoryWidth = mathMin(
 
15272
                                mathAbs(xAxis.transA) * (xAxis.ordinalSlope || options.pointRange || xAxis.closestPointRange || xAxis.tickInterval || 1), // #2610
 
15273
                                xAxis.len // #1535
 
15274
                        ),
 
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);
 
15288
 
 
15289
                // Save it for reading in linked series (Error bars particularly)
 
15290
                return (series.columnMetrics = { 
 
15291
                        width: pointWidth, 
 
15292
                        offset: pointXOffset 
 
15293
                });
 
15294
                        
 
15295
        },
 
15296
 
 
15297
        /**
 
15298
         * Translate each point to the plot area coordinate system and find shape positions
 
15299
         */
 
15300
        translate: function () {
 
15301
                var series = this,
 
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;
 
15315
 
 
15316
                if (chart.renderer.isVML && chart.inverted) {
 
15317
                        yCrisp += 1;
 
15318
                }
 
15319
 
 
15320
                Series.prototype.translate.apply(series);
 
15321
 
 
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,
 
15327
                                barW = seriesBarW,
 
15328
                                barY = mathMin(plotY, yBottom),
 
15329
                                right,
 
15330
                                bottom,
 
15331
                                fromTop,
 
15332
                                fromLeft,
 
15333
                                barH = mathMax(plotY, yBottom) - barY;
 
15334
 
 
15335
                        // Handle options.minPointLength
 
15336
                        if (mathAbs(barH) < minPointLength) {
 
15337
                                if (minPointLength) {
 
15338
                                        barH = minPointLength;
 
15339
                                        barY =
 
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)
 
15343
                                }
 
15344
                        }
 
15345
 
 
15346
                        // Cache for access in polar
 
15347
                        point.barX = barX;
 
15348
                        point.pointWidth = pointWidth;
 
15349
 
 
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;
 
15355
 
 
15356
                        fromTop = mathAbs(barY) < 0.5;
 
15357
                        bottom = mathRound(barY + barH) + yCrisp;
 
15358
                        barY = mathRound(barY) + yCrisp;
 
15359
                        barH = bottom - barY;
 
15360
 
 
15361
                        // Top and left edges are exceptions
 
15362
                        if (fromLeft) {
 
15363
                                barX += 1;
 
15364
                                barW -= 1;
 
15365
                        }
 
15366
                        if (fromTop) {
 
15367
                                barY -= 1;
 
15368
                                barH += 1;
 
15369
                        }
 
15370
 
 
15371
                        // Register shape type and arguments to be used in drawPoints
 
15372
                        point.shapeType = 'rect';
 
15373
                        point.shapeArgs = {
 
15374
                                x: barX,
 
15375
                                y: barY,
 
15376
                                width: barW,
 
15377
                                height: barH
 
15378
                        };
 
15379
                });
 
15380
 
 
15381
        },
 
15382
 
 
15383
        getSymbol: noop,
 
15384
        
 
15385
        /**
 
15386
         * Use a solid rectangle like the area series types
 
15387
         */
 
15388
        drawLegendSymbol: LegendSymbolMixin.drawRectangle,
 
15389
        
 
15390
        
 
15391
        /**
 
15392
         * Columns have no graph
 
15393
         */
 
15394
        drawGraph: noop,
 
15395
 
 
15396
        /**
 
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.
 
15399
         *
 
15400
         */
 
15401
        drawPoints: function () {
 
15402
                var series = this,
 
15403
                        chart = this.chart,
 
15404
                        options = series.options,
 
15405
                        renderer = chart.renderer,
 
15406
                        animationLimit = options.animationLimit || 250,
 
15407
                        shapeArgs;
 
15408
 
 
15409
                // draw the columns
 
15410
                each(series.points, function (point) {
 
15411
                        var plotY = point.plotY,
 
15412
                                graphic = point.graphic;
 
15413
 
 
15414
                        if (plotY !== UNDEFINED && !isNaN(plotY) && point.y !== null) {
 
15415
                                shapeArgs = point.shapeArgs;
 
15416
                                
 
15417
                                if (graphic) { // update
 
15418
                                        stop(graphic);
 
15419
                                        graphic[series.points.length < animationLimit ? 'animate' : 'attr'](merge(shapeArgs));
 
15420
 
 
15421
                                } else {
 
15422
                                        point.graphic = graphic = renderer[point.shapeType](shapeArgs)
 
15423
                                                .attr(point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE])
 
15424
                                                .add(series.group)
 
15425
                                                .shadow(options.shadow, null, options.stacking && !options.borderRadius);
 
15426
                                }
 
15427
 
 
15428
                        } else if (graphic) {
 
15429
                                point.graphic = graphic.destroy(); // #1269
 
15430
                        }
 
15431
                });
 
15432
        },
 
15433
 
 
15434
        /**
 
15435
         * Animate the column heights one by one from zero
 
15436
         * @param {Boolean} init Whether to initialize the animation or run it
 
15437
         */
 
15438
        animate: function (init) {
 
15439
                var series = this,
 
15440
                        yAxis = this.yAxis,
 
15441
                        options = series.options,
 
15442
                        inverted = this.chart.inverted,
 
15443
                        attr = {},
 
15444
                        translatedThreshold;
 
15445
 
 
15446
                if (hasSVG) { // VML is too slow anyway
 
15447
                        if (init) {
 
15448
                                attr.scaleY = 0.001;
 
15449
                                translatedThreshold = mathMin(yAxis.pos + yAxis.len, mathMax(yAxis.pos, yAxis.toPixels(options.threshold)));
 
15450
                                if (inverted) {
 
15451
                                        attr.translateX = translatedThreshold - yAxis.len;
 
15452
                                } else {
 
15453
                                        attr.translateY = translatedThreshold;
 
15454
                                }
 
15455
                                series.group.attr(attr);
 
15456
 
 
15457
                        } else { // run the animation
 
15458
                                
 
15459
                                attr.scaleY = 1;
 
15460
                                attr[inverted ? 'translateX' : 'translateY'] = yAxis.pos;
 
15461
                                series.group.animate(attr, series.options.animation);
 
15462
 
 
15463
                                // delete this function to allow it only once
 
15464
                                series.animate = null;
 
15465
                        }
 
15466
                }
 
15467
        },
 
15468
        
 
15469
        /**
 
15470
         * Remove this series from the chart
 
15471
         */
 
15472
        remove: function () {
 
15473
                var series = this,
 
15474
                        chart = series.chart;
 
15475
 
 
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;
 
15482
                                }
 
15483
                        });
 
15484
                }
 
15485
 
 
15486
                Series.prototype.remove.apply(series, arguments);
 
15487
        }
 
15488
});
 
15489
seriesTypes.column = ColumnSeries;
 
15490
/**
 
15491
 * Set the default options for bar
 
15492
 */
 
15493
defaultPlotOptions.bar = merge(defaultPlotOptions.column);
 
15494
/**
 
15495
 * The Bar series class
 
15496
 */
 
15497
var BarSeries = extendClass(ColumnSeries, {
 
15498
        type: 'bar',
 
15499
        inverted: true
 
15500
});
 
15501
seriesTypes.bar = BarSeries;
 
15502
 
 
15503
/**
 
15504
 * Set the default options for scatter
 
15505
 */
 
15506
defaultPlotOptions.scatter = merge(defaultSeriesOptions, {
 
15507
        lineWidth: 0,
 
15508
        tooltip: {
 
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
 
15512
        },
 
15513
        stickyTracking: false
 
15514
});
 
15515
 
 
15516
/**
 
15517
 * The scatter series class
 
15518
 */
 
15519
var ScatterSeries = extendClass(Series, {
 
15520
        type: 'scatter',
 
15521
        sorted: false,
 
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);
 
15530
                }
 
15531
        }
 
15532
});
 
15533
 
 
15534
seriesTypes.scatter = ScatterSeries;
 
15535
 
 
15536
/**
 
15537
 * Set the default options for pie
 
15538
 */
 
15539
defaultPlotOptions.pie = merge(defaultSeriesOptions, {
 
15540
        borderColor: '#FFFFFF',
 
15541
        borderWidth: 1,
 
15542
        center: [null, null],
 
15543
        clip: false,
 
15544
        colorByPoint: true, // always true for pies
 
15545
        dataLabels: {
 
15546
                // align: null,
 
15547
                // connectorWidth: 1,
 
15548
                // connectorColor: point.color,
 
15549
                // connectorPadding: 5,
 
15550
                distance: 30,
 
15551
                enabled: true,
 
15552
                formatter: function () {
 
15553
                        return this.point.name;
 
15554
                }
 
15555
                // softConnector: true,
 
15556
                //y: 0
 
15557
        },
 
15558
        ignoreHiddenPoint: true,
 
15559
        //innerSize: 0,
 
15560
        legendType: 'point',
 
15561
        marker: null, // point options are specified in the base options
 
15562
        size: null,
 
15563
        showInLegend: false,
 
15564
        slicedOffset: 10,
 
15565
        states: {
 
15566
                hover: {
 
15567
                        brightness: 0.1,
 
15568
                        shadow: false
 
15569
                }
 
15570
        },
 
15571
        stickyTracking: false,
 
15572
        tooltip: {
 
15573
                followPointer: true
 
15574
        }
 
15575
});
 
15576
 
 
15577
/**
 
15578
 * Extended point object for pies
 
15579
 */
 
15580
var PiePoint = extendClass(Point, {
 
15581
        /**
 
15582
         * Initiate the pie slice
 
15583
         */
 
15584
        init: function () {
 
15585
 
 
15586
                Point.prototype.init.apply(this, arguments);
 
15587
 
 
15588
                var point = this,
 
15589
                        toggleSlice;
 
15590
 
 
15591
                // Disallow negative values (#1530)
 
15592
                if (point.y < 0) {
 
15593
                        point.y = null;
 
15594
                }
 
15595
 
 
15596
                //visible: options.visible !== false,
 
15597
                extend(point, {
 
15598
                        visible: point.visible !== false,
 
15599
                        name: pick(point.name, 'Slice')
 
15600
                });
 
15601
 
 
15602
                // add event listener for select
 
15603
                toggleSlice = function (e) {
 
15604
                        point.slice(e.type === 'select');
 
15605
                };
 
15606
                addEvent(point, 'select', toggleSlice);
 
15607
                addEvent(point, 'unselect', toggleSlice);
 
15608
 
 
15609
                return point;
 
15610
        },
 
15611
 
 
15612
        /**
 
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
 
15616
         */
 
15617
        setVisible: function (vis) {
 
15618
                var point = this,
 
15619
                        series = point.series,
 
15620
                        chart = series.chart;
 
15621
 
 
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
 
15625
 
 
15626
                // Show and hide associated elements
 
15627
                each(['graphic', 'dataLabel', 'connector', 'shadowGroup'], function (key) {
 
15628
                        if (point[key]) {
 
15629
                                point[key][vis ? 'show' : 'hide'](true);
 
15630
                        }
 
15631
                });
 
15632
 
 
15633
                if (point.legendItem) {
 
15634
                        chart.legend.colorizeItem(point, vis);
 
15635
                }
 
15636
                
 
15637
                // Handle ignore hidden slices
 
15638
                if (!series.isDirty && series.options.ignoreHiddenPoint) {
 
15639
                        series.isDirty = true;
 
15640
                        chart.redraw();
 
15641
                }
 
15642
        },
 
15643
 
 
15644
        /**
 
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.
 
15648
         */
 
15649
        slice: function (sliced, redraw, animation) {
 
15650
                var point = this,
 
15651
                        series = point.series,
 
15652
                        chart = series.chart,
 
15653
                        translation;
 
15654
 
 
15655
                setAnimation(animation, chart);
 
15656
 
 
15657
                // redraw is true by default
 
15658
                redraw = pick(redraw, true);
 
15659
 
 
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
 
15663
 
 
15664
                translation = sliced ? point.slicedTranslation : {
 
15665
                        translateX: 0,
 
15666
                        translateY: 0
 
15667
                };
 
15668
 
 
15669
                point.graphic.animate(translation);
 
15670
                
 
15671
                if (point.shadowGroup) {
 
15672
                        point.shadowGroup.animate(translation);
 
15673
                }
 
15674
 
 
15675
        }
 
15676
});
 
15677
 
 
15678
/**
 
15679
 * The Pie series class
 
15680
 */
 
15681
var PieSeries = {
 
15682
        type: 'pie',
 
15683
        isCartesian: false,
 
15684
        pointClass: PiePoint,
 
15685
        requireSorting: false,
 
15686
        noSharedTooltip: true,
 
15687
        trackerGroups: ['group', 'dataLabelsGroup'],
 
15688
        axisTypes: [],
 
15689
        pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
 
15690
                stroke: 'borderColor',
 
15691
                'stroke-width': 'borderWidth',
 
15692
                fill: 'color'
 
15693
        },
 
15694
        singularTooltips: true,
 
15695
 
 
15696
        /**
 
15697
         * Pies have one color each point
 
15698
         */
 
15699
        getColor: noop,
 
15700
 
 
15701
        /**
 
15702
         * Animate the pies in
 
15703
         */
 
15704
        animate: function (init) {
 
15705
                var series = this,
 
15706
                        points = series.points,
 
15707
                        startAngleRad = series.startAngleRad;
 
15708
 
 
15709
                if (!init) {
 
15710
                        each(points, function (point) {
 
15711
                                var graphic = point.graphic,
 
15712
                                        args = point.shapeArgs;
 
15713
 
 
15714
                                if (graphic) {
 
15715
                                        // start values
 
15716
                                        graphic.attr({
 
15717
                                                r: series.center[3] / 2, // animate from inner radius (#779)
 
15718
                                                start: startAngleRad,
 
15719
                                                end: startAngleRad
 
15720
                                        });
 
15721
 
 
15722
                                        // animate
 
15723
                                        graphic.animate({
 
15724
                                                r: args.r,
 
15725
                                                start: args.start,
 
15726
                                                end: args.end
 
15727
                                        }, series.options.animation);
 
15728
                                }
 
15729
                        });
 
15730
 
 
15731
                        // delete this function to allow it only once
 
15732
                        series.animate = null;
 
15733
                }
 
15734
        },
 
15735
 
 
15736
        /**
 
15737
         * Extend the basic setData method by running processData and generatePoints immediately,
 
15738
         * in order to access the points from the legend.
 
15739
         */
 
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);
 
15746
                } 
 
15747
        },
 
15748
 
 
15749
        /**
 
15750
         * Extend the generatePoints method by adding total and percentage properties to each point
 
15751
         */
 
15752
        generatePoints: function () {
 
15753
                var i,
 
15754
                        total = 0,
 
15755
                        points,
 
15756
                        len,
 
15757
                        point,
 
15758
                        ignoreHiddenPoint = this.options.ignoreHiddenPoint;
 
15759
 
 
15760
                Series.prototype.generatePoints.call(this);
 
15761
 
 
15762
                // Populate local vars
 
15763
                points = this.points;
 
15764
                len = points.length;
 
15765
                
 
15766
                // Get the total sum
 
15767
                for (i = 0; i < len; i++) {
 
15768
                        point = points[i];
 
15769
                        total += (ignoreHiddenPoint && !point.visible) ? 0 : point.y;
 
15770
                }
 
15771
                this.total = total;
 
15772
 
 
15773
                // Set each point's properties
 
15774
                for (i = 0; i < len; i++) {
 
15775
                        point = points[i];
 
15776
                        point.percentage = total > 0 ? (point.y / total) * 100 : 0;
 
15777
                        point.total = total;
 
15778
                }
 
15779
                
 
15780
        },
 
15781
        
 
15782
        /**
 
15783
         * Do translation for pie slices
 
15784
         */
 
15785
        translate: function (positions) {
 
15786
                this.generatePoints();
 
15787
                
 
15788
                var series = this,
 
15789
                        cumulative = 0,
 
15790
                        precision = 1000, // issue #172
 
15791
                        options = series.options,
 
15792
                        slicedOffset = options.slicedOffset,
 
15793
                        connectorOffset = slicedOffset + options.borderWidth,
 
15794
                        start,
 
15795
                        end,
 
15796
                        angle,
 
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
 
15803
                        radiusY,
 
15804
                        labelDistance = options.dataLabels.distance,
 
15805
                        ignoreHiddenPoint = options.ignoreHiddenPoint,
 
15806
                        i,
 
15807
                        len = points.length,
 
15808
                        point;
 
15809
 
 
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.
 
15813
                if (!positions) {
 
15814
                        series.center = positions = series.getCenter();
 
15815
                }
 
15816
 
 
15817
                // utility for getting the x value from a given y, used for anticollision logic in data labels
 
15818
                series.getX = function (y, left) {
 
15819
 
 
15820
                        angle = math.asin(mathMin((y - positions[1]) / (positions[2] / 2 + labelDistance), 1));
 
15821
 
 
15822
                        return positions[0] +
 
15823
                                (left ? -1 : 1) *
 
15824
                                (mathCos(angle) * (positions[2] / 2 + labelDistance));
 
15825
                };
 
15826
 
 
15827
                // Calculate the geometry for each point
 
15828
                for (i = 0; i < len; i++) {
 
15829
                        
 
15830
                        point = points[i];
 
15831
                        
 
15832
                        // set start and end angle
 
15833
                        start = startAngleRad + (cumulative * circ);
 
15834
                        if (!ignoreHiddenPoint || point.visible) {
 
15835
                                cumulative += point.percentage / 100;
 
15836
                        }
 
15837
                        end = startAngleRad + (cumulative * circ);
 
15838
 
 
15839
                        // set the shape
 
15840
                        point.shapeType = 'arc';
 
15841
                        point.shapeArgs = {
 
15842
                                x: positions[0],
 
15843
                                y: positions[1],
 
15844
                                r: positions[2] / 2,
 
15845
                                innerR: positions[3] / 2,
 
15846
                                start: mathRound(start * precision) / precision,
 
15847
                                end: mathRound(end * precision) / precision
 
15848
                        };
 
15849
 
 
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;
 
15856
                        }
 
15857
 
 
15858
                        // Center for the sliced out slice
 
15859
                        point.slicedTranslation = {
 
15860
                                translateX: mathRound(mathCos(angle) * slicedOffset),
 
15861
                                translateY: mathRound(mathSin(angle) * slicedOffset)
 
15862
                        };
 
15863
 
 
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
 
15870
                        ];
 
15871
                        
 
15872
                        point.half = angle < -mathPI / 2 || angle > mathPI / 2 ? 1 : 0;
 
15873
                        point.angle = angle;
 
15874
 
 
15875
                        // set the anchor point for data labels
 
15876
                        connectorOffset = mathMin(connectorOffset, labelDistance / 2); // #1678
 
15877
                        point.labelPos = [
 
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
 
15885
                                        'center' :
 
15886
                                        point.half ? 'right' : 'left', // alignment
 
15887
                                angle // center angle
 
15888
                        ];
 
15889
 
 
15890
                }
 
15891
        },
 
15892
        
 
15893
        drawGraph: null,
 
15894
 
 
15895
        /**
 
15896
         * Draw the data points
 
15897
         */
 
15898
        drawPoints: function () {
 
15899
                var series = this,
 
15900
                        chart = series.chart,
 
15901
                        renderer = chart.renderer,
 
15902
                        groupTranslation,
 
15903
                        //center,
 
15904
                        graphic,
 
15905
                        //group,
 
15906
                        shadow = series.options.shadow,
 
15907
                        shadowGroup,
 
15908
                        shapeArgs;
 
15909
 
 
15910
                if (shadow && !series.shadowGroup) {
 
15911
                        series.shadowGroup = renderer.g('shadow')
 
15912
                                .add(series.group);
 
15913
                }
 
15914
 
 
15915
                // draw the slices
 
15916
                each(series.points, function (point) {
 
15917
                        graphic = point.graphic;
 
15918
                        shapeArgs = point.shapeArgs;
 
15919
                        shadowGroup = point.shadowGroup;
 
15920
 
 
15921
                        // put the shadow behind all points
 
15922
                        if (shadow && !shadowGroup) {
 
15923
                                shadowGroup = point.shadowGroup = renderer.g('shadow')
 
15924
                                        .add(series.shadowGroup);
 
15925
                        }
 
15926
 
 
15927
                        // if the point is sliced, use special translation, else use plot area traslation
 
15928
                        groupTranslation = point.sliced ? point.slicedTranslation : {
 
15929
                                translateX: 0,
 
15930
                                translateY: 0
 
15931
                        };
 
15932
 
 
15933
                        //group.translate(groupTranslation[0], groupTranslation[1]);
 
15934
                        if (shadowGroup) {
 
15935
                                shadowGroup.attr(groupTranslation);
 
15936
                        }
 
15937
 
 
15938
                        // draw the slice
 
15939
                        if (graphic) {
 
15940
                                graphic.animate(extend(shapeArgs, groupTranslation));
 
15941
                        } else {
 
15942
                                point.graphic = graphic = renderer[point.shapeType](shapeArgs)
 
15943
                                        .setRadialReference(series.center)
 
15944
                                        .attr(
 
15945
                                                point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE]
 
15946
                                        )
 
15947
                                        .attr({ 
 
15948
                                                'stroke-linejoin': 'round'
 
15949
                                                //zIndex: 1 // #2722 (reversed)
 
15950
                                        })
 
15951
                                        .attr(groupTranslation)
 
15952
                                        .add(series.group)
 
15953
                                        .shadow(shadow, shadowGroup);   
 
15954
                        }
 
15955
 
 
15956
                        // detect point specific visibility (#2430)
 
15957
                        if (point.visible !== undefined) {
 
15958
                                point.setVisible(point.visible);
 
15959
                        }
 
15960
 
 
15961
                });
 
15962
 
 
15963
        },
 
15964
 
 
15965
        /**
 
15966
         * Utility for sorting data labels
 
15967
         */
 
15968
        sortByAngle: function (points, sign) {
 
15969
                points.sort(function (a, b) {
 
15970
                        return a.angle !== undefined && (b.angle - a.angle) * sign;
 
15971
                });
 
15972
        },              
 
15973
 
 
15974
        /**
 
15975
         * Use a simple symbol from LegendSymbolMixin
 
15976
         */
 
15977
        drawLegendSymbol: LegendSymbolMixin.drawRectangle,
 
15978
 
 
15979
        /**
 
15980
         * Use the getCenter method from drawLegendSymbol
 
15981
         */
 
15982
        getCenter: CenteredSeriesMixin.getCenter,
 
15983
 
 
15984
        /**
 
15985
         * Pies don't have point marker symbols
 
15986
         */
 
15987
        getSymbol: noop
 
15988
 
 
15989
};
 
15990
PieSeries = extendClass(Series, PieSeries);
 
15991
seriesTypes.pie = PieSeries;
 
15992
 
 
15993
/**
 
15994
 * Draw the data labels
 
15995
 */
 
15996
Series.prototype.drawDataLabels = function () {
 
15997
 
 
15998
        var series = this,
 
15999
                seriesOptions = series.options,
 
16000
                cursor = seriesOptions.cursor,
 
16001
                options = seriesOptions.dataLabels,
 
16002
                points = series.points,
 
16003
                pointOptions,
 
16004
                generalOptions,
 
16005
                str,
 
16006
                dataLabelsGroup;
 
16007
 
 
16008
        if (options.enabled || series._hasPointLabels) {
 
16009
 
 
16010
                // Process default alignment of data labels for columns
 
16011
                if (series.dlProcessOptions) {
 
16012
                        series.dlProcessOptions(options);
 
16013
                }
 
16014
 
 
16015
                // Create a separate group for the data labels to avoid rotation
 
16016
                dataLabelsGroup = series.plotGroup(
 
16017
                        'dataLabelsGroup',
 
16018
                        'data-labels',
 
16019
                        series.visible ? VISIBLE : HIDDEN,
 
16020
                        options.zIndex || 6
 
16021
                );
 
16022
 
 
16023
                // Make the labels for each point
 
16024
                generalOptions = options;
 
16025
                each(points, function (point) {
 
16026
 
 
16027
                        var enabled,
 
16028
                                dataLabel = point.dataLabel,
 
16029
                                labelConfig,
 
16030
                                attr,
 
16031
                                name,
 
16032
                                rotation,
 
16033
                                connector = point.connector,
 
16034
                                isNew = true;
 
16035
 
 
16036
                        // Determine if each data label is enabled
 
16037
                        pointOptions = point.options && point.options.dataLabels;
 
16038
                        enabled = pick(pointOptions && pointOptions.enabled, generalOptions.enabled); // #2282
 
16039
 
 
16040
 
 
16041
                        // If the point is outside the plot area, destroy it. #678, #820
 
16042
                        if (dataLabel && !enabled) {
 
16043
                                point.dataLabel = dataLabel.destroy();
 
16044
 
 
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) {
 
16048
 
 
16049
                                // Create individual options structure that can be extended without
 
16050
                                // affecting others
 
16051
                                options = merge(generalOptions, pointOptions);
 
16052
 
 
16053
                                rotation = options.rotation;
 
16054
 
 
16055
                                // Get the string
 
16056
                                labelConfig = point.getLabelConfig();
 
16057
                                str = options.format ?
 
16058
                                        format(options.format, labelConfig) :
 
16059
                                        options.formatter.call(labelConfig, options);
 
16060
 
 
16061
                                // Determine the color
 
16062
                                options.style.color = pick(options.color, options.style.color, series.color, 'black');
 
16063
 
 
16064
 
 
16065
                                // update existing label
 
16066
                                if (dataLabel) {
 
16067
 
 
16068
                                        if (defined(str)) {
 
16069
                                                dataLabel
 
16070
                                                        .attr({
 
16071
                                                                text: str
 
16072
                                                        });
 
16073
                                                isNew = false;
 
16074
 
 
16075
                                        } else { // #1437 - the label is shown conditionally
 
16076
                                                point.dataLabel = dataLabel = dataLabel.destroy();
 
16077
                                                if (connector) {
 
16078
                                                        point.connector = connector.destroy();
 
16079
                                                }
 
16080
                                        }
 
16081
 
 
16082
                                // create new label
 
16083
                                } else if (defined(str)) {
 
16084
                                        attr = {
 
16085
                                                //align: align,
 
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,
 
16092
                                                zIndex: 1
 
16093
                                        };
 
16094
                                        // Remove unused attributes (#947)
 
16095
                                        for (name in attr) {
 
16096
                                                if (attr[name] === UNDEFINED) {
 
16097
                                                        delete attr[name];
 
16098
                                                }
 
16099
                                        }
 
16100
 
 
16101
                                        dataLabel = point.dataLabel = series.chart.renderer[rotation ? 'text' : 'label']( // labels don't support rotation
 
16102
                                                str,
 
16103
                                                0,
 
16104
                                                -999,
 
16105
                                                null,
 
16106
                                                null,
 
16107
                                                null,
 
16108
                                                options.useHTML
 
16109
                                        )
 
16110
                                        .attr(attr)
 
16111
                                        .css(extend(options.style, cursor && { cursor: cursor }))
 
16112
                                        .add(dataLabelsGroup)
 
16113
                                        .shadow(options.shadow);
 
16114
 
 
16115
                                }
 
16116
 
 
16117
                                if (dataLabel) {
 
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);
 
16120
                                }
 
16121
                        }
 
16122
                });
 
16123
        }
 
16124
};
 
16125
 
 
16126
/**
 
16127
 * Align each individual data label
 
16128
 */
 
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;
 
16139
 
 
16140
        if (visible) {
 
16141
 
 
16142
                // The alignment box is a singular point
 
16143
                alignTo = extend({
 
16144
                        x: inverted ? chart.plotWidth - plotY : plotX,
 
16145
                        y: mathRound(inverted ? chart.plotHeight - plotX : plotY),
 
16146
                        width: 0,
 
16147
                        height: 0
 
16148
                }, alignTo);
 
16149
 
 
16150
                // Add the text size for alignment calculation
 
16151
                extend(options, {
 
16152
                        width: bBox.width,
 
16153
                        height: bBox.height
 
16154
                });
 
16155
 
 
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
 
16158
                        alignAttr = {
 
16159
                                align: options.align,
 
16160
                                x: alignTo.x + options.x + alignTo.width / 2,
 
16161
                                y: alignTo.y + options.y + alignTo.height / 2
 
16162
                        };
 
16163
                        dataLabel[isNew ? 'attr' : 'animate'](alignAttr);
 
16164
                } else {
 
16165
                        dataLabel.align(options, null, alignTo);
 
16166
                        alignAttr = dataLabel.alignAttr;
 
16167
 
 
16168
                        // Handle justify or crop
 
16169
                        if (pick(options.overflow, 'justify') === 'justify') {
 
16170
                                this.justifyDataLabel(dataLabel, options, alignAttr, bBox, alignTo, isNew);
 
16171
 
 
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);
 
16175
 
 
16176
                        }
 
16177
                }
 
16178
        }
 
16179
 
 
16180
        // Show or hide based on the final aligned position
 
16181
        if (!visible) {
 
16182
                dataLabel.attr({ y: -999 });
 
16183
                dataLabel.placed = false; // don't animate back in
 
16184
        }
 
16185
 
 
16186
};
 
16187
 
 
16188
/**
 
16189
 * If data labels fall partly outside the plot area, align them back in, in a way that
 
16190
 * doesn't hide the point.
 
16191
 */
 
16192
Series.prototype.justifyDataLabel = function (dataLabel, options, alignAttr, bBox, alignTo, isNew) {
 
16193
        var chart = this.chart,
 
16194
                align = options.align,
 
16195
                verticalAlign = options.verticalAlign,
 
16196
                off,
 
16197
                justified;
 
16198
 
 
16199
        // Off left
 
16200
        off = alignAttr.x;
 
16201
        if (off < 0) {
 
16202
                if (align === 'right') {
 
16203
                        options.align = 'left';
 
16204
                } else {
 
16205
                        options.x = -off;
 
16206
                }
 
16207
                justified = true;
 
16208
        }
 
16209
 
 
16210
        // Off right
 
16211
        off = alignAttr.x + bBox.width;
 
16212
        if (off > chart.plotWidth) {
 
16213
                if (align === 'left') {
 
16214
                        options.align = 'right';
 
16215
                } else {
 
16216
                        options.x = chart.plotWidth - off;
 
16217
                }
 
16218
                justified = true;
 
16219
        }
 
16220
 
 
16221
        // Off top
 
16222
        off = alignAttr.y;
 
16223
        if (off < 0) {
 
16224
                if (verticalAlign === 'bottom') {
 
16225
                        options.verticalAlign = 'top';
 
16226
                } else {
 
16227
                        options.y = -off;
 
16228
                }
 
16229
                justified = true;
 
16230
        }
 
16231
 
 
16232
        // Off bottom
 
16233
        off = alignAttr.y + bBox.height;
 
16234
        if (off > chart.plotHeight) {
 
16235
                if (verticalAlign === 'top') {
 
16236
                        options.verticalAlign = 'bottom';
 
16237
                } else {
 
16238
                        options.y = chart.plotHeight - off;
 
16239
                }
 
16240
                justified = true;
 
16241
        }
 
16242
 
 
16243
        if (justified) {
 
16244
                dataLabel.placed = !isNew;
 
16245
                dataLabel.align(options, null, alignTo);
 
16246
        }
 
16247
};
 
16248
 
 
16249
/**
 
16250
 * Override the base drawDataLabels method by pie specific functionality
 
16251
 */
 
16252
if (seriesTypes.pie) {
 
16253
        seriesTypes.pie.prototype.drawDataLabels = function () {
 
16254
                var series = this,
 
16255
                        data = series.data,
 
16256
                        point,
 
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,
 
16263
                        connector,
 
16264
                        connectorPath,
 
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,
 
16271
                        dataLabel,
 
16272
                        dataLabelWidth,
 
16273
                        labelPos,
 
16274
                        labelHeight,
 
16275
                        halves = [// divide the points into right and left halves for anti collision
 
16276
                                [], // right
 
16277
                                []  // left
 
16278
                        ],
 
16279
                        x,
 
16280
                        y,
 
16281
                        visibility,
 
16282
                        rankArr,
 
16283
                        i,
 
16284
                        j,
 
16285
                        overflow = [0, 0, 0, 0], // top, right, bottom, left
 
16286
                        sort = function (a, b) {
 
16287
                                return b.y - a.y;
 
16288
                        };
 
16289
 
 
16290
                // get out if not enabled
 
16291
                if (!series.visible || (!options.enabled && !series._hasPointLabels)) {
 
16292
                        return;
 
16293
                }
 
16294
 
 
16295
                // run parent method
 
16296
                Series.prototype.drawDataLabels.apply(series);
 
16297
 
 
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);
 
16302
                        }
 
16303
                });
 
16304
 
 
16305
                // assume equal label heights
 
16306
                i = 0;
 
16307
                while (!labelHeight && data[i]) { // #1569
 
16308
                        labelHeight = data[i] && data[i].dataLabel && (data[i].dataLabel.getBBox().height || 21); // 21 is for #968
 
16309
                        i++;
 
16310
                }
 
16311
 
 
16312
                /* Loop over the points in each half, starting from the top and bottom
 
16313
                 * of the pie to detect overlapping labels.
 
16314
                 */
 
16315
                i = 2;
 
16316
                while (i--) {
 
16317
 
 
16318
                        var slots = [],
 
16319
                                slotsLength,
 
16320
                                usedSlots = [],
 
16321
                                points = halves[i],
 
16322
                                pos,
 
16323
                                length = points.length,
 
16324
                                slotIndex;
 
16325
 
 
16326
                        // Sort by angle
 
16327
                        series.sortByAngle(points, i - 0.5);
 
16328
 
 
16329
                        // Only do anti-collision when we are outside the pie and have connectors (#856)
 
16330
                        if (distanceOption > 0) {
 
16331
 
 
16332
                                // build the slots
 
16333
                                for (pos = centerY - radius - distanceOption; pos <= centerY + radius + distanceOption; pos += labelHeight) {
 
16334
                                        slots.push(pos);
 
16335
 
 
16336
                                        // visualize the slot
 
16337
                                        /*
 
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)
 
16342
                                                        .attr({
 
16343
                                                                'stroke-width': 1,
 
16344
                                                                stroke: 'silver'
 
16345
                                                        })
 
16346
                                                        .add();
 
16347
                                                chart.renderer.text('Slot '+ (slots.length - 1), slotX, slotY + 4)
 
16348
                                                        .attr({
 
16349
                                                                fill: 'silver'
 
16350
                                                        }).add();
 
16351
                                        }
 
16352
                                        */
 
16353
                                }
 
16354
                                slotsLength = slots.length;
 
16355
 
 
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);
 
16361
                                        j = length;
 
16362
                                        while (j--) {
 
16363
                                                rankArr[j].rank = j;
 
16364
                                        }
 
16365
                                        j = length;
 
16366
                                        while (j--) {
 
16367
                                                if (points[j].rank >= slotsLength) {
 
16368
                                                        points.splice(j, 1);
 
16369
                                                }
 
16370
                                        }
 
16371
                                        length = points.length;
 
16372
                                }
 
16373
 
 
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++) {
 
16377
 
 
16378
                                        point = points[j];
 
16379
                                        labelPos = point.labelPos;
 
16380
 
 
16381
                                        var closest = 9999,
 
16382
                                                distance,
 
16383
                                                slotI;
 
16384
 
 
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;
 
16390
                                                        slotIndex = slotI;
 
16391
                                                }
 
16392
                                        }
 
16393
 
 
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
 
16397
                                                slotIndex = j;
 
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
 
16401
                                                        slotIndex++;
 
16402
                                                }
 
16403
                                        } else {
 
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
 
16407
                                                        slotIndex++;
 
16408
                                                }
 
16409
                                        }
 
16410
 
 
16411
                                        usedSlots.push({ i: slotIndex, y: slots[slotIndex] });
 
16412
                                        slots[slotIndex] = null; // mark as taken
 
16413
                                }
 
16414
                                // sort them in order to fill in from the top
 
16415
                                usedSlots.sort(sort);
 
16416
                        }
 
16417
 
 
16418
                        // now the used slots are sorted, fill them up sequentially
 
16419
                        for (j = 0; j < length; j++) {
 
16420
 
 
16421
                                var slot, naturalY;
 
16422
 
 
16423
                                point = points[j];
 
16424
                                labelPos = point.labelPos;
 
16425
                                dataLabel = point.dataLabel;
 
16426
                                visibility = point.visible === false ? HIDDEN : VISIBLE;
 
16427
                                naturalY = labelPos[1];
 
16428
 
 
16429
                                if (distanceOption > 0) {
 
16430
                                        slot = usedSlots.pop();
 
16431
                                        slotIndex = slot.i;
 
16432
 
 
16433
                                        // if the slot next to currrent slot is free, the y value is allowed
 
16434
                                        // to fall back to the natural position
 
16435
                                        y = slot.y;
 
16436
                                        if ((naturalY > y && slots[slotIndex + 1] !== null) ||
 
16437
                                                        (naturalY < y &&  slots[slotIndex - 1] !== null)) {
 
16438
                                                y = naturalY;
 
16439
                                        }
 
16440
 
 
16441
                                } else {
 
16442
                                        y = naturalY;
 
16443
                                }
 
16444
 
 
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);
 
16450
 
 
16451
 
 
16452
                                // Record the placement and visibility
 
16453
                                dataLabel._attr = {
 
16454
                                        visibility: visibility,
 
16455
                                        align: labelPos[6]
 
16456
                                };
 
16457
                                dataLabel._pos = {
 
16458
                                        x: x + options.x +
 
16459
                                                ({ left: connectorPadding, right: -connectorPadding }[labelPos[6]] || 0),
 
16460
                                        y: y + options.y - 10 // 10 is for the baseline (label vs text)
 
16461
                                };
 
16462
                                dataLabel.connX = x;
 
16463
                                dataLabel.connY = y;
 
16464
 
 
16465
 
 
16466
                                // Detect overflowing data labels
 
16467
                                if (this.options.size === null) {
 
16468
                                        dataLabelWidth = dataLabel.width;
 
16469
                                        // Overflow left
 
16470
                                        if (x - dataLabelWidth < connectorPadding) {
 
16471
                                                overflow[3] = mathMax(mathRound(dataLabelWidth - x + connectorPadding), overflow[3]);
 
16472
 
 
16473
                                        // Overflow right
 
16474
                                        } else if (x + dataLabelWidth > plotWidth - connectorPadding) {
 
16475
                                                overflow[1] = mathMax(mathRound(x + dataLabelWidth - plotWidth + connectorPadding), overflow[1]);
 
16476
                                        }
 
16477
 
 
16478
                                        // Overflow top
 
16479
                                        if (y - labelHeight / 2 < 0) {
 
16480
                                                overflow[0] = mathMax(mathRound(-y + labelHeight / 2), overflow[0]);
 
16481
 
 
16482
                                        // Overflow left
 
16483
                                        } else if (y + labelHeight / 2 > plotHeight) {
 
16484
                                                overflow[2] = mathMax(mathRound(y + labelHeight / 2 - plotHeight), overflow[2]);
 
16485
                                        }
 
16486
                                }
 
16487
                        } // for each point
 
16488
                } // for each half
 
16489
 
 
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)) {
 
16493
 
 
16494
                        // Place the labels in the final position
 
16495
                        this.placeDataLabels();
 
16496
 
 
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;
 
16503
 
 
16504
                                        if (dataLabel && dataLabel._pos) {
 
16505
                                                visibility = dataLabel._attr.visibility;
 
16506
                                                x = dataLabel.connX;
 
16507
                                                y = dataLabel.connY;
 
16508
                                                connectorPath = softConnector ? [
 
16509
                                                        M,
 
16510
                                                        x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label
 
16511
                                                        'C',
 
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
 
16515
                                                        L,
 
16516
                                                        labelPos[4], labelPos[5] // base
 
16517
                                                ] : [
 
16518
                                                        M,
 
16519
                                                        x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label
 
16520
                                                        L,
 
16521
                                                        labelPos[2], labelPos[3], // second break
 
16522
                                                        L,
 
16523
                                                        labelPos[4], labelPos[5] // base
 
16524
                                                ];
 
16525
 
 
16526
                                                if (connector) {
 
16527
                                                        connector.animate({ d: connectorPath });
 
16528
                                                        connector.attr('visibility', visibility);
 
16529
 
 
16530
                                                } else {
 
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)
 
16536
                                                        })
 
16537
                                                        .add(series.group);
 
16538
                                                }
 
16539
                                        } else if (connector) {
 
16540
                                                point.connector = connector.destroy();
 
16541
                                        }
 
16542
                                });
 
16543
                        }
 
16544
                }
 
16545
        };
 
16546
        /**
 
16547
         * Perform the final placement of the data labels after we have verified that they
 
16548
         * fall within the plot area.
 
16549
         */
 
16550
        seriesTypes.pie.prototype.placeDataLabels = function () {
 
16551
                each(this.points, function (point) {
 
16552
                        var dataLabel = point.dataLabel,
 
16553
                                _pos;
 
16554
 
 
16555
                        if (dataLabel) {
 
16556
                                _pos = dataLabel._pos;
 
16557
                                if (_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 });
 
16563
                                }
 
16564
                        }
 
16565
                });
 
16566
        };
 
16567
 
 
16568
        seriesTypes.pie.prototype.alignDataLabel =  noop;
 
16569
 
 
16570
        /**
 
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
 
16573
         * to draw.
 
16574
         */
 
16575
        seriesTypes.pie.prototype.verifyDataLabelOverflow = function (overflow) {
 
16576
 
 
16577
                var center = this.center,
 
16578
                        options = this.options,
 
16579
                        centerOption = options.center,
 
16580
                        minSize = options.minSize || 80,
 
16581
                        newSize = minSize,
 
16582
                        ret;
 
16583
 
 
16584
                // Handle horizontal size and center
 
16585
                if (centerOption[0] !== null) { // Fixed center
 
16586
                        newSize = mathMax(center[2] - mathMax(overflow[1], overflow[3]), minSize);
 
16587
 
 
16588
                } else { // Auto center
 
16589
                        newSize = mathMax(
 
16590
                                center[2] - overflow[1] - overflow[3], // horizontal overflow
 
16591
                                minSize
 
16592
                        );
 
16593
                        center[0] += (overflow[3] - overflow[1]) / 2; // horizontal center
 
16594
                }
 
16595
 
 
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);
 
16599
 
 
16600
                } else { // Auto center
 
16601
                        newSize = mathMax(
 
16602
                                mathMin(
 
16603
                                        newSize,
 
16604
                                        center[2] - overflow[0] - overflow[2] // vertical overflow
 
16605
                                ),
 
16606
                                minSize
 
16607
                        );
 
16608
                        center[1] += (overflow[0] - overflow[2]) / 2; // vertical center
 
16609
                }
 
16610
 
 
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
 
16618
                                }
 
16619
                        });
 
16620
 
 
16621
                        if (this.drawDataLabels) {
 
16622
                                this.drawDataLabels();
 
16623
                        }
 
16624
                // Else, return true to indicate that the pie and its labels is within the plot area
 
16625
                } else {
 
16626
                        ret = true;
 
16627
                }
 
16628
                return ret;
 
16629
        };
 
16630
}
 
16631
 
 
16632
if (seriesTypes.column) {
 
16633
 
 
16634
        /**
 
16635
         * Override the basic data label alignment by adjusting for the position of the column
 
16636
         */
 
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?
 
16643
 
 
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);
 
16647
 
 
16648
                        if (inverted) {
 
16649
                                alignTo = {
 
16650
                                        x: chart.plotWidth - alignTo.y - alignTo.height,
 
16651
                                        y: chart.plotHeight - alignTo.x - alignTo.width,
 
16652
                                        width: alignTo.height,
 
16653
                                        height: alignTo.width
 
16654
                                };
 
16655
                        }
 
16656
 
 
16657
                        // Compute the alignment box
 
16658
                        if (!inside) {
 
16659
                                if (inverted) {
 
16660
                                        alignTo.x += below ? 0 : alignTo.width;
 
16661
                                        alignTo.width = 0;
 
16662
                                } else {
 
16663
                                        alignTo.y += below ? alignTo.height : 0;
 
16664
                                        alignTo.height = 0;
 
16665
                                }
 
16666
                        }
 
16667
                }
 
16668
 
 
16669
 
 
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(
 
16673
                        options.align,
 
16674
                        !inverted || inside ? 'center' : below ? 'right' : 'left'
 
16675
                );
 
16676
                options.verticalAlign = pick(
 
16677
                        options.verticalAlign,
 
16678
                        inverted || inside ? 'middle' : below ? 'top' : 'bottom'
 
16679
                );
 
16680
 
 
16681
                // Call the parent method
 
16682
                Series.prototype.alignDataLabel.call(this, point, dataLabel, options, alignTo, isNew);
 
16683
        };
 
16684
}
 
16685
 
 
16686
 
 
16687
 
 
16688
/**
 
16689
 * TrackerMixin for points and graphs
 
16690
 */
 
16691
 
 
16692
var TrackerMixin = Highcharts.TrackerMixin = {
 
16693
 
 
16694
        drawTrackerPoint: function () {
 
16695
                var series = this,
 
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,
 
16702
                                point;
 
16703
 
 
16704
                                if (chart.hoverSeries !== series) {
 
16705
                                        series.onMouseOver();
 
16706
                                }
 
16707
 
 
16708
                                while (target && !point) {
 
16709
                                        point = target.point;
 
16710
                                        target = target.parentNode;
 
16711
                                }
 
16712
 
 
16713
                                if (point !== UNDEFINED && point !== chart.hoverPoint) { // undefined on graph in scatterchart
 
16714
                                        point.onMouseOver(e);
 
16715
                                }
 
16716
                        };
 
16717
 
 
16718
                // Add reference to the point
 
16719
                each(series.points, function (point) {
 
16720
                        if (point.graphic) {
 
16721
                        point.graphic.element.point = point;
 
16722
                        }
 
16723
                        if (point.dataLabel) {
 
16724
                        point.dataLabel.element.point = point;
 
16725
                        }
 
16726
                });
 
16727
 
 
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
 
16732
                                series[key]
 
16733
                                .addClass(PREFIX + 'tracker')
 
16734
                                .on('mouseover', onMouseOver)
 
16735
                                .on('mouseout', function (e) { pointer.onTrackerMouseOut(e); })
 
16736
                                .css(css);
 
16737
                                if (hasTouch) {
 
16738
                                series[key].on('touchstart', onMouseOver);
 
16739
                                }
 
16740
                        }
 
16741
                        });
 
16742
                        series._hasTracking = true;
 
16743
                }
 
16744
        },
 
16745
 
 
16746
        /**
 
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.
 
16751
         */
 
16752
        drawTrackerGraph: function () {
 
16753
                var series = this,
 
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,
 
16766
                        singlePoint,
 
16767
                        i,
 
16768
                        onMouseOver = function () {
 
16769
                                if (chart.hoverSeries !== series) {
 
16770
                                        series.onMouseOver();
 
16771
                                }
 
16772
                        },
 
16773
                        /*
 
16774
                         * Empirical lowest possible opacities for TRACKER_FILL for an element to stay invisible but clickable
 
16775
                         * IE6: 0.002
 
16776
                         * IE7: 0.002
 
16777
                         * IE8: 0.002
 
16778
                         * IE9: 0.00000000001 (unlimited)
 
16779
                         * IE10: 0.0001 (exporting only)
 
16780
                         * FF: 0.00000000001 (unlimited)
 
16781
                         * Chrome: 0.000001
 
16782
                         * Safari: 0.000001
 
16783
                         * Opera: 0.00000000001 (unlimited)
 
16784
                         */
 
16785
                        TRACKER_FILL = 'rgba(192,192,192,' + (hasSVG ? 0.0001 : 0.002) + ')';
 
16786
 
 
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;
 
16791
                        while (i--) {
 
16792
                                if (trackerPath[i] === M) { // extend left side
 
16793
                                        trackerPath.splice(i + 1, 0, trackerPath[i + 1] - snap, trackerPath[i + 2], L);
 
16794
                                }
 
16795
                                if ((i && trackerPath[i] === M) || i === trackerPathLength) { // extend right side
 
16796
                                        trackerPath.splice(i, 0, L, trackerPath[i - 2] + snap, trackerPath[i - 1]);
 
16797
                                }
 
16798
                        }
 
16799
                }
 
16800
 
 
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);
 
16806
                }
 
16807
 
 
16808
                // draw the tracker
 
16809
                if (tracker) {
 
16810
                        tracker.attr({ d: trackerPath });
 
16811
                } else { // create
 
16812
 
 
16813
                        series.tracker = renderer.path(trackerPath)
 
16814
                        .attr({
 
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),
 
16820
                                zIndex: 2
 
16821
                        })
 
16822
                        .add(series.group);
 
16823
 
 
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); })
 
16830
                                        .css(css);
 
16831
 
 
16832
                                if (hasTouch) {
 
16833
                                        tracker.on('touchstart', onMouseOver);
 
16834
                                }
 
16835
                        });
 
16836
                }
 
16837
        }
 
16838
};
 
16839
/* End TrackerMixin */
 
16840
 
 
16841
 
 
16842
/**
 
16843
 * Add tracking event listener to the series group, so the point graphics
 
16844
 * themselves act as trackers
 
16845
 */ 
 
16846
 
 
16847
if (seriesTypes.column) {
 
16848
        ColumnSeries.prototype.drawTracker = TrackerMixin.drawTrackerPoint;     
 
16849
}
 
16850
 
 
16851
if (seriesTypes.pie) {
 
16852
        seriesTypes.pie.prototype.drawTracker = TrackerMixin.drawTrackerPoint;
 
16853
}
 
16854
 
 
16855
if (seriesTypes.scatter) {
 
16856
        ScatterSeries.prototype.drawTracker = TrackerMixin.drawTrackerPoint;
 
16857
}
 
16858
 
 
16859
/* 
 
16860
 * Extend Legend for item events 
 
16861
 */ 
 
16862
extend(Legend.prototype, {
 
16863
 
 
16864
        setItemEvents: function (item, legendItem, useHTML, itemStyle, itemHiddenStyle) {
 
16865
        var legend = this;
 
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);
 
16870
                })
 
16871
                .on('mouseout', function () {
 
16872
                        legendItem.css(item.visible ? itemStyle : itemHiddenStyle);
 
16873
                        item.setState();
 
16874
                })
 
16875
                .on('click', function (event) {
 
16876
                        var strLegendItemClick = 'legendItemClick',
 
16877
                                fnLegendItemClick = function () {
 
16878
                                        item.setVisible();
 
16879
                                };
 
16880
                                
 
16881
                        // Pass over the click/touch event. #4.
 
16882
                        event = {
 
16883
                                browserEvent: event
 
16884
                        };
 
16885
 
 
16886
                        // click the name or symbol
 
16887
                        if (item.firePointEvent) { // point
 
16888
                                item.firePointEvent(strLegendItemClick, event, fnLegendItemClick);
 
16889
                        } else {
 
16890
                                fireEvent(item, strLegendItemClick, event, fnLegendItemClick);
 
16891
                        }
 
16892
                });
 
16893
        },
 
16894
 
 
16895
        createCheckboxForItem: function (item) {
 
16896
                var legend = this;
 
16897
 
 
16898
                item.checkbox = createElement('input', {
 
16899
                        type: 'checkbox',
 
16900
                        checked: item.selected,
 
16901
                        defaultChecked: item.selected // required by IE7
 
16902
                }, legend.options.itemCheckboxStyle, legend.chart.container);
 
16903
 
 
16904
                addEvent(item.checkbox, 'click', function (event) {
 
16905
                        var target = event.target;
 
16906
                        fireEvent(item, 'checkboxClick', {
 
16907
                                        checked: target.checked
 
16908
                                },
 
16909
                                function () {
 
16910
                                        item.select();
 
16911
                                }
 
16912
                        );
 
16913
                });
 
16914
        }       
 
16915
});
 
16916
 
 
16917
/* 
 
16918
 * Add pointer cursor to legend itemstyle in defaultOptions
 
16919
 */
 
16920
defaultOptions.legend.itemStyle.cursor = 'pointer';
 
16921
 
 
16922
 
 
16923
/* 
 
16924
 * Extend the Chart object with interaction
 
16925
 */
 
16926
 
 
16927
extend(Chart.prototype, {
 
16928
        /**
 
16929
         * Display the zoom button
 
16930
         */
 
16931
        showResetZoom: function () {
 
16932
                var chart = this,
 
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';
 
16938
                        
 
16939
                this.resetZoomButton = chart.renderer.button(lang.resetZoom, null, null, function () { chart.zoomOut(); }, theme, states && states.hover)
 
16940
                        .attr({
 
16941
                                align: btnOptions.position.align,
 
16942
                                title: lang.resetZoomTitle
 
16943
                        })
 
16944
                        .add()
 
16945
                        .align(btnOptions.position, false, alignTo);
 
16946
                        
 
16947
        },
 
16948
 
 
16949
        /**
 
16950
         * Zoom out to 1:1
 
16951
         */
 
16952
        zoomOut: function () {
 
16953
                var chart = this;
 
16954
                fireEvent(chart, 'selection', { resetSelection: true }, function () { 
 
16955
                        chart.zoom();
 
16956
                });
 
16957
        },
 
16958
 
 
16959
        /**
 
16960
         * Zoom into a given portion of the chart given by axis coordinates
 
16961
         * @param {Object} event
 
16962
         */
 
16963
        zoom: function (event) {
 
16964
                var chart = this,
 
16965
                        hasZoomed,
 
16966
                        pointer = chart.pointer,
 
16967
                        displayButton = false,
 
16968
                        resetZoomButton;
 
16969
 
 
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();
 
16974
                        });
 
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;
 
16979
 
 
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;
 
16985
                                        }
 
16986
                                }
 
16987
                        });
 
16988
                }
 
16989
                
 
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();
 
16996
                }
 
16997
                
 
16998
 
 
16999
                // Redraw
 
17000
                if (hasZoomed) {
 
17001
                        chart.redraw(
 
17002
                                pick(chart.options.chart.animation, event && event.animation, chart.pointCount < 100) // animation
 
17003
                        );
 
17004
                }
 
17005
        },
 
17006
 
 
17007
        /**
 
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.
 
17011
         */
 
17012
        pan: function (e, panning) {
 
17013
 
 
17014
                var chart = this,
 
17015
                        hoverPoints = chart.hoverPoints,
 
17016
                        doRedraw;
 
17017
 
 
17018
                // remove active points for shared tooltip
 
17019
                if (hoverPoints) {
 
17020
                        each(hoverPoints, function (point) {
 
17021
                                point.setState();
 
17022
                        });
 
17023
                }
 
17024
 
 
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;
 
17033
 
 
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' });
 
17036
                                doRedraw = true;
 
17037
                        }
 
17038
 
 
17039
                        chart[isX ? 'mouseDownX' : 'mouseDownY'] = mousePos; // set new reference for next run
 
17040
                });
 
17041
 
 
17042
                if (doRedraw) {
 
17043
                        chart.redraw(false);
 
17044
                }
 
17045
                css(chart.container, { cursor: 'move' });
 
17046
        }
 
17047
});
 
17048
 
 
17049
/*
 
17050
 * Extend the Point object with interaction
 
17051
 */
 
17052
extend(Point.prototype, {
 
17053
        /**
 
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.
 
17058
         */
 
17059
        select: function (selected, accumulate) {
 
17060
                var point = this,
 
17061
                        series = point.series,
 
17062
                        chart = series.chart;
 
17063
 
 
17064
                selected = pick(selected, !point.selected);
 
17065
 
 
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;
 
17070
 
 
17071
                        point.setState(selected && SELECT_STATE);
 
17072
 
 
17073
                        // unselect all other points unless Ctrl or Cmd + click
 
17074
                        if (!accumulate) {
 
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');
 
17081
                                        }
 
17082
                                });
 
17083
                        }
 
17084
                });
 
17085
        },
 
17086
 
 
17087
        /**
 
17088
         * Runs on mouse over the point
 
17089
         */
 
17090
        onMouseOver: function (e) {
 
17091
                var point = this,
 
17092
                        series = point.series,
 
17093
                        chart = series.chart,
 
17094
                        tooltip = chart.tooltip,
 
17095
                        hoverPoint = chart.hoverPoint;
 
17096
 
 
17097
                // set normal state to previous series
 
17098
                if (hoverPoint && hoverPoint !== point) {
 
17099
                        hoverPoint.onMouseOut();
 
17100
                }
 
17101
 
 
17102
                // trigger the event
 
17103
                point.firePointEvent('mouseOver');
 
17104
 
 
17105
                // update the tooltip
 
17106
                if (tooltip && (!tooltip.shared || series.noSharedTooltip)) {
 
17107
                        tooltip.refresh(point, e);
 
17108
                }
 
17109
 
 
17110
                // hover this
 
17111
                point.setState(HOVER_STATE);
 
17112
                chart.hoverPoint = point;
 
17113
        },
 
17114
 
 
17115
        /**
 
17116
         * Runs on mouse out from the point
 
17117
         */
 
17118
        onMouseOut: function () {
 
17119
                var chart = this.series.chart,
 
17120
                        hoverPoints = chart.hoverPoints;
 
17121
 
 
17122
                if (!hoverPoints || inArray(this, hoverPoints) === -1) { // #887
 
17123
                        this.firePointEvent('mouseOut');
 
17124
 
 
17125
                        this.setState();
 
17126
                        chart.hoverPoint = null;
 
17127
                }
 
17128
        },
 
17129
 
 
17130
        /**
 
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
 
17136
         */
 
17137
        firePointEvent: function (eventType, eventArgs, defaultFunction) {
 
17138
                var point = this,
 
17139
                        series = this.series,
 
17140
                        seriesOptions = series.options;
 
17141
 
 
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();
 
17145
                }
 
17146
 
 
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);
 
17152
                        };
 
17153
                }
 
17154
 
 
17155
                fireEvent(this, eventType, eventArgs, defaultFunction);
 
17156
        },
 
17157
        /**
 
17158
         * Import events from the series' and point's options. Only do it on
 
17159
         * demand, to save processing time on hovering.
 
17160
         */
 
17161
        importEvents: function () {
 
17162
                if (!this.hasImportedEvents) {
 
17163
                        var point = this,
 
17164
                                options = merge(point.series.options.point, point.options),
 
17165
                                events = options.events,
 
17166
                                eventType;
 
17167
 
 
17168
                        point.events = events;
 
17169
 
 
17170
                        for (eventType in events) {
 
17171
                                addEvent(point, eventType, events[eventType]);
 
17172
                        }
 
17173
                        this.hasImportedEvents = true;
 
17174
 
 
17175
                }
 
17176
        },
 
17177
 
 
17178
        /**
 
17179
         * Set the point's state
 
17180
         * @param {String} state
 
17181
         */
 
17182
        setState: function (state, move) {
 
17183
                var point = this,
 
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,
 
17195
                        radius,
 
17196
                        newSymbol,
 
17197
                        pointAttr = point.pointAttr;
 
17198
 
 
17199
                state = state || NORMAL_STATE; // empty string
 
17200
                move = move && stateMarkerGraphic;
 
17201
 
 
17202
                if (
 
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
 
17213
 
 
17214
                        ) {
 
17215
                        return;
 
17216
                }
 
17217
 
 
17218
 
 
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(
 
17223
                                pointAttr[state],
 
17224
                                radius ? { // new symbol attributes (#507, #612)
 
17225
                                        x: plotX - radius,
 
17226
                                        y: plotY - radius,
 
17227
                                        width: 2 * radius,
 
17228
                                        height: 2 * radius
 
17229
                                } : {}
 
17230
                        ));
 
17231
                } else {
 
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;
 
17237
 
 
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();
 
17242
                                }
 
17243
 
 
17244
                                // Add a new state marker graphic
 
17245
                                if (!stateMarkerGraphic) {
 
17246
                                        series.stateMarkerGraphic = stateMarkerGraphic = chart.renderer.symbol(
 
17247
                                                newSymbol,
 
17248
                                                plotX - radius,
 
17249
                                                plotY - radius,
 
17250
                                                2 * radius,
 
17251
                                                2 * radius
 
17252
                                        )
 
17253
                                        .attr(pointAttr[state])
 
17254
                                        .add(series.markerGroup);
 
17255
                                        stateMarkerGraphic.currentSymbol = newSymbol;
 
17256
 
 
17257
                                // Move the existing graphic
 
17258
                                } else {
 
17259
                                        stateMarkerGraphic[move ? 'animate' : 'attr']({ // #1054
 
17260
                                                x: plotX - radius,
 
17261
                                                y: plotY - radius
 
17262
                                        });
 
17263
                                }
 
17264
                        }
 
17265
 
 
17266
                        if (stateMarkerGraphic) {
 
17267
                                stateMarkerGraphic[state && chart.isInsidePlot(plotX, plotY, chart.inverted) ? 'show' : 'hide'](); // #2450
 
17268
                        }
 
17269
                }
 
17270
 
 
17271
                point.state = state;
 
17272
        }
 
17273
});
 
17274
 
 
17275
/*
 
17276
 * Extend the Series object with interaction
 
17277
 */
 
17278
 
 
17279
extend(Series.prototype, {
 
17280
        /**
 
17281
         * Series mouse over handler
 
17282
         */
 
17283
        onMouseOver: function () {
 
17284
                var series = this,
 
17285
                        chart = series.chart,
 
17286
                        hoverSeries = chart.hoverSeries;
 
17287
 
 
17288
                // set normal state to previous series
 
17289
                if (hoverSeries && hoverSeries !== series) {
 
17290
                        hoverSeries.onMouseOut();
 
17291
                }
 
17292
 
 
17293
                // trigger the event, but to save processing time,
 
17294
                // only if defined
 
17295
                if (series.options.events.mouseOver) {
 
17296
                        fireEvent(series, 'mouseOver');
 
17297
                }
 
17298
 
 
17299
                // hover this
 
17300
                series.setState(HOVER_STATE);
 
17301
                chart.hoverSeries = series;
 
17302
        },
 
17303
 
 
17304
        /**
 
17305
         * Series mouse out handler
 
17306
         */
 
17307
        onMouseOut: function () {
 
17308
                // trigger the event only if listeners exist
 
17309
                var series = this,
 
17310
                        options = series.options,
 
17311
                        chart = series.chart,
 
17312
                        tooltip = chart.tooltip,
 
17313
                        hoverPoint = chart.hoverPoint;
 
17314
 
 
17315
                // trigger mouse out on the point, which must be in this series
 
17316
                if (hoverPoint) {
 
17317
                        hoverPoint.onMouseOut();
 
17318
                }
 
17319
 
 
17320
                // fire the mouse out event
 
17321
                if (series && options.events.mouseOut) {
 
17322
                        fireEvent(series, 'mouseOut');
 
17323
                }
 
17324
 
 
17325
 
 
17326
                // hide the tooltip
 
17327
                if (tooltip && !options.stickyTracking && (!tooltip.shared || series.noSharedTooltip)) {
 
17328
                        tooltip.hide();
 
17329
                }
 
17330
 
 
17331
                // set normal state
 
17332
                series.setState();
 
17333
                chart.hoverSeries = null;
 
17334
        },
 
17335
 
 
17336
        /**
 
17337
         * Set the state of the graph
 
17338
         */
 
17339
        setState: function (state) {
 
17340
                var series = this,
 
17341
                        options = series.options,
 
17342
                        graph = series.graph,
 
17343
                        graphNeg = series.graphNeg,
 
17344
                        stateOptions = options.states,
 
17345
                        lineWidth = options.lineWidth,
 
17346
                        attribs;
 
17347
 
 
17348
                state = state || NORMAL_STATE;
 
17349
 
 
17350
                if (series.state !== state) {
 
17351
                        series.state = state;
 
17352
 
 
17353
                        if (stateOptions[state] && stateOptions[state].enabled === false) {
 
17354
                                return;
 
17355
                        }
 
17356
 
 
17357
                        if (state) {
 
17358
                                lineWidth = stateOptions[state].lineWidth || lineWidth + 1;
 
17359
                        }
 
17360
 
 
17361
                        if (graph && !graph.dashstyle) { // hover is turned off for dashed lines in VML
 
17362
                                attribs = {
 
17363
                                        'stroke-width': lineWidth
 
17364
                                };
 
17365
                                // use attr because animate will cause any other animation on the graph to stop
 
17366
                                graph.attr(attribs);
 
17367
                                if (graphNeg) {
 
17368
                                        graphNeg.attr(attribs);
 
17369
                                }
 
17370
                        }
 
17371
                }
 
17372
        },
 
17373
 
 
17374
        /**
 
17375
         * Set the visibility of the graph
 
17376
         *
 
17377
         * @param vis {Boolean} True to show the series, false to hide. If UNDEFINED,
 
17378
         *                              the visibility is toggled.
 
17379
         */
 
17380
        setVisible: function (vis, redraw) {
 
17381
                var series = this,
 
17382
                        chart = series.chart,
 
17383
                        legendItem = series.legendItem,
 
17384
                        showOrHide,
 
17385
                        ignoreHiddenSeries = chart.options.chart.ignoreHiddenSeries,
 
17386
                        oldVisibility = series.visible;
 
17387
 
 
17388
                // if called without an argument, toggle visibility
 
17389
                series.visible = vis = series.userOptions.visible = vis === UNDEFINED ? !oldVisibility : vis;
 
17390
                showOrHide = vis ? 'show' : 'hide';
 
17391
 
 
17392
                // show or hide elements
 
17393
                each(['group', 'dataLabelsGroup', 'markerGroup', 'tracker'], function (key) {
 
17394
                        if (series[key]) {
 
17395
                                series[key][showOrHide]();
 
17396
                        }
 
17397
                });
 
17398
 
 
17399
 
 
17400
                // hide tooltip (#1361)
 
17401
                if (chart.hoverSeries === series) {
 
17402
                        series.onMouseOut();
 
17403
                }
 
17404
 
 
17405
 
 
17406
                if (legendItem) {
 
17407
                        chart.legend.colorizeItem(series, vis);
 
17408
                }
 
17409
 
 
17410
 
 
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;
 
17418
                                }
 
17419
                        });
 
17420
                }
 
17421
 
 
17422
                // show or hide linked series
 
17423
                each(series.linkedSeries, function (otherSeries) {
 
17424
                        otherSeries.setVisible(vis, false);
 
17425
                });
 
17426
 
 
17427
                if (ignoreHiddenSeries) {
 
17428
                        chart.isDirtyBox = true;
 
17429
                }
 
17430
                if (redraw !== false) {
 
17431
                        chart.redraw();
 
17432
                }
 
17433
 
 
17434
                fireEvent(series, showOrHide);
 
17435
        },
 
17436
 
 
17437
        /**
 
17438
         * Memorize tooltip texts and positions
 
17439
         */
 
17440
        setTooltipPoints: function (renew) {
 
17441
                var series = this,
 
17442
                        points = [],
 
17443
                        pointsLength,
 
17444
                        low,
 
17445
                        high,
 
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
 
17449
                        point,
 
17450
                        pointX,
 
17451
                        nextPoint,
 
17452
                        i,
 
17453
                        tooltipPoints = []; // a lookup array for each pixel in the x dimension
 
17454
 
 
17455
                // don't waste resources if tracker is disabled
 
17456
                if (series.options.enableMouseTracking === false || series.singularTooltips) {
 
17457
                        return;
 
17458
                }
 
17459
 
 
17460
                // renew
 
17461
                if (renew) {
 
17462
                        series.tooltipPoints = null;
 
17463
                }
 
17464
 
 
17465
                // concat segments to overcome null values
 
17466
                each(series.segments || series.points, function (segment) {
 
17467
                        points = points.concat(segment);
 
17468
                });
 
17469
 
 
17470
                // Reverse the points in case the X axis is reversed
 
17471
                if (xAxis && xAxis.reversed) {
 
17472
                        points = points.reverse();
 
17473
                }
 
17474
 
 
17475
                // Polar needs additional shaping
 
17476
                if (series.orderTooltipPoints) {
 
17477
                        series.orderTooltipPoints(points);
 
17478
                }
 
17479
 
 
17480
                // Assign each pixel position to the nearest point
 
17481
                pointsLength = points.length;
 
17482
                for (i = 0; i < pointsLength; i++) {
 
17483
                        point = points[i];
 
17484
                        pointX = point.x;
 
17485
                        if (pointX >= xExtremes.min && pointX <= xExtremes.max) { // #1149
 
17486
                                nextPoint = points[i + 1];
 
17487
 
 
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
 
17494
                                        )), axisLength) :
 
17495
                                        axisLength;
 
17496
 
 
17497
                                while (low >= 0 && low <= high) {
 
17498
                                        tooltipPoints[low++] = point;
 
17499
                                }
 
17500
                        }
 
17501
                }
 
17502
                series.tooltipPoints = tooltipPoints;
 
17503
        },
 
17504
 
 
17505
        /**
 
17506
         * Show the graph
 
17507
         */
 
17508
        show: function () {
 
17509
                this.setVisible(true);
 
17510
        },
 
17511
 
 
17512
        /**
 
17513
         * Hide the graph
 
17514
         */
 
17515
        hide: function () {
 
17516
                this.setVisible(false);
 
17517
        },
 
17518
 
 
17519
 
 
17520
        /**
 
17521
         * Set the selected state of the graph
 
17522
         *
 
17523
         * @param selected {Boolean} True to select the series, false to unselect. If
 
17524
         *                              UNDEFINED, the selection state is toggled.
 
17525
         */
 
17526
        select: function (selected) {
 
17527
                var series = this;
 
17528
                // if called without an argument, toggle
 
17529
                series.selected = selected = (selected === UNDEFINED) ? !series.selected : selected;
 
17530
 
 
17531
                if (series.checkbox) {
 
17532
                        series.checkbox.checked = selected;
 
17533
                }
 
17534
 
 
17535
                fireEvent(series, selected ? 'select' : 'unselect');
 
17536
        },
 
17537
 
 
17538
        drawTracker: TrackerMixin.drawTrackerGraph
 
17539
});/* ****************************************************************************
 
17540
 * Start ordinal axis logic                                                   *
 
17541
 *****************************************************************************/
 
17542
 
 
17543
 
 
17544
wrap(Series.prototype, 'init', function (proceed) {
 
17545
        var series = this,
 
17546
                xAxis;
 
17547
 
 
17548
        // call the original function
 
17549
        proceed.apply(this, Array.prototype.slice.call(arguments, 1));
 
17550
 
 
17551
        xAxis = series.xAxis;
 
17552
 
 
17553
        // Destroy the extended ordinal index on updated data
 
17554
        if (xAxis && xAxis.options.ordinal) {
 
17555
                addEvent(series, 'updatedData', function () {
 
17556
                        delete xAxis.ordinalIndex;
 
17557
                });
 
17558
        }
 
17559
});
 
17560
 
 
17561
/**
 
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.
 
17567
 */
 
17568
wrap(Axis.prototype, 'getTimeTicks', function (proceed, normalizedInterval, min, max, startOfWeek, positions, closestDistance, findHigherRanks) {
 
17569
 
 
17570
        var start = 0,
 
17571
                end = 0,
 
17572
                segmentPositions,
 
17573
                higherRanks = {},
 
17574
                hasCrossedHigherRank,
 
17575
                info,
 
17576
                posLength,
 
17577
                outsideMax,
 
17578
                groupPositions = [],
 
17579
                lastGroupPosition = -Number.MAX_VALUE,
 
17580
                tickPixelIntervalOption = this.options.tickPixelInterval;
 
17581
 
 
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);
 
17586
        }
 
17587
 
 
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++) {
 
17593
 
 
17594
                outsideMax = end && positions[end - 1] > max;
 
17595
 
 
17596
                if (positions[end] < min) { // Set the last position before min
 
17597
                        start = end;
 
17598
                }
 
17599
 
 
17600
                if (end === posLength - 1 || positions[end + 1] - positions[end] > closestDistance * 5 || outsideMax) {
 
17601
 
 
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
 
17605
 
 
17606
                                segmentPositions = proceed.call(this, normalizedInterval, positions[start], positions[end], startOfWeek);
 
17607
 
 
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();
 
17611
                                }
 
17612
                                if (segmentPositions.length) {
 
17613
                                        lastGroupPosition = segmentPositions[segmentPositions.length - 1];
 
17614
                                }
 
17615
 
 
17616
                                groupPositions = groupPositions.concat(segmentPositions);
 
17617
                        }
 
17618
                        // Set start of next segment
 
17619
                        start = end + 1;
 
17620
                }
 
17621
 
 
17622
                if (outsideMax) {
 
17623
                        break;
 
17624
                }
 
17625
        }
 
17626
 
 
17627
        // Get the grouping info from the last of the segments. The info is the same for
 
17628
        // all segments.
 
17629
        info = segmentPositions.info;
 
17630
 
 
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;
 
17635
 
 
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;
 
17641
                        }
 
17642
                }
 
17643
 
 
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;
 
17648
                }
 
17649
                info.higherRanks = higherRanks;
 
17650
        }
 
17651
 
 
17652
        // Save the info
 
17653
        groupPositions.info = info;
 
17654
 
 
17655
 
 
17656
 
 
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
 
17660
 
 
17661
                var length = groupPositions.length,
 
17662
                        i = length,
 
17663
                        itemToRemove,
 
17664
                        translated,
 
17665
                        translatedArr = [],
 
17666
                        lastTranslated,
 
17667
                        medianDistance,
 
17668
                        distance,
 
17669
                        distances = [];
 
17670
 
 
17671
                // Find median pixel distance in order to keep a reasonably even distance between
 
17672
                // ticks (#748)
 
17673
                while (i--) {
 
17674
                        translated = this.translate(groupPositions[i]);
 
17675
                        if (lastTranslated) {
 
17676
                                distances[i] = lastTranslated - translated;
 
17677
                        }
 
17678
                        translatedArr[i] = lastTranslated = translated;
 
17679
                }
 
17680
                distances.sort();
 
17681
                medianDistance = distances[mathFloor(distances.length / 2)];
 
17682
                if (medianDistance < tickPixelIntervalOption * 0.6) {
 
17683
                        medianDistance = null;
 
17684
                }
 
17685
 
 
17686
                // Now loop over again and remove ticks where needed
 
17687
                i = groupPositions[length - 1] > max ? length - 1 : length; // #817
 
17688
                lastTranslated = undefined;
 
17689
                while (i--) {
 
17690
                        translated = translatedArr[i];
 
17691
                        distance = lastTranslated - translated;
 
17692
 
 
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)) {
 
17697
 
 
17698
                                // Is this a higher ranked position with a normal position to the right?
 
17699
                                if (higherRanks[groupPositions[i]] && !higherRanks[groupPositions[i + 1]]) {
 
17700
 
 
17701
                                        // Yes: remove the lower ranked neighbour to the right
 
17702
                                        itemToRemove = i + 1;
 
17703
                                        lastTranslated = translated; // #709
 
17704
 
 
17705
                                } else {
 
17706
 
 
17707
                                        // No: remove this one
 
17708
                                        itemToRemove = i;
 
17709
                                }
 
17710
 
 
17711
                                groupPositions.splice(itemToRemove, 1);
 
17712
 
 
17713
                        } else {
 
17714
                                lastTranslated = translated;
 
17715
                        }
 
17716
                }
 
17717
        }
 
17718
        return groupPositions;
 
17719
});
 
17720
 
 
17721
// Extend the Axis prototype
 
17722
extend(Axis.prototype, {
 
17723
 
 
17724
        /**
 
17725
         * Calculate the ordinal positions before tick positions are calculated.
 
17726
         */
 
17727
        beforeSetTickPositions: function () {
 
17728
                var axis = this,
 
17729
                        len,
 
17730
                        ordinalPositions = [],
 
17731
                        useOrdinal = false,
 
17732
                        dist,
 
17733
                        extremes = axis.getExtremes(),
 
17734
                        min = extremes.min,
 
17735
                        max = extremes.max,
 
17736
                        minIndex,
 
17737
                        maxIndex,
 
17738
                        slope,
 
17739
                        i;
 
17740
 
 
17741
                // apply the ordinal logic
 
17742
                if (axis.options.ordinal) {
 
17743
 
 
17744
                        each(axis.series, function (series, i) {
 
17745
 
 
17746
                                if (series.visible !== false && series.takeOrdinalPosition !== false) {
 
17747
 
 
17748
                                        // concatenate the processed X data into the existing positions, or the empty array
 
17749
                                        ordinalPositions = ordinalPositions.concat(series.processedXData);
 
17750
                                        len = ordinalPositions.length;
 
17751
 
 
17752
                                        // remove duplicates (#1588)
 
17753
                                        ordinalPositions.sort(function (a, b) {
 
17754
                                                return a - b; // without a custom function it is sorted as strings
 
17755
                                        });
 
17756
 
 
17757
                                        if (len) {
 
17758
                                                i = len - 1;
 
17759
                                                while (i--) {
 
17760
                                                        if (ordinalPositions[i] === ordinalPositions[i + 1]) {
 
17761
                                                                ordinalPositions.splice(i, 1);
 
17762
                                                        }
 
17763
                                                }
 
17764
                                        }
 
17765
                                }
 
17766
 
 
17767
                        });
 
17768
 
 
17769
                        // cache the length
 
17770
                        len = ordinalPositions.length;
 
17771
 
 
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];
 
17776
                                i = len - 1;
 
17777
                                while (i-- && !useOrdinal) {
 
17778
                                        if (ordinalPositions[i + 1] - ordinalPositions[i] !== dist) {
 
17779
                                                useOrdinal = true;
 
17780
                                        }
 
17781
                                }
 
17782
 
 
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)) {
 
17786
                                        useOrdinal = true;
 
17787
                                }
 
17788
                        }
 
17789
 
 
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)
 
17793
                        if (useOrdinal) {
 
17794
 
 
17795
                                // Register
 
17796
                                axis.ordinalPositions = ordinalPositions;
 
17797
 
 
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);
 
17802
 
 
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);
 
17806
 
 
17807
                        } else {
 
17808
                                axis.ordinalPositions = axis.ordinalSlope = axis.ordinalOffset = UNDEFINED;
 
17809
                        }
 
17810
                }
 
17811
                axis.groupIntervalFactor = null; // reset for next run
 
17812
        },
 
17813
        /**
 
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.
 
17817
         *
 
17818
         * @param Number val The axis value
 
17819
         * @param Boolean toIndex Whether to return the index in the ordinalPositions or the new value
 
17820
         */
 
17821
        val2lin: function (val, toIndex) {
 
17822
                var axis = this,
 
17823
                        ordinalPositions = axis.ordinalPositions;
 
17824
 
 
17825
                if (!ordinalPositions) {
 
17826
                        return val;
 
17827
 
 
17828
                } else {
 
17829
 
 
17830
                        var ordinalLength = ordinalPositions.length,
 
17831
                                i,
 
17832
                                distance,
 
17833
                                ordinalIndex;
 
17834
 
 
17835
                        // first look for an exact match in the ordinalpositions array
 
17836
                        i = ordinalLength;
 
17837
                        while (i--) {
 
17838
                                if (ordinalPositions[i] === val) {
 
17839
                                        ordinalIndex = i;
 
17840
                                        break;
 
17841
                                }
 
17842
                        }
 
17843
 
 
17844
                        // if that failed, find the intermediate position between the two nearest values
 
17845
                        i = ordinalLength - 1;
 
17846
                        while (i--) {
 
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;
 
17850
                                        break;
 
17851
                                }
 
17852
                        }
 
17853
                        return toIndex ?
 
17854
                                ordinalIndex :
 
17855
                                axis.ordinalSlope * (ordinalIndex || 0) + axis.ordinalOffset;
 
17856
                }
 
17857
        },
 
17858
        /**
 
17859
         * Translate from linear (internal) to axis value
 
17860
         *
 
17861
         * @param Number val The linear abstracted value
 
17862
         * @param Boolean fromIndex Translate from an index in the ordinal positions rather than a value
 
17863
         */
 
17864
        lin2val: function (val, fromIndex) {
 
17865
                var axis = this,
 
17866
                        ordinalPositions = axis.ordinalPositions;
 
17867
 
 
17868
                if (!ordinalPositions) { // the visible range contains only equally spaced values
 
17869
                        return val;
 
17870
 
 
17871
                } else {
 
17872
 
 
17873
                        var ordinalSlope = axis.ordinalSlope,
 
17874
                                ordinalOffset = axis.ordinalOffset,
 
17875
                                i = ordinalPositions.length - 1,
 
17876
                                linearEquivalentLeft,
 
17877
                                linearEquivalentRight,
 
17878
                                distance;
 
17879
 
 
17880
 
 
17881
                        // Handle the case where we translate from the index directly, used only
 
17882
                        // when panning an ordinal axis
 
17883
                        if (fromIndex) {
 
17884
 
 
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
 
17892
                                }
 
17893
 
 
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.
 
17896
                        } else {
 
17897
                                while (i--) {
 
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
 
17902
                                                break;
 
17903
                                        }
 
17904
                                }
 
17905
                        }
 
17906
 
 
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) :
 
17911
                                val;
 
17912
                }
 
17913
        },
 
17914
        /**
 
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.
 
17920
         */
 
17921
        getExtendedPositions: function () {
 
17922
                var axis = this,
 
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(),
 
17928
                        fakeAxis,
 
17929
                        fakeSeries;
 
17930
 
 
17931
                // If this is the first time, or the ordinal index is deleted by updatedData,
 
17932
                // create it.
 
17933
                if (!ordinalIndex) {
 
17934
                        ordinalIndex = axis.ordinalIndex = {};
 
17935
                }
 
17936
 
 
17937
 
 
17938
                if (!ordinalIndex[key]) {
 
17939
 
 
17940
                        // Create a fake axis object where the extended ordinal positions are emulated
 
17941
                        fakeAxis = {
 
17942
                                series: [],
 
17943
                                getExtremes: function () {
 
17944
                                        return {
 
17945
                                                min: extremes.dataMin,
 
17946
                                                max: extremes.dataMax
 
17947
                                        };
 
17948
                                },
 
17949
                                options: {
 
17950
                                        ordinal: true
 
17951
                                },
 
17952
                                val2lin: Axis.prototype.val2lin // #2590
 
17953
                        };
 
17954
 
 
17955
                        // Add the fake series to hold the full data, then apply processData to it
 
17956
                        each(axis.series, function (series) {
 
17957
                                fakeSeries = {
 
17958
                                        xAxis: fakeAxis,
 
17959
                                        xData: series.xData,
 
17960
                                        chart: chart,
 
17961
                                        destroyGroupedData: noop
 
17962
                                };
 
17963
                                fakeSeries.options = {
 
17964
                                        dataGrouping : grouping ? {
 
17965
                                                enabled: true,
 
17966
                                                forced: true,
 
17967
                                                approximation: 'open', // doesn't matter which, use the fastest
 
17968
                                                units: [[grouping.unitName, [grouping.count]]]
 
17969
                                        } : {
 
17970
                                                enabled: false
 
17971
                                        }
 
17972
                                };
 
17973
                                series.processData.apply(fakeSeries);
 
17974
 
 
17975
                                fakeAxis.series.push(fakeSeries);
 
17976
                        });
 
17977
 
 
17978
                        // Run beforeSetTickPositions to compute the ordinalPositions
 
17979
                        axis.beforeSetTickPositions.apply(fakeAxis);
 
17980
 
 
17981
                        // Cache it
 
17982
                        ordinalIndex[key] = fakeAxis.ordinalPositions;
 
17983
                }
 
17984
                return ordinalIndex[key];
 
17985
        },
 
17986
 
 
17987
        /**
 
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.
 
17991
         *
 
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.
 
17998
         *
 
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
 
18001
         * not to change.
 
18002
         *
 
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.
 
18007
         */
 
18008
        getGroupIntervalFactor: function (xMin, xMax, series) {
 
18009
                var i = 0,
 
18010
                        processedXData = series.processedXData,
 
18011
                        len = processedXData.length,
 
18012
                        distances = [],
 
18013
                        median,
 
18014
                        groupIntervalFactor = this.groupIntervalFactor;
 
18015
 
 
18016
                // Only do this computation for the first series, let the other inherit it (#2416)
 
18017
                if (!groupIntervalFactor) {
 
18018
 
 
18019
                        // Register all the distances in an array
 
18020
                        for (; i < len - 1; i++) {
 
18021
                                distances[i] = processedXData[i + 1] - processedXData[i];
 
18022
                        }
 
18023
 
 
18024
                        // Sort them and find the median
 
18025
                        distances.sort(function (a, b) {
 
18026
                                        return a - b;
 
18027
                        });
 
18028
                        median = distances[mathFloor(len / 2)];
 
18029
 
 
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]);
 
18033
 
 
18034
                        this.groupIntervalFactor = groupIntervalFactor = (len * median) / (xMax - xMin);
 
18035
                }
 
18036
 
 
18037
                // Return the factor needed for data grouping
 
18038
                return groupIntervalFactor;
 
18039
        },
 
18040
 
 
18041
        /**
 
18042
         * Make the tick intervals closer because the ordinal gaps make the ticks spread out or cluster
 
18043
         */
 
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;
 
18051
 
 
18052
                return ordinalSlope ?
 
18053
                        tickInterval / (ordinalSlope / this.closestPointRange) :
 
18054
                        tickInterval;
 
18055
        }
 
18056
});
 
18057
 
 
18058
// Extending the Chart.pan method for ordinal axes
 
18059
wrap(Chart.prototype, 'pan', function (proceed, e) {
 
18060
        var chart = this,
 
18061
                xAxis = chart.xAxis[0],
 
18062
                chartX = e.chartX,
 
18063
                runBase = false;
 
18064
 
 
18065
        if (xAxis.options.ordinal && xAxis.series.length) {
 
18066
 
 
18067
                var mouseDownX = chart.mouseDownX,
 
18068
                        extremes = xAxis.getExtremes(),
 
18069
                        dataMax = extremes.dataMax,
 
18070
                        min = extremes.min,
 
18071
                        max = extremes.max,
 
18072
                        trimmedRange,
 
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
 
18078
                        ordinalPositions,
 
18079
                        searchAxisLeft,
 
18080
                        lin2val = xAxis.lin2val,
 
18081
                        val2lin = xAxis.val2lin,
 
18082
                        searchAxisRight;
 
18083
 
 
18084
                if (!extendedAxis.ordinalPositions) { // we have an ordinal axis, but the data is equally spaced
 
18085
                        runBase = true;
 
18086
 
 
18087
                } else if (mathAbs(movedUnits) > 1) {
 
18088
 
 
18089
                        // Remove active points for shared tooltip
 
18090
                        if (hoverPoints) {
 
18091
                                each(hoverPoints, function (point) {
 
18092
                                        point.setState();
 
18093
                                });
 
18094
                        }
 
18095
 
 
18096
                        if (movedUnits < 0) {
 
18097
                                searchAxisLeft = extendedAxis;
 
18098
                                searchAxisRight = xAxis.ordinalPositions ? xAxis : extendedAxis;
 
18099
                        } else {
 
18100
                                searchAxisLeft = xAxis.ordinalPositions ? xAxis : extendedAxis;
 
18101
                                searchAxisRight = extendedAxis;
 
18102
                        }
 
18103
 
 
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);
 
18110
                        }
 
18111
 
 
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
 
18121
                                ]),
 
18122
                                lin2val.apply(searchAxisRight, [
 
18123
                                        val2lin.apply(searchAxisRight, [max, true]) + movedUnits, // the new index
 
18124
                                        true // translate from index
 
18125
                                ])
 
18126
                        );
 
18127
 
 
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' });
 
18131
                        }
 
18132
 
 
18133
                        chart.mouseDownX = chartX; // set new reference for next run
 
18134
                        css(chart.container, { cursor: 'move' });
 
18135
                }
 
18136
 
 
18137
        } else {
 
18138
                runBase = true;
 
18139
        }
 
18140
 
 
18141
        // revert to the linear chart.pan version
 
18142
        if (runBase) {
 
18143
                // call the original function
 
18144
                proceed.apply(this, Array.prototype.slice.call(arguments, 1));
 
18145
        }
 
18146
});
 
18147
 
 
18148
 
 
18149
 
 
18150
/**
 
18151
 * Extend getSegments by identifying gaps in the ordinal data so that we can draw a gap in the
 
18152
 * line or area
 
18153
 */
 
18154
wrap(Series.prototype, 'getSegments', function (proceed) {
 
18155
 
 
18156
        var series = this,
 
18157
                segments,
 
18158
                gapSize = series.options.gapSize,
 
18159
                xAxis = series.xAxis;
 
18160
 
 
18161
        // call base method
 
18162
        proceed.apply(this, Array.prototype.slice.call(arguments, 1));
 
18163
 
 
18164
        if (gapSize) {
 
18165
 
 
18166
                // properties
 
18167
                segments = series.segments;
 
18168
 
 
18169
                // extension for ordinal breaks
 
18170
                each(segments, function (segment, no) {
 
18171
                        var i = segment.length - 1;
 
18172
                        while (i--) {
 
18173
                                if (segment[i + 1].x - segment[i].x > xAxis.closestPointRange * gapSize) {
 
18174
                                        segments.splice( // insert after this one
 
18175
                                                no + 1,
 
18176
                                                0,
 
18177
                                                segment.splice(i + 1, segment.length - i)
 
18178
                                        );
 
18179
                                }
 
18180
                        }
 
18181
                });
 
18182
        }
 
18183
});
 
18184
 
 
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,
 
18199
        NUMBER = 'number',
 
18200
 
 
18201
        commonOptions = {
 
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']
 
18217
                )
 
18218
                // smoothed = false, // enable this for navigator series only
 
18219
        },
 
18220
 
 
18221
        specificOptions = { // extends common options
 
18222
                line: {},
 
18223
                spline: {},
 
18224
                area: {},
 
18225
                areaspline: {},
 
18226
                column: {
 
18227
                        approximation: 'sum',
 
18228
                        groupPixelWidth: 10
 
18229
                },
 
18230
                arearange: {
 
18231
                        approximation: 'range'
 
18232
                },
 
18233
                areasplinerange: {
 
18234
                        approximation: 'range'
 
18235
                },
 
18236
                columnrange: {
 
18237
                        approximation: 'range',
 
18238
                        groupPixelWidth: 10
 
18239
                },
 
18240
                candlestick: {
 
18241
                        approximation: 'ohlc',
 
18242
                        groupPixelWidth: 10
 
18243
                },
 
18244
                ohlc: {
 
18245
                        approximation: 'ohlc',
 
18246
                        groupPixelWidth: 5
 
18247
                }
 
18248
        },
 
18249
 
 
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
 
18254
                ], [
 
18255
                        SECOND,
 
18256
                        [1, 2, 5, 10, 15, 30]
 
18257
                ], [
 
18258
                        MINUTE,
 
18259
                        [1, 2, 5, 10, 15, 30]
 
18260
                ], [
 
18261
                        HOUR,
 
18262
                        [1, 2, 3, 4, 6, 8, 12]
 
18263
                ], [
 
18264
                        DAY,
 
18265
                        [1]
 
18266
                ], [
 
18267
                        WEEK,
 
18268
                        [1]
 
18269
                ], [
 
18270
                        MONTH,
 
18271
                        [1, 3, 6]
 
18272
                ], [
 
18273
                        YEAR,
 
18274
                        null
 
18275
                ]
 
18276
        ],
 
18277
 
 
18278
 
 
18279
        /**
 
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.
 
18284
         */
 
18285
        approximations = {
 
18286
                sum: function (arr) {
 
18287
                        var len = arr.length,
 
18288
                                ret;
 
18289
 
 
18290
                        // 1. it consists of nulls exclusively
 
18291
                        if (!len && arr.hasNulls) {
 
18292
                                ret = null;
 
18293
                        // 2. it has a length and real values
 
18294
                        } else if (len) {
 
18295
                                ret = 0;
 
18296
                                while (len--) {
 
18297
                                        ret += arr[len];
 
18298
                                }
 
18299
                        }
 
18300
                        // 3. it has zero length, so just return undefined
 
18301
                        // => doNothing()
 
18302
 
 
18303
                        return ret;
 
18304
                },
 
18305
                average: function (arr) {
 
18306
                        var len = arr.length,
 
18307
                                ret = approximations.sum(arr);
 
18308
 
 
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) {
 
18312
                                ret = ret / len;
 
18313
                        }
 
18314
 
 
18315
                        return ret;
 
18316
                },
 
18317
                open: function (arr) {
 
18318
                        return arr.length ? arr[0] : (arr.hasNulls ? null : UNDEFINED);
 
18319
                },
 
18320
                high: function (arr) {
 
18321
                        return arr.length ? arrayMax(arr) : (arr.hasNulls ? null : UNDEFINED);
 
18322
                },
 
18323
                low: function (arr) {
 
18324
                        return arr.length ? arrayMin(arr) : (arr.hasNulls ? null : UNDEFINED);
 
18325
                },
 
18326
                close: function (arr) {
 
18327
                        return arr.length ? arr[arr.length - 1] : (arr.hasNulls ? null : UNDEFINED);
 
18328
                },
 
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);
 
18335
 
 
18336
                        if (typeof open === NUMBER || typeof high === NUMBER || typeof low === NUMBER || typeof close === NUMBER) {
 
18337
                                return [open, high, low, close];
 
18338
                        }
 
18339
                        // else, return is undefined
 
18340
                },
 
18341
                range: function (low, high) {
 
18342
                        low = approximations.low(low);
 
18343
                        high = approximations.high(high);
 
18344
 
 
18345
                        if (typeof low === NUMBER || typeof high === NUMBER) {
 
18346
                                return [low, high];
 
18347
                        }
 
18348
                        // else, return is undefined
 
18349
                }
 
18350
        };
 
18351
 
 
18352
/*jslint white:false */
 
18353
 
 
18354
/**
 
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.
 
18357
 */
 
18358
seriesProto.groupData = function (xData, yData, groupPositions, approximation) {
 
18359
        var series = this,
 
18360
                data = series.data,
 
18361
                dataOptions = series.options.data,
 
18362
                groupedXData = [],
 
18363
                groupedYData = [],
 
18364
                dataLength = xData.length,
 
18365
                pointX,
 
18366
                pointY,
 
18367
                groupedY,
 
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,
 
18373
                i;
 
18374
 
 
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]) {
 
18378
                        break;
 
18379
                }
 
18380
        }
 
18381
 
 
18382
        for (; i <= dataLength; i++) {
 
18383
 
 
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
 
18387
 
 
18388
                        // get group x and y
 
18389
                        pointX = groupPositions.shift();
 
18390
                        groupedY = approximationFn.apply(0, values);
 
18391
 
 
18392
                        // push the grouped data
 
18393
                        if (groupedY !== UNDEFINED) {
 
18394
                                groupedXData.push(pointX);
 
18395
                                groupedYData.push(groupedY);
 
18396
                        }
 
18397
 
 
18398
                        // reset the aggregate arrays
 
18399
                        values[0] = [];
 
18400
                        values[1] = [];
 
18401
                        values[2] = [];
 
18402
                        values[3] = [];
 
18403
 
 
18404
                        // don't loop beyond the last group
 
18405
                        if (i === dataLength) {
 
18406
                                break;
 
18407
                        }
 
18408
                }
 
18409
 
 
18410
                // break out
 
18411
                if (i === dataLength) {
 
18412
                        break;
 
18413
                }
 
18414
 
 
18415
                // for each raw data point, push it to an array that contains all values for this specific group
 
18416
                if (pointArrayMap) {
 
18417
 
 
18418
                        var index = series.cropStart + i,
 
18419
                                point = (data && data[index]) || series.pointClass.prototype.applyOptions.apply({ series: series }, [dataOptions[index]]),
 
18420
                                j,
 
18421
                                val;
 
18422
 
 
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;
 
18429
                                }
 
18430
                        }
 
18431
 
 
18432
                } else {
 
18433
                        pointY = handleYData ? yData[i] : null;
 
18434
 
 
18435
                        if (typeof pointY === NUMBER) {
 
18436
                                values[0].push(pointY);
 
18437
                        } else if (pointY === null) {
 
18438
                                values[0].hasNulls = true;
 
18439
                        }
 
18440
                }
 
18441
        }
 
18442
                
 
18443
        return [groupedXData, groupedYData];
 
18444
};
 
18445
 
 
18446
/**
 
18447
 * Extend the basic processData method, that crops the data to the current zoom
 
18448
 * range, with data grouping logic.
 
18449
 */
 
18450
seriesProto.processData = function () {
 
18451
        var series = this,
 
18452
                chart = series.chart,
 
18453
                options = series.options,
 
18454
                dataGroupingOptions = options[DATA_GROUPING],
 
18455
                groupingEnabled = dataGroupingOptions && pick(dataGroupingOptions.enabled, chart.options._stock),
 
18456
                hasGroupedData;
 
18457
 
 
18458
        // run base method
 
18459
        series.forceCrop = groupingEnabled; // #334
 
18460
        series.groupPixelWidth = null; // #2110
 
18461
        series.hasProcessed = true; // #2692
 
18462
 
 
18463
        // skip if processData returns false or if grouping is disabled (in that order)
 
18464
        if (baseProcessData.apply(series, arguments) === false || !groupingEnabled) {
 
18465
                return;
 
18466
 
 
18467
        } else {
 
18468
                series.destroyGroupedData();
 
18469
 
 
18470
        }
 
18471
        var i,
 
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;
 
18479
 
 
18480
        // Execute grouping if the amount of points is greater than the limit defined in groupPixelWidth
 
18481
        if (groupPixelWidth) {
 
18482
                hasGroupedData = true;
 
18483
 
 
18484
                series.points = null; // force recreation of point instances in series.translate
 
18485
 
 
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),
 
18493
                                xMin,
 
18494
                                xMax,
 
18495
                                null,
 
18496
                                processedXData,
 
18497
                                series.closestPointRange
 
18498
                        ),
 
18499
                        groupedXandY = seriesProto.groupData.apply(series, [processedXData, processedYData, groupPositions, dataGroupingOptions.approximation]),
 
18500
                        groupedXData = groupedXandY[0],
 
18501
                        groupedYData = groupedXandY[1];
 
18502
 
 
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;
 
18510
                        }
 
18511
                        groupedXData[0] = xMin;
 
18512
                }
 
18513
 
 
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;
 
18518
                }
 
18519
                series.closestPointRange = groupPositions.info.totalRange;
 
18520
 
 
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];
 
18524
                }
 
18525
 
 
18526
                // set series props
 
18527
                series.processedXData = groupedXData;
 
18528
                series.processedYData = groupedYData;
 
18529
        } else {
 
18530
                series.currentDataGrouping = null;
 
18531
                series.pointRange = nonGroupedPointRange;
 
18532
        }
 
18533
        series.hasGroupedData = hasGroupedData;
 
18534
};
 
18535
 
 
18536
/**
 
18537
 * Destroy the grouped data points. #622, #740
 
18538
 */
 
18539
seriesProto.destroyGroupedData = function () {
 
18540
 
 
18541
        var groupedData = this.groupedData;
 
18542
 
 
18543
        // clear previous groups
 
18544
        each(groupedData || [], function (point, i) {
 
18545
                if (point) {
 
18546
                        groupedData[i] = point.destroy ? point.destroy() : null;
 
18547
                }
 
18548
        });
 
18549
        this.groupedData = null;
 
18550
};
 
18551
 
 
18552
/**
 
18553
 * Override the generatePoints method by adding a reference to grouped data
 
18554
 */
 
18555
seriesProto.generatePoints = function () {
 
18556
 
 
18557
        baseGeneratePoints.apply(this);
 
18558
 
 
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;
 
18562
};
 
18563
 
 
18564
/**
 
18565
 * Extend the original method, make the tooltip's header reflect the grouped range
 
18566
 */
 
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,
 
18574
                xDateFormatEnd,
 
18575
                xAxis = series.xAxis,
 
18576
                currentDataGrouping,
 
18577
                dateTimeLabelFormats,
 
18578
                labelFormats,
 
18579
                formattedKey,
 
18580
                n,
 
18581
                ret;
 
18582
 
 
18583
        // apply only to grouped series
 
18584
        if (xAxis && xAxis.options.type === 'datetime' && dataGroupingOptions && isNumber(point.key)) {
 
18585
 
 
18586
                // set variables
 
18587
                currentDataGrouping = series.currentDataGrouping;
 
18588
                dateTimeLabelFormats = dataGroupingOptions.dateTimeLabelFormats;
 
18589
 
 
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];
 
18595
                        } else {
 
18596
                                xDateFormat = labelFormats[1];
 
18597
                                xDateFormatEnd = labelFormats[2];
 
18598
                        }
 
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)) {
 
18609
                                                
 
18610
                                        xDateFormat = dateTimeLabelFormats[n][0];
 
18611
                                        break;
 
18612
                                }
 
18613
                        }
 
18614
                }
 
18615
 
 
18616
                // now format the key
 
18617
                formattedKey = dateFormat(xDateFormat, point.key);
 
18618
                if (xDateFormatEnd) {
 
18619
                        formattedKey += dateFormat(xDateFormatEnd, point.key + currentDataGrouping.totalRange - 1);
 
18620
                }
 
18621
 
 
18622
                // return the replaced format
 
18623
                ret = tooltipOptions.headerFormat.replace('{point.key}', formattedKey);
 
18624
 
 
18625
        // else, fall back to the regular formatter
 
18626
        } else {
 
18627
                ret = baseTooltipHeaderFormatter.call(tooltip, point);
 
18628
        }
 
18629
 
 
18630
        return ret;
 
18631
};
 
18632
 
 
18633
/**
 
18634
 * Extend the series destroyer
 
18635
 */
 
18636
seriesProto.destroy = function () {
 
18637
        var series = this,
 
18638
                groupedData = series.groupedData || [],
 
18639
                i = groupedData.length;
 
18640
 
 
18641
        while (i--) {
 
18642
                if (groupedData[i]) {
 
18643
                        groupedData[i].destroy();
 
18644
                }
 
18645
        }
 
18646
        baseDestroy.apply(series);
 
18647
};
 
18648
 
 
18649
 
 
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) {
 
18653
 
 
18654
        var options = proceed.call(this, itemOptions),
 
18655
                type = this.type,
 
18656
                plotOptions = this.chart.options.plotOptions,
 
18657
                defaultOptions = defaultPlotOptions[type].dataGrouping;
 
18658
 
 
18659
        if (specificOptions[type]) { // #1284
 
18660
                if (!defaultOptions) {
 
18661
                        defaultOptions = merge(commonOptions, specificOptions[type]);
 
18662
                }
 
18663
 
 
18664
                options.dataGrouping = merge(
 
18665
                        defaultOptions,
 
18666
                        plotOptions.series && plotOptions.series.dataGrouping, // #1228
 
18667
                        plotOptions[type].dataGrouping, // Set by the StockChart constructor
 
18668
                        itemOptions.dataGrouping
 
18669
                );
 
18670
        }
 
18671
 
 
18672
        if (this.chart.options._stock) {
 
18673
                this.requireSorting = true;
 
18674
        }
 
18675
 
 
18676
        return options;
 
18677
});
 
18678
 
 
18679
 
 
18680
/**
 
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).
 
18683
 */
 
18684
wrap(Axis.prototype, 'setScale', function (proceed) {
 
18685
        proceed.call(this);
 
18686
        each(this.series, function (series) {
 
18687
                series.hasProcessed = false;
 
18688
        });
 
18689
});
 
18690
 
 
18691
/**
 
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.
 
18694
 */
 
18695
Axis.prototype.getGroupPixelWidth = function () {
 
18696
 
 
18697
        var series = this.series,
 
18698
                len = series.length,
 
18699
                i,
 
18700
                groupPixelWidth = 0,
 
18701
                doGrouping = false,
 
18702
                dataLength,
 
18703
                dgOptions;
 
18704
 
 
18705
        // If multiple series are compared on the same x axis, give them the same
 
18706
        // group pixel width (#334)
 
18707
        i = len;
 
18708
        while (i--) {
 
18709
                dgOptions = series[i].options.dataGrouping;
 
18710
                if (dgOptions) {
 
18711
                        groupPixelWidth = mathMax(groupPixelWidth, dgOptions.groupPixelWidth);
 
18712
 
 
18713
                }
 
18714
        }
 
18715
 
 
18716
        // If one of the series needs grouping, apply it to all (#1634)
 
18717
        i = len;
 
18718
        while (i--) {
 
18719
                dgOptions = series[i].options.dataGrouping;
 
18720
                        
 
18721
                if (dgOptions && series[i].hasProcessed) { // #2692
 
18722
 
 
18723
                        dataLength = (series[i].processedXData || series[i].data).length;
 
18724
 
 
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)) {
 
18727
                                doGrouping = true;
 
18728
                        }
 
18729
                }
 
18730
        }
 
18731
 
 
18732
        return doGrouping ? groupPixelWidth : 0;
 
18733
};
 
18734
 
 
18735
 
 
18736
 
 
18737
/* ****************************************************************************
 
18738
 * End data grouping module                                                                                                *
 
18739
 ******************************************************************************//* ****************************************************************************
 
18740
 * Start OHLC series code                                                                                                        *
 
18741
 *****************************************************************************/
 
18742
 
 
18743
// 1 - Set default options
 
18744
defaultPlotOptions.ohlc = merge(defaultPlotOptions.column, {
 
18745
        lineWidth: 1,
 
18746
        tooltip: {
 
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/>'
 
18752
        },
 
18753
        states: {
 
18754
                hover: {
 
18755
                        lineWidth: 3
 
18756
                }
 
18757
        },
 
18758
        threshold: null
 
18759
        //upColor: undefined
 
18760
});
 
18761
 
 
18762
// 2 - Create the OHLCSeries object
 
18763
var OHLCSeries = extendClass(seriesTypes.column, {
 
18764
        type: 'ohlc',
 
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];
 
18768
        },
 
18769
        pointValKey: 'high',
 
18770
 
 
18771
        pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
 
18772
                stroke: 'color',
 
18773
                'stroke-width': 'lineWidth'
 
18774
        },
 
18775
        upColorProp: 'stroke',
 
18776
 
 
18777
        /**
 
18778
         * Postprocess mapping between options and SVG attributes
 
18779
         */
 
18780
        getAttribs: function () {
 
18781
                seriesTypes.column.prototype.getAttribs.apply(this, arguments);
 
18782
                var series = this,
 
18783
                        options = series.options,
 
18784
                        stateOptions = options.states,
 
18785
                        upColor = options.upColor || series.color,
 
18786
                        seriesDownPointAttr = merge(series.pointAttr),
 
18787
                        upColorProp = series.upColorProp;
 
18788
 
 
18789
                seriesDownPointAttr[''][upColorProp] = upColor;
 
18790
                seriesDownPointAttr.hover[upColorProp] = stateOptions.hover.upColor || upColor;
 
18791
                seriesDownPointAttr.select[upColorProp] = stateOptions.select.upColor || upColor;
 
18792
 
 
18793
                each(series.points, function (point) {
 
18794
                        if (point.open < point.close) {
 
18795
                                point.pointAttr = seriesDownPointAttr;
 
18796
                        }
 
18797
                });
 
18798
        },
 
18799
 
 
18800
        /**
 
18801
         * Translate data points from raw values x and y to plotX and plotY
 
18802
         */
 
18803
        translate: function () {
 
18804
                var series = this,
 
18805
                        yAxis = series.yAxis;
 
18806
 
 
18807
                seriesTypes.column.prototype.translate.apply(series);
 
18808
 
 
18809
                // do the translation
 
18810
                each(series.points, function (point) {
 
18811
                        // the graphics
 
18812
                        if (point.open !== null) {
 
18813
                                point.plotOpen = yAxis.translate(point.open, 0, 1, 0, 1);
 
18814
                        }
 
18815
                        if (point.close !== null) {
 
18816
                                point.plotClose = yAxis.translate(point.close, 0, 1, 0, 1);
 
18817
                        }
 
18818
 
 
18819
                });
 
18820
        },
 
18821
 
 
18822
        /**
 
18823
         * Draw the data points
 
18824
         */
 
18825
        drawPoints: function () {
 
18826
                var series = this,
 
18827
                        points = series.points,
 
18828
                        chart = series.chart,
 
18829
                        pointAttr,
 
18830
                        plotOpen,
 
18831
                        plotClose,
 
18832
                        crispCorr,
 
18833
                        halfWidth,
 
18834
                        path,
 
18835
                        graphic,
 
18836
                        crispX;
 
18837
 
 
18838
 
 
18839
                each(points, function (point) {
 
18840
                        if (point.plotY !== UNDEFINED) {
 
18841
 
 
18842
                                graphic = point.graphic;
 
18843
                                pointAttr = point.pointAttr[point.selected ? 'selected' : ''];
 
18844
 
 
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);
 
18849
 
 
18850
                                // the vertical stem
 
18851
                                path = [
 
18852
                                        'M',
 
18853
                                        crispX, mathRound(point.yBottom),
 
18854
                                        'L',
 
18855
                                        crispX, mathRound(point.plotY)
 
18856
                                ];
 
18857
 
 
18858
                                // open
 
18859
                                if (point.open !== null) {
 
18860
                                        plotOpen = mathRound(point.plotOpen) + crispCorr;
 
18861
                                        path.push(
 
18862
                                                'M',
 
18863
                                                crispX,
 
18864
                                                plotOpen,
 
18865
                                                'L',
 
18866
                                                crispX - halfWidth,
 
18867
                                                plotOpen
 
18868
                                        );
 
18869
                                }
 
18870
 
 
18871
                                // close
 
18872
                                if (point.close !== null) {
 
18873
                                        plotClose = mathRound(point.plotClose) + crispCorr;
 
18874
                                        path.push(
 
18875
                                                'M',
 
18876
                                                crispX,
 
18877
                                                plotClose,
 
18878
                                                'L',
 
18879
                                                crispX + halfWidth,
 
18880
                                                plotClose
 
18881
                                        );
 
18882
                                }
 
18883
 
 
18884
                                // create and/or update the graphic
 
18885
                                if (graphic) {
 
18886
                                        graphic.animate({ d: path });
 
18887
                                } else {
 
18888
                                        point.graphic = chart.renderer.path(path)
 
18889
                                                .attr(pointAttr)
 
18890
                                                .add(series.group);
 
18891
                                }
 
18892
 
 
18893
                        }
 
18894
 
 
18895
 
 
18896
                });
 
18897
 
 
18898
        },
 
18899
 
 
18900
        /**
 
18901
         * Disable animation
 
18902
         */
 
18903
        animate: null
 
18904
 
 
18905
 
 
18906
});
 
18907
seriesTypes.ohlc = OHLCSeries;
 
18908
/* ****************************************************************************
 
18909
 * End OHLC series code                                                                                                    *
 
18910
 *****************************************************************************/
 
18911
/* ****************************************************************************
 
18912
 * Start Candlestick series code                                                                                          *
 
18913
 *****************************************************************************/
 
18914
 
 
18915
// 1 - set default options
 
18916
defaultPlotOptions.candlestick = merge(defaultPlotOptions.column, {
 
18917
        lineColor: 'black',
 
18918
        lineWidth: 1,
 
18919
        states: {
 
18920
                hover: {
 
18921
                        lineWidth: 2
 
18922
                }
 
18923
        },
 
18924
        tooltip: defaultPlotOptions.ohlc.tooltip,
 
18925
        threshold: null,
 
18926
        upColor: 'white'
 
18927
        // upLineColor: null
 
18928
});
 
18929
 
 
18930
// 2 - Create the CandlestickSeries object
 
18931
var CandlestickSeries = extendClass(OHLCSeries, {
 
18932
        type: 'candlestick',
 
18933
 
 
18934
        /**
 
18935
         * One-to-one mapping from options to SVG attributes
 
18936
         */
 
18937
        pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
 
18938
                fill: 'color',
 
18939
                stroke: 'lineColor',
 
18940
                'stroke-width': 'lineWidth'
 
18941
        },
 
18942
        upColorProp: 'fill',
 
18943
 
 
18944
        /**
 
18945
         * Postprocess mapping between options and SVG attributes
 
18946
         */
 
18947
        getAttribs: function () {
 
18948
                seriesTypes.ohlc.prototype.getAttribs.apply(this, arguments);
 
18949
                var series = this,
 
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;
 
18955
 
 
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;
 
18963
                        }
 
18964
                });
 
18965
        },
 
18966
 
 
18967
        /**
 
18968
         * Draw the data points
 
18969
         */
 
18970
        drawPoints: function () {
 
18971
                var series = this,  //state = series.state,
 
18972
                        points = series.points,
 
18973
                        chart = series.chart,
 
18974
                        pointAttr,
 
18975
                        seriesPointAttr = series.pointAttr[''],
 
18976
                        plotOpen,
 
18977
                        plotClose,
 
18978
                        topBox,
 
18979
                        bottomBox,
 
18980
                        hasTopWhisker,
 
18981
                        hasBottomWhisker,
 
18982
                        crispCorr,
 
18983
                        crispX,
 
18984
                        graphic,
 
18985
                        path,
 
18986
                        halfWidth;
 
18987
 
 
18988
 
 
18989
                each(points, function (point) {
 
18990
 
 
18991
                        graphic = point.graphic;
 
18992
                        if (point.plotY !== UNDEFINED) {
 
18993
 
 
18994
                                pointAttr = point.pointAttr[point.selected ? 'selected' : ''] || seriesPointAttr;
 
18995
 
 
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;
 
19008
 
 
19009
                                // create the path
 
19010
                                path = [
 
19011
                                        'M',
 
19012
                                        crispX - halfWidth, bottomBox,
 
19013
                                        'L',
 
19014
                                        crispX - halfWidth, topBox,
 
19015
                                        'L',
 
19016
                                        crispX + halfWidth, topBox,
 
19017
                                        'L',
 
19018
                                        crispX + halfWidth, bottomBox,
 
19019
                                        'Z', // Use a close statement to ensure a nice rectangle #2602
 
19020
                                        'M',
 
19021
                                        crispX, topBox,
 
19022
                                        'L',
 
19023
                                        crispX, hasTopWhisker ? mathRound(point.plotY) : topBox, // #460, #2094
 
19024
                                        'M',
 
19025
                                        crispX, bottomBox,
 
19026
                                        'L',
 
19027
                                        crispX, hasBottomWhisker ? mathRound(point.yBottom) : bottomBox, // #460, #2094
 
19028
                                        'Z'
 
19029
                                ];
 
19030
 
 
19031
                                if (graphic) {
 
19032
                                        graphic.animate({ d: path });
 
19033
                                } else {
 
19034
                                        point.graphic = chart.renderer.path(path)
 
19035
                                                .attr(pointAttr)
 
19036
                                                .add(series.group)
 
19037
                                                .shadow(series.options.shadow);
 
19038
                                }
 
19039
 
 
19040
                        }
 
19041
                });
 
19042
 
 
19043
        }
 
19044
 
 
19045
 
 
19046
});
 
19047
 
 
19048
seriesTypes.candlestick = CandlestickSeries;
 
19049
 
 
19050
/* ****************************************************************************
 
19051
 * End Candlestick series code                                                                                          *
 
19052
 *****************************************************************************/
 
19053
/* ****************************************************************************
 
19054
 * Start Flags series code                                                                                                      *
 
19055
 *****************************************************************************/
 
19056
 
 
19057
var symbols = SVGRenderer.prototype.symbols;
 
19058
 
 
19059
// 1 - set default options
 
19060
defaultPlotOptions.flags = merge(defaultPlotOptions.column, {
 
19061
        dataGrouping: null,
 
19062
        fillColor: 'white',
 
19063
        lineWidth: 1,
 
19064
        pointRange: 0, // #673
 
19065
        //radius: 2,
 
19066
        shape: 'flag',
 
19067
        stackDistance: 12,
 
19068
        states: {
 
19069
                hover: {
 
19070
                        lineColor: 'black',
 
19071
                        fillColor: '#FCFFC5'
 
19072
                }
 
19073
        },
 
19074
        style: {
 
19075
                fontSize: '11px',
 
19076
                fontWeight: 'bold',
 
19077
                textAlign: 'center'
 
19078
        },
 
19079
        tooltip: {
 
19080
                pointFormat: '{point.text}<br/>'
 
19081
        },
 
19082
        threshold: null,
 
19083
        y: -30
 
19084
});
 
19085
 
 
19086
// 2 - Create the CandlestickSeries object
 
19087
seriesTypes.flags = extendClass(seriesTypes.column, {
 
19088
        type: 'flags',
 
19089
        sorted: false,
 
19090
        noSharedTooltip: true,
 
19091
        takeOrdinalPosition: false, // #1074
 
19092
        trackerGroups: ['markerGroup'],
 
19093
        forceCrop: true,
 
19094
        /**
 
19095
         * Inherit the initialization from base Series
 
19096
         */
 
19097
        init: Series.prototype.init,
 
19098
 
 
19099
        /**
 
19100
         * One-to-one mapping from options to SVG attributes
 
19101
         */
 
19102
        pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
 
19103
                fill: 'fillColor',
 
19104
                stroke: 'color',
 
19105
                'stroke-width': 'lineWidth',
 
19106
                r: 'radius'
 
19107
        },
 
19108
 
 
19109
        /**
 
19110
         * Extend the translate method by placing the point on the related series
 
19111
         */
 
19112
        translate: function () {
 
19113
 
 
19114
                seriesTypes.column.prototype.translate.apply(this);
 
19115
 
 
19116
                var series = this,
 
19117
                        options = series.options,
 
19118
                        chart = series.chart,
 
19119
                        points = series.points,
 
19120
                        cursor = points.length - 1,
 
19121
                        point,
 
19122
                        lastPoint,
 
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(),
 
19130
                        leftPoint,
 
19131
                        lastX,
 
19132
                        rightPoint,
 
19133
                        currentDataGrouping;
 
19134
 
 
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
 
19139
 
 
19140
                        // sort the data points
 
19141
                        points.sort(function (a, b) {
 
19142
                                return (a.x - b.x);
 
19143
                        });
 
19144
 
 
19145
                        while (i-- && points[cursor]) {
 
19146
                                point = points[cursor];
 
19147
                                leftPoint = onData[i];
 
19148
                                
 
19149
                                if (leftPoint.x <= point.x && leftPoint.plotY !== UNDEFINED) {
 
19150
                                        if (point.x <= lastX) { // #803
 
19151
                                        
 
19152
                                                point.plotY = leftPoint.plotY;
 
19153
                                        
 
19154
                                                // interpolate between points, #666
 
19155
                                                if (leftPoint.x < point.x && !step) { 
 
19156
                                                        rightPoint = onData[i + 1];
 
19157
                                                        if (rightPoint && rightPoint.plotY !== UNDEFINED) {
 
19158
                                                                point.plotY += 
 
19159
                                                                        ((point.x - leftPoint.x) / (rightPoint.x - leftPoint.x)) * // the distance ratio, between 0 and 1 
 
19160
                                                                        (rightPoint.plotY - leftPoint.plotY); // the y distance
 
19161
                                                        }
 
19162
                                                }
 
19163
                                        }
 
19164
                                        cursor--;
 
19165
                                        i++; // check again for points in the same x position
 
19166
                                        if (cursor < 0) {
 
19167
                                                break;
 
19168
                                        }
 
19169
                                }
 
19170
                        }
 
19171
                }
 
19172
 
 
19173
                // Add plotY position and handle stacking
 
19174
                each(points, function (point, i) {
 
19175
                        
 
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;
 
19182
                                } else {
 
19183
                                        point.shapeArgs = {}; // 847
 
19184
                                }
 
19185
                        }
 
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;
 
19191
                                }
 
19192
                                point.stackIndex = lastPoint.stackIndex + 1;
 
19193
                        }
 
19194
                                        
 
19195
                });
 
19196
 
 
19197
 
 
19198
        },
 
19199
 
 
19200
        /**
 
19201
         * Draw the markers
 
19202
         */
 
19203
        drawPoints: function () {
 
19204
                var series = this,
 
19205
                        pointAttr,
 
19206
                        seriesPointAttr = series.pointAttr[''],
 
19207
                        points = series.points,
 
19208
                        chart = series.chart,
 
19209
                        renderer = chart.renderer,
 
19210
                        plotX,
 
19211
                        plotY,
 
19212
                        options = series.options,
 
19213
                        optionsY = options.y,
 
19214
                        shape,
 
19215
                        i,
 
19216
                        point,
 
19217
                        graphic,
 
19218
                        stackIndex,
 
19219
                        crisp = (options.lineWidth % 2 / 2),
 
19220
                        anchorX,
 
19221
                        anchorY,
 
19222
                        outsideRight;
 
19223
 
 
19224
                i = points.length;
 
19225
                while (i--) {
 
19226
                        point = points[i];
 
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);
 
19234
                        }
 
19235
                        anchorX = stackIndex ? UNDEFINED : point.plotX + crisp; // skip connectors for higher level stacked points
 
19236
                        anchorY = stackIndex ? UNDEFINED : point.plotY;
 
19237
 
 
19238
                        graphic = point.graphic;
 
19239
 
 
19240
                        // only draw the point if y is defined and the flag is within the visible area
 
19241
                        if (plotY !== UNDEFINED && plotX >= 0 && !outsideRight) {
 
19242
                                // shortcuts
 
19243
                                pointAttr = point.pointAttr[point.selected ? 'select' : ''] || seriesPointAttr;
 
19244
                                if (graphic) { // update
 
19245
                                        graphic.attr({
 
19246
                                                x: plotX,
 
19247
                                                y: plotY,
 
19248
                                                r: pointAttr.r,
 
19249
                                                anchorX: anchorX,
 
19250
                                                anchorY: anchorY
 
19251
                                        });
 
19252
                                } else {
 
19253
                                        graphic = point.graphic = renderer.label(
 
19254
                                                point.options.title || options.title || 'A',
 
19255
                                                plotX,
 
19256
                                                plotY,
 
19257
                                                shape,
 
19258
                                                anchorX,
 
19259
                                                anchorY,
 
19260
                                                options.useHTML
 
19261
                                        )
 
19262
                                        .css(merge(options.style, point.style))
 
19263
                                        .attr(pointAttr)
 
19264
                                        .attr({
 
19265
                                                align: shape === 'flag' ? 'left' : 'center',
 
19266
                                                width: options.width,
 
19267
                                                height: options.height
 
19268
                                        })
 
19269
                                        .add(series.markerGroup)
 
19270
                                        .shadow(options.shadow);
 
19271
 
 
19272
                                }
 
19273
 
 
19274
                                // Set the tooltip anchor position
 
19275
                                point.tooltipPos = [plotX, plotY];
 
19276
 
 
19277
                        } else if (graphic) {
 
19278
                                point.graphic = graphic.destroy();
 
19279
                        }
 
19280
 
 
19281
                }
 
19282
 
 
19283
        },
 
19284
 
 
19285
        /**
 
19286
         * Extend the column trackers with listeners to expand and contract stacks
 
19287
         */
 
19288
        drawTracker: function () {
 
19289
                var series = this,
 
19290
                        points = series.points;
 
19291
                
 
19292
                TrackerMixin.drawTrackerPoint.apply(this);
 
19293
 
 
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;
 
19298
                        if (graphic) {
 
19299
                                addEvent(graphic.element, 'mouseover', function () {
 
19300
 
 
19301
                                        // Raise this point
 
19302
                                        if (point.stackIndex > 0 && !point.raised) {
 
19303
                                                point._y = graphic.y;
 
19304
                                                graphic.attr({
 
19305
                                                        y: point._y - 8
 
19306
                                                });
 
19307
                                                point.raised = true;
 
19308
                                        }
 
19309
 
 
19310
                                        // Revert other raised points
 
19311
                                        each(points, function (otherPoint) {
 
19312
                                                if (otherPoint !== point && otherPoint.raised && otherPoint.graphic) {
 
19313
                                                        otherPoint.graphic.attr({
 
19314
                                                                y: otherPoint._y
 
19315
                                                        });
 
19316
                                                        otherPoint.raised = false;
 
19317
                                                }
 
19318
                                        });
 
19319
                                });
 
19320
                        }
 
19321
                });
 
19322
        },
 
19323
 
 
19324
        /**
 
19325
         * Disable animation
 
19326
         */
 
19327
        animate: noop
 
19328
 
 
19329
});
 
19330
 
 
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;
 
19335
 
 
19336
        return [
 
19337
                'M', anchorX, anchorY,
 
19338
                'L', x, y + h,
 
19339
                x, y,
 
19340
                x + w, y,
 
19341
                x + w, y + h,
 
19342
                x, y + h,
 
19343
                'M', anchorX, anchorY,
 
19344
                'Z'
 
19345
        ];
 
19346
};
 
19347
 
 
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) {
 
19351
 
 
19352
                var anchorX = options && options.anchorX,
 
19353
                        anchorY = options &&  options.anchorY,
 
19354
                        path = symbols[shape](x, y, w, h),
 
19355
                        labelTopOrBottomY;
 
19356
 
 
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);
 
19362
                }
 
19363
 
 
19364
                return path;
 
19365
        };
 
19366
});
 
19367
 
 
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];
 
19374
        });
 
19375
}
 
19376
 
 
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 },
 
19385
                stops: [
 
19386
                        [0, '#FFF'],
 
19387
                        [1, '#CCC']
 
19388
                ]
 
19389
        },
 
19390
        units = [].concat(defaultDataGroupingUnits), // copy
 
19391
        defaultSeriesType;
 
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
 
19395
 
 
19396
defaultSeriesType = seriesTypes.areaspline === UNDEFINED ? 'line' : 'areaspline';
 
19397
 
 
19398
extend(defaultOptions, {
 
19399
        navigator: {
 
19400
                //enabled: true,
 
19401
                handles: {
 
19402
                        backgroundColor: '#FFF',
 
19403
                        borderColor: '#666'
 
19404
                },
 
19405
                height: 40,
 
19406
                margin: 10,
 
19407
                maskFill: 'rgba(255, 255, 255, 0.75)',
 
19408
                outlineColor: '#444',
 
19409
                outlineWidth: 1,
 
19410
                series: {
 
19411
                        type: defaultSeriesType,
 
19412
                        color: '#4572A7',
 
19413
                        compare: null,
 
19414
                        fillOpacity: 0.4,
 
19415
                        dataGrouping: {
 
19416
                                approximation: 'average',
 
19417
                                enabled: true,
 
19418
                                groupPixelWidth: 2,
 
19419
                                smoothed: true,
 
19420
                                units: units
 
19421
                        },
 
19422
                        dataLabels: {
 
19423
                                enabled: false,
 
19424
                                zIndex: 2 // #1839
 
19425
                        },
 
19426
                        id: PREFIX + 'navigator-series',
 
19427
                        lineColor: '#4572A7',
 
19428
                        lineWidth: 1,
 
19429
                        marker: {
 
19430
                                enabled: false
 
19431
                        },
 
19432
                        pointRange: 0,
 
19433
                        shadow: false,
 
19434
                        threshold: null
 
19435
                },
 
19436
                //top: undefined,
 
19437
                xAxis: {
 
19438
                        tickWidth: 0,
 
19439
                        lineWidth: 0,
 
19440
                        gridLineWidth: 1,
 
19441
                        tickPixelInterval: 200,
 
19442
                        labels: {
 
19443
                                align: 'left',
 
19444
                                x: 3,
 
19445
                                y: -4
 
19446
                        },
 
19447
                        crosshair: false
 
19448
                },
 
19449
                yAxis: {
 
19450
                        gridLineWidth: 0,
 
19451
                        startOnTick: false,
 
19452
                        endOnTick: false,
 
19453
                        minPadding: 0.1,
 
19454
                        maxPadding: 0.1,
 
19455
                        labels: {
 
19456
                                enabled: false
 
19457
                        },
 
19458
                        crosshair: false,
 
19459
                        title: {
 
19460
                                text: null
 
19461
                        },
 
19462
                        tickWidth: 0
 
19463
                }
 
19464
        },
 
19465
        scrollbar: {
 
19466
                //enabled: true
 
19467
                height: isTouchDevice ? 20 : 14,
 
19468
                barBackgroundColor: buttonGradient,
 
19469
                barBorderRadius: 2,
 
19470
                barBorderWidth: 1,
 
19471
                barBorderColor: '#666',
 
19472
                buttonArrowColor: '#666',
 
19473
                buttonBackgroundColor: buttonGradient,
 
19474
                buttonBorderColor: '#666',
 
19475
                buttonBorderRadius: 2,
 
19476
                buttonBorderWidth: 1,
 
19477
                minWidth: 6,
 
19478
                rifleColor: '#666',
 
19479
                trackBackgroundColor: {
 
19480
                        linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 },
 
19481
                        stops: [
 
19482
                                [0, '#EEE'],
 
19483
                                [1, '#FFF']
 
19484
                        ]
 
19485
                },
 
19486
                trackBorderColor: '#CCC',
 
19487
                trackBorderWidth: 1,
 
19488
                // trackBorderRadius: 0
 
19489
                liveRedraw: hasSVG && !isTouchDevice
 
19490
        }
 
19491
});
 
19492
 
 
19493
/**
 
19494
 * The Scroller class
 
19495
 * @param {Object} chart
 
19496
 */
 
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;
 
19505
 
 
19506
 
 
19507
        this.handles = [];
 
19508
        this.scrollbarButtons = [];
 
19509
        this.elementsToDestroy = []; // Array containing the elements to destroy when Scroller is destroyed
 
19510
 
 
19511
        this.chart = chart;
 
19512
        this.setBaseSeries();
 
19513
 
 
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;
 
19521
 
 
19522
        // Run scroller
 
19523
        this.init();
 
19524
}
 
19525
 
 
19526
Scroller.prototype = {
 
19527
        /**
 
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
 
19531
         */
 
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,
 
19539
                        attr = {
 
19540
                                fill: handlesOptions.backgroundColor,
 
19541
                                stroke: handlesOptions.borderColor,
 
19542
                                'stroke-width': 1
 
19543
                        },
 
19544
                        tempElem;
 
19545
 
 
19546
                // create the elements
 
19547
                if (!scroller.rendered) {
 
19548
                        // the group
 
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
 
19552
                                .add();
 
19553
 
 
19554
                        // the rectangle
 
19555
                        tempElem = renderer.rect(-4.5, 0, 9, 16, 3, 1)
 
19556
                                .attr(attr)
 
19557
                                .add(handles[index]);
 
19558
                        elementsToDestroy.push(tempElem);
 
19559
 
 
19560
                        // the rifles
 
19561
                        tempElem = renderer.path([
 
19562
                                        'M',
 
19563
                                        -1.5, 4,
 
19564
                                        'L',
 
19565
                                        -1.5, 12,
 
19566
                                        'M',
 
19567
                                        0.5, 4,
 
19568
                                        'L',
 
19569
                                        0.5, 12
 
19570
                                ]).attr(attr)
 
19571
                                .add(handles[index]);
 
19572
                        elementsToDestroy.push(tempElem);
 
19573
                }
 
19574
 
 
19575
                // Place it
 
19576
                handles[index][chart.isResizing ? 'animate' : 'attr']({
 
19577
                        translateX: scroller.scrollerLeft + scroller.scrollbarHeight + parseInt(x, 10),
 
19578
                        translateY: scroller.top + scroller.height / 2 - 8
 
19579
                });
 
19580
        },
 
19581
 
 
19582
        /**
 
19583
         * Draw the scrollbar buttons with arrows
 
19584
         * @param {Number} index 0 is left, 1 is right
 
19585
         */
 
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,
 
19594
                        tempElem;
 
19595
 
 
19596
                if (!scroller.rendered) {
 
19597
                        scrollbarButtons[index] = renderer.g().add(scroller.scrollbarGroup);
 
19598
 
 
19599
                        tempElem = renderer.rect(
 
19600
                                        -0.5,
 
19601
                                        -0.5,
 
19602
                                        scrollbarHeight + 1, // +1 to compensate for crispifying in rect method
 
19603
                                        scrollbarHeight + 1,
 
19604
                                        scrollbarOptions.buttonBorderRadius,
 
19605
                                        scrollbarOptions.buttonBorderWidth
 
19606
                                ).attr({
 
19607
                                        stroke: scrollbarOptions.buttonBorderColor,
 
19608
                                        'stroke-width': scrollbarOptions.buttonBorderWidth,
 
19609
                                        fill: scrollbarOptions.buttonBackgroundColor
 
19610
                                }).add(scrollbarButtons[index]);
 
19611
                        elementsToDestroy.push(tempElem);
 
19612
 
 
19613
                        tempElem = renderer.path([
 
19614
                                        'M',
 
19615
                                        scrollbarHeight / 2 + (index ? -1 : 1), scrollbarHeight / 2 - 3,
 
19616
                                        'L',
 
19617
                                        scrollbarHeight / 2 + (index ? -1 : 1), scrollbarHeight / 2 + 3,
 
19618
                                        scrollbarHeight / 2 + (index ? 2 : -2), scrollbarHeight / 2
 
19619
                                ]).attr({
 
19620
                                        fill: scrollbarOptions.buttonArrowColor
 
19621
                                }).add(scrollbarButtons[index]);
 
19622
                        elementsToDestroy.push(tempElem);
 
19623
                }
 
19624
 
 
19625
                // adjust the right side button to the varying length of the scroll track
 
19626
                if (index) {
 
19627
                        scrollbarButtons[index].attr({
 
19628
                                translateX: scroller.scrollerWidth - scrollbarHeight
 
19629
                        });
 
19630
                }
 
19631
        },
 
19632
 
 
19633
        /**
 
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
 
19639
         */
 
19640
        render: function (min, max, pxMin, pxMax) {
 
19641
                var scroller = this,
 
19642
                        chart = scroller.chart,
 
19643
                        renderer = chart.renderer,
 
19644
                        navigatorLeft,
 
19645
                        navigatorWidth,
 
19646
                        scrollerLeft,
 
19647
                        scrollerWidth,
 
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,
 
19663
                        zoomedMin,
 
19664
                        zoomedMax,
 
19665
                        range,
 
19666
                        scrX,
 
19667
                        scrWidth,
 
19668
                        scrollbarPad = 0,
 
19669
                        outlineHeight = scroller.outlineHeight,
 
19670
                        barBorderRadius = scrollbarOptions.barBorderRadius,
 
19671
                        strokeWidth,
 
19672
                        scrollbarStrokeWidth = scrollbarOptions.barBorderWidth,
 
19673
                        centerBarX,
 
19674
                        outlineTop = top + halfOutline,
 
19675
                        verb,
 
19676
                        unionExtremes;
 
19677
 
 
19678
                // don't render the navigator until we have data (#486)
 
19679
                if (isNaN(min)) {
 
19680
                        return;
 
19681
                }
 
19682
 
 
19683
                scroller.navigatorLeft = navigatorLeft = pick(
 
19684
                        xAxis.left,
 
19685
                        chart.plotLeft + scrollbarHeight // in case of scrollbar only, without navigator
 
19686
                );
 
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;
 
19690
 
 
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);
 
19696
 
 
19697
                        if (unionExtremes && (unionExtremes.dataMin !== xAxis.min || unionExtremes.dataMax !== xAxis.max)) {
 
19698
                                xAxis.setExtremes(unionExtremes.dataMin, unionExtremes.dataMax, true, false);
 
19699
                        }
 
19700
                }
 
19701
 
 
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)
 
19706
                        pxMin = 0;
 
19707
                        pxMax = scrollerWidth;
 
19708
                }
 
19709
 
 
19710
                // Are we below the minRange? (#2618)
 
19711
                if (xAxis.translate(pxMax, true) - xAxis.translate(pxMin, true) < chart.xAxis[0].minRange) {
 
19712
                        return;
 
19713
                }
 
19714
 
 
19715
 
 
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;
 
19724
 
 
19725
 
 
19726
 
 
19727
                // on first render, create all elements
 
19728
                if (!scroller.rendered) {
 
19729
 
 
19730
                        if (navigatorEnabled) {
 
19731
 
 
19732
                                // draw the navigator group
 
19733
                                scroller.navigatorGroup = navigatorGroup = renderer.g('navigator')
 
19734
                                        .attr({
 
19735
                                                zIndex: 3
 
19736
                                        })
 
19737
                                        .add();
 
19738
 
 
19739
                                scroller.leftShade = renderer.rect()
 
19740
                                        .attr({
 
19741
                                                fill: navigatorOptions.maskFill
 
19742
                                        }).add(navigatorGroup);
 
19743
                                scroller.rightShade = renderer.rect()
 
19744
                                        .attr({
 
19745
                                                fill: navigatorOptions.maskFill
 
19746
                                        }).add(navigatorGroup);
 
19747
                                scroller.outline = renderer.path()
 
19748
                                        .attr({
 
19749
                                                'stroke-width': outlineWidth,
 
19750
                                                stroke: navigatorOptions.outlineColor
 
19751
                                        })
 
19752
                                        .add(navigatorGroup);
 
19753
                        }
 
19754
 
 
19755
                        if (scrollbarEnabled) {
 
19756
 
 
19757
                                // draw the scrollbar group
 
19758
                                scroller.scrollbarGroup = scrollbarGroup = renderer.g('scrollbar').add();
 
19759
 
 
19760
                                // the scrollbar track
 
19761
                                strokeWidth = scrollbarOptions.trackBorderWidth;
 
19762
                                scroller.scrollbarTrack = scrollbarTrack = renderer.rect().attr({
 
19763
                                        x: 0,
 
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);
 
19771
 
 
19772
                                // the scrollbar itself
 
19773
                                scroller.scrollbar = scrollbar = renderer.rect()
 
19774
                                        .attr({
 
19775
                                                y: -scrollbarStrokeWidth % 2 / 2,
 
19776
                                                height: scrollbarHeight,
 
19777
                                                fill: scrollbarOptions.barBackgroundColor,
 
19778
                                                stroke: scrollbarOptions.barBorderColor,
 
19779
                                                'stroke-width': scrollbarStrokeWidth,
 
19780
                                                r: barBorderRadius
 
19781
                                        })
 
19782
                                        .add(scrollbarGroup);
 
19783
 
 
19784
                                scroller.scrollbarRifles = renderer.path()
 
19785
                                        .attr({
 
19786
                                                stroke: scrollbarOptions.rifleColor,
 
19787
                                                'stroke-width': 1
 
19788
                                        })
 
19789
                                        .add(scrollbarGroup);
 
19790
                        }
 
19791
                }
 
19792
 
 
19793
                // place elements
 
19794
                verb = chart.isResizing ? 'animate' : 'attr';
 
19795
 
 
19796
                if (navigatorEnabled) {
 
19797
                        scroller.leftShade[verb]({
 
19798
                                x: navigatorLeft,
 
19799
                                y: top,
 
19800
                                width: zoomedMin,
 
19801
                                height: height
 
19802
                        });
 
19803
                        scroller.rightShade[verb]({
 
19804
                                x: navigatorLeft + zoomedMax,
 
19805
                                y: top,
 
19806
                                width: navigatorWidth - zoomedMax,
 
19807
                                height: height
 
19808
                        });
 
19809
                        scroller.outline[verb]({ d: [
 
19810
                                M,
 
19811
                                scrollerLeft, outlineTop, // left
 
19812
                                L,
 
19813
                                navigatorLeft + zoomedMin + halfOutline, outlineTop, // upper left of zoomed range
 
19814
                                navigatorLeft + zoomedMin + halfOutline, outlineTop + outlineHeight - scrollbarHeight, // lower left of z.r.
 
19815
                                M,
 
19816
                                navigatorLeft + zoomedMax - halfOutline, outlineTop + outlineHeight - scrollbarHeight, // lower right of z.r.
 
19817
                                L,
 
19818
                                navigatorLeft + zoomedMax - halfOutline, outlineTop, // upper right of z.r.
 
19819
                                scrollerLeft + scrollerWidth, outlineTop // right
 
19820
                        ]});
 
19821
                        // draw handles
 
19822
                        scroller.drawHandle(zoomedMin + halfOutline, 0);
 
19823
                        scroller.drawHandle(zoomedMax + halfOutline, 1);
 
19824
                }
 
19825
 
 
19826
                // draw the scrollbar
 
19827
                if (scrollbarEnabled && scrollbarGroup) {
 
19828
 
 
19829
                        // draw the buttons
 
19830
                        scroller.drawScrollbarButton(0);
 
19831
                        scroller.drawScrollbarButton(1);
 
19832
 
 
19833
                        scrollbarGroup[verb]({
 
19834
                                translateX: scrollerLeft,
 
19835
                                translateY: mathRound(outlineTop + height)
 
19836
                        });
 
19837
 
 
19838
                        scrollbarTrack[verb]({
 
19839
                                width: scrollerWidth
 
19840
                        });
 
19841
 
 
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;
 
19849
                        }
 
19850
                        scroller.scrollbarPad = scrollbarPad;
 
19851
                        scrollbar[verb]({
 
19852
                                x: mathFloor(scrX) + (scrollbarStrokeWidth % 2 / 2),
 
19853
                                width: scrWidth
 
19854
                        });
 
19855
 
 
19856
                        centerBarX = scrollbarHeight + zoomedMin + range / 2 - 0.5;
 
19857
 
 
19858
                        scroller.scrollbarRifles
 
19859
                                .attr({
 
19860
                                        visibility: range > 12 ? VISIBLE : HIDDEN
 
19861
                                })[verb]({
 
19862
                                        d: [
 
19863
                                                M,
 
19864
                                                centerBarX - 3, scrollbarHeight / 4,
 
19865
                                                L,
 
19866
                                                centerBarX - 3, 2 * scrollbarHeight / 3,
 
19867
                                                M,
 
19868
                                                centerBarX, scrollbarHeight / 4,
 
19869
                                                L,
 
19870
                                                centerBarX, 2 * scrollbarHeight / 3,
 
19871
                                                M,
 
19872
                                                centerBarX + 3, scrollbarHeight / 4,
 
19873
                                                L,
 
19874
                                                centerBarX + 3, 2 * scrollbarHeight / 3
 
19875
                                        ]
 
19876
                                });
 
19877
                }
 
19878
 
 
19879
                scroller.scrollbarPad = scrollbarPad;
 
19880
                scroller.rendered = true;
 
19881
        },
 
19882
 
 
19883
        /**
 
19884
         * Set up the mouse and touch events for the navigator and scrollbar
 
19885
         */
 
19886
        addEvents: function () {
 
19887
                var container = this.chart.container,
 
19888
                        mouseDownHandler = this.mouseDownHandler,
 
19889
                        mouseMoveHandler = this.mouseMoveHandler,
 
19890
                        mouseUpHandler = this.mouseUpHandler,
 
19891
                        _events;
 
19892
 
 
19893
                // Mouse events
 
19894
                _events = [
 
19895
                        [container, 'mousedown', mouseDownHandler],
 
19896
                        [container, 'mousemove', mouseMoveHandler],
 
19897
                        [document, 'mouseup', mouseUpHandler]
 
19898
                ];
 
19899
 
 
19900
                // Touch events
 
19901
                if (hasTouch) {
 
19902
                        _events.push(
 
19903
                                [container, 'touchstart', mouseDownHandler],
 
19904
                                [container, 'touchmove', mouseMoveHandler],
 
19905
                                [document, 'touchend', mouseUpHandler]
 
19906
                        );
 
19907
                }
 
19908
 
 
19909
                // Add them all
 
19910
                each(_events, function (args) {
 
19911
                        addEvent.apply(null, args);
 
19912
                });
 
19913
                this._events = _events;
 
19914
        },
 
19915
 
 
19916
        /**
 
19917
         * Removes the event handlers attached previously with addEvents.
 
19918
         */
 
19919
        removeEvents: function () {
 
19920
 
 
19921
                each(this._events, function (args) {
 
19922
                        removeEvent.apply(null, args);
 
19923
                });
 
19924
                this._events = UNDEFINED;
 
19925
                if (this.navigatorEnabled && this.baseSeries) {
 
19926
                        removeEvent(this.baseSeries, 'updatedData', this.updatedDataHandler);
 
19927
                }
 
19928
        },
 
19929
 
 
19930
        /**
 
19931
         * Initiate the Scroller object
 
19932
         */
 
19933
        init: function () {
 
19934
                var scroller = this,
 
19935
                        chart = scroller.chart,
 
19936
                        xAxis,
 
19937
                        yAxis,
 
19938
                        scrollbarHeight = scroller.scrollbarHeight,
 
19939
                        navigatorOptions = scroller.navigatorOptions,
 
19940
                        height = scroller.height,
 
19941
                        top = scroller.top,
 
19942
                        dragOffset,
 
19943
                        hasDragged,
 
19944
                        bodyStyle = document.body.style,
 
19945
                        defaultBodyCursor,
 
19946
                        baseSeries = scroller.baseSeries;
 
19947
 
 
19948
                /**
 
19949
                 * Event handler for the mouse down event.
 
19950
                 */
 
19951
                scroller.mouseDownHandler = function (e) {
 
19952
                        e = chart.pointer.normalize(e);
 
19953
 
 
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,
 
19964
                                chartX = e.chartX,
 
19965
                                chartY = e.chartY,
 
19966
                                baseXAxis = chart.xAxis[0],
 
19967
                                fixedMax,
 
19968
                                ext,
 
19969
                                handleSensitivity = isTouchDevice ? 10 : 7,
 
19970
                                left,
 
19971
                                isOnNavigator;
 
19972
 
 
19973
                        if (chartY > top && chartY < top + height + scrollbarHeight) { // we're vertically inside the navigator
 
19974
                                isOnNavigator = !scroller.scrollbarEnabled || chartY < top + height;
 
19975
 
 
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;
 
19982
 
 
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;
 
19989
 
 
19990
                                // grab the zoomed range
 
19991
                                } else if (chartX > navigatorLeft + zoomedMin - scrollbarPad && chartX < navigatorLeft + zoomedMax + scrollbarPad) {
 
19992
                                        scroller.grabbedCenter = chartX;
 
19993
                                        scroller.fixedWidth = range;
 
19994
 
 
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';
 
20000
                                        }
 
20001
 
 
20002
                                        dragOffset = chartX - zoomedMin;
 
20003
 
 
20004
 
 
20005
                                // shift the range by clicking on shaded areas, scrollbar track or scrollbar buttons
 
20006
                                } else if (chartX > scrollerLeft && chartX < scrollerLeft + scrollerWidth) {
 
20007
 
 
20008
                                        // Center around the clicked point
 
20009
                                        if (isOnNavigator) {
 
20010
                                                left = chartX - navigatorLeft - range / 2;
 
20011
                                        
 
20012
                                        // Click on scrollbar
 
20013
                                        } else {
 
20014
 
 
20015
                                                // Click left scrollbar button
 
20016
                                                if (chartX < navigatorLeft) { 
 
20017
                                                        left = zoomedMin - range * 0.2;
 
20018
 
 
20019
                                                // Click right scrollbar button
 
20020
                                                } else if (chartX > scrollerLeft + scrollerWidth - scrollbarHeight) {
 
20021
                                                        left = zoomedMin + range * 0.2;
 
20022
                                                
 
20023
                                                // Click on scrollbar track, shift the scrollbar by one range
 
20024
                                                } else {
 
20025
                                                        left = chartX < navigatorLeft + zoomedMin ? // on the left
 
20026
                                                                zoomedMin - range :
 
20027
                                                                zoomedMax;
 
20028
                                                }
 
20029
                                        }
 
20030
                                        if (left < 0) {
 
20031
                                                left = 0;
 
20032
                                        } else if (left + range >= navigatorWidth) {
 
20033
                                                left = navigatorWidth - range;
 
20034
                                                fixedMax = xAxis.dataMax; // #2293
 
20035
                                        }
 
20036
                                        if (left !== zoomedMin) { // it has actually moved
 
20037
                                                scroller.fixedWidth = range; // #1370
 
20038
 
 
20039
                                                ext = xAxis.toFixedRange(left, left + range, null, fixedMax);
 
20040
                                                baseXAxis.setExtremes(
 
20041
                                                        ext.min,
 
20042
                                                        ext.max,
 
20043
                                                        true,
 
20044
                                                        false,
 
20045
                                                        { trigger: 'navigator' }
 
20046
                                                );
 
20047
                                        }
 
20048
                                }
 
20049
 
 
20050
                        }
 
20051
                };
 
20052
 
 
20053
                /**
 
20054
                 * Event handler for the mouse move event.
 
20055
                 */
 
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,
 
20063
                                chartX;
 
20064
 
 
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) {
 
20068
 
 
20069
                                e = chart.pointer.normalize(e);
 
20070
                                chartX = e.chartX;
 
20071
 
 
20072
                                // validation for handle dragging
 
20073
                                if (chartX < navigatorLeft) {
 
20074
                                        chartX = navigatorLeft;
 
20075
                                } else if (chartX > scrollerLeft + scrollerWidth - scrollbarHeight) {
 
20076
                                        chartX = scrollerLeft + scrollerWidth - scrollbarHeight;
 
20077
                                }
 
20078
 
 
20079
                                // drag left handle
 
20080
                                if (scroller.grabbedLeft) {
 
20081
                                        hasDragged = true;
 
20082
                                        scroller.render(0, 0, chartX - navigatorLeft, scroller.otherHandlePos);
 
20083
 
 
20084
                                // drag right handle
 
20085
                                } else if (scroller.grabbedRight) {
 
20086
                                        hasDragged = true;
 
20087
                                        scroller.render(0, 0, scroller.otherHandlePos, chartX - navigatorLeft);
 
20088
 
 
20089
                                // drag scrollbar or open area in navigator
 
20090
                                } else if (scroller.grabbedCenter) {
 
20091
 
 
20092
                                        hasDragged = true;
 
20093
                                        if (chartX < dragOffset) { // outside left
 
20094
                                                chartX = dragOffset;
 
20095
                                        } else if (chartX > navigatorWidth + dragOffset - range) { // outside right
 
20096
                                                chartX = navigatorWidth + dragOffset - range;
 
20097
                                        }
 
20098
 
 
20099
                                        scroller.render(0, 0, chartX - dragOffset, chartX - dragOffset + range);
 
20100
 
 
20101
                                }
 
20102
                                if (hasDragged && scroller.scrollbarOptions.liveRedraw) {
 
20103
                                        setTimeout(function () {
 
20104
                                                scroller.mouseUpHandler(e);
 
20105
                                        }, 0);
 
20106
                                }
 
20107
                        }
 
20108
                };
 
20109
 
 
20110
                /**
 
20111
                 * Event handler for the mouse up event.
 
20112
                 */
 
20113
                scroller.mouseUpHandler = function (e) {
 
20114
                        var ext,
 
20115
                                fixedMin,
 
20116
                                fixedMax;
 
20117
 
 
20118
                        if (hasDragged) {
 
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;
 
20124
                                }
 
20125
                                
 
20126
                                ext = xAxis.toFixedRange(scroller.zoomedMin, scroller.zoomedMax, fixedMin, fixedMax);
 
20127
                                chart.xAxis[0].setExtremes(
 
20128
                                        ext.min,
 
20129
                                        ext.max,
 
20130
                                        true,
 
20131
                                        false,
 
20132
                                        {
 
20133
                                                trigger: 'navigator',
 
20134
                                                triggerOp: 'navigator-drag',
 
20135
                                                DOMEvent: e // #1838
 
20136
                                        }
 
20137
                                );
 
20138
                        }
 
20139
 
 
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 || '';
 
20144
                        }
 
20145
 
 
20146
                };
 
20147
 
 
20148
 
 
20149
 
 
20150
                var xAxisIndex = chart.xAxis.length,
 
20151
                        yAxisIndex = chart.yAxis.length;
 
20152
 
 
20153
                // make room below the chart
 
20154
                chart.extraBottomMargin = scroller.outlineHeight + navigatorOptions.margin;
 
20155
 
 
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',
 
20162
                                isX: true,
 
20163
                                type: 'datetime',
 
20164
                                index: xAxisIndex,
 
20165
                                height: height,
 
20166
                                offset: 0,
 
20167
                                offsetLeft: scrollbarHeight,
 
20168
                                offsetRight: -scrollbarHeight,
 
20169
                                keepOrdinalPadding: true, // #2436
 
20170
                                startOnTick: false,
 
20171
                                endOnTick: false,
 
20172
                                minPadding: 0,
 
20173
                                maxPadding: 0,
 
20174
                                zoomEnabled: false
 
20175
                        }));
 
20176
 
 
20177
                        scroller.yAxis = yAxis = new Axis(chart, merge(navigatorOptions.yAxis, {
 
20178
                                id: 'navigator-y-axis',
 
20179
                                alignTicks: false,
 
20180
                                height: height,
 
20181
                                offset: 0,
 
20182
                                index: yAxisIndex,
 
20183
                                zoomEnabled: false
 
20184
                        }));
 
20185
 
 
20186
                        // If we have a base series, initialize the navigator series
 
20187
                        if (baseSeries || navigatorOptions.series.data) {
 
20188
                                scroller.addBaseSeries();
 
20189
 
 
20190
                        // If not, set up an event to listen for added series
 
20191
                        } else if (chart.series.length === 0) {
 
20192
 
 
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
 
20198
                                        }
 
20199
                                        proceed.call(chart, animation);
 
20200
                                });
 
20201
                        }
 
20202
 
 
20203
 
 
20204
                // in case of scrollbar only, fake an x axis to get translation
 
20205
                } else {
 
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;
 
20212
 
 
20213
                                        return reverse ?
 
20214
                                                // from pixel to value
 
20215
                                                (value * valueRange / scrollTrackWidth) + dataMin :
 
20216
                                                // from value to pixel
 
20217
                                                scrollTrackWidth * (value - dataMin) / valueRange;
 
20218
                                },
 
20219
                                toFixedRange: Axis.prototype.toFixedRange
 
20220
                        };
 
20221
                }
 
20222
 
 
20223
 
 
20224
                /**
 
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.
 
20227
                 */
 
20228
                wrap(chart, 'getMargins', function (proceed) {
 
20229
 
 
20230
                        var legend = this.legend,
 
20231
                                legendOptions = legend.options;
 
20232
 
 
20233
                        proceed.call(this);
 
20234
 
 
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);
 
20240
 
 
20241
                        if (xAxis && yAxis) { // false if navigator is disabled (#904)
 
20242
 
 
20243
                                xAxis.options.top = yAxis.options.top = top;
 
20244
 
 
20245
                                xAxis.setAxisSize();
 
20246
                                yAxis.setAxisSize();
 
20247
                        }
 
20248
                });
 
20249
 
 
20250
 
 
20251
                scroller.addEvents();
 
20252
        },
 
20253
 
 
20254
        /**
 
20255
         * Get the union data extremes of the chart - the outer data extremes of the base
 
20256
         * X axis and the navigator axis.
 
20257
         */
 
20258
        getUnionExtremes: function (returnFalseOnNoBaseSeries) {
 
20259
                var baseAxis = this.chart.xAxis[0],
 
20260
                        navAxis = this.xAxis,
 
20261
                        navAxisOptions = navAxis.options;
 
20262
 
 
20263
                if (!returnFalseOnNoBaseSeries || baseAxis.dataMin !== null) {
 
20264
                        return {
 
20265
                                dataMin: pick(
 
20266
                                        navAxisOptions && navAxisOptions.min,
 
20267
                                        ((defined(baseAxis.dataMin) && defined(navAxis.dataMin)) ? mathMin : pick)(baseAxis.dataMin, navAxis.dataMin)
 
20268
                                ),
 
20269
                                dataMax: pick(
 
20270
                                        navAxisOptions && navAxisOptions.max,
 
20271
                                        ((defined(baseAxis.dataMax) && defined(navAxis.dataMax)) ? mathMax : pick)(baseAxis.dataMax, navAxis.dataMax)
 
20272
                                )
 
20273
                        };
 
20274
                }
 
20275
 
 
20276
        },
 
20277
 
 
20278
        /**
 
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
 
20281
         */
 
20282
        setBaseSeries: function (baseSeriesOption) {
 
20283
                var chart = this.chart;
 
20284
 
 
20285
                baseSeriesOption = baseSeriesOption || chart.options.navigator.baseSeries;
 
20286
 
 
20287
                // If we're resetting, remove the existing series
 
20288
                if (this.series) {
 
20289
                        this.series.remove();
 
20290
                }
 
20291
 
 
20292
                // Set the new base series
 
20293
                this.baseSeries = chart.series[baseSeriesOption] ||
 
20294
                        (typeof baseSeriesOption === 'string' && chart.get(baseSeriesOption)) ||
 
20295
                        chart.series[0];
 
20296
 
 
20297
                // When run after render, this.xAxis already exists
 
20298
                if (this.xAxis) {
 
20299
                        this.addBaseSeries();
 
20300
                }
 
20301
        },
 
20302
 
 
20303
        addBaseSeries: function () {
 
20304
                var baseSeries = this.baseSeries,
 
20305
                        baseOptions = baseSeries ? baseSeries.options : {},
 
20306
                        baseData = baseOptions.data,
 
20307
                        mergedNavSeriesOptions,
 
20308
                        navigatorSeriesOptions = this.navigatorOptions.series,
 
20309
                        navigatorData;
 
20310
 
 
20311
                // remove it to prevent merging one by one
 
20312
                navigatorData = navigatorSeriesOptions.data;
 
20313
                this.hasNavigatorData = !!navigatorData;
 
20314
 
 
20315
                // Merge the series options
 
20316
                mergedNavSeriesOptions = merge(baseOptions, navigatorSeriesOptions, {
 
20317
                        clip: false,
 
20318
                        enableMouseTracking: false,
 
20319
                        group: 'nav', // for columns
 
20320
                        padXAxis: false,
 
20321
                        xAxis: 'navigator-x-axis',
 
20322
                        yAxis: 'navigator-y-axis',
 
20323
                        name: 'Navigator',
 
20324
                        showInLegend: false,
 
20325
                        isInternal: true,
 
20326
                        visible: true
 
20327
                });
 
20328
 
 
20329
                // set the data back
 
20330
                mergedNavSeriesOptions.data = navigatorData || baseData;
 
20331
 
 
20332
                // add the series
 
20333
                this.series = this.chart.initSeries(mergedNavSeriesOptions);
 
20334
 
 
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 });
 
20341
 
 
20342
                }
 
20343
        },
 
20344
 
 
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,
 
20355
                        stickToMin,
 
20356
                        stickToMax,
 
20357
                        newMax,
 
20358
                        newMin,
 
20359
                        doRedraw,
 
20360
                        navigatorSeries = scroller.series,
 
20361
                        navXData = navigatorSeries.xData,
 
20362
                        hasSetExtremes = !!baseXAxis.setExtremes;
 
20363
 
 
20364
                // detect whether to move the range
 
20365
                stickToMax = baseMax >= navXData[navXData.length - 1] - (this.closestPointRange || 0); // #570
 
20366
                stickToMin = baseMin <= baseDataMin;
 
20367
 
 
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);
 
20372
                        doRedraw = true;
 
20373
                }
 
20374
 
 
20375
                // if the zoomed range is already at the min, move it to the right as new data
 
20376
                // comes in
 
20377
                if (stickToMin) {
 
20378
                        newMin = baseDataMin;
 
20379
                        newMax = newMin + range;
 
20380
                }
 
20381
 
 
20382
                // if the zoomed range is already at the max, move it to the right as new data
 
20383
                // comes in
 
20384
                if (stickToMax) {
 
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]);
 
20388
                        }
 
20389
                }
 
20390
 
 
20391
                // update the extremes
 
20392
                if (hasSetExtremes && (stickToMin || stickToMax)) {
 
20393
                        if (!isNaN(newMin)) {
 
20394
                                baseXAxis.setExtremes(newMin, newMax, true, false, { trigger: 'updatedData' });
 
20395
                        }
 
20396
 
 
20397
                // if it is not at any edge, just move the scroller window to reflect the new series data
 
20398
                } else {
 
20399
                        if (doRedraw) {
 
20400
                                this.chart.redraw(false);
 
20401
                        }
 
20402
 
 
20403
                        scroller.render(
 
20404
                                mathMax(baseMin, baseDataMin),
 
20405
                                mathMin(baseMax, baseDataMax)
 
20406
                        );
 
20407
                }
 
20408
        },
 
20409
 
 
20410
        /**
 
20411
         * Destroys allocated elements.
 
20412
         */
 
20413
        destroy: function () {
 
20414
                var scroller = this;
 
20415
 
 
20416
                // Disconnect events added in addEvents
 
20417
                scroller.removeEvents();
 
20418
 
 
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) {
 
20422
                                prop.destroy();
 
20423
                        }
 
20424
                });
 
20425
                scroller.xAxis = scroller.yAxis = scroller.leftShade = scroller.rightShade = scroller.outline = scroller.scrollbarTrack = scroller.scrollbarRifles = scroller.scrollbarGroup = scroller.scrollbar = null;
 
20426
 
 
20427
                // Destroy elements in collection
 
20428
                each([scroller.scrollbarButtons, scroller.handles, scroller.elementsToDestroy], function (coll) {
 
20429
                        destroyObjectProperties(coll);
 
20430
                });
 
20431
        }
 
20432
};
 
20433
 
 
20434
Highcharts.Scroller = Scroller;
 
20435
 
 
20436
 
 
20437
/**
 
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.
 
20440
 */
 
20441
wrap(Axis.prototype, 'zoom', function (proceed, newMin, newMax) {
 
20442
        var chart = this.chart,
 
20443
                chartOptions = chart.options,
 
20444
                zoomType = chartOptions.chart.zoomType,
 
20445
                previousZoom,
 
20446
                navigator = chartOptions.navigator,
 
20447
                rangeSelector = chartOptions.rangeSelector,
 
20448
                ret;
 
20449
 
 
20450
        if (this.isXAxis && ((navigator && navigator.enabled) ||
 
20451
                        (rangeSelector && rangeSelector.enabled))) {
 
20452
 
 
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';
 
20457
 
 
20458
                // For y only zooming, ignore the X axis completely
 
20459
                } else if (zoomType === 'y') {
 
20460
                        ret = false;
 
20461
 
 
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;
 
20472
                        }
 
20473
                }
 
20474
 
 
20475
        }
 
20476
        return ret !== UNDEFINED ? ret : proceed.call(this, newMin, newMax);
 
20477
});
 
20478
 
 
20479
// Initialize scroller for stock charts
 
20480
wrap(Chart.prototype, 'init', function (proceed, options, callback) {
 
20481
 
 
20482
        addEvent(this, 'beforeRender', function () {
 
20483
                var options = this.options;
 
20484
                if (options.navigator.enabled || options.scrollbar.enabled) {
 
20485
                        this.scroller = new Scroller(this);
 
20486
                }
 
20487
        });
 
20488
 
 
20489
        proceed.call(this, options, callback);
 
20490
 
 
20491
});
 
20492
 
 
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) {
 
20497
                error(20, true);
 
20498
        }
 
20499
        proceed.call(this, options, redraw, shift, animation);
 
20500
});
 
20501
 
 
20502
/* ****************************************************************************
 
20503
 * End Scroller code                                                                                                              *
 
20504
 *****************************************************************************/
 
20505
/* ****************************************************************************
 
20506
 * Start Range Selector code                                                                                              *
 
20507
 *****************************************************************************/
 
20508
extend(defaultOptions, {
 
20509
        rangeSelector: {
 
20510
                // enabled: true,
 
20511
                // buttons: {Object}
 
20512
                // buttonSpacing: 0,
 
20513
                buttonTheme: {
 
20514
                        width: 28,
 
20515
                        height: 16,
 
20516
                        padding: 1,
 
20517
                        r: 0,
 
20518
                        stroke: '#68A',
 
20519
                        zIndex: 7 // #484, #852
 
20520
                //      states: {
 
20521
                //              hover: {},
 
20522
                //              select: {}
 
20523
                // }
 
20524
                },
 
20525
                inputPosition: {
 
20526
                        align: 'right'
 
20527
                },
 
20528
                // inputDateFormat: '%b %e, %Y',
 
20529
                // inputEditDateFormat: '%Y-%m-%d',
 
20530
                // inputEnabled: true,
 
20531
                //inputStyle: {},
 
20532
                labelStyle: {
 
20533
                        color: '#666'
 
20534
                }
 
20535
                // selected: undefined
 
20536
        }
 
20537
});
 
20538
defaultOptions.lang = merge(defaultOptions.lang, {
 
20539
        rangeSelectorZoom: 'Zoom',
 
20540
        rangeSelectorFrom: 'From',
 
20541
        rangeSelectorTo: 'To'
 
20542
});
 
20543
 
 
20544
/**
 
20545
 * The object constructor for the range selector
 
20546
 * @param {Object} chart
 
20547
 */
 
20548
function RangeSelector(chart) {
 
20549
 
 
20550
        // Run RangeSelector
 
20551
        this.init(chart);
 
20552
}
 
20553
 
 
20554
RangeSelector.prototype = {
 
20555
        /**
 
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
 
20560
         */
 
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,
 
20571
                        newMin,
 
20572
                        newMax = baseAxis && mathRound(mathMin(baseAxis.max, pick(dataMax, baseAxis.max))), // #1568
 
20573
                        now,
 
20574
                        date = new Date(newMax),
 
20575
                        type = rangeOptions.type,
 
20576
                        count = rangeOptions.count,
 
20577
                        baseXAxisOptions,
 
20578
                        range = rangeOptions._range,
 
20579
                        rangeMin,
 
20580
                        year,
 
20581
                        timeName;
 
20582
 
 
20583
                if (dataMin === null || dataMax === null || // chart has no data, base series is removed
 
20584
                                i === rangeSelector.selected) { // same button is clicked twice
 
20585
                        return;
 
20586
                }
 
20587
 
 
20588
                if (type === 'month' || type === 'year') {
 
20589
                        timeName = { month: 'Month', year: 'FullYear'}[type];
 
20590
                        date['set' + timeName](date['get' + timeName]() - count);
 
20591
 
 
20592
                        newMin = date.getTime();
 
20593
                        dataMin = pick(dataMin, Number.MIN_VALUE);
 
20594
                        if (isNaN(newMin) || newMin < dataMin) {
 
20595
                                newMin = dataMin;
 
20596
                                newMax = mathMin(newMin + range, dataMax);
 
20597
                        } else {
 
20598
                                range = newMax - newMin;
 
20599
                        }
 
20600
 
 
20601
                // Fixed times like minutes, hours, days
 
20602
                } else if (range) {
 
20603
                        newMin = mathMax(newMax - range, dataMin);
 
20604
                        newMax = mathMin(newMin + range, dataMax);
 
20605
                
 
20606
                } else if (type === 'ytd') {
 
20607
 
 
20608
                        // On user clicks on the buttons, or a delayed action running from the beforeRender 
 
20609
                        // event (below), the baseAxis is defined.
 
20610
                        if (baseAxis) {
 
20611
 
 
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
 
20615
                                // array (#942).
 
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);
 
20623
                                        });
 
20624
                                        redraw = false;
 
20625
                                }
 
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);
 
20631
 
 
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)
 
20634
                        } else {
 
20635
                                addEvent(chart, 'beforeRender', function () {
 
20636
                                        rangeSelector.clickButton(i);
 
20637
                                });
 
20638
                                return;
 
20639
                        }
 
20640
                } else if (type === 'all' && baseAxis) {
 
20641
                        newMin = dataMin;
 
20642
                        newMax = dataMax;
 
20643
                }
 
20644
 
 
20645
                // Deselect previous button
 
20646
                if (buttons[selected]) {
 
20647
                        buttons[selected].setState(0);
 
20648
                }
 
20649
                // Select this button
 
20650
                if (buttons[i]) {
 
20651
                        buttons[i].setState(2);
 
20652
                }
 
20653
 
 
20654
                chart.fixedRange = range;
 
20655
 
 
20656
                // update the chart
 
20657
                if (!baseAxis) { // axis not yet instanciated
 
20658
                        baseXAxisOptions = chart.options.xAxis;
 
20659
                        baseXAxisOptions[0] = merge(
 
20660
                                baseXAxisOptions[0],
 
20661
                                {
 
20662
                                        range: range,
 
20663
                                        min: rangeMin
 
20664
                                }
 
20665
                        );
 
20666
                        rangeSelector.setSelected(i);
 
20667
                } else { // existing axis object; after render time
 
20668
                        baseAxis.setExtremes(
 
20669
                                newMin,
 
20670
                                newMax,
 
20671
                                pick(redraw, 1),
 
20672
                                0, 
 
20673
                                { 
 
20674
                                        trigger: 'rangeSelectorButton',
 
20675
                                        rangeSelectorButton: rangeOptions
 
20676
                                }
 
20677
                        );
 
20678
                        rangeSelector.setSelected(i);
 
20679
                }
 
20680
        },
 
20681
 
 
20682
        /**
 
20683
         * Set the selected option. This method only sets the internal flag, it doesn't
 
20684
         * update the buttons or the actual zoomed range.
 
20685
         */
 
20686
        setSelected: function (selected) {
 
20687
                this.selected = this.options.selected = selected;
 
20688
        },
 
20689
        
 
20690
        /**
 
20691
         * The default buttons for pre-selecting time frames
 
20692
         */
 
20693
        defaultButtons: [{
 
20694
                type: 'month',
 
20695
                count: 1,
 
20696
                text: '1m'
 
20697
        }, {
 
20698
                type: 'month',
 
20699
                count: 3,
 
20700
                text: '3m'
 
20701
        }, {
 
20702
                type: 'month',
 
20703
                count: 6,
 
20704
                text: '6m'
 
20705
        }, {
 
20706
                type: 'ytd',
 
20707
                text: 'YTD'
 
20708
        }, {
 
20709
                type: 'year',
 
20710
                count: 1,
 
20711
                text: '1y'
 
20712
        }, {
 
20713
                type: 'all',
 
20714
                text: 'All'
 
20715
        }],
 
20716
 
 
20717
        /**
 
20718
         * Initialize the range selector
 
20719
         */
 
20720
        init: function (chart) {
 
20721
                
 
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;
 
20729
                                if (minInput) {
 
20730
                                        minInput.blur();
 
20731
                                }
 
20732
                                if (maxInput) {
 
20733
                                        maxInput.blur();
 
20734
                                }
 
20735
                        };
 
20736
 
 
20737
                rangeSelector.chart = chart;
 
20738
                rangeSelector.options = options;
 
20739
                rangeSelector.buttons = [];
 
20740
                
 
20741
                chart.extraTopMargin = 25;
 
20742
                rangeSelector.buttonOptions = buttonOptions;
 
20743
 
 
20744
                addEvent(chart.container, 'mousedown', blurInputs);
 
20745
                addEvent(chart, 'resize', blurInputs);
 
20746
 
 
20747
                // Extend the buttonOptions with actual range
 
20748
                each(buttonOptions, rangeSelector.computeButtonRange);
 
20749
 
 
20750
                // zoomed range based on a pre-selected button index
 
20751
                if (selectedOption !== UNDEFINED && buttonOptions[selectedOption]) {
 
20752
                        this.clickButton(selectedOption, false);
 
20753
                }
 
20754
 
 
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);
 
20759
                        });
 
20760
                });
 
20761
        },
 
20762
 
 
20763
        /**
 
20764
         * Dynamically update the range selector buttons after a new range has been set
 
20765
         */
 
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;
 
20775
 
 
20776
                if (updating && chart.fixedRange !== mathRound(baseAxis.max - baseAxis.min)) {
 
20777
                        if (buttons[selected]) {
 
20778
                                buttons[selected].setState(0);
 
20779
                        }
 
20780
                        rangeSelector.setSelected(null);
 
20781
                }
 
20782
 
 
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);
 
20794
 
 
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);
 
20801
                        
 
20802
                        } else if (isTooGreatRange || isTooSmallRange || isAllButAlreadyShowingAll || isYTDButNotAvailable) {
 
20803
                                buttons[i].setState(3);
 
20804
 
 
20805
                        } else if (buttons[i].state === 3) {
 
20806
                                buttons[i].setState(0);
 
20807
                        }
 
20808
                });
 
20809
        },
 
20810
 
 
20811
        /** 
 
20812
         * Compute and cache the range for an individual button
 
20813
         */
 
20814
        computeButtonRange: function (rangeOptions) {
 
20815
                var type = rangeOptions.type,
 
20816
                        count = rangeOptions.count || 1,
 
20817
 
 
20818
                        // these time intervals have a fixed number of milliseconds, as opposed
 
20819
                        // to month, ytd and year
 
20820
                        fixedTimes = {
 
20821
                                millisecond: 1,
 
20822
                                second: 1000,
 
20823
                                minute: 60 * 1000,
 
20824
                                hour: 3600 * 1000,
 
20825
                                day: 24 * 3600 * 1000,
 
20826
                                week: 7 * 24 * 3600 * 1000
 
20827
                        };
 
20828
                
 
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;
 
20834
                }
 
20835
        },
 
20836
        
 
20837
        /**
 
20838
         * Set the internal and displayed value of a HTML input for the dates
 
20839
         * @param {String} name
 
20840
         * @param {Number} time
 
20841
         */
 
20842
        setInputValue: function (name, time) {
 
20843
                var options = this.chart.options.rangeSelector;
 
20844
 
 
20845
                if (defined(time)) {
 
20846
                        this[name + 'Input'].HCTime = time;
 
20847
                }
 
20848
                
 
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) });
 
20851
        },
 
20852
 
 
20853
        /**
 
20854
         * Draw either the 'from' or the 'to' HTML input box of the range selector
 
20855
         * @param {Object} name
 
20856
         */
 
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',
 
20866
                        input,
 
20867
                        label,
 
20868
                        dateBox,
 
20869
                        inputGroup = this.inputGroup;
 
20870
 
 
20871
                // Create the text label
 
20872
                this[name + 'Label'] = label = renderer.label(lang[isMin ? 'rangeSelectorFrom' : 'rangeSelectorTo'], this.inputGroup.offset)
 
20873
                        .attr({
 
20874
                                padding: 1
 
20875
                        })
 
20876
                        .css(merge(chartStyle, options.labelStyle))
 
20877
                        .add(inputGroup);
 
20878
                inputGroup.offset += label.width + 5;
 
20879
                
 
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)
 
20883
                        .attr({
 
20884
                                padding: 1,
 
20885
                                width: options.inputBoxWidth || 90,
 
20886
                                height: options.inputBoxHeight || 16,
 
20887
                                stroke: options.inputBoxBorderColor || 'silver',
 
20888
                                'stroke-width': 1
 
20889
                        })
 
20890
                        .css(merge({
 
20891
                                textAlign: 'center'
 
20892
                        }, chartStyle, options.inputStyle))
 
20893
                        .on('click', function () {
 
20894
                                rangeSelector[name + 'Input'].focus();
 
20895
                        })
 
20896
                        .add(inputGroup);
 
20897
                inputGroup.offset += dateBox.width + (isMin ? 10 : 0);
 
20898
                
 
20899
 
 
20900
                // Create the HTML input element. This is rendered as 1x1 pixel then set to the right size 
 
20901
                // when focused.
 
20902
                this[name + 'Input'] = input = createElement('input', {
 
20903
                        name: name,
 
20904
                        className: PREFIX + 'range-selector',
 
20905
                        type: 'text'
 
20906
                }, extend({
 
20907
                        position: ABSOLUTE,
 
20908
                        border: 0,
 
20909
                        width: '1px', // Chrome needs a pixel to see it
 
20910
                        height: '1px',
 
20911
                        padding: 0,
 
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);
 
20917
 
 
20918
                // Blow up the input box
 
20919
                input.onfocus = function () {
 
20920
                        css(this, {
 
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'
 
20926
                        });
 
20927
                };
 
20928
                // Hide away the input box
 
20929
                input.onblur = function () {
 
20930
                        css(this, {
 
20931
                                border: 0,
 
20932
                                width: '1px',
 
20933
                                height: '1px'
 
20934
                        });
 
20935
                        rangeSelector.setInputValue(name);
 
20936
                };
 
20937
 
 
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;
 
20945
 
 
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]));
 
20951
                        }
 
20952
 
 
20953
                        if (!isNaN(value)) {
 
20954
 
 
20955
                                // Correct for timezone offset (#433)
 
20956
                                if (!defaultOptions.global.useUTC) {
 
20957
                                        value = value + new Date().getTimezoneOffset() * 60 * 1000;
 
20958
                                }
 
20959
 
 
20960
                                // Validate the extremes. If it goes beyound the data min or max, use the
 
20961
                                // actual data extreme (#2438).
 
20962
                                if (isMin) {
 
20963
                                        if (value > rangeSelector.maxInput.HCTime) {
 
20964
                                                value = UNDEFINED;
 
20965
                                        } else if (value < dataMin) {
 
20966
                                                value = dataMin;
 
20967
                                        }
 
20968
                                } else {
 
20969
                                        if (value < rangeSelector.minInput.HCTime) {
 
20970
                                                value = UNDEFINED;
 
20971
                                        } else if (value > dataMax) {
 
20972
                                                value = dataMax;
 
20973
                                        }
 
20974
                                }
 
20975
 
 
20976
                                // Set the extremes
 
20977
                                if (value !== UNDEFINED) {
 
20978
                                        chart.xAxis[0].setExtremes(
 
20979
                                                isMin ? value : xAxis.min,
 
20980
                                                isMin ? xAxis.max : value,
 
20981
                                                UNDEFINED,
 
20982
                                                UNDEFINED,
 
20983
                                                { trigger: 'rangeSelectorInput' }
 
20984
                                        );
 
20985
                                }
 
20986
                        }
 
20987
                };
 
20988
        },
 
20989
 
 
20990
        /**
 
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
 
20996
         */
 
20997
        render: function (min, max) {
 
20998
 
 
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,
 
21014
                        yAlign,
 
21015
                        buttonLeft;
 
21016
 
 
21017
                // create the elements
 
21018
                if (!rangeSelector.rendered) {
 
21019
                        rangeSelector.zoomText = renderer.text(lang.rangeSelectorZoom, plotLeft, chart.plotTop - 10)
 
21020
                                .css(options.labelStyle)
 
21021
                                .add();
 
21022
 
 
21023
                        // button starting position
 
21024
                        buttonLeft = plotLeft + rangeSelector.zoomText.getBBox().width + 5;
 
21025
 
 
21026
                        each(rangeSelector.buttonOptions, function (rangeOptions, i) {
 
21027
                                buttons[i] = renderer.button(
 
21028
                                                rangeOptions.text,
 
21029
                                                buttonLeft,
 
21030
                                                chart.plotTop - 25,
 
21031
                                                function () {
 
21032
                                                        rangeSelector.clickButton(i);
 
21033
                                                        rangeSelector.isActive = true;
 
21034
                                                },
 
21035
                                                buttonTheme,
 
21036
                                                states && states.hover,
 
21037
                                                states && states.select
 
21038
                                        )
 
21039
                                        .css({
 
21040
                                                textAlign: 'center'
 
21041
                                        })
 
21042
                                        .add();
 
21043
 
 
21044
                                // increase button position for the next button
 
21045
                                buttonLeft += buttons[i].width + (options.buttonSpacing || 0);
 
21046
 
 
21047
                                if (rangeSelector.selected === i) {
 
21048
                                        buttons[i].setState(2);
 
21049
                                }
 
21050
                        });
 
21051
 
 
21052
                        rangeSelector.updateButtonStates();
 
21053
 
 
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',
 
21059
                                        height: 0,
 
21060
                                        zIndex: 1 // above container
 
21061
                                });
 
21062
 
 
21063
                                container.parentNode.insertBefore(div, container);
 
21064
 
 
21065
                                // Create the group to keep the inputs
 
21066
                                rangeSelector.inputGroup = inputGroup = renderer.g('input-group')
 
21067
                                        .add();
 
21068
                                inputGroup.offset = 0;
 
21069
 
 
21070
                                rangeSelector.drawInput('min');
 
21071
                                rangeSelector.drawInput('max'); 
 
21072
                        }
 
21073
                }
 
21074
                
 
21075
                if (inputEnabled) {
 
21076
                
 
21077
                        // Update the alignment to the updated spacing box
 
21078
                        yAlign = chart.plotTop - 35;            
 
21079
                        inputGroup.align(extend({
 
21080
                                y: yAlign,
 
21081
                                width: inputGroup.offset,
 
21082
                                // detect collision with the exporting buttons
 
21083
                                x: navButtonOptions && (yAlign < (navButtonOptions.y || 0) + navButtonOptions.height - chart.spacing[0]) ? 
 
21084
                                        -40 : 0
 
21085
                        }, options.inputPosition), true, chart.spacingBox);
 
21086
        
 
21087
                        // Set or reset the input values
 
21088
                        rangeSelector.setInputValue('min', min);
 
21089
                        rangeSelector.setInputValue('max', max);
 
21090
                }
 
21091
 
 
21092
                rangeSelector.rendered = true;
 
21093
        },
 
21094
 
 
21095
        /**
 
21096
         * Destroys allocated elements.
 
21097
         */
 
21098
        destroy: function () {
 
21099
                var minInput = this.minInput,
 
21100
                        maxInput = this.maxInput,
 
21101
                        chart = this.chart,
 
21102
                        blurInputs = this.blurInputs,
 
21103
                        key;
 
21104
 
 
21105
                removeEvent(chart.container, 'mousedown', blurInputs);
 
21106
                removeEvent(chart, 'resize', blurInputs);
 
21107
 
 
21108
                // Destroy elements in collections
 
21109
                destroyObjectProperties(this.buttons);
 
21110
                
 
21111
                // Clear input element events
 
21112
                if (minInput) {
 
21113
                        minInput.onfocus = minInput.onblur = minInput.onchange = null;
 
21114
                }
 
21115
                if (maxInput) {
 
21116
                        maxInput.onfocus = maxInput.onblur = maxInput.onchange = null;
 
21117
                }
 
21118
 
 
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]);
 
21126
                                }
 
21127
                        }
 
21128
                        this[key] = null;
 
21129
                }
 
21130
        }
 
21131
};
 
21132
 
 
21133
/**
 
21134
 * Add logic to normalize the zoomed range in order to preserve the pressed state of range selector buttons
 
21135
 */
 
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;
 
21141
 
 
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) {
 
21146
                if (fixedMax) {
 
21147
                        newMin = newMax - fixedRange;
 
21148
                } else {
 
21149
                        newMax = newMin + fixedRange;
 
21150
                }
 
21151
        }
 
21152
 
 
21153
        return {
 
21154
                min: newMin,
 
21155
                max: newMax
 
21156
        };
 
21157
};
 
21158
 
 
21159
// Initialize scroller for stock charts
 
21160
wrap(Chart.prototype, 'init', function (proceed, options, callback) {
 
21161
        
 
21162
        addEvent(this, 'init', function () {
 
21163
                if (this.options.rangeSelector.enabled) {
 
21164
                        this.rangeSelector = new RangeSelector(this);
 
21165
                }
 
21166
        });
 
21167
 
 
21168
        proceed.call(this, options, callback);
 
21169
        
 
21170
});
 
21171
 
 
21172
 
 
21173
Highcharts.RangeSelector = RangeSelector;
 
21174
 
 
21175
/* ****************************************************************************
 
21176
 * End Range Selector code                                                                                                      *
 
21177
 *****************************************************************************/
 
21178
 
 
21179
 
 
21180
 
 
21181
Chart.prototype.callbacks.push(function (chart) {
 
21182
        var extremes,
 
21183
                scroller = chart.scroller,
 
21184
                rangeSelector = chart.rangeSelector;
 
21185
 
 
21186
        function renderScroller() {
 
21187
                extremes = chart.xAxis[0].getExtremes();
 
21188
                scroller.render(extremes.min, extremes.max);
 
21189
        }
 
21190
 
 
21191
        function renderRangeSelector() {
 
21192
                extremes = chart.xAxis[0].getExtremes();
 
21193
                if (!isNaN(extremes.min)) {
 
21194
                        rangeSelector.render(extremes.min, extremes.max);
 
21195
                }
 
21196
        }
 
21197
 
 
21198
        function afterSetExtremesHandlerScroller(e) {
 
21199
                if (e.triggerOp !== 'navigator-drag') {
 
21200
                        scroller.render(e.min, e.max);
 
21201
                }
 
21202
        }
 
21203
 
 
21204
        function afterSetExtremesHandlerRangeSelector(e) {
 
21205
                rangeSelector.render(e.min, e.max);
 
21206
        }
 
21207
 
 
21208
        function destroyEvents() {
 
21209
                if (scroller) {
 
21210
                        removeEvent(chart.xAxis[0], 'afterSetExtremes', afterSetExtremesHandlerScroller);
 
21211
                }
 
21212
                if (rangeSelector) {
 
21213
                        removeEvent(chart, 'resize', renderRangeSelector);
 
21214
                        removeEvent(chart.xAxis[0], 'afterSetExtremes', afterSetExtremesHandlerRangeSelector);
 
21215
                }
 
21216
        }
 
21217
 
 
21218
        // initiate the scroller
 
21219
        if (scroller) {
 
21220
                // redraw the scroller on setExtremes
 
21221
                addEvent(chart.xAxis[0], 'afterSetExtremes', afterSetExtremesHandlerScroller);
 
21222
 
 
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);
 
21227
                        if (isDirtyBox) {
 
21228
                                renderScroller();
 
21229
                        }
 
21230
                });
 
21231
 
 
21232
                // do it now
 
21233
                renderScroller();
 
21234
        }
 
21235
        if (rangeSelector) {
 
21236
                // redraw the scroller on setExtremes
 
21237
                addEvent(chart.xAxis[0], 'afterSetExtremes', afterSetExtremesHandlerRangeSelector);
 
21238
 
 
21239
                // redraw the scroller chart resize
 
21240
                addEvent(chart, 'resize', renderRangeSelector);
 
21241
 
 
21242
                // do it now
 
21243
                renderRangeSelector();
 
21244
        }
 
21245
 
 
21246
        // Remove resize/afterSetExtremes at chart destroy
 
21247
        addEvent(chart, 'destroy', destroyEvents);
 
21248
});
 
21249
/**
 
21250
 * A wrapper for Chart with all the default values for a Stock chart
 
21251
 */
 
21252
Highcharts.StockChart = function (options, callback) {
 
21253
        var seriesOptions = options.series, // to increase performance, don't merge the data 
 
21254
                opposite,
 
21255
 
 
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,
 
21260
                        endOnTick: false
 
21261
                } : null,
 
21262
 
 
21263
                lineOptions = {
 
21264
 
 
21265
                        marker: {
 
21266
                                enabled: false,
 
21267
                                states: {
 
21268
                                        hover: {
 
21269
                                                radius: 5
 
21270
                                        }
 
21271
                                }
 
21272
                        },
 
21273
                        // gapSize: 0,
 
21274
                        states: {
 
21275
                                hover: {
 
21276
                                        lineWidth: 2
 
21277
                                }
 
21278
                        }
 
21279
                },
 
21280
                columnOptions = {
 
21281
                        shadow: false,
 
21282
                        borderWidth: 0
 
21283
                };
 
21284
 
 
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
 
21288
                                minPadding: 0,
 
21289
                                maxPadding: 0,
 
21290
                                ordinal: true,
 
21291
                                title: {
 
21292
                                        text: null
 
21293
                                },
 
21294
                                labels: {
 
21295
                                        overflow: 'justify'
 
21296
                                },
 
21297
                                showLastLabel: true
 
21298
                        }, xAxisOptions, // user options 
 
21299
                        { // forced options
 
21300
                                type: 'datetime',
 
21301
                                categories: null
 
21302
                        },
 
21303
                        disableStartOnTick
 
21304
                );
 
21305
        });
 
21306
 
 
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
 
21311
                        labels: {
 
21312
                                align: opposite ? 'right' : 'left',
 
21313
                                x: opposite ? -2 : 2,
 
21314
                                y: -2
 
21315
                        },
 
21316
                        showLastLabel: false,
 
21317
                        title: {
 
21318
                                text: null
 
21319
                        }
 
21320
                }, yAxisOptions // user options
 
21321
                );
 
21322
        });
 
21323
 
 
21324
        options.series = null;
 
21325
 
 
21326
        options = merge({
 
21327
                chart: {
 
21328
                        panning: true,
 
21329
                        pinchType: 'x'
 
21330
                },
 
21331
                navigator: {
 
21332
                        enabled: true
 
21333
                },
 
21334
                scrollbar: {
 
21335
                        enabled: true
 
21336
                },
 
21337
                rangeSelector: {
 
21338
                        enabled: true
 
21339
                },
 
21340
                title: {
 
21341
                        text: null
 
21342
                },
 
21343
                tooltip: {
 
21344
                        shared: true,
 
21345
                        crosshairs: true
 
21346
                },
 
21347
                legend: {
 
21348
                        enabled: false
 
21349
                },
 
21350
 
 
21351
                plotOptions: {
 
21352
                        line: lineOptions,
 
21353
                        spline: lineOptions,
 
21354
                        area: lineOptions,
 
21355
                        areaspline: lineOptions,
 
21356
                        arearange: lineOptions,
 
21357
                        areasplinerange: lineOptions,
 
21358
                        column: columnOptions,
 
21359
                        columnrange: columnOptions,
 
21360
                        candlestick: columnOptions,
 
21361
                        ohlc: columnOptions
 
21362
                }
 
21363
 
 
21364
        },
 
21365
        options, // user's options
 
21366
 
 
21367
        { // forced options
 
21368
                _stock: true, // internal flag
 
21369
                chart: {
 
21370
                        inverted: false
 
21371
                }
 
21372
        });
 
21373
 
 
21374
        options.series = seriesOptions;
 
21375
 
 
21376
 
 
21377
        return new Chart(options, callback);
 
21378
};
 
21379
 
 
21380
// Implement the pinchType option
 
21381
wrap(Pointer.prototype, 'init', function (proceed, chart, options) {
 
21382
 
 
21383
        var pinchType = options.chart.pinchType || '';
 
21384
                
 
21385
        proceed.call(this, chart, options);
 
21386
 
 
21387
        // Pinch status
 
21388
        this.pinchX = this.pinchHor = pinchType.indexOf('x') !== -1;
 
21389
        this.pinchY = this.pinchVert = pinchType.indexOf('y') !== -1;
 
21390
});
 
21391
 
 
21392
// Override getPlotLinePath to allow for multipane charts
 
21393
Axis.prototype.getPlotLinePath = function (value, lineWidth, old, force, translatedValue) {
 
21394
        var axis = this,
 
21395
                series = (this.isLinked ? this.linkedParent.series : this.series),
 
21396
                renderer = axis.chart.renderer,
 
21397
                axisLeft = axis.left,
 
21398
                axisTop = axis.top,
 
21399
                x1,
 
21400
                y1,
 
21401
                x2,
 
21402
                y2,
 
21403
                result = [];
 
21404
 
 
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; })
 
21410
                                        ) :
 
21411
                                        (defined(this.options.xAxis) ?
 
21412
                                                [this.chart.xAxis[this.options.xAxis]] : 
 
21413
                                                map(series, function (S) { return S.xAxis; })
 
21414
                                        )
 
21415
                                );
 
21416
 
 
21417
        // remove duplicates in the axes array
 
21418
        var uAxes = [];
 
21419
        each(axes, function (axis2) {
 
21420
                if (inArray(axis2, uAxes) === -1) {
 
21421
                        uAxes.push(axis2);
 
21422
                }
 
21423
        });
 
21424
        
 
21425
        translatedValue = pick(translatedValue, axis.translate(value, null, null, old));
 
21426
        
 
21427
        if (!isNaN(translatedValue)) {
 
21428
                if (axis.horiz) {
 
21429
                        each(uAxes, function (axis2) {
 
21430
                                y1 = axis2.top;
 
21431
                                y2 = y1 + axis2.len;
 
21432
                                x1 = x2 = mathRound(translatedValue + axis.transB);
 
21433
 
 
21434
                                if ((x1 >= axisLeft && x1 <= axisLeft + axis.width) || force) {
 
21435
                                        result.push('M', x1, y1, 'L', x2, y2);
 
21436
                                }
 
21437
                        });
 
21438
                } else {
 
21439
                        each(uAxes, function (axis2) {
 
21440
                                x1 = axis2.left;
 
21441
                                x2 = x1 + axis2.width;
 
21442
                                y1 = y2 = mathRound(axisTop + axis.height - translatedValue);
 
21443
 
 
21444
                                if ((y1 >= axisTop && y1 <= axisTop + axis.height) || force) {
 
21445
                                        result.push('M', x1, y1, 'L', x2, y2);
 
21446
                                }
 
21447
                        });
 
21448
                }
 
21449
        }
 
21450
        if (result.length > 0) {
 
21451
                return renderer.crispPolyLine(result, lineWidth || 1); 
 
21452
        } else {
 
21453
                return null;
 
21454
        }
 
21455
};
 
21456
 
 
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
 
21461
        var i;
 
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);
 
21466
                }
 
21467
                if (points[i + 2] === points[i + 5]) {
 
21468
                        points[i + 2] = points[i + 5] = mathRound(points[i + 2]) + (width % 2 / 2);
 
21469
                }
 
21470
        }
 
21471
        return points;
 
21472
};
 
21473
if (Renderer === Highcharts.VMLRenderer) {
 
21474
        VMLRenderer.prototype.crispPolyLine = SVGRenderer.prototype.crispPolyLine;
 
21475
}
 
21476
 
 
21477
 
 
21478
// Wrapper to hide the label
 
21479
wrap(Axis.prototype, 'hideCrosshair', function (proceed, i) {
 
21480
        proceed.call(this, i);
 
21481
 
 
21482
        if (!defined(this.crossLabelArray)) { return; }
 
21483
 
 
21484
        if (defined(i)) {
 
21485
                if (this.crossLabelArray[i]) { this.crossLabelArray[i].hide(); }
 
21486
        } else {
 
21487
                each(this.crossLabelArray, function (crosslabel) {
 
21488
                        crosslabel.hide();
 
21489
                });
 
21490
        }
 
21491
});
 
21492
 
 
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);
 
21497
 
 
21498
        // Check if the label has to be drawn
 
21499
        if (!defined(this.crosshair.label) || !this.crosshair.label.enabled || !defined(point)) { 
 
21500
                return; 
 
21501
        }
 
21502
 
 
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
 
21511
                posx,
 
21512
                posy,
 
21513
                crossBox,
 
21514
                formatOption = options.format,
 
21515
                formatFormat = '',
 
21516
                limit;
 
21517
 
 
21518
        // If the label does not exist yet, create it.
 
21519
        if (!crossLabel) {
 
21520
                crossLabel = this.crossLabel = chart.renderer.label()                   
 
21521
                .attr({
 
21522
                        align: options.align || (horiz ? 'center' : opposite ? (this.labelAlign === 'right' ? 'right' : 'left') : (this.labelAlign === 'left' ? 'left' : 'center')),
 
21523
                        zIndex: 12,
 
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
 
21529
                })
 
21530
                .css(extend({                           
 
21531
                        color: 'white',
 
21532
                        fontWeight: 'normal',
 
21533
                        fontSize: '11px',
 
21534
                        textAlign: 'center'
 
21535
                }, options.style))
 
21536
                .add();
 
21537
        }
 
21538
 
 
21539
        if (horiz) {
 
21540
                posx = point.plotX + left;
 
21541
                posy = top + (opposite ? 0 : this.height);
 
21542
        } else {
 
21543
                posx = opposite ? this.width + left : 0;
 
21544
                posy = point.plotY + top;
 
21545
        }
 
21546
 
 
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();
 
21550
                return;
 
21551
        }
 
21552
 
 
21553
        // TODO: Dynamic date formats like in Series.tooltipHeaderFormat. 
 
21554
        if (!formatOption && !options.formatter) {
 
21555
                if (this.isDatetimeAxis) {
 
21556
                        formatFormat = '%b %d, %Y';
 
21557
                }
 
21558
                formatOption = '{value' + (formatFormat ? ':' + formatFormat : '') + '}';
 
21559
        }
 
21560
 
 
21561
        // show the label
 
21562
        crossLabel.attr({
 
21563
                x: posx, 
 
21564
                y: posy, 
 
21565
                text: formatOption ? format(formatOption, {value: point[axis]}) : options.formatter.call(this, point[axis]), 
 
21566
                visibility: VISIBLE
 
21567
        });
 
21568
        crossBox = crossLabel.box;
 
21569
 
 
21570
        // now it is placed we can correct its position
 
21571
        if (horiz) {
 
21572
                if (((this.options.tickPosition === 'inside') && !opposite) ||
 
21573
                        ((this.options.tickPosition !== 'inside') && opposite)) {
 
21574
                        posy = crossLabel.y - crossBox.height;
 
21575
                }       
 
21576
        } else {
 
21577
                posy = crossLabel.y - (crossBox.height / 2);
 
21578
        }
 
21579
 
 
21580
        // check the edges
 
21581
        if (horiz) {
 
21582
                limit = {
 
21583
                        left: left - crossBox.x,
 
21584
                        right: left + this.width - crossBox.x
 
21585
                };
 
21586
        } else {
 
21587
                limit = {
 
21588
                        left: this.labelAlign === 'left' ? left : 0,
 
21589
                        right: this.labelAlign === 'right' ? left + this.width : chart.chartWidth
 
21590
                };
 
21591
        }
 
21592
 
 
21593
        // left edge
 
21594
        if (crossLabel.translateX < limit.left) {
 
21595
                posx += limit.left - crossLabel.translateX;
 
21596
        }
 
21597
        // right edge
 
21598
        if (crossLabel.translateX + crossBox.width >= limit.right) {
 
21599
                posx -= crossLabel.translateX + crossBox.width - limit.right;
 
21600
        }
 
21601
 
 
21602
        // show the crosslabel
 
21603
        crossLabel.attr({x: posx, y: posy, visibility: VISIBLE});
 
21604
});
 
21605
 
 
21606
/* ****************************************************************************
 
21607
 * Start value compare logic                                                  *
 
21608
 *****************************************************************************/
 
21609
 
 
21610
var seriesInit = seriesProto.init, 
 
21611
        seriesProcessData = seriesProto.processData,
 
21612
        pointTooltipFormatter = Point.prototype.tooltipFormatter;
 
21613
        
 
21614
/**
 
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.
 
21618
 */
 
21619
seriesProto.init = function () {
 
21620
        
 
21621
        // Call base method
 
21622
        seriesInit.apply(this, arguments);
 
21623
        
 
21624
        // Set comparison mode
 
21625
        this.setCompare(this.options.compare);
 
21626
};
 
21627
 
 
21628
/**
 
21629
 * The setCompare method can be called also from the outside after render time
 
21630
 */
 
21631
seriesProto.setCompare = function (compare) {
 
21632
 
 
21633
        // Set or unset the modifyValue method
 
21634
        this.modifyValue = (compare === 'value' || compare === 'percent') ? function (value, point) {
 
21635
                var compareValue = this.compareValue;
 
21636
                
 
21637
                if (value !== UNDEFINED) { // #2601
 
21638
 
 
21639
                        // get the modified value
 
21640
                        value = compare === 'value' ? 
 
21641
                                value - compareValue : // compare value
 
21642
                                value = 100 * (value / compareValue) - 100; // compare percent
 
21643
                                
 
21644
                        // record for tooltip etc.
 
21645
                        if (point) {
 
21646
                                point.change = value;
 
21647
                        }
 
21648
                        
 
21649
                }
 
21650
                
 
21651
                return value;
 
21652
        } : null;
 
21653
 
 
21654
        // Mark dirty
 
21655
        if (this.chart.hasRendered) {
 
21656
                this.isDirty = true;
 
21657
        }
 
21658
 
 
21659
};
 
21660
 
 
21661
/**
 
21662
 * Extend series.processData by finding the first y value in the plot area,
 
21663
 * used for comparing the following values 
 
21664
 */
 
21665
seriesProto.processData = function () {
 
21666
        var series = this,
 
21667
                i = 0,
 
21668
                processedXData,
 
21669
                processedYData,
 
21670
                length;
 
21671
        
 
21672
        // call base method
 
21673
        seriesProcessData.apply(this, arguments);
 
21674
 
 
21675
        if (series.xAxis && series.processedYData) { // not pies
 
21676
                
 
21677
                // local variables
 
21678
                processedXData = series.processedXData;
 
21679
                processedYData = series.processedYData;
 
21680
                length = processedYData.length;
 
21681
                
 
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];
 
21686
                                break;
 
21687
                        }
 
21688
                }
 
21689
        }
 
21690
};
 
21691
 
 
21692
/**
 
21693
 * Modify series extremes
 
21694
 */
 
21695
wrap(seriesProto, 'getExtremes', function (proceed) {
 
21696
        proceed.call(this);
 
21697
 
 
21698
        if (this.modifyValue) {
 
21699
                this.dataMax = this.modifyValue(this.dataMax);
 
21700
                this.dataMin = this.modifyValue(this.dataMin);
 
21701
        }               
 
21702
});
 
21703
 
 
21704
/**
 
21705
 * Add a utility method, setCompare, to the Y axis
 
21706
 */
 
21707
Axis.prototype.setCompare = function (compare, redraw) {
 
21708
        if (!this.isXAxis) {
 
21709
                each(this.series, function (series) {
 
21710
                        series.setCompare(compare);
 
21711
                });
 
21712
                if (pick(redraw, true)) {
 
21713
                        this.chart.redraw();
 
21714
                }
 
21715
        }
 
21716
};
 
21717
 
 
21718
/**
 
21719
 * Extend the tooltip formatter by adding support for the point.change variable
 
21720
 * as well as the changeDecimals option
 
21721
 */
 
21722
Point.prototype.tooltipFormatter = function (pointFormat) {
 
21723
        var point = this;
 
21724
        
 
21725
        pointFormat = pointFormat.replace(
 
21726
                '{point.change}',
 
21727
                (point.change > 0 ? '+' : '') + numberFormat(point.change, pick(point.series.tooltipOptions.changeDecimals, 2))
 
21728
        ); 
 
21729
        
 
21730
        return pointTooltipFormatter.apply(this, [pointFormat]);
 
21731
};
 
21732
 
 
21733
/* ****************************************************************************
 
21734
 * End value compare logic                                                    *
 
21735
 *****************************************************************************/
 
21736
 
 
21737
// global variables
 
21738
extend(Highcharts, {
 
21739
        
 
21740
        // Constructors
 
21741
        Axis: Axis,
 
21742
        Chart: Chart,
 
21743
        Color: Color,
 
21744
        Point: Point,
 
21745
        Tick: Tick,     
 
21746
        Renderer: Renderer,
 
21747
        Series: Series,
 
21748
        SVGElement: SVGElement,
 
21749
        SVGRenderer: SVGRenderer,
 
21750
        
 
21751
        // Various
 
21752
        arrayMin: arrayMin,
 
21753
        arrayMax: arrayMax,
 
21754
        charts: charts,
 
21755
        dateFormat: dateFormat,
 
21756
        format: format,
 
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,
 
21768
        css: css,
 
21769
        each: each,
 
21770
        extend: extend,
 
21771
        map: map,
 
21772
        merge: merge,
 
21773
        pick: pick,
 
21774
        splat: splat,
 
21775
        extendClass: extendClass,
 
21776
        pInt: pInt,
 
21777
        wrap: wrap,
 
21778
        svg: hasSVG,
 
21779
        canvas: useCanVG,
 
21780
        vml: !hasSVG && !useCanVG,
 
21781
        product: PRODUCT,
 
21782
        version: VERSION
 
21783
});
 
21784
 
 
21785
}());