Created
June 15, 2017 06:42
-
-
Save tnrn9b/80ccb1e193cc16a26fdde4996a554c50 to your computer and use it in GitHub Desktop.
built using the assembler
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
(function(root, factory) { | |
if (typeof module === 'object' && module.exports) { | |
module.exports = root.document ? | |
factory(root) : | |
factory; | |
} else { | |
root.Highcharts = factory(root); | |
} | |
}(typeof window !== 'undefined' ? window : this, function(win) { | |
var Highcharts = (function() { | |
/** | |
* (c) 2010-2017 Torstein Honsi | |
* | |
* License: www.highcharts.com/license | |
*/ | |
/* global window */ | |
var win = window, | |
doc = win.document; | |
var SVG_NS = 'http://www.w3.org/2000/svg', | |
userAgent = (win.navigator && win.navigator.userAgent) || '', | |
svg = doc && doc.createElementNS && !!doc.createElementNS(SVG_NS, 'svg').createSVGRect, | |
isMS = /(edge|msie|trident)/i.test(userAgent) && !window.opera, | |
vml = !svg, | |
isFirefox = /Firefox/.test(userAgent), | |
hasBidiBug = isFirefox && parseInt(userAgent.split('Firefox/')[1], 10) < 4; // issue #38 | |
var Highcharts = win.Highcharts ? win.Highcharts.error(16, true) : { | |
product: 'Highcharts', | |
version: 'x.x.x', | |
deg2rad: Math.PI * 2 / 360, | |
doc: doc, | |
hasBidiBug: hasBidiBug, | |
hasTouch: doc && doc.documentElement.ontouchstart !== undefined, | |
isMS: isMS, | |
isWebKit: /AppleWebKit/.test(userAgent), | |
isFirefox: isFirefox, | |
isTouchDevice: /(Mobile|Android|Windows Phone)/.test(userAgent), | |
SVG_NS: SVG_NS, | |
chartCount: 0, | |
seriesTypes: {}, | |
symbolSizes: {}, | |
svg: svg, | |
vml: vml, | |
win: win, | |
marginNames: ['plotTop', 'marginRight', 'marginBottom', 'plotLeft'], | |
noop: function() { | |
return undefined; | |
}, | |
/** | |
* An array containing the current chart objects in the page. A chart's | |
* position in the array is preserved throughout the page's lifetime. When | |
* a chart is destroyed, the array item becomes `undefined`. | |
* @type {Array.<Highcharts.Chart>} | |
* @memberOf Highcharts | |
*/ | |
charts: [] | |
}; | |
return Highcharts; | |
}()); | |
(function(H) { | |
/** | |
* (c) 2010-2017 Torstein Honsi | |
* | |
* License: www.highcharts.com/license | |
*/ | |
/* eslint max-len: ["warn", 80, 4] */ | |
/** | |
* The Highcharts object is the placeholder for all other members, and various | |
* utility functions. The most important member of the namespace would be the | |
* chart constructor. | |
* | |
* @example | |
* var chart = Highcharts.chart('container', { ... }); | |
* | |
* @namespace Highcharts | |
*/ | |
var timers = []; | |
var charts = H.charts, | |
doc = H.doc, | |
win = H.win; | |
/** | |
* Provide error messages for debugging, with links to online explanation. This | |
* function can be overridden to provide custom error handling. | |
* | |
* @function #error | |
* @memberOf Highcharts | |
* @param {Number|String} code - The error code. See [errors.xml]{@link | |
* https://github.com/highcharts/highcharts/blob/master/errors/errors.xml} | |
* for available codes. If it is a string, the error message is printed | |
* directly in the console. | |
* @param {Boolean} [stop=false] - Whether to throw an error or just log a | |
* warning in the console. | |
* | |
* @sample highcharts/chart/highcharts-error/ Custom error handler | |
*/ | |
H.error = function(code, stop) { | |
var msg = H.isNumber(code) ? | |
'Highcharts error #' + code + ': www.highcharts.com/errors/' + code : | |
code; | |
if (stop) { | |
throw new Error(msg); | |
} | |
// else ... | |
if (win.console) { | |
console.log(msg); // eslint-disable-line no-console | |
} | |
}; | |
/** | |
* An animator object used internally. One instance applies to one property | |
* (attribute or style prop) on one element. Animation is always initiated | |
* through {@link SVGElement#animate}. | |
* | |
* @constructor Fx | |
* @memberOf Highcharts | |
* @param {HTMLDOMElement|SVGElement} elem - The element to animate. | |
* @param {AnimationOptions} options - Animation options. | |
* @param {string} prop - The single attribute or CSS property to animate. | |
* @private | |
* | |
* @example | |
* var rect = renderer.rect(0, 0, 10, 10).add(); | |
* rect.animate({ width: 100 }); | |
*/ | |
H.Fx = function(elem, options, prop) { | |
this.options = options; | |
this.elem = elem; | |
this.prop = prop; | |
}; | |
H.Fx.prototype = { | |
/** | |
* Set the current step of a path definition on SVGElement. | |
* | |
* @function #dSetter | |
* @memberOf Highcharts.Fx | |
*/ | |
dSetter: function() { | |
var start = this.paths[0], | |
end = this.paths[1], | |
ret = [], | |
now = this.now, | |
i = start.length, | |
startVal; | |
// Land on the final path without adjustment points appended in the ends | |
if (now === 1) { | |
ret = this.toD; | |
} else if (i === end.length && now < 1) { | |
while (i--) { | |
startVal = parseFloat(start[i]); | |
ret[i] = | |
isNaN(startVal) ? // a letter instruction like M or L | |
start[i] : | |
now * (parseFloat(end[i] - startVal)) + startVal; | |
} | |
// If animation is finished or length not matching, land on right value | |
} else { | |
ret = end; | |
} | |
this.elem.attr('d', ret, null, true); | |
}, | |
/** | |
* Update the element with the current animation step. | |
* | |
* @function #update | |
* @memberOf Highcharts.Fx | |
*/ | |
update: function() { | |
var elem = this.elem, | |
prop = this.prop, // if destroyed, it is null | |
now = this.now, | |
step = this.options.step; | |
// Animation setter defined from outside | |
if (this[prop + 'Setter']) { | |
this[prop + 'Setter'](); | |
// Other animations on SVGElement | |
} else if (elem.attr) { | |
if (elem.element) { | |
elem.attr(prop, now, null, true); | |
} | |
// HTML styles, raw HTML content like container size | |
} else { | |
elem.style[prop] = now + this.unit; | |
} | |
if (step) { | |
step.call(elem, now, this); | |
} | |
}, | |
/** | |
* Run an animation. | |
* | |
* @function #run | |
* @memberOf Highcharts.Fx | |
* @param {Number} from - The current value, value to start from. | |
* @param {Number} to - The end value, value to land on. | |
* @param {String} [unit] - The property unit, for example `px`. | |
* @returns {void} | |
*/ | |
run: function(from, to, unit) { | |
var self = this, | |
timer = function(gotoEnd) { | |
return timer.stopped ? false : self.step(gotoEnd); | |
}, | |
i; | |
this.startTime = +new Date(); | |
this.start = from; | |
this.end = to; | |
this.unit = unit; | |
this.now = this.start; | |
this.pos = 0; | |
timer.elem = this.elem; | |
timer.prop = this.prop; | |
if (timer() && timers.push(timer) === 1) { | |
timer.timerId = setInterval(function() { | |
for (i = 0; i < timers.length; i++) { | |
if (!timers[i]()) { | |
timers.splice(i--, 1); | |
} | |
} | |
if (!timers.length) { | |
clearInterval(timer.timerId); | |
} | |
}, 13); | |
} | |
}, | |
/** | |
* Run a single step in the animation. | |
* | |
* @function #step | |
* @memberOf Highcharts.Fx | |
* @param {Boolean} [gotoEnd] - Whether to go to the endpoint of the | |
* animation after abort. | |
* @returns {Boolean} Returns `true` if animation continues. | |
*/ | |
step: function(gotoEnd) { | |
var t = +new Date(), | |
ret, | |
done, | |
options = this.options, | |
elem = this.elem, | |
complete = options.complete, | |
duration = options.duration, | |
curAnim = options.curAnim; | |
if (elem.attr && !elem.element) { // #2616, element is destroyed | |
ret = false; | |
} else if (gotoEnd || t >= duration + this.startTime) { | |
this.now = this.end; | |
this.pos = 1; | |
this.update(); | |
curAnim[this.prop] = true; | |
done = true; | |
H.objectEach(curAnim, function(val) { | |
if (val !== true) { | |
done = false; | |
} | |
}); | |
if (done && complete) { | |
complete.call(elem); | |
} | |
ret = false; | |
} else { | |
this.pos = options.easing((t - this.startTime) / duration); | |
this.now = this.start + ((this.end - this.start) * this.pos); | |
this.update(); | |
ret = true; | |
} | |
return ret; | |
}, | |
/** | |
* Prepare start and end values so that the path can be animated one to one. | |
* | |
* @function #initPath | |
* @memberOf Highcharts.Fx | |
* @param {SVGElement} elem - The SVGElement item. | |
* @param {String} fromD - Starting path definition. | |
* @param {Array} toD - Ending path definition. | |
* @returns {Array} An array containing start and end paths in array form | |
* so that they can be animated in parallel. | |
*/ | |
initPath: function(elem, fromD, toD) { | |
fromD = fromD || ''; | |
var shift, | |
startX = elem.startX, | |
endX = elem.endX, | |
bezier = fromD.indexOf('C') > -1, | |
numParams = bezier ? 7 : 3, | |
fullLength, | |
slice, | |
i, | |
start = fromD.split(' '), | |
end = toD.slice(), // copy | |
isArea = elem.isArea, | |
positionFactor = isArea ? 2 : 1, | |
reverse; | |
/** | |
* In splines make moveTo and lineTo points have six parameters like | |
* bezier curves, to allow animation one-to-one. | |
*/ | |
function sixify(arr) { | |
var isOperator, | |
nextIsOperator; | |
i = arr.length; | |
while (i--) { | |
// Fill in dummy coordinates only if the next operator comes | |
// three places behind (#5788) | |
isOperator = arr[i] === 'M' || arr[i] === 'L'; | |
nextIsOperator = /[a-zA-Z]/.test(arr[i + 3]); | |
if (isOperator && nextIsOperator) { | |
arr.splice( | |
i + 1, 0, | |
arr[i + 1], arr[i + 2], | |
arr[i + 1], arr[i + 2] | |
); | |
} | |
} | |
} | |
/** | |
* Insert an array at the given position of another array | |
*/ | |
function insertSlice(arr, subArr, index) { | |
[].splice.apply( | |
arr, [index, 0].concat(subArr) | |
); | |
} | |
/** | |
* If shifting points, prepend a dummy point to the end path. | |
*/ | |
function prepend(arr, other) { | |
while (arr.length < fullLength) { | |
// Move to, line to or curve to? | |
arr[0] = other[fullLength - arr.length]; | |
// Prepend a copy of the first point | |
insertSlice(arr, arr.slice(0, numParams), 0); | |
// For areas, the bottom path goes back again to the left, so we | |
// need to append a copy of the last point. | |
if (isArea) { | |
insertSlice( | |
arr, | |
arr.slice(arr.length - numParams), arr.length | |
); | |
i--; | |
} | |
} | |
arr[0] = 'M'; | |
} | |
/** | |
* Copy and append last point until the length matches the end length | |
*/ | |
function append(arr, other) { | |
var i = (fullLength - arr.length) / numParams; | |
while (i > 0 && i--) { | |
// Pull out the slice that is going to be appended or inserted. | |
// In a line graph, the positionFactor is 1, and the last point | |
// is sliced out. In an area graph, the positionFactor is 2, | |
// causing the middle two points to be sliced out, since an area | |
// path starts at left, follows the upper path then turns and | |
// follows the bottom back. | |
slice = arr.slice().splice( | |
(arr.length / positionFactor) - numParams, | |
numParams * positionFactor | |
); | |
// Move to, line to or curve to? | |
slice[0] = other[fullLength - numParams - (i * numParams)]; | |
// Disable first control point | |
if (bezier) { | |
slice[numParams - 6] = slice[numParams - 2]; | |
slice[numParams - 5] = slice[numParams - 1]; | |
} | |
// Now insert the slice, either in the middle (for areas) or at | |
// the end (for lines) | |
insertSlice(arr, slice, arr.length / positionFactor); | |
if (isArea) { | |
i--; | |
} | |
} | |
} | |
if (bezier) { | |
sixify(start); | |
sixify(end); | |
} | |
// For sideways animation, find out how much we need to shift to get the | |
// start path Xs to match the end path Xs. | |
if (startX && endX) { | |
for (i = 0; i < startX.length; i++) { | |
// Moving left, new points coming in on right | |
if (startX[i] === endX[0]) { | |
shift = i; | |
break; | |
// Moving right | |
} else if (startX[0] === | |
endX[endX.length - startX.length + i]) { | |
shift = i; | |
reverse = true; | |
break; | |
} | |
} | |
if (shift === undefined) { | |
start = []; | |
} | |
} | |
if (start.length && H.isNumber(shift)) { | |
// The common target length for the start and end array, where both | |
// arrays are padded in opposite ends | |
fullLength = end.length + shift * positionFactor * numParams; | |
if (!reverse) { | |
prepend(end, start); | |
append(start, end); | |
} else { | |
prepend(start, end); | |
append(end, start); | |
} | |
} | |
return [start, end]; | |
} | |
}; // End of Fx prototype | |
/** | |
* Handle animation of the color attributes directly. | |
*/ | |
H.Fx.prototype.fillSetter = | |
H.Fx.prototype.strokeSetter = function() { | |
this.elem.attr( | |
this.prop, | |
H.color(this.start).tweenTo(H.color(this.end), this.pos), | |
null, | |
true | |
); | |
}; | |
/** | |
* Utility function to extend an object with the members of another. | |
* | |
* @function #extend | |
* @memberOf Highcharts | |
* @param {Object} a - The object to be extended. | |
* @param {Object} b - The object to add to the first one. | |
* @returns {Object} Object a, the original object. | |
*/ | |
H.extend = function(a, b) { | |
var n; | |
if (!a) { | |
a = {}; | |
} | |
for (n in b) { | |
a[n] = b[n]; | |
} | |
return a; | |
}; | |
/** | |
* Utility function to deep merge two or more objects and return a third object. | |
* If the first argument is true, the contents of the second object is copied | |
* into the first object. The merge function can also be used with a single | |
* object argument to create a deep copy of an object. | |
* | |
* @function #merge | |
* @memberOf Highcharts | |
* @param {Boolean} [extend] - Whether to extend the left-side object (a) or | |
return a whole new object. | |
* @param {Object} a - The first object to extend. When only this is given, the | |
function returns a deep copy. | |
* @param {...Object} [n] - An object to merge into the previous one. | |
* @returns {Object} - The merged object. If the first argument is true, the | |
* return is the same as the second argument. | |
*/ | |
H.merge = function() { | |
var i, | |
args = arguments, | |
len, | |
ret = {}, | |
doCopy = function(copy, original) { | |
// An object is replacing a primitive | |
if (typeof copy !== 'object') { | |
copy = {}; | |
} | |
H.objectEach(original, function(value, key) { | |
// Copy the contents of objects, but not arrays or DOM nodes | |
if ( | |
H.isObject(value, true) && | |
!H.isClass(value) && | |
!H.isDOMElement(value) | |
) { | |
copy[key] = doCopy(copy[key] || {}, value); | |
// Primitives and arrays are copied over directly | |
} else { | |
copy[key] = original[key]; | |
} | |
}); | |
return copy; | |
}; | |
// If first argument is true, copy into the existing object. Used in | |
// setOptions. | |
if (args[0] === true) { | |
ret = args[1]; | |
args = Array.prototype.slice.call(args, 2); | |
} | |
// For each argument, extend the return | |
len = args.length; | |
for (i = 0; i < len; i++) { | |
ret = doCopy(ret, args[i]); | |
} | |
return ret; | |
}; | |
/** | |
* Shortcut for parseInt | |
* @ignore | |
* @param {Object} s | |
* @param {Number} mag Magnitude | |
*/ | |
H.pInt = function(s, mag) { | |
return parseInt(s, mag || 10); | |
}; | |
/** | |
* Utility function to check for string type. | |
* | |
* @function #isString | |
* @memberOf Highcharts | |
* @param {Object} s - The item to check. | |
* @returns {Boolean} - True if the argument is a string. | |
*/ | |
H.isString = function(s) { | |
return typeof s === 'string'; | |
}; | |
/** | |
* Utility function to check if an item is an array. | |
* | |
* @function #isArray | |
* @memberOf Highcharts | |
* @param {Object} obj - The item to check. | |
* @returns {Boolean} - True if the argument is an array. | |
*/ | |
H.isArray = function(obj) { | |
var str = Object.prototype.toString.call(obj); | |
return str === '[object Array]' || str === '[object Array Iterator]'; | |
}; | |
/** | |
* Utility function to check if an item is of type object. | |
* | |
* @function #isObject | |
* @memberOf Highcharts | |
* @param {Object} obj - The item to check. | |
* @param {Boolean} [strict=false] - Also checks that the object is not an | |
* array. | |
* @returns {Boolean} - True if the argument is an object. | |
*/ | |
H.isObject = function(obj, strict) { | |
return !!obj && typeof obj === 'object' && (!strict || !H.isArray(obj)); | |
}; | |
/** | |
* Utility function to check if an Object is a HTML Element. | |
* | |
* @function #isDOMElement | |
* @memberOf Highcharts | |
* @param {Object} obj - The item to check. | |
* @returns {Boolean} - True if the argument is a HTML Element. | |
*/ | |
H.isDOMElement = function(obj) { | |
return H.isObject(obj) && typeof obj.nodeType === 'number'; | |
}; | |
/** | |
* Utility function to check if an Object is an class. | |
* | |
* @function #isClass | |
* @memberOf Highcharts | |
* @param {Object} obj - The item to check. | |
* @returns {Boolean} - True if the argument is an class. | |
*/ | |
H.isClass = function(obj) { | |
var c = obj && obj.constructor; | |
return !!( | |
H.isObject(obj, true) && | |
!H.isDOMElement(obj) && | |
(c && c.name && c.name !== 'Object') | |
); | |
}; | |
/** | |
* Utility function to check if an item is of type number. | |
* | |
* @function #isNumber | |
* @memberOf Highcharts | |
* @param {Object} n - The item to check. | |
* @returns {Boolean} - True if the item is a number and is not NaN. | |
*/ | |
H.isNumber = function(n) { | |
return typeof n === 'number' && !isNaN(n); | |
}; | |
/** | |
* Remove the last occurence of an item from an array. | |
* | |
* @function #erase | |
* @memberOf Highcharts | |
* @param {Array} arr - The array. | |
* @param {*} item - The item to remove. | |
*/ | |
H.erase = function(arr, item) { | |
var i = arr.length; | |
while (i--) { | |
if (arr[i] === item) { | |
arr.splice(i, 1); | |
break; | |
} | |
} | |
}; | |
/** | |
* Check if an object is null or undefined. | |
* | |
* @function #defined | |
* @memberOf Highcharts | |
* @param {Object} obj - The object to check. | |
* @returns {Boolean} - False if the object is null or undefined, otherwise | |
* true. | |
*/ | |
H.defined = function(obj) { | |
return obj !== undefined && obj !== null; | |
}; | |
/** | |
* Set or get an attribute or an object of attributes. To use as a setter, pass | |
* a key and a value, or let the second argument be a collection of keys and | |
* values. To use as a getter, pass only a string as the second argument. | |
* | |
* @function #attr | |
* @memberOf Highcharts | |
* @param {Object} elem - The DOM element to receive the attribute(s). | |
* @param {String|Object} [prop] - The property or an object of key-value pairs. | |
* @param {String} [value] - The value if a single property is set. | |
* @returns {*} When used as a getter, return the value. | |
*/ | |
H.attr = function(elem, prop, value) { | |
var ret; | |
// if the prop is a string | |
if (H.isString(prop)) { | |
// set the value | |
if (H.defined(value)) { | |
elem.setAttribute(prop, value); | |
// get the value | |
} else if (elem && elem.getAttribute) { | |
ret = elem.getAttribute(prop); | |
} | |
// else if prop is defined, it is a hash of key/value pairs | |
} else if (H.defined(prop) && H.isObject(prop)) { | |
H.objectEach(prop, function(val, key) { | |
elem.setAttribute(key, val); | |
}); | |
} | |
return ret; | |
}; | |
/** | |
* Check if an element is an array, and if not, make it into an array. | |
* | |
* @function #splat | |
* @memberOf Highcharts | |
* @param obj {*} - The object to splat. | |
* @returns {Array} The produced or original array. | |
*/ | |
H.splat = function(obj) { | |
return H.isArray(obj) ? obj : [obj]; | |
}; | |
/** | |
* Set a timeout if the delay is given, otherwise perform the function | |
* synchronously. | |
* | |
* @function #syncTimeout | |
* @memberOf Highcharts | |
* @param {Function} fn - The function callback. | |
* @param {Number} delay - Delay in milliseconds. | |
* @param {Object} [context] - The context. | |
* @returns {Number} An identifier for the timeout that can later be cleared | |
* with clearTimeout. | |
*/ | |
H.syncTimeout = function(fn, delay, context) { | |
if (delay) { | |
return setTimeout(fn, delay, context); | |
} | |
fn.call(0, context); | |
}; | |
/** | |
* Return the first value that is not null or undefined. | |
* | |
* @function #pick | |
* @memberOf Highcharts | |
* @param {...*} items - Variable number of arguments to inspect. | |
* @returns {*} The value of the first argument that is not null or undefined. | |
*/ | |
H.pick = function() { | |
var args = arguments, | |
i, | |
arg, | |
length = args.length; | |
for (i = 0; i < length; i++) { | |
arg = args[i]; | |
if (arg !== undefined && arg !== null) { | |
return arg; | |
} | |
} | |
}; | |
/** | |
* @typedef {Object} CSSObject - A style object with camel case property names. | |
* The properties can be whatever styles are supported on the given SVG or HTML | |
* element. | |
* @example | |
* { | |
* fontFamily: 'monospace', | |
* fontSize: '1.2em' | |
* } | |
*/ | |
/** | |
* Set CSS on a given element. | |
* | |
* @function #css | |
* @memberOf Highcharts | |
* @param {HTMLDOMElement} el - A HTML DOM element. | |
* @param {CSSObject} styles - Style object with camel case property names. | |
* @returns {void} | |
*/ | |
H.css = function(el, styles) { | |
if (H.isMS && !H.svg) { // #2686 | |
if (styles && styles.opacity !== undefined) { | |
styles.filter = 'alpha(opacity=' + (styles.opacity * 100) + ')'; | |
} | |
} | |
H.extend(el.style, styles); | |
}; | |
/** | |
* A HTML DOM element. | |
* @typedef {Object} HTMLDOMElement | |
*/ | |
/** | |
* Utility function to create an HTML element with attributes and styles. | |
* | |
* @function #createElement | |
* @memberOf Highcharts | |
* @param {String} tag - The HTML tag. | |
* @param {Object} [attribs] - Attributes as an object of key-value pairs. | |
* @param {CSSObject} [styles] - Styles as an object of key-value pairs. | |
* @param {Object} [parent] - The parent HTML object. | |
* @param {Boolean} [nopad=false] - If true, remove all padding, border and | |
* margin. | |
* @returns {HTMLDOMElement} The created DOM element. | |
*/ | |
H.createElement = function(tag, attribs, styles, parent, nopad) { | |
var el = doc.createElement(tag), | |
css = H.css; | |
if (attribs) { | |
H.extend(el, attribs); | |
} | |
if (nopad) { | |
css(el, { | |
padding: 0, | |
border: 'none', | |
margin: 0 | |
}); | |
} | |
if (styles) { | |
css(el, styles); | |
} | |
if (parent) { | |
parent.appendChild(el); | |
} | |
return el; | |
}; | |
/** | |
* Extend a prototyped class by new members. | |
* | |
* @function #extendClass | |
* @memberOf Highcharts | |
* @param {Object} parent - The parent prototype to inherit. | |
* @param {Object} members - A collection of prototype members to add or | |
* override compared to the parent prototype. | |
* @returns {Object} A new prototype. | |
*/ | |
H.extendClass = function(parent, members) { | |
var object = function() {}; | |
object.prototype = new parent(); // eslint-disable-line new-cap | |
H.extend(object.prototype, members); | |
return object; | |
}; | |
/** | |
* Left-pad a string to a given length by adding a character repetetively. | |
* | |
* @function #pad | |
* @memberOf Highcharts | |
* @param {Number} number - The input string or number. | |
* @param {Number} length - The desired string length. | |
* @param {String} [padder=0] - The character to pad with. | |
* @returns {String} The padded string. | |
*/ | |
H.pad = function(number, length, padder) { | |
return new Array((length || 2) + 1 - | |
String(number).length).join(padder || 0) + number; | |
}; | |
/** | |
* @typedef {Number|String} RelativeSize - If a number is given, it defines the | |
* pixel length. If a percentage string is given, like for example `'50%'`, | |
* the setting defines a length relative to a base size, for example the size | |
* of a container. | |
*/ | |
/** | |
* Return a length based on either the integer value, or a percentage of a base. | |
* | |
* @function #relativeLength | |
* @memberOf Highcharts | |
* @param {RelativeSize} value - A percentage string or a number. | |
* @param {Number} base - The full length that represents 100%. | |
* @returns {Number} The computed length. | |
*/ | |
H.relativeLength = function(value, base) { | |
return (/%$/).test(value) ? | |
base * parseFloat(value) / 100 : | |
parseFloat(value); | |
}; | |
/** | |
* Wrap a method with extended functionality, preserving the original function. | |
* | |
* @function #wrap | |
* @memberOf Highcharts | |
* @param {Object} obj - The context object that the method belongs to. In real | |
* cases, this is often a prototype. | |
* @param {String} method - The name of the method to extend. | |
* @param {Function} func - A wrapper function callback. This function is called | |
* with the same arguments as the original function, except that the | |
* original function is unshifted and passed as the first argument. | |
* @returns {void} | |
*/ | |
H.wrap = function(obj, method, func) { | |
var proceed = obj[method]; | |
obj[method] = function() { | |
var args = Array.prototype.slice.call(arguments), | |
outerArgs = arguments, | |
ctx = this, | |
ret; | |
ctx.proceed = function() { | |
proceed.apply(ctx, arguments.length ? arguments : outerArgs); | |
}; | |
args.unshift(proceed); | |
ret = func.apply(this, args); | |
ctx.proceed = null; | |
return ret; | |
}; | |
}; | |
/** | |
* Get the time zone offset based on the current timezone information as set in | |
* the global options. | |
* | |
* @function #getTZOffset | |
* @memberOf Highcharts | |
* @param {Number} timestamp - The JavaScript timestamp to inspect. | |
* @return {Number} - The timezone offset in minutes compared to UTC. | |
*/ | |
H.getTZOffset = function(timestamp) { | |
var d = H.Date; | |
return ((d.hcGetTimezoneOffset && d.hcGetTimezoneOffset(timestamp)) || | |
d.hcTimezoneOffset || 0) * 60000; | |
}; | |
/** | |
* Formats a JavaScript date timestamp (milliseconds since Jan 1st 1970) into a | |
* human readable date string. The format is a subset of the formats for PHP's | |
* [strftime]{@link | |
* http://www.php.net/manual/en/function.strftime.php} function. Additional | |
* formats can be given in the {@link Highcharts.dateFormats} hook. | |
* | |
* @function #dateFormat | |
* @memberOf Highcharts | |
* @param {String} format - The desired format where various time | |
* representations are prefixed with %. | |
* @param {Number} timestamp - The JavaScript timestamp. | |
* @param {Boolean} [capitalize=false] - Upper case first letter in the return. | |
* @returns {String} The formatted date. | |
*/ | |
H.dateFormat = function(format, timestamp, capitalize) { | |
if (!H.defined(timestamp) || isNaN(timestamp)) { | |
return H.defaultOptions.lang.invalidDate || ''; | |
} | |
format = H.pick(format, '%Y-%m-%d %H:%M:%S'); | |
var D = H.Date, | |
date = new D(timestamp - H.getTZOffset(timestamp)), | |
// get the basic time values | |
hours = date[D.hcGetHours](), | |
day = date[D.hcGetDay](), | |
dayOfMonth = date[D.hcGetDate](), | |
month = date[D.hcGetMonth](), | |
fullYear = date[D.hcGetFullYear](), | |
lang = H.defaultOptions.lang, | |
langWeekdays = lang.weekdays, | |
shortWeekdays = lang.shortWeekdays, | |
pad = H.pad, | |
// List all format keys. Custom formats can be added from the outside. | |
replacements = H.extend({ | |
//-- Day | |
// Short weekday, like 'Mon' | |
'a': shortWeekdays ? | |
shortWeekdays[day] : langWeekdays[day].substr(0, 3), | |
// Long weekday, like 'Monday' | |
'A': langWeekdays[day], | |
// Two digit day of the month, 01 to 31 | |
'd': pad(dayOfMonth), | |
// Day of the month, 1 through 31 | |
'e': pad(dayOfMonth, 2, ' '), | |
'w': day, | |
// Week (none implemented) | |
//'W': weekNumber(), | |
//-- Month | |
// Short month, like 'Jan' | |
'b': lang.shortMonths[month], | |
// Long month, like 'January' | |
'B': lang.months[month], | |
// Two digit month number, 01 through 12 | |
'm': pad(month + 1), | |
//-- Year | |
// Two digits year, like 09 for 2009 | |
'y': fullYear.toString().substr(2, 2), | |
// Four digits year, like 2009 | |
'Y': fullYear, | |
//-- Time | |
// Two digits hours in 24h format, 00 through 23 | |
'H': pad(hours), | |
// Hours in 24h format, 0 through 23 | |
'k': hours, | |
// Two digits hours in 12h format, 00 through 11 | |
'I': pad((hours % 12) || 12), | |
// Hours in 12h format, 1 through 12 | |
'l': (hours % 12) || 12, | |
// Two digits minutes, 00 through 59 | |
'M': pad(date[D.hcGetMinutes]()), | |
// Upper case AM or PM | |
'p': hours < 12 ? 'AM' : 'PM', | |
// Lower case AM or PM | |
'P': hours < 12 ? 'am' : 'pm', | |
// Two digits seconds, 00 through 59 | |
'S': pad(date.getSeconds()), | |
// Milliseconds (naming from Ruby) | |
'L': pad(Math.round(timestamp % 1000), 3) | |
}, | |
/** | |
* A hook for defining additional date format specifiers. New | |
* specifiers are defined as key-value pairs by using the specifier | |
* as key, and a function which takes the timestamp as value. This | |
* function returns the formatted portion of the date. | |
* | |
* @type {Object} | |
* @name dateFormats | |
* @memberOf Highcharts | |
* @sample highcharts/global/dateformats/ Adding support for week | |
* number | |
*/ | |
H.dateFormats | |
); | |
// Do the replaces | |
H.objectEach(replacements, function(val, key) { | |
// Regex would do it in one line, but this is faster | |
while (format.indexOf('%' + key) !== -1) { | |
format = format.replace( | |
'%' + key, | |
typeof val === 'function' ? val(timestamp) : val | |
); | |
} | |
}); | |
// Optionally capitalize the string and return | |
return capitalize ? | |
format.substr(0, 1).toUpperCase() + format.substr(1) : | |
format; | |
}; | |
/** | |
* Format a single variable. Similar to sprintf, without the % prefix. | |
* | |
* @example | |
* formatSingle('.2f', 5); // => '5.00'. | |
* | |
* @function #formatSingle | |
* @memberOf Highcharts | |
* @param {String} format The format string. | |
* @param {*} val The value. | |
* @returns {String} The formatted representation of the value. | |
*/ | |
H.formatSingle = function(format, val) { | |
var floatRegex = /f$/, | |
decRegex = /\.([0-9])/, | |
lang = H.defaultOptions.lang, | |
decimals; | |
if (floatRegex.test(format)) { // float | |
decimals = format.match(decRegex); | |
decimals = decimals ? decimals[1] : -1; | |
if (val !== null) { | |
val = H.numberFormat( | |
val, | |
decimals, | |
lang.decimalPoint, | |
format.indexOf(',') > -1 ? lang.thousandsSep : '' | |
); | |
} | |
} else { | |
val = H.dateFormat(format, val); | |
} | |
return val; | |
}; | |
/** | |
* Format a string according to a subset of the rules of Python's String.format | |
* method. | |
* | |
* @function #format | |
* @memberOf Highcharts | |
* @param {String} str The string to format. | |
* @param {Object} ctx The context, a collection of key-value pairs where each | |
* key is replaced by its value. | |
* @returns {String} The formatted string. | |
* | |
* @example | |
* var s = Highcharts.format( | |
* 'The {color} fox was {len:.2f} feet long', | |
* { color: 'red', len: Math.PI } | |
* ); | |
* // => The red fox was 3.14 feet long | |
*/ | |
H.format = function(str, ctx) { | |
var splitter = '{', | |
isInside = false, | |
segment, | |
valueAndFormat, | |
path, | |
i, | |
len, | |
ret = [], | |
val, | |
index; | |
while (str) { | |
index = str.indexOf(splitter); | |
if (index === -1) { | |
break; | |
} | |
segment = str.slice(0, index); | |
if (isInside) { // we're on the closing bracket looking back | |
valueAndFormat = segment.split(':'); | |
path = valueAndFormat.shift().split('.'); // get first and leave | |
len = path.length; | |
val = ctx; | |
// Assign deeper paths | |
for (i = 0; i < len; i++) { | |
val = val[path[i]]; | |
} | |
// Format the replacement | |
if (valueAndFormat.length) { | |
val = H.formatSingle(valueAndFormat.join(':'), val); | |
} | |
// Push the result and advance the cursor | |
ret.push(val); | |
} else { | |
ret.push(segment); | |
} | |
str = str.slice(index + 1); // the rest | |
isInside = !isInside; // toggle | |
splitter = isInside ? '}' : '{'; // now look for next matching bracket | |
} | |
ret.push(str); | |
return ret.join(''); | |
}; | |
/** | |
* Get the magnitude of a number. | |
* | |
* @function #getMagnitude | |
* @memberOf Highcharts | |
* @param {Number} number The number. | |
* @returns {Number} The magnitude, where 1-9 are magnitude 1, 10-99 magnitude 2 | |
* etc. | |
*/ | |
H.getMagnitude = function(num) { | |
return Math.pow(10, Math.floor(Math.log(num) / Math.LN10)); | |
}; | |
/** | |
* Take an interval and normalize it to multiples of round numbers. | |
* | |
* @todo Move this function to the Axis prototype. It is here only for | |
* historical reasons. | |
* @function #normalizeTickInterval | |
* @memberOf Highcharts | |
* @param {Number} interval - The raw, un-rounded interval. | |
* @param {Array} [multiples] - Allowed multiples. | |
* @param {Number} [magnitude] - The magnitude of the number. | |
* @param {Boolean} [allowDecimals] - Whether to allow decimals. | |
* @param {Boolean} [hasTickAmount] - If it has tickAmount, avoid landing | |
* on tick intervals lower than original. | |
* @returns {Number} The normalized interval. | |
*/ | |
H.normalizeTickInterval = function(interval, multiples, magnitude, | |
allowDecimals, hasTickAmount) { | |
var normalized, | |
i, | |
retInterval = interval; | |
// round to a tenfold of 1, 2, 2.5 or 5 | |
magnitude = H.pick(magnitude, 1); | |
normalized = interval / magnitude; | |
// multiples for a linear scale | |
if (!multiples) { | |
multiples = hasTickAmount ? | |
// Finer grained ticks when the tick amount is hard set, including | |
// when alignTicks is true on multiple axes (#4580). | |
[1, 1.2, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10] : | |
// Else, let ticks fall on rounder numbers | |
[1, 2, 2.5, 5, 10]; | |
// the allowDecimals option | |
if (allowDecimals === false) { | |
if (magnitude === 1) { | |
multiples = H.grep(multiples, function(num) { | |
return num % 1 === 0; | |
}); | |
} else if (magnitude <= 0.1) { | |
multiples = [1 / magnitude]; | |
} | |
} | |
} | |
// normalize the interval to the nearest multiple | |
for (i = 0; i < multiples.length; i++) { | |
retInterval = multiples[i]; | |
// only allow tick amounts smaller than natural | |
if ((hasTickAmount && retInterval * magnitude >= interval) || | |
(!hasTickAmount && (normalized <= (multiples[i] + | |
(multiples[i + 1] || multiples[i])) / 2))) { | |
break; | |
} | |
} | |
// Multiply back to the correct magnitude. Correct floats to appropriate | |
// precision (#6085). | |
retInterval = H.correctFloat( | |
retInterval * magnitude, -Math.round(Math.log(0.001) / Math.LN10) | |
); | |
return retInterval; | |
}; | |
/** | |
* Sort an object array and keep the order of equal items. The ECMAScript | |
* standard does not specify the behaviour when items are equal. | |
* | |
* @function #stableSort | |
* @memberOf Highcharts | |
* @param {Array} arr - The array to sort. | |
* @param {Function} sortFunction - The function to sort it with, like with | |
* regular Array.prototype.sort. | |
* @returns {void} | |
*/ | |
H.stableSort = function(arr, sortFunction) { | |
var length = arr.length, | |
sortValue, | |
i; | |
// Add index to each item | |
for (i = 0; i < length; i++) { | |
arr[i].safeI = i; // stable sort index | |
} | |
arr.sort(function(a, b) { | |
sortValue = sortFunction(a, b); | |
return sortValue === 0 ? a.safeI - b.safeI : sortValue; | |
}); | |
// Remove index from items | |
for (i = 0; i < length; i++) { | |
delete arr[i].safeI; // stable sort index | |
} | |
}; | |
/** | |
* Non-recursive method to find the lowest member of an array. `Math.min` raises | |
* a maximum call stack size exceeded error in Chrome when trying to apply more | |
* than 150.000 points. This method is slightly slower, but safe. | |
* | |
* @function #arrayMin | |
* @memberOf Highcharts | |
* @param {Array} data An array of numbers. | |
* @returns {Number} The lowest number. | |
*/ | |
H.arrayMin = function(data) { | |
var i = data.length, | |
min = data[0]; | |
while (i--) { | |
if (data[i] < min) { | |
min = data[i]; | |
} | |
} | |
return min; | |
}; | |
/** | |
* Non-recursive method to find the lowest member of an array. `Math.max` raises | |
* a maximum call stack size exceeded error in Chrome when trying to apply more | |
* than 150.000 points. This method is slightly slower, but safe. | |
* | |
* @function #arrayMax | |
* @memberOf Highcharts | |
* @param {Array} data - An array of numbers. | |
* @returns {Number} The highest number. | |
*/ | |
H.arrayMax = function(data) { | |
var i = data.length, | |
max = data[0]; | |
while (i--) { | |
if (data[i] > max) { | |
max = data[i]; | |
} | |
} | |
return max; | |
}; | |
/** | |
* Utility method that destroys any SVGElement instances that are properties on | |
* the given object. It loops all properties and invokes destroy if there is a | |
* destroy method. The property is then delete. | |
* | |
* @function #destroyObjectProperties | |
* @memberOf Highcharts | |
* @param {Object} obj - The object to destroy properties on. | |
* @param {Object} [except] - Exception, do not destroy this property, only | |
* delete it. | |
* @returns {void} | |
*/ | |
H.destroyObjectProperties = function(obj, except) { | |
H.objectEach(obj, function(val, n) { | |
// If the object is non-null and destroy is defined | |
if (val && val !== except && val.destroy) { | |
// Invoke the destroy | |
val.destroy(); | |
} | |
// Delete the property from the object. | |
delete obj[n]; | |
}); | |
}; | |
/** | |
* Discard a HTML element by moving it to the bin and delete. | |
* | |
* @function #discardElement | |
* @memberOf Highcharts | |
* @param {HTMLDOMElement} element - The HTML node to discard. | |
* @returns {void} | |
*/ | |
H.discardElement = function(element) { | |
var garbageBin = H.garbageBin; | |
// create a garbage bin element, not part of the DOM | |
if (!garbageBin) { | |
garbageBin = H.createElement('div'); | |
} | |
// move the node and empty bin | |
if (element) { | |
garbageBin.appendChild(element); | |
} | |
garbageBin.innerHTML = ''; | |
}; | |
/** | |
* Fix JS round off float errors. | |
* | |
* @function #correctFloat | |
* @memberOf Highcharts | |
* @param {Number} num - A float number to fix. | |
* @param {Number} [prec=14] - The precision. | |
* @returns {Number} The corrected float number. | |
*/ | |
H.correctFloat = function(num, prec) { | |
return parseFloat( | |
num.toPrecision(prec || 14) | |
); | |
}; | |
/** | |
* Set the global animation to either a given value, or fall back to the given | |
* chart's animation option. | |
* | |
* @function #setAnimation | |
* @memberOf Highcharts | |
* @param {Boolean|Animation} animation - The animation object. | |
* @param {Object} chart - The chart instance. | |
* @returns {void} | |
* @todo This function always relates to a chart, and sets a property on the | |
* renderer, so it should be moved to the SVGRenderer. | |
*/ | |
H.setAnimation = function(animation, chart) { | |
chart.renderer.globalAnimation = H.pick( | |
animation, | |
chart.options.chart.animation, | |
true | |
); | |
}; | |
/** | |
* Get the animation in object form, where a disabled animation is always | |
* returned as `{ duration: 0 }`. | |
* | |
* @function #animObject | |
* @memberOf Highcharts | |
* @param {Boolean|AnimationOptions} animation - An animation setting. Can be an | |
* object with duration, complete and easing properties, or a boolean to | |
* enable or disable. | |
* @returns {AnimationOptions} An object with at least a duration property. | |
*/ | |
H.animObject = function(animation) { | |
return H.isObject(animation) ? | |
H.merge(animation) : { | |
duration: animation ? 500 : 0 | |
}; | |
}; | |
/** | |
* The time unit lookup | |
*/ | |
H.timeUnits = { | |
millisecond: 1, | |
second: 1000, | |
minute: 60000, | |
hour: 3600000, | |
day: 24 * 3600000, | |
week: 7 * 24 * 3600000, | |
month: 28 * 24 * 3600000, | |
year: 364 * 24 * 3600000 | |
}; | |
/** | |
* Format a number and return a string based on input settings. | |
* | |
* @function #numberFormat | |
* @memberOf Highcharts | |
* @param {Number} number - The input number to format. | |
* @param {Number} decimals - The amount of decimals. A value of -1 preserves | |
* the amount in the input number. | |
* @param {String} [decimalPoint] - The decimal point, defaults to the one given | |
* in the lang options, or a dot. | |
* @param {String} [thousandsSep] - The thousands separator, defaults to the one | |
* given in the lang options, or a space character. | |
* @returns {String} The formatted number. | |
* | |
* @sample members/highcharts-numberformat/ Custom number format | |
*/ | |
H.numberFormat = function(number, decimals, decimalPoint, thousandsSep) { | |
number = +number || 0; | |
decimals = +decimals; | |
var lang = H.defaultOptions.lang, | |
origDec = (number.toString().split('.')[1] || '').split('e')[0].length, | |
strinteger, | |
thousands, | |
ret, | |
roundedNumber, | |
exponent = number.toString().split('e'); | |
if (decimals === -1) { | |
// Preserve decimals. Not huge numbers (#3793). | |
decimals = Math.min(origDec, 20); | |
} else if (!H.isNumber(decimals)) { | |
decimals = 2; | |
} | |
// Add another decimal to avoid rounding errors of float numbers. (#4573) | |
// Then use toFixed to handle rounding. | |
roundedNumber = ( | |
Math.abs(exponent[1] ? exponent[0] : number) + | |
Math.pow(10, -Math.max(decimals, origDec) - 1) | |
).toFixed(decimals); | |
// A string containing the positive integer component of the number | |
strinteger = String(H.pInt(roundedNumber)); | |
// Leftover after grouping into thousands. Can be 0, 1 or 3. | |
thousands = strinteger.length > 3 ? strinteger.length % 3 : 0; | |
// Language | |
decimalPoint = H.pick(decimalPoint, lang.decimalPoint); | |
thousandsSep = H.pick(thousandsSep, lang.thousandsSep); | |
// Start building the return | |
ret = number < 0 ? '-' : ''; | |
// Add the leftover after grouping into thousands. For example, in the | |
// number 42 000 000, this line adds 42. | |
ret += thousands ? strinteger.substr(0, thousands) + thousandsSep : ''; | |
// Add the remaining thousands groups, joined by the thousands separator | |
ret += strinteger | |
.substr(thousands) | |
.replace(/(\d{3})(?=\d)/g, '$1' + thousandsSep); | |
// Add the decimal point and the decimal component | |
if (decimals) { | |
// Get the decimal component | |
ret += decimalPoint + roundedNumber.slice(-decimals); | |
} | |
if (exponent[1]) { | |
ret += 'e' + exponent[1]; | |
} | |
return ret; | |
}; | |
/** | |
* Easing definition | |
* @ignore | |
* @param {Number} pos Current position, ranging from 0 to 1. | |
*/ | |
Math.easeInOutSine = function(pos) { | |
return -0.5 * (Math.cos(Math.PI * pos) - 1); | |
}; | |
/** | |
* Get the computed CSS value for given element and property, only for numerical | |
* properties. For width and height, the dimension of the inner box (excluding | |
* padding) is returned. Used for fitting the chart within the container. | |
* | |
* @function #getStyle | |
* @memberOf Highcharts | |
* @param {HTMLDOMElement} el - A HTML element. | |
* @param {String} prop - The property name. | |
* @param {Boolean} [toInt=true] - Parse to integer. | |
* @returns {Number} - The numeric value. | |
*/ | |
H.getStyle = function(el, prop, toInt) { | |
var style; | |
// For width and height, return the actual inner pixel size (#4913) | |
if (prop === 'width') { | |
return Math.min(el.offsetWidth, el.scrollWidth) - | |
H.getStyle(el, 'padding-left') - | |
H.getStyle(el, 'padding-right'); | |
} else if (prop === 'height') { | |
return Math.min(el.offsetHeight, el.scrollHeight) - | |
H.getStyle(el, 'padding-top') - | |
H.getStyle(el, 'padding-bottom'); | |
} | |
// Otherwise, get the computed style | |
style = win.getComputedStyle(el, undefined); | |
if (style) { | |
style = style.getPropertyValue(prop); | |
if (H.pick(toInt, true)) { | |
style = H.pInt(style); | |
} | |
} | |
return style; | |
}; | |
/** | |
* Search for an item in an array. | |
* | |
* @function #inArray | |
* @memberOf Highcharts | |
* @param {*} item - The item to search for. | |
* @param {arr} arr - The array or node collection to search in. | |
* @returns {Number} - The index within the array, or -1 if not found. | |
*/ | |
H.inArray = function(item, arr) { | |
return arr.indexOf ? arr.indexOf(item) : [].indexOf.call(arr, item); | |
}; | |
/** | |
* Filter an array by a callback. | |
* | |
* @function #grep | |
* @memberOf Highcharts | |
* @param {Array} arr - The array to filter. | |
* @param {Function} callback - The callback function. The function receives the | |
* item as the first argument. Return `true` if the item is to be | |
* preserved. | |
* @returns {Array} - A new, filtered array. | |
*/ | |
H.grep = function(arr, callback) { | |
return [].filter.call(arr, callback); | |
}; | |
/** | |
* Return the value of the first element in the array that satisfies the | |
* provided testing function. | |
* | |
* @function #find | |
* @memberOf Highcharts | |
* @param {Array} arr - The array to test. | |
* @param {Function} callback - The callback function. The function receives the | |
* item as the first argument. Return `true` if this item satisfies the | |
* condition. | |
* @returns {Mixed} - The value of the element. | |
*/ | |
H.find = function(arr, callback) { | |
return [].find.call(arr, callback); | |
}; | |
/** | |
* Map an array by a callback. | |
* | |
* @function #map | |
* @memberOf Highcharts | |
* @param {Array} arr - The array to map. | |
* @param {Function} fn - The callback function. Return the new value for the | |
* new array. | |
* @returns {Array} - A new array item with modified items. | |
*/ | |
H.map = function(arr, fn) { | |
var results = [], | |
i = 0, | |
len = arr.length; | |
for (; i < len; i++) { | |
results[i] = fn.call(arr[i], arr[i], i, arr); | |
} | |
return results; | |
}; | |
/** | |
* Get the element's offset position, corrected for `overflow: auto`. | |
* | |
* @function #offset | |
* @memberOf Highcharts | |
* @param {HTMLDOMElement} el - The HTML element. | |
* @returns {Object} An object containing `left` and `top` properties for the | |
* position in the page. | |
*/ | |
H.offset = function(el) { | |
var docElem = doc.documentElement, | |
box = el.getBoundingClientRect(); | |
return { | |
top: box.top + (win.pageYOffset || docElem.scrollTop) - | |
(docElem.clientTop || 0), | |
left: box.left + (win.pageXOffset || docElem.scrollLeft) - | |
(docElem.clientLeft || 0) | |
}; | |
}; | |
/** | |
* Stop running animation. | |
* | |
* @todo A possible extension to this would be to stop a single property, when | |
* we want to continue animating others. Then assign the prop to the timer | |
* in the Fx.run method, and check for the prop here. This would be an | |
* improvement in all cases where we stop the animation from .attr. Instead of | |
* stopping everything, we can just stop the actual attributes we're setting. | |
* | |
* @function #stop | |
* @memberOf Highcharts | |
* @param {SVGElement} el - The SVGElement to stop animation on. | |
* @param {string} [prop] - The property to stop animating. If given, the stop | |
* method will stop a single property from animating, while others continue. | |
* @returns {void} | |
*/ | |
H.stop = function(el, prop) { | |
var i = timers.length; | |
// Remove timers related to this element (#4519) | |
while (i--) { | |
if (timers[i].elem === el && (!prop || prop === timers[i].prop)) { | |
timers[i].stopped = true; // #4667 | |
} | |
} | |
}; | |
/** | |
* Iterate over an array. | |
* | |
* @function #each | |
* @memberOf Highcharts | |
* @param {Array} arr - The array to iterate over. | |
* @param {Function} fn - The iterator callback. It passes three arguments: | |
* * item - The array item. | |
* * index - The item's index in the array. | |
* * arr - The array that each is being applied to. | |
* @param {Object} [ctx] The context. | |
*/ | |
H.each = function(arr, fn, ctx) { // modern browsers | |
return Array.prototype.forEach.call(arr, fn, ctx); | |
}; | |
/** | |
* Iterate over object key pairs in an object. | |
* | |
* @function #objectEach | |
* @memberOf Highcharts | |
* @param {Object} obj - The object to iterate over. | |
* @param {Function} fn - The iterator callback. It passes three arguments: | |
* * value - The property value. | |
* * key - The property key. | |
* * obj - The object that objectEach is being applied to. | |
* @param {Object} ctx The context | |
*/ | |
H.objectEach = function(obj, fn, ctx) { | |
for (var key in obj) { | |
if (obj.hasOwnProperty(key)) { | |
fn.call(ctx, obj[key], key, obj); | |
} | |
} | |
}; | |
/** | |
* Add an event listener. | |
* | |
* @function #addEvent | |
* @memberOf Highcharts | |
* @param {Object} el - The element or object to add a listener to. It can be a | |
* {@link HTMLDOMElement}, an {@link SVGElement} or any other object. | |
* @param {String} type - The event type. | |
* @param {Function} fn - The function callback to execute when the event is | |
* fired. | |
* @returns {Function} A callback function to remove the added event. | |
*/ | |
H.addEvent = function(el, type, fn) { | |
var events = el.hcEvents = el.hcEvents || {}; | |
function wrappedFn(e) { | |
e.target = e.srcElement || win; // #2820 | |
fn.call(el, e); | |
} | |
// Handle DOM events in modern browsers | |
if (el.addEventListener) { | |
el.addEventListener(type, fn, false); | |
// Handle old IE implementation | |
} else if (el.attachEvent) { | |
if (!el.hcEventsIE) { | |
el.hcEventsIE = {}; | |
} | |
// Link wrapped fn with original fn, so we can get this in removeEvent | |
el.hcEventsIE[fn.toString()] = wrappedFn; | |
el.attachEvent('on' + type, wrappedFn); | |
} | |
if (!events[type]) { | |
events[type] = []; | |
} | |
events[type].push(fn); | |
// Return a function that can be called to remove this event. | |
return function() { | |
H.removeEvent(el, type, fn); | |
}; | |
}; | |
/** | |
* Remove an event that was added with {@link Highcharts#addEvent}. | |
* | |
* @function #removeEvent | |
* @memberOf Highcharts | |
* @param {Object} el - The element to remove events on. | |
* @param {String} [type] - The type of events to remove. If undefined, all | |
* events are removed from the element. | |
* @param {Function} [fn] - The specific callback to remove. If undefined, all | |
* events that match the element and optionally the type are removed. | |
* @returns {void} | |
*/ | |
H.removeEvent = function(el, type, fn) { | |
var events, | |
hcEvents = el.hcEvents, | |
index; | |
function removeOneEvent(type, fn) { | |
if (el.removeEventListener) { | |
el.removeEventListener(type, fn, false); | |
} else if (el.attachEvent) { | |
fn = el.hcEventsIE[fn.toString()]; | |
el.detachEvent('on' + type, fn); | |
} | |
} | |
function removeAllEvents() { | |
var types, | |
len; | |
if (!el.nodeName) { | |
return; // break on non-DOM events | |
} | |
if (type) { | |
types = {}; | |
types[type] = true; | |
} else { | |
types = hcEvents; | |
} | |
H.objectEach(types, function(val, n) { | |
if (hcEvents[n]) { | |
len = hcEvents[n].length; | |
while (len--) { | |
removeOneEvent(n, hcEvents[n][len]); | |
} | |
} | |
}); | |
} | |
if (hcEvents) { | |
if (type) { | |
events = hcEvents[type] || []; | |
if (fn) { | |
index = H.inArray(fn, events); | |
if (index > -1) { | |
events.splice(index, 1); | |
hcEvents[type] = events; | |
} | |
removeOneEvent(type, fn); | |
} else { | |
removeAllEvents(); | |
hcEvents[type] = []; | |
} | |
} else { | |
removeAllEvents(); | |
el.hcEvents = {}; | |
} | |
} | |
}; | |
/** | |
* Fire an event that was registered with {@link Highcharts#addEvent}. | |
* | |
* @function #fireEvent | |
* @memberOf Highcharts | |
* @param {Object} el - The object to fire the event on. It can be a | |
* {@link HTMLDOMElement}, an {@link SVGElement} or any other object. | |
* @param {String} type - The type of event. | |
* @param {Object} [eventArguments] - Custom event arguments that are passed on | |
* as an argument to the event handler. | |
* @param {Function} [defaultFunction] - The default function to execute if the | |
* other listeners haven't returned false. | |
* @returns {void} | |
*/ | |
H.fireEvent = function(el, type, eventArguments, defaultFunction) { | |
var e, | |
hcEvents = el.hcEvents, | |
events, | |
len, | |
i, | |
fn; | |
eventArguments = eventArguments || {}; | |
if (doc.createEvent && (el.dispatchEvent || el.fireEvent)) { | |
e = doc.createEvent('Events'); | |
e.initEvent(type, true, true); | |
//e.target = el; | |
H.extend(e, eventArguments); | |
if (el.dispatchEvent) { | |
el.dispatchEvent(e); | |
} else { | |
el.fireEvent(type, e); | |
} | |
} else if (hcEvents) { | |
events = hcEvents[type] || []; | |
len = events.length; | |
if (!eventArguments.target) { // We're running a custom event | |
H.extend(eventArguments, { | |
// Attach a simple preventDefault function to skip default | |
// handler if called. The built-in defaultPrevented property is | |
// not overwritable (#5112) | |
preventDefault: function() { | |
eventArguments.defaultPrevented = true; | |
}, | |
// Setting target to native events fails with clicking the | |
// zoom-out button in Chrome. | |
target: el, | |
// If the type is not set, we're running a custom event (#2297). | |
// If it is set, we're running a browser event, and setting it | |
// will cause en error in IE8 (#2465). | |
type: type | |
}); | |
} | |
for (i = 0; i < len; i++) { | |
fn = events[i]; | |
// If the event handler return false, prevent the default handler | |
// from executing | |
if (fn && fn.call(el, eventArguments) === false) { | |
eventArguments.preventDefault(); | |
} | |
} | |
} | |
// Run the default if not prevented | |
if (defaultFunction && !eventArguments.defaultPrevented) { | |
defaultFunction(eventArguments); | |
} | |
}; | |
/** | |
* An animation configuration. Animation configurations can also be defined as | |
* booleans, where `false` turns off animation and `true` defaults to a duration | |
* of 500ms. | |
* @typedef {Object} AnimationOptions | |
* @property {Number} duration - The animation duration in milliseconds. | |
* @property {String} [easing] - The name of an easing function as defined on | |
* the `Math` object. | |
* @property {Function} [complete] - A callback function to exectute when the | |
* animation finishes. | |
* @property {Function} [step] - A callback function to execute on each step of | |
* each attribute or CSS property that's being animated. The first argument | |
* contains information about the animation and progress. | |
*/ | |
/** | |
* The global animate method, which uses Fx to create individual animators. | |
* | |
* @function #animate | |
* @memberOf Highcharts | |
* @param {HTMLDOMElement|SVGElement} el - The element to animate. | |
* @param {Object} params - An object containing key-value pairs of the | |
* properties to animate. Supports numeric as pixel-based CSS properties | |
* for HTML objects and attributes for SVGElements. | |
* @param {AnimationOptions} [opt] - Animation options. | |
*/ | |
H.animate = function(el, params, opt) { | |
var start, | |
unit = '', | |
end, | |
fx, | |
args; | |
if (!H.isObject(opt)) { // Number or undefined/null | |
args = arguments; | |
opt = { | |
duration: args[2], | |
easing: args[3], | |
complete: args[4] | |
}; | |
} | |
if (!H.isNumber(opt.duration)) { | |
opt.duration = 400; | |
} | |
opt.easing = typeof opt.easing === 'function' ? | |
opt.easing : | |
(Math[opt.easing] || Math.easeInOutSine); | |
opt.curAnim = H.merge(params); | |
H.objectEach(params, function(val, prop) { | |
// Stop current running animation of this property | |
H.stop(el, prop); | |
fx = new H.Fx(el, opt, prop); | |
end = null; | |
if (prop === 'd') { | |
fx.paths = fx.initPath( | |
el, | |
el.d, | |
params.d | |
); | |
fx.toD = params.d; | |
start = 0; | |
end = 1; | |
} else if (el.attr) { | |
start = el.attr(prop); | |
} else { | |
start = parseFloat(H.getStyle(el, prop)) || 0; | |
if (prop !== 'opacity') { | |
unit = 'px'; | |
} | |
} | |
if (!end) { | |
end = val; | |
} | |
if (end && end.match && end.match('px')) { | |
end = end.replace(/px/g, ''); // #4351 | |
} | |
fx.run(start, end, unit); | |
}); | |
}; | |
/** | |
* Factory to create new series prototypes. | |
* | |
* @function #seriesType | |
* @memberOf Highcharts | |
* | |
* @param {String} type - The series type name. | |
* @param {String} parent - The parent series type name. Use `line` to inherit | |
* from the basic {@link Series} object. | |
* @param {Object} options - The additional default options that is merged with | |
* the parent's options. | |
* @param {Object} props - The properties (functions and primitives) to set on | |
* the new prototype. | |
* @param {Object} [pointProps] - Members for a series-specific extension of the | |
* {@link Point} prototype if needed. | |
* @returns {*} - The newly created prototype as extended from {@link Series} | |
* or its derivatives. | |
*/ | |
// docs: add to API + extending Highcharts | |
H.seriesType = function(type, parent, options, props, pointProps) { | |
var defaultOptions = H.getOptions(), | |
seriesTypes = H.seriesTypes; | |
// Merge the options | |
defaultOptions.plotOptions[type] = H.merge( | |
defaultOptions.plotOptions[parent], | |
options | |
); | |
// Create the class | |
seriesTypes[type] = H.extendClass(seriesTypes[parent] || | |
function() {}, props); | |
seriesTypes[type].prototype.type = type; | |
// Create the point class if needed | |
if (pointProps) { | |
seriesTypes[type].prototype.pointClass = | |
H.extendClass(H.Point, pointProps); | |
} | |
return seriesTypes[type]; | |
}; | |
/** | |
* Get a unique key for using in internal element id's and pointers. The key | |
* is composed of a random hash specific to this Highcharts instance, and a | |
* counter. | |
* @function #uniqueKey | |
* @memberOf Highcharts | |
* @return {string} The key. | |
* @example | |
* var id = H.uniqueKey(); // => 'highcharts-x45f6hp-0' | |
*/ | |
H.uniqueKey = (function() { | |
var uniqueKeyHash = Math.random().toString(36).substring(2, 9), | |
idCounter = 0; | |
return function() { | |
return 'highcharts-' + uniqueKeyHash + '-' + idCounter++; | |
}; | |
}()); | |
/** | |
* Register Highcharts as a plugin in jQuery | |
*/ | |
if (win.jQuery) { | |
win.jQuery.fn.highcharts = function() { | |
var args = [].slice.call(arguments); | |
if (this[0]) { // this[0] is the renderTo div | |
// Create the chart | |
if (args[0]) { | |
new H[ // eslint-disable-line no-new | |
// Constructor defaults to Chart | |
H.isString(args[0]) ? args.shift() : 'Chart' | |
](this[0], args[0], args[1]); | |
return this; | |
} | |
// When called without parameters or with the return argument, | |
// return an existing chart | |
return charts[H.attr(this[0], 'data-highcharts-chart')]; | |
} | |
}; | |
} | |
/** | |
* Compatibility section to add support for legacy IE. This can be removed if | |
* old IE support is not needed. | |
*/ | |
if (doc && !doc.defaultView) { | |
H.getStyle = function(el, prop) { | |
var val, | |
alias = { | |
width: 'clientWidth', | |
height: 'clientHeight' | |
}[prop]; | |
if (el.style[prop]) { | |
return H.pInt(el.style[prop]); | |
} | |
if (prop === 'opacity') { | |
prop = 'filter'; | |
} | |
// Getting the rendered width and height | |
if (alias) { | |
el.style.zoom = 1; | |
return Math.max(el[alias] - 2 * H.getStyle(el, 'padding'), 0); | |
} | |
val = el.currentStyle[prop.replace(/\-(\w)/g, function(a, b) { | |
return b.toUpperCase(); | |
})]; | |
if (prop === 'filter') { | |
val = val.replace( | |
/alpha\(opacity=([0-9]+)\)/, | |
function(a, b) { | |
return b / 100; | |
} | |
); | |
} | |
return val === '' ? 1 : H.pInt(val); | |
}; | |
} | |
if (!Array.prototype.forEach) { | |
H.each = function(arr, fn, ctx) { // legacy | |
var i = 0, | |
len = arr.length; | |
for (; i < len; i++) { | |
if (fn.call(ctx, arr[i], i, arr) === false) { | |
return i; | |
} | |
} | |
}; | |
} | |
if (!Array.prototype.indexOf) { | |
H.inArray = function(item, arr) { | |
var len, | |
i = 0; | |
if (arr) { | |
len = arr.length; | |
for (; i < len; i++) { | |
if (arr[i] === item) { | |
return i; | |
} | |
} | |
} | |
return -1; | |
}; | |
} | |
if (!Array.prototype.filter) { | |
H.grep = function(elements, fn) { | |
var ret = [], | |
i = 0, | |
length = elements.length; | |
for (; i < length; i++) { | |
if (fn(elements[i], i)) { | |
ret.push(elements[i]); | |
} | |
} | |
return ret; | |
}; | |
} | |
if (!Array.prototype.find) { | |
H.find = function(arr, fn) { | |
var i, | |
length = arr.length; | |
for (i = 0; i < length; i++) { | |
if (fn(arr[i], i)) { | |
return arr[i]; | |
} | |
} | |
}; | |
} | |
//--- End compatibility section --- | |
}(Highcharts)); | |
(function(H) { | |
/** | |
* (c) 2010-2017 Torstein Honsi | |
* | |
* License: www.highcharts.com/license | |
*/ | |
var each = H.each, | |
isNumber = H.isNumber, | |
map = H.map, | |
merge = H.merge, | |
pInt = H.pInt; | |
/** | |
* @typedef {string} ColorString | |
* A valid color to be parsed and handled by Highcharts. Highcharts internally | |
* supports hex colors like `#ffffff`, rgb colors like `rgb(255,255,255)` and | |
* rgba colors like `rgba(255,255,255,1)`. Other colors may be supported by the | |
* browsers and displayed correctly, but Highcharts is not able to process them | |
* and apply concepts like opacity and brightening. | |
*/ | |
/** | |
* Handle color operations. The object methods are chainable. | |
* @param {String} input The input color in either rbga or hex format | |
*/ | |
H.Color = function(input) { | |
// Backwards compatibility, allow instanciation without new | |
if (!(this instanceof H.Color)) { | |
return new H.Color(input); | |
} | |
// Initialize | |
this.init(input); | |
}; | |
H.Color.prototype = { | |
// Collection of parsers. This can be extended from the outside by pushing parsers | |
// to Highcharts.Color.prototype.parsers. | |
parsers: [{ | |
// RGBA color | |
regex: /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*\)/, | |
parse: function(result) { | |
return [pInt(result[1]), pInt(result[2]), pInt(result[3]), parseFloat(result[4], 10)]; | |
} | |
}, { | |
// RGB color | |
regex: /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/, | |
parse: function(result) { | |
return [pInt(result[1]), pInt(result[2]), pInt(result[3]), 1]; | |
} | |
}], | |
// Collection of named colors. Can be extended from the outside by adding | |
// colors to Highcharts.Color.prototype.names. | |
names: { | |
none: 'rgba(255,255,255,0)', | |
white: '#ffffff', | |
black: '#000000' | |
}, | |
/** | |
* Parse the input color to rgba array | |
* @param {String} input | |
*/ | |
init: function(input) { | |
var result, | |
rgba, | |
i, | |
parser, | |
len; | |
this.input = input = this.names[ | |
input && input.toLowerCase ? | |
input.toLowerCase() : | |
'' | |
] || input; | |
// Gradients | |
if (input && input.stops) { | |
this.stops = map(input.stops, function(stop) { | |
return new H.Color(stop[1]); | |
}); | |
// Solid colors | |
} else { | |
// Check if it's possible to do bitmasking instead of regex | |
if (input && input[0] === '#') { | |
len = input.length; | |
input = parseInt(input.substr(1), 16); | |
// Handle long-form, e.g. #AABBCC | |
if (len === 7) { | |
rgba = [ | |
(input & 0xFF0000) >> 16, | |
(input & 0xFF00) >> 8, | |
(input & 0xFF), | |
1 | |
]; | |
// Handle short-form, e.g. #ABC | |
// In short form, the value is assumed to be the same | |
// for both nibbles for each component. e.g. #ABC = #AABBCC | |
} else if (len === 4) { | |
rgba = [ | |
((input & 0xF00) >> 4) | (input & 0xF00) >> 8, | |
((input & 0xF0) >> 4) | (input & 0xF0), | |
((input & 0xF) << 4) | (input & 0xF), | |
1 | |
]; | |
} | |
} | |
// Otherwise, check regex parsers | |
if (!rgba) { | |
i = this.parsers.length; | |
while (i-- && !rgba) { | |
parser = this.parsers[i]; | |
result = parser.regex.exec(input); | |
if (result) { | |
rgba = parser.parse(result); | |
} | |
} | |
} | |
} | |
this.rgba = rgba || []; | |
}, | |
/** | |
* Return the color a specified format | |
* @param {String} format | |
*/ | |
get: function(format) { | |
var input = this.input, | |
rgba = this.rgba, | |
ret; | |
if (this.stops) { | |
ret = merge(input); | |
ret.stops = [].concat(ret.stops); | |
each(this.stops, function(stop, i) { | |
ret.stops[i] = [ret.stops[i][0], stop.get(format)]; | |
}); | |
// it's NaN if gradient colors on a column chart | |
} else if (rgba && isNumber(rgba[0])) { | |
if (format === 'rgb' || (!format && rgba[3] === 1)) { | |
ret = 'rgb(' + rgba[0] + ',' + rgba[1] + ',' + rgba[2] + ')'; | |
} else if (format === 'a') { | |
ret = rgba[3]; | |
} else { | |
ret = 'rgba(' + rgba.join(',') + ')'; | |
} | |
} else { | |
ret = input; | |
} | |
return ret; | |
}, | |
/** | |
* Brighten the color | |
* @param {Number} alpha | |
*/ | |
brighten: function(alpha) { | |
var i, | |
rgba = this.rgba; | |
if (this.stops) { | |
each(this.stops, function(stop) { | |
stop.brighten(alpha); | |
}); | |
} else if (isNumber(alpha) && alpha !== 0) { | |
for (i = 0; i < 3; i++) { | |
rgba[i] += pInt(alpha * 255); | |
if (rgba[i] < 0) { | |
rgba[i] = 0; | |
} | |
if (rgba[i] > 255) { | |
rgba[i] = 255; | |
} | |
} | |
} | |
return this; | |
}, | |
/** | |
* Set the color's opacity to a given alpha value | |
* @param {Number} alpha | |
*/ | |
setOpacity: function(alpha) { | |
this.rgba[3] = alpha; | |
return this; | |
}, | |
/* | |
* Return an intermediate color between two colors. | |
* | |
* @param {Highcharts.Color} to | |
* The color object to tween to. | |
* @param {Number} pos | |
* The intermediate position, where 0 is the from color (current | |
* color item), and 1 is the `to` color. | |
* | |
* @return {String} | |
* The intermediate color in rgba notation. | |
*/ | |
tweenTo: function(to, pos) { | |
// Check for has alpha, because rgba colors perform worse due to lack of | |
// support in WebKit. | |
var from = this, | |
hasAlpha, | |
ret; | |
// Unsupported color, return to-color (#3920) | |
if (!to.rgba.length) { | |
ret = to.input || 'none'; | |
// Interpolate | |
} else { | |
from = from.rgba; | |
to = to.rgba; | |
hasAlpha = (to[3] !== 1 || from[3] !== 1); | |
ret = (hasAlpha ? 'rgba(' : 'rgb(') + | |
Math.round(to[0] + (from[0] - to[0]) * (1 - pos)) + ',' + | |
Math.round(to[1] + (from[1] - to[1]) * (1 - pos)) + ',' + | |
Math.round(to[2] + (from[2] - to[2]) * (1 - pos)) + | |
(hasAlpha ? | |
(',' + (to[3] + (from[3] - to[3]) * (1 - pos))) : | |
'') + ')'; | |
} | |
return ret; | |
} | |
}; | |
H.color = function(input) { | |
return new H.Color(input); | |
}; | |
}(Highcharts)); | |
(function(H) { | |
/** | |
* (c) 2010-2017 Torstein Honsi | |
* | |
* License: www.highcharts.com/license | |
*/ | |
var SVGElement, | |
SVGRenderer, | |
addEvent = H.addEvent, | |
animate = H.animate, | |
attr = H.attr, | |
charts = H.charts, | |
color = H.color, | |
css = H.css, | |
createElement = H.createElement, | |
defined = H.defined, | |
deg2rad = H.deg2rad, | |
destroyObjectProperties = H.destroyObjectProperties, | |
doc = H.doc, | |
each = H.each, | |
extend = H.extend, | |
erase = H.erase, | |
grep = H.grep, | |
hasTouch = H.hasTouch, | |
inArray = H.inArray, | |
isArray = H.isArray, | |
isFirefox = H.isFirefox, | |
isMS = H.isMS, | |
isObject = H.isObject, | |
isString = H.isString, | |
isWebKit = H.isWebKit, | |
merge = H.merge, | |
noop = H.noop, | |
objectEach = H.objectEach, | |
pick = H.pick, | |
pInt = H.pInt, | |
removeEvent = H.removeEvent, | |
splat = H.splat, | |
stop = H.stop, | |
svg = H.svg, | |
SVG_NS = H.SVG_NS, | |
symbolSizes = H.symbolSizes, | |
win = H.win; | |
/** | |
* @typedef {Object} SVGDOMElement - An SVG DOM element. | |
*/ | |
/** | |
* The SVGElement prototype is a JavaScript wrapper for SVG elements used in the | |
* rendering layer of Highcharts. Combined with the {@link | |
* Highcharts.SVGRenderer} object, these prototypes allow freeform annotation | |
* in the charts or even in HTML pages without instanciating a chart. The | |
* SVGElement can also wrap HTML labels, when `text` or `label` elements are | |
* created with the `useHTML` parameter. | |
* | |
* The SVGElement instances are created through factory functions on the | |
* {@link Highcharts.SVGRenderer} object, like | |
* [rect]{@link Highcharts.SVGRenderer#rect}, [path]{@link | |
* Highcharts.SVGRenderer#path}, [text]{@link Highcharts.SVGRenderer#text}, | |
* [label]{@link Highcharts.SVGRenderer#label}, [g]{@link | |
* Highcharts.SVGRenderer#g} and more. | |
* | |
* @class Highcharts.SVGElement | |
*/ | |
SVGElement = H.SVGElement = function() { | |
return this; | |
}; | |
extend(SVGElement.prototype, /** @lends Highcharts.SVGElement.prototype */ { | |
// Default base for animation | |
opacity: 1, | |
SVG_NS: SVG_NS, | |
/** | |
* For labels, these CSS properties are applied to the `text` node directly. | |
* @type {Array.<string>} | |
*/ | |
textProps: ['direction', 'fontSize', 'fontWeight', 'fontFamily', | |
'fontStyle', 'color', 'lineHeight', 'width', 'textAlign', | |
'textDecoration', 'textOverflow', 'textOutline' | |
], | |
/** | |
* Initialize the SVG renderer. This function only exists to make the | |
* initiation process overridable. It should not be called directly. | |
* | |
* @param {HighchartsSVGRenderer} renderer | |
* The SVGRenderer instance to initialize to. | |
* @param {String} nodeName | |
* The SVG node name. | |
* @returns {void} | |
*/ | |
init: function(renderer, nodeName) { | |
/** | |
* The DOM node. Each SVGRenderer instance wraps a main DOM node, but | |
* may also represent more nodes. | |
* @type {SVGDOMNode|HTMLDOMNode} | |
*/ | |
this.element = nodeName === 'span' ? | |
createElement(nodeName) : | |
doc.createElementNS(this.SVG_NS, nodeName); | |
/** | |
* The renderer that the SVGElement belongs to. | |
* @type {Highcharts.SVGRenderer} | |
*/ | |
this.renderer = renderer; | |
}, | |
/** | |
* Animate to given attributes or CSS properties. | |
* | |
* @param {SVGAttributes} params SVG attributes or CSS to animate. | |
* @param {AnimationOptions} [options] Animation options. | |
* @param {Function} [complete] Function to perform at the end of animation. | |
* | |
* @sample highcharts/members/element-on/ | |
* Setting some attributes by animation | |
* | |
* @returns {SVGElement} Returns the SVGElement for chaining. | |
*/ | |
animate: function(params, options, complete) { | |
var animOptions = H.animObject( | |
pick(options, this.renderer.globalAnimation, true) | |
); | |
if (animOptions.duration !== 0) { | |
// allows using a callback with the global animation without | |
// overwriting it | |
if (complete) { | |
animOptions.complete = complete; | |
} | |
animate(this, params, animOptions); | |
} else { | |
this.attr(params, null, complete); | |
if (animOptions.step) { | |
animOptions.step.call(this); | |
} | |
} | |
return this; | |
}, | |
/** | |
* @typedef {Object} GradientOptions | |
* @property {Object} linearGradient Holds an object that defines the start | |
* position and the end position relative to the shape. | |
* @property {Number} linearGradient.x1 Start horizontal position of the | |
* gradient. Ranges 0-1. | |
* @property {Number} linearGradient.x2 End horizontal position of the | |
* gradient. Ranges 0-1. | |
* @property {Number} linearGradient.y1 Start vertical position of the | |
* gradient. Ranges 0-1. | |
* @property {Number} linearGradient.y2 End vertical position of the | |
* gradient. Ranges 0-1. | |
* @property {Object} radialGradient Holds an object that defines the center | |
* position and the radius. | |
* @property {Number} radialGradient.cx Center horizontal position relative | |
* to the shape. Ranges 0-1. | |
* @property {Number} radialGradient.cy Center vertical position relative | |
* to the shape. Ranges 0-1. | |
* @property {Number} radialGradient.r Radius relative to the shape. Ranges | |
* 0-1. | |
* @property {Array.<Array>} stops The first item in each tuple is the | |
* position in the gradient, where 0 is the start of the gradient and 1 | |
* is the end of the gradient. Multiple stops can be applied. The second | |
* item is the color for each stop. This color can also be given in the | |
* rgba format. | |
* | |
* @example | |
* // Linear gradient used as a color option | |
* color: { | |
* linearGradient: { x1: 0, x2: 0, y1: 0, y2: 1 }, | |
* stops: [ | |
* [0, '#003399'], // start | |
* [0.5, '#ffffff'], // middle | |
* [1, '#3366AA'] // end | |
* ] | |
* } | |
* } | |
*/ | |
/** | |
* Build and apply an SVG gradient out of a common JavaScript configuration | |
* object. This function is called from the attribute setters. | |
* | |
* @private | |
* @param {GradientOptions} color The gradient options structure. | |
* @param {string} prop The property to apply, can either be `fill` or | |
* `stroke`. | |
* @param {SVGDOMElement} elem SVG DOM element to apply the gradient on. | |
*/ | |
colorGradient: function(color, prop, elem) { | |
var renderer = this.renderer, | |
colorObject, | |
gradName, | |
gradAttr, | |
radAttr, | |
gradients, | |
gradientObject, | |
stops, | |
stopColor, | |
stopOpacity, | |
radialReference, | |
id, | |
key = [], | |
value; | |
// Apply linear or radial gradients | |
if (color.radialGradient) { | |
gradName = 'radialGradient'; | |
} else if (color.linearGradient) { | |
gradName = 'linearGradient'; | |
} | |
if (gradName) { | |
gradAttr = color[gradName]; | |
gradients = renderer.gradients; | |
stops = color.stops; | |
radialReference = elem.radialReference; | |
// Keep < 2.2 kompatibility | |
if (isArray(gradAttr)) { | |
color[gradName] = gradAttr = { | |
x1: gradAttr[0], | |
y1: gradAttr[1], | |
x2: gradAttr[2], | |
y2: gradAttr[3], | |
gradientUnits: 'userSpaceOnUse' | |
}; | |
} | |
// Correct the radial gradient for the radial reference system | |
if ( | |
gradName === 'radialGradient' && | |
radialReference && | |
!defined(gradAttr.gradientUnits) | |
) { | |
radAttr = gradAttr; // Save the radial attributes for updating | |
gradAttr = merge( | |
gradAttr, | |
renderer.getRadialAttr(radialReference, radAttr), { | |
gradientUnits: 'userSpaceOnUse' | |
} | |
); | |
} | |
// Build the unique key to detect whether we need to create a new | |
// element (#1282) | |
objectEach(gradAttr, function(val, n) { | |
if (n !== 'id') { | |
key.push(n, val); | |
} | |
}); | |
objectEach(stops, function(val) { | |
key.push(val); | |
}); | |
key = key.join(','); | |
// Check if a gradient object with the same config object is created | |
// within this renderer | |
if (gradients[key]) { | |
id = gradients[key].attr('id'); | |
} else { | |
// Set the id and create the element | |
gradAttr.id = id = H.uniqueKey(); | |
gradients[key] = gradientObject = | |
renderer.createElement(gradName) | |
.attr(gradAttr) | |
.add(renderer.defs); | |
gradientObject.radAttr = radAttr; | |
// The gradient needs to keep a list of stops to be able to | |
// destroy them | |
gradientObject.stops = []; | |
each(stops, function(stop) { | |
var stopObject; | |
if (stop[1].indexOf('rgba') === 0) { | |
colorObject = H.color(stop[1]); | |
stopColor = colorObject.get('rgb'); | |
stopOpacity = colorObject.get('a'); | |
} else { | |
stopColor = stop[1]; | |
stopOpacity = 1; | |
} | |
stopObject = renderer.createElement('stop').attr({ | |
offset: stop[0], | |
'stop-color': stopColor, | |
'stop-opacity': stopOpacity | |
}).add(gradientObject); | |
// Add the stop element to the gradient | |
gradientObject.stops.push(stopObject); | |
}); | |
} | |
// Set the reference to the gradient object | |
value = 'url(' + renderer.url + '#' + id + ')'; | |
elem.setAttribute(prop, value); | |
elem.gradient = key; | |
// Allow the color to be concatenated into tooltips formatters etc. | |
// (#2995) | |
color.toString = function() { | |
return value; | |
}; | |
} | |
}, | |
/** | |
* Apply a text outline through a custom CSS property, by copying the text | |
* element and apply stroke to the copy. Used internally. Contrast checks | |
* at http://jsfiddle.net/highcharts/43soe9m1/2/ . | |
* | |
* @private | |
* @param {String} textOutline A custom CSS `text-outline` setting, defined | |
* by `width color`. | |
* @example | |
* // Specific color | |
* text.css({ | |
* textOutline: '1px black' | |
* }); | |
* // Automatic contrast | |
* text.css({ | |
* color: '#000000', // black text | |
* textOutline: '1px contrast' // => white outline | |
* }); | |
*/ | |
applyTextOutline: function(textOutline) { | |
var elem = this.element, | |
tspans, | |
tspan, | |
hasContrast = textOutline.indexOf('contrast') !== -1, | |
styles = {}, | |
color, | |
strokeWidth, | |
firstRealChild, | |
i; | |
// When the text shadow is set to contrast, use dark stroke for light | |
// text and vice versa. | |
if (hasContrast) { | |
styles.textOutline = textOutline = textOutline.replace( | |
/contrast/g, | |
this.renderer.getContrast(elem.style.fill) | |
); | |
} | |
// Extract the stroke width and color | |
textOutline = textOutline.split(' '); | |
color = textOutline[textOutline.length - 1]; | |
strokeWidth = textOutline[0]; | |
if (strokeWidth && strokeWidth !== 'none' && H.svg) { | |
this.fakeTS = true; // Fake text shadow | |
tspans = [].slice.call(elem.getElementsByTagName('tspan')); | |
// In order to get the right y position of the clone, | |
// copy over the y setter | |
this.ySetter = this.xSetter; | |
// Since the stroke is applied on center of the actual outline, we | |
// need to double it to get the correct stroke-width outside the | |
// glyphs. | |
strokeWidth = strokeWidth.replace( | |
/(^[\d\.]+)(.*?)$/g, | |
function(match, digit, unit) { | |
return (2 * digit) + unit; | |
} | |
); | |
// Remove shadows from previous runs. Iterate from the end to | |
// support removing items inside the cycle (#6472). | |
i = tspans.length; | |
while (i--) { | |
tspan = tspans[i]; | |
if (tspan.getAttribute('class') === 'highcharts-text-outline') { | |
// Remove then erase | |
erase(tspans, elem.removeChild(tspan)); | |
} | |
} | |
// For each of the tspans, create a stroked copy behind it. | |
firstRealChild = elem.firstChild; | |
each(tspans, function(tspan, y) { | |
var clone; | |
// Let the first line start at the correct X position | |
if (y === 0) { | |
tspan.setAttribute('x', elem.getAttribute('x')); | |
y = elem.getAttribute('y'); | |
tspan.setAttribute('y', y || 0); | |
if (y === null) { | |
elem.setAttribute('y', 0); | |
} | |
} | |
// Create the clone and apply outline properties | |
clone = tspan.cloneNode(1); | |
attr(clone, { | |
'class': 'highcharts-text-outline', | |
'fill': color, | |
'stroke': color, | |
'stroke-width': strokeWidth, | |
'stroke-linejoin': 'round' | |
}); | |
elem.insertBefore(clone, firstRealChild); | |
}); | |
} | |
}, | |
/** | |
* | |
* @typedef {Object} SVGAttributes An object of key-value pairs for SVG | |
* attributes. Attributes in Highcharts elements for the most parts | |
* correspond to SVG, but some are specific to Highcharts, like `zIndex`, | |
* `rotation`, `translateX`, `translateY`, `scaleX` and `scaleY`. SVG | |
* attributes containing a hyphen are _not_ camel-cased, they should be | |
* quoted to preserve the hyphen. | |
* @example | |
* { | |
* 'stroke': '#ff0000', // basic | |
* 'stroke-width': 2, // hyphenated | |
* 'rotation': 45 // custom | |
* 'd': ['M', 10, 10, 'L', 30, 30, 'z'] // path definition, note format | |
* } | |
*/ | |
/** | |
* Apply native and custom attributes to the SVG elements. | |
* | |
* In order to set the rotation center for rotation, set x and y to 0 and | |
* use `translateX` and `translateY` attributes to position the element | |
* instead. | |
* | |
* Attributes frequently used in Highcharts are `fill`, `stroke`, | |
* `stroke-width`. | |
* | |
* @param {SVGAttributes|String} hash - The native and custom SVG | |
* attributes. | |
* @param {string} [val] - If the type of the first argument is `string`, | |
* the second can be a value, which will serve as a single attribute | |
* setter. If the first argument is a string and the second is undefined, | |
* the function serves as a getter and the current value of the property | |
* is returned. | |
* @param {Function} [complete] - A callback function to execute after | |
* setting the attributes. This makes the function compliant and | |
* interchangeable with the {@link SVGElement#animate} function. | |
* @param {boolean} [continueAnimation=true] Used internally when `.attr` is | |
* called as part of an animation step. Otherwise, calling `.attr` for an | |
* attribute will stop animation for that attribute. | |
* | |
* @returns {SVGElement|string|number} If used as a setter, it returns the | |
* current {@link SVGElement} so the calls can be chained. If used as a | |
* getter, the current value of the attribute is returned. | |
* | |
* @sample highcharts/members/renderer-rect/ | |
* Setting some attributes | |
* | |
* @example | |
* // Set multiple attributes | |
* element.attr({ | |
* stroke: 'red', | |
* fill: 'blue', | |
* x: 10, | |
* y: 10 | |
* }); | |
* | |
* // Set a single attribute | |
* element.attr('stroke', 'red'); | |
* | |
* // Get an attribute | |
* element.attr('stroke'); // => 'red' | |
* | |
*/ | |
attr: function(hash, val, complete, continueAnimation) { | |
var key, | |
element = this.element, | |
hasSetSymbolSize, | |
ret = this, | |
skipAttr, | |
setter; | |
// single key-value pair | |
if (typeof hash === 'string' && val !== undefined) { | |
key = hash; | |
hash = {}; | |
hash[key] = val; | |
} | |
// used as a getter: first argument is a string, second is undefined | |
if (typeof hash === 'string') { | |
ret = (this[hash + 'Getter'] || this._defaultGetter).call( | |
this, | |
hash, | |
element | |
); | |
// setter | |
} else { | |
objectEach(hash, function(val, key) { | |
skipAttr = false; | |
// Unless .attr is from the animator update, stop current | |
// running animation of this property | |
if (!continueAnimation) { | |
stop(this, key); | |
} | |
// Special handling of symbol attributes | |
if ( | |
this.symbolName && | |
/^(x|y|width|height|r|start|end|innerR|anchorX|anchorY)$/ | |
.test(key) | |
) { | |
if (!hasSetSymbolSize) { | |
this.symbolAttr(hash); | |
hasSetSymbolSize = true; | |
} | |
skipAttr = true; | |
} | |
if (this.rotation && (key === 'x' || key === 'y')) { | |
this.doTransform = true; | |
} | |
if (!skipAttr) { | |
setter = this[key + 'Setter'] || this._defaultSetter; | |
setter.call(this, val, key, element); | |
} | |
}, this); | |
this.afterSetters(); | |
} | |
// In accordance with animate, run a complete callback | |
if (complete) { | |
complete(); | |
} | |
return ret; | |
}, | |
/** | |
* This method is executed in the end of {attr}, after setting all | |
* attributes in the hash. In can be used to efficiently consolidate | |
* multiple attributes in one SVG property -- e.g., translate, rotate and | |
* scale are merged in one "transform" attribute in the SVG node. | |
*/ | |
afterSetters: function() { | |
// Update transform. Do this outside the loop to prevent redundant | |
// updating for batch setting of attributes. | |
if (this.doTransform) { | |
this.updateTransform(); | |
this.doTransform = false; | |
} | |
}, | |
/** | |
* Add a class name to an element. | |
* | |
* @param {string} className - The new class name to add. | |
* @param {boolean} [replace=false] - When true, the existing class name(s) | |
* will be overwritten with the new one. When false, the new one is | |
* added. | |
* @returns {SVGElement} Return the SVG element for chainability. | |
*/ | |
addClass: function(className, replace) { | |
var currentClassName = this.attr('class') || ''; | |
if (currentClassName.indexOf(className) === -1) { | |
if (!replace) { | |
className = | |
(currentClassName + (currentClassName ? ' ' : '') + | |
className).replace(' ', ' '); | |
} | |
this.attr('class', className); | |
} | |
return this; | |
}, | |
/** | |
* Check if an element has the given class name. | |
* @param {string} className - The class name to check for. | |
* @return {Boolean} | |
*/ | |
hasClass: function(className) { | |
return attr(this.element, 'class').indexOf(className) !== -1; | |
}, | |
/** | |
* Remove a class name from the element. | |
* @param {string} className The class name to remove. | |
* @return {SVGElement} Returns the SVG element for chainability. | |
*/ | |
removeClass: function(className) { | |
attr( | |
this.element, | |
'class', | |
(attr(this.element, 'class') || '').replace(className, '') | |
); | |
return this; | |
}, | |
/** | |
* If one of the symbol size affecting parameters are changed, | |
* check all the others only once for each call to an element's | |
* .attr() method | |
* @param {Object} hash - The attributes to set. | |
* @private | |
*/ | |
symbolAttr: function(hash) { | |
var wrapper = this; | |
each([ | |
'x', | |
'y', | |
'r', | |
'start', | |
'end', | |
'width', | |
'height', | |
'innerR', | |
'anchorX', | |
'anchorY' | |
], function(key) { | |
wrapper[key] = pick(hash[key], wrapper[key]); | |
}); | |
wrapper.attr({ | |
d: wrapper.renderer.symbols[wrapper.symbolName]( | |
wrapper.x, | |
wrapper.y, | |
wrapper.width, | |
wrapper.height, | |
wrapper | |
) | |
}); | |
}, | |
/** | |
* Apply a clipping rectangle to this element. | |
* | |
* @param {ClipRect} [clipRect] - The clipping rectangle. If skipped, the | |
* current clip is removed. | |
* @returns {SVGElement} Returns the SVG element to allow chaining. | |
*/ | |
clip: function(clipRect) { | |
return this.attr( | |
'clip-path', | |
clipRect ? | |
'url(' + this.renderer.url + '#' + clipRect.id + ')' : | |
'none' | |
); | |
}, | |
/** | |
* Calculate the coordinates needed for drawing a rectangle crisply and | |
* return the calculated attributes. | |
* | |
* @param {Object} rect - A rectangle. | |
* @param {number} rect.x - The x position. | |
* @param {number} rect.y - The y position. | |
* @param {number} rect.width - The width. | |
* @param {number} rect.height - The height. | |
* @param {number} [strokeWidth] - The stroke width to consider when | |
* computing crisp positioning. It can also be set directly on the rect | |
* parameter. | |
* | |
* @returns {{x: Number, y: Number, width: Number, height: Number}} The | |
* modified rectangle arguments. | |
*/ | |
crisp: function(rect, strokeWidth) { | |
var wrapper = this, | |
attribs = {}, | |
normalizer; | |
strokeWidth = strokeWidth || rect.strokeWidth || 0; | |
// Math.round because strokeWidth can sometimes have roundoff errors | |
normalizer = Math.round(strokeWidth) % 2 / 2; | |
// normalize for crisp edges | |
rect.x = Math.floor(rect.x || wrapper.x || 0) + normalizer; | |
rect.y = Math.floor(rect.y || wrapper.y || 0) + normalizer; | |
rect.width = Math.floor( | |
(rect.width || wrapper.width || 0) - 2 * normalizer | |
); | |
rect.height = Math.floor( | |
(rect.height || wrapper.height || 0) - 2 * normalizer | |
); | |
if (defined(rect.strokeWidth)) { | |
rect.strokeWidth = strokeWidth; | |
} | |
objectEach(rect, function(val, key) { | |
if (wrapper[key] !== val) { // only set attribute if changed | |
wrapper[key] = attribs[key] = val; | |
} | |
}); | |
return attribs; | |
}, | |
/** | |
* Set styles for the element. In addition to CSS styles supported by | |
* native SVG and HTML elements, there are also some custom made for | |
* Highcharts, like `width`, `ellipsis` and `textOverflow` for SVG text | |
* elements. | |
* @param {CSSObject} styles The new CSS styles. | |
* @returns {SVGElement} Return the SVG element for chaining. | |
* | |
* @sample highcharts/members/renderer-text-on-chart/ | |
* Styled text | |
*/ | |
css: function(styles) { | |
var oldStyles = this.styles, | |
newStyles = {}, | |
elem = this.element, | |
textWidth, | |
serializedCss = '', | |
hyphenate, | |
hasNew = !oldStyles, | |
// These CSS properties are interpreted internally by the SVG | |
// renderer, but are not supported by SVG and should not be added to | |
// the DOM. In styled mode, no CSS should find its way to the DOM | |
// whatsoever (#6173, #6474). | |
svgPseudoProps = ['textOutline', 'textOverflow', 'width']; | |
// convert legacy | |
if (styles && styles.color) { | |
styles.fill = styles.color; | |
} | |
// Filter out existing styles to increase performance (#2640) | |
if (oldStyles) { | |
objectEach(styles, function(style, n) { | |
if (style !== oldStyles[n]) { | |
newStyles[n] = style; | |
hasNew = true; | |
} | |
}); | |
} | |
if (hasNew) { | |
// Merge the new styles with the old ones | |
if (oldStyles) { | |
styles = extend( | |
oldStyles, | |
newStyles | |
); | |
} | |
// Get the text width from style | |
textWidth = this.textWidth = ( | |
styles && | |
styles.width && | |
styles.width !== 'auto' && | |
elem.nodeName.toLowerCase() === 'text' && | |
pInt(styles.width) | |
); | |
// store object | |
this.styles = styles; | |
if (textWidth && (!svg && this.renderer.forExport)) { | |
delete styles.width; | |
} | |
// serialize and set style attribute | |
if (isMS && !svg) { | |
css(this.element, styles); | |
} else { | |
hyphenate = function(a, b) { | |
return '-' + b.toLowerCase(); | |
}; | |
objectEach(styles, function(style, n) { | |
if (inArray(n, svgPseudoProps) === -1) { | |
serializedCss += | |
n.replace(/([A-Z])/g, hyphenate) + ':' + | |
style + ';'; | |
} | |
}); | |
if (serializedCss) { | |
attr(elem, 'style', serializedCss); // #1881 | |
} | |
} | |
if (this.added) { | |
// Rebuild text after added. Cache mechanisms in the buildText | |
// will prevent building if there are no significant changes. | |
if (this.element.nodeName === 'text') { | |
this.renderer.buildText(this); | |
} | |
// Apply text outline after added | |
if (styles && styles.textOutline) { | |
this.applyTextOutline(styles.textOutline); | |
} | |
} | |
} | |
return this; | |
}, | |
/** | |
* Get the computed style. Only in styled mode. | |
* @param {string} prop - The property name to check for. | |
* @returns {string} The current computed value. | |
* @example | |
* chart.series[0].points[0].graphic.getStyle('stroke-width'); // => '1px' | |
*/ | |
getStyle: function(prop) { | |
return win.getComputedStyle(this.element || this, '') | |
.getPropertyValue(prop); | |
}, | |
/** | |
* Get the computed stroke width in pixel values. This is used extensively | |
* when drawing shapes to ensure the shapes are rendered crsip and | |
* positioned correctly relative to each other. Using `shape-rendering: | |
* crispEdges` leaves us less control over positioning, for example when we | |
* want to stack columns next to each other, or position things | |
* pixel-perfectly within the plot box. | |
* | |
* The common pattern when placing a shape is: | |
* * Create the SVGElement and add it to the DOM. | |
* * Read the computed `elem.strokeWidth()`. | |
* * Place it based on the stroke width. | |
* | |
* @returns {number} The stroke width in pixels. Even if the given stroke | |
* widtch (in CSS or by attributes) is based on `em` or other units, the | |
* pixel size is returned. | |
*/ | |
strokeWidth: function() { | |
var val = this.getStyle('stroke-width'), | |
ret, | |
dummy; | |
// Read pixel values directly | |
if (val.indexOf('px') === val.length - 2) { | |
ret = pInt(val); | |
// Other values like em, pt etc need to be measured | |
} else { | |
dummy = doc.createElementNS(SVG_NS, 'rect'); | |
attr(dummy, { | |
'width': val, | |
'stroke-width': 0 | |
}); | |
this.element.parentNode.appendChild(dummy); | |
ret = dummy.getBBox().width; | |
dummy.parentNode.removeChild(dummy); | |
} | |
return ret; | |
}, | |
/** | |
* Add an event listener. This is a simple setter that replaces all other | |
* events of the same type, opposed to the {@link Highcharts#addEvent} | |
* function. | |
* @param {string} eventType - The event type. If the type is `click`, | |
* Highcharts will internally translate it to a `touchstart` event on | |
* touch devices, to prevent the browser from waiting for a click event | |
* from firing. | |
* @param {Function} handler - The handler callback. | |
* @returns {SVGElement} The SVGElement for chaining. | |
* | |
* @sample highcharts/members/element-on/ | |
* A clickable rectangle | |
*/ | |
on: function(eventType, handler) { | |
var svgElement = this, | |
element = svgElement.element; | |
// touch | |
if (hasTouch && eventType === 'click') { | |
element.ontouchstart = function(e) { | |
svgElement.touchEventFired = Date.now(); // #2269 | |
e.preventDefault(); | |
handler.call(element, e); | |
}; | |
element.onclick = function(e) { | |
if (win.navigator.userAgent.indexOf('Android') === -1 || | |
Date.now() - (svgElement.touchEventFired || 0) > 1100) { | |
handler.call(element, e); | |
} | |
}; | |
} else { | |
// simplest possible event model for internal use | |
element['on' + eventType] = handler; | |
} | |
return this; | |
}, | |
/** | |
* Set the coordinates needed to draw a consistent radial gradient across | |
* a shape regardless of positioning inside the chart. Used on pie slices | |
* to make all the slices have the same radial reference point. | |
* | |
* @param {Array} coordinates The center reference. The format is | |
* `[centerX, centerY, diameter]` in pixels. | |
* @returns {SVGElement} Returns the SVGElement for chaining. | |
*/ | |
setRadialReference: function(coordinates) { | |
var existingGradient = this.renderer.gradients[this.element.gradient]; | |
this.element.radialReference = coordinates; | |
// On redrawing objects with an existing gradient, the gradient needs | |
// to be repositioned (#3801) | |
if (existingGradient && existingGradient.radAttr) { | |
existingGradient.animate( | |
this.renderer.getRadialAttr( | |
coordinates, | |
existingGradient.radAttr | |
) | |
); | |
} | |
return this; | |
}, | |
/** | |
* Move an object and its children by x and y values. | |
* | |
* @param {number} x - The x value. | |
* @param {number} y - The y value. | |
*/ | |
translate: function(x, y) { | |
return this.attr({ | |
translateX: x, | |
translateY: y | |
}); | |
}, | |
/** | |
* Invert a group, rotate and flip. This is used internally on inverted | |
* charts, where the points and graphs are drawn as if not inverted, then | |
* the series group elements are inverted. | |
* | |
* @param {boolean} inverted - Whether to invert or not. An inverted shape | |
* can be un-inverted by setting it to false. | |
* @returns {SVGElement} Return the SVGElement for chaining. | |
*/ | |
invert: function(inverted) { | |
var wrapper = this; | |
wrapper.inverted = inverted; | |
wrapper.updateTransform(); | |
return wrapper; | |
}, | |
/** | |
* Update the transform attribute based on internal properties. Deals with | |
* the custom `translateX`, `translateY`, `rotation`, `scaleX` and `scaleY` | |
* attributes and updates the SVG `transform` attribute. | |
* @private | |
* @returns {void} | |
*/ | |
updateTransform: function() { | |
var wrapper = this, | |
translateX = wrapper.translateX || 0, | |
translateY = wrapper.translateY || 0, | |
scaleX = wrapper.scaleX, | |
scaleY = wrapper.scaleY, | |
inverted = wrapper.inverted, | |
rotation = wrapper.rotation, | |
element = wrapper.element, | |
transform; | |
// Flipping affects translate as adjustment for flipping around the | |
// group's axis | |
if (inverted) { | |
translateX += wrapper.width; | |
translateY += wrapper.height; | |
} | |
// Apply translate. Nearly all transformed elements have translation, | |
// so instead of checking for translate = 0, do it always (#1767, #1846). | |
transform = ['translate(' + translateX + ',' + translateY + ')']; | |
// apply rotation | |
if (inverted) { | |
transform.push('rotate(90) scale(-1,1)'); | |
} else if (rotation) { // text rotation | |
transform.push( | |
'rotate(' + rotation + ' ' + (element.getAttribute('x') || 0) + | |
' ' + (element.getAttribute('y') || 0) + ')' | |
); | |
} | |
// apply scale | |
if (defined(scaleX) || defined(scaleY)) { | |
transform.push( | |
'scale(' + pick(scaleX, 1) + ' ' + pick(scaleY, 1) + ')' | |
); | |
} | |
if (transform.length) { | |
element.setAttribute('transform', transform.join(' ')); | |
} | |
}, | |
/** | |
* Bring the element to the front. Alternatively, a new zIndex can be set. | |
* | |
* @returns {SVGElement} Returns the SVGElement for chaining. | |
* | |
* @sample highcharts/members/element-tofront/ | |
* Click an element to bring it to front | |
*/ | |
toFront: function() { | |
var element = this.element; | |
element.parentNode.appendChild(element); | |
return this; | |
}, | |
/** | |
* Align the element relative to the chart or another box. | |
* | |
* @param {Object} [alignOptions] The alignment options. The function can be | |
* called without this parameter in order to re-align an element after the | |
* box has been updated. | |
* @param {string} [alignOptions.align=left] Horizontal alignment. Can be | |
* one of `left`, `center` and `right`. | |
* @param {string} [alignOptions.verticalAlign=top] Vertical alignment. Can | |
* be one of `top`, `middle` and `bottom`. | |
* @param {number} [alignOptions.x=0] Horizontal pixel offset from | |
* alignment. | |
* @param {number} [alignOptions.y=0] Vertical pixel offset from alignment. | |
* @param {Boolean} [alignByTranslate=false] Use the `transform` attribute | |
* with translateX and translateY custom attributes to align this elements | |
* rather than `x` and `y` attributes. | |
* @param {String|Object} box The box to align to, needs a width and height. | |
* When the box is a string, it refers to an object in the Renderer. For | |
* example, when box is `spacingBox`, it refers to `Renderer.spacingBox` | |
* which holds `width`, `height`, `x` and `y` properties. | |
* @returns {SVGElement} Returns the SVGElement for chaining. | |
*/ | |
align: function(alignOptions, alignByTranslate, box) { | |
var align, | |
vAlign, | |
x, | |
y, | |
attribs = {}, | |
alignTo, | |
renderer = this.renderer, | |
alignedObjects = renderer.alignedObjects, | |
alignFactor, | |
vAlignFactor; | |
// First call on instanciate | |
if (alignOptions) { | |
this.alignOptions = alignOptions; | |
this.alignByTranslate = alignByTranslate; | |
if (!box || isString(box)) { // boxes other than renderer handle this internally | |
this.alignTo = alignTo = box || 'renderer'; | |
erase(alignedObjects, this); // prevent duplicates, like legendGroup after resize | |
alignedObjects.push(this); | |
box = null; // reassign it below | |
} | |
// When called on resize, no arguments are supplied | |
} else { | |
alignOptions = this.alignOptions; | |
alignByTranslate = this.alignByTranslate; | |
alignTo = this.alignTo; | |
} | |
box = pick(box, renderer[alignTo], renderer); | |
// Assign variables | |
align = alignOptions.align; | |
vAlign = alignOptions.verticalAlign; | |
x = (box.x || 0) + (alignOptions.x || 0); // default: left align | |
y = (box.y || 0) + (alignOptions.y || 0); // default: top align | |
// Align | |
if (align === 'right') { | |
alignFactor = 1; | |
} else if (align === 'center') { | |
alignFactor = 2; | |
} | |
if (alignFactor) { | |
x += (box.width - (alignOptions.width || 0)) / alignFactor; | |
} | |
attribs[alignByTranslate ? 'translateX' : 'x'] = Math.round(x); | |
// Vertical align | |
if (vAlign === 'bottom') { | |
vAlignFactor = 1; | |
} else if (vAlign === 'middle') { | |
vAlignFactor = 2; | |
} | |
if (vAlignFactor) { | |
y += (box.height - (alignOptions.height || 0)) / vAlignFactor; | |
} | |
attribs[alignByTranslate ? 'translateY' : 'y'] = Math.round(y); | |
// Animate only if already placed | |
this[this.placed ? 'animate' : 'attr'](attribs); | |
this.placed = true; | |
this.alignAttr = attribs; | |
return this; | |
}, | |
/** | |
* Get the bounding box (width, height, x and y) for the element. Generally | |
* used to get rendered text size. Since this is called a lot in charts, | |
* the results are cached based on text properties, in order to save DOM | |
* traffic. The returned bounding box includes the rotation, so for example | |
* a single text line of rotation 90 will report a greater height, and a | |
* width corresponding to the line-height. | |
* | |
* @param {boolean} [reload] Skip the cache and get the updated DOM bouding | |
* box. | |
* @param {number} [rot] Override the element's rotation. This is internally | |
* used on axis labels with a value of 0 to find out what the bounding box | |
* would be have been if it were not rotated. | |
* @returns {Object} The bounding box with `x`, `y`, `width` and `height` | |
* properties. | |
* | |
* @sample highcharts/members/renderer-on-chart/ | |
* Draw a rectangle based on a text's bounding box | |
*/ | |
getBBox: function(reload, rot) { | |
var wrapper = this, | |
bBox, // = wrapper.bBox, | |
renderer = wrapper.renderer, | |
width, | |
height, | |
rotation, | |
rad, | |
element = wrapper.element, | |
styles = wrapper.styles, | |
fontSize, | |
textStr = wrapper.textStr, | |
toggleTextShadowShim, | |
cache = renderer.cache, | |
cacheKeys = renderer.cacheKeys, | |
cacheKey; | |
rotation = pick(rot, wrapper.rotation); | |
rad = rotation * deg2rad; | |
fontSize = element && SVGElement.prototype.getStyle.call(element, 'font-size'); | |
if (textStr !== undefined) { | |
cacheKey = textStr.toString(); | |
// Since numbers are monospaced, and numerical labels appear a lot | |
// in a chart, we assume that a label of n characters has the same | |
// bounding box as others of the same length. Unless there is inner | |
// HTML in the label. In that case, leave the numbers as is (#5899). | |
if (cacheKey.indexOf('<') === -1) { | |
cacheKey = cacheKey.replace(/[0-9]/g, '0'); | |
} | |
// Properties that affect bounding box | |
cacheKey += [ | |
'', | |
rotation || 0, | |
fontSize, | |
styles && styles.width, | |
styles && styles.textOverflow // #5968 | |
] | |
.join(','); | |
} | |
if (cacheKey && !reload) { | |
bBox = cache[cacheKey]; | |
} | |
// No cache found | |
if (!bBox) { | |
// SVG elements | |
if (element.namespaceURI === wrapper.SVG_NS || renderer.forExport) { | |
try { // Fails in Firefox if the container has display: none. | |
// When the text shadow shim is used, we need to hide the fake shadows | |
// to get the correct bounding box (#3872) | |
toggleTextShadowShim = this.fakeTS && function(display) { | |
each(element.querySelectorAll('.highcharts-text-outline'), function(tspan) { | |
tspan.style.display = display; | |
}); | |
}; | |
// Workaround for #3842, Firefox reporting wrong bounding box for shadows | |
if (toggleTextShadowShim) { | |
toggleTextShadowShim('none'); | |
} | |
bBox = element.getBBox ? | |
// SVG: use extend because IE9 is not allowed to change width and height in case | |
// of rotation (below) | |
extend({}, element.getBBox()) : { | |
// Legacy IE in export mode | |
width: element.offsetWidth, | |
height: element.offsetHeight | |
}; | |
// #3842 | |
if (toggleTextShadowShim) { | |
toggleTextShadowShim(''); | |
} | |
} catch (e) {} | |
// If the bBox is not set, the try-catch block above failed. The other condition | |
// is for Opera that returns a width of -Infinity on hidden elements. | |
if (!bBox || bBox.width < 0) { | |
bBox = { | |
width: 0, | |
height: 0 | |
}; | |
} | |
// VML Renderer or useHTML within SVG | |
} else { | |
bBox = wrapper.htmlGetBBox(); | |
} | |
// True SVG elements as well as HTML elements in modern browsers using the .useHTML option | |
// need to compensated for rotation | |
if (renderer.isSVG) { | |
width = bBox.width; | |
height = bBox.height; | |
// Workaround for wrong bounding box in IE, Edge and Chrome on | |
// Windows. With Highcharts' default font, IE and Edge report | |
// a box height of 16.899 and Chrome rounds it to 17. If this | |
// stands uncorrected, it results in more padding added below | |
// the text than above when adding a label border or background. | |
// Also vertical positioning is affected. | |
// http://jsfiddle.net/highcharts/em37nvuj/ | |
// (#1101, #1505, #1669, #2568, #6213). | |
if ( | |
styles && | |
styles.fontSize === '11px' && | |
Math.round(height) === 17 | |
) { | |
bBox.height = height = 14; | |
} | |
// Adjust for rotated text | |
if (rotation) { | |
bBox.width = Math.abs(height * Math.sin(rad)) + Math.abs(width * Math.cos(rad)); | |
bBox.height = Math.abs(height * Math.cos(rad)) + Math.abs(width * Math.sin(rad)); | |
} | |
} | |
// Cache it. When loading a chart in a hidden iframe in Firefox and IE/Edge, the | |
// bounding box height is 0, so don't cache it (#5620). | |
if (cacheKey && bBox.height > 0) { | |
// Rotate (#4681) | |
while (cacheKeys.length > 250) { | |
delete cache[cacheKeys.shift()]; | |
} | |
if (!cache[cacheKey]) { | |
cacheKeys.push(cacheKey); | |
} | |
cache[cacheKey] = bBox; | |
} | |
} | |
return bBox; | |
}, | |
/** | |
* Show the element after it has been hidden. | |
* | |
* @param {boolean} [inherit=false] Set the visibility attribute to | |
* `inherit` rather than `visible`. The difference is that an element with | |
* `visibility="visible"` will be visible even if the parent is hidden. | |
* | |
* @returns {SVGElement} Returns the SVGElement for chaining. | |
*/ | |
show: function(inherit) { | |
return this.attr({ | |
visibility: inherit ? 'inherit' : 'visible' | |
}); | |
}, | |
/** | |
* Hide the element, equivalent to setting the `visibility` attribute to | |
* `hidden`. | |
* | |
* @returns {SVGElement} Returns the SVGElement for chaining. | |
*/ | |
hide: function() { | |
return this.attr({ | |
visibility: 'hidden' | |
}); | |
}, | |
/** | |
* Fade out an element by animating its opacity down to 0, and hide it on | |
* complete. Used internally for the tooltip. | |
* | |
* @param {number} [duration=150] The fade duration in milliseconds. | |
*/ | |
fadeOut: function(duration) { | |
var elemWrapper = this; | |
elemWrapper.animate({ | |
opacity: 0 | |
}, { | |
duration: duration || 150, | |
complete: function() { | |
// #3088, assuming we're only using this for tooltips | |
elemWrapper.attr({ | |
y: -9999 | |
}); | |
} | |
}); | |
}, | |
/** | |
* Add the element to the DOM. All elements must be added this way. | |
* | |
* @param {SVGElement|SVGDOMElement} [parent] The parent item to add it to. | |
* If undefined, the element is added to the {@link | |
* Highcharts.SVGRenderer.box}. | |
* | |
* @returns {SVGElement} Returns the SVGElement for chaining. | |
* | |
* @sample highcharts/members/renderer-g - Elements added to a group | |
*/ | |
add: function(parent) { | |
var renderer = this.renderer, | |
element = this.element, | |
inserted; | |
if (parent) { | |
this.parentGroup = parent; | |
} | |
// mark as inverted | |
this.parentInverted = parent && parent.inverted; | |
// build formatted text | |
if (this.textStr !== undefined) { | |
renderer.buildText(this); | |
} | |
// Mark as added | |
this.added = true; | |
// If we're adding to renderer root, or other elements in the group | |
// have a z index, we need to handle it | |
if (!parent || parent.handleZ || this.zIndex) { | |
inserted = this.zIndexSetter(); | |
} | |
// If zIndex is not handled, append at the end | |
if (!inserted) { | |
(parent ? parent.element : renderer.box).appendChild(element); | |
} | |
// fire an event for internal hooks | |
if (this.onAdd) { | |
this.onAdd(); | |
} | |
return this; | |
}, | |
/** | |
* Removes an element from the DOM. | |
* | |
* @private | |
* @param {SVGDOMElement|HTMLDOMElement} element The DOM node to remove. | |
*/ | |
safeRemoveChild: function(element) { | |
var parentNode = element.parentNode; | |
if (parentNode) { | |
parentNode.removeChild(element); | |
} | |
}, | |
/** | |
* Destroy the element and element wrapper and clear up the DOM and event | |
* hooks. | |
* | |
* @returns {void} | |
*/ | |
destroy: function() { | |
var wrapper = this, | |
element = wrapper.element || {}, | |
parentToClean = | |
wrapper.renderer.isSVG && | |
element.nodeName === 'SPAN' && | |
wrapper.parentGroup, | |
grandParent, | |
ownerSVGElement = element.ownerSVGElement, | |
i; | |
// remove events | |
element.onclick = element.onmouseout = element.onmouseover = | |
element.onmousemove = element.point = null; | |
stop(wrapper); // stop running animations | |
if (wrapper.clipPath && ownerSVGElement) { | |
// Look for existing references to this clipPath and remove them | |
// before destroying the element (#6196). | |
each( | |
ownerSVGElement.querySelectorAll('[clip-path]'), | |
function(el) { | |
// Include the closing paranthesis in the test to rule out | |
// id's from 10 and above (#6550) | |
if (el.getAttribute('clip-path') | |
.indexOf(wrapper.clipPath.element.id + ')') > -1) { | |
el.removeAttribute('clip-path'); | |
} | |
} | |
); | |
wrapper.clipPath = wrapper.clipPath.destroy(); | |
} | |
// Destroy stops in case this is a gradient object | |
if (wrapper.stops) { | |
for (i = 0; i < wrapper.stops.length; i++) { | |
wrapper.stops[i] = wrapper.stops[i].destroy(); | |
} | |
wrapper.stops = null; | |
} | |
// remove element | |
wrapper.safeRemoveChild(element); | |
// In case of useHTML, clean up empty containers emulating SVG groups | |
// (#1960, #2393, #2697). | |
while ( | |
parentToClean && | |
parentToClean.div && | |
parentToClean.div.childNodes.length === 0 | |
) { | |
grandParent = parentToClean.parentGroup; | |
wrapper.safeRemoveChild(parentToClean.div); | |
delete parentToClean.div; | |
parentToClean = grandParent; | |
} | |
// remove from alignObjects | |
if (wrapper.alignTo) { | |
erase(wrapper.renderer.alignedObjects, wrapper); | |
} | |
objectEach(wrapper, function(val, key) { | |
delete wrapper[key]; | |
}); | |
return null; | |
}, | |
xGetter: function(key) { | |
if (this.element.nodeName === 'circle') { | |
if (key === 'x') { | |
key = 'cx'; | |
} else if (key === 'y') { | |
key = 'cy'; | |
} | |
} | |
return this._defaultGetter(key); | |
}, | |
/** | |
* Get the current value of an attribute or pseudo attribute, used mainly | |
* for animation. Called internally from the {@link | |
* Highcharts.SVGRenderer#attr} | |
* function. | |
* | |
* @private | |
*/ | |
_defaultGetter: function(key) { | |
var ret = pick(this[key], this.element ? this.element.getAttribute(key) : null, 0); | |
if (/^[\-0-9\.]+$/.test(ret)) { // is numerical | |
ret = parseFloat(ret); | |
} | |
return ret; | |
}, | |
dSetter: function(value, key, element) { | |
if (value && value.join) { // join path | |
value = value.join(' '); | |
} | |
if (/(NaN| {2}|^$)/.test(value)) { | |
value = 'M 0 0'; | |
} | |
// Check for cache before resetting. Resetting causes disturbance in the | |
// DOM, causing flickering in some cases in Edge/IE (#6747#. Also | |
// possible performance gain. | |
if (this[key] !== value) { | |
element.setAttribute(key, value); | |
this[key] = value; | |
} | |
}, | |
alignSetter: function(value) { | |
var convert = { | |
left: 'start', | |
center: 'middle', | |
right: 'end' | |
}; | |
this.element.setAttribute('text-anchor', convert[value]); | |
}, | |
opacitySetter: function(value, key, element) { | |
this[key] = value; | |
element.setAttribute(key, value); | |
}, | |
titleSetter: function(value) { | |
var titleNode = this.element.getElementsByTagName('title')[0]; | |
if (!titleNode) { | |
titleNode = doc.createElementNS(this.SVG_NS, 'title'); | |
this.element.appendChild(titleNode); | |
} | |
// Remove text content if it exists | |
if (titleNode.firstChild) { | |
titleNode.removeChild(titleNode.firstChild); | |
} | |
titleNode.appendChild( | |
doc.createTextNode( | |
(String(pick(value), '')).replace(/<[^>]*>/g, '') // #3276, #3895 | |
) | |
); | |
}, | |
textSetter: function(value) { | |
if (value !== this.textStr) { | |
// Delete bBox memo when the text changes | |
delete this.bBox; | |
this.textStr = value; | |
if (this.added) { | |
this.renderer.buildText(this); | |
} | |
} | |
}, | |
fillSetter: function(value, key, element) { | |
if (typeof value === 'string') { | |
element.setAttribute(key, value); | |
} else if (value) { | |
this.colorGradient(value, key, element); | |
} | |
}, | |
visibilitySetter: function(value, key, element) { | |
// IE9-11 doesn't handle visibilty:inherit well, so we remove the attribute instead (#2881, #3909) | |
if (value === 'inherit') { | |
element.removeAttribute(key); | |
} else if (this[key] !== value) { // #6747 | |
element.setAttribute(key, value); | |
} | |
this[key] = value; | |
}, | |
zIndexSetter: function(value, key) { | |
var renderer = this.renderer, | |
parentGroup = this.parentGroup, | |
parentWrapper = parentGroup || renderer, | |
parentNode = parentWrapper.element || renderer.box, | |
childNodes, | |
otherElement, | |
otherZIndex, | |
element = this.element, | |
inserted, | |
run = this.added, | |
i; | |
if (defined(value)) { | |
element.zIndex = value; // So we can read it for other elements in the group | |
value = +value; | |
if (this[key] === value) { // Only update when needed (#3865) | |
run = false; | |
} | |
this[key] = value; | |
} | |
// Insert according to this and other elements' zIndex. Before .add() is called, | |
// nothing is done. Then on add, or by later calls to zIndexSetter, the node | |
// is placed on the right place in the DOM. | |
if (run) { | |
value = this.zIndex; | |
if (value && parentGroup) { | |
parentGroup.handleZ = true; | |
} | |
childNodes = parentNode.childNodes; | |
for (i = 0; i < childNodes.length && !inserted; i++) { | |
otherElement = childNodes[i]; | |
otherZIndex = otherElement.zIndex; | |
if (otherElement !== element && ( | |
// Insert before the first element with a higher zIndex | |
pInt(otherZIndex) > value || | |
// If no zIndex given, insert before the first element with a zIndex | |
(!defined(value) && defined(otherZIndex)) || | |
// Negative zIndex versus no zIndex: | |
// On all levels except the highest. If the parent is <svg>, | |
// then we don't want to put items before <desc> or <defs> | |
(value < 0 && !defined(otherZIndex) && parentNode !== renderer.box) | |
)) { | |
parentNode.insertBefore(element, otherElement); | |
inserted = true; | |
} | |
} | |
if (!inserted) { | |
parentNode.appendChild(element); | |
} | |
} | |
return inserted; | |
}, | |
_defaultSetter: function(value, key, element) { | |
element.setAttribute(key, value); | |
} | |
}); | |
// Some shared setters and getters | |
SVGElement.prototype.yGetter = SVGElement.prototype.xGetter; | |
SVGElement.prototype.translateXSetter = SVGElement.prototype.translateYSetter = | |
SVGElement.prototype.rotationSetter = SVGElement.prototype.verticalAlignSetter = | |
SVGElement.prototype.scaleXSetter = SVGElement.prototype.scaleYSetter = function(value, key) { | |
this[key] = value; | |
this.doTransform = true; | |
}; | |
/** | |
* Allows direct access to the Highcharts rendering layer in order to draw | |
* primitive shapes like circles, rectangles, paths or text directly on a chart, | |
* or independent from any chart. The SVGRenderer represents a wrapper object | |
* for SVGin modern browsers and through the VMLRenderer, for VML in IE < 8. | |
* | |
* An existing chart's renderer can be accessed through {@link Chart#renderer}. | |
* The renderer can also be used completely decoupled from a chart. | |
* | |
* @param {HTMLDOMElement} container - Where to put the SVG in the web page. | |
* @param {number} width - The width of the SVG. | |
* @param {number} height - The height of the SVG. | |
* @param {boolean} [forExport=false] - Whether the rendered content is intended | |
* for export. | |
* @param {boolean} [allowHTML=true] - Whether the renderer is allowed to | |
* include HTML text, which will be projected on top of the SVG. | |
* | |
* @example | |
* // Use directly without a chart object. | |
* var renderer = new Highcharts.Renderer(parentNode, 600, 400); | |
* | |
* @sample highcharts/members/renderer-on-chart - Annotating a chart programmatically. | |
* @sample highcharts/members/renderer-basic - Independedt SVG drawing. | |
* | |
* @class Highcharts.SVGRenderer | |
*/ | |
SVGRenderer = H.SVGRenderer = function() { | |
this.init.apply(this, arguments); | |
}; | |
extend(SVGRenderer.prototype, /** @lends Highcharts.SVGRenderer.prototype */ { | |
/** | |
* A pointer to the renderer's associated Element class. The VMLRenderer | |
* will have a pointer to VMLElement here. | |
* @type {SVGElement} | |
*/ | |
Element: SVGElement, | |
SVG_NS: SVG_NS, | |
/** | |
* Initialize the SVGRenderer. Overridable initiator function that takes | |
* the same parameters as the constructor. | |
*/ | |
init: function(container, width, height, style, forExport, allowHTML) { | |
var renderer = this, | |
boxWrapper, | |
element, | |
desc; | |
boxWrapper = renderer.createElement('svg') | |
.attr({ | |
'version': '1.1', | |
'class': 'highcharts-root' | |
}); | |
element = boxWrapper.element; | |
container.appendChild(element); | |
// For browsers other than IE, add the namespace attribute (#1978) | |
if (container.innerHTML.indexOf('xmlns') === -1) { | |
attr(element, 'xmlns', this.SVG_NS); | |
} | |
// object properties | |
renderer.isSVG = true; | |
/** | |
* The root `svg` node of the renderer. | |
* @type {SVGDOMElement} | |
*/ | |
this.box = element; | |
/** | |
* The wrapper for the root `svg` node of the renderer. | |
* @type {SVGElement} | |
*/ | |
this.boxWrapper = boxWrapper; | |
renderer.alignedObjects = []; | |
/** | |
* Page url used for internal references. | |
* @type {string} | |
*/ | |
// #24, #672, #1070 | |
this.url = (isFirefox || isWebKit) && doc.getElementsByTagName('base').length ? | |
win.location.href | |
.replace(/#.*?$/, '') // remove the hash | |
.replace(/<[^>]*>/g, '') // wing cut HTML | |
.replace(/([\('\)])/g, '\\$1') // escape parantheses and quotes | |
.replace(/ /g, '%20') : // replace spaces (needed for Safari only) | |
''; | |
// Add description | |
desc = this.createElement('desc').add(); | |
desc.element.appendChild(doc.createTextNode('Created with Highcharts x.x.x')); | |
renderer.defs = this.createElement('defs').add(); | |
renderer.allowHTML = allowHTML; | |
renderer.forExport = forExport; | |
renderer.gradients = {}; // Object where gradient SvgElements are stored | |
renderer.cache = {}; // Cache for numerical bounding boxes | |
renderer.cacheKeys = []; | |
renderer.imgCount = 0; | |
renderer.setSize(width, height, false); | |
// Issue 110 workaround: | |
// In Firefox, if a div is positioned by percentage, its pixel position may land | |
// between pixels. The container itself doesn't display this, but an SVG element | |
// inside this container will be drawn at subpixel precision. In order to draw | |
// sharp lines, this must be compensated for. This doesn't seem to work inside | |
// iframes though (like in jsFiddle). | |
var subPixelFix, rect; | |
if (isFirefox && container.getBoundingClientRect) { | |
subPixelFix = function() { | |
css(container, { | |
left: 0, | |
top: 0 | |
}); | |
rect = container.getBoundingClientRect(); | |
css(container, { | |
left: (Math.ceil(rect.left) - rect.left) + 'px', | |
top: (Math.ceil(rect.top) - rect.top) + 'px' | |
}); | |
}; | |
// run the fix now | |
subPixelFix(); | |
// run it on resize | |
renderer.unSubPixelFix = addEvent(win, 'resize', subPixelFix); | |
} | |
}, | |
/** | |
* General method for adding a definition to the SVG `defs` tag. Can be used | |
* for gradients, fills, filters etc. Styled mode only. A hook for adding | |
* general definitions to the SVG's defs tag. Definitions can be | |
* referenced from the CSS by its `id`. Read more in | |
* [gradients, shadows and patterns]{@link http://www.highcharts.com/docs/chart-design-and-style/gradients-shadows-and-patterns}. | |
* {@link http://www.highcharts.com/docs/chart-design-and-style/style-by-css| | |
* Styled mode} only. | |
* | |
* @param {Object} def - A serialized form of an SVG definition, including | |
* children | |
* | |
* @return {SVGElement} The inserted node. | |
*/ | |
definition: function(def) { | |
var ren = this; | |
function recurse(config, parent) { | |
var ret; | |
each(splat(config), function(item) { | |
var node = ren.createElement(item.tagName), | |
attr = {}; | |
// Set attributes | |
objectEach(item, function(val, key) { | |
if (key !== 'tagName' && key !== 'children' && key !== 'textContent') { | |
attr[key] = val; | |
} | |
}); | |
node.attr(attr); | |
// Add to the tree | |
node.add(parent || ren.defs); | |
// Add text content | |
if (item.textContent) { | |
node.element.appendChild(doc.createTextNode(item.textContent)); | |
} | |
// Recurse | |
recurse(item.children || [], node); | |
ret = node; | |
}); | |
// Return last node added (on top level it's the only one) | |
return ret; | |
} | |
return recurse(def); | |
}, | |
/** | |
* Detect whether the renderer is hidden. This happens when one of the | |
* parent elements has display: none. Used internally to detect when we need | |
* to render preliminarily in another div to get the text bounding boxes | |
* right. | |
* | |
* @returns {boolean} True if it is hidden. | |
*/ | |
isHidden: function() { // #608 | |
return !this.boxWrapper.getBBox().width; | |
}, | |
/** | |
* Destroys the renderer and its allocated members. | |
*/ | |
destroy: function() { | |
var renderer = this, | |
rendererDefs = renderer.defs; | |
renderer.box = null; | |
renderer.boxWrapper = renderer.boxWrapper.destroy(); | |
// Call destroy on all gradient elements | |
destroyObjectProperties(renderer.gradients || {}); | |
renderer.gradients = null; | |
// Defs are null in VMLRenderer | |
// Otherwise, destroy them here. | |
if (rendererDefs) { | |
renderer.defs = rendererDefs.destroy(); | |
} | |
// Remove sub pixel fix handler (#982) | |
if (renderer.unSubPixelFix) { | |
renderer.unSubPixelFix(); | |
} | |
renderer.alignedObjects = null; | |
return null; | |
}, | |
/** | |
* Create a wrapper for an SVG element. Serves as a factory for | |
* {@link SVGElement}, but this function is itself mostly called from | |
* primitive factories like {@link SVGRenderer#path}, {@link | |
* SVGRenderer#rect} or {@link SVGRenderer#text}. | |
* | |
* @param {string} nodeName - The node name, for example `rect`, `g` etc. | |
* @returns {SVGElement} The generated SVGElement. | |
*/ | |
createElement: function(nodeName) { | |
var wrapper = new this.Element(); | |
wrapper.init(this, nodeName); | |
return wrapper; | |
}, | |
/** | |
* Dummy function for plugins, called every time the renderer is updated. | |
* Prior to Highcharts 5, this was used for the canvg renderer. | |
* @function | |
*/ | |
draw: noop, | |
/** | |
* Get converted radial gradient attributes according to the radial | |
* reference. Used internally from the {@link SVGElement#colorGradient} | |
* function. | |
* | |
* @private | |
*/ | |
getRadialAttr: function(radialReference, gradAttr) { | |
return { | |
cx: (radialReference[0] - radialReference[2] / 2) + gradAttr.cx * radialReference[2], | |
cy: (radialReference[1] - radialReference[2] / 2) + gradAttr.cy * radialReference[2], | |
r: gradAttr.r * radialReference[2] | |
}; | |
}, | |
getSpanWidth: function(wrapper, tspan) { | |
var renderer = this, | |
bBox = wrapper.getBBox(true), | |
actualWidth = bBox.width; | |
// Old IE cannot measure the actualWidth for SVG elements (#2314) | |
if (!svg && renderer.forExport) { | |
actualWidth = renderer.measureSpanWidth(tspan.firstChild.data, wrapper.styles); | |
} | |
return actualWidth; | |
}, | |
applyEllipsis: function(wrapper, tspan, text, width) { | |
var renderer = this, | |
actualWidth = renderer.getSpanWidth(wrapper, tspan), | |
wasTooLong = actualWidth > width, | |
str = text, | |
currentIndex, | |
minIndex = 0, | |
maxIndex = text.length, | |
updateTSpan = function(s) { | |
tspan.removeChild(tspan.firstChild); | |
if (s) { | |
tspan.appendChild(doc.createTextNode(s)); | |
} | |
}; | |
if (wasTooLong) { | |
while (minIndex <= maxIndex) { | |
currentIndex = Math.ceil((minIndex + maxIndex) / 2); | |
str = text.substring(0, currentIndex) + '\u2026'; | |
updateTSpan(str); | |
actualWidth = renderer.getSpanWidth(wrapper, tspan); | |
if (minIndex === maxIndex) { | |
// Complete | |
minIndex = maxIndex + 1; | |
} else if (actualWidth > width) { | |
// Too large. Set max index to current. | |
maxIndex = currentIndex - 1; | |
} else { | |
// Within width. Set min index to current. | |
minIndex = currentIndex; | |
} | |
} | |
// If max index was 0 it means just ellipsis was also to large. | |
if (maxIndex === 0) { | |
// Remove ellipses. | |
updateTSpan(''); | |
} | |
} | |
return wasTooLong; | |
}, | |
/** | |
* Parse a simple HTML string into SVG tspans. Called internally when text | |
* is set on an SVGElement. The function supports a subset of HTML tags, | |
* CSS text features like `width`, `text-overflow`, `white-space`, and | |
* also attributes like `href` and `style`. | |
* @private | |
* @param {SVGElement} wrapper The parent SVGElement. | |
*/ | |
buildText: function(wrapper) { | |
var textNode = wrapper.element, | |
renderer = this, | |
forExport = renderer.forExport, | |
textStr = pick(wrapper.textStr, '').toString(), | |
hasMarkup = textStr.indexOf('<') !== -1, | |
lines, | |
childNodes = textNode.childNodes, | |
clsRegex, | |
styleRegex, | |
hrefRegex, | |
wasTooLong, | |
parentX = attr(textNode, 'x'), | |
textStyles = wrapper.styles, | |
width = wrapper.textWidth, | |
textLineHeight = textStyles && textStyles.lineHeight, | |
textOutline = textStyles && textStyles.textOutline, | |
ellipsis = textStyles && textStyles.textOverflow === 'ellipsis', | |
noWrap = textStyles && textStyles.whiteSpace === 'nowrap', | |
fontSize = textStyles && textStyles.fontSize, | |
textCache, | |
isSubsequentLine, | |
i = childNodes.length, | |
tempParent = width && !wrapper.added && this.box, | |
getLineHeight = function(tspan) { | |
var fontSizeStyle; | |
return textLineHeight ? | |
pInt(textLineHeight) : | |
renderer.fontMetrics( | |
fontSizeStyle, | |
// Get the computed size from parent if not explicit | |
tspan.getAttribute('style') ? tspan : textNode | |
).h; | |
}, | |
unescapeAngleBrackets = function(inputStr) { | |
return inputStr.replace(/</g, '<').replace(/>/g, '>'); | |
}; | |
// The buildText code is quite heavy, so if we're not changing something | |
// that affects the text, skip it (#6113). | |
textCache = [ | |
textStr, | |
ellipsis, | |
noWrap, | |
textLineHeight, | |
textOutline, | |
fontSize, | |
width | |
].join(','); | |
if (textCache === wrapper.textCache) { | |
return; | |
} | |
wrapper.textCache = textCache; | |
/// remove old text | |
while (i--) { | |
textNode.removeChild(childNodes[i]); | |
} | |
// Skip tspans, add text directly to text node. The forceTSpan is a hook | |
// used in text outline hack. | |
if (!hasMarkup && !textOutline && !ellipsis && !width && textStr.indexOf(' ') === -1) { | |
textNode.appendChild(doc.createTextNode(unescapeAngleBrackets(textStr))); | |
// Complex strings, add more logic | |
} else { | |
clsRegex = /<.*class="([^"]+)".*>/; | |
styleRegex = /<.*style="([^"]+)".*>/; | |
hrefRegex = /<.*href="([^"]+)".*>/; | |
if (tempParent) { | |
tempParent.appendChild(textNode); // attach it to the DOM to read offset width | |
} | |
if (hasMarkup) { | |
lines = textStr | |
.replace(/<(b|strong)>/g, '<span class="highcharts-strong">') | |
.replace(/<(i|em)>/g, '<span class="highcharts-emphasized">') | |
.replace(/<a/g, '<span') | |
.replace(/<\/(b|strong|i|em|a)>/g, '</span>') | |
.split(/<br.*?>/g); | |
} else { | |
lines = [textStr]; | |
} | |
// Trim empty lines (#5261) | |
lines = grep(lines, function(line) { | |
return line !== ''; | |
}); | |
// build the lines | |
each(lines, function buildTextLines(line, lineNo) { | |
var spans, | |
spanNo = 0; | |
line = line | |
.replace(/^\s+|\s+$/g, '') // Trim to prevent useless/costly process on the spaces (#5258) | |
.replace(/<span/g, '|||<span') | |
.replace(/<\/span>/g, '</span>|||'); | |
spans = line.split('|||'); | |
each(spans, function buildTextSpans(span) { | |
if (span !== '' || spans.length === 1) { | |
var attributes = {}, | |
tspan = doc.createElementNS(renderer.SVG_NS, 'tspan'), | |
spanCls, | |
spanStyle; // #390 | |
if (clsRegex.test(span)) { | |
spanCls = span.match(clsRegex)[1]; | |
attr(tspan, 'class', spanCls); | |
} | |
if (styleRegex.test(span)) { | |
spanStyle = span.match(styleRegex)[1].replace(/(;| |^)color([ :])/, '$1fill$2'); | |
attr(tspan, 'style', spanStyle); | |
} | |
if (hrefRegex.test(span) && !forExport) { // Not for export - #1529 | |
attr(tspan, 'onclick', 'location.href=\"' + span.match(hrefRegex)[1] + '\"'); | |
css(tspan, { | |
cursor: 'pointer' | |
}); | |
} | |
span = unescapeAngleBrackets(span.replace(/<(.|\n)*?>/g, '') || ' '); | |
// Nested tags aren't supported, and cause crash in Safari (#1596) | |
if (span !== ' ') { | |
// add the text node | |
tspan.appendChild(doc.createTextNode(span)); | |
if (!spanNo) { // first span in a line, align it to the left | |
if (lineNo && parentX !== null) { | |
attributes.x = parentX; | |
} | |
} else { | |
attributes.dx = 0; // #16 | |
} | |
// add attributes | |
attr(tspan, attributes); | |
// Append it | |
textNode.appendChild(tspan); | |
// first span on subsequent line, add the line height | |
if (!spanNo && isSubsequentLine) { | |
// allow getting the right offset height in exporting in IE | |
if (!svg && forExport) { | |
css(tspan, { | |
display: 'block' | |
}); | |
} | |
// Set the line height based on the font size of either | |
// the text element or the tspan element | |
attr( | |
tspan, | |
'dy', | |
getLineHeight(tspan) | |
); | |
} | |
/*if (width) { | |
renderer.breakText(wrapper, width); | |
}*/ | |
// Check width and apply soft breaks or ellipsis | |
if (width) { | |
var words = span.replace(/([^\^])-/g, '$1- ').split(' '), // #1273 | |
hasWhiteSpace = spans.length > 1 || lineNo || (words.length > 1 && !noWrap), | |
tooLong, | |
rest = [], | |
actualWidth, | |
dy = getLineHeight(tspan), | |
rotation = wrapper.rotation; | |
if (ellipsis) { | |
wasTooLong = renderer.applyEllipsis(wrapper, tspan, span, width); | |
} | |
while (!ellipsis && hasWhiteSpace && (words.length || rest.length)) { | |
wrapper.rotation = 0; // discard rotation when computing box | |
actualWidth = renderer.getSpanWidth(wrapper, tspan); | |
tooLong = actualWidth > width; | |
// For ellipsis, do a binary search for the correct string length | |
if (wasTooLong === undefined) { | |
wasTooLong = tooLong; // First time | |
} | |
// Looping down, this is the first word sequence that is not too long, | |
// so we can move on to build the next line. | |
if (!tooLong || words.length === 1) { | |
words = rest; | |
rest = []; | |
if (words.length && !noWrap) { | |
tspan = doc.createElementNS(SVG_NS, 'tspan'); | |
attr(tspan, { | |
dy: dy, | |
x: parentX | |
}); | |
if (spanStyle) { // #390 | |
attr(tspan, 'style', spanStyle); | |
} | |
textNode.appendChild(tspan); | |
} | |
if (actualWidth > width) { // a single word is pressing it out | |
width = actualWidth; | |
} | |
} else { // append to existing line tspan | |
tspan.removeChild(tspan.firstChild); | |
rest.unshift(words.pop()); | |
} | |
if (words.length) { | |
tspan.appendChild(doc.createTextNode(words.join(' ').replace(/- /g, '-'))); | |
} | |
} | |
wrapper.rotation = rotation; | |
} | |
spanNo++; | |
} | |
} | |
}); | |
// To avoid beginning lines that doesn't add to the textNode (#6144) | |
isSubsequentLine = isSubsequentLine || textNode.childNodes.length; | |
}); | |
if (wasTooLong) { | |
wrapper.attr('title', wrapper.textStr); | |
} | |
if (tempParent) { | |
tempParent.removeChild(textNode); // attach it to the DOM to read offset width | |
} | |
// Apply the text outline | |
if (textOutline && wrapper.applyTextOutline) { | |
wrapper.applyTextOutline(textOutline); | |
} | |
} | |
}, | |
/* | |
breakText: function (wrapper, width) { | |
var bBox = wrapper.getBBox(), | |
node = wrapper.element, | |
textLength = node.textContent.length, | |
pos = Math.round(width * textLength / bBox.width), // try this position first, based on average character width | |
increment = 0, | |
finalPos; | |
if (bBox.width > width) { | |
while (finalPos === undefined) { | |
textLength = node.getSubStringLength(0, pos); | |
if (textLength <= width) { | |
if (increment === -1) { | |
finalPos = pos; | |
} else { | |
increment = 1; | |
} | |
} else { | |
if (increment === 1) { | |
finalPos = pos - 1; | |
} else { | |
increment = -1; | |
} | |
} | |
pos += increment; | |
} | |
} | |
console.log('width', width, 'stringWidth', node.getSubStringLength(0, finalPos)) | |
}, | |
*/ | |
/** | |
* Returns white for dark colors and black for bright colors. | |
* | |
* @param {ColorString} rgba - The color to get the contrast for. | |
* @returns {string} The contrast color, either `#000000` or `#FFFFFF`. | |
*/ | |
getContrast: function(rgba) { | |
rgba = color(rgba).rgba; | |
// The threshold may be discussed. Here's a proposal for adding | |
// different weight to the color channels (#6216) | |
/* | |
rgba[0] *= 1; // red | |
rgba[1] *= 1.2; // green | |
rgba[2] *= 0.7; // blue | |
*/ | |
return rgba[0] + rgba[1] + rgba[2] > 2 * 255 ? '#000000' : '#FFFFFF'; | |
}, | |
/** | |
* Create a button with preset states. | |
* @param {string} text - The text or HTML to draw. | |
* @param {number} x - The x position of the button's left side. | |
* @param {number} y - The y position of the button's top side. | |
* @param {Function} callback - The function to execute on button click or | |
* touch. | |
* @param {SVGAttributes} [normalState] - SVG attributes for the normal | |
* state. | |
* @param {SVGAttributes} [hoverState] - SVG attributes for the hover state. | |
* @param {SVGAttributes} [pressedState] - SVG attributes for the pressed | |
* state. | |
* @param {SVGAttributes} [disabledState] - SVG attributes for the disabled | |
* state. | |
* @param {Symbol} [shape=rect] - The shape type. | |
* @returns {SVGRenderer} The button element. | |
*/ | |
button: function(text, x, y, callback, normalState, hoverState, pressedState, disabledState, shape) { | |
var label = this.label(text, x, y, shape, null, null, null, null, 'button'), | |
curState = 0; | |
// Default, non-stylable attributes | |
label.attr(merge({ | |
'padding': 8, | |
'r': 2 | |
}, normalState)); | |
// Add the events. IE9 and IE10 need mouseover and mouseout to funciton (#667). | |
addEvent(label.element, isMS ? 'mouseover' : 'mouseenter', function() { | |
if (curState !== 3) { | |
label.setState(1); | |
} | |
}); | |
addEvent(label.element, isMS ? 'mouseout' : 'mouseleave', function() { | |
if (curState !== 3) { | |
label.setState(curState); | |
} | |
}); | |
label.setState = function(state) { | |
// Hover state is temporary, don't record it | |
if (state !== 1) { | |
label.state = curState = state; | |
} | |
// Update visuals | |
label.removeClass(/highcharts-button-(normal|hover|pressed|disabled)/) | |
.addClass('highcharts-button-' + ['normal', 'hover', 'pressed', 'disabled'][state || 0]); | |
}; | |
return label | |
.on('click', function(e) { | |
if (curState !== 3) { | |
callback.call(label, e); | |
} | |
}); | |
}, | |
/** | |
* Make a straight line crisper by not spilling out to neighbour pixels. | |
* | |
* @param {Array} points - The original points on the format `['M', 0, 0, | |
* 'L', 100, 0]`. | |
* @param {number} width - The width of the line. | |
* @returns {Array} The original points array, but modified to render | |
* crisply. | |
*/ | |
crispLine: function(points, width) { | |
// normalize to a crisp line | |
if (points[1] === points[4]) { | |
// Substract due to #1129. Now bottom and left axis gridlines behave the same. | |
points[1] = points[4] = Math.round(points[1]) - (width % 2 / 2); | |
} | |
if (points[2] === points[5]) { | |
points[2] = points[5] = Math.round(points[2]) + (width % 2 / 2); | |
} | |
return points; | |
}, | |
/** | |
* Draw a path, wraps the SVG `path` element. | |
* | |
* @param {Array} [path] An SVG path definition in array form. | |
* | |
* @example | |
* var path = renderer.path(['M', 10, 10, 'L', 30, 30, 'z']) | |
* .attr({ stroke: '#ff00ff' }) | |
* .add(); | |
* @returns {SVGElement} The generated wrapper element. | |
* | |
* @sample highcharts/members/renderer-path-on-chart/ | |
* Draw a path in a chart | |
* @sample highcharts/members/renderer-path/ | |
* Draw a path independent from a chart | |
* | |
*/ | |
/** | |
* Draw a path, wraps the SVG `path` element. | |
* | |
* @param {SVGAttributes} [attribs] The initial attributes. | |
* @returns {SVGElement} The generated wrapper element. | |
*/ | |
path: function(path) { | |
var attribs = { | |
}; | |
if (isArray(path)) { | |
attribs.d = path; | |
} else if (isObject(path)) { // attributes | |
extend(attribs, path); | |
} | |
return this.createElement('path').attr(attribs); | |
}, | |
/** | |
* Draw a circle, wraps the SVG `circle` element. | |
* | |
* @param {number} [x] The center x position. | |
* @param {number} [y] The center y position. | |
* @param {number} [r] The radius. | |
* @returns {SVGElement} The generated wrapper element. | |
* | |
* @sample highcharts/members/renderer-circle/ Drawing a circle | |
*/ | |
/** | |
* Draw a circle, wraps the SVG `circle` element. | |
* | |
* @param {SVGAttributes} [attribs] The initial attributes. | |
* @returns {SVGElement} The generated wrapper element. | |
*/ | |
circle: function(x, y, r) { | |
var attribs = isObject(x) ? x : { | |
x: x, | |
y: y, | |
r: r | |
}, | |
wrapper = this.createElement('circle'); | |
// Setting x or y translates to cx and cy | |
wrapper.xSetter = wrapper.ySetter = function(value, key, element) { | |
element.setAttribute('c' + key, value); | |
}; | |
return wrapper.attr(attribs); | |
}, | |
/** | |
* Draw and return an arc. | |
* @param {number} [x=0] Center X position. | |
* @param {number} [y=0] Center Y position. | |
* @param {number} [r=0] The outer radius of the arc. | |
* @param {number} [innerR=0] Inner radius like used in donut charts. | |
* @param {number} [start=0] The starting angle of the arc in radians, where | |
* 0 is to the right and `-Math.PI/2` is up. | |
* @param {number} [end=0] The ending angle of the arc in radians, where 0 | |
* is to the right and `-Math.PI/2` is up. | |
* @returns {SVGElement} The generated wrapper element. | |
* | |
* @sample highcharts/members/renderer-arc/ | |
* Drawing an arc | |
*/ | |
/** | |
* Draw and return an arc. Overloaded function that takes arguments object. | |
* @param {SVGAttributes} attribs Initial SVG attributes. | |
* @returns {SVGElement} The generated wrapper element. | |
*/ | |
arc: function(x, y, r, innerR, start, end) { | |
var arc, | |
options; | |
if (isObject(x)) { | |
options = x; | |
y = options.y; | |
r = options.r; | |
innerR = options.innerR; | |
start = options.start; | |
end = options.end; | |
x = options.x; | |
} else { | |
options = { | |
innerR: innerR, | |
start: start, | |
end: end | |
}; | |
} | |
// Arcs are defined as symbols for the ability to set | |
// attributes in attr and animate | |
arc = this.symbol('arc', x, y, r, r, options); | |
arc.r = r; // #959 | |
return arc; | |
}, | |
/** | |
* Draw and return a rectangle. | |
* @param {number} [x] Left position. | |
* @param {number} [y] Top position. | |
* @param {number} [width] Width of the rectangle. | |
* @param {number} [height] Height of the rectangle. | |
* @param {number} [r] Border corner radius. | |
* @param {number} [strokeWidth] A stroke width can be supplied to allow | |
* crisp drawing. | |
* @returns {SVGElement} The generated wrapper element. | |
*/ | |
/** | |
* Draw and return a rectangle. | |
* @param {SVGAttributes} [attributes] | |
* General SVG attributes for the rectangle. | |
* @return {SVGElement} | |
* The generated wrapper element. | |
* | |
* @sample highcharts/members/renderer-rect-on-chart/ | |
* Draw a rectangle in a chart | |
* @sample highcharts/members/renderer-rect/ | |
* Draw a rectangle independent from a chart | |
*/ | |
rect: function(x, y, width, height, r, strokeWidth) { | |
r = isObject(x) ? x.r : r; | |
var wrapper = this.createElement('rect'), | |
attribs = isObject(x) ? x : x === undefined ? {} : { | |
x: x, | |
y: y, | |
width: Math.max(width, 0), | |
height: Math.max(height, 0) | |
}; | |
if (r) { | |
attribs.r = r; | |
} | |
wrapper.rSetter = function(value, key, element) { | |
attr(element, { | |
rx: value, | |
ry: value | |
}); | |
}; | |
return wrapper.attr(attribs); | |
}, | |
/** | |
* Resize the {@link SVGRenderer#box} and re-align all aligned child | |
* elements. | |
* @param {number} width | |
* The new pixel width. | |
* @param {number} height | |
* The new pixel height. | |
* @param {Boolean|AnimationOptions} [animate=true] | |
* Whether and how to animate. | |
*/ | |
setSize: function(width, height, animate) { | |
var renderer = this, | |
alignedObjects = renderer.alignedObjects, | |
i = alignedObjects.length; | |
renderer.width = width; | |
renderer.height = height; | |
renderer.boxWrapper.animate({ | |
width: width, | |
height: height | |
}, { | |
step: function() { | |
this.attr({ | |
viewBox: '0 0 ' + this.attr('width') + ' ' + this.attr('height') | |
}); | |
}, | |
duration: pick(animate, true) ? undefined : 0 | |
}); | |
while (i--) { | |
alignedObjects[i].align(); | |
} | |
}, | |
/** | |
* Create and return an svg group element. Child {@link Highcharts.SVGElement} | |
* objects are added to the group by using the group as the first parameter | |
* in {@link Highcharts.SVGElement#add|add()}. | |
* | |
* @param {string} [name] The group will be given a class name of | |
* `highcharts-{name}`. This can be used for styling and scripting. | |
* @returns {SVGElement} The generated wrapper element. | |
* | |
* @sample highcharts/members/renderer-g/ | |
* Show and hide grouped objects | |
*/ | |
g: function(name) { | |
var elem = this.createElement('g'); | |
return name ? elem.attr({ | |
'class': 'highcharts-' + name | |
}) : elem; | |
}, | |
/** | |
* Display an image. | |
* @param {string} src The image source. | |
* @param {number} [x] The X position. | |
* @param {number} [y] The Y position. | |
* @param {number} [width] The image width. If omitted, it defaults to the | |
* image file width. | |
* @param {number} [height] The image height. If omitted it defaults to the | |
* image file height. | |
* @returns {SVGElement} The generated wrapper element. | |
* | |
* @sample highcharts/members/renderer-image-on-chart/ | |
* Add an image in a chart | |
* @sample highcharts/members/renderer-image/ | |
* Add an image independent of a chart | |
*/ | |
image: function(src, x, y, width, height) { | |
var attribs = { | |
preserveAspectRatio: 'none' | |
}, | |
elemWrapper; | |
// optional properties | |
if (arguments.length > 1) { | |
extend(attribs, { | |
x: x, | |
y: y, | |
width: width, | |
height: height | |
}); | |
} | |
elemWrapper = this.createElement('image').attr(attribs); | |
// set the href in the xlink namespace | |
if (elemWrapper.element.setAttributeNS) { | |
elemWrapper.element.setAttributeNS('http://www.w3.org/1999/xlink', | |
'href', src); | |
} else { | |
// could be exporting in IE | |
// using href throws "not supported" in ie7 and under, requries regex shim to fix later | |
elemWrapper.element.setAttribute('hc-svg-href', src); | |
} | |
return elemWrapper; | |
}, | |
/** | |
* Draw a symbol out of pre-defined shape paths from {@SVGRenderer#symbols}. | |
* It is used in Highcharts for point makers, which cake a `symbol` option, | |
* and label and button backgrounds like in the tooltip and stock flags. | |
* | |
* @param {Symbol} symbol - The symbol name. | |
* @param {number} x - The X coordinate for the top left position. | |
* @param {number} y - The Y coordinate for the top left position. | |
* @param {number} width - The pixel width. | |
* @param {number} height - The pixel height. | |
* @param {Object} [options] - Additional options, depending on the actual | |
* symbol drawn. | |
* @param {number} [options.anchorX] - The anchor X position for the | |
* `callout` symbol. This is where the chevron points to. | |
* @param {number} [options.anchorY] - The anchor Y position for the | |
* `callout` symbol. This is where the chevron points to. | |
* @param {number} [options.end] - The end angle of an `arc` symbol. | |
* @param {boolean} [options.open] - Whether to draw `arc` symbol open or | |
* closed. | |
* @param {number} [options.r] - The radius of an `arc` symbol, or the | |
* border radius for the `callout` symbol. | |
* @param {number} [options.start] - The start angle of an `arc` symbol. | |
*/ | |
symbol: function(symbol, x, y, width, height, options) { | |
var ren = this, | |
obj, | |
imageRegex = /^url\((.*?)\)$/, | |
isImage = imageRegex.test(symbol), | |
sym = !isImage && (this.symbols[symbol] ? symbol : 'circle'), | |
// get the symbol definition function | |
symbolFn = sym && this.symbols[sym], | |
// check if there's a path defined for this symbol | |
path = defined(x) && symbolFn && symbolFn.call( | |
this.symbols, | |
Math.round(x), | |
Math.round(y), | |
width, | |
height, | |
options | |
), | |
imageSrc, | |
centerImage; | |
if (symbolFn) { | |
obj = this.path(path); | |
// expando properties for use in animate and attr | |
extend(obj, { | |
symbolName: sym, | |
x: x, | |
y: y, | |
width: width, | |
height: height | |
}); | |
if (options) { | |
extend(obj, options); | |
} | |
// Image symbols | |
} else if (isImage) { | |
imageSrc = symbol.match(imageRegex)[1]; | |
// Create the image synchronously, add attribs async | |
obj = this.image(imageSrc); | |
// The image width is not always the same as the symbol width. The | |
// image may be centered within the symbol, as is the case when | |
// image shapes are used as label backgrounds, for example in flags. | |
obj.imgwidth = pick( | |
symbolSizes[imageSrc] && symbolSizes[imageSrc].width, | |
options && options.width | |
); | |
obj.imgheight = pick( | |
symbolSizes[imageSrc] && symbolSizes[imageSrc].height, | |
options && options.height | |
); | |
/** | |
* Set the size and position | |
*/ | |
centerImage = function() { | |
obj.attr({ | |
width: obj.width, | |
height: obj.height | |
}); | |
}; | |
/** | |
* Width and height setters that take both the image's physical size | |
* and the label size into consideration, and translates the image | |
* to center within the label. | |
*/ | |
each(['width', 'height'], function(key) { | |
obj[key + 'Setter'] = function(value, key) { | |
var attribs = {}, | |
imgSize = this['img' + key], | |
trans = key === 'width' ? 'translateX' : 'translateY'; | |
this[key] = value; | |
if (defined(imgSize)) { | |
if (this.element) { | |
this.element.setAttribute(key, imgSize); | |
} | |
if (!this.alignByTranslate) { | |
attribs[trans] = ((this[key] || 0) - imgSize) / 2; | |
this.attr(attribs); | |
} | |
} | |
}; | |
}); | |
if (defined(x)) { | |
obj.attr({ | |
x: x, | |
y: y | |
}); | |
} | |
obj.isImg = true; | |
if (defined(obj.imgwidth) && defined(obj.imgheight)) { | |
centerImage(); | |
} else { | |
// Initialize image to be 0 size so export will still function if there's no cached sizes. | |
obj.attr({ | |
width: 0, | |
height: 0 | |
}); | |
// Create a dummy JavaScript image to get the width and height. Due to a bug in IE < 8, | |
// the created element must be assigned to a variable in order to load (#292). | |
createElement('img', { | |
onload: function() { | |
var chart = charts[ren.chartIndex]; | |
// Special case for SVGs on IE11, the width is not accessible until the image is | |
// part of the DOM (#2854). | |
if (this.width === 0) { | |
css(this, { | |
position: 'absolute', | |
top: '-999em' | |
}); | |
doc.body.appendChild(this); | |
} | |
// Center the image | |
symbolSizes[imageSrc] = { // Cache for next | |
width: this.width, | |
height: this.height | |
}; | |
obj.imgwidth = this.width; | |
obj.imgheight = this.height; | |
if (obj.element) { | |
centerImage(); | |
} | |
// Clean up after #2854 workaround. | |
if (this.parentNode) { | |
this.parentNode.removeChild(this); | |
} | |
// Fire the load event when all external images are loaded | |
ren.imgCount--; | |
if (!ren.imgCount && chart && chart.onload) { | |
chart.onload(); | |
} | |
}, | |
src: imageSrc | |
}); | |
this.imgCount++; | |
} | |
} | |
return obj; | |
}, | |
/** | |
* @typedef {string} Symbol | |
* | |
* Can be one of `arc`, `callout`, `circle`, `diamond`, `square`, | |
* `triangle`, `triangle-down`. Symbols are used internally for point | |
* markers, button and label borders and backgrounds, or custom shapes. | |
* Extendable by adding to {@link SVGRenderer#symbols}. | |
*/ | |
/** | |
* An extendable collection of functions for defining symbol paths. | |
*/ | |
symbols: { | |
'circle': function(x, y, w, h) { | |
// Return a full arc | |
return this.arc(x + w / 2, y + h / 2, w / 2, h / 2, { | |
start: 0, | |
end: Math.PI * 2, | |
open: false | |
}); | |
}, | |
'square': function(x, y, w, h) { | |
return [ | |
'M', x, y, | |
'L', x + w, y, | |
x + w, y + h, | |
x, y + h, | |
'Z' | |
]; | |
}, | |
'triangle': function(x, y, w, h) { | |
return [ | |
'M', x + w / 2, y, | |
'L', x + w, y + h, | |
x, y + h, | |
'Z' | |
]; | |
}, | |
'triangle-down': function(x, y, w, h) { | |
return [ | |
'M', x, y, | |
'L', x + w, y, | |
x + w / 2, y + h, | |
'Z' | |
]; | |
}, | |
'diamond': function(x, y, w, h) { | |
return [ | |
'M', x + w / 2, y, | |
'L', x + w, y + h / 2, | |
x + w / 2, y + h, | |
x, y + h / 2, | |
'Z' | |
]; | |
}, | |
'arc': function(x, y, w, h, options) { | |
var start = options.start, | |
rx = options.r || w, | |
ry = options.r || h || w, | |
proximity = 0.001, | |
fullCircle = | |
Math.abs(options.end - options.start - 2 * Math.PI) < | |
proximity, | |
// Substract a small number to prevent cos and sin of start and | |
// end from becoming equal on 360 arcs (related: #1561) | |
end = options.end - proximity, | |
innerRadius = options.innerR, | |
open = pick(options.open, fullCircle), | |
cosStart = Math.cos(start), | |
sinStart = Math.sin(start), | |
cosEnd = Math.cos(end), | |
sinEnd = Math.sin(end), | |
longArc = options.end - start < Math.PI ? 0 : 1, | |
arc; | |
arc = [ | |
'M', | |
x + rx * cosStart, | |
y + ry * sinStart, | |
'A', // arcTo | |
rx, // x radius | |
ry, // y radius | |
0, // slanting | |
longArc, // long or short arc | |
1, // clockwise | |
x + rx * cosEnd, | |
y + ry * sinEnd | |
]; | |
if (defined(innerRadius)) { | |
arc.push( | |
open ? 'M' : 'L', | |
x + innerRadius * cosEnd, | |
y + innerRadius * sinEnd, | |
'A', // arcTo | |
innerRadius, // x radius | |
innerRadius, // y radius | |
0, // slanting | |
longArc, // long or short arc | |
0, // clockwise | |
x + innerRadius * cosStart, | |
y + innerRadius * sinStart | |
); | |
} | |
arc.push(open ? '' : 'Z'); // close | |
return arc; | |
}, | |
/** | |
* Callout shape used for default tooltips, also used for rounded rectangles in VML | |
*/ | |
callout: function(x, y, w, h, options) { | |
var arrowLength = 6, | |
halfDistance = 6, | |
r = Math.min((options && options.r) || 0, w, h), | |
safeDistance = r + halfDistance, | |
anchorX = options && options.anchorX, | |
anchorY = options && options.anchorY, | |
path; | |
path = [ | |
'M', x + r, y, | |
'L', x + w - r, y, // top side | |
'C', x + w, y, x + w, y, x + w, y + r, // top-right corner | |
'L', x + w, y + h - r, // right side | |
'C', x + w, y + h, x + w, y + h, x + w - r, y + h, // bottom-right corner | |
'L', x + r, y + h, // bottom side | |
'C', x, y + h, x, y + h, x, y + h - r, // bottom-left corner | |
'L', x, y + r, // left side | |
'C', x, y, x, y, x + r, y // top-left corner | |
]; | |
// Anchor on right side | |
if (anchorX && anchorX > w) { | |
// Chevron | |
if (anchorY > y + safeDistance && anchorY < y + h - safeDistance) { | |
path.splice(13, 3, | |
'L', x + w, anchorY - halfDistance, | |
x + w + arrowLength, anchorY, | |
x + w, anchorY + halfDistance, | |
x + w, y + h - r | |
); | |
// Simple connector | |
} else { | |
path.splice(13, 3, | |
'L', x + w, h / 2, | |
anchorX, anchorY, | |
x + w, h / 2, | |
x + w, y + h - r | |
); | |
} | |
// Anchor on left side | |
} else if (anchorX && anchorX < 0) { | |
// Chevron | |
if (anchorY > y + safeDistance && anchorY < y + h - safeDistance) { | |
path.splice(33, 3, | |
'L', x, anchorY + halfDistance, | |
x - arrowLength, anchorY, | |
x, anchorY - halfDistance, | |
x, y + r | |
); | |
// Simple connector | |
} else { | |
path.splice(33, 3, | |
'L', x, h / 2, | |
anchorX, anchorY, | |
x, h / 2, | |
x, y + r | |
); | |
} | |
} else if (anchorY && anchorY > h && anchorX > x + safeDistance && anchorX < x + w - safeDistance) { // replace bottom | |
path.splice(23, 3, | |
'L', anchorX + halfDistance, y + h, | |
anchorX, y + h + arrowLength, | |
anchorX - halfDistance, y + h, | |
x + r, y + h | |
); | |
} else if (anchorY && anchorY < 0 && anchorX > x + safeDistance && anchorX < x + w - safeDistance) { // replace top | |
path.splice(3, 3, | |
'L', anchorX - halfDistance, y, | |
anchorX, y - arrowLength, | |
anchorX + halfDistance, y, | |
w - r, y | |
); | |
} | |
return path; | |
} | |
}, | |
/** | |
* @typedef {SVGElement} ClipRect - A clipping rectangle that can be applied | |
* to one or more {@link SVGElement} instances. It is instanciated with the | |
* {@link SVGRenderer#clipRect} function and applied with the {@link | |
* SVGElement#clip} function. | |
* | |
* @example | |
* var circle = renderer.circle(100, 100, 100) | |
* .attr({ fill: 'red' }) | |
* .add(); | |
* var clipRect = renderer.clipRect(100, 100, 100, 100); | |
* | |
* // Leave only the lower right quarter visible | |
* circle.clip(clipRect); | |
*/ | |
/** | |
* Define a clipping rectangle | |
* @param {String} id | |
* @param {number} x | |
* @param {number} y | |
* @param {number} width | |
* @param {number} height | |
* @returns {ClipRect} A clipping rectangle. | |
*/ | |
clipRect: function(x, y, width, height) { | |
var wrapper, | |
id = H.uniqueKey(), | |
clipPath = this.createElement('clipPath').attr({ | |
id: id | |
}).add(this.defs); | |
wrapper = this.rect(x, y, width, height, 0).add(clipPath); | |
wrapper.id = id; | |
wrapper.clipPath = clipPath; | |
wrapper.count = 0; | |
return wrapper; | |
}, | |
/** | |
* Draw text. The text can contain a subset of HTML, like spans and anchors | |
* and some basic text styling of these. For more advanced features like | |
* border and background, use {@link Highcharts.SVGRenderer#label} instead. | |
* To update the text after render, run `text.attr({ text: 'New text' })`. | |
* @param {String} str | |
* The text of (subset) HTML to draw. | |
* @param {number} x | |
* The x position of the text's lower left corner. | |
* @param {number} y | |
* The y position of the text's lower left corner. | |
* @param {Boolean} [useHTML=false] | |
* Use HTML to render the text. | |
* | |
* @return {SVGElement} The text object. | |
* | |
* @sample highcharts/members/renderer-text-on-chart/ | |
* Annotate the chart freely | |
* @sample highcharts/members/renderer-on-chart/ | |
* Annotate with a border and in response to the data | |
* @sample highcharts/members/renderer-text/ | |
* Formatted text | |
*/ | |
text: function(str, x, y, useHTML) { | |
// declare variables | |
var renderer = this, | |
fakeSVG = !svg && renderer.forExport, | |
wrapper, | |
attribs = {}; | |
if (useHTML && (renderer.allowHTML || !renderer.forExport)) { | |
return renderer.html(str, x, y); | |
} | |
attribs.x = Math.round(x || 0); // X is always needed for line-wrap logic | |
if (y) { | |
attribs.y = Math.round(y); | |
} | |
if (str || str === 0) { | |
attribs.text = str; | |
} | |
wrapper = renderer.createElement('text') | |
.attr(attribs); | |
// Prevent wrapping from creating false offsetWidths in export in legacy IE (#1079, #1063) | |
if (fakeSVG) { | |
wrapper.css({ | |
position: 'absolute' | |
}); | |
} | |
if (!useHTML) { | |
wrapper.xSetter = function(value, key, element) { | |
var tspans = element.getElementsByTagName('tspan'), | |
tspan, | |
parentVal = element.getAttribute(key), | |
i; | |
for (i = 0; i < tspans.length; i++) { | |
tspan = tspans[i]; | |
// If the x values are equal, the tspan represents a linebreak | |
if (tspan.getAttribute(key) === parentVal) { | |
tspan.setAttribute(key, value); | |
} | |
} | |
element.setAttribute(key, value); | |
}; | |
} | |
return wrapper; | |
}, | |
/** | |
* Utility to return the baseline offset and total line height from the font | |
* size. | |
* | |
* @param {?string} fontSize The current font size to inspect. If not given, | |
* the font size will be found from the DOM element. | |
* @param {SVGElement|SVGDOMElement} [elem] The element to inspect for a | |
* current font size. | |
* @returns {Object} An object containing `h`: the line height, `b`: the | |
* baseline relative to the top of the box, and `f`: the font size. | |
*/ | |
fontMetrics: function(fontSize, elem) { | |
var lineHeight, | |
baseline; | |
fontSize = elem && SVGElement.prototype.getStyle.call( | |
elem, | |
'font-size' | |
); | |
// Handle different units | |
if (/px/.test(fontSize)) { | |
fontSize = pInt(fontSize); | |
} else if (/em/.test(fontSize)) { | |
// The em unit depends on parent items | |
fontSize = parseFloat(fontSize) * | |
(elem ? this.fontMetrics(null, elem.parentNode).f : 16); | |
} else { | |
fontSize = 12; | |
} | |
// Empirical values found by comparing font size and bounding box | |
// height. Applies to the default font family. | |
// http://jsfiddle.net/highcharts/7xvn7/ | |
lineHeight = fontSize < 24 ? fontSize + 3 : Math.round(fontSize * 1.2); | |
baseline = Math.round(lineHeight * 0.8); | |
return { | |
h: lineHeight, | |
b: baseline, | |
f: fontSize | |
}; | |
}, | |
/** | |
* Correct X and Y positioning of a label for rotation (#1764) | |
*/ | |
rotCorr: function(baseline, rotation, alterY) { | |
var y = baseline; | |
if (rotation && alterY) { | |
y = Math.max(y * Math.cos(rotation * deg2rad), 4); | |
} | |
return { | |
x: (-baseline / 3) * Math.sin(rotation * deg2rad), | |
y: y | |
}; | |
}, | |
/** | |
* Draw a label, which is an extended text element with support for border | |
* and background. Highcharts creates a `g` element with a text and a `path` | |
* or `rect` inside, to make it behave somewhat like a HTML div. Border and | |
* background are set through `stroke`, `stroke-width` and `fill` attributes | |
* using the {@link Highcharts.SVGElement#attr|attr} method. To update the | |
* text after render, run `label.attr({ text: 'New text' })`. | |
* | |
* @param {string} str | |
* The initial text string or (subset) HTML to render. | |
* @param {number} x | |
* The x position of the label's left side. | |
* @param {number} y | |
* The y position of the label's top side or baseline, depending on | |
* the `baseline` parameter. | |
* @param {String} shape | |
* The shape of the label's border/background, if any. Defaults to | |
* `rect`. Other possible values are `callout` or other shapes | |
* defined in {@link Highcharts.SVGRenderer#symbols}. | |
* @param {number} anchorX | |
* In case the `shape` has a pointer, like a flag, this is the | |
* coordinates it should be pinned to. | |
* @param {number} anchorY | |
* In case the `shape` has a pointer, like a flag, this is the | |
* coordinates it should be pinned to. | |
* @param {Boolean} baseline | |
* Whether to position the label relative to the text baseline, | |
* like {@link Highcharts.SVGRenderer#text|renderer.text}, or to the | |
* upper border of the rectangle. | |
* @param {String} className | |
* Class name for the group. | |
* | |
* @return {SVGElement} | |
* The generated label. | |
* | |
* @sample highcharts/members/renderer-label-on-chart/ | |
* A label on the chart | |
*/ | |
label: function(str, x, y, shape, anchorX, anchorY, useHTML, baseline, className) { | |
var renderer = this, | |
wrapper = renderer.g(className !== 'button' && 'label'), | |
text = wrapper.text = renderer.text('', 0, 0, useHTML) | |
.attr({ | |
zIndex: 1 | |
}), | |
box, | |
bBox, | |
alignFactor = 0, | |
padding = 3, | |
paddingLeft = 0, | |
width, | |
height, | |
wrapperX, | |
wrapperY, | |
textAlign, | |
deferredAttr = {}, | |
strokeWidth, | |
baselineOffset, | |
hasBGImage = /^url\((.*?)\)$/.test(shape), | |
needsBox = hasBGImage, | |
getCrispAdjust, | |
updateBoxSize, | |
updateTextPadding, | |
boxAttr; | |
if (className) { | |
wrapper.addClass('highcharts-' + className); | |
} | |
needsBox = true; // for styling | |
getCrispAdjust = function() { | |
return box.strokeWidth() % 2 / 2; | |
}; | |
/** | |
* This function runs after the label is added to the DOM (when the bounding box is | |
* available), and after the text of the label is updated to detect the new bounding | |
* box and reflect it in the border box. | |
*/ | |
updateBoxSize = function() { | |
var style = text.element.style, | |
crispAdjust, | |
attribs = {}; | |
bBox = (width === undefined || height === undefined || textAlign) && defined(text.textStr) && | |
text.getBBox(); //#3295 && 3514 box failure when string equals 0 | |
wrapper.width = (width || bBox.width || 0) + 2 * padding + paddingLeft; | |
wrapper.height = (height || bBox.height || 0) + 2 * padding; | |
// Update the label-scoped y offset | |
baselineOffset = padding + renderer.fontMetrics(style && style.fontSize, text).b; | |
if (needsBox) { | |
// Create the border box if it is not already present | |
if (!box) { | |
wrapper.box = box = renderer.symbols[shape] || hasBGImage ? // Symbol definition exists (#5324) | |
renderer.symbol(shape) : | |
renderer.rect(); | |
box.addClass( | |
(className === 'button' ? '' : 'highcharts-label-box') + // Don't use label className for buttons | |
(className ? ' highcharts-' + className + '-box' : '') | |
); | |
box.add(wrapper); | |
crispAdjust = getCrispAdjust(); | |
attribs.x = crispAdjust; | |
attribs.y = (baseline ? -baselineOffset : 0) + crispAdjust; | |
} | |
// Apply the box attributes | |
attribs.width = Math.round(wrapper.width); | |
attribs.height = Math.round(wrapper.height); | |
box.attr(extend(attribs, deferredAttr)); | |
deferredAttr = {}; | |
} | |
}; | |
/** | |
* This function runs after setting text or padding, but only if padding is changed | |
*/ | |
updateTextPadding = function() { | |
var textX = paddingLeft + padding, | |
textY; | |
// determin y based on the baseline | |
textY = baseline ? 0 : baselineOffset; | |
// compensate for alignment | |
if (defined(width) && bBox && (textAlign === 'center' || textAlign === 'right')) { | |
textX += { | |
center: 0.5, | |
right: 1 | |
}[textAlign] * (width - bBox.width); | |
} | |
// update if anything changed | |
if (textX !== text.x || textY !== text.y) { | |
text.attr('x', textX); | |
if (textY !== undefined) { | |
text.attr('y', textY); | |
} | |
} | |
// record current values | |
text.x = textX; | |
text.y = textY; | |
}; | |
/** | |
* Set a box attribute, or defer it if the box is not yet created | |
* @param {Object} key | |
* @param {Object} value | |
*/ | |
boxAttr = function(key, value) { | |
if (box) { | |
box.attr(key, value); | |
} else { | |
deferredAttr[key] = value; | |
} | |
}; | |
/** | |
* After the text element is added, get the desired size of the border box | |
* and add it before the text in the DOM. | |
*/ | |
wrapper.onAdd = function() { | |
text.add(wrapper); | |
wrapper.attr({ | |
text: (str || str === 0) ? str : '', // alignment is available now // #3295: 0 not rendered if given as a value | |
x: x, | |
y: y | |
}); | |
if (box && defined(anchorX)) { | |
wrapper.attr({ | |
anchorX: anchorX, | |
anchorY: anchorY | |
}); | |
} | |
}; | |
/* | |
* Add specific attribute setters. | |
*/ | |
// only change local variables | |
wrapper.widthSetter = function(value) { | |
width = H.isNumber(value) ? value : null; // width:auto => null | |
}; | |
wrapper.heightSetter = function(value) { | |
height = value; | |
}; | |
wrapper['text-alignSetter'] = function(value) { | |
textAlign = value; | |
}; | |
wrapper.paddingSetter = function(value) { | |
if (defined(value) && value !== padding) { | |
padding = wrapper.padding = value; | |
updateTextPadding(); | |
} | |
}; | |
wrapper.paddingLeftSetter = function(value) { | |
if (defined(value) && value !== paddingLeft) { | |
paddingLeft = value; | |
updateTextPadding(); | |
} | |
}; | |
// change local variable and prevent setting attribute on the group | |
wrapper.alignSetter = function(value) { | |
value = { | |
left: 0, | |
center: 0.5, | |
right: 1 | |
}[value]; | |
if (value !== alignFactor) { | |
alignFactor = value; | |
if (bBox) { // Bounding box exists, means we're dynamically changing | |
wrapper.attr({ | |
x: wrapperX | |
}); // #5134 | |
} | |
} | |
}; | |
// apply these to the box and the text alike | |
wrapper.textSetter = function(value) { | |
if (value !== undefined) { | |
text.textSetter(value); | |
} | |
updateBoxSize(); | |
updateTextPadding(); | |
}; | |
// apply these to the box but not to the text | |
wrapper['stroke-widthSetter'] = function(value, key) { | |
if (value) { | |
needsBox = true; | |
} | |
strokeWidth = this['stroke-width'] = value; | |
boxAttr(key, value); | |
}; | |
wrapper.rSetter = function(value, key) { | |
boxAttr(key, value); | |
}; | |
wrapper.anchorXSetter = function(value, key) { | |
anchorX = wrapper.anchorX = value; | |
boxAttr(key, Math.round(value) - getCrispAdjust() - wrapperX); | |
}; | |
wrapper.anchorYSetter = function(value, key) { | |
anchorY = wrapper.anchorY = value; | |
boxAttr(key, value - wrapperY); | |
}; | |
// rename attributes | |
wrapper.xSetter = function(value) { | |
wrapper.x = value; // for animation getter | |
if (alignFactor) { | |
value -= alignFactor * ((width || bBox.width) + 2 * padding); | |
} | |
wrapperX = Math.round(value); | |
wrapper.attr('translateX', wrapperX); | |
}; | |
wrapper.ySetter = function(value) { | |
wrapperY = wrapper.y = Math.round(value); | |
wrapper.attr('translateY', wrapperY); | |
}; | |
// Redirect certain methods to either the box or the text | |
var baseCss = wrapper.css; | |
return extend(wrapper, { | |
/** | |
* Pick up some properties and apply them to the text instead of the | |
* wrapper. | |
* @ignore | |
*/ | |
css: function(styles) { | |
if (styles) { | |
var textStyles = {}; | |
styles = merge(styles); // create a copy to avoid altering the original object (#537) | |
each(wrapper.textProps, function(prop) { | |
if (styles[prop] !== undefined) { | |
textStyles[prop] = styles[prop]; | |
delete styles[prop]; | |
} | |
}); | |
text.css(textStyles); | |
} | |
return baseCss.call(wrapper, styles); | |
}, | |
/** | |
* Return the bounding box of the box, not the group. | |
* @ignore | |
*/ | |
getBBox: function() { | |
return { | |
width: bBox.width + 2 * padding, | |
height: bBox.height + 2 * padding, | |
x: bBox.x - padding, | |
y: bBox.y - padding | |
}; | |
}, | |
/** | |
* Destroy and release memory. | |
* @ignore | |
*/ | |
destroy: function() { | |
// Added by button implementation | |
removeEvent(wrapper.element, 'mouseenter'); | |
removeEvent(wrapper.element, 'mouseleave'); | |
if (text) { | |
text = text.destroy(); | |
} | |
if (box) { | |
box = box.destroy(); | |
} | |
// Call base implementation to destroy the rest | |
SVGElement.prototype.destroy.call(wrapper); | |
// Release local pointers (#1298) | |
wrapper = renderer = updateBoxSize = updateTextPadding = boxAttr = null; | |
} | |
}); | |
} | |
}); // end SVGRenderer | |
// general renderer | |
H.Renderer = SVGRenderer; | |
}(Highcharts)); | |
(function(H) { | |
/** | |
* (c) 2010-2017 Torstein Honsi | |
* | |
* License: www.highcharts.com/license | |
*/ | |
var color = H.color, | |
each = H.each, | |
getTZOffset = H.getTZOffset, | |
isTouchDevice = H.isTouchDevice, | |
merge = H.merge, | |
pick = H.pick, | |
svg = H.svg, | |
win = H.win; | |
/* **************************************************************************** | |
* Handle the options * | |
*****************************************************************************/ | |
H.defaultOptions = { | |
symbols: ['circle', 'diamond', 'square', 'triangle', 'triangle-down'], | |
lang: { | |
loading: 'Loading...', | |
months: [ | |
'January', 'February', 'March', 'April', 'May', 'June', 'July', | |
'August', 'September', 'October', 'November', 'December' | |
], | |
shortMonths: [ | |
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', | |
'Aug', 'Sep', 'Oct', 'Nov', 'Dec' | |
], | |
weekdays: [ | |
'Sunday', 'Monday', 'Tuesday', 'Wednesday', | |
'Thursday', 'Friday', 'Saturday' | |
], | |
// invalidDate: '', | |
decimalPoint: '.', | |
numericSymbols: ['k', 'M', 'G', 'T', 'P', 'E'], // SI prefixes used in axis labels | |
resetZoom: 'Reset zoom', | |
resetZoomTitle: 'Reset zoom level 1:1', | |
thousandsSep: ' ' | |
}, | |
global: { | |
useUTC: true, | |
//timezoneOffset: 0 | |
}, | |
chart: { | |
//animation: true, | |
//alignTicks: false, | |
//reflow: true, | |
//className: null, | |
//events: { load, selection }, | |
//margin: [null], | |
//marginTop: null, | |
//marginRight: null, | |
//marginBottom: null, | |
//marginLeft: null, | |
borderRadius: 0, | |
colorCount: 10, | |
defaultSeriesType: 'line', | |
ignoreHiddenSeries: true, | |
//inverted: false, | |
spacing: [10, 10, 15, 10], | |
//spacingTop: 10, | |
//spacingRight: 10, | |
//spacingBottom: 15, | |
//spacingLeft: 10, | |
//zoomType: '' | |
resetZoomButton: { | |
theme: { | |
zIndex: 20 | |
}, | |
position: { | |
align: 'right', | |
x: -10, | |
//verticalAlign: 'top', | |
y: 10 | |
} | |
// relativeTo: 'plot' | |
}, | |
width: null, | |
height: null | |
}, | |
title: { | |
text: 'Chart title', | |
align: 'center', | |
// floating: false, | |
margin: 15, | |
// x: 0, | |
// verticalAlign: 'top', | |
// y: null, | |
// style: {}, // defined inline | |
widthAdjust: -44 | |
}, | |
subtitle: { | |
text: '', | |
align: 'center', | |
// floating: false | |
// x: 0, | |
// verticalAlign: 'top', | |
// y: null, | |
// style: {}, // defined inline | |
widthAdjust: -44 | |
}, | |
plotOptions: {}, | |
labels: { | |
//items: [], | |
style: { | |
//font: defaultFont, | |
position: 'absolute', | |
color: '#333333' | |
} | |
}, | |
legend: { | |
enabled: true, | |
align: 'center', | |
//floating: false, | |
layout: 'horizontal', | |
labelFormatter: function() { | |
return this.name; | |
}, | |
//borderWidth: 0, | |
borderColor: '#999999', | |
borderRadius: 0, | |
navigation: { | |
// animation: true, | |
// arrowSize: 12 | |
// style: {} // text styles | |
}, | |
// margin: 20, | |
// reversed: false, | |
// backgroundColor: null, | |
/*style: { | |
padding: '5px' | |
},*/ | |
itemCheckboxStyle: { | |
position: 'absolute', | |
width: '13px', // for IE precision | |
height: '13px' | |
}, | |
// itemWidth: undefined, | |
squareSymbol: true, | |
// symbolRadius: 0, | |
// symbolWidth: 16, | |
symbolPadding: 5, | |
verticalAlign: 'bottom', | |
// width: undefined, | |
x: 0, | |
y: 0, | |
title: { | |
//text: null | |
} | |
}, | |
loading: { | |
// hideDuration: 100, | |
// showDuration: 0 | |
}, | |
tooltip: { | |
enabled: true, | |
animation: svg, | |
//crosshairs: null, | |
borderRadius: 3, | |
dateTimeLabelFormats: { | |
millisecond: '%A, %b %e, %H:%M:%S.%L', | |
second: '%A, %b %e, %H:%M:%S', | |
minute: '%A, %b %e, %H:%M', | |
hour: '%A, %b %e, %H:%M', | |
day: '%A, %b %e, %Y', | |
week: 'Week from %A, %b %e, %Y', | |
month: '%B %Y', | |
year: '%Y' | |
}, | |
footerFormat: '', | |
//formatter: defaultFormatter, | |
/* todo: em font-size when finished comparing against HC4 | |
headerFormat: '<span style="font-size: 0.85em">{point.key}</span><br/>', | |
*/ | |
padding: 8, | |
//shape: 'callout', | |
//shared: false, | |
snap: isTouchDevice ? 25 : 10, | |
headerFormat: '<span class="highcharts-header">{point.key}</span><br/>', | |
pointFormat: '<span class="highcharts-color-{point.colorIndex}">' + | |
'\u25CF</span> {series.name}: <span class="highcharts-strong">' + | |
'{point.y}</span><br/>', | |
//xDateFormat: '%A, %b %e, %Y', | |
//valueDecimals: null, | |
//valuePrefix: '', | |
//valueSuffix: '' | |
}, | |
credits: { | |
enabled: true, | |
href: 'http://www.highcharts.com', | |
position: { | |
align: 'right', | |
x: -10, | |
verticalAlign: 'bottom', | |
y: -5 | |
}, | |
text: 'Highcharts.com' | |
} | |
}; | |
/** | |
* Sets the getTimezoneOffset function. If the timezone option is set, a default | |
* getTimezoneOffset function with that timezone is returned. If not, the | |
* specified getTimezoneOffset function is returned. If neither are specified, | |
* undefined is returned. | |
* @return {function} a getTimezoneOffset function or undefined | |
*/ | |
function getTimezoneOffsetOption() { | |
var globalOptions = H.defaultOptions.global, | |
moment = win.moment; | |
if (globalOptions.timezone) { | |
if (!moment) { | |
// getTimezoneOffset-function stays undefined because it depends on | |
// Moment.js | |
H.error(25); | |
} else { | |
return function(timestamp) { | |
return -moment.tz( | |
timestamp, | |
globalOptions.timezone | |
).utcOffset(); | |
}; | |
} | |
} | |
// If not timezone is set, look for the getTimezoneOffset callback | |
return globalOptions.useUTC && globalOptions.getTimezoneOffset; | |
} | |
/** | |
* Set the time methods globally based on the useUTC option. Time method can be | |
* either local time or UTC (default). It is called internally on initiating | |
* Highcharts and after running `Highcharts.setOptions`. | |
* | |
* @private | |
*/ | |
function setTimeMethods() { | |
var globalOptions = H.defaultOptions.global, | |
Date, | |
useUTC = globalOptions.useUTC, | |
GET = useUTC ? 'getUTC' : 'get', | |
SET = useUTC ? 'setUTC' : 'set'; | |
H.Date = Date = globalOptions.Date || win.Date; // Allow using a different Date class | |
Date.hcTimezoneOffset = useUTC && globalOptions.timezoneOffset; | |
Date.hcGetTimezoneOffset = getTimezoneOffsetOption(); | |
Date.hcMakeTime = function(year, month, date, hours, minutes, seconds) { | |
var d; | |
if (useUTC) { | |
d = Date.UTC.apply(0, arguments); | |
d += getTZOffset(d); | |
} else { | |
d = new Date( | |
year, | |
month, | |
pick(date, 1), | |
pick(hours, 0), | |
pick(minutes, 0), | |
pick(seconds, 0) | |
).getTime(); | |
} | |
return d; | |
}; | |
each(['Minutes', 'Hours', 'Day', 'Date', 'Month', 'FullYear'], function(s) { | |
Date['hcGet' + s] = GET + s; | |
}); | |
each(['Milliseconds', 'Seconds', 'Minutes', 'Hours', 'Date', 'Month', 'FullYear'], function(s) { | |
Date['hcSet' + s] = SET + s; | |
}); | |
} | |
/** | |
* Merge the default options with custom options and return the new options | |
* structure. Commonly used for defining reusable templates. | |
* | |
* @function #setOptions | |
* @memberOf Highcharts | |
* @sample highcharts/global/useutc-false Setting a global option | |
* @sample highcharts/members/setoptions Applying a global theme | |
* @param {Object} options The new custom chart options. | |
* @returns {Object} Updated options. | |
*/ | |
H.setOptions = function(options) { | |
// Copy in the default options | |
H.defaultOptions = merge(true, H.defaultOptions, options); | |
// Apply UTC | |
setTimeMethods(); | |
return H.defaultOptions; | |
}; | |
/** | |
* Get the updated default options. Until 3.0.7, merely exposing defaultOptions for outside modules | |
* wasn't enough because the setOptions method created a new object. | |
*/ | |
H.getOptions = function() { | |
return H.defaultOptions; | |
}; | |
// Series defaults | |
H.defaultPlotOptions = H.defaultOptions.plotOptions; | |
// set the default time methods | |
setTimeMethods(); | |
}(Highcharts)); | |
(function(H) { | |
/** | |
* (c) 2010-2017 Torstein Honsi | |
* | |
* License: www.highcharts.com/license | |
*/ | |
var correctFloat = H.correctFloat, | |
defined = H.defined, | |
destroyObjectProperties = H.destroyObjectProperties, | |
isNumber = H.isNumber, | |
merge = H.merge, | |
pick = H.pick, | |
deg2rad = H.deg2rad; | |
/** | |
* The Tick class | |
*/ | |
H.Tick = function(axis, pos, type, noLabel) { | |
this.axis = axis; | |
this.pos = pos; | |
this.type = type || ''; | |
this.isNew = true; | |
this.isNewLabel = true; | |
if (!type && !noLabel) { | |
this.addLabel(); | |
} | |
}; | |
H.Tick.prototype = { | |
/** | |
* Write the tick label | |
*/ | |
addLabel: function() { | |
var tick = this, | |
axis = tick.axis, | |
options = axis.options, | |
chart = axis.chart, | |
categories = axis.categories, | |
names = axis.names, | |
pos = tick.pos, | |
labelOptions = options.labels, | |
str, | |
tickPositions = axis.tickPositions, | |
isFirst = pos === tickPositions[0], | |
isLast = pos === tickPositions[tickPositions.length - 1], | |
value = categories ? | |
pick(categories[pos], names[pos], pos) : | |
pos, | |
label = tick.label, | |
tickPositionInfo = tickPositions.info, | |
dateTimeLabelFormat; | |
// Set the datetime label format. If a higher rank is set for this position, use that. If not, | |
// use the general format. | |
if (axis.isDatetimeAxis && tickPositionInfo) { | |
dateTimeLabelFormat = | |
options.dateTimeLabelFormats[ | |
tickPositionInfo.higherRanks[pos] || tickPositionInfo.unitName | |
]; | |
} | |
// set properties for access in render method | |
tick.isFirst = isFirst; | |
tick.isLast = isLast; | |
// get the string | |
str = axis.labelFormatter.call({ | |
axis: axis, | |
chart: chart, | |
isFirst: isFirst, | |
isLast: isLast, | |
dateTimeLabelFormat: dateTimeLabelFormat, | |
value: axis.isLog ? correctFloat(axis.lin2log(value)) : value | |
}); | |
// prepare CSS | |
//css = width && { width: Math.max(1, Math.round(width - 2 * (labelOptions.padding || 10))) + 'px' }; | |
// first call | |
if (!defined(label)) { | |
tick.label = label = | |
defined(str) && labelOptions.enabled ? | |
chart.renderer.text( | |
str, | |
0, | |
0, | |
labelOptions.useHTML | |
) | |
.add(axis.labelGroup) : | |
null; | |
tick.labelLength = label && label.getBBox().width; // Un-rotated length | |
tick.rotation = 0; // Base value to detect change for new calls to getBBox | |
// update | |
} else if (label) { | |
label.attr({ | |
text: str | |
}); | |
} | |
}, | |
/** | |
* Get the offset height or width of the label | |
*/ | |
getLabelSize: function() { | |
return this.label ? | |
this.label.getBBox()[this.axis.horiz ? 'height' : 'width'] : | |
0; | |
}, | |
/** | |
* Handle the label overflow by adjusting the labels to the left and right edge, or | |
* hide them if they collide into the neighbour label. | |
*/ | |
handleOverflow: function(xy) { | |
var axis = this.axis, | |
pxPos = xy.x, | |
chartWidth = axis.chart.chartWidth, | |
spacing = axis.chart.spacing, | |
leftBound = pick(axis.labelLeft, Math.min(axis.pos, spacing[3])), | |
rightBound = pick(axis.labelRight, Math.max(axis.pos + axis.len, chartWidth - spacing[1])), | |
label = this.label, | |
rotation = this.rotation, | |
factor = { | |
left: 0, | |
center: 0.5, | |
right: 1 | |
}[axis.labelAlign], | |
labelWidth = label.getBBox().width, | |
slotWidth = axis.getSlotWidth(), | |
modifiedSlotWidth = slotWidth, | |
xCorrection = factor, | |
goRight = 1, | |
leftPos, | |
rightPos, | |
textWidth, | |
css = {}; | |
// Check if the label overshoots the chart spacing box. If it does, move it. | |
// If it now overshoots the slotWidth, add ellipsis. | |
if (!rotation) { | |
leftPos = pxPos - factor * labelWidth; | |
rightPos = pxPos + (1 - factor) * labelWidth; | |
if (leftPos < leftBound) { | |
modifiedSlotWidth = xy.x + modifiedSlotWidth * (1 - factor) - leftBound; | |
} else if (rightPos > rightBound) { | |
modifiedSlotWidth = rightBound - xy.x + modifiedSlotWidth * factor; | |
goRight = -1; | |
} | |
modifiedSlotWidth = Math.min(slotWidth, modifiedSlotWidth); // #4177 | |
if (modifiedSlotWidth < slotWidth && axis.labelAlign === 'center') { | |
xy.x += goRight * (slotWidth - modifiedSlotWidth - xCorrection * | |
(slotWidth - Math.min(labelWidth, modifiedSlotWidth))); | |
} | |
// If the label width exceeds the available space, set a text width to be | |
// picked up below. Also, if a width has been set before, we need to set a new | |
// one because the reported labelWidth will be limited by the box (#3938). | |
if (labelWidth > modifiedSlotWidth || (axis.autoRotation && (label.styles || {}).width)) { | |
textWidth = modifiedSlotWidth; | |
} | |
// Add ellipsis to prevent rotated labels to be clipped against the edge of the chart | |
} else if (rotation < 0 && pxPos - factor * labelWidth < leftBound) { | |
textWidth = Math.round(pxPos / Math.cos(rotation * deg2rad) - leftBound); | |
} else if (rotation > 0 && pxPos + factor * labelWidth > rightBound) { | |
textWidth = Math.round((chartWidth - pxPos) / Math.cos(rotation * deg2rad)); | |
} | |
if (textWidth) { | |
css.width = textWidth; | |
if (!(axis.options.labels.style || {}).textOverflow) { | |
css.textOverflow = 'ellipsis'; | |
} | |
label.css(css); | |
} | |
}, | |
/** | |
* Get the x and y position for ticks and labels | |
*/ | |
getPosition: function(horiz, pos, tickmarkOffset, old) { | |
var axis = this.axis, | |
chart = axis.chart, | |
cHeight = (old && chart.oldChartHeight) || chart.chartHeight; | |
return { | |
x: horiz ? | |
axis.translate(pos + tickmarkOffset, null, null, old) + axis.transB : axis.left + axis.offset + | |
(axis.opposite ? | |
((old && chart.oldChartWidth) || chart.chartWidth) - axis.right - axis.left : | |
0 | |
), | |
y: horiz ? | |
cHeight - axis.bottom + axis.offset - (axis.opposite ? axis.height : 0) : cHeight - axis.translate(pos + tickmarkOffset, null, null, old) - axis.transB | |
}; | |
}, | |
/** | |
* Get the x, y position of the tick label | |
*/ | |
getLabelPosition: function(x, y, label, horiz, labelOptions, tickmarkOffset, index, step) { | |
var axis = this.axis, | |
transA = axis.transA, | |
reversed = axis.reversed, | |
staggerLines = axis.staggerLines, | |
rotCorr = axis.tickRotCorr || { | |
x: 0, | |
y: 0 | |
}, | |
yOffset = labelOptions.y, | |
line; | |
if (!defined(yOffset)) { | |
if (axis.side === 0) { | |
yOffset = label.rotation ? -8 : -label.getBBox().height; | |
} else if (axis.side === 2) { | |
yOffset = rotCorr.y + 8; | |
} else { | |
// #3140, #3140 | |
yOffset = Math.cos(label.rotation * deg2rad) * (rotCorr.y - label.getBBox(false, 0).height / 2); | |
} | |
} | |
x = x + labelOptions.x + rotCorr.x - (tickmarkOffset && horiz ? | |
tickmarkOffset * transA * (reversed ? -1 : 1) : 0); | |
y = y + yOffset - (tickmarkOffset && !horiz ? | |
tickmarkOffset * transA * (reversed ? 1 : -1) : 0); | |
// Correct for staggered labels | |
if (staggerLines) { | |
line = (index / (step || 1) % staggerLines); | |
if (axis.opposite) { | |
line = staggerLines - line - 1; | |
} | |
y += line * (axis.labelOffset / staggerLines); | |
} | |
return { | |
x: x, | |
y: Math.round(y) | |
}; | |
}, | |
/** | |
* Extendible method to return the path of the marker | |
*/ | |
getMarkPath: function(x, y, tickLength, tickWidth, horiz, renderer) { | |
return renderer.crispLine([ | |
'M', | |
x, | |
y, | |
'L', | |
x + (horiz ? 0 : -tickLength), | |
y + (horiz ? tickLength : 0) | |
], tickWidth); | |
}, | |
/** | |
* Renders the gridLine. | |
* @param {Boolean} old Whether or not the tick is old | |
* @param {number} opacity The opacity of the grid line | |
* @param {number} reverseCrisp Modifier for avoiding overlapping 1 or -1 | |
* @return {undefined} | |
*/ | |
renderGridLine: function(old, opacity, reverseCrisp) { | |
var tick = this, | |
axis = tick.axis, | |
options = axis.options, | |
gridLine = tick.gridLine, | |
gridLinePath, | |
attribs = {}, | |
pos = tick.pos, | |
type = tick.type, | |
tickmarkOffset = axis.tickmarkOffset, | |
renderer = axis.chart.renderer; | |
if (!gridLine) { | |
if (!type) { | |
attribs.zIndex = 1; | |
} | |
if (old) { | |
attribs.opacity = 0; | |
} | |
tick.gridLine = gridLine = renderer.path() | |
.attr(attribs) | |
.addClass( | |
'highcharts-' + (type ? type + '-' : '') + 'grid-line' | |
) | |
.add(axis.gridGroup); | |
} | |
// If the parameter 'old' is set, the current call will be followed | |
// by another call, therefore do not do any animations this time | |
if (!old && gridLine) { | |
gridLinePath = axis.getPlotLinePath( | |
pos + tickmarkOffset, | |
gridLine.strokeWidth() * reverseCrisp, | |
old, true | |
); | |
if (gridLinePath) { | |
gridLine[tick.isNew ? 'attr' : 'animate']({ | |
d: gridLinePath, | |
opacity: opacity | |
}); | |
} | |
} | |
}, | |
/** | |
* Renders the tick mark. | |
* @param {Object} xy The position vector of the mark | |
* @param {number} xy.x The x position of the mark | |
* @param {number} xy.y The y position of the mark | |
* @param {number} opacity The opacity of the mark | |
* @param {number} reverseCrisp Modifier for avoiding overlapping 1 or -1 | |
* @return {undefined} | |
*/ | |
renderMark: function(xy, opacity, reverseCrisp) { | |
var tick = this, | |
axis = tick.axis, | |
options = axis.options, | |
renderer = axis.chart.renderer, | |
type = tick.type, | |
tickPrefix = type ? type + 'Tick' : 'tick', | |
tickSize = axis.tickSize(tickPrefix), | |
mark = tick.mark, | |
isNewMark = !mark, | |
x = xy.x, | |
y = xy.y; | |
if (tickSize) { | |
// negate the length | |
if (axis.opposite) { | |
tickSize[0] = -tickSize[0]; | |
} | |
// First time, create it | |
if (isNewMark) { | |
tick.mark = mark = renderer.path() | |
.addClass('highcharts-' + (type ? type + '-' : '') + 'tick') | |
.add(axis.axisGroup); | |
} | |
mark[isNewMark ? 'attr' : 'animate']({ | |
d: tick.getMarkPath( | |
x, | |
y, | |
tickSize[0], | |
mark.strokeWidth() * reverseCrisp, | |
axis.horiz, | |
renderer), | |
opacity: opacity | |
}); | |
} | |
}, | |
/** | |
* Renders the tick label. | |
* Note: The label should already be created in init(), so it should only | |
* have to be moved into place. | |
* @param {Object} xy The position vector of the label | |
* @param {number} xy.x The x position of the label | |
* @param {number} xy.y The y position of the label | |
* @param {Boolean} old Whether or not the tick is old | |
* @param {number} opacity The opacity of the label | |
* @param {number} index The index of the tick | |
* @return {undefined} | |
*/ | |
renderLabel: function(xy, old, opacity, index) { | |
var tick = this, | |
axis = tick.axis, | |
horiz = axis.horiz, | |
options = axis.options, | |
label = tick.label, | |
labelOptions = options.labels, | |
step = labelOptions.step, | |
tickmarkOffset = axis.tickmarkOffset, | |
show = true, | |
x = xy.x, | |
y = xy.y; | |
if (label && isNumber(x)) { | |
label.xy = xy = tick.getLabelPosition( | |
x, | |
y, | |
label, | |
horiz, | |
labelOptions, | |
tickmarkOffset, | |
index, | |
step | |
); | |
// Apply show first and show last. If the tick is both first and | |
// last, it is a single centered tick, in which case we show the | |
// label anyway (#2100). | |
if ( | |
( | |
tick.isFirst && | |
!tick.isLast && | |
!pick(options.showFirstLabel, 1) | |
) || | |
( | |
tick.isLast && | |
!tick.isFirst && | |
!pick(options.showLastLabel, 1) | |
) | |
) { | |
show = false; | |
// Handle label overflow and show or hide accordingly | |
} else if (horiz && !axis.isRadial && !labelOptions.step && | |
!labelOptions.rotation && !old && opacity !== 0) { | |
tick.handleOverflow(xy); | |
} | |
// apply step | |
if (step && index % step) { | |
// show those indices dividable by step | |
show = false; | |
} | |
// Set the new position, and show or hide | |
if (show && isNumber(xy.y)) { | |
xy.opacity = opacity; | |
label[tick.isNewLabel ? 'attr' : 'animate'](xy); | |
tick.isNewLabel = false; | |
} else { | |
label.attr('y', -9999); // #1338 | |
tick.isNewLabel = true; | |
} | |
tick.isNew = false; | |
} | |
}, | |
/** | |
* Put everything in place | |
* | |
* @param index {Number} | |
* @param old {Boolean} Use old coordinates to prepare an animation into new | |
* position | |
*/ | |
render: function(index, old, opacity) { | |
var tick = this, | |
axis = tick.axis, | |
horiz = axis.horiz, | |
pos = tick.pos, | |
tickmarkOffset = axis.tickmarkOffset, | |
xy = tick.getPosition(horiz, pos, tickmarkOffset, old), | |
x = xy.x, | |
y = xy.y, | |
reverseCrisp = ((horiz && x === axis.pos + axis.len) || | |
(!horiz && y === axis.pos)) ? -1 : 1; // #1480, #1687 | |
opacity = pick(opacity, 1); | |
this.isActive = true; | |
// Create the grid line | |
this.renderGridLine(old, opacity, reverseCrisp); | |
// create the tick mark | |
this.renderMark(xy, opacity, reverseCrisp); | |
// the label is created on init - now move it into place | |
this.renderLabel(xy, old, opacity, index); | |
}, | |
/** | |
* Destructor for the tick prototype | |
*/ | |
destroy: function() { | |
destroyObjectProperties(this, this.axis); | |
} | |
}; | |
}(Highcharts)); | |
var Axis = (function(H) { | |
/** | |
* (c) 2010-2017 Torstein Honsi | |
* | |
* License: www.highcharts.com/license | |
*/ | |
var addEvent = H.addEvent, | |
animObject = H.animObject, | |
arrayMax = H.arrayMax, | |
arrayMin = H.arrayMin, | |
color = H.color, | |
correctFloat = H.correctFloat, | |
defaultOptions = H.defaultOptions, | |
defined = H.defined, | |
deg2rad = H.deg2rad, | |
destroyObjectProperties = H.destroyObjectProperties, | |
each = H.each, | |
extend = H.extend, | |
fireEvent = H.fireEvent, | |
format = H.format, | |
getMagnitude = H.getMagnitude, | |
grep = H.grep, | |
inArray = H.inArray, | |
isArray = H.isArray, | |
isNumber = H.isNumber, | |
isString = H.isString, | |
merge = H.merge, | |
normalizeTickInterval = H.normalizeTickInterval, | |
objectEach = H.objectEach, | |
pick = H.pick, | |
removeEvent = H.removeEvent, | |
splat = H.splat, | |
syncTimeout = H.syncTimeout, | |
Tick = H.Tick; | |
/** | |
* Create a new axis object. Called internally when instanciating a new chart or | |
* adding axes by {@link Highcharts.Chart#addAxis}. | |
* | |
* A chart can have from 0 axes (pie chart) to multiples. In a normal, single | |
* series cartesian chart, there is one X axis and one Y axis. | |
* | |
* The X axis or axes are referenced by {@link Highcharts.Chart.xAxis}, which is | |
* an array of Axis objects. If there is only one axis, it can be referenced | |
* through `chart.xAxis[0]`, and multiple axes have increasing indices. The same | |
* pattern goes for Y axes. | |
* | |
* If you need to get the axes from a series object, use the `series.xAxis` and | |
* `series.yAxis` properties. These are not arrays, as one series can only be | |
* associated to one X and one Y axis. | |
* | |
* A third way to reference the axis programmatically is by `id`. Add an `id` in | |
* the axis configuration options, and get the axis by | |
* {@link Highcharts.Chart#get}. | |
* | |
* Configuration options for the axes are given in options.xAxis and | |
* options.yAxis. | |
* | |
* @class Highcharts.Axis | |
* @memberOf Highcharts | |
* @param {Highcharts.Chart} chart - The Chart instance to apply the axis on. | |
* @param {Object} options - Axis options | |
*/ | |
var Axis = function() { | |
this.init.apply(this, arguments); | |
}; | |
H.extend(Axis.prototype, /** @lends Highcharts.Axis.prototype */ { | |
/** | |
* Default options for the X axis - the Y axis has extended defaults. | |
* | |
* @private | |
* @type {Object} | |
*/ | |
defaultOptions: { | |
// allowDecimals: null, | |
// alternateGridColor: null, | |
// categories: [], | |
dateTimeLabelFormats: { | |
millisecond: '%H:%M:%S.%L', | |
second: '%H:%M:%S', | |
minute: '%H:%M', | |
hour: '%H:%M', | |
day: '%e. %b', | |
week: '%e. %b', | |
month: '%b \'%y', | |
year: '%Y' | |
}, | |
endOnTick: false, | |
// reversed: false, | |
labels: { | |
enabled: true, | |
// rotation: 0, | |
// align: 'center', | |
// step: null, | |
x: 0 | |
//y: undefined | |
/*formatter: function () { | |
return this.value; | |
},*/ | |
}, | |
//linkedTo: null, | |
//max: undefined, | |
//min: undefined, | |
minPadding: 0.01, | |
maxPadding: 0.01, | |
//minRange: null, | |
//minorTickInterval: null, | |
minorTickLength: 2, | |
minorTickPosition: 'outside', // inside or outside | |
//opposite: false, | |
//offset: 0, | |
//plotBands: [{ | |
// events: {}, | |
// zIndex: 1, | |
// labels: { align, x, verticalAlign, y, style, rotation, textAlign } | |
//}], | |
//plotLines: [{ | |
// events: {} | |
// dashStyle: {} | |
// zIndex: | |
// labels: { align, x, verticalAlign, y, style, rotation, textAlign } | |
//}], | |
//reversed: false, | |
// showFirstLabel: true, | |
// showLastLabel: true, | |
startOfWeek: 1, | |
startOnTick: false, | |
//tickInterval: null, | |
tickLength: 10, | |
tickmarkPlacement: 'between', // on or between | |
tickPixelInterval: 100, | |
tickPosition: 'outside', | |
title: { | |
//text: null, | |
align: 'middle', // low, middle or high | |
//margin: 0 for horizontal, 10 for vertical axes, | |
// reserveSpace: true, | |
//rotation: 0, | |
//side: 'outside', | |
//x: 0, | |
//y: 0 | |
}, | |
type: 'linear', // linear, logarithmic or datetime | |
//visible: true | |
}, | |
/** | |
* This options set extends the defaultOptions for Y axes. | |
* | |
* @private | |
* @type {Object} | |
*/ | |
defaultYAxisOptions: { | |
endOnTick: true, | |
tickPixelInterval: 72, | |
showLastLabel: true, | |
labels: { | |
x: -8 | |
}, | |
maxPadding: 0.05, | |
minPadding: 0.05, | |
startOnTick: true, | |
title: { | |
rotation: 270, | |
text: 'Values' | |
}, | |
stackLabels: { | |
enabled: false, | |
//align: dynamic, | |
//y: dynamic, | |
//x: dynamic, | |
//verticalAlign: dynamic, | |
//textAlign: dynamic, | |
//rotation: 0, | |
formatter: function() { | |
return H.numberFormat(this.total, -1); | |
} | |
} | |
}, | |
/** | |
* These options extend the defaultOptions for left axes. | |
* | |
* @private | |
* @type {Object} | |
*/ | |
defaultLeftAxisOptions: { | |
labels: { | |
x: -15 | |
}, | |
title: { | |
rotation: 270 | |
} | |
}, | |
/** | |
* These options extend the defaultOptions for right axes. | |
* | |
* @private | |
* @type {Object} | |
*/ | |
defaultRightAxisOptions: { | |
labels: { | |
x: 15 | |
}, | |
title: { | |
rotation: 90 | |
} | |
}, | |
/** | |
* These options extend the defaultOptions for bottom axes. | |
* | |
* @private | |
* @type {Object} | |
*/ | |
defaultBottomAxisOptions: { | |
labels: { | |
autoRotation: [-45], | |
x: 0 | |
// overflow: undefined, | |
// staggerLines: null | |
}, | |
title: { | |
rotation: 0 | |
} | |
}, | |
/** | |
* These options extend the defaultOptions for top axes. | |
* | |
* @private | |
* @type {Object} | |
*/ | |
defaultTopAxisOptions: { | |
labels: { | |
autoRotation: [-45], | |
x: 0 | |
// overflow: undefined | |
// staggerLines: null | |
}, | |
title: { | |
rotation: 0 | |
} | |
}, | |
/** | |
* Overrideable function to initialize the axis. | |
* | |
* @see {@link Axis} | |
*/ | |
init: function(chart, userOptions) { | |
var isXAxis = userOptions.isX, | |
axis = this; | |
/** | |
* The Chart that the axis belongs to. | |
* | |
* @name chart | |
* @memberOf Axis | |
* @type {Chart} | |
*/ | |
axis.chart = chart; | |
/** | |
* Whether the axis is horizontal. | |
* | |
* @name horiz | |
* @memberOf Axis | |
* @type {Boolean} | |
*/ | |
axis.horiz = chart.inverted && !axis.isZAxis ? !isXAxis : isXAxis; | |
// Flag, isXAxis | |
axis.isXAxis = isXAxis; | |
/** | |
* The collection where the axis belongs, for example `xAxis`, `yAxis` | |
* or `colorAxis`. Corresponds to properties on Chart, for example | |
* {@link Chart.xAxis}. | |
* | |
* @name coll | |
* @memberOf Axis | |
* @type {String} | |
*/ | |
axis.coll = axis.coll || (isXAxis ? 'xAxis' : 'yAxis'); | |
axis.opposite = userOptions.opposite; // needed in setOptions | |
/** | |
* The side on which the axis is rendered. 0 is top, 1 is right, 2 is | |
* bottom and 3 is left. | |
* | |
* @name side | |
* @memberOf Axis | |
* @type {Number} | |
*/ | |
axis.side = userOptions.side || (axis.horiz ? | |
(axis.opposite ? 0 : 2) : // top : bottom | |
(axis.opposite ? 1 : 3)); // right : left | |
axis.setOptions(userOptions); | |
var options = this.options, | |
type = options.type, | |
isDatetimeAxis = type === 'datetime'; | |
axis.labelFormatter = options.labels.formatter || | |
axis.defaultLabelFormatter; // can be overwritten by dynamic format | |
// Flag, stagger lines or not | |
axis.userOptions = userOptions; | |
//axis.axisTitleMargin = undefined,// = options.title.margin, | |
axis.minPixelPadding = 0; | |
/** | |
* Whether the axis is reversed. Based on the `axis.reversed`, | |
* option, but inverted charts have reversed xAxis by default. | |
* | |
* @name reversed | |
* @memberOf Axis | |
* @type {Boolean} | |
*/ | |
axis.reversed = options.reversed; | |
axis.visible = options.visible !== false; | |
axis.zoomEnabled = options.zoomEnabled !== false; | |
// Initial categories | |
axis.hasNames = type === 'category' || options.categories === true; | |
axis.categories = options.categories || axis.hasNames; | |
axis.names = axis.names || []; // Preserve on update (#3830) | |
// Elements | |
//axis.axisGroup = undefined; | |
//axis.gridGroup = undefined; | |
//axis.axisTitle = undefined; | |
//axis.axisLine = undefined; | |
// Placeholder for plotlines and plotbands groups | |
axis.plotLinesAndBandsGroups = {}; | |
// Shorthand types | |
axis.isLog = type === 'logarithmic'; | |
axis.isDatetimeAxis = isDatetimeAxis; | |
axis.positiveValuesOnly = axis.isLog && !axis.allowNegativeLog; | |
// Flag, if axis is linked to another axis | |
axis.isLinked = defined(options.linkedTo); | |
// Linked axis. | |
//axis.linkedParent = undefined; | |
// Major ticks | |
axis.ticks = {}; | |
axis.labelEdge = []; | |
// Minor ticks | |
axis.minorTicks = {}; | |
// List of plotLines/Bands | |
axis.plotLinesAndBands = []; | |
// Alternate bands | |
axis.alternateBands = {}; | |
// Axis metrics | |
//axis.left = undefined; | |
//axis.top = undefined; | |
//axis.width = undefined; | |
//axis.height = undefined; | |
//axis.bottom = undefined; | |
//axis.right = undefined; | |
//axis.transA = undefined; | |
//axis.transB = undefined; | |
//axis.oldTransA = undefined; | |
axis.len = 0; | |
//axis.oldMin = undefined; | |
//axis.oldMax = undefined; | |
//axis.oldUserMin = undefined; | |
//axis.oldUserMax = undefined; | |
//axis.oldAxisLength = undefined; | |
axis.minRange = axis.userMinRange = options.minRange || options.maxZoom; | |
axis.range = options.range; | |
axis.offset = options.offset || 0; | |
// Dictionary for stacks | |
axis.stacks = {}; | |
axis.oldStacks = {}; | |
axis.stacksTouched = 0; | |
// Min and max in the data | |
//axis.dataMin = undefined, | |
//axis.dataMax = undefined, | |
/** | |
* The maximum value of the axis. In a logarithmic axis, this is the | |
* logarithm of the real value, and the real value can be obtained from | |
* {@link Axis#getExtremes}. | |
* | |
* @name max | |
* @memberOf Axis | |
* @type {Number} | |
*/ | |
axis.max = null; | |
/** | |
* The minimum value of the axis. In a logarithmic axis, this is the | |
* logarithm of the real value, and the real value can be obtained from | |
* {@link Axis#getExtremes}. | |
* | |
* @name min | |
* @memberOf Axis | |
* @type {Number} | |
*/ | |
axis.min = null; | |
// User set min and max | |
//axis.userMin = undefined, | |
//axis.userMax = undefined, | |
/** | |
* The processed crosshair options. | |
* | |
* @name crosshair | |
* @memberOf Axis | |
* @type {AxisCrosshairOptions} | |
*/ | |
axis.crosshair = pick( | |
options.crosshair, | |
splat(chart.options.tooltip.crosshairs)[isXAxis ? 0 : 1], | |
false | |
); | |
var events = axis.options.events; | |
// Register. Don't add it again on Axis.update(). | |
if (inArray(axis, chart.axes) === -1) { // | |
if (isXAxis) { // #2713 | |
chart.axes.splice(chart.xAxis.length, 0, axis); | |
} else { | |
chart.axes.push(axis); | |
} | |
chart[axis.coll].push(axis); | |
} | |
/** | |
* All series associated to the axis. | |
* | |
* @name series | |
* @memberOf Axis | |
* @type {Array.<Series>} | |
*/ | |
axis.series = axis.series || []; // populated by Series | |
// Reversed axis | |
if ( | |
chart.inverted && | |
!axis.isZAxis && | |
isXAxis && | |
axis.reversed === undefined | |
) { | |
axis.reversed = true; | |
} | |
// register event listeners | |
objectEach(events, function(event, eventType) { | |
addEvent(axis, eventType, event); | |
}); | |
// extend logarithmic axis | |
axis.lin2log = options.linearToLogConverter || axis.lin2log; | |
if (axis.isLog) { | |
axis.val2lin = axis.log2lin; | |
axis.lin2val = axis.lin2log; | |
} | |
}, | |
/** | |
* Merge and set options. | |
* | |
* @private | |
*/ | |
setOptions: function(userOptions) { | |
this.options = merge( | |
this.defaultOptions, | |
this.coll === 'yAxis' && this.defaultYAxisOptions, [ | |
this.defaultTopAxisOptions, | |
this.defaultRightAxisOptions, | |
this.defaultBottomAxisOptions, | |
this.defaultLeftAxisOptions | |
][this.side], | |
merge( | |
defaultOptions[this.coll], // if set in setOptions (#1053) | |
userOptions | |
) | |
); | |
}, | |
/** | |
* The default label formatter. The context is a special config object for | |
* the label. In apps, use the {@link | |
* https://api.highcharts.com/highcharts/xAxis.labels.formatter| | |
* labels.formatter} instead except when a modification is needed. | |
* | |
* @private | |
*/ | |
defaultLabelFormatter: function() { | |
var axis = this.axis, | |
value = this.value, | |
categories = axis.categories, | |
dateTimeLabelFormat = this.dateTimeLabelFormat, | |
lang = defaultOptions.lang, | |
numericSymbols = lang.numericSymbols, | |
numSymMagnitude = lang.numericSymbolMagnitude || 1000, | |
i = numericSymbols && numericSymbols.length, | |
multi, | |
ret, | |
formatOption = axis.options.labels.format, | |
// make sure the same symbol is added for all labels on a linear | |
// axis | |
numericSymbolDetector = axis.isLog ? | |
Math.abs(value) : | |
axis.tickInterval; | |
if (formatOption) { | |
ret = format(formatOption, this); | |
} else if (categories) { | |
ret = value; | |
} else if (dateTimeLabelFormat) { // datetime axis | |
ret = H.dateFormat(dateTimeLabelFormat, value); | |
} else if (i && numericSymbolDetector >= 1000) { | |
// Decide whether we should add a numeric symbol like k (thousands) | |
// or M (millions). If we are to enable this in tooltip or other | |
// places as well, we can move this logic to the numberFormatter and | |
// enable it by a parameter. | |
while (i-- && ret === undefined) { | |
multi = Math.pow(numSymMagnitude, i + 1); | |
if ( | |
numericSymbolDetector >= multi && | |
(value * 10) % multi === 0 && | |
numericSymbols[i] !== null && | |
value !== 0 | |
) { // #5480 | |
ret = H.numberFormat(value / multi, -1) + numericSymbols[i]; | |
} | |
} | |
} | |
if (ret === undefined) { | |
if (Math.abs(value) >= 10000) { // add thousands separators | |
ret = H.numberFormat(value, -1); | |
} else { // small numbers | |
ret = H.numberFormat(value, -1, undefined, ''); // #2466 | |
} | |
} | |
return ret; | |
}, | |
/** | |
* Get the minimum and maximum for the series of each axis. The function | |
* analyzes the axis series and updates `this.dataMin` and `this.dataMax`. | |
* | |
* @private | |
*/ | |
getSeriesExtremes: function() { | |
var axis = this, | |
chart = axis.chart; | |
axis.hasVisibleSeries = false; | |
// Reset properties in case we're redrawing (#3353) | |
axis.dataMin = axis.dataMax = axis.threshold = null; | |
axis.softThreshold = !axis.isXAxis; | |
if (axis.buildStacks) { | |
axis.buildStacks(); | |
} | |
// loop through this axis' series | |
each(axis.series, function(series) { | |
if (series.visible || !chart.options.chart.ignoreHiddenSeries) { | |
var seriesOptions = series.options, | |
xData, | |
threshold = seriesOptions.threshold, | |
seriesDataMin, | |
seriesDataMax; | |
axis.hasVisibleSeries = true; | |
// Validate threshold in logarithmic axes | |
if (axis.positiveValuesOnly && threshold <= 0) { | |
threshold = null; | |
} | |
// Get dataMin and dataMax for X axes | |
if (axis.isXAxis) { | |
xData = series.xData; | |
if (xData.length) { | |
// If xData contains values which is not numbers, then | |
// filter them out. To prevent performance hit, we only | |
// do this after we have already found seriesDataMin | |
// because in most cases all data is valid. #5234. | |
seriesDataMin = arrayMin(xData); | |
if (!isNumber(seriesDataMin) && | |
!(seriesDataMin instanceof Date) // #5010 | |
) { | |
xData = grep(xData, function(x) { | |
return isNumber(x); | |
}); | |
seriesDataMin = arrayMin(xData); // Do it again with valid data | |
} | |
axis.dataMin = Math.min( | |
pick(axis.dataMin, xData[0]), | |
seriesDataMin | |
); | |
axis.dataMax = Math.max( | |
pick(axis.dataMax, xData[0]), | |
arrayMax(xData) | |
); | |
} | |
// Get dataMin and dataMax for Y axes, as well as handle | |
// stacking and processed data | |
} else { | |
// Get this particular series extremes | |
series.getExtremes(); | |
seriesDataMax = series.dataMax; | |
seriesDataMin = series.dataMin; | |
// Get the dataMin and dataMax so far. If percentage is | |
// used, the min and max are always 0 and 100. If | |
// seriesDataMin and seriesDataMax is null, then series | |
// doesn't have active y data, we continue with nulls | |
if (defined(seriesDataMin) && defined(seriesDataMax)) { | |
axis.dataMin = Math.min( | |
pick(axis.dataMin, seriesDataMin), | |
seriesDataMin | |
); | |
axis.dataMax = Math.max( | |
pick(axis.dataMax, seriesDataMax), | |
seriesDataMax | |
); | |
} | |
// Adjust to threshold | |
if (defined(threshold)) { | |
axis.threshold = threshold; | |
} | |
// If any series has a hard threshold, it takes precedence | |
if (!seriesOptions.softThreshold || | |
axis.positiveValuesOnly | |
) { | |
axis.softThreshold = false; | |
} | |
} | |
} | |
}); | |
}, | |
/** | |
* Translate from axis value to pixel position on the chart, or back. Use | |
* the `toPixels` and `toValue` functions in applications. | |
* | |
* @private | |
*/ | |
translate: function(val, backwards, cvsCoord, old, handleLog, pointPlacement) { | |
var axis = this.linkedParent || this, // #1417 | |
sign = 1, | |
cvsOffset = 0, | |
localA = old ? axis.oldTransA : axis.transA, | |
localMin = old ? axis.oldMin : axis.min, | |
returnValue, | |
minPixelPadding = axis.minPixelPadding, | |
doPostTranslate = (axis.isOrdinal || axis.isBroken || (axis.isLog && handleLog)) && axis.lin2val; | |
if (!localA) { | |
localA = axis.transA; | |
} | |
// In vertical axes, the canvas coordinates start from 0 at the top like in | |
// SVG. | |
if (cvsCoord) { | |
sign *= -1; // canvas coordinates inverts the value | |
cvsOffset = axis.len; | |
} | |
// Handle reversed axis | |
if (axis.reversed) { | |
sign *= -1; | |
cvsOffset -= sign * (axis.sector || axis.len); | |
} | |
// From pixels to value | |
if (backwards) { // reverse translation | |
val = val * sign + cvsOffset; | |
val -= minPixelPadding; | |
returnValue = val / localA + localMin; // from chart pixel to value | |
if (doPostTranslate) { // log and ordinal axes | |
returnValue = axis.lin2val(returnValue); | |
} | |
// From value to pixels | |
} else { | |
if (doPostTranslate) { // log and ordinal axes | |
val = axis.val2lin(val); | |
} | |
returnValue = sign * (val - localMin) * localA + cvsOffset + | |
(sign * minPixelPadding) + | |
(isNumber(pointPlacement) ? localA * pointPlacement : 0); | |
} | |
return returnValue; | |
}, | |
/** | |
* Translate a value in terms of axis units into pixels within the chart. | |
* | |
* @param {Number} value | |
* A value in terms of axis units. | |
* @param {Boolean} paneCoordinates | |
* Whether to return the pixel coordinate relative to the chart or | |
* just the axis/pane itself. | |
* @return {Number} Pixel position of the value on the chart or axis. | |
*/ | |
toPixels: function(value, paneCoordinates) { | |
return this.translate(value, false, !this.horiz, null, true) + | |
(paneCoordinates ? 0 : this.pos); | |
}, | |
/** | |
* Translate a pixel position along the axis to a value in terms of axis | |
* units. | |
* @param {Number} pixel | |
* The pixel value coordinate. | |
* @param {Boolean} paneCoordiantes | |
* Whether the input pixel is relative to the chart or just the | |
* axis/pane itself. | |
* @return {Number} The axis value. | |
*/ | |
toValue: function(pixel, paneCoordinates) { | |
return this.translate( | |
pixel - (paneCoordinates ? 0 : this.pos), | |
true, !this.horiz, | |
null, | |
true | |
); | |
}, | |
/** | |
* Create the path for a plot line that goes from the given value on | |
* this axis, across the plot to the opposite side. Also used internally for | |
* grid lines and crosshairs. | |
* | |
* @param {Number} value | |
* Axis value. | |
* @param {Number} [lineWidth=1] | |
* Used for calculation crisp line coordinates. | |
* @param {Boolean} [old=false] | |
* Use old coordinates (for resizing and rescaling). | |
* @param {Boolean} [force=false] | |
* If `false`, the function will return null when it falls outside | |
* the axis bounds. | |
* @param {Number} [translatedValue] | |
* If given, return the plot line path of a pixel position on the | |
* axis. | |
* | |
* @return {Array.<String|Number>} | |
* The SVG path definition for the plot line. | |
*/ | |
getPlotLinePath: function(value, lineWidth, old, force, translatedValue) { | |
var axis = this, | |
chart = axis.chart, | |
axisLeft = axis.left, | |
axisTop = axis.top, | |
x1, | |
y1, | |
x2, | |
y2, | |
cHeight = (old && chart.oldChartHeight) || chart.chartHeight, | |
cWidth = (old && chart.oldChartWidth) || chart.chartWidth, | |
skip, | |
transB = axis.transB, | |
/** | |
* Check if x is between a and b. If not, either move to a/b or skip, | |
* depending on the force parameter. | |
*/ | |
between = function(x, a, b) { | |
if (x < a || x > b) { | |
if (force) { | |
x = Math.min(Math.max(a, x), b); | |
} else { | |
skip = true; | |
} | |
} | |
return x; | |
}; | |
translatedValue = pick(translatedValue, axis.translate(value, null, null, old)); | |
x1 = x2 = Math.round(translatedValue + transB); | |
y1 = y2 = Math.round(cHeight - translatedValue - transB); | |
if (!isNumber(translatedValue)) { // no min or max | |
skip = true; | |
} else if (axis.horiz) { | |
y1 = axisTop; | |
y2 = cHeight - axis.bottom; | |
x1 = x2 = between(x1, axisLeft, axisLeft + axis.width); | |
} else { | |
x1 = axisLeft; | |
x2 = cWidth - axis.right; | |
y1 = y2 = between(y1, axisTop, axisTop + axis.height); | |
} | |
return skip && !force ? | |
null : | |
chart.renderer.crispLine(['M', x1, y1, 'L', x2, y2], lineWidth || 1); | |
}, | |
/** | |
* Internal function to et the tick positions of a linear axis to round | |
* values like whole tens or every five. | |
* | |
* @param {Number} tickInterval | |
* The normalized tick interval | |
* @param {Number} min | |
* Axis minimum. | |
* @param {Number} max | |
* Axis maximum. | |
* | |
* @return {Array.<Number>} | |
* An array of axis values where ticks should be placed. | |
*/ | |
getLinearTickPositions: function(tickInterval, min, max) { | |
var pos, | |
lastPos, | |
roundedMin = correctFloat(Math.floor(min / tickInterval) * tickInterval), | |
roundedMax = correctFloat(Math.ceil(max / tickInterval) * tickInterval), | |
tickPositions = []; | |
// For single points, add a tick regardless of the relative position | |
// (#2662, #6274) | |
if (this.single) { | |
return [min]; | |
} | |
// Populate the intermediate values | |
pos = roundedMin; | |
while (pos <= roundedMax) { | |
// Place the tick on the rounded value | |
tickPositions.push(pos); | |
// Always add the raw tickInterval, not the corrected one. | |
pos = correctFloat(pos + tickInterval); | |
// If the interval is not big enough in the current min - max range to actually increase | |
// the loop variable, we need to break out to prevent endless loop. Issue #619 | |
if (pos === lastPos) { | |
break; | |
} | |
// Record the last value | |
lastPos = pos; | |
} | |
return tickPositions; | |
}, | |
/** | |
* Internal function to return the minor tick positions. For logarithmic | |
* axes, the same logic as for major ticks is reused. | |
* | |
* @return {Array.<Number>} | |
* An array of axis values where ticks should be placed. | |
*/ | |
getMinorTickPositions: function() { | |
var axis = this, | |
options = axis.options, | |
tickPositions = axis.tickPositions, | |
minorTickInterval = axis.minorTickInterval, | |
minorTickPositions = [], | |
pos, | |
pointRangePadding = axis.pointRangePadding || 0, | |
min = axis.min - pointRangePadding, // #1498 | |
max = axis.max + pointRangePadding, // #1498 | |
range = max - min; | |
// If minor ticks get too dense, they are hard to read, and may cause long running script. So we don't draw them. | |
if (range && range / minorTickInterval < axis.len / 3) { // #3875 | |
if (axis.isLog) { | |
// For each interval in the major ticks, compute the minor ticks | |
// separately. | |
each(this.paddedTicks, function(pos, i, paddedTicks) { | |
if (i) { | |
minorTickPositions.push.apply( | |
minorTickPositions, | |
axis.getLogTickPositions( | |
minorTickInterval, | |
paddedTicks[i - 1], | |
paddedTicks[i], | |
true | |
) | |
); | |
} | |
}); | |
} else if (axis.isDatetimeAxis && options.minorTickInterval === 'auto') { // #1314 | |
minorTickPositions = minorTickPositions.concat( | |
axis.getTimeTicks( | |
axis.normalizeTimeTickInterval(minorTickInterval), | |
min, | |
max, | |
options.startOfWeek | |
) | |
); | |
} else { | |
for ( | |
pos = min + (tickPositions[0] - min) % minorTickInterval; pos <= max; pos += minorTickInterval | |
) { | |
// Very, very, tight grid lines (#5771) | |
if (pos === minorTickPositions[0]) { | |
break; | |
} | |
minorTickPositions.push(pos); | |
} | |
} | |
} | |
if (minorTickPositions.length !== 0) { | |
axis.trimTicks(minorTickPositions); // #3652 #3743 #1498 #6330 | |
} | |
return minorTickPositions; | |
}, | |
/** | |
* Adjust the min and max for the minimum range. Keep in mind that the series data is | |
* not yet processed, so we don't have information on data cropping and grouping, or | |
* updated axis.pointRange or series.pointRange. The data can't be processed until | |
* we have finally established min and max. | |
* | |
* @private | |
*/ | |
adjustForMinRange: function() { | |
var axis = this, | |
options = axis.options, | |
min = axis.min, | |
max = axis.max, | |
zoomOffset, | |
spaceAvailable, | |
closestDataRange, | |
i, | |
distance, | |
xData, | |
loopLength, | |
minArgs, | |
maxArgs, | |
minRange; | |
// Set the automatic minimum range based on the closest point distance | |
if (axis.isXAxis && axis.minRange === undefined && !axis.isLog) { | |
if (defined(options.min) || defined(options.max)) { | |
axis.minRange = null; // don't do this again | |
} else { | |
// Find the closest distance between raw data points, as opposed to | |
// closestPointRange that applies to processed points (cropped and grouped) | |
each(axis.series, function(series) { | |
xData = series.xData; | |
loopLength = series.xIncrement ? 1 : xData.length - 1; | |
for (i = loopLength; i > 0; i--) { | |
distance = xData[i] - xData[i - 1]; | |
if (closestDataRange === undefined || distance < closestDataRange) { | |
closestDataRange = distance; | |
} | |
} | |
}); | |
axis.minRange = Math.min(closestDataRange * 5, axis.dataMax - axis.dataMin); | |
} | |
} | |
// if minRange is exceeded, adjust | |
if (max - min < axis.minRange) { | |
spaceAvailable = axis.dataMax - axis.dataMin >= axis.minRange; | |
minRange = axis.minRange; | |
zoomOffset = (minRange - max + min) / 2; | |
// if min and max options have been set, don't go beyond it | |
minArgs = [min - zoomOffset, pick(options.min, min - zoomOffset)]; | |
if (spaceAvailable) { // if space is available, stay within the data range | |
minArgs[2] = axis.isLog ? axis.log2lin(axis.dataMin) : axis.dataMin; | |
} | |
min = arrayMax(minArgs); | |
maxArgs = [min + minRange, pick(options.max, min + minRange)]; | |
if (spaceAvailable) { // if space is availabe, stay within the data range | |
maxArgs[2] = axis.isLog ? axis.log2lin(axis.dataMax) : axis.dataMax; | |
} | |
max = arrayMin(maxArgs); | |
// now if the max is adjusted, adjust the min back | |
if (max - min < minRange) { | |
minArgs[0] = max - minRange; | |
minArgs[1] = pick(options.min, max - minRange); | |
min = arrayMax(minArgs); | |
} | |
} | |
// Record modified extremes | |
axis.min = min; | |
axis.max = max; | |
}, | |
/** | |
* Find the closestPointRange across all series. | |
* | |
* @private | |
*/ | |
getClosest: function() { | |
var ret; | |
if (this.categories) { | |
ret = 1; | |
} else { | |
each(this.series, function(series) { | |
var seriesClosest = series.closestPointRange, | |
visible = series.visible || | |
!series.chart.options.chart.ignoreHiddenSeries; | |
if (!series.noSharedTooltip && | |
defined(seriesClosest) && | |
visible | |
) { | |
ret = defined(ret) ? | |
Math.min(ret, seriesClosest) : | |
seriesClosest; | |
} | |
}); | |
} | |
return ret; | |
}, | |
/** | |
* When a point name is given and no x, search for the name in the existing | |
* categories, or if categories aren't provided, search names or create a | |
* new category (#2522). | |
* | |
* @private | |
* | |
* @param {Point} | |
* The point to inspect. | |
* | |
* @return {Number} | |
* The X value that the point is given. | |
*/ | |
nameToX: function(point) { | |
var explicitCategories = isArray(this.categories), | |
names = explicitCategories ? this.categories : this.names, | |
nameX = point.options.x, | |
x; | |
point.series.requireSorting = false; | |
if (!defined(nameX)) { | |
nameX = this.options.uniqueNames === false ? | |
point.series.autoIncrement() : | |
inArray(point.name, names); | |
} | |
if (nameX === -1) { // The name is not found in currenct categories | |
if (!explicitCategories) { | |
x = names.length; | |
} | |
} else { | |
x = nameX; | |
} | |
// Write the last point's name to the names array | |
if (x !== undefined) { | |
this.names[x] = point.name; | |
} | |
return x; | |
}, | |
/** | |
* When changes have been done to series data, update the axis.names. | |
* | |
* @private | |
*/ | |
updateNames: function() { | |
var axis = this; | |
if (this.names.length > 0) { | |
this.names.length = 0; | |
this.minRange = this.userMinRange; // Reset | |
each(this.series || [], function(series) { | |
// Reset incrementer (#5928) | |
series.xIncrement = null; | |
// When adding a series, points are not yet generated | |
if (!series.points || series.isDirtyData) { | |
series.processData(); | |
series.generatePoints(); | |
} | |
each(series.points, function(point, i) { | |
var x; | |
if (point.options) { | |
x = axis.nameToX(point); | |
if (x !== undefined && x !== point.x) { | |
point.x = x; | |
series.xData[i] = x; | |
} | |
} | |
}); | |
}); | |
} | |
}, | |
/** | |
* Update translation information. | |
* | |
* @private | |
*/ | |
setAxisTranslation: function(saveOld) { | |
var axis = this, | |
range = axis.max - axis.min, | |
pointRange = axis.axisPointRange || 0, | |
closestPointRange, | |
minPointOffset = 0, | |
pointRangePadding = 0, | |
linkedParent = axis.linkedParent, | |
ordinalCorrection, | |
hasCategories = !!axis.categories, | |
transA = axis.transA, | |
isXAxis = axis.isXAxis; | |
// Adjust translation for padding. Y axis with categories need to go through the same (#1784). | |
if (isXAxis || hasCategories || pointRange) { | |
// Get the closest points | |
closestPointRange = axis.getClosest(); | |
if (linkedParent) { | |
minPointOffset = linkedParent.minPointOffset; | |
pointRangePadding = linkedParent.pointRangePadding; | |
} else { | |
each(axis.series, function(series) { | |
var seriesPointRange = hasCategories ? | |
1 : | |
(isXAxis ? | |
pick(series.options.pointRange, closestPointRange, 0) : | |
(axis.axisPointRange || 0)), // #2806 | |
pointPlacement = series.options.pointPlacement; | |
pointRange = Math.max(pointRange, seriesPointRange); | |
if (!axis.single) { | |
// minPointOffset is the value padding to the left of the axis in order to make | |
// room for points with a pointRange, typically columns. When the pointPlacement option | |
// is 'between' or 'on', this padding does not apply. | |
minPointOffset = Math.max( | |
minPointOffset, | |
isString(pointPlacement) ? 0 : seriesPointRange / 2 | |
); | |
// Determine the total padding needed to the length of the axis to make room for the | |
// pointRange. If the series' pointPlacement is 'on', no padding is added. | |
pointRangePadding = Math.max( | |
pointRangePadding, | |
pointPlacement === 'on' ? 0 : seriesPointRange | |
); | |
} | |
}); | |
} | |
// Record minPointOffset and pointRangePadding | |
ordinalCorrection = axis.ordinalSlope && closestPointRange ? axis.ordinalSlope / closestPointRange : 1; // #988, #1853 | |
axis.minPointOffset = minPointOffset = minPointOffset * ordinalCorrection; | |
axis.pointRangePadding = pointRangePadding = pointRangePadding * ordinalCorrection; | |
// pointRange means the width reserved for each point, like in a column chart | |
axis.pointRange = Math.min(pointRange, range); | |
// closestPointRange means the closest distance between points. In columns | |
// it is mostly equal to pointRange, but in lines pointRange is 0 while closestPointRange | |
// is some other value | |
if (isXAxis) { | |
axis.closestPointRange = closestPointRange; | |
} | |
} | |
// Secondary values | |
if (saveOld) { | |
axis.oldTransA = transA; | |
} | |
axis.translationSlope = axis.transA = transA = | |
axis.options.staticScale || | |
axis.len / ((range + pointRangePadding) || 1); | |
axis.transB = axis.horiz ? axis.left : axis.bottom; // translation addend | |
axis.minPixelPadding = transA * minPointOffset; | |
}, | |
minFromRange: function() { | |
return this.max - this.range; | |
}, | |
/** | |
* Set the tick positions to round values and optionally extend the extremes | |
* to the nearest tick. | |
* | |
* @private | |
*/ | |
setTickInterval: function(secondPass) { | |
var axis = this, | |
chart = axis.chart, | |
options = axis.options, | |
isLog = axis.isLog, | |
log2lin = axis.log2lin, | |
isDatetimeAxis = axis.isDatetimeAxis, | |
isXAxis = axis.isXAxis, | |
isLinked = axis.isLinked, | |
maxPadding = options.maxPadding, | |
minPadding = options.minPadding, | |
length, | |
linkedParentExtremes, | |
tickIntervalOption = options.tickInterval, | |
minTickInterval, | |
tickPixelIntervalOption = options.tickPixelInterval, | |
categories = axis.categories, | |
threshold = axis.threshold, | |
softThreshold = axis.softThreshold, | |
thresholdMin, | |
thresholdMax, | |
hardMin, | |
hardMax; | |
if (!isDatetimeAxis && !categories && !isLinked) { | |
this.getTickAmount(); | |
} | |
// Min or max set either by zooming/setExtremes or initial options | |
hardMin = pick(axis.userMin, options.min); | |
hardMax = pick(axis.userMax, options.max); | |
// Linked axis gets the extremes from the parent axis | |
if (isLinked) { | |
axis.linkedParent = chart[axis.coll][options.linkedTo]; | |
linkedParentExtremes = axis.linkedParent.getExtremes(); | |
axis.min = pick(linkedParentExtremes.min, linkedParentExtremes.dataMin); | |
axis.max = pick(linkedParentExtremes.max, linkedParentExtremes.dataMax); | |
if (options.type !== axis.linkedParent.options.type) { | |
H.error(11, 1); // Can't link axes of different type | |
} | |
// Initial min and max from the extreme data values | |
} else { | |
// Adjust to hard threshold | |
if (!softThreshold && defined(threshold)) { | |
if (axis.dataMin >= threshold) { | |
thresholdMin = threshold; | |
minPadding = 0; | |
} else if (axis.dataMax <= threshold) { | |
thresholdMax = threshold; | |
maxPadding = 0; | |
} | |
} | |
axis.min = pick(hardMin, thresholdMin, axis.dataMin); | |
axis.max = pick(hardMax, thresholdMax, axis.dataMax); | |
} | |
if (isLog) { | |
if ( | |
axis.positiveValuesOnly && | |
!secondPass && | |
Math.min(axis.min, pick(axis.dataMin, axis.min)) <= 0 | |
) { // #978 | |
H.error(10, 1); // Can't plot negative values on log axis | |
} | |
// The correctFloat cures #934, float errors on full tens. But it | |
// was too aggressive for #4360 because of conversion back to lin, | |
// therefore use precision 15. | |
axis.min = correctFloat(log2lin(axis.min), 15); | |
axis.max = correctFloat(log2lin(axis.max), 15); | |
} | |
// handle zoomed range | |
if (axis.range && defined(axis.max)) { | |
axis.userMin = axis.min = hardMin = Math.max(axis.dataMin, axis.minFromRange()); // #618, #6773 | |
axis.userMax = hardMax = axis.max; | |
axis.range = null; // don't use it when running setExtremes | |
} | |
// Hook for Highstock Scroller. Consider combining with beforePadding. | |
fireEvent(axis, 'foundExtremes'); | |
// Hook for adjusting this.min and this.max. Used by bubble series. | |
if (axis.beforePadding) { | |
axis.beforePadding(); | |
} | |
// adjust min and max for the minimum range | |
axis.adjustForMinRange(); | |
// Pad the values to get clear of the chart's edges. To avoid tickInterval taking the padding | |
// into account, we do this after computing tick interval (#1337). | |
if (!categories && !axis.axisPointRange && !axis.usePercentage && !isLinked && defined(axis.min) && defined(axis.max)) { | |
length = axis.max - axis.min; | |
if (length) { | |
if (!defined(hardMin) && minPadding) { | |
axis.min -= length * minPadding; | |
} | |
if (!defined(hardMax) && maxPadding) { | |
axis.max += length * maxPadding; | |
} | |
} | |
} | |
// Handle options for floor, ceiling, softMin and softMax (#6359) | |
if (isNumber(options.softMin)) { | |
axis.min = Math.min(axis.min, options.softMin); | |
} | |
if (isNumber(options.softMax)) { | |
axis.max = Math.max(axis.max, options.softMax); | |
} | |
if (isNumber(options.floor)) { | |
axis.min = Math.max(axis.min, options.floor); | |
} | |
if (isNumber(options.ceiling)) { | |
axis.max = Math.min(axis.max, options.ceiling); | |
} | |
// When the threshold is soft, adjust the extreme value only if | |
// the data extreme and the padded extreme land on either side of the threshold. For example, | |
// a series of [0, 1, 2, 3] would make the yAxis add a tick for -1 because of the | |
// default minPadding and startOnTick options. This is prevented by the softThreshold | |
// option. | |
if (softThreshold && defined(axis.dataMin)) { | |
threshold = threshold || 0; | |
if (!defined(hardMin) && axis.min < threshold && axis.dataMin >= threshold) { | |
axis.min = threshold; | |
} else if (!defined(hardMax) && axis.max > threshold && axis.dataMax <= threshold) { | |
axis.max = threshold; | |
} | |
} | |
// get tickInterval | |
if (axis.min === axis.max || axis.min === undefined || axis.max === undefined) { | |
axis.tickInterval = 1; | |
} else if (isLinked && !tickIntervalOption && | |
tickPixelIntervalOption === axis.linkedParent.options.tickPixelInterval) { | |
axis.tickInterval = tickIntervalOption = axis.linkedParent.tickInterval; | |
} else { | |
axis.tickInterval = pick( | |
tickIntervalOption, | |
this.tickAmount ? ((axis.max - axis.min) / Math.max(this.tickAmount - 1, 1)) : undefined, | |
categories ? // for categoried axis, 1 is default, for linear axis use tickPix | |
1 : | |
// don't let it be more than the data range | |
(axis.max - axis.min) * tickPixelIntervalOption / Math.max(axis.len, tickPixelIntervalOption) | |
); | |
} | |
// Now we're finished detecting min and max, crop and group series data. This | |
// is in turn needed in order to find tick positions in ordinal axes. | |
if (isXAxis && !secondPass) { | |
each(axis.series, function(series) { | |
series.processData(axis.min !== axis.oldMin || axis.max !== axis.oldMax); | |
}); | |
} | |
// set the translation factor used in translate function | |
axis.setAxisTranslation(true); | |
// hook for ordinal axes and radial axes | |
if (axis.beforeSetTickPositions) { | |
axis.beforeSetTickPositions(); | |
} | |
// hook for extensions, used in Highstock ordinal axes | |
if (axis.postProcessTickInterval) { | |
axis.tickInterval = axis.postProcessTickInterval(axis.tickInterval); | |
} | |
// In column-like charts, don't cramp in more ticks than there are points (#1943, #4184) | |
if (axis.pointRange && !tickIntervalOption) { | |
axis.tickInterval = Math.max(axis.pointRange, axis.tickInterval); | |
} | |
// Before normalizing the tick interval, handle minimum tick interval. This applies only if tickInterval is not defined. | |
minTickInterval = pick(options.minTickInterval, axis.isDatetimeAxis && axis.closestPointRange); | |
if (!tickIntervalOption && axis.tickInterval < minTickInterval) { | |
axis.tickInterval = minTickInterval; | |
} | |
// for linear axes, get magnitude and normalize the interval | |
if (!isDatetimeAxis && !isLog && !tickIntervalOption) { | |
axis.tickInterval = normalizeTickInterval( | |
axis.tickInterval, | |
null, | |
getMagnitude(axis.tickInterval), | |
// If the tick interval is between 0.5 and 5 and the axis max is in the order of | |
// thousands, chances are we are dealing with years. Don't allow decimals. #3363. | |
pick(options.allowDecimals, !(axis.tickInterval > 0.5 && axis.tickInterval < 5 && axis.max > 1000 && axis.max < 9999)), !!this.tickAmount | |
); | |
} | |
// Prevent ticks from getting so close that we can't draw the labels | |
if (!this.tickAmount) { | |
axis.tickInterval = axis.unsquish(); | |
} | |
this.setTickPositions(); | |
}, | |
/** | |
* Now we have computed the normalized tickInterval, get the tick positions | |
*/ | |
setTickPositions: function() { | |
var options = this.options, | |
tickPositions, | |
tickPositionsOption = options.tickPositions, | |
tickPositioner = options.tickPositioner, | |
startOnTick = options.startOnTick, | |
endOnTick = options.endOnTick; | |
// Set the tickmarkOffset | |
this.tickmarkOffset = (this.categories && options.tickmarkPlacement === 'between' && | |
this.tickInterval === 1) ? 0.5 : 0; // #3202 | |
// get minorTickInterval | |
this.minorTickInterval = options.minorTickInterval === 'auto' && this.tickInterval ? | |
this.tickInterval / 5 : options.minorTickInterval; | |
// When there is only one point, or all points have the same value on | |
// this axis, then min and max are equal and tickPositions.length is 0 | |
// or 1. In this case, add some padding in order to center the point, | |
// but leave it with one tick. #1337. | |
this.single = | |
this.min === this.max && | |
defined(this.min) && | |
!this.tickAmount && | |
( | |
// Data is on integer (#6563) | |
parseInt(this.min, 10) === this.min || | |
// Between integers and decimals are not allowed (#6274) | |
options.allowDecimals !== false | |
); | |
// Find the tick positions | |
this.tickPositions = tickPositions = tickPositionsOption && tickPositionsOption.slice(); // Work on a copy (#1565) | |
if (!tickPositions) { | |
if (this.isDatetimeAxis) { | |
tickPositions = this.getTimeTicks( | |
this.normalizeTimeTickInterval( | |
this.tickInterval, | |
options.units | |
), | |
this.min, | |
this.max, | |
options.startOfWeek, | |
this.ordinalPositions, | |
this.closestPointRange, | |
true | |
); | |
} else if (this.isLog) { | |
tickPositions = this.getLogTickPositions( | |
this.tickInterval, | |
this.min, | |
this.max | |
); | |
} else { | |
tickPositions = this.getLinearTickPositions( | |
this.tickInterval, | |
this.min, | |
this.max | |
); | |
} | |
// Too dense ticks, keep only the first and last (#4477) | |
if (tickPositions.length > this.len) { | |
tickPositions = [tickPositions[0], tickPositions.pop()]; | |
} | |
this.tickPositions = tickPositions; | |
// Run the tick positioner callback, that allows modifying auto tick positions. | |
if (tickPositioner) { | |
tickPositioner = tickPositioner.apply(this, [this.min, this.max]); | |
if (tickPositioner) { | |
this.tickPositions = tickPositions = tickPositioner; | |
} | |
} | |
} | |
// Reset min/max or remove extremes based on start/end on tick | |
this.paddedTicks = tickPositions.slice(0); // Used for logarithmic minor | |
this.trimTicks(tickPositions, startOnTick, endOnTick); | |
if (!this.isLinked) { | |
// Substract half a unit (#2619, #2846, #2515, #3390) | |
if (this.single) { | |
this.min -= 0.5; | |
this.max += 0.5; | |
} | |
if (!tickPositionsOption && !tickPositioner) { | |
this.adjustTickAmount(); | |
} | |
} | |
}, | |
/** | |
* Handle startOnTick and endOnTick by either adapting to padding min/max or | |
* rounded min/max. Also handle single data points. | |
* | |
* @private | |
*/ | |
trimTicks: function(tickPositions, startOnTick, endOnTick) { | |
var roundedMin = tickPositions[0], | |
roundedMax = tickPositions[tickPositions.length - 1], | |
minPointOffset = this.minPointOffset || 0; | |
if (!this.isLinked) { | |
if (startOnTick && roundedMin !== -Infinity) { // #6502 | |
this.min = roundedMin; | |
} else { | |
while (this.min - minPointOffset > tickPositions[0]) { | |
tickPositions.shift(); | |
} | |
} | |
if (endOnTick) { | |
this.max = roundedMax; | |
} else { | |
while (this.max + minPointOffset < tickPositions[tickPositions.length - 1]) { | |
tickPositions.pop(); | |
} | |
} | |
// If no tick are left, set one tick in the middle (#3195) | |
if (tickPositions.length === 0 && defined(roundedMin)) { | |
tickPositions.push((roundedMax + roundedMin) / 2); | |
} | |
} | |
}, | |
/** | |
* Check if there are multiple axes in the same pane. | |
* | |
* @private | |
* @return {Boolean} | |
* True if there are other axes. | |
*/ | |
alignToOthers: function() { | |
var others = {}, // Whether there is another axis to pair with this one | |
hasOther, | |
options = this.options; | |
if ( | |
// Only if alignTicks is true | |
this.chart.options.chart.alignTicks !== false && | |
options.alignTicks !== false && | |
// Don't try to align ticks on a log axis, they are not evenly | |
// spaced (#6021) | |
!this.isLog | |
) { | |
each(this.chart[this.coll], function(axis) { | |
var otherOptions = axis.options, | |
horiz = axis.horiz, | |
key = [ | |
horiz ? otherOptions.left : otherOptions.top, | |
otherOptions.width, | |
otherOptions.height, | |
otherOptions.pane | |
].join(','); | |
if (axis.series.length) { // #4442 | |
if (others[key]) { | |
hasOther = true; // #4201 | |
} else { | |
others[key] = 1; | |
} | |
} | |
}); | |
} | |
return hasOther; | |
}, | |
/** | |
* Find the max ticks of either the x and y axis collection, and record it | |
* in `this.tickAmount`. | |
* | |
* @private | |
*/ | |
getTickAmount: function() { | |
var options = this.options, | |
tickAmount = options.tickAmount, | |
tickPixelInterval = options.tickPixelInterval; | |
if (!defined(options.tickInterval) && this.len < tickPixelInterval && !this.isRadial && | |
!this.isLog && options.startOnTick && options.endOnTick) { | |
tickAmount = 2; | |
} | |
if (!tickAmount && this.alignToOthers()) { | |
// Add 1 because 4 tick intervals require 5 ticks (including first and last) | |
tickAmount = Math.ceil(this.len / tickPixelInterval) + 1; | |
} | |
// For tick amounts of 2 and 3, compute five ticks and remove the intermediate ones. This | |
// prevents the axis from adding ticks that are too far away from the data extremes. | |
if (tickAmount < 4) { | |
this.finalTickAmt = tickAmount; | |
tickAmount = 5; | |
} | |
this.tickAmount = tickAmount; | |
}, | |
/** | |
* When using multiple axes, adjust the number of ticks to match the highest | |
* number of ticks in that group. | |
* | |
* @private | |
*/ | |
adjustTickAmount: function() { | |
var tickInterval = this.tickInterval, | |
tickPositions = this.tickPositions, | |
tickAmount = this.tickAmount, | |
finalTickAmt = this.finalTickAmt, | |
currentTickAmount = tickPositions && tickPositions.length, | |
i, | |
len; | |
if (currentTickAmount < tickAmount) { | |
while (tickPositions.length < tickAmount) { | |
tickPositions.push(correctFloat( | |
tickPositions[tickPositions.length - 1] + tickInterval | |
)); | |
} | |
this.transA *= (currentTickAmount - 1) / (tickAmount - 1); | |
this.max = tickPositions[tickPositions.length - 1]; | |
// We have too many ticks, run second pass to try to reduce ticks | |
} else if (currentTickAmount > tickAmount) { | |
this.tickInterval *= 2; | |
this.setTickPositions(); | |
} | |
// The finalTickAmt property is set in getTickAmount | |
if (defined(finalTickAmt)) { | |
i = len = tickPositions.length; | |
while (i--) { | |
if ( | |
(finalTickAmt === 3 && i % 2 === 1) || // Remove every other tick | |
(finalTickAmt <= 2 && i > 0 && i < len - 1) // Remove all but first and last | |
) { | |
tickPositions.splice(i, 1); | |
} | |
} | |
this.finalTickAmt = undefined; | |
} | |
}, | |
/** | |
* Set the scale based on data min and max, user set min and max or options. | |
* | |
* @private | |
*/ | |
setScale: function() { | |
var axis = this, | |
isDirtyData, | |
isDirtyAxisLength; | |
axis.oldMin = axis.min; | |
axis.oldMax = axis.max; | |
axis.oldAxisLength = axis.len; | |
// set the new axisLength | |
axis.setAxisSize(); | |
//axisLength = horiz ? axisWidth : axisHeight; | |
isDirtyAxisLength = axis.len !== axis.oldAxisLength; | |
// is there new data? | |
each(axis.series, function(series) { | |
if (series.isDirtyData || series.isDirty || | |
series.xAxis.isDirty) { // when x axis is dirty, we need new data extremes for y as well | |
isDirtyData = true; | |
} | |
}); | |
// do we really need to go through all this? | |
if (isDirtyAxisLength || isDirtyData || axis.isLinked || axis.forceRedraw || | |
axis.userMin !== axis.oldUserMin || axis.userMax !== axis.oldUserMax || axis.alignToOthers()) { | |
if (axis.resetStacks) { | |
axis.resetStacks(); | |
} | |
axis.forceRedraw = false; | |
// get data extremes if needed | |
axis.getSeriesExtremes(); | |
// get fixed positions based on tickInterval | |
axis.setTickInterval(); | |
// record old values to decide whether a rescale is necessary later on (#540) | |
axis.oldUserMin = axis.userMin; | |
axis.oldUserMax = axis.userMax; | |
// Mark as dirty if it is not already set to dirty and extremes have changed. #595. | |
if (!axis.isDirty) { | |
axis.isDirty = isDirtyAxisLength || axis.min !== axis.oldMin || axis.max !== axis.oldMax; | |
} | |
} else if (axis.cleanStacks) { | |
axis.cleanStacks(); | |
} | |
}, | |
/** | |
* Set the minimum and maximum of the axes after render time. If the | |
* `startOnTick` and `endOnTick` options are true, the minimum and maximum | |
* values are rounded off to the nearest tick. To prevent this, these | |
* options can be set to false before calling setExtremes. Also, setExtremes | |
* will not allow a range lower than the `minRange` option, which by default | |
* is the range of five points. | |
* | |
* @param {Number} [newMin] | |
* The new minimum value. | |
* @param {Number} [newMax] | |
* The new maximum value. | |
* @param {Boolean} [redraw=true] | |
* Whether to redraw the chart or wait for an explicit call to | |
* {@link Highcharts.Chart#redraw} | |
* @param {AnimationOptions} [animation=true] | |
* Enable or modify animations. | |
* @param {Object} [eventArguments] | |
* Arguments to be accessed in event handler. | |
* | |
* @sample highcharts/members/axis-setextremes/ | |
* Set extremes from a button | |
* @sample highcharts/members/axis-setextremes-datetime/ | |
* Set extremes on a datetime axis | |
* @sample highcharts/members/axis-setextremes-off-ticks/ | |
* Set extremes off ticks | |
* @sample stock/members/axis-setextremes/ | |
* Set extremes in Highstock | |
* @sample maps/members/axis-setextremes/ | |
* Set extremes in Highmaps | |
*/ | |
setExtremes: function(newMin, newMax, redraw, animation, eventArguments) { | |
var axis = this, | |
chart = axis.chart; | |
redraw = pick(redraw, true); // defaults to true | |
each(axis.series, function(serie) { | |
delete serie.kdTree; | |
}); | |
// Extend the arguments with min and max | |
eventArguments = extend(eventArguments, { | |
min: newMin, | |
max: newMax | |
}); | |
// Fire the event | |
fireEvent(axis, 'setExtremes', eventArguments, function() { // the default event handler | |
axis.userMin = newMin; | |
axis.userMax = newMax; | |
axis.eventArgs = eventArguments; | |
if (redraw) { | |
chart.redraw(animation); | |
} | |
}); | |
}, | |
/** | |
* Overridable method for zooming chart. Pulled out in a separate method to | |
* allow overriding in stock charts. | |
* | |
* @private | |
*/ | |
zoom: function(newMin, newMax) { | |
var dataMin = this.dataMin, | |
dataMax = this.dataMax, | |
options = this.options, | |
min = Math.min(dataMin, pick(options.min, dataMin)), | |
max = Math.max(dataMax, pick(options.max, dataMax)); | |
if (newMin !== this.min || newMax !== this.max) { // #5790 | |
// Prevent pinch zooming out of range. Check for defined is for #1946. #1734. | |
if (!this.allowZoomOutside) { | |
// #6014, sometimes newMax will be smaller than min (or newMin will be larger than max). | |
if (defined(dataMin)) { | |
if (newMin < min) { | |
newMin = min; | |
} | |
if (newMin > max) { | |
newMin = max; | |
} | |
} | |
if (defined(dataMax)) { | |
if (newMax < min) { | |
newMax = min; | |
} | |
if (newMax > max) { | |
newMax = max; | |
} | |
} | |
} | |
// In full view, displaying the reset zoom button is not required | |
this.displayBtn = newMin !== undefined || newMax !== undefined; | |
// Do it | |
this.setExtremes( | |
newMin, | |
newMax, | |
false, | |
undefined, { | |
trigger: 'zoom' | |
} | |
); | |
} | |
return true; | |
}, | |
/** | |
* Update the axis metrics. | |
* | |
* @private | |
*/ | |
setAxisSize: function() { | |
var chart = this.chart, | |
options = this.options, | |
offsets = options.offsets || [0, 0, 0, 0], // top / right / bottom / left | |
horiz = this.horiz, | |
width = pick(options.width, chart.plotWidth - offsets[3] + offsets[1]), | |
height = pick(options.height, chart.plotHeight - offsets[0] + offsets[2]), | |
top = pick(options.top, chart.plotTop + offsets[0]), | |
left = pick(options.left, chart.plotLeft + offsets[3]), | |
percentRegex = /%$/; | |
// Check for percentage based input values. Rounding fixes problems with | |
// column overflow and plot line filtering (#4898, #4899) | |
if (percentRegex.test(height)) { | |
height = Math.round(parseFloat(height) / 100 * chart.plotHeight); | |
} | |
if (percentRegex.test(top)) { | |
top = Math.round(parseFloat(top) / 100 * chart.plotHeight + chart.plotTop); | |
} | |
// Expose basic values to use in Series object and navigator | |
this.left = left; | |
this.top = top; | |
this.width = width; | |
this.height = height; | |
this.bottom = chart.chartHeight - height - top; | |
this.right = chart.chartWidth - width - left; | |
// Direction agnostic properties | |
this.len = Math.max(horiz ? width : height, 0); // Math.max fixes #905 | |
this.pos = horiz ? left : top; // distance from SVG origin | |
}, | |
/** | |
* The returned object literal from the {@link Highcharts.Axis#getExtremes} | |
* function. | |
* @typedef {Object} Extremes | |
* @property {Number} dataMax | |
* The maximum value of the axis' associated series. | |
* @property {Number} dataMin | |
* The minimum value of the axis' associated series. | |
* @property {Number} max | |
* The maximum axis value, either automatic or set manually. If the | |
* `max` option is not set, `maxPadding` is 0 and `endOnTick` is | |
* false, this value will be the same as `dataMax`. | |
* @property {Number} min | |
* The minimum axis value, either automatic or set manually. If the | |
* `min` option is not set, `minPadding` is 0 and `startOnTick` is | |
* false, this value will be the same as `dataMin`. | |
*/ | |
/** | |
* Get the current extremes for the axis. | |
* | |
* @returns {Extremes} | |
* An object containing extremes information. | |
* | |
* @sample members/axis-getextremes/ | |
* Report extremes by click on a button | |
* @sample maps/members/axis-getextremes/ | |
* Get extremes in Highmaps | |
*/ | |
getExtremes: function() { | |
var axis = this, | |
isLog = axis.isLog, | |
lin2log = axis.lin2log; | |
return { | |
min: isLog ? correctFloat(lin2log(axis.min)) : axis.min, | |
max: isLog ? correctFloat(lin2log(axis.max)) : axis.max, | |
dataMin: axis.dataMin, | |
dataMax: axis.dataMax, | |
userMin: axis.userMin, | |
userMax: axis.userMax | |
}; | |
}, | |
/** | |
* Get the zero plane either based on zero or on the min or max value. | |
* Used in bar and area plots. | |
* | |
* @param {Number} threshold | |
* The threshold in axis values. | |
* | |
* @return {Number} | |
* The translated threshold position in terms of pixels, and | |
* corrected to stay within the axis bounds. | |
*/ | |
getThreshold: function(threshold) { | |
var axis = this, | |
isLog = axis.isLog, | |
lin2log = axis.lin2log, | |
realMin = isLog ? lin2log(axis.min) : axis.min, | |
realMax = isLog ? lin2log(axis.max) : axis.max; | |
if (threshold === null) { | |
threshold = realMin; | |
} else if (realMin > threshold) { | |
threshold = realMin; | |
} else if (realMax < threshold) { | |
threshold = realMax; | |
} | |
return axis.translate(threshold, 0, 1, 0, 1); | |
}, | |
/** | |
* Compute auto alignment for the axis label based on which side the axis is | |
* on and the given rotation for the label. | |
* | |
* @param {Number} rotation | |
* The rotation in degrees as set by either the `rotation` or | |
* `autoRotation` options. | |
* @private | |
*/ | |
autoLabelAlign: function(rotation) { | |
var ret, | |
angle = (pick(rotation, 0) - (this.side * 90) + 720) % 360; | |
if (angle > 15 && angle < 165) { | |
ret = 'right'; | |
} else if (angle > 195 && angle < 345) { | |
ret = 'left'; | |
} else { | |
ret = 'center'; | |
} | |
return ret; | |
}, | |
/** | |
* Get the tick length and width for the axis based on axis options. | |
* | |
* @private | |
* | |
* @param {String} prefix | |
* 'tick' or 'minorTick' | |
* @return {Array.<Number>} | |
* An array of tickLength and tickWidth | |
*/ | |
tickSize: function(prefix) { | |
var options = this.options, | |
tickLength = options[prefix + 'Length'], | |
tickWidth = pick(options[prefix + 'Width'], prefix === 'tick' && this.isXAxis ? 1 : 0); // X axis defaults to 1 | |
if (tickWidth && tickLength) { | |
// Negate the length | |
if (options[prefix + 'Position'] === 'inside') { | |
tickLength = -tickLength; | |
} | |
return [tickLength, tickWidth]; | |
} | |
}, | |
/** | |
* Return the size of the labels. | |
* | |
* @private | |
*/ | |
labelMetrics: function() { | |
var index = this.tickPositions && this.tickPositions[0] || 0; | |
return this.chart.renderer.fontMetrics( | |
this.options.labels.style && this.options.labels.style.fontSize, | |
this.ticks[index] && this.ticks[index].label | |
); | |
}, | |
/** | |
* Prevent the ticks from getting so close we can't draw the labels. On a | |
* horizontal axis, this is handled by rotating the labels, removing ticks | |
* and adding ellipsis. On a vertical axis remove ticks and add ellipsis. | |
* | |
* @private | |
*/ | |
unsquish: function() { | |
var labelOptions = this.options.labels, | |
horiz = this.horiz, | |
tickInterval = this.tickInterval, | |
newTickInterval = tickInterval, | |
slotSize = this.len / (((this.categories ? 1 : 0) + this.max - this.min) / tickInterval), | |
rotation, | |
rotationOption = labelOptions.rotation, | |
labelMetrics = this.labelMetrics(), | |
step, | |
bestScore = Number.MAX_VALUE, | |
autoRotation, | |
// Return the multiple of tickInterval that is needed to avoid collision | |
getStep = function(spaceNeeded) { | |
var step = spaceNeeded / (slotSize || 1); | |
step = step > 1 ? Math.ceil(step) : 1; | |
return step * tickInterval; | |
}; | |
if (horiz) { | |
autoRotation = !labelOptions.staggerLines && !labelOptions.step && ( // #3971 | |
defined(rotationOption) ? [rotationOption] : | |
slotSize < pick(labelOptions.autoRotationLimit, 80) && labelOptions.autoRotation | |
); | |
if (autoRotation) { | |
// Loop over the given autoRotation options, and determine which gives the best score. The | |
// best score is that with the lowest number of steps and a rotation closest to horizontal. | |
each(autoRotation, function(rot) { | |
var score; | |
if (rot === rotationOption || (rot && rot >= -90 && rot <= 90)) { // #3891 | |
step = getStep(Math.abs(labelMetrics.h / Math.sin(deg2rad * rot))); | |
score = step + Math.abs(rot / 360); | |
if (score < bestScore) { | |
bestScore = score; | |
rotation = rot; | |
newTickInterval = step; | |
} | |
} | |
}); | |
} | |
} else if (!labelOptions.step) { // #4411 | |
newTickInterval = getStep(labelMetrics.h); | |
} | |
this.autoRotation = autoRotation; | |
this.labelRotation = pick(rotation, rotationOption); | |
return newTickInterval; | |
}, | |
/** | |
* Get the general slot width for labels/categories on this axis. This may | |
* change between the pre-render (from Axis.getOffset) and the final tick | |
* rendering and placement. | |
* | |
* @private | |
* @return {Number} | |
* The pixel width allocated to each axis label. | |
*/ | |
getSlotWidth: function() { | |
// #5086, #1580, #1931 | |
var chart = this.chart, | |
horiz = this.horiz, | |
labelOptions = this.options.labels, | |
slotCount = Math.max(this.tickPositions.length - (this.categories ? 0 : 1), 1), | |
marginLeft = chart.margin[3]; | |
return ( | |
horiz && | |
(labelOptions.step || 0) < 2 && | |
!labelOptions.rotation && // #4415 | |
((this.staggerLines || 1) * this.len) / slotCount | |
) || (!horiz && ( | |
(marginLeft && (marginLeft - chart.spacing[3])) || | |
chart.chartWidth * 0.33 | |
)); | |
}, | |
/** | |
* Render the axis labels and determine whether ellipsis or rotation need | |
* to be applied. | |
* | |
* @private | |
*/ | |
renderUnsquish: function() { | |
var chart = this.chart, | |
renderer = chart.renderer, | |
tickPositions = this.tickPositions, | |
ticks = this.ticks, | |
labelOptions = this.options.labels, | |
horiz = this.horiz, | |
slotWidth = this.getSlotWidth(), | |
innerWidth = Math.max(1, Math.round(slotWidth - 2 * (labelOptions.padding || 5))), | |
attr = {}, | |
labelMetrics = this.labelMetrics(), | |
textOverflowOption = labelOptions.style && labelOptions.style.textOverflow, | |
css, | |
maxLabelLength = 0, | |
label, | |
i, | |
pos; | |
// Set rotation option unless it is "auto", like in gauges | |
if (!isString(labelOptions.rotation)) { | |
attr.rotation = labelOptions.rotation || 0; // #4443 | |
} | |
// Get the longest label length | |
each(tickPositions, function(tick) { | |
tick = ticks[tick]; | |
if (tick && tick.labelLength > maxLabelLength) { | |
maxLabelLength = tick.labelLength; | |
} | |
}); | |
this.maxLabelLength = maxLabelLength; | |
// Handle auto rotation on horizontal axis | |
if (this.autoRotation) { | |
// Apply rotation only if the label is too wide for the slot, and | |
// the label is wider than its height. | |
if (maxLabelLength > innerWidth && maxLabelLength > labelMetrics.h) { | |
attr.rotation = this.labelRotation; | |
} else { | |
this.labelRotation = 0; | |
} | |
// Handle word-wrap or ellipsis on vertical axis | |
} else if (slotWidth) { | |
// For word-wrap or ellipsis | |
css = { | |
width: innerWidth + 'px' | |
}; | |
if (!textOverflowOption) { | |
css.textOverflow = 'clip'; | |
// On vertical axis, only allow word wrap if there is room for more lines. | |
i = tickPositions.length; | |
while (!horiz && i--) { | |
pos = tickPositions[i]; | |
label = ticks[pos].label; | |
if (label) { | |
// Reset ellipsis in order to get the correct bounding box (#4070) | |
if (label.styles && label.styles.textOverflow === 'ellipsis') { | |
label.css({ | |
textOverflow: 'clip' | |
}); | |
// Set the correct width in order to read the bounding box height (#4678, #5034) | |
} else if (ticks[pos].labelLength > slotWidth) { | |
label.css({ | |
width: slotWidth + 'px' | |
}); | |
} | |
if (label.getBBox().height > this.len / tickPositions.length - (labelMetrics.h - labelMetrics.f)) { | |
label.specCss = { | |
textOverflow: 'ellipsis' | |
}; | |
} | |
} | |
} | |
} | |
} | |
// Add ellipsis if the label length is significantly longer than ideal | |
if (attr.rotation) { | |
css = { | |
width: (maxLabelLength > chart.chartHeight * 0.5 ? chart.chartHeight * 0.33 : chart.chartHeight) + 'px' | |
}; | |
if (!textOverflowOption) { | |
css.textOverflow = 'ellipsis'; | |
} | |
} | |
// Set the explicit or automatic label alignment | |
this.labelAlign = labelOptions.align || this.autoLabelAlign(this.labelRotation); | |
if (this.labelAlign) { | |
attr.align = this.labelAlign; | |
} | |
// Apply general and specific CSS | |
each(tickPositions, function(pos) { | |
var tick = ticks[pos], | |
label = tick && tick.label; | |
if (label) { | |
label.attr(attr); // This needs to go before the CSS in old IE (#4502) | |
if (css) { | |
label.css(merge(css, label.specCss)); | |
} | |
delete label.specCss; | |
tick.rotation = attr.rotation; | |
} | |
}); | |
// Note: Why is this not part of getLabelPosition? | |
this.tickRotCorr = renderer.rotCorr(labelMetrics.b, this.labelRotation || 0, this.side !== 0); | |
}, | |
/** | |
* Return true if the axis has associated data. | |
* | |
* @return {Boolean} | |
* True if the axis has associated visible series and those series | |
* have either valid data points or explicit `min` and `max` | |
* settings. | |
*/ | |
hasData: function() { | |
return ( | |
this.hasVisibleSeries || | |
(defined(this.min) && defined(this.max) && !!this.tickPositions) | |
); | |
}, | |
/** | |
* Adds the title defined in axis.options.title. | |
* @param {Boolean} display - whether or not to display the title | |
*/ | |
addTitle: function(display) { | |
var axis = this, | |
renderer = axis.chart.renderer, | |
horiz = axis.horiz, | |
opposite = axis.opposite, | |
options = axis.options, | |
axisTitleOptions = options.title, | |
textAlign; | |
if (!axis.axisTitle) { | |
textAlign = axisTitleOptions.textAlign; | |
if (!textAlign) { | |
textAlign = (horiz ? { | |
low: 'left', | |
middle: 'center', | |
high: 'right' | |
} : { | |
low: opposite ? 'right' : 'left', | |
middle: 'center', | |
high: opposite ? 'left' : 'right' | |
})[axisTitleOptions.align]; | |
} | |
axis.axisTitle = renderer.text( | |
axisTitleOptions.text, | |
0, | |
0, | |
axisTitleOptions.useHTML | |
) | |
.attr({ | |
zIndex: 7, | |
rotation: axisTitleOptions.rotation || 0, | |
align: textAlign | |
}) | |
.addClass('highcharts-axis-title') | |
.add(axis.axisGroup); | |
axis.axisTitle.isNew = true; | |
} | |
// hide or show the title depending on whether showEmpty is set | |
axis.axisTitle[display ? 'show' : 'hide'](true); | |
}, | |
/** | |
* Generates a tick for initial positioning. | |
* | |
* @private | |
* @param {number} pos | |
* The tick position in axis values. | |
* @param {number} i | |
* The index of the tick in {@link Axis.tickPositions}. | |
*/ | |
generateTick: function(pos) { | |
var ticks = this.ticks; | |
if (!ticks[pos]) { | |
ticks[pos] = new Tick(this, pos); | |
} else { | |
ticks[pos].addLabel(); // update labels depending on tick interval | |
} | |
}, | |
/** | |
* Render the tick labels to a preliminary position to get their sizes. | |
* | |
* @private | |
*/ | |
getOffset: function() { | |
var axis = this, | |
chart = axis.chart, | |
renderer = chart.renderer, | |
options = axis.options, | |
tickPositions = axis.tickPositions, | |
ticks = axis.ticks, | |
horiz = axis.horiz, | |
side = axis.side, | |
invertedSide = chart.inverted && !axis.isZAxis ? [1, 0, 3, 2][side] : side, | |
hasData, | |
showAxis, | |
titleOffset = 0, | |
titleOffsetOption, | |
titleMargin = 0, | |
axisTitleOptions = options.title, | |
labelOptions = options.labels, | |
labelOffset = 0, // reset | |
labelOffsetPadded, | |
axisOffset = chart.axisOffset, | |
clipOffset = chart.clipOffset, | |
clip, | |
directionFactor = [-1, 1, 1, -1][side], | |
className = options.className, | |
axisParent = axis.axisParent, // Used in color axis | |
lineHeightCorrection, | |
tickSize = this.tickSize('tick'); | |
// For reuse in Axis.render | |
hasData = axis.hasData(); | |
axis.showAxis = showAxis = hasData || pick(options.showEmpty, true); | |
// Set/reset staggerLines | |
axis.staggerLines = axis.horiz && labelOptions.staggerLines; | |
// Create the axisGroup and gridGroup elements on first iteration | |
if (!axis.axisGroup) { | |
axis.gridGroup = renderer.g('grid') | |
.attr({ | |
zIndex: options.gridZIndex || 1 | |
}) | |
.addClass('highcharts-' + this.coll.toLowerCase() + '-grid ' + (className || '')) | |
.add(axisParent); | |
axis.axisGroup = renderer.g('axis') | |
.attr({ | |
zIndex: options.zIndex || 2 | |
}) | |
.addClass('highcharts-' + this.coll.toLowerCase() + ' ' + (className || '')) | |
.add(axisParent); | |
axis.labelGroup = renderer.g('axis-labels') | |
.attr({ | |
zIndex: labelOptions.zIndex || 7 | |
}) | |
.addClass('highcharts-' + axis.coll.toLowerCase() + '-labels ' + (className || '')) | |
.add(axisParent); | |
} | |
if (hasData || axis.isLinked) { | |
// Generate ticks | |
each(tickPositions, function(pos, i) { | |
// i is not used here, but may be used in overrides | |
axis.generateTick(pos, i); | |
}); | |
axis.renderUnsquish(); | |
// Left side must be align: right and right side must have align: left for labels | |
if (labelOptions.reserveSpace !== false && (side === 0 || side === 2 || { | |
1: 'left', | |
3: 'right' | |
}[side] === axis.labelAlign || axis.labelAlign === 'center')) { | |
each(tickPositions, function(pos) { | |
// get the highest offset | |
labelOffset = Math.max( | |
ticks[pos].getLabelSize(), | |
labelOffset | |
); | |
}); | |
} | |
if (axis.staggerLines) { | |
labelOffset *= axis.staggerLines; | |
axis.labelOffset = labelOffset * (axis.opposite ? -1 : 1); | |
} | |
} else { // doesn't have data | |
objectEach(ticks, function(tick, n) { | |
tick.destroy(); | |
delete ticks[n]; | |
}); | |
} | |
if (axisTitleOptions && axisTitleOptions.text && axisTitleOptions.enabled !== false) { | |
axis.addTitle(showAxis); | |
if (showAxis && axisTitleOptions.reserveSpace !== false) { | |
axis.titleOffset = titleOffset = | |
axis.axisTitle.getBBox()[horiz ? 'height' : 'width']; | |
titleOffsetOption = axisTitleOptions.offset; | |
titleMargin = defined(titleOffsetOption) ? 0 : pick(axisTitleOptions.margin, horiz ? 5 : 10); | |
} | |
} | |
// Render the axis line | |
axis.renderLine(); | |
// handle automatic or user set offset | |
axis.offset = directionFactor * pick(options.offset, axisOffset[side]); | |
axis.tickRotCorr = axis.tickRotCorr || { | |
x: 0, | |
y: 0 | |
}; // polar | |
if (side === 0) { | |
lineHeightCorrection = -axis.labelMetrics().h; | |
} else if (side === 2) { | |
lineHeightCorrection = axis.tickRotCorr.y; | |
} else { | |
lineHeightCorrection = 0; | |
} | |
// Find the padded label offset | |
labelOffsetPadded = Math.abs(labelOffset) + titleMargin; | |
if (labelOffset) { | |
labelOffsetPadded -= lineHeightCorrection; | |
labelOffsetPadded += directionFactor * (horiz ? pick(labelOptions.y, axis.tickRotCorr.y + directionFactor * 8) : labelOptions.x); | |
} | |
axis.axisTitleMargin = pick(titleOffsetOption, labelOffsetPadded); | |
axisOffset[side] = Math.max( | |
axisOffset[side], | |
axis.axisTitleMargin + titleOffset + directionFactor * axis.offset, | |
labelOffsetPadded, // #3027 | |
hasData && tickPositions.length && tickSize ? | |
tickSize[0] + directionFactor * axis.offset : | |
0 // #4866 | |
); | |
// Decide the clipping needed to keep the graph inside the plot area and | |
// axis lines | |
clip = Math.floor(axis.axisLine.strokeWidth() / 2) * 2; // #4308, #4371 | |
if (options.offset > 0) { | |
clip -= options.offset * 2; | |
} | |
clipOffset[invertedSide] = Math.max( | |
clipOffset[invertedSide] || clip, | |
clip | |
); | |
}, | |
/** | |
* Internal function to get the path for the axis line. Extended for polar | |
* charts. | |
* | |
* @param {Number} lineWidth | |
* The line width in pixels. | |
* @return {Array} | |
* The SVG path definition in array form. | |
*/ | |
getLinePath: function(lineWidth) { | |
var chart = this.chart, | |
opposite = this.opposite, | |
offset = this.offset, | |
horiz = this.horiz, | |
lineLeft = this.left + (opposite ? this.width : 0) + offset, | |
lineTop = chart.chartHeight - this.bottom - | |
(opposite ? this.height : 0) + offset; | |
if (opposite) { | |
lineWidth *= -1; // crispify the other way - #1480, #1687 | |
} | |
return chart.renderer | |
.crispLine([ | |
'M', | |
horiz ? | |
this.left : | |
lineLeft, | |
horiz ? | |
lineTop : | |
this.top, | |
'L', | |
horiz ? | |
chart.chartWidth - this.right : | |
lineLeft, | |
horiz ? | |
lineTop : | |
chart.chartHeight - this.bottom | |
], lineWidth); | |
}, | |
/** | |
* Render the axis line. Called internally when rendering and redrawing the | |
* axis. | |
*/ | |
renderLine: function() { | |
if (!this.axisLine) { | |
this.axisLine = this.chart.renderer.path() | |
.addClass('highcharts-axis-line') | |
.add(this.axisGroup); | |
} | |
}, | |
/** | |
* Position the axis title. | |
* | |
* @private | |
* | |
* @return {Object} | |
* X and Y positions for the title. | |
*/ | |
getTitlePosition: function() { | |
// compute anchor points for each of the title align options | |
var horiz = this.horiz, | |
axisLeft = this.left, | |
axisTop = this.top, | |
axisLength = this.len, | |
axisTitleOptions = this.options.title, | |
margin = horiz ? axisLeft : axisTop, | |
opposite = this.opposite, | |
offset = this.offset, | |
xOption = axisTitleOptions.x || 0, | |
yOption = axisTitleOptions.y || 0, | |
fontSize = this.chart.renderer.fontMetrics( | |
axisTitleOptions.style && axisTitleOptions.style.fontSize, | |
this.axisTitle | |
).f, | |
// the position in the length direction of the axis | |
alongAxis = { | |
low: margin + (horiz ? 0 : axisLength), | |
middle: margin + axisLength / 2, | |
high: margin + (horiz ? axisLength : 0) | |
}[axisTitleOptions.align], | |
// the position in the perpendicular direction of the axis | |
offAxis = (horiz ? axisTop + this.height : axisLeft) + | |
(horiz ? 1 : -1) * // horizontal axis reverses the margin | |
(opposite ? -1 : 1) * // so does opposite axes | |
this.axisTitleMargin + | |
(this.side === 2 ? fontSize : 0); | |
return { | |
x: horiz ? | |
alongAxis + xOption : offAxis + (opposite ? this.width : 0) + offset + xOption, | |
y: horiz ? | |
offAxis + yOption - (opposite ? this.height : 0) + offset : alongAxis + yOption | |
}; | |
}, | |
/** | |
* Render a minor tick into the given position. If a minor tick already | |
* exists in this position, move it. | |
* | |
* @param {number} pos | |
* The position in axis values. | |
*/ | |
renderMinorTick: function(pos) { | |
var slideInTicks = this.chart.hasRendered && isNumber(this.oldMin), | |
minorTicks = this.minorTicks; | |
if (!minorTicks[pos]) { | |
minorTicks[pos] = new Tick(this, pos, 'minor'); | |
} | |
// Render new ticks in old position | |
if (slideInTicks && minorTicks[pos].isNew) { | |
minorTicks[pos].render(null, true); | |
} | |
minorTicks[pos].render(null, false, 1); | |
}, | |
/** | |
* Render a major tick into the given position. If a tick already exists | |
* in this position, move it. | |
* | |
* @param {number} pos | |
* The position in axis values. | |
* @param {number} i | |
* The tick index. | |
*/ | |
renderTick: function(pos, i) { | |
var isLinked = this.isLinked, | |
ticks = this.ticks, | |
slideInTicks = this.chart.hasRendered && isNumber(this.oldMin); | |
// Linked axes need an extra check to find out if | |
if (!isLinked || (pos >= this.min && pos <= this.max)) { | |
if (!ticks[pos]) { | |
ticks[pos] = new Tick(this, pos); | |
} | |
// render new ticks in old position | |
if (slideInTicks && ticks[pos].isNew) { | |
ticks[pos].render(i, true, 0.1); | |
} | |
ticks[pos].render(i); | |
} | |
}, | |
/** | |
* Render the axis. | |
* | |
* @private | |
*/ | |
render: function() { | |
var axis = this, | |
chart = axis.chart, | |
renderer = chart.renderer, | |
options = axis.options, | |
isLog = axis.isLog, | |
lin2log = axis.lin2log, | |
isLinked = axis.isLinked, | |
tickPositions = axis.tickPositions, | |
axisTitle = axis.axisTitle, | |
ticks = axis.ticks, | |
minorTicks = axis.minorTicks, | |
alternateBands = axis.alternateBands, | |
stackLabelOptions = options.stackLabels, | |
alternateGridColor = options.alternateGridColor, | |
tickmarkOffset = axis.tickmarkOffset, | |
axisLine = axis.axisLine, | |
showAxis = axis.showAxis, | |
animation = animObject(renderer.globalAnimation), | |
from, | |
to; | |
// Reset | |
axis.labelEdge.length = 0; | |
//axis.justifyToPlot = overflow === 'justify'; | |
axis.overlap = false; | |
// Mark all elements inActive before we go over and mark the active ones | |
each([ticks, minorTicks, alternateBands], function(coll) { | |
objectEach(coll, function(tick) { | |
tick.isActive = false; | |
}); | |
}); | |
// If the series has data draw the ticks. Else only the line and title | |
if (axis.hasData() || isLinked) { | |
// minor ticks | |
if (axis.minorTickInterval && !axis.categories) { | |
each(axis.getMinorTickPositions(), function(pos) { | |
axis.renderMinorTick(pos); | |
}); | |
} | |
// Major ticks. Pull out the first item and render it last so that | |
// we can get the position of the neighbour label. #808. | |
if (tickPositions.length) { // #1300 | |
each(tickPositions, function(pos, i) { | |
axis.renderTick(pos, i); | |
}); | |
// In a categorized axis, the tick marks are displayed between labels. So | |
// we need to add a tick mark and grid line at the left edge of the X axis. | |
if (tickmarkOffset && (axis.min === 0 || axis.single)) { | |
if (!ticks[-1]) { | |
ticks[-1] = new Tick(axis, -1, null, true); | |
} | |
ticks[-1].render(-1); | |
} | |
} | |
// alternate grid color | |
if (alternateGridColor) { | |
each(tickPositions, function(pos, i) { | |
to = tickPositions[i + 1] !== undefined ? tickPositions[i + 1] + tickmarkOffset : axis.max - tickmarkOffset; | |
if (i % 2 === 0 && pos < axis.max && to <= axis.max + (chart.polar ? -tickmarkOffset : tickmarkOffset)) { // #2248, #4660 | |
if (!alternateBands[pos]) { | |
alternateBands[pos] = new H.PlotLineOrBand(axis); | |
} | |
from = pos + tickmarkOffset; // #949 | |
alternateBands[pos].options = { | |
from: isLog ? lin2log(from) : from, | |
to: isLog ? lin2log(to) : to, | |
color: alternateGridColor | |
}; | |
alternateBands[pos].render(); | |
alternateBands[pos].isActive = true; | |
} | |
}); | |
} | |
// custom plot lines and bands | |
if (!axis._addedPlotLB) { // only first time | |
each((options.plotLines || []).concat(options.plotBands || []), function(plotLineOptions) { | |
axis.addPlotBandOrLine(plotLineOptions); | |
}); | |
axis._addedPlotLB = true; | |
} | |
} // end if hasData | |
// Remove inactive ticks | |
each([ticks, minorTicks, alternateBands], function(coll) { | |
var i, | |
forDestruction = [], | |
delay = animation.duration, | |
destroyInactiveItems = function() { | |
i = forDestruction.length; | |
while (i--) { | |
// When resizing rapidly, the same items may be destroyed in different timeouts, | |
// or the may be reactivated | |
if (coll[forDestruction[i]] && !coll[forDestruction[i]].isActive) { | |
coll[forDestruction[i]].destroy(); | |
delete coll[forDestruction[i]]; | |
} | |
} | |
}; | |
objectEach(coll, function(tick, pos) { | |
if (!tick.isActive) { | |
// Render to zero opacity | |
tick.render(pos, false, 0); | |
tick.isActive = false; | |
forDestruction.push(pos); | |
} | |
}); | |
// When the objects are finished fading out, destroy them | |
syncTimeout( | |
destroyInactiveItems, | |
coll === alternateBands || !chart.hasRendered || !delay ? 0 : delay | |
); | |
}); | |
// Set the axis line path | |
if (axisLine) { | |
axisLine[axisLine.isPlaced ? 'animate' : 'attr']({ | |
d: this.getLinePath(axisLine.strokeWidth()) | |
}); | |
axisLine.isPlaced = true; | |
// Show or hide the line depending on options.showEmpty | |
axisLine[showAxis ? 'show' : 'hide'](true); | |
} | |
if (axisTitle && showAxis) { | |
var titleXy = axis.getTitlePosition(); | |
if (isNumber(titleXy.y)) { | |
axisTitle[axisTitle.isNew ? 'attr' : 'animate'](titleXy); | |
axisTitle.isNew = false; | |
} else { | |
axisTitle.attr('y', -9999); | |
axisTitle.isNew = true; | |
} | |
} | |
// Stacked totals: | |
if (stackLabelOptions && stackLabelOptions.enabled) { | |
axis.renderStackTotals(); | |
} | |
// End stacked totals | |
axis.isDirty = false; | |
}, | |
/** | |
* Redraw the axis to reflect changes in the data or axis extremes. Called | |
* internally from {@link Chart#redraw}. | |
* | |
* @private | |
*/ | |
redraw: function() { | |
if (this.visible) { | |
// render the axis | |
this.render(); | |
// move plot lines and bands | |
each(this.plotLinesAndBands, function(plotLine) { | |
plotLine.render(); | |
}); | |
} | |
// mark associated series as dirty and ready for redraw | |
each(this.series, function(series) { | |
series.isDirty = true; | |
}); | |
}, | |
// Properties to survive after destroy, needed for Axis.update (#4317, | |
// #5773, #5881). | |
keepProps: ['extKey', 'hcEvents', 'names', 'series', 'userMax', 'userMin'], | |
/** | |
* Destroys an Axis instance. See {@link Axis#remove} for the API endpoint | |
* to fully remove the axis. | |
* | |
* @private | |
* @param {Boolean} keepEvents | |
* Whether to preserve events, used internally in Axis.update. | |
*/ | |
destroy: function(keepEvents) { | |
var axis = this, | |
stacks = axis.stacks, | |
plotLinesAndBands = axis.plotLinesAndBands, | |
plotGroup, | |
i; | |
// Remove the events | |
if (!keepEvents) { | |
removeEvent(axis); | |
} | |
// Destroy each stack total | |
objectEach(stacks, function(stack, stackKey) { | |
destroyObjectProperties(stack); | |
stacks[stackKey] = null; | |
}); | |
// Destroy collections | |
each([axis.ticks, axis.minorTicks, axis.alternateBands], function(coll) { | |
destroyObjectProperties(coll); | |
}); | |
if (plotLinesAndBands) { | |
i = plotLinesAndBands.length; | |
while (i--) { // #1975 | |
plotLinesAndBands[i].destroy(); | |
} | |
} | |
// Destroy local variables | |
each(['stackTotalGroup', 'axisLine', 'axisTitle', 'axisGroup', 'gridGroup', 'labelGroup', 'cross'], function(prop) { | |
if (axis[prop]) { | |
axis[prop] = axis[prop].destroy(); | |
} | |
}); | |
// Destroy each generated group for plotlines and plotbands | |
for (plotGroup in axis.plotLinesAndBandsGroups) { | |
axis.plotLinesAndBandsGroups[plotGroup] = axis.plotLinesAndBandsGroups[plotGroup].destroy(); | |
} | |
// Delete all properties and fall back to the prototype. | |
objectEach(axis, function(val, key) { | |
if (inArray(key, axis.keepProps) === -1) { | |
delete axis[key]; | |
} | |
}); | |
}, | |
/** | |
* Internal function to draw a crosshair. | |
* | |
* @param {PointerEvent} [e] | |
* The event arguments from the modified pointer event, extended | |
* with `chartX` and `chartY` | |
* @param {Point} [point] | |
* The Point object if the crosshair snaps to points. | |
*/ | |
drawCrosshair: function(e, point) { | |
var path, | |
options = this.crosshair, | |
snap = pick(options.snap, true), | |
pos, | |
categorized, | |
graphic = this.cross; | |
// Use last available event when updating non-snapped crosshairs without | |
// mouse interaction (#5287) | |
if (!e) { | |
e = this.cross && this.cross.e; | |
} | |
if ( | |
// Disabled in options | |
!this.crosshair || | |
// Snap | |
((defined(point) || !snap) === false) | |
) { | |
this.hideCrosshair(); | |
} else { | |
// Get the path | |
if (!snap) { | |
pos = e && (this.horiz ? e.chartX - this.pos : this.len - e.chartY + this.pos); | |
} else if (defined(point)) { | |
pos = this.isXAxis ? point.plotX : this.len - point.plotY; // #3834 | |
} | |
if (defined(pos)) { | |
path = this.getPlotLinePath( | |
// First argument, value, only used on radial | |
point && (this.isXAxis ? point.x : pick(point.stackY, point.y)), | |
null, | |
null, | |
null, | |
pos // Translated position | |
) || null; // #3189 | |
} | |
if (!defined(path)) { | |
this.hideCrosshair(); | |
return; | |
} | |
categorized = this.categories && !this.isRadial; | |
// Draw the cross | |
if (!graphic) { | |
this.cross = graphic = this.chart.renderer | |
.path() | |
.addClass('highcharts-crosshair highcharts-crosshair-' + | |
(categorized ? 'category ' : 'thin ') + options.className) | |
.attr({ | |
zIndex: pick(options.zIndex, 2) | |
}) | |
.add(); | |
} | |
graphic.show().attr({ | |
d: path | |
}); | |
if (categorized && !options.width) { | |
graphic.attr({ | |
'stroke-width': this.transA | |
}); | |
} | |
this.cross.e = e; | |
} | |
}, | |
/** | |
* Hide the crosshair if visible. | |
*/ | |
hideCrosshair: function() { | |
if (this.cross) { | |
this.cross.hide(); | |
} | |
} | |
}); // end Axis | |
H.Axis = Axis; | |
return Axis; | |
}(Highcharts)); | |
(function(Highcharts) { | |
/** | |
* (c) 2010-2017 Torstein Honsi | |
* | |
* License: www.highcharts.com/license | |
*/ | |
var H = Highcharts, | |
addEvent = H.addEvent, | |
css = H.css, | |
discardElement = H.discardElement, | |
defined = H.defined, | |
each = H.each, | |
isFirefox = H.isFirefox, | |
marginNames = H.marginNames, | |
merge = H.merge, | |
pick = H.pick, | |
setAnimation = H.setAnimation, | |
stableSort = H.stableSort, | |
win = H.win, | |
wrap = H.wrap; | |
/** | |
* The overview of the chart's series. The legend object is instanciated | |
* internally in the chart constructor, and available from `chart.legend`. Each | |
* chart has only one legend. | |
* | |
* @class | |
*/ | |
Highcharts.Legend = function(chart, options) { | |
this.init(chart, options); | |
}; | |
Highcharts.Legend.prototype = { | |
/** | |
* Initialize the legend | |
*/ | |
init: function(chart, options) { | |
this.chart = chart; | |
this.setOptions(options); | |
if (options.enabled) { | |
// Render it | |
this.render(); | |
// move checkboxes | |
addEvent(this.chart, 'endResize', function() { | |
this.legend.positionCheckboxes(); | |
}); | |
} | |
}, | |
setOptions: function(options) { | |
var padding = pick(options.padding, 8); | |
this.options = options; | |
this.itemMarginTop = options.itemMarginTop || 0; | |
this.padding = padding; | |
this.initialItemY = padding - 5; // 5 is pixels above the text | |
this.maxItemWidth = 0; | |
this.itemHeight = 0; | |
this.symbolWidth = pick(options.symbolWidth, 16); | |
this.pages = []; | |
}, | |
/** | |
* Update the legend with new options. Equivalent to running `chart.update` | |
* with a legend configuration option. | |
* @param {LegendOptions} options | |
* Legend options. | |
* @param {Boolean} [redraw=true] | |
* Whether to redraw the chart. | |
* | |
* @sample highcharts/legend/legend-update/ | |
* Legend update | |
*/ | |
update: function(options, redraw) { | |
var chart = this.chart; | |
this.setOptions(merge(true, this.options, options)); | |
this.destroy(); | |
chart.isDirtyLegend = chart.isDirtyBox = true; | |
if (pick(redraw, true)) { | |
chart.redraw(); | |
} | |
}, | |
/** | |
* Set the colors for the legend item | |
* @param {Object} item A Series or Point instance | |
* @param {Object} visible Dimmed or colored | |
*/ | |
colorizeItem: function(item, visible) { | |
item.legendGroup[visible ? 'removeClass' : 'addClass']( | |
'highcharts-legend-item-hidden' | |
); | |
}, | |
/** | |
* Position the legend item | |
* @param {Object} item A Series or Point instance | |
*/ | |
positionItem: function(item) { | |
var legend = this, | |
options = legend.options, | |
symbolPadding = options.symbolPadding, | |
ltr = !options.rtl, | |
legendItemPos = item._legendItemPos, | |
itemX = legendItemPos[0], | |
itemY = legendItemPos[1], | |
checkbox = item.checkbox, | |
legendGroup = item.legendGroup; | |
if (legendGroup && legendGroup.element) { | |
legendGroup.translate( | |
ltr ? | |
itemX : | |
legend.legendWidth - itemX - 2 * symbolPadding - 4, | |
itemY | |
); | |
} | |
if (checkbox) { | |
checkbox.x = itemX; | |
checkbox.y = itemY; | |
} | |
}, | |
/** | |
* Destroy a single legend item | |
* @param {Object} item The series or point | |
*/ | |
destroyItem: function(item) { | |
var checkbox = item.checkbox; | |
// destroy SVG elements | |
each( | |
['legendItem', 'legendLine', 'legendSymbol', 'legendGroup'], | |
function(key) { | |
if (item[key]) { | |
item[key] = item[key].destroy(); | |
} | |
} | |
); | |
if (checkbox) { | |
discardElement(item.checkbox); | |
} | |
}, | |
/** | |
* Destroys the legend. | |
*/ | |
destroy: function() { | |
function destroyItems(key) { | |
if (this[key]) { | |
this[key] = this[key].destroy(); | |
} | |
} | |
// Destroy items | |
each(this.getAllItems(), function(item) { | |
each(['legendItem', 'legendGroup'], destroyItems, item); | |
}); | |
// Destroy legend elements | |
each([ | |
'clipRect', | |
'up', | |
'down', | |
'pager', | |
'nav', | |
'box', | |
'title', | |
'group' | |
], destroyItems, this); | |
this.display = null; // Reset in .render on update. | |
}, | |
/** | |
* Position the checkboxes after the width is determined | |
*/ | |
positionCheckboxes: function(scrollOffset) { | |
var alignAttr = this.group && this.group.alignAttr, | |
translateY, | |
clipHeight = this.clipHeight || this.legendHeight, | |
titleHeight = this.titleHeight; | |
if (alignAttr) { | |
translateY = alignAttr.translateY; | |
each(this.allItems, function(item) { | |
var checkbox = item.checkbox, | |
top; | |
if (checkbox) { | |
top = translateY + titleHeight + checkbox.y + | |
(scrollOffset || 0) + 3; | |
css(checkbox, { | |
left: (alignAttr.translateX + item.checkboxOffset + | |
checkbox.x - 20) + 'px', | |
top: top + 'px', | |
display: top > translateY - 6 && top < translateY + | |
clipHeight - 6 ? '' : 'none' | |
}); | |
} | |
}); | |
} | |
}, | |
/** | |
* Render the legend title on top of the legend | |
*/ | |
renderTitle: function() { | |
var options = this.options, | |
padding = this.padding, | |
titleOptions = options.title, | |
titleHeight = 0, | |
bBox; | |
if (titleOptions.text) { | |
if (!this.title) { | |
this.title = this.chart.renderer.label( | |
titleOptions.text, | |
padding - 3, | |
padding - 4, | |
null, | |
null, | |
null, | |
options.useHTML, | |
null, | |
'legend-title' | |
) | |
.attr({ | |
zIndex: 1 | |
}) | |
.add(this.group); | |
} | |
bBox = this.title.getBBox(); | |
titleHeight = bBox.height; | |
this.offsetWidth = bBox.width; // #1717 | |
this.contentGroup.attr({ | |
translateY: titleHeight | |
}); | |
} | |
this.titleHeight = titleHeight; | |
}, | |
/** | |
* Set the legend item text | |
*/ | |
setText: function(item) { | |
var options = this.options; | |
item.legendItem.attr({ | |
text: options.labelFormat ? | |
H.format(options.labelFormat, item) : options.labelFormatter.call(item) | |
}); | |
}, | |
/** | |
* Render a single specific legend item | |
* @param {Object} item A series or point | |
*/ | |
renderItem: function(item) { | |
var legend = this, | |
chart = legend.chart, | |
renderer = chart.renderer, | |
options = legend.options, | |
horizontal = options.layout === 'horizontal', | |
symbolWidth = legend.symbolWidth, | |
symbolPadding = options.symbolPadding, | |
padding = legend.padding, | |
itemDistance = horizontal ? pick(options.itemDistance, 20) : 0, | |
ltr = !options.rtl, | |
itemHeight, | |
widthOption = options.width, | |
itemMarginBottom = options.itemMarginBottom || 0, | |
itemMarginTop = legend.itemMarginTop, | |
bBox, | |
itemWidth, | |
li = item.legendItem, | |
isSeries = !item.series, | |
series = !isSeries && item.series.drawLegendSymbol ? | |
item.series : | |
item, | |
seriesOptions = series.options, | |
showCheckbox = legend.createCheckboxForItem && | |
seriesOptions && | |
seriesOptions.showCheckbox, | |
// full width minus text width | |
itemExtraWidth = symbolWidth + symbolPadding + itemDistance + | |
(showCheckbox ? 20 : 0), | |
useHTML = options.useHTML, | |
fontSize = 12, | |
itemClassName = item.options.className; | |
if (!li) { // generate it once, later move it | |
// Generate the group box, a group to hold the symbol and text. Text | |
// is to be appended in Legend class. | |
item.legendGroup = renderer.g('legend-item') | |
.addClass( | |
'highcharts-' + series.type + '-series ' + | |
'highcharts-color-' + item.colorIndex + | |
(itemClassName ? ' ' + itemClassName : '') + | |
(isSeries ? ' highcharts-series-' + item.index : '') | |
) | |
.attr({ | |
zIndex: 1 | |
}) | |
.add(legend.scrollGroup); | |
// Generate the list item text and add it to the group | |
item.legendItem = li = renderer.text( | |
'', | |
ltr ? symbolWidth + symbolPadding : -symbolPadding, | |
legend.baseline || 0, | |
useHTML | |
) | |
.attr({ | |
align: ltr ? 'left' : 'right', | |
zIndex: 2 | |
}) | |
.add(item.legendGroup); | |
// Get the baseline for the first item - the font size is equal for | |
// all | |
if (!legend.baseline) { | |
legend.fontMetrics = renderer.fontMetrics( | |
fontSize, | |
li | |
); | |
legend.baseline = legend.fontMetrics.f + 3 + itemMarginTop; | |
li.attr('y', legend.baseline); | |
} | |
// Draw the legend symbol inside the group box | |
legend.symbolHeight = options.symbolHeight || legend.fontMetrics.f; | |
series.drawLegendSymbol(legend, item); | |
if (legend.setItemEvents) { | |
legend.setItemEvents(item, li, useHTML); | |
} | |
// add the HTML checkbox on top | |
if (showCheckbox) { | |
legend.createCheckboxForItem(item); | |
} | |
} | |
// Colorize the items | |
legend.colorizeItem(item, item.visible); | |
// Take care of max width and text overflow (#6659) | |
li.css({ | |
width: (options.itemWidth || chart.spacingBox.width) - | |
itemExtraWidth | |
}); | |
// Always update the text | |
legend.setText(item); | |
// calculate the positions for the next line | |
bBox = li.getBBox(); | |
itemWidth = item.checkboxOffset = | |
options.itemWidth || | |
item.legendItemWidth || | |
bBox.width + itemExtraWidth; | |
legend.itemHeight = itemHeight = Math.round( | |
item.legendItemHeight || bBox.height || legend.symbolHeight | |
); | |
// If the item exceeds the width, start a new line | |
if ( | |
horizontal && | |
legend.itemX - padding + itemWidth > ( | |
widthOption || ( | |
chart.spacingBox.width - 2 * padding - options.x | |
) | |
) | |
) { | |
legend.itemX = padding; | |
legend.itemY += itemMarginTop + legend.lastLineHeight + | |
itemMarginBottom; | |
legend.lastLineHeight = 0; // reset for next line (#915, #3976) | |
} | |
// If the item exceeds the height, start a new column | |
/*if (!horizontal && legend.itemY + options.y + | |
itemHeight > chart.chartHeight - spacingTop - spacingBottom) { | |
legend.itemY = legend.initialItemY; | |
legend.itemX += legend.maxItemWidth; | |
legend.maxItemWidth = 0; | |
}*/ | |
// Set the edge positions | |
legend.maxItemWidth = Math.max(legend.maxItemWidth, itemWidth); | |
legend.lastItemY = itemMarginTop + legend.itemY + itemMarginBottom; | |
legend.lastLineHeight = Math.max( // #915 | |
itemHeight, | |
legend.lastLineHeight | |
); | |
// cache the position of the newly generated or reordered items | |
item._legendItemPos = [legend.itemX, legend.itemY]; | |
// advance | |
if (horizontal) { | |
legend.itemX += itemWidth; | |
} else { | |
legend.itemY += itemMarginTop + itemHeight + itemMarginBottom; | |
legend.lastLineHeight = itemHeight; | |
} | |
// the width of the widest item | |
legend.offsetWidth = widthOption || Math.max( | |
( | |
horizontal ? legend.itemX - padding - (item.checkbox ? | |
// decrease by itemDistance only when no checkbox #4853 | |
0 : | |
itemDistance | |
) : itemWidth | |
) + padding, | |
legend.offsetWidth | |
); | |
}, | |
/** | |
* Get all items, which is one item per series for normal series and one | |
* item per point for pie series. | |
*/ | |
getAllItems: function() { | |
var allItems = []; | |
each(this.chart.series, function(series) { | |
var seriesOptions = series && series.options; | |
// Handle showInLegend. If the series is linked to another series, | |
// defaults to false. | |
if (series && pick( | |
seriesOptions.showInLegend, !defined(seriesOptions.linkedTo) ? undefined : false, true | |
)) { | |
// Use points or series for the legend item depending on | |
// legendType | |
allItems = allItems.concat( | |
series.legendItems || | |
( | |
seriesOptions.legendType === 'point' ? | |
series.data : | |
series | |
) | |
); | |
} | |
}); | |
return allItems; | |
}, | |
/** | |
* Adjust the chart margins by reserving space for the legend on only one | |
* side of the chart. If the position is set to a corner, top or bottom is | |
* reserved for horizontal legends and left or right for vertical ones. | |
*/ | |
adjustMargins: function(margin, spacing) { | |
var chart = this.chart, | |
options = this.options, | |
// Use the first letter of each alignment option in order to detect | |
// the side. (#4189 - use charAt(x) notation instead of [x] for IE7) | |
alignment = options.align.charAt(0) + | |
options.verticalAlign.charAt(0) + | |
options.layout.charAt(0); | |
if (!options.floating) { | |
each([ | |
/(lth|ct|rth)/, | |
/(rtv|rm|rbv)/, | |
/(rbh|cb|lbh)/, | |
/(lbv|lm|ltv)/ | |
], function(alignments, side) { | |
if (alignments.test(alignment) && !defined(margin[side])) { | |
// Now we have detected on which side of the chart we should | |
// reserve space for the legend | |
chart[marginNames[side]] = Math.max( | |
chart[marginNames[side]], | |
( | |
chart.legend[ | |
(side + 1) % 2 ? 'legendHeight' : 'legendWidth' | |
] + [1, -1, -1, 1][side] * options[ | |
(side % 2) ? 'x' : 'y' | |
] + | |
pick(options.margin, 12) + | |
spacing[side] | |
) | |
); | |
} | |
}); | |
} | |
}, | |
/** | |
* Render the legend. This method can be called both before and after | |
* chart.render. If called after, it will only rearrange items instead | |
* of creating new ones. | |
*/ | |
render: function() { | |
var legend = this, | |
chart = legend.chart, | |
renderer = chart.renderer, | |
legendGroup = legend.group, | |
allItems, | |
display, | |
legendWidth, | |
legendHeight, | |
box = legend.box, | |
options = legend.options, | |
padding = legend.padding; | |
legend.itemX = padding; | |
legend.itemY = legend.initialItemY; | |
legend.offsetWidth = 0; | |
legend.lastItemY = 0; | |
if (!legendGroup) { | |
legend.group = legendGroup = renderer.g('legend') | |
.attr({ | |
zIndex: 7 | |
}) | |
.add(); | |
legend.contentGroup = renderer.g() | |
.attr({ | |
zIndex: 1 | |
}) // above background | |
.add(legendGroup); | |
legend.scrollGroup = renderer.g() | |
.add(legend.contentGroup); | |
} | |
legend.renderTitle(); | |
// add each series or point | |
allItems = legend.getAllItems(); | |
// sort by legendIndex | |
stableSort(allItems, function(a, b) { | |
return ((a.options && a.options.legendIndex) || 0) - | |
((b.options && b.options.legendIndex) || 0); | |
}); | |
// reversed legend | |
if (options.reversed) { | |
allItems.reverse(); | |
} | |
legend.allItems = allItems; | |
legend.display = display = !!allItems.length; | |
// render the items | |
legend.lastLineHeight = 0; | |
each(allItems, function(item) { | |
legend.renderItem(item); | |
}); | |
// Get the box | |
legendWidth = (options.width || legend.offsetWidth) + padding; | |
legendHeight = legend.lastItemY + legend.lastLineHeight + | |
legend.titleHeight; | |
legendHeight = legend.handleOverflow(legendHeight); | |
legendHeight += padding; | |
// Draw the border and/or background | |
if (!box) { | |
legend.box = box = renderer.rect() | |
.addClass('highcharts-legend-box') | |
.attr({ | |
r: options.borderRadius | |
}) | |
.add(legendGroup); | |
box.isNew = true; | |
} | |
if (legendWidth > 0 && legendHeight > 0) { | |
box[box.isNew ? 'attr' : 'animate']( | |
box.crisp({ | |
x: 0, | |
y: 0, | |
width: legendWidth, | |
height: legendHeight | |
}, box.strokeWidth()) | |
); | |
box.isNew = false; | |
} | |
// hide the border if no items | |
box[display ? 'show' : 'hide'](); | |
// Open for responsiveness | |
if (legendGroup.getStyle('display') === 'none') { | |
legendWidth = legendHeight = 0; | |
} | |
legend.legendWidth = legendWidth; | |
legend.legendHeight = legendHeight; | |
// Now that the legend width and height are established, put the items | |
// in the final position | |
each(allItems, function(item) { | |
legend.positionItem(item); | |
}); | |
// 1.x compatibility: positioning based on style | |
/*var props = ['left', 'right', 'top', 'bottom'], | |
prop, | |
i = 4; | |
while (i--) { | |
prop = props[i]; | |
if (options.style[prop] && options.style[prop] !== 'auto') { | |
options[i < 2 ? 'align' : 'verticalAlign'] = prop; | |
options[i < 2 ? 'x' : 'y'] = | |
pInt(options.style[prop]) * (i % 2 ? -1 : 1); | |
} | |
}*/ | |
if (display) { | |
legendGroup.align(merge(options, { | |
width: legendWidth, | |
height: legendHeight | |
}), true, 'spacingBox'); | |
} | |
if (!chart.isResizing) { | |
this.positionCheckboxes(); | |
} | |
}, | |
/** | |
* Set up the overflow handling by adding navigation with up and down arrows | |
* below the legend. | |
*/ | |
handleOverflow: function(legendHeight) { | |
var legend = this, | |
chart = this.chart, | |
renderer = chart.renderer, | |
options = this.options, | |
optionsY = options.y, | |
alignTop = options.verticalAlign === 'top', | |
padding = this.padding, | |
spaceHeight = chart.spacingBox.height + | |
(alignTop ? -optionsY : optionsY) - padding, | |
maxHeight = options.maxHeight, | |
clipHeight, | |
clipRect = this.clipRect, | |
navOptions = options.navigation, | |
animation = pick(navOptions.animation, true), | |
arrowSize = navOptions.arrowSize || 12, | |
nav = this.nav, | |
pages = this.pages, | |
lastY, | |
allItems = this.allItems, | |
clipToHeight = function(height) { | |
if (typeof height === 'number') { | |
clipRect.attr({ | |
height: height | |
}); | |
} else if (clipRect) { // Reset (#5912) | |
legend.clipRect = clipRect.destroy(); | |
legend.contentGroup.clip(); | |
} | |
// useHTML | |
if (legend.contentGroup.div) { | |
legend.contentGroup.div.style.clip = height ? | |
'rect(' + padding + 'px,9999px,' + | |
(padding + height) + 'px,0)' : | |
'auto'; | |
} | |
}; | |
// Adjust the height | |
if ( | |
options.layout === 'horizontal' && | |
options.verticalAlign !== 'middle' && | |
!options.floating | |
) { | |
spaceHeight /= 2; | |
} | |
if (maxHeight) { | |
spaceHeight = Math.min(spaceHeight, maxHeight); | |
} | |
// Reset the legend height and adjust the clipping rectangle | |
pages.length = 0; | |
if (legendHeight > spaceHeight && navOptions.enabled !== false) { | |
this.clipHeight = clipHeight = | |
Math.max(spaceHeight - 20 - this.titleHeight - padding, 0); | |
this.currentPage = pick(this.currentPage, 1); | |
this.fullHeight = legendHeight; | |
// Fill pages with Y positions so that the top of each a legend item | |
// defines the scroll top for each page (#2098) | |
each(allItems, function(item, i) { | |
var y = item._legendItemPos[1], | |
h = Math.round(item.legendItem.getBBox().height), | |
len = pages.length; | |
if (!len || (y - pages[len - 1] > clipHeight && | |
(lastY || y) !== pages[len - 1])) { | |
pages.push(lastY || y); | |
len++; | |
} | |
if (i === allItems.length - 1 && | |
y + h - pages[len - 1] > clipHeight) { | |
pages.push(y); | |
} | |
if (y !== lastY) { | |
lastY = y; | |
} | |
}); | |
// Only apply clipping if needed. Clipping causes blurred legend in | |
// PDF export (#1787) | |
if (!clipRect) { | |
clipRect = legend.clipRect = | |
renderer.clipRect(0, padding, 9999, 0); | |
legend.contentGroup.clip(clipRect); | |
} | |
clipToHeight(clipHeight); | |
// Add navigation elements | |
if (!nav) { | |
this.nav = nav = renderer.g() | |
.attr({ | |
zIndex: 1 | |
}) | |
.add(this.group); | |
this.up = renderer | |
.symbol( | |
'triangle', | |
0, | |
0, | |
arrowSize, | |
arrowSize | |
) | |
.on('click', function() { | |
legend.scroll(-1, animation); | |
}) | |
.add(nav); | |
this.pager = renderer.text('', 15, 10) | |
.addClass('highcharts-legend-navigation') | |
.add(nav); | |
this.down = renderer | |
.symbol( | |
'triangle-down', | |
0, | |
0, | |
arrowSize, | |
arrowSize | |
) | |
.on('click', function() { | |
legend.scroll(1, animation); | |
}) | |
.add(nav); | |
} | |
// Set initial position | |
legend.scroll(0); | |
legendHeight = spaceHeight; | |
// Reset | |
} else if (nav) { | |
clipToHeight(); | |
this.nav = nav.destroy(); // #6322 | |
this.scrollGroup.attr({ | |
translateY: 1 | |
}); | |
this.clipHeight = 0; // #1379 | |
} | |
return legendHeight; | |
}, | |
/** | |
* Scroll the legend by a number of pages | |
* @param {Object} scrollBy | |
* @param {Object} animation | |
*/ | |
scroll: function(scrollBy, animation) { | |
var pages = this.pages, | |
pageCount = pages.length, | |
currentPage = this.currentPage + scrollBy, | |
clipHeight = this.clipHeight, | |
navOptions = this.options.navigation, | |
pager = this.pager, | |
padding = this.padding, | |
scrollOffset; | |
// When resizing while looking at the last page | |
if (currentPage > pageCount) { | |
currentPage = pageCount; | |
} | |
if (currentPage > 0) { | |
if (animation !== undefined) { | |
setAnimation(animation, this.chart); | |
} | |
this.nav.attr({ | |
translateX: padding, | |
translateY: clipHeight + this.padding + 7 + this.titleHeight, | |
visibility: 'visible' | |
}); | |
this.up.attr({ | |
'class': currentPage === 1 ? | |
'highcharts-legend-nav-inactive' : 'highcharts-legend-nav-active' | |
}); | |
pager.attr({ | |
text: currentPage + '/' + pageCount | |
}); | |
this.down.attr({ | |
'x': 18 + this.pager.getBBox().width, // adjust to text width | |
'class': currentPage === pageCount ? | |
'highcharts-legend-nav-inactive' : 'highcharts-legend-nav-active' | |
}); | |
scrollOffset = -pages[currentPage - 1] + this.initialItemY; | |
this.scrollGroup.animate({ | |
translateY: scrollOffset | |
}); | |
this.currentPage = currentPage; | |
this.positionCheckboxes(scrollOffset); | |
} | |
} | |
}; | |
/* | |
* LegendSymbolMixin | |
*/ | |
H.LegendSymbolMixin = { | |
/** | |
* Get the series' symbol in the legend | |
* | |
* @param {Object} legend The legend object | |
* @param {Object} item The series (this) or point | |
*/ | |
drawRectangle: function(legend, item) { | |
var options = legend.options, | |
symbolHeight = legend.symbolHeight, | |
square = options.squareSymbol, | |
symbolWidth = square ? symbolHeight : legend.symbolWidth; | |
item.legendSymbol = this.chart.renderer.rect( | |
square ? (legend.symbolWidth - symbolHeight) / 2 : 0, | |
legend.baseline - symbolHeight + 1, // #3988 | |
symbolWidth, | |
symbolHeight, | |
pick(legend.options.symbolRadius, symbolHeight / 2) | |
) | |
.addClass('highcharts-point') | |
.attr({ | |
zIndex: 3 | |
}).add(item.legendGroup); | |
}, | |
/** | |
* Get the series' symbol in the legend. This method should be overridable | |
* to create custom symbols through | |
* Highcharts.seriesTypes[type].prototype.drawLegendSymbols. | |
* | |
* @param {Object} legend The legend object | |
*/ | |
drawLineMarker: function(legend) { | |
var options = this.options, | |
markerOptions = options.marker, | |
radius, | |
legendSymbol, | |
symbolWidth = legend.symbolWidth, | |
symbolHeight = legend.symbolHeight, | |
generalRadius = symbolHeight / 2, | |
renderer = this.chart.renderer, | |
legendItemGroup = this.legendGroup, | |
verticalCenter = legend.baseline - | |
Math.round(legend.fontMetrics.b * 0.3), | |
attr = {}; | |
// Draw the line | |
this.legendLine = renderer.path([ | |
'M', | |
0, | |
verticalCenter, | |
'L', | |
symbolWidth, | |
verticalCenter | |
]) | |
.addClass('highcharts-graph') | |
.attr(attr) | |
.add(legendItemGroup); | |
// Draw the marker | |
if (markerOptions && markerOptions.enabled !== false) { | |
// Do not allow the marker to be larger than the symbolHeight | |
radius = Math.min( | |
pick(markerOptions.radius, generalRadius), | |
generalRadius | |
); | |
// Restrict symbol markers size | |
if (this.symbol.indexOf('url') === 0) { | |
markerOptions = merge(markerOptions, { | |
width: symbolHeight, | |
height: symbolHeight | |
}); | |
radius = 0; | |
} | |
this.legendSymbol = legendSymbol = renderer.symbol( | |
this.symbol, | |
(symbolWidth / 2) - radius, | |
verticalCenter - radius, | |
2 * radius, | |
2 * radius, | |
markerOptions | |
) | |
.addClass('highcharts-point') | |
.add(legendItemGroup); | |
legendSymbol.isMarker = true; | |
} | |
} | |
}; | |
// Workaround for #2030, horizontal legend items not displaying in IE11 Preview, | |
// and for #2580, a similar drawing flaw in Firefox 26. | |
// Explore if there's a general cause for this. The problem may be related | |
// to nested group elements, as the legend item texts are within 4 group | |
// elements. | |
if (/Trident\/7\.0/.test(win.navigator.userAgent) || isFirefox) { | |
wrap(Highcharts.Legend.prototype, 'positionItem', function(proceed, item) { | |
var legend = this, | |
// If chart destroyed in sync, this is undefined (#2030) | |
runPositionItem = function() { | |
if (item._legendItemPos) { | |
proceed.call(legend, item); | |
} | |
}; | |
// Do it now, for export and to get checkbox placement | |
runPositionItem(); | |
// Do it after to work around the core issue | |
setTimeout(runPositionItem); | |
}); | |
} | |
}(Highcharts)); | |
(function(H) { | |
/** | |
* (c) 2010-2017 Torstein Honsi | |
* | |
* License: www.highcharts.com/license | |
*/ | |
var dateFormat = H.dateFormat, | |
each = H.each, | |
extend = H.extend, | |
format = H.format, | |
isNumber = H.isNumber, | |
map = H.map, | |
merge = H.merge, | |
pick = H.pick, | |
splat = H.splat, | |
syncTimeout = H.syncTimeout, | |
timeUnits = H.timeUnits; | |
/** | |
* The tooltip object | |
* @param {Object} chart The chart instance | |
* @param {Object} options Tooltip options | |
*/ | |
H.Tooltip = function() { | |
this.init.apply(this, arguments); | |
}; | |
H.Tooltip.prototype = { | |
init: function(chart, options) { | |
// Save the chart and options | |
this.chart = chart; | |
this.options = options; | |
// Keep track of the current series | |
//this.currentSeries = undefined; | |
// List of crosshairs | |
this.crosshairs = []; | |
// Current values of x and y when animating | |
this.now = { | |
x: 0, | |
y: 0 | |
}; | |
// The tooltip is initially hidden | |
this.isHidden = true; | |
// Public property for getting the shared state. | |
this.split = options.split && !chart.inverted; | |
this.shared = options.shared || this.split; | |
}, | |
/** | |
* Destroy the single tooltips in a split tooltip. | |
* If the tooltip is active then it is not destroyed, unless forced to. | |
* @param {boolean} force Force destroy all tooltips. | |
* @return {undefined} | |
*/ | |
cleanSplit: function(force) { | |
each(this.chart.series, function(series) { | |
var tt = series && series.tt; | |
if (tt) { | |
if (!tt.isActive || force) { | |
series.tt = tt.destroy(); | |
} else { | |
tt.isActive = false; | |
} | |
} | |
}); | |
}, | |
/** | |
* In styled mode, apply the default filter for the tooltip drop-shadow. It | |
* needs to have an id specific to the chart, otherwise there will be issues | |
* when one tooltip adopts the filter of a different chart, specifically one | |
* where the container is hidden. | |
*/ | |
applyFilter: function() { | |
var chart = this.chart; | |
chart.renderer.definition({ | |
tagName: 'filter', | |
id: 'drop-shadow-' + chart.index, | |
opacity: 0.5, | |
children: [{ | |
tagName: 'feGaussianBlur', | |
in: 'SourceAlpha', | |
stdDeviation: 1 | |
}, { | |
tagName: 'feOffset', | |
dx: 1, | |
dy: 1 | |
}, { | |
tagName: 'feComponentTransfer', | |
children: [{ | |
tagName: 'feFuncA', | |
type: 'linear', | |
slope: 0.3 | |
}] | |
}, { | |
tagName: 'feMerge', | |
children: [{ | |
tagName: 'feMergeNode' | |
}, { | |
tagName: 'feMergeNode', | |
in: 'SourceGraphic' | |
}] | |
}] | |
}); | |
chart.renderer.definition({ | |
tagName: 'style', | |
textContent: '.highcharts-tooltip-' + chart.index + '{' + | |
'filter:url(#drop-shadow-' + chart.index + ')' + | |
'}' | |
}); | |
}, | |
/** | |
* Create the Tooltip label element if it doesn't exist, then return the | |
* label. | |
*/ | |
getLabel: function() { | |
var renderer = this.chart.renderer, | |
options = this.options; | |
if (!this.label) { | |
// Create the label | |
if (this.split) { | |
this.label = renderer.g('tooltip'); | |
} else { | |
this.label = renderer.label( | |
'', | |
0, | |
0, | |
options.shape || 'callout', | |
null, | |
null, | |
options.useHTML, | |
null, | |
'tooltip' | |
) | |
.attr({ | |
padding: options.padding, | |
r: options.borderRadius | |
}); | |
} | |
// Apply the drop-shadow filter | |
this.applyFilter(); | |
this.label.addClass('highcharts-tooltip-' + this.chart.index); | |
this.label | |
.attr({ | |
zIndex: 8 | |
}) | |
.add(); | |
} | |
return this.label; | |
}, | |
update: function(options) { | |
this.destroy(); | |
// Update user options (#6218) | |
merge(true, this.chart.options.tooltip.userOptions, options); | |
this.init(this.chart, merge(true, this.options, options)); | |
}, | |
/** | |
* Destroy the tooltip and its elements. | |
*/ | |
destroy: function() { | |
// Destroy and clear local variables | |
if (this.label) { | |
this.label = this.label.destroy(); | |
} | |
if (this.split && this.tt) { | |
this.cleanSplit(this.chart, true); | |
this.tt = this.tt.destroy(); | |
} | |
clearTimeout(this.hideTimer); | |
clearTimeout(this.tooltipTimeout); | |
}, | |
/** | |
* Provide a soft movement for the tooltip | |
* | |
* @param {Number} x | |
* @param {Number} y | |
* @private | |
*/ | |
move: function(x, y, anchorX, anchorY) { | |
var tooltip = this, | |
now = tooltip.now, | |
animate = tooltip.options.animation !== false && !tooltip.isHidden && | |
// When we get close to the target position, abort animation and land on the right place (#3056) | |
(Math.abs(x - now.x) > 1 || Math.abs(y - now.y) > 1), | |
skipAnchor = tooltip.followPointer || tooltip.len > 1; | |
// Get intermediate values for animation | |
extend(now, { | |
x: animate ? (2 * now.x + x) / 3 : x, | |
y: animate ? (now.y + y) / 2 : y, | |
anchorX: skipAnchor ? undefined : animate ? (2 * now.anchorX + anchorX) / 3 : anchorX, | |
anchorY: skipAnchor ? undefined : animate ? (now.anchorY + anchorY) / 2 : anchorY | |
}); | |
// Move to the intermediate value | |
tooltip.getLabel().attr(now); | |
// Run on next tick of the mouse tracker | |
if (animate) { | |
// Never allow two timeouts | |
clearTimeout(this.tooltipTimeout); | |
// Set the fixed interval ticking for the smooth tooltip | |
this.tooltipTimeout = setTimeout(function() { | |
// The interval function may still be running during destroy, | |
// so check that the chart is really there before calling. | |
if (tooltip) { | |
tooltip.move(x, y, anchorX, anchorY); | |
} | |
}, 32); | |
} | |
}, | |
/** | |
* Hide the tooltip | |
*/ | |
hide: function(delay) { | |
var tooltip = this; | |
clearTimeout(this.hideTimer); // disallow duplicate timers (#1728, #1766) | |
delay = pick(delay, this.options.hideDelay, 500); | |
if (!this.isHidden) { | |
this.hideTimer = syncTimeout(function() { | |
tooltip.getLabel()[delay ? 'fadeOut' : 'hide'](); | |
tooltip.isHidden = true; | |
}, delay); | |
} | |
}, | |
/** | |
* Extendable method to get the anchor position of the tooltip | |
* from a point or set of points | |
*/ | |
getAnchor: function(points, mouseEvent) { | |
var ret, | |
chart = this.chart, | |
inverted = chart.inverted, | |
plotTop = chart.plotTop, | |
plotLeft = chart.plotLeft, | |
plotX = 0, | |
plotY = 0, | |
yAxis, | |
xAxis; | |
points = splat(points); | |
// Pie uses a special tooltipPos | |
ret = points[0].tooltipPos; | |
// When tooltip follows mouse, relate the position to the mouse | |
if (this.followPointer && mouseEvent) { | |
if (mouseEvent.chartX === undefined) { | |
mouseEvent = chart.pointer.normalize(mouseEvent); | |
} | |
ret = [ | |
mouseEvent.chartX - chart.plotLeft, | |
mouseEvent.chartY - plotTop | |
]; | |
} | |
// When shared, use the average position | |
if (!ret) { | |
each(points, function(point) { | |
yAxis = point.series.yAxis; | |
xAxis = point.series.xAxis; | |
plotX += point.plotX + (!inverted && xAxis ? xAxis.left - plotLeft : 0); | |
plotY += (point.plotLow ? (point.plotLow + point.plotHigh) / 2 : point.plotY) + | |
(!inverted && yAxis ? yAxis.top - plotTop : 0); // #1151 | |
}); | |
plotX /= points.length; | |
plotY /= points.length; | |
ret = [ | |
inverted ? chart.plotWidth - plotY : plotX, | |
this.shared && !inverted && points.length > 1 && mouseEvent ? | |
mouseEvent.chartY - plotTop : // place shared tooltip next to the mouse (#424) | |
inverted ? chart.plotHeight - plotX : plotY | |
]; | |
} | |
return map(ret, Math.round); | |
}, | |
/** | |
* Place the tooltip in a chart without spilling over | |
* and not covering the point it self. | |
*/ | |
getPosition: function(boxWidth, boxHeight, point) { | |
var chart = this.chart, | |
distance = this.distance, | |
ret = {}, | |
h = point.h || 0, // #4117 | |
swapped, | |
first = ['y', chart.chartHeight, boxHeight, | |
point.plotY + chart.plotTop, chart.plotTop, | |
chart.plotTop + chart.plotHeight | |
], | |
second = ['x', chart.chartWidth, boxWidth, | |
point.plotX + chart.plotLeft, chart.plotLeft, | |
chart.plotLeft + chart.plotWidth | |
], | |
// The far side is right or bottom | |
preferFarSide = !this.followPointer && pick(point.ttBelow, !chart.inverted === !!point.negative), // #4984 | |
/** | |
* Handle the preferred dimension. When the preferred dimension is tooltip | |
* on top or bottom of the point, it will look for space there. | |
*/ | |
firstDimension = function(dim, outerSize, innerSize, point, min, max) { | |
var roomLeft = innerSize < point - distance, | |
roomRight = point + distance + innerSize < outerSize, | |
alignedLeft = point - distance - innerSize, | |
alignedRight = point + distance; | |
if (preferFarSide && roomRight) { | |
ret[dim] = alignedRight; | |
} else if (!preferFarSide && roomLeft) { | |
ret[dim] = alignedLeft; | |
} else if (roomLeft) { | |
ret[dim] = Math.min(max - innerSize, alignedLeft - h < 0 ? alignedLeft : alignedLeft - h); | |
} else if (roomRight) { | |
ret[dim] = Math.max( | |
min, | |
alignedRight + h + innerSize > outerSize ? | |
alignedRight : | |
alignedRight + h | |
); | |
} else { | |
return false; | |
} | |
}, | |
/** | |
* Handle the secondary dimension. If the preferred dimension is tooltip | |
* on top or bottom of the point, the second dimension is to align the tooltip | |
* above the point, trying to align center but allowing left or right | |
* align within the chart box. | |
*/ | |
secondDimension = function(dim, outerSize, innerSize, point) { | |
var retVal; | |
// Too close to the edge, return false and swap dimensions | |
if (point < distance || point > outerSize - distance) { | |
retVal = false; | |
// Align left/top | |
} else if (point < innerSize / 2) { | |
ret[dim] = 1; | |
// Align right/bottom | |
} else if (point > outerSize - innerSize / 2) { | |
ret[dim] = outerSize - innerSize - 2; | |
// Align center | |
} else { | |
ret[dim] = point - innerSize / 2; | |
} | |
return retVal; | |
}, | |
/** | |
* Swap the dimensions | |
*/ | |
swap = function(count) { | |
var temp = first; | |
first = second; | |
second = temp; | |
swapped = count; | |
}, | |
run = function() { | |
if (firstDimension.apply(0, first) !== false) { | |
if (secondDimension.apply(0, second) === false && !swapped) { | |
swap(true); | |
run(); | |
} | |
} else if (!swapped) { | |
swap(true); | |
run(); | |
} else { | |
ret.x = ret.y = 0; | |
} | |
}; | |
// Under these conditions, prefer the tooltip on the side of the point | |
if (chart.inverted || this.len > 1) { | |
swap(); | |
} | |
run(); | |
return ret; | |
}, | |
/** | |
* In case no user defined formatter is given, this will be used. Note that the context | |
* here is an object holding point, series, x, y etc. | |
* | |
* @returns {String|Array<String>} | |
*/ | |
defaultFormatter: function(tooltip) { | |
var items = this.points || splat(this), | |
s; | |
// Build the header | |
s = [tooltip.tooltipFooterHeaderFormatter(items[0])]; | |
// build the values | |
s = s.concat(tooltip.bodyFormatter(items)); | |
// footer | |
s.push(tooltip.tooltipFooterHeaderFormatter(items[0], true)); | |
return s; | |
}, | |
/** | |
* Refresh the tooltip's text and position. | |
* @param {Object|Array} pointOrPoints Rither a point or an array of points | |
*/ | |
refresh: function(pointOrPoints, mouseEvent) { | |
var tooltip = this, | |
label, | |
options = tooltip.options, | |
x, | |
y, | |
point = pointOrPoints, | |
anchor, | |
textConfig = {}, | |
text, | |
pointConfig = [], | |
formatter = options.formatter || tooltip.defaultFormatter, | |
shared = tooltip.shared, | |
currentSeries; | |
if (!options.enabled) { | |
return; | |
} | |
clearTimeout(this.hideTimer); | |
// get the reference point coordinates (pie charts use tooltipPos) | |
tooltip.followPointer = splat(point)[0].series.tooltipOptions.followPointer; | |
anchor = tooltip.getAnchor(point, mouseEvent); | |
x = anchor[0]; | |
y = anchor[1]; | |
// shared tooltip, array is sent over | |
if (shared && !(point.series && point.series.noSharedTooltip)) { | |
each(point, function(item) { | |
item.setState('hover'); | |
pointConfig.push(item.getLabelConfig()); | |
}); | |
textConfig = { | |
x: point[0].category, | |
y: point[0].y | |
}; | |
textConfig.points = pointConfig; | |
point = point[0]; | |
// single point tooltip | |
} else { | |
textConfig = point.getLabelConfig(); | |
} | |
this.len = pointConfig.length; // #6128 | |
text = formatter.call(textConfig, tooltip); | |
// register the current series | |
currentSeries = point.series; | |
this.distance = pick(currentSeries.tooltipOptions.distance, 16); | |
// update the inner HTML | |
if (text === false) { | |
this.hide(); | |
} else { | |
label = tooltip.getLabel(); | |
// show it | |
if (tooltip.isHidden) { | |
label.attr({ | |
opacity: 1 | |
}).show(); | |
} | |
// update text | |
if (tooltip.split) { | |
this.renderSplit(text, pointOrPoints); | |
} else { | |
// Prevent the tooltip from flowing over the chart box (#6659) | |
label.css({ | |
width: this.chart.spacingBox.width | |
}); | |
label.attr({ | |
text: text && text.join ? text.join('') : text | |
}); | |
// Set the stroke color of the box to reflect the point | |
label.removeClass(/highcharts-color-[\d]+/g) | |
.addClass('highcharts-color-' + pick(point.colorIndex, currentSeries.colorIndex)); | |
tooltip.updatePosition({ | |
plotX: x, | |
plotY: y, | |
negative: point.negative, | |
ttBelow: point.ttBelow, | |
h: anchor[2] || 0 | |
}); | |
} | |
this.isHidden = false; | |
} | |
}, | |
/** | |
* Render the split tooltip. Loops over each point's text and adds | |
* a label next to the point, then uses the distribute function to | |
* find best non-overlapping positions. | |
*/ | |
renderSplit: function(labels, points) { | |
var tooltip = this, | |
boxes = [], | |
chart = this.chart, | |
ren = chart.renderer, | |
rightAligned = true, | |
options = this.options, | |
headerHeight = 0, | |
tooltipLabel = this.getLabel(); | |
// Create the individual labels for header and points, ignore footer | |
each(labels.slice(0, points.length + 1), function(str, i) { | |
if (str !== false) { | |
var point = points[i - 1] || | |
// Item 0 is the header. Instead of this, we could also | |
// use the crosshair label | |
{ | |
isHeader: true, | |
plotX: points[0].plotX | |
}, | |
owner = point.series || tooltip, | |
tt = owner.tt, | |
series = point.series || {}, | |
colorClass = 'highcharts-color-' + pick( | |
point.colorIndex, | |
series.colorIndex, | |
'none' | |
), | |
target, | |
x, | |
bBox, | |
boxWidth; | |
// Store the tooltip referance on the series | |
if (!tt) { | |
owner.tt = tt = ren.label(null, null, null, 'callout') | |
.addClass('highcharts-tooltip-box ' + colorClass) | |
.attr({ | |
'padding': options.padding, | |
'r': options.borderRadius | |
}) | |
.add(tooltipLabel); | |
} | |
tt.isActive = true; | |
tt.attr({ | |
text: str | |
}); | |
// Get X position now, so we can move all to the other side in | |
// case of overflow | |
bBox = tt.getBBox(); | |
boxWidth = bBox.width + tt.strokeWidth(); | |
if (point.isHeader) { | |
headerHeight = bBox.height; | |
x = Math.max( | |
0, // No left overflow | |
Math.min( | |
point.plotX + chart.plotLeft - boxWidth / 2, | |
// No right overflow (#5794) | |
chart.chartWidth - boxWidth | |
) | |
); | |
} else { | |
x = point.plotX + chart.plotLeft - | |
pick(options.distance, 16) - boxWidth; | |
} | |
// If overflow left, we don't use this x in the next loop | |
if (x < 0) { | |
rightAligned = false; | |
} | |
// Prepare for distribution | |
target = (point.series && point.series.yAxis && | |
point.series.yAxis.pos) + (point.plotY || 0); | |
target -= chart.plotTop; | |
boxes.push({ | |
target: point.isHeader ? | |
chart.plotHeight + headerHeight : target, | |
rank: point.isHeader ? 1 : 0, | |
size: owner.tt.getBBox().height + 1, | |
point: point, | |
x: x, | |
tt: tt | |
}); | |
} | |
}); | |
// Clean previous run (for missing points) | |
this.cleanSplit(); | |
// Distribute and put in place | |
H.distribute(boxes, chart.plotHeight + headerHeight); | |
each(boxes, function(box) { | |
var point = box.point, | |
series = point.series; | |
// Put the label in place | |
box.tt.attr({ | |
visibility: box.pos === undefined ? 'hidden' : 'inherit', | |
x: (rightAligned || point.isHeader ? | |
box.x : | |
point.plotX + chart.plotLeft + pick(options.distance, 16)), | |
y: box.pos + chart.plotTop, | |
anchorX: point.isHeader ? | |
point.plotX + chart.plotLeft : point.plotX + series.xAxis.pos, | |
anchorY: point.isHeader ? | |
box.pos + chart.plotTop - 15 : point.plotY + series.yAxis.pos | |
}); | |
}); | |
}, | |
/** | |
* Find the new position and perform the move | |
*/ | |
updatePosition: function(point) { | |
var chart = this.chart, | |
label = this.getLabel(), | |
pos = (this.options.positioner || this.getPosition).call( | |
this, | |
label.width, | |
label.height, | |
point | |
); | |
// do the move | |
this.move( | |
Math.round(pos.x), | |
Math.round(pos.y || 0), // can be undefined (#3977) | |
point.plotX + chart.plotLeft, | |
point.plotY + chart.plotTop | |
); | |
}, | |
/** | |
* Get the optimal date format for a point, based on a range. | |
* @param {number} range - The time range | |
* @param {number|Date} date - The date of the point in question | |
* @param {number} startOfWeek - An integer representing the first day of | |
* the week, where 0 is Sunday | |
* @param {Object} dateTimeLabelFormats - A map of time units to formats | |
* @return {string} - the optimal date format for a point | |
*/ | |
getDateFormat: function(range, date, startOfWeek, dateTimeLabelFormats) { | |
var dateStr = dateFormat('%m-%d %H:%M:%S.%L', date), | |
format, | |
n, | |
blank = '01-01 00:00:00.000', | |
strpos = { | |
millisecond: 15, | |
second: 12, | |
minute: 9, | |
hour: 6, | |
day: 3 | |
}, | |
lastN = 'millisecond'; // for sub-millisecond data, #4223 | |
for (n in timeUnits) { | |
// If the range is exactly one week and we're looking at a Sunday/Monday, go for the week format | |
if (range === timeUnits.week && +dateFormat('%w', date) === startOfWeek && | |
dateStr.substr(6) === blank.substr(6)) { | |
n = 'week'; | |
break; | |
} | |
// The first format that is too great for the range | |
if (timeUnits[n] > range) { | |
n = lastN; | |
break; | |
} | |
// If the point is placed every day at 23:59, we need to show | |
// the minutes as well. #2637. | |
if (strpos[n] && dateStr.substr(strpos[n]) !== blank.substr(strpos[n])) { | |
break; | |
} | |
// Weeks are outside the hierarchy, only apply them on Mondays/Sundays like in the first condition | |
if (n !== 'week') { | |
lastN = n; | |
} | |
} | |
if (n) { | |
format = dateTimeLabelFormats[n]; | |
} | |
return format; | |
}, | |
/** | |
* Get the best X date format based on the closest point range on the axis. | |
*/ | |
getXDateFormat: function(point, options, xAxis) { | |
var xDateFormat, | |
dateTimeLabelFormats = options.dateTimeLabelFormats, | |
closestPointRange = xAxis && xAxis.closestPointRange; | |
if (closestPointRange) { | |
xDateFormat = this.getDateFormat( | |
closestPointRange, | |
point.x, | |
xAxis.options.startOfWeek, | |
dateTimeLabelFormats | |
); | |
} else { | |
xDateFormat = dateTimeLabelFormats.day; | |
} | |
return xDateFormat || dateTimeLabelFormats.year; // #2546, 2581 | |
}, | |
/** | |
* Format the footer/header of the tooltip | |
* #3397: abstraction to enable formatting of footer and header | |
*/ | |
tooltipFooterHeaderFormatter: function(labelConfig, isFooter) { | |
var footOrHead = isFooter ? 'footer' : 'header', | |
series = labelConfig.series, | |
tooltipOptions = series.tooltipOptions, | |
xDateFormat = tooltipOptions.xDateFormat, | |
xAxis = series.xAxis, | |
isDateTime = xAxis && xAxis.options.type === 'datetime' && isNumber(labelConfig.key), | |
formatString = tooltipOptions[footOrHead + 'Format']; | |
// Guess the best date format based on the closest point distance (#568, #3418) | |
if (isDateTime && !xDateFormat) { | |
xDateFormat = this.getXDateFormat(labelConfig, tooltipOptions, xAxis); | |
} | |
// Insert the footer date format if any | |
if (isDateTime && xDateFormat) { | |
formatString = formatString.replace('{point.key}', '{point.key:' + xDateFormat + '}'); | |
} | |
return format(formatString, { | |
point: labelConfig, | |
series: series | |
}); | |
}, | |
/** | |
* Build the body (lines) of the tooltip by iterating over the items and returning one entry for each item, | |
* abstracting this functionality allows to easily overwrite and extend it. | |
*/ | |
bodyFormatter: function(items) { | |
return map(items, function(item) { | |
var tooltipOptions = item.series.tooltipOptions; | |
return (tooltipOptions.pointFormatter || item.point.tooltipFormatter) | |
.call(item.point, tooltipOptions.pointFormat); | |
}); | |
} | |
}; | |
}(Highcharts)); | |
(function(H) { | |
/** | |
* (c) 2010-2017 Torstein Honsi | |
* | |
* License: www.highcharts.com/license | |
*/ | |
var addEvent = H.addEvent, | |
attr = H.attr, | |
charts = H.charts, | |
color = H.color, | |
css = H.css, | |
defined = H.defined, | |
each = H.each, | |
extend = H.extend, | |
fireEvent = H.fireEvent, | |
offset = H.offset, | |
pick = H.pick, | |
removeEvent = H.removeEvent, | |
splat = H.splat, | |
Tooltip = H.Tooltip, | |
win = H.win; | |
/** | |
* The mouse tracker object. All methods starting with "on" are primary DOM | |
* event handlers. Subsequent methods should be named differently from what they | |
* are doing. | |
* | |
* @constructor Highcharts.Pointer | |
* @param {Object} chart The Chart instance | |
* @param {Object} options The root options object | |
*/ | |
H.Pointer = function(chart, options) { | |
this.init(chart, options); | |
}; | |
H.Pointer.prototype = { | |
/** | |
* Initialize Pointer | |
*/ | |
init: function(chart, options) { | |
// Store references | |
this.options = options; | |
this.chart = chart; | |
// Do we need to handle click on a touch device? | |
this.runChartClick = options.chart.events && !!options.chart.events.click; | |
this.pinchDown = []; | |
this.lastValidTouch = {}; | |
if (Tooltip) { | |
chart.tooltip = new Tooltip(chart, options.tooltip); | |
this.followTouchMove = pick(options.tooltip.followTouchMove, true); | |
} | |
this.setDOMEvents(); | |
}, | |
/** | |
* Resolve the zoomType option, this is reset on all touch start and mouse | |
* down events. | |
*/ | |
zoomOption: function(e) { | |
var chart = this.chart, | |
options = chart.options.chart, | |
zoomType = options.zoomType || '', | |
inverted = chart.inverted, | |
zoomX, | |
zoomY; | |
// Look for the pinchType option | |
if (/touch/.test(e.type)) { | |
zoomType = pick(options.pinchType, zoomType); | |
} | |
this.zoomX = zoomX = /x/.test(zoomType); | |
this.zoomY = zoomY = /y/.test(zoomType); | |
this.zoomHor = (zoomX && !inverted) || (zoomY && inverted); | |
this.zoomVert = (zoomY && !inverted) || (zoomX && inverted); | |
this.hasZoom = zoomX || zoomY; | |
}, | |
/** | |
* @typedef {Object} PointerEvent | |
* A native browser mouse or touch event, extended with position | |
* information relative to the {@link Chart.container}. | |
* @property {Number} chartX | |
* The X coordinate of the pointer interaction relative to the | |
* chart. | |
* @property {Number} chartY | |
* The Y coordinate of the pointer interaction relative to the | |
* chart. | |
* | |
*/ | |
/** | |
* Add crossbrowser support for chartX and chartY. | |
* | |
* @param {Object} e | |
* The event object in standard browsers. | |
* | |
* @return {PointerEvent} | |
* A browser event with extended properties `chartX` and `chartY` | |
*/ | |
normalize: function(e, chartPosition) { | |
var chartX, | |
chartY, | |
ePos; | |
// IE normalizing | |
e = e || win.event; | |
if (!e.target) { | |
e.target = e.srcElement; | |
} | |
// iOS (#2757) | |
ePos = e.touches ? (e.touches.length ? e.touches.item(0) : e.changedTouches[0]) : e; | |
// Get mouse position | |
if (!chartPosition) { | |
this.chartPosition = chartPosition = offset(this.chart.container); | |
} | |
// chartX and chartY | |
if (ePos.pageX === undefined) { // IE < 9. #886. | |
chartX = Math.max(e.x, e.clientX - chartPosition.left); // #2005, #2129: the second case is | |
// for IE10 quirks mode within framesets | |
chartY = e.y; | |
} else { | |
chartX = ePos.pageX - chartPosition.left; | |
chartY = ePos.pageY - chartPosition.top; | |
} | |
return extend(e, { | |
chartX: Math.round(chartX), | |
chartY: Math.round(chartY) | |
}); | |
}, | |
/** | |
* Get the click position in terms of axis values. | |
* | |
* @param {Object} e A pointer event | |
*/ | |
getCoordinates: function(e) { | |
var coordinates = { | |
xAxis: [], | |
yAxis: [] | |
}; | |
each(this.chart.axes, function(axis) { | |
coordinates[axis.isXAxis ? 'xAxis' : 'yAxis'].push({ | |
axis: axis, | |
value: axis.toValue(e[axis.horiz ? 'chartX' : 'chartY']) | |
}); | |
}); | |
return coordinates; | |
}, | |
/** | |
* Collects the points closest to a mouseEvent | |
* @param {Array} series Array of series to gather points from | |
* @param {Boolean} shared True if shared tooltip, otherwise false | |
* @param {Object} e Mouse event which possess a position to compare against | |
* @return {Array} KDPoints sorted by distance | |
*/ | |
getKDPoints: function(series, shared, e) { | |
var kdpoints = [], | |
noSharedTooltip, | |
directTouch, | |
kdpointT, | |
i; | |
// Find nearest points on all series | |
each(series, function(s) { | |
// Skip hidden series | |
noSharedTooltip = s.noSharedTooltip && shared; | |
directTouch = !shared && s.directTouch; | |
if (s.visible && !directTouch && pick(s.options.enableMouseTracking, true)) { // #3821 | |
// #3828 | |
kdpointT = s.searchPoint( | |
e, !noSharedTooltip && s.options.findNearestPointBy.indexOf('y') < 0 | |
); | |
if (kdpointT && kdpointT.series) { // Point.series becomes null when reset and before redraw (#5197) | |
kdpoints.push(kdpointT); | |
} | |
} | |
}); | |
// Sort kdpoints by distance to mouse pointer | |
kdpoints.sort(function(p1, p2) { | |
var isCloserX = p1.distX - p2.distX, | |
isCloser = p1.dist - p2.dist, | |
isAbove = | |
(p2.series.group && p2.series.group.zIndex) - | |
(p1.series.group && p1.series.group.zIndex), | |
result; | |
// We have two points which are not in the same place on xAxis and shared tooltip: | |
if (isCloserX !== 0 && shared) { // #5721 | |
result = isCloserX; | |
// Points are not exactly in the same place on x/yAxis: | |
} else if (isCloser !== 0) { | |
result = isCloser; | |
// The same xAxis and yAxis position, sort by z-index: | |
} else if (isAbove !== 0) { | |
result = isAbove; | |
// The same zIndex, sort by array index: | |
} else { | |
result = p1.series.index > p2.series.index ? -1 : 1; | |
} | |
return result; | |
}); | |
// Remove points with different x-positions, required for shared tooltip and crosshairs (#4645): | |
if (shared && kdpoints[0] && !kdpoints[0].series.noSharedTooltip) { | |
i = kdpoints.length; | |
while (i--) { | |
if (kdpoints[i].x !== kdpoints[0].x || kdpoints[i].series.noSharedTooltip) { | |
kdpoints.splice(i, 1); | |
} | |
} | |
} | |
return kdpoints; | |
}, | |
getPointFromEvent: function(e) { | |
var target = e.target, | |
point; | |
while (target && !point) { | |
point = target.point; | |
target = target.parentNode; | |
} | |
return point; | |
}, | |
getChartCoordinatesFromPoint: function(point, inverted) { | |
var series = point.series, | |
xAxis = series.xAxis, | |
yAxis = series.yAxis; | |
if (xAxis && yAxis) { | |
return inverted ? { | |
chartX: xAxis.len + xAxis.pos - point.clientX, | |
chartY: yAxis.len + yAxis.pos - point.plotY | |
} : { | |
chartX: point.clientX + xAxis.pos, | |
chartY: point.plotY + yAxis.pos | |
}; | |
} | |
}, | |
/** | |
* Calculates what is the current hovered point/points and series. | |
* | |
* @private | |
* | |
* @param {undefined|Point} existingHoverPoint | |
* The point currrently beeing hovered. | |
* @param {undefined|Series} existingHoverSeries | |
* The series currently beeing hovered. | |
* @param {Array.<Series>} series | |
* All the series in the chart. | |
* @param {boolean} isDirectTouch | |
* Is the pointer directly hovering the point. | |
* @param {boolean} shared | |
* Whether it is a shared tooltip or not. | |
* @param {object} coordinates | |
* Chart coordinates of the pointer. | |
* @param {number} coordinates.chartX | |
* @param {number} coordinates.chartY | |
* | |
* @return {object} | |
* Object containing resulting hover data. | |
*/ | |
getHoverData: function( | |
existingHoverPoint, | |
existingHoverSeries, | |
series, | |
isDirectTouch, | |
shared, | |
coordinates | |
) { | |
var hoverPoint = existingHoverPoint, | |
hoverSeries = existingHoverSeries, | |
searchSeries = shared ? series : [hoverSeries], | |
useExisting = !!(isDirectTouch && existingHoverPoint), | |
notSticky = hoverSeries && !hoverSeries.stickyTracking, | |
isHoverPoint = function(point, i) { | |
return i === 0; | |
}, | |
hoverPoints; | |
// If there is a hoverPoint and its series requires direct touch (like | |
// columns, #3899), or we're on a noSharedTooltip series among shared | |
// tooltip series (#4546), use the existing hoverPoint. | |
if (useExisting) { | |
isHoverPoint = function(p) { | |
return p === existingHoverPoint; | |
}; | |
} else if (notSticky) { | |
isHoverPoint = function(p) { | |
return p.series === hoverSeries; | |
}; | |
} else { | |
// Avoid series with stickyTracking false | |
searchSeries = H.grep(series, function(s) { | |
return s.stickyTracking; | |
}); | |
} | |
hoverPoints = (useExisting && !shared) ? | |
// Non-shared tooltips with directTouch don't use the k-d-tree | |
[existingHoverPoint] : | |
this.getKDPoints(searchSeries, shared, coordinates); | |
hoverPoint = H.find(hoverPoints, isHoverPoint); | |
hoverSeries = hoverPoint && hoverPoint.series; | |
// In this case we could only look for the hoverPoint in series with | |
// stickyTracking, but we should still include all series in the shared | |
// tooltip. | |
if (!useExisting && !notSticky && shared) { | |
hoverPoints = this.getKDPoints(series, shared, coordinates); | |
} | |
// Keep the order of series in tooltip | |
// Must be done after assigning of hoverPoint | |
hoverPoints.sort(function(p1, p2) { | |
return p1.series.index - p2.series.index; | |
}); | |
return { | |
hoverPoint: hoverPoint, | |
hoverSeries: hoverSeries, | |
hoverPoints: hoverPoints | |
}; | |
}, | |
/** | |
* With line type charts with a single tracker, get the point closest to the mouse. | |
* Run Point.onMouseOver and display tooltip for the point or points. | |
*/ | |
runPointActions: function(e, p) { | |
var pointer = this, | |
chart = pointer.chart, | |
series = chart.series, | |
tooltip = chart.tooltip, | |
shared = tooltip ? tooltip.shared : false, | |
hoverPoint = p || chart.hoverPoint, | |
hoverSeries = hoverPoint && hoverPoint.series || chart.hoverSeries, | |
// onMouseOver or already hovering a series with directTouch | |
isDirectTouch = !!p || ( | |
(hoverSeries && hoverSeries.directTouch) && | |
pointer.isDirectTouch | |
), | |
hoverData = this.getHoverData( | |
hoverPoint, | |
hoverSeries, | |
series, | |
isDirectTouch, | |
shared, | |
e | |
), | |
useSharedTooltip, | |
followPointer, | |
anchor, | |
points; | |
// Update variables from hoverData. | |
hoverPoint = hoverData.hoverPoint; | |
hoverSeries = hoverData.hoverSeries; | |
followPointer = hoverSeries && hoverSeries.tooltipOptions.followPointer; | |
useSharedTooltip = ( | |
shared && | |
hoverPoint && | |
!hoverPoint.series.noSharedTooltip | |
); | |
points = (useSharedTooltip ? | |
hoverData.hoverPoints : | |
(hoverPoint ? [hoverPoint] : []) | |
); | |
// Refresh tooltip for kdpoint if new hover point or tooltip was hidden | |
// #3926, #4200 | |
if ( | |
hoverPoint && | |
// !(hoverSeries && hoverSeries.directTouch) && | |
(hoverPoint !== chart.hoverPoint || (tooltip && tooltip.isHidden)) | |
) { | |
each(chart.hoverPoints || [], function(p) { | |
if (H.inArray(p, points) === -1) { | |
p.setState(); | |
} | |
}); | |
// Do mouseover on all points (#3919, #3985, #4410, #5622) | |
each(points || [], function(p) { | |
p.setState('hover'); | |
}); | |
// set normal state to previous series | |
if (chart.hoverSeries !== hoverSeries) { | |
hoverSeries.onMouseOver(); | |
} | |
// If tracking is on series in stead of on each point, | |
// fire mouseOver on hover point. // #4448 | |
if (chart.hoverPoint) { | |
chart.hoverPoint.firePointEvent('mouseOut'); | |
} | |
hoverPoint.firePointEvent('mouseOver'); | |
chart.hoverPoints = points; | |
chart.hoverPoint = hoverPoint; | |
// Draw tooltip if necessary | |
if (tooltip) { | |
tooltip.refresh(useSharedTooltip ? points : hoverPoint, e); | |
} | |
// Update positions (regardless of kdpoint or hoverPoint) | |
} else if (followPointer && tooltip && !tooltip.isHidden) { | |
anchor = tooltip.getAnchor([{}], e); | |
tooltip.updatePosition({ | |
plotX: anchor[0], | |
plotY: anchor[1] | |
}); | |
} | |
// Start the event listener to pick up the tooltip and crosshairs | |
if (!pointer.unDocMouseMove) { | |
pointer.unDocMouseMove = addEvent( | |
chart.container.ownerDocument, | |
'mousemove', | |
function(e) { | |
var chart = charts[H.hoverChartIndex]; | |
if (chart) { | |
chart.pointer.onDocumentMouseMove(e); | |
} | |
} | |
); | |
} | |
// Issues related to crosshair #4927, #5269 #5066, #5658 | |
each(chart.axes, function drawAxisCrosshair(axis) { | |
var snap = pick(axis.crosshair.snap, true); | |
if (!snap) { | |
axis.drawCrosshair(e); | |
// Axis has snapping crosshairs, and one of the hover points belongs | |
// to axis | |
} else if (H.find(points, function(p) { | |
return p.series[axis.coll] === axis; | |
})) { | |
axis.drawCrosshair(e, hoverPoint); | |
// Axis has snapping crosshairs, but no hover point belongs to axis | |
} else { | |
axis.hideCrosshair(); | |
} | |
}); | |
}, | |
/** | |
* Reset the tracking by hiding the tooltip, the hover series state and the | |
* hover point | |
* | |
* @param allowMove {Boolean} | |
* Instead of destroying the tooltip altogether, allow moving it if | |
* possible | |
*/ | |
reset: function(allowMove, delay) { | |
var pointer = this, | |
chart = pointer.chart, | |
hoverSeries = chart.hoverSeries, | |
hoverPoint = chart.hoverPoint, | |
hoverPoints = chart.hoverPoints, | |
tooltip = chart.tooltip, | |
tooltipPoints = tooltip && tooltip.shared ? hoverPoints : hoverPoint; | |
// Check if the points have moved outside the plot area (#1003, #4736, #5101) | |
if (allowMove && tooltipPoints) { | |
each(splat(tooltipPoints), function(point) { | |
if (point.series.isCartesian && point.plotX === undefined) { | |
allowMove = false; | |
} | |
}); | |
} | |
// Just move the tooltip, #349 | |
if (allowMove) { | |
if (tooltip && tooltipPoints) { | |
tooltip.refresh(tooltipPoints); | |
if (hoverPoint) { // #2500 | |
hoverPoint.setState(hoverPoint.state, true); | |
each(chart.axes, function(axis) { | |
if (axis.crosshair) { | |
axis.drawCrosshair(null, hoverPoint); | |
} | |
}); | |
} | |
} | |
// Full reset | |
} else { | |
if (hoverPoint) { | |
hoverPoint.onMouseOut(); | |
} | |
if (hoverPoints) { | |
each(hoverPoints, function(point) { | |
point.setState(); | |
}); | |
} | |
if (hoverSeries) { | |
hoverSeries.onMouseOut(); | |
} | |
if (tooltip) { | |
tooltip.hide(delay); | |
} | |
if (pointer.unDocMouseMove) { | |
pointer.unDocMouseMove = pointer.unDocMouseMove(); | |
} | |
// Remove crosshairs | |
each(chart.axes, function(axis) { | |
axis.hideCrosshair(); | |
}); | |
pointer.hoverX = chart.hoverPoints = chart.hoverPoint = null; | |
} | |
}, | |
/** | |
* Scale series groups to a certain scale and translation | |
*/ | |
scaleGroups: function(attribs, clip) { | |
var chart = this.chart, | |
seriesAttribs; | |
// Scale each series | |
each(chart.series, function(series) { | |
seriesAttribs = attribs || series.getPlotBox(); // #1701 | |
if (series.xAxis && series.xAxis.zoomEnabled && series.group) { | |
series.group.attr(seriesAttribs); | |
if (series.markerGroup) { | |
series.markerGroup.attr(seriesAttribs); | |
series.markerGroup.clip(clip ? chart.clipRect : null); | |
} | |
if (series.dataLabelsGroup) { | |
series.dataLabelsGroup.attr(seriesAttribs); | |
} | |
} | |
}); | |
// Clip | |
chart.clipRect.attr(clip || chart.clipBox); | |
}, | |
/** | |
* Start a drag operation | |
*/ | |
dragStart: function(e) { | |
var chart = this.chart; | |
// Record the start position | |
chart.mouseIsDown = e.type; | |
chart.cancelClick = false; | |
chart.mouseDownX = this.mouseDownX = e.chartX; | |
chart.mouseDownY = this.mouseDownY = e.chartY; | |
}, | |
/** | |
* Perform a drag operation in response to a mousemove event while the mouse is down | |
*/ | |
drag: function(e) { | |
var chart = this.chart, | |
chartOptions = chart.options.chart, | |
chartX = e.chartX, | |
chartY = e.chartY, | |
zoomHor = this.zoomHor, | |
zoomVert = this.zoomVert, | |
plotLeft = chart.plotLeft, | |
plotTop = chart.plotTop, | |
plotWidth = chart.plotWidth, | |
plotHeight = chart.plotHeight, | |
clickedInside, | |
size, | |
selectionMarker = this.selectionMarker, | |
mouseDownX = this.mouseDownX, | |
mouseDownY = this.mouseDownY, | |
panKey = chartOptions.panKey && e[chartOptions.panKey + 'Key']; | |
// If the device supports both touch and mouse (like IE11), and we are touch-dragging | |
// inside the plot area, don't handle the mouse event. #4339. | |
if (selectionMarker && selectionMarker.touch) { | |
return; | |
} | |
// If the mouse is outside the plot area, adjust to cooordinates | |
// inside to prevent the selection marker from going outside | |
if (chartX < plotLeft) { | |
chartX = plotLeft; | |
} else if (chartX > plotLeft + plotWidth) { | |
chartX = plotLeft + plotWidth; | |
} | |
if (chartY < plotTop) { | |
chartY = plotTop; | |
} else if (chartY > plotTop + plotHeight) { | |
chartY = plotTop + plotHeight; | |
} | |
// determine if the mouse has moved more than 10px | |
this.hasDragged = Math.sqrt( | |
Math.pow(mouseDownX - chartX, 2) + | |
Math.pow(mouseDownY - chartY, 2) | |
); | |
if (this.hasDragged > 10) { | |
clickedInside = chart.isInsidePlot(mouseDownX - plotLeft, mouseDownY - plotTop); | |
// make a selection | |
if (chart.hasCartesianSeries && (this.zoomX || this.zoomY) && clickedInside && !panKey) { | |
if (!selectionMarker) { | |
this.selectionMarker = selectionMarker = chart.renderer.rect( | |
plotLeft, | |
plotTop, | |
zoomHor ? 1 : plotWidth, | |
zoomVert ? 1 : plotHeight, | |
0 | |
) | |
.attr({ | |
'class': 'highcharts-selection-marker', | |
'zIndex': 7 | |
}) | |
.add(); | |
} | |
} | |
// adjust the width of the selection marker | |
if (selectionMarker && zoomHor) { | |
size = chartX - mouseDownX; | |
selectionMarker.attr({ | |
width: Math.abs(size), | |
x: (size > 0 ? 0 : size) + mouseDownX | |
}); | |
} | |
// adjust the height of the selection marker | |
if (selectionMarker && zoomVert) { | |
size = chartY - mouseDownY; | |
selectionMarker.attr({ | |
height: Math.abs(size), | |
y: (size > 0 ? 0 : size) + mouseDownY | |
}); | |
} | |
// panning | |
if (clickedInside && !selectionMarker && chartOptions.panning) { | |
chart.pan(e, chartOptions.panning); | |
} | |
} | |
}, | |
/** | |
* On mouse up or touch end across the entire document, drop the selection. | |
*/ | |
drop: function(e) { | |
var pointer = this, | |
chart = this.chart, | |
hasPinched = this.hasPinched; | |
if (this.selectionMarker) { | |
var selectionData = { | |
originalEvent: e, // #4890 | |
xAxis: [], | |
yAxis: [] | |
}, | |
selectionBox = this.selectionMarker, | |
selectionLeft = selectionBox.attr ? selectionBox.attr('x') : selectionBox.x, | |
selectionTop = selectionBox.attr ? selectionBox.attr('y') : selectionBox.y, | |
selectionWidth = selectionBox.attr ? selectionBox.attr('width') : selectionBox.width, | |
selectionHeight = selectionBox.attr ? selectionBox.attr('height') : selectionBox.height, | |
runZoom; | |
// a selection has been made | |
if (this.hasDragged || hasPinched) { | |
// record each axis' min and max | |
each(chart.axes, function(axis) { | |
if (axis.zoomEnabled && defined(axis.min) && (hasPinched || pointer[{ | |
xAxis: 'zoomX', | |
yAxis: 'zoomY' | |
}[axis.coll]])) { // #859, #3569 | |
var horiz = axis.horiz, | |
minPixelPadding = e.type === 'touchend' ? axis.minPixelPadding : 0, // #1207, #3075 | |
selectionMin = axis.toValue((horiz ? selectionLeft : selectionTop) + minPixelPadding), | |
selectionMax = axis.toValue((horiz ? selectionLeft + selectionWidth : selectionTop + selectionHeight) - minPixelPadding); | |
selectionData[axis.coll].push({ | |
axis: axis, | |
min: Math.min(selectionMin, selectionMax), // for reversed axes | |
max: Math.max(selectionMin, selectionMax) | |
}); | |
runZoom = true; | |
} | |
}); | |
if (runZoom) { | |
fireEvent(chart, 'selection', selectionData, function(args) { | |
chart.zoom(extend(args, hasPinched ? { | |
animation: false | |
} : null)); | |
}); | |
} | |
} | |
this.selectionMarker = this.selectionMarker.destroy(); | |
// Reset scaling preview | |
if (hasPinched) { | |
this.scaleGroups(); | |
} | |
} | |
// Reset all | |
if (chart) { // it may be destroyed on mouse up - #877 | |
css(chart.container, { | |
cursor: chart._cursor | |
}); | |
chart.cancelClick = this.hasDragged > 10; // #370 | |
chart.mouseIsDown = this.hasDragged = this.hasPinched = false; | |
this.pinchDown = []; | |
} | |
}, | |
onContainerMouseDown: function(e) { | |
e = this.normalize(e); | |
this.zoomOption(e); | |
// issue #295, dragging not always working in Firefox | |
if (e.preventDefault) { | |
e.preventDefault(); | |
} | |
this.dragStart(e); | |
}, | |
onDocumentMouseUp: function(e) { | |
if (charts[H.hoverChartIndex]) { | |
charts[H.hoverChartIndex].pointer.drop(e); | |
} | |
}, | |
/** | |
* Special handler for mouse move that will hide the tooltip when the mouse leaves the plotarea. | |
* Issue #149 workaround. The mouseleave event does not always fire. | |
*/ | |
onDocumentMouseMove: function(e) { | |
var chart = this.chart, | |
chartPosition = this.chartPosition; | |
e = this.normalize(e, chartPosition); | |
// If we're outside, hide the tooltip | |
if (chartPosition && !this.inClass(e.target, 'highcharts-tracker') && | |
!chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) { | |
this.reset(); | |
} | |
}, | |
/** | |
* When mouse leaves the container, hide the tooltip. | |
*/ | |
onContainerMouseLeave: function(e) { | |
var chart = charts[H.hoverChartIndex]; | |
if (chart && (e.relatedTarget || e.toElement)) { // #4886, MS Touch end fires mouseleave but with no related target | |
chart.pointer.reset(); | |
chart.pointer.chartPosition = null; // also reset the chart position, used in #149 fix | |
} | |
}, | |
// The mousemove, touchmove and touchstart event handler | |
onContainerMouseMove: function(e) { | |
var chart = this.chart; | |
if (!defined(H.hoverChartIndex) || !charts[H.hoverChartIndex] || !charts[H.hoverChartIndex].mouseIsDown) { | |
H.hoverChartIndex = chart.index; | |
} | |
e = this.normalize(e); | |
e.returnValue = false; // #2251, #3224 | |
if (chart.mouseIsDown === 'mousedown') { | |
this.drag(e); | |
} | |
// Show the tooltip and run mouse over events (#977) | |
if ((this.inClass(e.target, 'highcharts-tracker') || | |
chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) && !chart.openMenu) { | |
this.runPointActions(e); | |
} | |
}, | |
/** | |
* Utility to detect whether an element has, or has a parent with, a specific | |
* class name. Used on detection of tracker objects and on deciding whether | |
* hovering the tooltip should cause the active series to mouse out. | |
*/ | |
inClass: function(element, className) { | |
var elemClassName; | |
while (element) { | |
elemClassName = attr(element, 'class'); | |
if (elemClassName) { | |
if (elemClassName.indexOf(className) !== -1) { | |
return true; | |
} | |
if (elemClassName.indexOf('highcharts-container') !== -1) { | |
return false; | |
} | |
} | |
element = element.parentNode; | |
} | |
}, | |
onTrackerMouseOut: function(e) { | |
var series = this.chart.hoverSeries, | |
relatedTarget = e.relatedTarget || e.toElement; | |
this.isDirectTouch = false; | |
if (series && relatedTarget && !series.stickyTracking && | |
!this.inClass(relatedTarget, 'highcharts-tooltip') && | |
(!this.inClass(relatedTarget, 'highcharts-series-' + series.index) || // #2499, #4465 | |
!this.inClass(relatedTarget, 'highcharts-tracker') // #5553 | |
) | |
) { | |
series.onMouseOut(); | |
} | |
}, | |
onContainerClick: function(e) { | |
var chart = this.chart, | |
hoverPoint = chart.hoverPoint, | |
plotLeft = chart.plotLeft, | |
plotTop = chart.plotTop; | |
e = this.normalize(e); | |
if (!chart.cancelClick) { | |
// On tracker click, fire the series and point events. #783, #1583 | |
if (hoverPoint && this.inClass(e.target, 'highcharts-tracker')) { | |
// the series click event | |
fireEvent(hoverPoint.series, 'click', extend(e, { | |
point: hoverPoint | |
})); | |
// the point click event | |
if (chart.hoverPoint) { // it may be destroyed (#1844) | |
hoverPoint.firePointEvent('click', e); | |
} | |
// When clicking outside a tracker, fire a chart event | |
} else { | |
extend(e, this.getCoordinates(e)); | |
// fire a click event in the chart | |
if (chart.isInsidePlot(e.chartX - plotLeft, e.chartY - plotTop)) { | |
fireEvent(chart, 'click', e); | |
} | |
} | |
} | |
}, | |
/** | |
* Set the JS DOM events on the container and document. This method should contain | |
* a one-to-one assignment between methods and their handlers. Any advanced logic should | |
* be moved to the handler reflecting the event's name. | |
*/ | |
setDOMEvents: function() { | |
var pointer = this, | |
container = pointer.chart.container, | |
ownerDoc = container.ownerDocument; | |
container.onmousedown = function(e) { | |
pointer.onContainerMouseDown(e); | |
}; | |
container.onmousemove = function(e) { | |
pointer.onContainerMouseMove(e); | |
}; | |
container.onclick = function(e) { | |
pointer.onContainerClick(e); | |
}; | |
addEvent(container, 'mouseleave', pointer.onContainerMouseLeave); | |
if (H.chartCount === 1) { | |
addEvent( | |
ownerDoc, | |
'mouseup', | |
pointer.onDocumentMouseUp | |
); | |
} | |
if (H.hasTouch) { | |
container.ontouchstart = function(e) { | |
pointer.onContainerTouchStart(e); | |
}; | |
container.ontouchmove = function(e) { | |
pointer.onContainerTouchMove(e); | |
}; | |
if (H.chartCount === 1) { | |
addEvent( | |
ownerDoc, | |
'touchend', | |
pointer.onDocumentTouchEnd | |
); | |
} | |
} | |
}, | |
/** | |
* Destroys the Pointer object and disconnects DOM events. | |
*/ | |
destroy: function() { | |
var pointer = this, | |
ownerDoc = this.chart.container.ownerDocument; | |
if (pointer.unDocMouseMove) { | |
pointer.unDocMouseMove(); | |
} | |
removeEvent( | |
pointer.chart.container, | |
'mouseleave', | |
pointer.onContainerMouseLeave | |
); | |
if (!H.chartCount) { | |
removeEvent(ownerDoc, 'mouseup', pointer.onDocumentMouseUp); | |
removeEvent(ownerDoc, 'touchend', pointer.onDocumentTouchEnd); | |
} | |
// memory and CPU leak | |
clearInterval(pointer.tooltipTimeout); | |
H.objectEach(pointer, function(val, prop) { | |
pointer[prop] = null; | |
}); | |
} | |
}; | |
}(Highcharts)); | |
(function(H) { | |
/** | |
* (c) 2010-2017 Torstein Honsi | |
* | |
* License: www.highcharts.com/license | |
*/ | |
var addEvent = H.addEvent, | |
animate = H.animate, | |
animObject = H.animObject, | |
attr = H.attr, | |
doc = H.doc, | |
Axis = H.Axis, // @todo add as requirement | |
createElement = H.createElement, | |
defaultOptions = H.defaultOptions, | |
discardElement = H.discardElement, | |
charts = H.charts, | |
css = H.css, | |
defined = H.defined, | |
each = H.each, | |
extend = H.extend, | |
find = H.find, | |
fireEvent = H.fireEvent, | |
getStyle = H.getStyle, | |
grep = H.grep, | |
isNumber = H.isNumber, | |
isObject = H.isObject, | |
isString = H.isString, | |
Legend = H.Legend, // @todo add as requirement | |
marginNames = H.marginNames, | |
merge = H.merge, | |
objectEach = H.objectEach, | |
Pointer = H.Pointer, // @todo add as requirement | |
pick = H.pick, | |
pInt = H.pInt, | |
removeEvent = H.removeEvent, | |
seriesTypes = H.seriesTypes, | |
splat = H.splat, | |
svg = H.svg, | |
syncTimeout = H.syncTimeout, | |
win = H.win, | |
Renderer = H.Renderer; | |
/** | |
* The Chart class. The recommended constructor is {@link Highcharts#chart}. | |
* @class Highcharts.Chart | |
* @param {String|HTMLDOMElement} renderTo | |
* The DOM element to render to, or its id. | |
* @param {Options} options | |
* The chart options structure. | |
* @param {Function} [callback] | |
* Function to run when the chart has loaded and and all external images | |
* are loaded. Defining a {@link | |
* https://api.highcharts.com/highcharts/chart.events.load|chart.event.load} | |
* handler is equivalent. | |
* | |
* @example | |
* var chart = new Highcharts.Chart('container', { | |
* title: { | |
* text: 'My chart' | |
* }, | |
* series: [{ | |
* data: [1, 3, 2, 4] | |
* }] | |
* }) | |
*/ | |
var Chart = H.Chart = function() { | |
this.getArgs.apply(this, arguments); | |
}; | |
/** | |
* Factory function for basic charts. | |
* | |
* @function #chart | |
* @memberOf Highcharts | |
* @param {String|HTMLDOMElement} renderTo - The DOM element to render to, or | |
* its id. | |
* @param {Options} options - The chart options structure. | |
* @param {Function} [callback] - Function to run when the chart has loaded and | |
* and all external images are loaded. Defining a {@link | |
* https://api.highcharts.com/highcharts/chart.events.load|chart.event.load} | |
* handler is equivalent. | |
* @return {Highcharts.Chart} - Returns the Chart object. | |
* | |
* @example | |
* // Render a chart in to div#container | |
* var chart = Highcharts.chart('container', { | |
* title: { | |
* text: 'My chart' | |
* }, | |
* series: [{ | |
* data: [1, 3, 2, 4] | |
* }] | |
* }); | |
*/ | |
H.chart = function(a, b, c) { | |
return new Chart(a, b, c); | |
}; | |
extend(Chart.prototype, /** @lends Highcharts.Chart.prototype */ { | |
/** | |
* Hook for modules | |
*/ | |
callbacks: [], | |
/** | |
* Handle the arguments passed to the constructor | |
* @returns {Array} Arguments without renderTo | |
*/ | |
getArgs: function() { | |
var args = [].slice.call(arguments); | |
// Remove the optional first argument, renderTo, and | |
// set it on this. | |
if (isString(args[0]) || args[0].nodeName) { | |
this.renderTo = args.shift(); | |
} | |
this.init(args[0], args[1]); | |
}, | |
/** | |
* Initialize the chart | |
*/ | |
init: function(userOptions, callback) { | |
// Handle regular options | |
var options, | |
type, | |
seriesOptions = userOptions.series, // skip merging data points to increase performance | |
userPlotOptions = userOptions.plotOptions || {}; | |
userOptions.series = null; | |
options = merge(defaultOptions, userOptions); // do the merge | |
// Override (by copy of user options) or clear tooltip options | |
// in chart.options.plotOptions (#6218) | |
for (type in options.plotOptions) { | |
options.plotOptions[type].tooltip = ( | |
userPlotOptions[type] && | |
merge(userPlotOptions[type].tooltip) // override by copy | |
) || undefined; // or clear | |
} | |
// User options have higher priority than default options (#6218). | |
// In case of exporting: path is changed | |
options.tooltip.userOptions = (userOptions.chart && | |
userOptions.chart.forExport && userOptions.tooltip.userOptions) || | |
userOptions.tooltip; | |
options.series = userOptions.series = seriesOptions; // set back the series data | |
this.userOptions = userOptions; | |
var optionsChart = options.chart; | |
var chartEvents = optionsChart.events; | |
this.margin = []; | |
this.spacing = []; | |
//this.runChartClick = chartEvents && !!chartEvents.click; | |
this.bounds = { | |
h: {}, | |
v: {} | |
}; // Pixel data bounds for touch zoom | |
this.callback = callback; | |
this.isResizing = 0; | |
/** | |
* The options structure for the chart. It contains members for the sub | |
* elements like series, legend, tooltip etc. | |
* | |
* @memberof Highcharts.Chart | |
* @name options | |
* @type {Options} | |
*/ | |
this.options = options; | |
/** | |
* All the axes in the chart. | |
* | |
* @memberof Highcharts.Chart | |
* @name axes | |
* @see Highcharts.Chart.xAxis | |
* @see Highcharts.Chart.yAxis | |
* @type {Array.<Highcharts.Axis>} | |
*/ | |
this.axes = []; | |
/** | |
* All the current series in the chart. | |
* | |
* @memberof Highcharts.Chart | |
* @name series | |
* @type {Array.<Highcharts.Series>} | |
*/ | |
this.series = []; | |
/** | |
* The chart title. The title has an `update` method that allows | |
* modifying the options directly or indirectly via `chart.update`. | |
* | |
* @memberof Highcharts.Chart | |
* @name title | |
* @type Object | |
* | |
* @sample highcharts/members/title-update/ | |
* Updating titles | |
*/ | |
/** | |
* The chart subtitle. The subtitle has an `update` method that allows | |
* modifying the options directly or indirectly via `chart.update`. | |
* | |
* @memberof Highcharts.Chart | |
* @name subtitle | |
* @type Object | |
*/ | |
this.hasCartesianSeries = optionsChart.showAxes; | |
//this.axisOffset = undefined; | |
//this.inverted = undefined; | |
//this.loadingShown = undefined; | |
//this.container = undefined; | |
//this.chartWidth = undefined; | |
//this.chartHeight = undefined; | |
//this.marginRight = undefined; | |
//this.marginBottom = undefined; | |
//this.containerWidth = undefined; | |
//this.containerHeight = undefined; | |
//this.oldChartWidth = undefined; | |
//this.oldChartHeight = undefined; | |
//this.renderTo = undefined; | |
//this.spacingBox = undefined | |
//this.legend = undefined; | |
// Elements | |
//this.chartBackground = undefined; | |
//this.plotBackground = undefined; | |
//this.plotBGImage = undefined; | |
//this.plotBorder = undefined; | |
//this.loadingDiv = undefined; | |
//this.loadingSpan = undefined; | |
var chart = this; | |
// Add the chart to the global lookup | |
chart.index = charts.length; | |
charts.push(chart); | |
H.chartCount++; | |
// Chart event handlers | |
if (chartEvents) { | |
objectEach(chartEvents, function(event, eventType) { | |
addEvent(chart, eventType, event); | |
}); | |
} | |
/** | |
* A collection of the X axes in the chart. | |
* @type {Array.<Highcharts.Axis>} | |
* @name xAxis | |
* @memberOf Highcharts.Chart | |
*/ | |
chart.xAxis = []; | |
/** | |
* A collection of the Y axes in the chart. | |
* @type {Array.<Highcharts.Axis>} | |
* @name yAxis | |
* @memberOf Highcharts.Chart | |
*/ | |
chart.yAxis = []; | |
chart.pointCount = chart.colorCounter = chart.symbolCounter = 0; | |
chart.firstRender(); | |
}, | |
/** | |
* Initialize an individual series, called internally before render time | |
*/ | |
initSeries: function(options) { | |
var chart = this, | |
optionsChart = chart.options.chart, | |
type = options.type || optionsChart.type || optionsChart.defaultSeriesType, | |
series, | |
Constr = seriesTypes[type]; | |
// No such series type | |
if (!Constr) { | |
H.error(17, true); | |
} | |
series = new Constr(); | |
series.init(this, options); | |
return series; | |
}, | |
/** | |
* Order all series above a given index. When series are added and ordered | |
* by configuration, only the last series is handled (#248, #1123, #2456, | |
* #6112). This function is called on series initialization and destroy. | |
* | |
* @param {number} fromIndex - If this is given, only the series above this | |
* index are handled. | |
*/ | |
orderSeries: function(fromIndex) { | |
var series = this.series, | |
i = fromIndex || 0; | |
for (; i < series.length; i++) { | |
if (series[i]) { | |
series[i].index = i; | |
series[i].name = series[i].name || | |
'Series ' + (series[i].index + 1); | |
} | |
} | |
}, | |
/** | |
* Check whether a given point is within the plot area | |
* | |
* @param {Number} plotX Pixel x relative to the plot area | |
* @param {Number} plotY Pixel y relative to the plot area | |
* @param {Boolean} inverted Whether the chart is inverted | |
*/ | |
isInsidePlot: function(plotX, plotY, inverted) { | |
var x = inverted ? plotY : plotX, | |
y = inverted ? plotX : plotY; | |
return x >= 0 && | |
x <= this.plotWidth && | |
y >= 0 && | |
y <= this.plotHeight; | |
}, | |
/** | |
* Redraw the chart after changes have been done to the data, axis extremes | |
* chart size or chart elements. All methods for updating axes, series or | |
* points have a parameter for redrawing the chart. This is `true` by | |
* default. But in many cases you want to do more than one operation on the | |
* chart before redrawing, for example add a number of points. In those | |
* cases it is a waste of resources to redraw the chart for each new point | |
* added. So you add the points and call `chart.redraw()` after. | |
* | |
* @param {AnimationOptions} animation | |
* If or how to apply animation to the redraw. | |
*/ | |
redraw: function(animation) { | |
var chart = this, | |
axes = chart.axes, | |
series = chart.series, | |
pointer = chart.pointer, | |
legend = chart.legend, | |
redrawLegend = chart.isDirtyLegend, | |
hasStackedSeries, | |
hasDirtyStacks, | |
hasCartesianSeries = chart.hasCartesianSeries, | |
isDirtyBox = chart.isDirtyBox, | |
i, | |
serie, | |
renderer = chart.renderer, | |
isHiddenChart = renderer.isHidden(), | |
afterRedraw = []; | |
// Handle responsive rules, not only on resize (#6130) | |
if (chart.setResponsive) { | |
chart.setResponsive(false); | |
} | |
H.setAnimation(animation, chart); | |
if (isHiddenChart) { | |
chart.temporaryDisplay(); | |
} | |
// Adjust title layout (reflow multiline text) | |
chart.layOutTitles(); | |
// link stacked series | |
i = series.length; | |
while (i--) { | |
serie = series[i]; | |
if (serie.options.stacking) { | |
hasStackedSeries = true; | |
if (serie.isDirty) { | |
hasDirtyStacks = true; | |
break; | |
} | |
} | |
} | |
if (hasDirtyStacks) { // mark others as dirty | |
i = series.length; | |
while (i--) { | |
serie = series[i]; | |
if (serie.options.stacking) { | |
serie.isDirty = true; | |
} | |
} | |
} | |
// Handle updated data in the series | |
each(series, function(serie) { | |
if (serie.isDirty) { | |
if (serie.options.legendType === 'point') { | |
if (serie.updateTotals) { | |
serie.updateTotals(); | |
} | |
redrawLegend = true; | |
} | |
} | |
if (serie.isDirtyData) { | |
fireEvent(serie, 'updatedData'); | |
} | |
}); | |
// handle added or removed series | |
if (redrawLegend && legend.options.enabled) { // series or pie points are added or removed | |
// draw legend graphics | |
legend.render(); | |
chart.isDirtyLegend = false; | |
} | |
// reset stacks | |
if (hasStackedSeries) { | |
chart.getStacks(); | |
} | |
if (hasCartesianSeries) { | |
// set axes scales | |
each(axes, function(axis) { | |
axis.updateNames(); | |
axis.setScale(); | |
}); | |
} | |
chart.getMargins(); // #3098 | |
if (hasCartesianSeries) { | |
// If one axis is dirty, all axes must be redrawn (#792, #2169) | |
each(axes, function(axis) { | |
if (axis.isDirty) { | |
isDirtyBox = true; | |
} | |
}); | |
// redraw axes | |
each(axes, function(axis) { | |
// Fire 'afterSetExtremes' only if extremes are set | |
var key = axis.min + ',' + axis.max; | |
if (axis.extKey !== key) { // #821, #4452 | |
axis.extKey = key; | |
afterRedraw.push(function() { // prevent a recursive call to chart.redraw() (#1119) | |
fireEvent(axis, 'afterSetExtremes', extend(axis.eventArgs, axis.getExtremes())); // #747, #751 | |
delete axis.eventArgs; | |
}); | |
} | |
if (isDirtyBox || hasStackedSeries) { | |
axis.redraw(); | |
} | |
}); | |
} | |
// the plot areas size has changed | |
if (isDirtyBox) { | |
chart.drawChartBox(); | |
} | |
// Fire an event before redrawing series, used by the boost module to | |
// clear previous series renderings. | |
fireEvent(chart, 'predraw'); | |
// redraw affected series | |
each(series, function(serie) { | |
if ((isDirtyBox || serie.isDirty) && serie.visible) { | |
serie.redraw(); | |
} | |
// Set it here, otherwise we will have unlimited 'updatedData' calls | |
// for a hidden series after setData(). Fixes #6012 | |
serie.isDirtyData = false; | |
}); | |
// move tooltip or reset | |
if (pointer) { | |
pointer.reset(true); | |
} | |
// redraw if canvas | |
renderer.draw(); | |
// Fire the events | |
fireEvent(chart, 'redraw'); | |
fireEvent(chart, 'render'); | |
if (isHiddenChart) { | |
chart.temporaryDisplay(true); | |
} | |
// Fire callbacks that are put on hold until after the redraw | |
each(afterRedraw, function(callback) { | |
callback.call(); | |
}); | |
}, | |
/** | |
* Get an axis, series or point object by `id` as given in the configuration | |
* options. Returns `undefined` if no item is found. | |
* @param id {String} The id as given in the configuration options. | |
* @return {Highcharts.Axis|Highcharts.Series|Highcharts.Point|undefined} | |
* The retrieved item. | |
* @sample highcharts/plotoptions/series-id/ | |
* Get series by id | |
*/ | |
get: function(id) { | |
var ret, | |
series = this.series, | |
i; | |
function itemById(item) { | |
return item.id === id || (item.options && item.options.id === id); | |
} | |
ret = | |
// Search axes | |
find(this.axes, itemById) || | |
// Search series | |
find(this.series, itemById); | |
// Search points | |
for (i = 0; !ret && i < series.length; i++) { | |
ret = find(series[i].points || [], itemById); | |
} | |
return ret; | |
}, | |
/** | |
* Create the Axis instances based on the config options | |
*/ | |
getAxes: function() { | |
var chart = this, | |
options = this.options, | |
xAxisOptions = options.xAxis = splat(options.xAxis || {}), | |
yAxisOptions = options.yAxis = splat(options.yAxis || {}), | |
optionsArray; | |
// make sure the options are arrays and add some members | |
each(xAxisOptions, function(axis, i) { | |
axis.index = i; | |
axis.isX = true; | |
}); | |
each(yAxisOptions, function(axis, i) { | |
axis.index = i; | |
}); | |
// concatenate all axis options into one array | |
optionsArray = xAxisOptions.concat(yAxisOptions); | |
each(optionsArray, function(axisOptions) { | |
new Axis(chart, axisOptions); // eslint-disable-line no-new | |
}); | |
}, | |
/** | |
* Returns an array of all currently selected points in the chart. Points | |
* can be selected by clicking or programmatically by the {@link | |
* Highcharts.Point#select} function. | |
* | |
* @return {Array.<Highcharts.Point>} | |
* The currently selected points. | |
* | |
* @sample highcharts/plotoptions/series-allowpointselect-line/ | |
* Get selected points | |
*/ | |
getSelectedPoints: function() { | |
var points = []; | |
each(this.series, function(serie) { | |
// series.data - for points outside of viewed range (#6445) | |
points = points.concat(grep(serie.data || [], function(point) { | |
return point.selected; | |
})); | |
}); | |
return points; | |
}, | |
/** | |
* Returns an array of all currently selected series in the chart. Series | |
* can be selected either programmatically by the {@link | |
* Highcharts.Series#select} function or by checking the checkbox next to | |
* the legend item if {@link | |
* https://api.highcharts.com/highcharts/plotOptions.series.showCheckbox| | |
* series.showCheckBox} is true. | |
* | |
* @return {Array.<Highcharts.Series>} | |
* The currently selected series. | |
* | |
* @sample highcharts/members/chart-getselectedseries/ | |
* Get selected series | |
*/ | |
getSelectedSeries: function() { | |
return grep(this.series, function(serie) { | |
return serie.selected; | |
}); | |
}, | |
/** | |
* Set a new title or subtitle for the chart. | |
* | |
* @param titleOptions {TitleOptions} | |
* New title options. | |
* @param subtitleOptions {SubtitleOptions} | |
* New subtitle options. | |
* @param redraw {Boolean} | |
* Whether to redraw the chart or wait for a later call to | |
* `chart.redraw()`. | |
* | |
* @sample highcharts/members/chart-settitle/ Set title text and styles | |
* | |
*/ | |
setTitle: function(titleOptions, subtitleOptions, redraw) { | |
var chart = this, | |
options = chart.options, | |
chartTitleOptions, | |
chartSubtitleOptions; | |
chartTitleOptions = options.title = merge( | |
options.title, | |
titleOptions | |
); | |
chartSubtitleOptions = options.subtitle = merge( | |
options.subtitle, | |
subtitleOptions | |
); | |
// add title and subtitle | |
each([ | |
['title', titleOptions, chartTitleOptions], | |
['subtitle', subtitleOptions, chartSubtitleOptions] | |
], function(arr, i) { | |
var name = arr[0], | |
title = chart[name], | |
titleOptions = arr[1], | |
chartTitleOptions = arr[2]; | |
if (title && titleOptions) { | |
chart[name] = title = title.destroy(); // remove old | |
} | |
if (chartTitleOptions && chartTitleOptions.text && !title) { | |
chart[name] = chart.renderer.text( | |
chartTitleOptions.text, | |
0, | |
0, | |
chartTitleOptions.useHTML | |
) | |
.attr({ | |
align: chartTitleOptions.align, | |
'class': 'highcharts-' + name, | |
zIndex: chartTitleOptions.zIndex || 4 | |
}) | |
.add(); | |
// Update methods, shortcut to Chart.setTitle | |
chart[name].update = function(o) { | |
chart.setTitle(!i && o, i && o); | |
}; | |
} | |
}); | |
chart.layOutTitles(redraw); | |
}, | |
/** | |
* Lay out the chart titles and cache the full offset height for use | |
* in getMargins | |
*/ | |
layOutTitles: function(redraw) { | |
var titleOffset = 0, | |
requiresDirtyBox, | |
renderer = this.renderer, | |
spacingBox = this.spacingBox; | |
// Lay out the title and the subtitle respectively | |
each(['title', 'subtitle'], function(key) { | |
var title = this[key], | |
titleOptions = this.options[key], | |
offset = key === 'title' ? -3 : | |
// Floating subtitle (#6574) | |
titleOptions.verticalAlign ? 0 : titleOffset + 2, | |
titleSize; | |
if (title) { | |
titleSize = renderer.fontMetrics(titleSize, title).b; | |
title | |
.css({ | |
width: (titleOptions.width || | |
spacingBox.width + titleOptions.widthAdjust) + 'px' | |
}) | |
.align(extend({ | |
y: offset + titleSize | |
}, titleOptions), false, 'spacingBox'); | |
if (!titleOptions.floating && !titleOptions.verticalAlign) { | |
titleOffset = Math.ceil( | |
titleOffset + | |
// Skip the cache for HTML (#3481) | |
title.getBBox(titleOptions.useHTML).height | |
); | |
} | |
} | |
}, this); | |
requiresDirtyBox = this.titleOffset !== titleOffset; | |
this.titleOffset = titleOffset; // used in getMargins | |
if (!this.isDirtyBox && requiresDirtyBox) { | |
this.isDirtyBox = requiresDirtyBox; | |
// Redraw if necessary (#2719, #2744) | |
if (this.hasRendered && pick(redraw, true) && this.isDirtyBox) { | |
this.redraw(); | |
} | |
} | |
}, | |
/** | |
* Get chart width and height according to options and container size | |
*/ | |
getChartSize: function() { | |
var chart = this, | |
optionsChart = chart.options.chart, | |
widthOption = optionsChart.width, | |
heightOption = optionsChart.height, | |
renderTo = chart.renderTo; | |
// Get inner width and height | |
if (!defined(widthOption)) { | |
chart.containerWidth = getStyle(renderTo, 'width'); | |
} | |
if (!defined(heightOption)) { | |
chart.containerHeight = getStyle(renderTo, 'height'); | |
} | |
chart.chartWidth = Math.max( // #1393 | |
0, | |
widthOption || chart.containerWidth || 600 // #1460 | |
); | |
chart.chartHeight = Math.max( | |
0, | |
H.relativeLength( | |
heightOption, | |
chart.chartWidth | |
) || chart.containerHeight || 400 | |
); | |
}, | |
/** | |
* If the renderTo element has no offsetWidth, most likely one or more of | |
* its parents are hidden. Loop up the DOM tree to temporarily display the | |
* parents, then save the original display properties, and when the true | |
* size is retrieved, reset them. Used on first render and on redraws. | |
* | |
* @param {Boolean} revert - Revert to the saved original styles. | |
*/ | |
temporaryDisplay: function(revert) { | |
var node = this.renderTo, | |
tempStyle; | |
if (!revert) { | |
while (node && node.style) { | |
if (getStyle(node, 'display', false) === 'none') { | |
node.hcOrigStyle = { | |
display: node.style.display, | |
height: node.style.height, | |
overflow: node.style.overflow | |
}; | |
tempStyle = { | |
display: 'block', | |
overflow: 'hidden' | |
}; | |
if (node !== this.renderTo) { | |
tempStyle.height = 0; | |
} | |
H.css(node, tempStyle); | |
// If it still doesn't have an offset width after setting | |
// display to block, it probably has an !important priority | |
// #2631, 6803 | |
if (!node.offsetWidth) { | |
node.style.setProperty('display', 'block', 'important'); | |
} | |
} | |
node = node.parentNode; | |
} | |
} else { | |
while (node && node.style) { | |
if (node.hcOrigStyle) { | |
H.css(node, node.hcOrigStyle); | |
delete node.hcOrigStyle; | |
} | |
node = node.parentNode; | |
} | |
} | |
}, | |
/** | |
* Setter for the chart class name | |
*/ | |
setClassName: function(className) { | |
this.container.className = 'highcharts-container ' + (className || ''); | |
}, | |
/** | |
* Get the containing element, determine the size and create the inner | |
* container div to hold the chart | |
*/ | |
getContainer: function() { | |
var chart = this, | |
container, | |
options = chart.options, | |
optionsChart = options.chart, | |
chartWidth, | |
chartHeight, | |
renderTo = chart.renderTo, | |
indexAttrName = 'data-highcharts-chart', | |
oldChartIndex, | |
Ren, | |
containerId = H.uniqueKey(), | |
containerStyle, | |
key; | |
if (!renderTo) { | |
chart.renderTo = renderTo = optionsChart.renderTo; | |
} | |
if (isString(renderTo)) { | |
chart.renderTo = renderTo = doc.getElementById(renderTo); | |
} | |
// Display an error if the renderTo is wrong | |
if (!renderTo) { | |
H.error(13, true); | |
} | |
// If the container already holds a chart, destroy it. The check for | |
// hasRendered is there because web pages that are saved to disk from | |
// the browser, will preserve the data-highcharts-chart attribute and | |
// the SVG contents, but not an interactive chart. So in this case, | |
// charts[oldChartIndex] will point to the wrong chart if any (#2609). | |
oldChartIndex = pInt(attr(renderTo, indexAttrName)); | |
if ( | |
isNumber(oldChartIndex) && | |
charts[oldChartIndex] && | |
charts[oldChartIndex].hasRendered | |
) { | |
charts[oldChartIndex].destroy(); | |
} | |
// Make a reference to the chart from the div | |
attr(renderTo, indexAttrName, chart.index); | |
// remove previous chart | |
renderTo.innerHTML = ''; | |
// If the container doesn't have an offsetWidth, it has or is a child of | |
// a node that has display:none. We need to temporarily move it out to a | |
// visible state to determine the size, else the legend and tooltips | |
// won't render properly. The skipClone option is used in sparklines as | |
// a micro optimization, saving about 1-2 ms each chart. | |
if (!optionsChart.skipClone && !renderTo.offsetWidth) { | |
chart.temporaryDisplay(); | |
} | |
// get the width and height | |
chart.getChartSize(); | |
chartWidth = chart.chartWidth; | |
chartHeight = chart.chartHeight; | |
// Create the inner container | |
/** | |
* The containing HTML element of the chart. The container is | |
* dynamically inserted into the element given as the `renderTo` | |
* parameterin the {@link Highcharts#chart} constructor. | |
* | |
* @memberOf Highcharts.Chart | |
* @type {HTMLDOMElement} | |
*/ | |
container = createElement( | |
'div', { | |
id: containerId | |
}, | |
containerStyle, | |
renderTo | |
); | |
chart.container = container; | |
// cache the cursor (#1650) | |
chart._cursor = container.style.cursor; | |
// Initialize the renderer | |
Ren = H[optionsChart.renderer] || Renderer; | |
chart.renderer = new Ren( | |
container, | |
chartWidth, | |
chartHeight, | |
null, | |
optionsChart.forExport, | |
options.exporting && options.exporting.allowHTML | |
); | |
chart.setClassName(optionsChart.className); | |
// Initialize definitions | |
for (key in options.defs) { | |
this.renderer.definition(options.defs[key]); | |
} | |
// Add a reference to the charts index | |
chart.renderer.chartIndex = chart.index; | |
}, | |
/** | |
* Calculate margins by rendering axis labels in a preliminary position. | |
* Title, subtitle and legend have already been rendered at this stage, but | |
* will be moved into their final positions | |
*/ | |
getMargins: function(skipAxes) { | |
var chart = this, | |
spacing = chart.spacing, | |
margin = chart.margin, | |
titleOffset = chart.titleOffset; | |
chart.resetMargins(); | |
// Adjust for title and subtitle | |
if (titleOffset && !defined(margin[0])) { | |
chart.plotTop = Math.max( | |
chart.plotTop, | |
titleOffset + chart.options.title.margin + spacing[0] | |
); | |
} | |
// Adjust for legend | |
if (chart.legend.display) { | |
chart.legend.adjustMargins(margin, spacing); | |
} | |
// adjust for scroller | |
if (chart.extraMargin) { | |
chart[chart.extraMargin.type] = | |
(chart[chart.extraMargin.type] || 0) + chart.extraMargin.value; | |
} | |
if (chart.extraTopMargin) { | |
chart.plotTop += chart.extraTopMargin; | |
} | |
if (!skipAxes) { | |
this.getAxisMargins(); | |
} | |
}, | |
getAxisMargins: function() { | |
var chart = this, | |
// [top, right, bottom, left] | |
axisOffset = chart.axisOffset = [0, 0, 0, 0], | |
margin = chart.margin; | |
// pre-render axes to get labels offset width | |
if (chart.hasCartesianSeries) { | |
each(chart.axes, function(axis) { | |
if (axis.visible) { | |
axis.getOffset(); | |
} | |
}); | |
} | |
// Add the axis offsets | |
each(marginNames, function(m, side) { | |
if (!defined(margin[side])) { | |
chart[m] += axisOffset[side]; | |
} | |
}); | |
chart.setChartSize(); | |
}, | |
/** | |
* Reflows the chart to its container. By default, the chart reflows | |
* automatically to its container following a `window.resize` event, as per | |
* the {@link https://api.highcharts/highcharts/chart.reflow|chart.reflow} | |
* option. However, there are no reliable events for div resize, so if the | |
* container is resized without a window resize event, this must be called | |
* explicitly. | |
* | |
* @param {Object} e | |
* Event arguments. Used primarily when the function is called | |
* internally as a response to window resize. | |
* | |
* @sample highcharts/members/chart-reflow/ | |
* Resize div and reflow | |
* @sample highcharts/chart/events-container/ | |
* Pop up and reflow | |
*/ | |
reflow: function(e) { | |
var chart = this, | |
optionsChart = chart.options.chart, | |
renderTo = chart.renderTo, | |
hasUserWidth = defined(optionsChart.width), | |
width = optionsChart.width || getStyle(renderTo, 'width'), | |
height = optionsChart.height || getStyle(renderTo, 'height'), | |
target = e ? e.target : win; | |
// Width and height checks for display:none. Target is doc in IE8 and | |
// Opera, win in Firefox, Chrome and IE9. | |
if (!hasUserWidth && | |
!chart.isPrinting && | |
width && | |
height && | |
(target === win || target === doc) | |
) { | |
if ( | |
width !== chart.containerWidth || | |
height !== chart.containerHeight | |
) { | |
clearTimeout(chart.reflowTimeout); | |
// When called from window.resize, e is set, else it's called | |
// directly (#2224) | |
chart.reflowTimeout = syncTimeout(function() { | |
// Set size, it may have been destroyed in the meantime | |
// (#1257) | |
if (chart.container) { | |
chart.setSize(undefined, undefined, false); | |
} | |
}, e ? 100 : 0); | |
} | |
chart.containerWidth = width; | |
chart.containerHeight = height; | |
} | |
}, | |
/** | |
* Add the event handlers necessary for auto resizing | |
*/ | |
initReflow: function() { | |
var chart = this, | |
unbind; | |
unbind = addEvent(win, 'resize', function(e) { | |
chart.reflow(e); | |
}); | |
addEvent(chart, 'destroy', unbind); | |
// The following will add listeners to re-fit the chart before and after | |
// printing (#2284). However it only works in WebKit. Should have worked | |
// in Firefox, but not supported in IE. | |
/* | |
if (win.matchMedia) { | |
win.matchMedia('print').addListener(function reflow() { | |
chart.reflow(); | |
}); | |
} | |
*/ | |
}, | |
/** | |
* Resize the chart to a given width and height. In order to set the width | |
* only, the height argument may be skipped. To set the height only, pass | |
* `undefined for the width. | |
* @param {Number|undefined|null} [width] | |
* The new pixel width of the chart. Since v4.2.6, the argument can | |
* be `undefined` in order to preserve the current value (when | |
* setting height only), or `null` to adapt to the width of the | |
* containing element. | |
* @param {Number|undefined|null} [height] | |
* The new pixel height of the chart. Since v4.2.6, the argument can | |
* be `undefined` in order to preserve the current value, or `null` | |
* in order to adapt to the height of the containing element. | |
* @param {AnimationOptions} [animation=true] | |
* Whether and how to apply animation. | |
* | |
* @sample highcharts/members/chart-setsize-button/ | |
* Test resizing from buttons | |
* @sample highcharts/members/chart-setsize-jquery-resizable/ | |
* Add a jQuery UI resizable | |
* @sample stock/members/chart-setsize/ | |
* Highstock with UI resizable | |
*/ | |
setSize: function(width, height, animation) { | |
var chart = this, | |
renderer = chart.renderer, | |
globalAnimation; | |
// Handle the isResizing counter | |
chart.isResizing += 1; | |
// set the animation for the current process | |
H.setAnimation(animation, chart); | |
chart.oldChartHeight = chart.chartHeight; | |
chart.oldChartWidth = chart.chartWidth; | |
if (width !== undefined) { | |
chart.options.chart.width = width; | |
} | |
if (height !== undefined) { | |
chart.options.chart.height = height; | |
} | |
chart.getChartSize(); | |
// Resize the container with the global animation applied if enabled | |
// (#2503) | |
chart.setChartSize(true); | |
renderer.setSize(chart.chartWidth, chart.chartHeight, animation); | |
// handle axes | |
each(chart.axes, function(axis) { | |
axis.isDirty = true; | |
axis.setScale(); | |
}); | |
chart.isDirtyLegend = true; // force legend redraw | |
chart.isDirtyBox = true; // force redraw of plot and chart border | |
chart.layOutTitles(); // #2857 | |
chart.getMargins(); | |
chart.redraw(animation); | |
chart.oldChartHeight = null; | |
fireEvent(chart, 'resize'); | |
// Fire endResize and set isResizing back. If animation is disabled, | |
// fire without delay | |
syncTimeout(function() { | |
if (chart) { | |
fireEvent(chart, 'endResize', null, function() { | |
chart.isResizing -= 1; | |
}); | |
} | |
}, animObject(globalAnimation).duration); | |
}, | |
/** | |
* Set the public chart properties. This is done before and after the | |
* pre-render to determine margin sizes | |
*/ | |
setChartSize: function(skipAxes) { | |
var chart = this, | |
inverted = chart.inverted, | |
renderer = chart.renderer, | |
chartWidth = chart.chartWidth, | |
chartHeight = chart.chartHeight, | |
optionsChart = chart.options.chart, | |
spacing = chart.spacing, | |
clipOffset = chart.clipOffset, | |
clipX, | |
clipY, | |
plotLeft, | |
plotTop, | |
plotWidth, | |
plotHeight, | |
plotBorderWidth; | |
function clipOffsetSide(side) { | |
var offset = clipOffset[side] || 0; | |
return Math.max(plotBorderWidth || offset, offset) / 2; | |
} | |
chart.plotLeft = plotLeft = Math.round(chart.plotLeft); | |
chart.plotTop = plotTop = Math.round(chart.plotTop); | |
chart.plotWidth = plotWidth = Math.max( | |
0, | |
Math.round(chartWidth - plotLeft - chart.marginRight) | |
); | |
chart.plotHeight = plotHeight = Math.max( | |
0, | |
Math.round(chartHeight - plotTop - chart.marginBottom) | |
); | |
chart.plotSizeX = inverted ? plotHeight : plotWidth; | |
chart.plotSizeY = inverted ? plotWidth : plotHeight; | |
chart.plotBorderWidth = optionsChart.plotBorderWidth || 0; | |
// Set boxes used for alignment | |
chart.spacingBox = renderer.spacingBox = { | |
x: spacing[3], | |
y: spacing[0], | |
width: chartWidth - spacing[3] - spacing[1], | |
height: chartHeight - spacing[0] - spacing[2] | |
}; | |
chart.plotBox = renderer.plotBox = { | |
x: plotLeft, | |
y: plotTop, | |
width: plotWidth, | |
height: plotHeight | |
}; | |
plotBorderWidth = 2 * Math.floor(chart.plotBorderWidth / 2); | |
clipX = Math.ceil(clipOffsetSide(3)); | |
clipY = Math.ceil(clipOffsetSide(0)); | |
chart.clipBox = { | |
x: clipX, | |
y: clipY, | |
width: Math.floor( | |
chart.plotSizeX - | |
clipOffsetSide(1) - | |
clipX | |
), | |
height: Math.max( | |
0, | |
Math.floor( | |
chart.plotSizeY - | |
clipOffsetSide(2) - | |
clipY | |
) | |
) | |
}; | |
if (!skipAxes) { | |
each(chart.axes, function(axis) { | |
axis.setAxisSize(); | |
axis.setAxisTranslation(); | |
}); | |
} | |
}, | |
/** | |
* Initial margins before auto size margins are applied | |
*/ | |
resetMargins: function() { | |
var chart = this, | |
chartOptions = chart.options.chart; | |
// Create margin and spacing array | |
each(['margin', 'spacing'], function splashArrays(target) { | |
var value = chartOptions[target], | |
values = isObject(value) ? value : [value, value, value, value]; | |
each(['Top', 'Right', 'Bottom', 'Left'], function(sideName, side) { | |
chart[target][side] = pick( | |
chartOptions[target + sideName], | |
values[side] | |
); | |
}); | |
}); | |
// Set margin names like chart.plotTop, chart.plotLeft, | |
// chart.marginRight, chart.marginBottom. | |
each(marginNames, function(m, side) { | |
chart[m] = pick(chart.margin[side], chart.spacing[side]); | |
}); | |
chart.axisOffset = [0, 0, 0, 0]; // top, right, bottom, left | |
chart.clipOffset = []; | |
}, | |
/** | |
* Draw the borders and backgrounds for chart and plot area | |
*/ | |
drawChartBox: function() { | |
var chart = this, | |
optionsChart = chart.options.chart, | |
renderer = chart.renderer, | |
chartWidth = chart.chartWidth, | |
chartHeight = chart.chartHeight, | |
chartBackground = chart.chartBackground, | |
plotBackground = chart.plotBackground, | |
plotBorder = chart.plotBorder, | |
chartBorderWidth, | |
mgn, | |
bgAttr, | |
plotLeft = chart.plotLeft, | |
plotTop = chart.plotTop, | |
plotWidth = chart.plotWidth, | |
plotHeight = chart.plotHeight, | |
plotBox = chart.plotBox, | |
clipRect = chart.clipRect, | |
clipBox = chart.clipBox, | |
verb = 'animate'; | |
// Chart area | |
if (!chartBackground) { | |
chart.chartBackground = chartBackground = renderer.rect() | |
.addClass('highcharts-background') | |
.add(); | |
verb = 'attr'; | |
} | |
chartBorderWidth = mgn = chartBackground.strokeWidth(); | |
chartBackground[verb]({ | |
x: mgn / 2, | |
y: mgn / 2, | |
width: chartWidth - mgn - chartBorderWidth % 2, | |
height: chartHeight - mgn - chartBorderWidth % 2, | |
r: optionsChart.borderRadius | |
}); | |
// Plot background | |
verb = 'animate'; | |
if (!plotBackground) { | |
verb = 'attr'; | |
chart.plotBackground = plotBackground = renderer.rect() | |
.addClass('highcharts-plot-background') | |
.add(); | |
} | |
plotBackground[verb](plotBox); | |
// Plot clip | |
if (!clipRect) { | |
chart.clipRect = renderer.clipRect(clipBox); | |
} else { | |
clipRect.animate({ | |
width: clipBox.width, | |
height: clipBox.height | |
}); | |
} | |
// Plot area border | |
verb = 'animate'; | |
if (!plotBorder) { | |
verb = 'attr'; | |
chart.plotBorder = plotBorder = renderer.rect() | |
.addClass('highcharts-plot-border') | |
.attr({ | |
zIndex: 1 // Above the grid | |
}) | |
.add(); | |
} | |
plotBorder[verb](plotBorder.crisp({ | |
x: plotLeft, | |
y: plotTop, | |
width: plotWidth, | |
height: plotHeight | |
}, -plotBorder.strokeWidth())); //#3282 plotBorder should be negative; | |
// reset | |
chart.isDirtyBox = false; | |
}, | |
/** | |
* Detect whether a certain chart property is needed based on inspecting its | |
* options and series. This mainly applies to the chart.inverted property, | |
* and in extensions to the chart.angular and chart.polar properties. | |
*/ | |
propFromSeries: function() { | |
var chart = this, | |
optionsChart = chart.options.chart, | |
klass, | |
seriesOptions = chart.options.series, | |
i, | |
value; | |
each(['inverted', 'angular', 'polar'], function(key) { | |
// The default series type's class | |
klass = seriesTypes[optionsChart.type || | |
optionsChart.defaultSeriesType]; | |
// Get the value from available chart-wide properties | |
value = | |
optionsChart[key] || // It is set in the options | |
(klass && klass.prototype[key]); // The default series class | |
// requires it | |
// 4. Check if any the chart's series require it | |
i = seriesOptions && seriesOptions.length; | |
while (!value && i--) { | |
klass = seriesTypes[seriesOptions[i].type]; | |
if (klass && klass.prototype[key]) { | |
value = true; | |
} | |
} | |
// Set the chart property | |
chart[key] = value; | |
}); | |
}, | |
/** | |
* Link two or more series together. This is done initially from | |
* Chart.render, and after Chart.addSeries and Series.remove. | |
*/ | |
linkSeries: function() { | |
var chart = this, | |
chartSeries = chart.series; | |
// Reset links | |
each(chartSeries, function(series) { | |
series.linkedSeries.length = 0; | |
}); | |
// Apply new links | |
each(chartSeries, function(series) { | |
var linkedTo = series.options.linkedTo; | |
if (isString(linkedTo)) { | |
if (linkedTo === ':previous') { | |
linkedTo = chart.series[series.index - 1]; | |
} else { | |
linkedTo = chart.get(linkedTo); | |
} | |
// #3341 avoid mutual linking | |
if (linkedTo && linkedTo.linkedParent !== series) { | |
linkedTo.linkedSeries.push(series); | |
series.linkedParent = linkedTo; | |
series.visible = pick( | |
series.options.visible, | |
linkedTo.options.visible, | |
series.visible | |
); // #3879 | |
} | |
} | |
}); | |
}, | |
/** | |
* Render series for the chart | |
*/ | |
renderSeries: function() { | |
each(this.series, function(serie) { | |
serie.translate(); | |
serie.render(); | |
}); | |
}, | |
/** | |
* Render labels for the chart | |
*/ | |
renderLabels: function() { | |
var chart = this, | |
labels = chart.options.labels; | |
if (labels.items) { | |
each(labels.items, function(label) { | |
var style = extend(labels.style, label.style), | |
x = pInt(style.left) + chart.plotLeft, | |
y = pInt(style.top) + chart.plotTop + 12; | |
// delete to prevent rewriting in IE | |
delete style.left; | |
delete style.top; | |
chart.renderer.text( | |
label.html, | |
x, | |
y | |
) | |
.attr({ | |
zIndex: 2 | |
}) | |
.css(style) | |
.add(); | |
}); | |
} | |
}, | |
/** | |
* Render all graphics for the chart | |
*/ | |
render: function() { | |
var chart = this, | |
axes = chart.axes, | |
renderer = chart.renderer, | |
options = chart.options, | |
tempWidth, | |
tempHeight, | |
redoHorizontal, | |
redoVertical; | |
// Title | |
chart.setTitle(); | |
// Legend | |
chart.legend = new Legend(chart, options.legend); | |
// Get stacks | |
if (chart.getStacks) { | |
chart.getStacks(); | |
} | |
// Get chart margins | |
chart.getMargins(true); | |
chart.setChartSize(); | |
// Record preliminary dimensions for later comparison | |
tempWidth = chart.plotWidth; | |
tempHeight = chart.plotHeight = chart.plotHeight - 21; // 21 is the most common correction for X axis labels | |
// Get margins by pre-rendering axes | |
each(axes, function(axis) { | |
axis.setScale(); | |
}); | |
chart.getAxisMargins(); | |
// If the plot area size has changed significantly, calculate tick positions again | |
redoHorizontal = tempWidth / chart.plotWidth > 1.1; | |
redoVertical = tempHeight / chart.plotHeight > 1.05; // Height is more sensitive | |
if (redoHorizontal || redoVertical) { | |
each(axes, function(axis) { | |
if ((axis.horiz && redoHorizontal) || (!axis.horiz && redoVertical)) { | |
axis.setTickInterval(true); // update to reflect the new margins | |
} | |
}); | |
chart.getMargins(); // second pass to check for new labels | |
} | |
// Draw the borders and backgrounds | |
chart.drawChartBox(); | |
// Axes | |
if (chart.hasCartesianSeries) { | |
each(axes, function(axis) { | |
if (axis.visible) { | |
axis.render(); | |
} | |
}); | |
} | |
// The series | |
if (!chart.seriesGroup) { | |
chart.seriesGroup = renderer.g('series-group') | |
.attr({ | |
zIndex: 3 | |
}) | |
.add(); | |
} | |
chart.renderSeries(); | |
// Labels | |
chart.renderLabels(); | |
// Credits | |
chart.addCredits(); | |
// Handle responsiveness | |
if (chart.setResponsive) { | |
chart.setResponsive(); | |
} | |
// Set flag | |
chart.hasRendered = true; | |
}, | |
/** | |
* Set a new credits label for the chart. | |
* | |
* @param {CreditOptions} options | |
* A configuration object for the new credits. | |
* @sample highcharts/credits/credits-update/ Add and update credits | |
*/ | |
addCredits: function(credits) { | |
var chart = this; | |
credits = merge(true, this.options.credits, credits); | |
if (credits.enabled && !this.credits) { | |
/** | |
* The chart's credits label. The label has an `update` method that | |
* allows setting new options as per the {@link | |
* https://api.highcharts.com/highcharts/credits| | |
* credits options set}. | |
* | |
* @memberof Highcharts.Chart | |
* @name credits | |
* @type {Highcharts.SVGElement} | |
*/ | |
this.credits = this.renderer.text( | |
credits.text + (this.mapCredits || ''), | |
0, | |
0 | |
) | |
.addClass('highcharts-credits') | |
.on('click', function() { | |
if (credits.href) { | |
win.location.href = credits.href; | |
} | |
}) | |
.attr({ | |
align: credits.position.align, | |
zIndex: 8 | |
}) | |
.add() | |
.align(credits.position); | |
// Dynamically update | |
this.credits.update = function(options) { | |
chart.credits = chart.credits.destroy(); | |
chart.addCredits(options); | |
}; | |
} | |
}, | |
/** | |
* Remove the chart and purge memory. This method is called internally | |
* before adding a second chart into the same container, as well as on | |
* window unload to prevent leaks. | |
* | |
* @sample highcharts/members/chart-destroy/ | |
* Destroy the chart from a button | |
* @sample stock/members/chart-destroy/ | |
* Destroy with Highstock | |
*/ | |
destroy: function() { | |
var chart = this, | |
axes = chart.axes, | |
series = chart.series, | |
container = chart.container, | |
i, | |
parentNode = container && container.parentNode; | |
// fire the chart.destoy event | |
fireEvent(chart, 'destroy'); | |
// Delete the chart from charts lookup array | |
if (chart.renderer.forExport) { | |
H.erase(charts, chart); // #6569 | |
} else { | |
charts[chart.index] = undefined; | |
} | |
H.chartCount--; | |
chart.renderTo.removeAttribute('data-highcharts-chart'); | |
// remove events | |
removeEvent(chart); | |
// ==== Destroy collections: | |
// Destroy axes | |
i = axes.length; | |
while (i--) { | |
axes[i] = axes[i].destroy(); | |
} | |
// Destroy scroller & scroller series before destroying base series | |
if (this.scroller && this.scroller.destroy) { | |
this.scroller.destroy(); | |
} | |
// Destroy each series | |
i = series.length; | |
while (i--) { | |
series[i] = series[i].destroy(); | |
} | |
// ==== Destroy chart properties: | |
each([ | |
'title', 'subtitle', 'chartBackground', 'plotBackground', | |
'plotBGImage', 'plotBorder', 'seriesGroup', 'clipRect', 'credits', | |
'pointer', 'rangeSelector', 'legend', 'resetZoomButton', 'tooltip', | |
'renderer' | |
], function(name) { | |
var prop = chart[name]; | |
if (prop && prop.destroy) { | |
chart[name] = prop.destroy(); | |
} | |
}); | |
// remove container and all SVG | |
if (container) { // can break in IE when destroyed before finished loading | |
container.innerHTML = ''; | |
removeEvent(container); | |
if (parentNode) { | |
discardElement(container); | |
} | |
} | |
// clean it all up | |
objectEach(chart, function(val, key) { | |
delete chart[key]; | |
}); | |
}, | |
/** | |
* VML namespaces can't be added until after complete. Listening | |
* for Perini's doScroll hack is not enough. | |
*/ | |
isReadyToRender: function() { | |
var chart = this; | |
// Note: win == win.top is required | |
if ((!svg && (win == win.top && doc.readyState !== 'complete'))) { // eslint-disable-line eqeqeq | |
doc.attachEvent('onreadystatechange', function() { | |
doc.detachEvent('onreadystatechange', chart.firstRender); | |
if (doc.readyState === 'complete') { | |
chart.firstRender(); | |
} | |
}); | |
return false; | |
} | |
return true; | |
}, | |
/** | |
* Prepare for first rendering after all data are loaded | |
*/ | |
firstRender: function() { | |
var chart = this, | |
options = chart.options; | |
// Check whether the chart is ready to render | |
if (!chart.isReadyToRender()) { | |
return; | |
} | |
// Create the container | |
chart.getContainer(); | |
// Run an early event after the container and renderer are established | |
fireEvent(chart, 'init'); | |
chart.resetMargins(); | |
chart.setChartSize(); | |
// Set the common chart properties (mainly invert) from the given series | |
chart.propFromSeries(); | |
// get axes | |
chart.getAxes(); | |
// Initialize the series | |
each(options.series || [], function(serieOptions) { | |
chart.initSeries(serieOptions); | |
}); | |
chart.linkSeries(); | |
// Run an event after axes and series are initialized, but before render. At this stage, | |
// the series data is indexed and cached in the xData and yData arrays, so we can access | |
// those before rendering. Used in Highstock. | |
fireEvent(chart, 'beforeRender'); | |
// depends on inverted and on margins being set | |
if (Pointer) { | |
chart.pointer = new Pointer(chart, options); | |
} | |
chart.render(); | |
// Fire the load event if there are no external images | |
if (!chart.renderer.imgCount && chart.onload) { | |
chart.onload(); | |
} | |
// If the chart was rendered outside the top container, put it back in (#3679) | |
chart.temporaryDisplay(true); | |
}, | |
/** | |
* On chart load | |
*/ | |
onload: function() { | |
// Run callbacks | |
each([this.callback].concat(this.callbacks), function(fn) { | |
if (fn && this.index !== undefined) { // Chart destroyed in its own callback (#3600) | |
fn.apply(this, [this]); | |
} | |
}, this); | |
fireEvent(this, 'load'); | |
fireEvent(this, 'render'); | |
// Set up auto resize, check for not destroyed (#6068) | |
if (defined(this.index) && this.options.chart.reflow !== false) { | |
this.initReflow(); | |
} | |
// Don't run again | |
this.onload = null; | |
} | |
}); // end Chart | |
}(Highcharts)); | |
(function(Highcharts) { | |
/** | |
* (c) 2010-2017 Torstein Honsi | |
* | |
* License: www.highcharts.com/license | |
*/ | |
var Point, | |
H = Highcharts, | |
each = H.each, | |
extend = H.extend, | |
erase = H.erase, | |
fireEvent = H.fireEvent, | |
format = H.format, | |
isArray = H.isArray, | |
isNumber = H.isNumber, | |
pick = H.pick, | |
removeEvent = H.removeEvent; | |
/** | |
* The Point object. The point objects are generated from the `series.data` | |
* configuration objects or raw numbers. They can be accessed from the | |
* `Series.points` array. Other ways to instaniate points are through {@link | |
* Highcharts.Series#addPoint} or {@link Highcharts.Series#setData}. | |
* | |
* @class | |
*/ | |
Highcharts.Point = Point = function() {}; | |
Highcharts.Point.prototype = { | |
/** | |
* Initialize the point. Called internally based on the series.data option. | |
* @param {Object} series The series object containing this point. | |
* @param {Object} options The data in either number, array or object | |
* format. | |
* @param {Number} x Optionally, the X value of the. | |
* @returns {Object} The Point instance. | |
*/ | |
init: function(series, options, x) { | |
var point = this, | |
colors, | |
colorCount = series.chart.options.chart.colorCount, | |
colorIndex; | |
/** | |
* The series object associated with the point. | |
* | |
* @name series | |
* @memberof Highcharts.Point | |
* @type Highcharts.Series | |
*/ | |
point.series = series; | |
point.applyOptions(options, x); | |
if (series.options.colorByPoint) { | |
colorIndex = series.colorCounter; | |
series.colorCounter++; | |
// loop back to zero | |
if (series.colorCounter === colorCount) { | |
series.colorCounter = 0; | |
} | |
} else { | |
colorIndex = series.colorIndex; | |
} | |
point.colorIndex = pick(point.colorIndex, colorIndex); | |
series.chart.pointCount++; | |
return point; | |
}, | |
/** | |
* Apply the options containing the x and y data and possible some extra | |
* properties. Called on point init or from point.update. | |
* | |
* @param {Object} options The point options as defined in series.data. | |
* @param {Number} x Optionally, the X value. | |
* @returns {Object} The Point instance. | |
*/ | |
applyOptions: function(options, x) { | |
var point = this, | |
series = point.series, | |
pointValKey = series.options.pointValKey || series.pointValKey; | |
options = Point.prototype.optionsToObject.call(this, options); | |
// copy options directly to point | |
extend(point, options); | |
point.options = point.options ? extend(point.options, options) : options; | |
// Since options are copied into the Point instance, some accidental options must be shielded (#5681) | |
if (options.group) { | |
delete point.group; | |
} | |
// For higher dimension series types. For instance, for ranges, point.y is mapped to point.low. | |
if (pointValKey) { | |
point.y = point[pointValKey]; | |
} | |
point.isNull = pick( | |
point.isValid && !point.isValid(), | |
point.x === null || !isNumber(point.y, true) | |
); // #3571, check for NaN | |
// The point is initially selected by options (#5777) | |
if (point.selected) { | |
point.state = 'select'; | |
} | |
// If no x is set by now, get auto incremented value. All points must have an | |
// x value, however the y value can be null to create a gap in the series | |
if ('name' in point && x === undefined && series.xAxis && series.xAxis.hasNames) { | |
point.x = series.xAxis.nameToX(point); | |
} | |
if (point.x === undefined && series) { | |
if (x === undefined) { | |
point.x = series.autoIncrement(point); | |
} else { | |
point.x = x; | |
} | |
} | |
return point; | |
}, | |
/** | |
* Transform number or array configs into objects | |
*/ | |
optionsToObject: function(options) { | |
var ret = {}, | |
series = this.series, | |
keys = series.options.keys, | |
pointArrayMap = keys || series.pointArrayMap || ['y'], | |
valueCount = pointArrayMap.length, | |
firstItemType, | |
i = 0, | |
j = 0; | |
if (isNumber(options) || options === null) { | |
ret[pointArrayMap[0]] = options; | |
} else if (isArray(options)) { | |
// with leading x value | |
if (!keys && options.length > valueCount) { | |
firstItemType = typeof options[0]; | |
if (firstItemType === 'string') { | |
ret.name = options[0]; | |
} else if (firstItemType === 'number') { | |
ret.x = options[0]; | |
} | |
i++; | |
} | |
while (j < valueCount) { | |
if (!keys || options[i] !== undefined) { // Skip undefined positions for keys | |
ret[pointArrayMap[j]] = options[i]; | |
} | |
i++; | |
j++; | |
} | |
} else if (typeof options === 'object') { | |
ret = options; | |
// This is the fastest way to detect if there are individual point dataLabels that need | |
// to be considered in drawDataLabels. These can only occur in object configs. | |
if (options.dataLabels) { | |
series._hasPointLabels = true; | |
} | |
// Same approach as above for markers | |
if (options.marker) { | |
series._hasPointMarkers = true; | |
} | |
} | |
return ret; | |
}, | |
/** | |
* Get the CSS class names for individual points | |
* @returns {String} The class name | |
*/ | |
getClassName: function() { | |
return 'highcharts-point' + | |
(this.selected ? ' highcharts-point-select' : '') + | |
(this.negative ? ' highcharts-negative' : '') + | |
(this.isNull ? ' highcharts-null-point' : '') + | |
(this.colorIndex !== undefined ? ' highcharts-color-' + | |
this.colorIndex : '') + | |
(this.options.className ? ' ' + this.options.className : '') + | |
(this.zone && this.zone.className ? ' ' + | |
this.zone.className.replace('highcharts-negative', '') : ''); | |
}, | |
/** | |
* Return the zone that the point belongs to | |
*/ | |
getZone: function() { | |
var series = this.series, | |
zones = series.zones, | |
zoneAxis = series.zoneAxis || 'y', | |
i = 0, | |
zone; | |
zone = zones[i]; | |
while (this[zoneAxis] >= zone.value) { | |
zone = zones[++i]; | |
} | |
if (zone && zone.color && !this.options.color) { | |
this.color = zone.color; | |
} | |
return zone; | |
}, | |
/** | |
* Destroy a point to clear memory. Its reference still stays in series.data. | |
*/ | |
destroy: function() { | |
var point = this, | |
series = point.series, | |
chart = series.chart, | |
hoverPoints = chart.hoverPoints, | |
prop; | |
chart.pointCount--; | |
if (hoverPoints) { | |
point.setState(); | |
erase(hoverPoints, point); | |
if (!hoverPoints.length) { | |
chart.hoverPoints = null; | |
} | |
} | |
if (point === chart.hoverPoint) { | |
point.onMouseOut(); | |
} | |
// remove all events | |
if (point.graphic || point.dataLabel) { // removeEvent and destroyElements are performance expensive | |
removeEvent(point); | |
point.destroyElements(); | |
} | |
if (point.legendItem) { // pies have legend items | |
chart.legend.destroyItem(point); | |
} | |
for (prop in point) { | |
point[prop] = null; | |
} | |
}, | |
/** | |
* Destroy SVG elements associated with the point | |
*/ | |
destroyElements: function() { | |
var point = this, | |
props = ['graphic', 'dataLabel', 'dataLabelUpper', 'connector', 'shadowGroup'], | |
prop, | |
i = 6; | |
while (i--) { | |
prop = props[i]; | |
if (point[prop]) { | |
point[prop] = point[prop].destroy(); | |
} | |
} | |
}, | |
/** | |
* Return the configuration hash needed for the data label and tooltip formatters | |
*/ | |
getLabelConfig: function() { | |
return { | |
x: this.category, | |
y: this.y, | |
color: this.color, | |
colorIndex: this.colorIndex, | |
key: this.name || this.category, | |
series: this.series, | |
point: this, | |
percentage: this.percentage, | |
total: this.total || this.stackTotal | |
}; | |
}, | |
/** | |
* Extendable method for formatting each point's tooltip line | |
* | |
* @return {String} A string to be concatenated in to the common tooltip text | |
*/ | |
tooltipFormatter: function(pointFormat) { | |
// Insert options for valueDecimals, valuePrefix, and valueSuffix | |
var series = this.series, | |
seriesTooltipOptions = series.tooltipOptions, | |
valueDecimals = pick(seriesTooltipOptions.valueDecimals, ''), | |
valuePrefix = seriesTooltipOptions.valuePrefix || '', | |
valueSuffix = seriesTooltipOptions.valueSuffix || ''; | |
// Loop over the point array map and replace unformatted values with sprintf formatting markup | |
each(series.pointArrayMap || ['y'], function(key) { | |
key = '{point.' + key; // without the closing bracket | |
if (valuePrefix || valueSuffix) { | |
pointFormat = pointFormat.replace(key + '}', valuePrefix + key + '}' + valueSuffix); | |
} | |
pointFormat = pointFormat.replace(key + '}', key + ':,.' + valueDecimals + 'f}'); | |
}); | |
return format(pointFormat, { | |
point: this, | |
series: this.series | |
}); | |
}, | |
/** | |
* Fire an event on the Point object. | |
* @param {String} eventType | |
* @param {Object} eventArgs Additional event arguments | |
* @param {Function} defaultFunction Default event handler | |
*/ | |
firePointEvent: function(eventType, eventArgs, defaultFunction) { | |
var point = this, | |
series = this.series, | |
seriesOptions = series.options; | |
// load event handlers on demand to save time on mouseover/out | |
if (seriesOptions.point.events[eventType] || (point.options && point.options.events && point.options.events[eventType])) { | |
this.importEvents(); | |
} | |
// add default handler if in selection mode | |
if (eventType === 'click' && seriesOptions.allowPointSelect) { | |
defaultFunction = function(event) { | |
// Control key is for Windows, meta (= Cmd key) for Mac, Shift for Opera | |
if (point.select) { // Could be destroyed by prior event handlers (#2911) | |
point.select(null, event.ctrlKey || event.metaKey || event.shiftKey); | |
} | |
}; | |
} | |
fireEvent(this, eventType, eventArgs, defaultFunction); | |
}, | |
/** | |
* For certain series types, like pie charts, where individual points can | |
* be shown or hidden. | |
* | |
* @name visible | |
* @memberOf Highcharts.Point | |
* @type {Boolean} | |
*/ | |
visible: true | |
}; | |
/** | |
* For categorized axes this property holds the category name for the | |
* point. For other axes it holds the X value. | |
* | |
* @name category | |
* @memberOf Highcharts.Point | |
* @type {String|Number} | |
*/ | |
/** | |
* The percentage for points in a stacked series or pies. | |
* | |
* @name percentage | |
* @memberOf Highcharts.Point | |
* @type {Number} | |
*/ | |
/** | |
* The total of values in either a stack for stacked series, or a pie in a pie | |
* series. | |
* | |
* @name total | |
* @memberOf Highcharts.Point | |
* @type {Number} | |
*/ | |
/** | |
* The x value of the point. | |
* | |
* @name x | |
* @memberOf Highcharts.Point | |
* @type {Number} | |
*/ | |
/** | |
* The y value of the point. | |
* | |
* @name y | |
* @memberOf Highcharts.Point | |
* @type {Number} | |
*/ | |
}(Highcharts)); | |
(function(H) { | |
/** | |
* (c) 2010-2017 Torstein Honsi | |
* | |
* License: www.highcharts.com/license | |
*/ | |
var addEvent = H.addEvent, | |
animObject = H.animObject, | |
arrayMax = H.arrayMax, | |
arrayMin = H.arrayMin, | |
correctFloat = H.correctFloat, | |
Date = H.Date, | |
defaultOptions = H.defaultOptions, | |
defaultPlotOptions = H.defaultPlotOptions, | |
defined = H.defined, | |
each = H.each, | |
erase = H.erase, | |
extend = H.extend, | |
fireEvent = H.fireEvent, | |
grep = H.grep, | |
isArray = H.isArray, | |
isNumber = H.isNumber, | |
isString = H.isString, | |
LegendSymbolMixin = H.LegendSymbolMixin, // @todo add as a requirement | |
merge = H.merge, | |
objectEach = H.objectEach, | |
pick = H.pick, | |
Point = H.Point, // @todo add as a requirement | |
removeEvent = H.removeEvent, | |
splat = H.splat, | |
SVGElement = H.SVGElement, | |
syncTimeout = H.syncTimeout, | |
win = H.win; | |
/** | |
* This is the base series prototype that all other series types inherit from. | |
* A new series is initiated either through the {@link https://api.highcharts.com/highcharts/series| | |
* series} option structure, or after the chart is initiated, through {@link | |
* Highcharts.Chart#addSeries}. | |
* | |
* The object can be accessed in a number of ways. All series and point event | |
* handlers give a reference to the `series` object. The chart object has a | |
* {@link Highcharts.Chart.series|series} property that is a collection of all | |
* the chart's series. The point objects and axis objects also have the same | |
* reference. | |
* | |
* Another way to reference the series programmatically is by `id`. Add an id | |
* in the series configuration options, and get the series object by {@link | |
* Highcharts.Chart#get}. | |
* | |
* Configuration options for the series are given in three levels. Options for | |
* all series in a chart are given in the {@link https://api.highcharts.com/highcharts/plotOptions.series| | |
* plotOptions.series} object. Then options for all series of a specific type | |
* are given in the plotOptions of that type, for example `plotOptions.line`. | |
* Next, options for one single series are given in the series array, or as | |
* arguements to `chart.addSeries`. | |
* | |
* The data in the series is stored in various arrays. | |
* | |
* - First, `series.options.data` contains all the original config options for | |
* each point whether added by options or methods like `series.addPoint`. | |
* - Next, `series.data` contains those values converted to points, but in case | |
* the series data length exceeds the `cropThreshold`, or if the data is grouped, | |
* `series.data` doesn't contain all the points. It only contains the points that | |
* have been created on demand. | |
* - Then there's `series.points` that contains all currently visible point | |
* objects. In case of cropping, the cropped-away points are not part of this | |
* array. The `series.points` array starts at `series.cropStart` compared to | |
* `series.data` and `series.options.data`. If however the series data is grouped, | |
* these can't be correlated one to one. | |
* - `series.xData` and `series.processedXData` contain clean x values, equivalent | |
* to `series.data` and `series.points`. | |
* - `series.yData` and `series.processedYData` contain clean y values, equivalent | |
* to `series.data` and `series.points`. | |
* | |
* @class Highcharts.Series | |
* @param {Highcharts.Chart} chart | |
* The chart instance. | |
* @param {Object} options | |
* The series options. | |
*/ | |
H.Series = H.seriesType('line', null, { // base series options | |
allowPointSelect: false, | |
showCheckbox: false, | |
animation: { | |
duration: 1000 | |
}, | |
//clip: true, | |
//connectNulls: false, | |
//enableMouseTracking: true, | |
events: {}, | |
//legendIndex: 0, | |
// stacking: null, | |
marker: { | |
//enabled: true, | |
//symbol: null, | |
radius: 4, | |
states: { // states for a single point | |
hover: { | |
animation: { | |
duration: 50 | |
}, | |
enabled: true, | |
radiusPlus: 2 | |
} | |
} | |
}, | |
point: { | |
events: {} | |
}, | |
dataLabels: { | |
align: 'center', | |
// defer: true, | |
// enabled: false, | |
formatter: function() { | |
return this.y === null ? '' : H.numberFormat(this.y, -1); | |
}, | |
verticalAlign: 'bottom', // above singular point | |
x: 0, | |
y: 0, | |
// borderRadius: undefined, | |
padding: 5 | |
}, | |
// draw points outside the plot area when the number of points is less than | |
// this | |
cropThreshold: 300, | |
pointRange: 0, | |
//pointStart: 0, | |
//pointInterval: 1, | |
//showInLegend: null, // auto = false for linked series | |
softThreshold: true, | |
states: { // states for the entire series | |
hover: { | |
//enabled: false, | |
animation: { | |
duration: 50 | |
}, | |
lineWidthPlus: 1, | |
marker: { | |
// lineWidth: base + 1, | |
// radius: base + 1 | |
}, | |
halo: { | |
size: 10 | |
} | |
}, | |
select: { | |
marker: {} | |
} | |
}, | |
stickyTracking: true, | |
//tooltip: { | |
//pointFormat: '<span style="color:{point.color}">\u25CF</span>' + | |
// '{series.name}: <b>{point.y}</b>' | |
//valueDecimals: null, | |
//xDateFormat: '%A, %b %e, %Y', | |
//valuePrefix: '', | |
//ySuffix: '' | |
//} | |
turboThreshold: 1000, | |
// zIndex: null | |
findNearestPointBy: 'x' | |
}, /** @lends Highcharts.Series.prototype */ { | |
isCartesian: true, | |
pointClass: Point, | |
sorted: true, // requires the data to be sorted | |
requireSorting: true, | |
directTouch: false, | |
axisTypes: ['xAxis', 'yAxis'], | |
colorCounter: 0, | |
// each point's x and y values are stored in this.xData and this.yData | |
parallelArrays: ['x', 'y'], | |
coll: 'series', | |
init: function(chart, options) { | |
var series = this, | |
events, | |
chartSeries = chart.series, | |
lastSeries; | |
/** | |
* Read only. The chart that the series belongs to. | |
* | |
* @name chart | |
* @memberOf Series | |
* @type {Chart} | |
*/ | |
series.chart = chart; | |
/** | |
* Read only. The series' type, like "line", "area", "column" etc. The | |
* type in the series options anc can be altered using {@link | |
* Series#update}. | |
* | |
* @name type | |
* @memberOf Series | |
* @type String | |
*/ | |
/** | |
* Read only. The series' current options. To update, use {@link | |
* Series#update}. | |
* | |
* @name options | |
* @memberOf Series | |
* @type SeriesOptions | |
*/ | |
series.options = options = series.setOptions(options); | |
series.linkedSeries = []; | |
// bind the axes | |
series.bindAxes(); | |
// set some variables | |
extend(series, { | |
/** | |
* The series name as given in the options. Defaults to | |
* "Series {n}". | |
* | |
* @name name | |
* @memberOf Series | |
* @type {String} | |
*/ | |
name: options.name, | |
state: '', | |
/** | |
* Read only. The series' visibility state as set by {@link | |
* Series#show}, {@link Series#hide}, or in the initial | |
* configuration. | |
* | |
* @name visible | |
* @memberOf Series | |
* @type {Boolean} | |
*/ | |
visible: options.visible !== false, // true by default | |
/** | |
* Read only. The series' selected state as set by {@link | |
* Highcharts.Series#select}. | |
* | |
* @name selected | |
* @memberOf Series | |
* @type {Boolean} | |
*/ | |
selected: options.selected === true // false by default | |
}); | |
// register event listeners | |
events = options.events; | |
objectEach(events, function(event, eventType) { | |
addEvent(series, eventType, event); | |
}); | |
if ( | |
(events && events.click) || | |
( | |
options.point && | |
options.point.events && | |
options.point.events.click | |
) || | |
options.allowPointSelect | |
) { | |
chart.runTrackerClick = true; | |
} | |
series.getColor(); | |
series.getSymbol(); | |
// Set the data | |
each(series.parallelArrays, function(key) { | |
series[key + 'Data'] = []; | |
}); | |
series.setData(options.data, false); | |
// Mark cartesian | |
if (series.isCartesian) { | |
chart.hasCartesianSeries = true; | |
} | |
// Get the index and register the series in the chart. The index is one | |
// more than the current latest series index (#5960). | |
if (chartSeries.length) { | |
lastSeries = chartSeries[chartSeries.length - 1]; | |
} | |
series._i = pick(lastSeries && lastSeries._i, -1) + 1; | |
// Insert the series and re-order all series above the insertion point. | |
chart.orderSeries(this.insert(chartSeries)); | |
}, | |
/** | |
* Insert the series in a collection with other series, either the chart | |
* series or yAxis series, in the correct order according to the index | |
* option. | |
* @param {Array} collection A collection of series. | |
* @returns {Number} The index of the series in the collection. | |
*/ | |
insert: function(collection) { | |
var indexOption = this.options.index, | |
i; | |
// Insert by index option | |
if (isNumber(indexOption)) { | |
i = collection.length; | |
while (i--) { | |
// Loop down until the interted element has higher index | |
if (indexOption >= | |
pick(collection[i].options.index, collection[i]._i)) { | |
collection.splice(i + 1, 0, this); | |
break; | |
} | |
} | |
if (i === -1) { | |
collection.unshift(this); | |
} | |
i = i + 1; | |
// Or just push it to the end | |
} else { | |
collection.push(this); | |
} | |
return pick(i, collection.length - 1); | |
}, | |
/** | |
* Set the xAxis and yAxis properties of cartesian series, and register the | |
* series in the `axis.series` array. | |
* | |
* @function #bindAxes | |
* @memberOf Series | |
* @returns {void} | |
*/ | |
bindAxes: function() { | |
var series = this, | |
seriesOptions = series.options, | |
chart = series.chart, | |
axisOptions; | |
// repeat for xAxis and yAxis | |
each(series.axisTypes || [], function(AXIS) { | |
// loop through the chart's axis objects | |
each(chart[AXIS], function(axis) { | |
axisOptions = axis.options; | |
// apply if the series xAxis or yAxis option mathches the number | |
// of the axis, or if undefined, use the first axis | |
if ( | |
seriesOptions[AXIS] === axisOptions.index || | |
( | |
seriesOptions[AXIS] !== undefined && | |
seriesOptions[AXIS] === axisOptions.id | |
) || | |
( | |
seriesOptions[AXIS] === undefined && | |
axisOptions.index === 0 | |
) | |
) { | |
// register this series in the axis.series lookup | |
series.insert(axis.series); | |
// set this series.xAxis or series.yAxis reference | |
/** | |
* Read only. The unique xAxis object associated with the | |
* series. | |
* | |
* @name xAxis | |
* @memberOf Series | |
* @type Axis | |
*/ | |
/** | |
* Read only. The unique yAxis object associated with the | |
* series. | |
* | |
* @name yAxis | |
* @memberOf Series | |
* @type Axis | |
*/ | |
series[AXIS] = axis; | |
// mark dirty for redraw | |
axis.isDirty = true; | |
} | |
}); | |
// The series needs an X and an Y axis | |
if (!series[AXIS] && series.optionalAxis !== AXIS) { | |
H.error(18, true); | |
} | |
}); | |
}, | |
/** | |
* For simple series types like line and column, the data values are held in | |
* arrays like xData and yData for quick lookup to find extremes and more. | |
* For multidimensional series like bubble and map, this can be extended | |
* with arrays like zData and valueData by adding to the | |
* series.parallelArrays array. | |
*/ | |
updateParallelArrays: function(point, i) { | |
var series = point.series, | |
args = arguments, | |
fn = isNumber(i) ? | |
// Insert the value in the given position | |
function(key) { | |
var val = key === 'y' && series.toYData ? | |
series.toYData(point) : | |
point[key]; | |
series[key + 'Data'][i] = val; | |
} : | |
// Apply the method specified in i with the following arguments | |
// as arguments | |
function(key) { | |
Array.prototype[i].apply( | |
series[key + 'Data'], | |
Array.prototype.slice.call(args, 2) | |
); | |
}; | |
each(series.parallelArrays, fn); | |
}, | |
/** | |
* Return an auto incremented x value based on the pointStart and | |
* pointInterval options. This is only used if an x value is not given for | |
* the point that calls autoIncrement. | |
*/ | |
autoIncrement: function() { | |
var options = this.options, | |
xIncrement = this.xIncrement, | |
date, | |
pointInterval, | |
pointIntervalUnit = options.pointIntervalUnit; | |
xIncrement = pick(xIncrement, options.pointStart, 0); | |
this.pointInterval = pointInterval = pick( | |
this.pointInterval, | |
options.pointInterval, | |
1 | |
); | |
// Added code for pointInterval strings | |
if (pointIntervalUnit) { | |
date = new Date(xIncrement); | |
if (pointIntervalUnit === 'day') { | |
date = +date[Date.hcSetDate]( | |
date[Date.hcGetDate]() + pointInterval | |
); | |
} else if (pointIntervalUnit === 'month') { | |
date = +date[Date.hcSetMonth]( | |
date[Date.hcGetMonth]() + pointInterval | |
); | |
} else if (pointIntervalUnit === 'year') { | |
date = +date[Date.hcSetFullYear]( | |
date[Date.hcGetFullYear]() + pointInterval | |
); | |
} | |
pointInterval = date - xIncrement; | |
} | |
this.xIncrement = xIncrement + pointInterval; | |
return xIncrement; | |
}, | |
/** | |
* Set the series options by merging from the options tree | |
* @param {Object} itemOptions | |
*/ | |
setOptions: function(itemOptions) { | |
var chart = this.chart, | |
chartOptions = chart.options, | |
plotOptions = chartOptions.plotOptions, | |
userOptions = chart.userOptions || {}, | |
userPlotOptions = userOptions.plotOptions || {}, | |
typeOptions = plotOptions[this.type], | |
options, | |
zones; | |
this.userOptions = itemOptions; | |
// General series options take precedence over type options because | |
// otherwise, default type options like column.animation would be | |
// overwritten by the general option. But issues have been raised here | |
// (#3881), and the solution may be to distinguish between default | |
// option and userOptions like in the tooltip below. | |
options = merge( | |
typeOptions, | |
plotOptions.series, | |
itemOptions | |
); | |
// The tooltip options are merged between global and series specific | |
// options. Importance order asscendingly: | |
// globals: (1)tooltip, (2)plotOptions.series, (3)plotOptions[this.type] | |
// init userOptions with possible later updates: 4-6 like 1-3 and | |
// (7)this series options | |
this.tooltipOptions = merge( | |
defaultOptions.tooltip, // 1 | |
defaultOptions.plotOptions.series && | |
defaultOptions.plotOptions.series.tooltip, // 2 | |
defaultOptions.plotOptions[this.type].tooltip, // 3 | |
chartOptions.tooltip.userOptions, // 4 | |
plotOptions.series && plotOptions.series.tooltip, // 5 | |
plotOptions[this.type].tooltip, // 6 | |
itemOptions.tooltip // 7 | |
); | |
// When shared tooltip, stickyTracking is true by default, | |
// unless user says otherwise. | |
this.stickyTracking = pick( | |
itemOptions.stickyTracking, | |
userPlotOptions[this.type] && | |
userPlotOptions[this.type].stickyTracking, | |
userPlotOptions.series && userPlotOptions.series.stickyTracking, | |
( | |
this.tooltipOptions.shared && !this.noSharedTooltip ? | |
true : | |
options.stickyTracking | |
) | |
); | |
// Delete marker object if not allowed (#1125) | |
if (typeOptions.marker === null) { | |
delete options.marker; | |
} | |
// Handle color zones | |
this.zoneAxis = options.zoneAxis; | |
zones = this.zones = (options.zones || []).slice(); | |
if ( | |
(options.negativeColor || options.negativeFillColor) && | |
!options.zones | |
) { | |
zones.push({ | |
value: options[this.zoneAxis + 'Threshold'] || | |
options.threshold || | |
0, | |
className: 'highcharts-negative' | |
}); | |
} | |
if (zones.length) { // Push one extra zone for the rest | |
if (defined(zones[zones.length - 1].value)) { | |
zones.push({ | |
}); | |
} | |
} | |
return options; | |
}, | |
getCyclic: function(prop, value, defaults) { | |
var i, | |
chart = this.chart, | |
userOptions = this.userOptions, | |
indexName = prop + 'Index', | |
counterName = prop + 'Counter', | |
len = defaults ? defaults.length : pick( | |
chart.options.chart[prop + 'Count'], | |
chart[prop + 'Count'] | |
), | |
setting; | |
if (!value) { | |
// Pick up either the colorIndex option, or the _colorIndex after | |
// Series.update() | |
setting = pick( | |
userOptions[indexName], | |
userOptions['_' + indexName] | |
); | |
if (defined(setting)) { // after Series.update() | |
i = setting; | |
} else { | |
// #6138 | |
if (!chart.series.length) { | |
chart[counterName] = 0; | |
} | |
userOptions['_' + indexName] = i = chart[counterName] % len; | |
chart[counterName] += 1; | |
} | |
if (defaults) { | |
value = defaults[i]; | |
} | |
} | |
// Set the colorIndex | |
if (i !== undefined) { | |
this[indexName] = i; | |
} | |
this[prop] = value; | |
}, | |
/** | |
* Get the series' color | |
*/ | |
getColor: function() { | |
this.getCyclic('color'); | |
}, | |
/** | |
* Get the series' symbol | |
*/ | |
getSymbol: function() { | |
var seriesMarkerOption = this.options.marker; | |
this.getCyclic( | |
'symbol', | |
seriesMarkerOption.symbol, | |
this.chart.options.symbols | |
); | |
}, | |
drawLegendSymbol: LegendSymbolMixin.drawLineMarker, | |
/** | |
* Apply a new set of data to the series and optionally redraw it. The new | |
* data array is passed by reference (except in case of `updatePoints`), and | |
* may later be mutated when updating the chart data. | |
* | |
* Note the difference in behaviour when setting the same amount of points, | |
* or a different amount of points, as handled by the `updatePoints` | |
* parameter. | |
* | |
* @param {SeriesDataOptions} data | |
* Takes an array of data in the same format as described under | |
* `series<type>data` for the given series type. | |
* @param {Boolean} [redraw=true] | |
* Whether to redraw the chart after the series is altered. If doing | |
* more operations on the chart, it is a good idea to set redraw to | |
* false and call {@link Chart#redraw} after. | |
* @param {AnimationOptions} [animation] | |
* When the updated data is the same length as the existing data, | |
* points will be updated by default, and animation visualizes how | |
* the points are changed. Set false to disable animation, or a | |
* configuration object to set duration or easing. | |
* @param {Boolean} [updatePoints=true] | |
* When the updated data is the same length as the existing data, | |
* points will be updated instead of replaced. This allows updating | |
* with animation and performs better. In this case, the original | |
* array is not passed by reference. Set false to prevent. | |
* | |
* @sample highcharts/members/series-setdata/ | |
* Set new data from a button | |
* @sample highcharts/members/series-setdata-pie/ | |
* Set data in a pie | |
* @sample stock/members/series-setdata/ | |
* Set new data in Highstock | |
* @sample maps/members/series-setdata/ | |
* Set new data in Highmaps | |
*/ | |
setData: function(data, redraw, animation, updatePoints) { | |
var series = this, | |
oldData = series.points, | |
oldDataLength = (oldData && oldData.length) || 0, | |
dataLength, | |
options = series.options, | |
chart = series.chart, | |
firstPoint = null, | |
xAxis = series.xAxis, | |
i, | |
turboThreshold = options.turboThreshold, | |
pt, | |
xData = this.xData, | |
yData = this.yData, | |
pointArrayMap = series.pointArrayMap, | |
valueCount = pointArrayMap && pointArrayMap.length; | |
data = data || []; | |
dataLength = data.length; | |
redraw = pick(redraw, true); | |
// If the point count is the same as is was, just run Point.update which | |
// is cheaper, allows animation, and keeps references to points. | |
if ( | |
updatePoints !== false && | |
dataLength && | |
oldDataLength === dataLength && | |
!series.cropped && | |
!series.hasGroupedData && | |
series.visible | |
) { | |
each(data, function(point, i) { | |
// .update doesn't exist on a linked, hidden series (#3709) | |
if (oldData[i].update && point !== options.data[i]) { | |
oldData[i].update(point, false, null, false); | |
} | |
}); | |
} else { | |
// Reset properties | |
series.xIncrement = null; | |
series.colorCounter = 0; // for series with colorByPoint (#1547) | |
// Update parallel arrays | |
each(this.parallelArrays, function(key) { | |
series[key + 'Data'].length = 0; | |
}); | |
// In turbo mode, only one- or twodimensional arrays of numbers are | |
// allowed. The first value is tested, and we assume that all the | |
// rest are defined the same way. Although the 'for' loops are | |
// similar, they are repeated inside each if-else conditional for | |
// max performance. | |
if (turboThreshold && dataLength > turboThreshold) { | |
// find the first non-null point | |
i = 0; | |
while (firstPoint === null && i < dataLength) { | |
firstPoint = data[i]; | |
i++; | |
} | |
if (isNumber(firstPoint)) { // assume all points are numbers | |
for (i = 0; i < dataLength; i++) { | |
xData[i] = this.autoIncrement(); | |
yData[i] = data[i]; | |
} | |
// Assume all points are arrays when first point is | |
} else if (isArray(firstPoint)) { | |
if (valueCount) { // [x, low, high] or [x, o, h, l, c] | |
for (i = 0; i < dataLength; i++) { | |
pt = data[i]; | |
xData[i] = pt[0]; | |
yData[i] = pt.slice(1, valueCount + 1); | |
} | |
} else { // [x, y] | |
for (i = 0; i < dataLength; i++) { | |
pt = data[i]; | |
xData[i] = pt[0]; | |
yData[i] = pt[1]; | |
} | |
} | |
} else { | |
// Highcharts expects configs to be numbers or arrays in | |
// turbo mode | |
H.error(12); | |
} | |
} else { | |
for (i = 0; i < dataLength; i++) { | |
if (data[i] !== undefined) { // stray commas in oldIE | |
pt = { | |
series: series | |
}; | |
series.pointClass.prototype.applyOptions.apply( | |
pt, [data[i]] | |
); | |
series.updateParallelArrays(pt, i); | |
} | |
} | |
} | |
// Forgetting to cast strings to numbers is a common caveat when | |
// handling CSV or JSON | |
if (isString(yData[0])) { | |
H.error(14, true); | |
} | |
/** | |
* Read only. An array containing the series' data point objects. To | |
* modify the data, use {@link Highcharts.Series#setData} or {@link | |
* Highcharts.Point#update}. | |
* | |
* @name data | |
* @memberOf Highcharts.Series | |
* @type {Array.<Highcharts.Point>} | |
*/ | |
series.data = []; | |
series.options.data = series.userOptions.data = data; | |
// destroy old points | |
i = oldDataLength; | |
while (i--) { | |
if (oldData[i] && oldData[i].destroy) { | |
oldData[i].destroy(); | |
} | |
} | |
// reset minRange (#878) | |
if (xAxis) { | |
xAxis.minRange = xAxis.userMinRange; | |
} | |
// redraw | |
series.isDirty = chart.isDirtyBox = true; | |
series.isDirtyData = !!oldData; | |
animation = false; | |
} | |
// Typically for pie series, points need to be processed and generated | |
// prior to rendering the legend | |
if (options.legendType === 'point') { | |
this.processData(); | |
this.generatePoints(); | |
} | |
if (redraw) { | |
chart.redraw(animation); | |
} | |
}, | |
/** | |
* Process the data by cropping away unused data points if the series is | |
* longer than the crop threshold. This saves computing time for large | |
* series. | |
*/ | |
processData: function(force) { | |
var series = this, | |
processedXData = series.xData, // copied during slice operation | |
processedYData = series.yData, | |
dataLength = processedXData.length, | |
croppedData, | |
cropStart = 0, | |
cropped, | |
distance, | |
closestPointRange, | |
xAxis = series.xAxis, | |
i, // loop variable | |
options = series.options, | |
cropThreshold = options.cropThreshold, | |
getExtremesFromAll = | |
series.getExtremesFromAll || | |
options.getExtremesFromAll, // #4599 | |
isCartesian = series.isCartesian, | |
xExtremes, | |
val2lin = xAxis && xAxis.val2lin, | |
isLog = xAxis && xAxis.isLog, | |
min, | |
max; | |
// If the series data or axes haven't changed, don't go through this. | |
// Return false to pass the message on to override methods like in data | |
// grouping. | |
if ( | |
isCartesian && | |
!series.isDirty && | |
!xAxis.isDirty && | |
!series.yAxis.isDirty && | |
!force | |
) { | |
return false; | |
} | |
if (xAxis) { | |
xExtremes = xAxis.getExtremes(); // corrected for log axis (#3053) | |
min = xExtremes.min; | |
max = xExtremes.max; | |
} | |
// optionally filter out points outside the plot area | |
if ( | |
isCartesian && | |
series.sorted && | |
!getExtremesFromAll && | |
(!cropThreshold || dataLength > cropThreshold || series.forceCrop) | |
) { | |
// it's outside current extremes | |
if ( | |
processedXData[dataLength - 1] < min || | |
processedXData[0] > max | |
) { | |
processedXData = []; | |
processedYData = []; | |
// only crop if it's actually spilling out | |
} else if ( | |
processedXData[0] < min || | |
processedXData[dataLength - 1] > max | |
) { | |
croppedData = this.cropData( | |
series.xData, | |
series.yData, | |
min, | |
max | |
); | |
processedXData = croppedData.xData; | |
processedYData = croppedData.yData; | |
cropStart = croppedData.start; | |
cropped = true; | |
} | |
} | |
// Find the closest distance between processed points | |
i = processedXData.length || 1; | |
while (--i) { | |
distance = isLog ? | |
val2lin(processedXData[i]) - val2lin(processedXData[i - 1]) : | |
processedXData[i] - processedXData[i - 1]; | |
if ( | |
distance > 0 && | |
( | |
closestPointRange === undefined || | |
distance < closestPointRange | |
) | |
) { | |
closestPointRange = distance; | |
// Unsorted data is not supported by the line tooltip, as well as | |
// data grouping and navigation in Stock charts (#725) and width | |
// calculation of columns (#1900) | |
} else if (distance < 0 && series.requireSorting) { | |
H.error(15); | |
} | |
} | |
// Record the properties | |
series.cropped = cropped; // undefined or true | |
series.cropStart = cropStart; | |
series.processedXData = processedXData; | |
series.processedYData = processedYData; | |
series.closestPointRange = closestPointRange; | |
}, | |
/** | |
* Iterate over xData and crop values between min and max. Returns object | |
* containing crop start/end cropped xData with corresponding part of yData, | |
* dataMin and dataMax within the cropped range | |
*/ | |
cropData: function(xData, yData, min, max) { | |
var dataLength = xData.length, | |
cropStart = 0, | |
cropEnd = dataLength, | |
// line-type series need one point outside | |
cropShoulder = pick(this.cropShoulder, 1), | |
i, | |
j; | |
// iterate up to find slice start | |
for (i = 0; i < dataLength; i++) { | |
if (xData[i] >= min) { | |
cropStart = Math.max(0, i - cropShoulder); | |
break; | |
} | |
} | |
// proceed to find slice end | |
for (j = i; j < dataLength; j++) { | |
if (xData[j] > max) { | |
cropEnd = j + cropShoulder; | |
break; | |
} | |
} | |
return { | |
xData: xData.slice(cropStart, cropEnd), | |
yData: yData.slice(cropStart, cropEnd), | |
start: cropStart, | |
end: cropEnd | |
}; | |
}, | |
/** | |
* Generate the data point after the data has been processed by cropping | |
* away unused points and optionally grouped in Highcharts Stock. | |
*/ | |
generatePoints: function() { | |
var series = this, | |
options = series.options, | |
dataOptions = options.data, | |
data = series.data, | |
dataLength, | |
processedXData = series.processedXData, | |
processedYData = series.processedYData, | |
PointClass = series.pointClass, | |
processedDataLength = processedXData.length, | |
cropStart = series.cropStart || 0, | |
cursor, | |
hasGroupedData = series.hasGroupedData, | |
keys = options.keys, | |
point, | |
points = [], | |
i; | |
if (!data && !hasGroupedData) { | |
var arr = []; | |
arr.length = dataOptions.length; | |
data = series.data = arr; | |
} | |
if (keys && hasGroupedData) { | |
// grouped data has already applied keys (#6590) | |
series.options.keys = false; | |
} | |
for (i = 0; i < processedDataLength; i++) { | |
cursor = cropStart + i; | |
if (!hasGroupedData) { | |
point = data[cursor]; | |
if (!point && dataOptions[cursor] !== undefined) { // #970 | |
data[cursor] = point = (new PointClass()).init( | |
series, | |
dataOptions[cursor], | |
processedXData[i] | |
); | |
} | |
} else { | |
// splat the y data in case of ohlc data array | |
point = (new PointClass()).init( | |
series, [processedXData[i]].concat(splat(processedYData[i])) | |
); | |
/** | |
* Highstock only. If a point object is created by data | |
* grouping, it doesn't reflect actual points in the raw data. | |
* In this case, the `dataGroup` property holds information | |
* that points back to the raw data. | |
* | |
* - `dataGroup.start` is the index of the first raw data point | |
* in the group. | |
* - `dataGroup.length` is the amount of points in the group. | |
* | |
* @name dataGroup | |
* @memberOf Point | |
* @type {Object} | |
* | |
*/ | |
point.dataGroup = series.groupMap[i]; | |
} | |
if (point) { // #6279 | |
point.index = cursor; // For faster access in Point.update | |
points[i] = point; | |
} | |
} | |
// restore keys options (#6590) | |
series.options.keys = keys; | |
// Hide cropped-away points - this only runs when the number of points | |
// is above cropThreshold, or when swithching view from non-grouped | |
// data to grouped data (#637) | |
if ( | |
data && | |
( | |
processedDataLength !== (dataLength = data.length) || | |
hasGroupedData | |
) | |
) { | |
for (i = 0; i < dataLength; i++) { | |
// when has grouped data, clear all points | |
if (i === cropStart && !hasGroupedData) { | |
i += processedDataLength; | |
} | |
if (data[i]) { | |
data[i].destroyElements(); | |
data[i].plotX = undefined; // #1003 | |
} | |
} | |
} | |
series.data = data; | |
series.points = points; | |
}, | |
/** | |
* Calculate Y extremes for visible data | |
*/ | |
getExtremes: function(yData) { | |
var xAxis = this.xAxis, | |
yAxis = this.yAxis, | |
xData = this.processedXData, | |
yDataLength, | |
activeYData = [], | |
activeCounter = 0, | |
// #2117, need to compensate for log X axis | |
xExtremes = xAxis.getExtremes(), | |
xMin = xExtremes.min, | |
xMax = xExtremes.max, | |
validValue, | |
withinRange, | |
x, | |
y, | |
i, | |
j; | |
yData = yData || this.stackedYData || this.processedYData || []; | |
yDataLength = yData.length; | |
for (i = 0; i < yDataLength; i++) { | |
x = xData[i]; | |
y = yData[i]; | |
// For points within the visible range, including the first point | |
// outside the visible range, consider y extremes | |
validValue = | |
(isNumber(y, true) || isArray(y)) && | |
(!yAxis.positiveValuesOnly || (y.length || y > 0)); | |
withinRange = | |
this.getExtremesFromAll || | |
this.options.getExtremesFromAll || | |
this.cropped || | |
((xData[i] || x) >= xMin && (xData[i] || x) <= xMax); | |
if (validValue && withinRange) { | |
j = y.length; | |
if (j) { // array, like ohlc or range data | |
while (j--) { | |
if (y[j] !== null) { | |
activeYData[activeCounter++] = y[j]; | |
} | |
} | |
} else { | |
activeYData[activeCounter++] = y; | |
} | |
} | |
} | |
this.dataMin = arrayMin(activeYData); | |
this.dataMax = arrayMax(activeYData); | |
}, | |
/** | |
* Translate data points from raw data values to chart specific positioning | |
* data needed later in drawPoints, drawGraph and drawTracker. | |
* | |
* @function #translate | |
* @memberOf Series | |
* @returns {void} | |
*/ | |
translate: function() { | |
if (!this.processedXData) { // hidden series | |
this.processData(); | |
} | |
this.generatePoints(); | |
var series = this, | |
options = series.options, | |
stacking = options.stacking, | |
xAxis = series.xAxis, | |
categories = xAxis.categories, | |
yAxis = series.yAxis, | |
points = series.points, | |
dataLength = points.length, | |
hasModifyValue = !!series.modifyValue, | |
i, | |
pointPlacement = options.pointPlacement, | |
dynamicallyPlaced = | |
pointPlacement === 'between' || | |
isNumber(pointPlacement), | |
threshold = options.threshold, | |
stackThreshold = options.startFromThreshold ? threshold : 0, | |
plotX, | |
plotY, | |
lastPlotX, | |
stackIndicator, | |
closestPointRangePx = Number.MAX_VALUE; | |
// Point placement is relative to each series pointRange (#5889) | |
if (pointPlacement === 'between') { | |
pointPlacement = 0.5; | |
} | |
if (isNumber(pointPlacement)) { | |
pointPlacement *= pick(options.pointRange || xAxis.pointRange); | |
} | |
// Translate each point | |
for (i = 0; i < dataLength; i++) { | |
var point = points[i], | |
xValue = point.x, | |
yValue = point.y, | |
yBottom = point.low, | |
stack = stacking && yAxis.stacks[( | |
series.negStacks && | |
yValue < (stackThreshold ? 0 : threshold) ? '-' : '' | |
) + series.stackKey], | |
pointStack, | |
stackValues; | |
// Discard disallowed y values for log axes (#3434) | |
if (yAxis.positiveValuesOnly && yValue !== null && yValue <= 0) { | |
point.isNull = true; | |
} | |
// Get the plotX translation | |
point.plotX = plotX = correctFloat( // #5236 | |
Math.min(Math.max(-1e5, xAxis.translate( | |
xValue, | |
0, | |
0, | |
0, | |
1, | |
pointPlacement, | |
this.type === 'flags' | |
)), 1e5) // #3923 | |
); | |
// Calculate the bottom y value for stacked series | |
if ( | |
stacking && | |
series.visible && | |
!point.isNull && | |
stack && | |
stack[xValue] | |
) { | |
stackIndicator = series.getStackIndicator( | |
stackIndicator, | |
xValue, | |
series.index | |
); | |
pointStack = stack[xValue]; | |
stackValues = pointStack.points[stackIndicator.key]; | |
yBottom = stackValues[0]; | |
yValue = stackValues[1]; | |
if ( | |
yBottom === stackThreshold && | |
stackIndicator.key === stack[xValue].base | |
) { | |
yBottom = pick(threshold, yAxis.min); | |
} | |
if (yAxis.positiveValuesOnly && yBottom <= 0) { // #1200, #1232 | |
yBottom = null; | |
} | |
point.total = point.stackTotal = pointStack.total; | |
point.percentage = | |
pointStack.total && | |
(point.y / pointStack.total * 100); | |
point.stackY = yValue; | |
// Place the stack label | |
pointStack.setOffset( | |
series.pointXOffset || 0, | |
series.barW || 0 | |
); | |
} | |
// Set translated yBottom or remove it | |
point.yBottom = defined(yBottom) ? | |
yAxis.translate(yBottom, 0, 1, 0, 1) : | |
null; | |
// general hook, used for Highstock compare mode | |
if (hasModifyValue) { | |
yValue = series.modifyValue(yValue, point); | |
} | |
// Set the the plotY value, reset it for redraws | |
point.plotY = plotY = | |
(typeof yValue === 'number' && yValue !== Infinity) ? | |
Math.min(Math.max(-1e5, | |
yAxis.translate(yValue, 0, 1, 0, 1)), 1e5) : // #3201 | |
undefined; | |
point.isInside = | |
plotY !== undefined && | |
plotY >= 0 && | |
plotY <= yAxis.len && // #3519 | |
plotX >= 0 && | |
plotX <= xAxis.len; | |
// Set client related positions for mouse tracking | |
point.clientX = dynamicallyPlaced ? | |
correctFloat( | |
xAxis.translate(xValue, 0, 0, 0, 1, pointPlacement) | |
) : | |
plotX; // #1514, #5383, #5518 | |
point.negative = point.y < (threshold || 0); | |
// some API data | |
point.category = categories && categories[point.x] !== undefined ? | |
categories[point.x] : point.x; | |
// Determine auto enabling of markers (#3635, #5099) | |
if (!point.isNull) { | |
if (lastPlotX !== undefined) { | |
closestPointRangePx = Math.min( | |
closestPointRangePx, | |
Math.abs(plotX - lastPlotX) | |
); | |
} | |
lastPlotX = plotX; | |
} | |
// Find point zone | |
point.zone = this.zones.length && point.getZone(); | |
} | |
series.closestPointRangePx = closestPointRangePx; | |
}, | |
/** | |
* Return the series points with null points filtered out | |
*/ | |
getValidPoints: function(points, insideOnly) { | |
var chart = this.chart; | |
// #3916, #5029, #5085 | |
return grep(points || this.points || [], function isValidPoint(point) { | |
if (insideOnly && !chart.isInsidePlot( | |
point.plotX, | |
point.plotY, | |
chart.inverted | |
)) { | |
return false; | |
} | |
return !point.isNull; | |
}); | |
}, | |
/** | |
* Set the clipping for the series. For animated series it is called twice, | |
* first to initiate animating the clip then the second time without the | |
* animation to set the final clip. | |
*/ | |
setClip: function(animation) { | |
var chart = this.chart, | |
options = this.options, | |
renderer = chart.renderer, | |
inverted = chart.inverted, | |
seriesClipBox = this.clipBox, | |
clipBox = seriesClipBox || chart.clipBox, | |
sharedClipKey = | |
this.sharedClipKey || [ | |
'_sharedClip', | |
animation && animation.duration, | |
animation && animation.easing, | |
clipBox.height, | |
options.xAxis, | |
options.yAxis | |
].join(','), // #4526 | |
clipRect = chart[sharedClipKey], | |
markerClipRect = chart[sharedClipKey + 'm']; | |
// If a clipping rectangle with the same properties is currently present | |
// in the chart, use that. | |
if (!clipRect) { | |
// When animation is set, prepare the initial positions | |
if (animation) { | |
clipBox.width = 0; | |
chart[sharedClipKey + 'm'] = markerClipRect = renderer.clipRect(-99, // include the width of the first marker | |
inverted ? -chart.plotLeft : -chart.plotTop, | |
99, | |
inverted ? chart.chartWidth : chart.chartHeight | |
); | |
} | |
chart[sharedClipKey] = clipRect = renderer.clipRect(clipBox); | |
// Create hashmap for series indexes | |
clipRect.count = { | |
length: 0 | |
}; | |
} | |
if (animation) { | |
if (!clipRect.count[this.index]) { | |
clipRect.count[this.index] = true; | |
clipRect.count.length += 1; | |
} | |
} | |
if (options.clip !== false) { | |
this.group.clip(animation || seriesClipBox ? clipRect : chart.clipRect); | |
this.markerGroup.clip(markerClipRect); | |
this.sharedClipKey = sharedClipKey; | |
} | |
// Remove the shared clipping rectangle when all series are shown | |
if (!animation) { | |
if (clipRect.count[this.index]) { | |
delete clipRect.count[this.index]; | |
clipRect.count.length -= 1; | |
} | |
if (clipRect.count.length === 0 && sharedClipKey && chart[sharedClipKey]) { | |
if (!seriesClipBox) { | |
chart[sharedClipKey] = chart[sharedClipKey].destroy(); | |
} | |
if (chart[sharedClipKey + 'm']) { | |
chart[sharedClipKey + 'm'] = chart[sharedClipKey + 'm'].destroy(); | |
} | |
} | |
} | |
}, | |
/** | |
* Animate in the series | |
*/ | |
animate: function(init) { | |
var series = this, | |
chart = series.chart, | |
clipRect, | |
animation = animObject(series.options.animation), | |
sharedClipKey; | |
// Initialize the animation. Set up the clipping rectangle. | |
if (init) { | |
series.setClip(animation); | |
// Run the animation | |
} else { | |
sharedClipKey = this.sharedClipKey; | |
clipRect = chart[sharedClipKey]; | |
if (clipRect) { | |
clipRect.animate({ | |
width: chart.plotSizeX | |
}, animation); | |
} | |
if (chart[sharedClipKey + 'm']) { | |
chart[sharedClipKey + 'm'].animate({ | |
width: chart.plotSizeX + 99 | |
}, animation); | |
} | |
// Delete this function to allow it only once | |
series.animate = null; | |
} | |
}, | |
/** | |
* This runs after animation to land on the final plot clipping | |
*/ | |
afterAnimate: function() { | |
this.setClip(); | |
fireEvent(this, 'afterAnimate'); | |
}, | |
/** | |
* Draw the markers. | |
* | |
* @function #drawPoints | |
* @memberOf Series | |
* @returns {void} | |
*/ | |
drawPoints: function() { | |
var series = this, | |
points = series.points, | |
chart = series.chart, | |
plotY, | |
i, | |
point, | |
symbol, | |
graphic, | |
options = series.options, | |
seriesMarkerOptions = options.marker, | |
pointMarkerOptions, | |
hasPointMarker, | |
enabled, | |
isInside, | |
markerGroup = series[series.specialGroup] || series.markerGroup, | |
xAxis = series.xAxis, | |
markerAttribs, | |
globallyEnabled = pick( | |
seriesMarkerOptions.enabled, | |
xAxis.isRadial ? true : null, | |
// Use larger or equal as radius is null in bubbles (#6321) | |
series.closestPointRangePx >= 2 * seriesMarkerOptions.radius | |
); | |
if (seriesMarkerOptions.enabled !== false || series._hasPointMarkers) { | |
for (i = 0; i < points.length; i++) { | |
point = points[i]; | |
plotY = point.plotY; | |
graphic = point.graphic; | |
pointMarkerOptions = point.marker || {}; | |
hasPointMarker = !!point.marker; | |
enabled = (globallyEnabled && pointMarkerOptions.enabled === undefined) || pointMarkerOptions.enabled; | |
isInside = point.isInside; | |
// only draw the point if y is defined | |
if (enabled && isNumber(plotY) && point.y !== null) { | |
// Shortcuts | |
symbol = pick(pointMarkerOptions.symbol, series.symbol); | |
point.hasImage = symbol.indexOf('url') === 0; | |
markerAttribs = series.markerAttribs( | |
point, | |
point.selected && 'select' | |
); | |
if (graphic) { // update | |
graphic[isInside ? 'show' : 'hide'](true) // Since the marker group isn't clipped, each individual marker must be toggled | |
.animate(markerAttribs); | |
} else if (isInside && (markerAttribs.width > 0 || point.hasImage)) { | |
point.graphic = graphic = chart.renderer.symbol( | |
symbol, | |
markerAttribs.x, | |
markerAttribs.y, | |
markerAttribs.width, | |
markerAttribs.height, | |
hasPointMarker ? pointMarkerOptions : seriesMarkerOptions | |
) | |
.add(markerGroup); | |
} | |
if (graphic) { | |
graphic.addClass(point.getClassName(), true); | |
} | |
} else if (graphic) { | |
point.graphic = graphic.destroy(); // #1269 | |
} | |
} | |
} | |
}, | |
/** | |
* Get non-presentational attributes for the point. | |
*/ | |
markerAttribs: function(point, state) { | |
var seriesMarkerOptions = this.options.marker, | |
seriesStateOptions, | |
pointMarkerOptions = point.marker || {}, | |
pointStateOptions, | |
radius = pick( | |
pointMarkerOptions.radius, | |
seriesMarkerOptions.radius | |
), | |
attribs; | |
// Handle hover and select states | |
if (state) { | |
seriesStateOptions = seriesMarkerOptions.states[state]; | |
pointStateOptions = pointMarkerOptions.states && | |
pointMarkerOptions.states[state]; | |
radius = pick( | |
pointStateOptions && pointStateOptions.radius, | |
seriesStateOptions && seriesStateOptions.radius, | |
radius + (seriesStateOptions && seriesStateOptions.radiusPlus || 0) | |
); | |
} | |
if (point.hasImage) { | |
radius = 0; // and subsequently width and height is not set | |
} | |
attribs = { | |
x: Math.floor(point.plotX) - radius, // Math.floor for #1843 | |
y: point.plotY - radius | |
}; | |
if (radius) { | |
attribs.width = attribs.height = 2 * radius; | |
} | |
return attribs; | |
}, | |
/** | |
* Clear DOM objects and free up memory | |
*/ | |
destroy: function() { | |
var series = this, | |
chart = series.chart, | |
issue134 = /AppleWebKit\/533/.test(win.navigator.userAgent), | |
destroy, | |
i, | |
data = series.data || [], | |
point, | |
axis; | |
// add event hook | |
fireEvent(series, 'destroy'); | |
// remove all events | |
removeEvent(series); | |
// erase from axes | |
each(series.axisTypes || [], function(AXIS) { | |
axis = series[AXIS]; | |
if (axis && axis.series) { | |
erase(axis.series, series); | |
axis.isDirty = axis.forceRedraw = true; | |
} | |
}); | |
// remove legend items | |
if (series.legendItem) { | |
series.chart.legend.destroyItem(series); | |
} | |
// destroy all points with their elements | |
i = data.length; | |
while (i--) { | |
point = data[i]; | |
if (point && point.destroy) { | |
point.destroy(); | |
} | |
} | |
series.points = null; | |
// Clear the animation timeout if we are destroying the series during initial animation | |
clearTimeout(series.animationTimeout); | |
// Destroy all SVGElements associated to the series | |
objectEach(series, function(val, prop) { | |
if (val instanceof SVGElement && !val.survive) { // Survive provides a hook for not destroying | |
// issue 134 workaround | |
destroy = issue134 && prop === 'group' ? | |
'hide' : | |
'destroy'; | |
val[destroy](); | |
} | |
}); | |
// remove from hoverSeries | |
if (chart.hoverSeries === series) { | |
chart.hoverSeries = null; | |
} | |
erase(chart.series, series); | |
chart.orderSeries(); | |
// clear all members | |
objectEach(series, function(val, prop) { | |
delete series[prop]; | |
}); | |
}, | |
/** | |
* Get the graph path | |
*/ | |
getGraphPath: function(points, nullsAsZeroes, connectCliffs) { | |
var series = this, | |
options = series.options, | |
step = options.step, | |
reversed, | |
graphPath = [], | |
xMap = [], | |
gap; | |
points = points || series.points; | |
// Bottom of a stack is reversed | |
reversed = points.reversed; | |
if (reversed) { | |
points.reverse(); | |
} | |
// Reverse the steps (#5004) | |
step = { | |
right: 1, | |
center: 2 | |
}[step] || (step && 3); | |
if (step && reversed) { | |
step = 4 - step; | |
} | |
// Remove invalid points, especially in spline (#5015) | |
if (options.connectNulls && !nullsAsZeroes && !connectCliffs) { | |
points = this.getValidPoints(points); | |
} | |
// Build the line | |
each(points, function(point, i) { | |
var plotX = point.plotX, | |
plotY = point.plotY, | |
lastPoint = points[i - 1], | |
pathToPoint; // the path to this point from the previous | |
if ((point.leftCliff || (lastPoint && lastPoint.rightCliff)) && !connectCliffs) { | |
gap = true; // ... and continue | |
} | |
// Line series, nullsAsZeroes is not handled | |
if (point.isNull && !defined(nullsAsZeroes) && i > 0) { | |
gap = !options.connectNulls; | |
// Area series, nullsAsZeroes is set | |
} else if (point.isNull && !nullsAsZeroes) { | |
gap = true; | |
} else { | |
if (i === 0 || gap) { | |
pathToPoint = ['M', point.plotX, point.plotY]; | |
} else if (series.getPointSpline) { // generate the spline as defined in the SplineSeries object | |
pathToPoint = series.getPointSpline(points, point, i); | |
} else if (step) { | |
if (step === 1) { // right | |
pathToPoint = [ | |
'L', | |
lastPoint.plotX, | |
plotY | |
]; | |
} else if (step === 2) { // center | |
pathToPoint = [ | |
'L', | |
(lastPoint.plotX + plotX) / 2, | |
lastPoint.plotY, | |
'L', | |
(lastPoint.plotX + plotX) / 2, | |
plotY | |
]; | |
} else { | |
pathToPoint = [ | |
'L', | |
plotX, | |
lastPoint.plotY | |
]; | |
} | |
pathToPoint.push('L', plotX, plotY); | |
} else { | |
// normal line to next point | |
pathToPoint = [ | |
'L', | |
plotX, | |
plotY | |
]; | |
} | |
// Prepare for animation. When step is enabled, there are two path nodes for each x value. | |
xMap.push(point.x); | |
if (step) { | |
xMap.push(point.x); | |
} | |
graphPath.push.apply(graphPath, pathToPoint); | |
gap = false; | |
} | |
}); | |
graphPath.xMap = xMap; | |
series.graphPath = graphPath; | |
return graphPath; | |
}, | |
/** | |
* Draw the actual graph | |
*/ | |
drawGraph: function() { | |
var series = this, | |
options = this.options, | |
graphPath = (this.gappedPath || this.getGraphPath).call(this), | |
props = [ | |
[ | |
'graph', | |
'highcharts-graph' | |
] | |
]; | |
// Add the zone properties if any | |
each(this.zones, function(zone, i) { | |
props.push([ | |
'zone-graph-' + i, | |
'highcharts-graph highcharts-zone-graph-' + i + ' ' + (zone.className || '') | |
]); | |
}); | |
// Draw the graph | |
each(props, function(prop, i) { | |
var graphKey = prop[0], | |
graph = series[graphKey], | |
attribs; | |
if (graph) { | |
graph.endX = graphPath.xMap; | |
graph.animate({ | |
d: graphPath | |
}); | |
} else if (graphPath.length) { // #1487 | |
series[graphKey] = series.chart.renderer.path(graphPath) | |
.addClass(prop[1]) | |
.attr({ | |
zIndex: 1 | |
}) // #1069 | |
.add(series.group); | |
} | |
// Helpers for animation | |
if (graph) { | |
graph.startX = graphPath.xMap; | |
//graph.shiftUnit = options.step ? 2 : 1; | |
graph.isArea = graphPath.isArea; // For arearange animation | |
} | |
}); | |
}, | |
/** | |
* Clip the graphs into the positive and negative coloured graphs | |
*/ | |
applyZones: function() { | |
var series = this, | |
chart = this.chart, | |
renderer = chart.renderer, | |
zones = this.zones, | |
translatedFrom, | |
translatedTo, | |
clips = this.clips || [], | |
clipAttr, | |
graph = this.graph, | |
area = this.area, | |
chartSizeMax = Math.max(chart.chartWidth, chart.chartHeight), | |
axis = this[(this.zoneAxis || 'y') + 'Axis'], | |
extremes, | |
reversed, | |
inverted = chart.inverted, | |
horiz, | |
pxRange, | |
pxPosMin, | |
pxPosMax, | |
ignoreZones = false; | |
if (zones.length && (graph || area) && axis && axis.min !== undefined) { | |
reversed = axis.reversed; | |
horiz = axis.horiz; | |
// The use of the Color Threshold assumes there are no gaps | |
// so it is safe to hide the original graph and area | |
if (graph) { | |
graph.hide(); | |
} | |
if (area) { | |
area.hide(); | |
} | |
// Create the clips | |
extremes = axis.getExtremes(); | |
each(zones, function(threshold, i) { | |
translatedFrom = reversed ? | |
(horiz ? chart.plotWidth : 0) : | |
(horiz ? 0 : axis.toPixels(extremes.min)); | |
translatedFrom = Math.min(Math.max(pick(translatedTo, translatedFrom), 0), chartSizeMax); | |
translatedTo = Math.min(Math.max(Math.round(axis.toPixels(pick(threshold.value, extremes.max), true)), 0), chartSizeMax); | |
if (ignoreZones) { | |
translatedFrom = translatedTo = axis.toPixels(extremes.max); | |
} | |
pxRange = Math.abs(translatedFrom - translatedTo); | |
pxPosMin = Math.min(translatedFrom, translatedTo); | |
pxPosMax = Math.max(translatedFrom, translatedTo); | |
if (axis.isXAxis) { | |
clipAttr = { | |
x: inverted ? pxPosMax : pxPosMin, | |
y: 0, | |
width: pxRange, | |
height: chartSizeMax | |
}; | |
if (!horiz) { | |
clipAttr.x = chart.plotHeight - clipAttr.x; | |
} | |
} else { | |
clipAttr = { | |
x: 0, | |
y: inverted ? pxPosMax : pxPosMin, | |
width: chartSizeMax, | |
height: pxRange | |
}; | |
if (horiz) { | |
clipAttr.y = chart.plotWidth - clipAttr.y; | |
} | |
} | |
if (clips[i]) { | |
clips[i].animate(clipAttr); | |
} else { | |
clips[i] = renderer.clipRect(clipAttr); | |
if (graph) { | |
series['zone-graph-' + i].clip(clips[i]); | |
} | |
if (area) { | |
series['zone-area-' + i].clip(clips[i]); | |
} | |
} | |
// if this zone extends out of the axis, ignore the others | |
ignoreZones = threshold.value > extremes.max; | |
}); | |
this.clips = clips; | |
} | |
}, | |
/** | |
* Initialize and perform group inversion on series.group and series.markerGroup | |
*/ | |
invertGroups: function(inverted) { | |
var series = this, | |
chart = series.chart, | |
remover; | |
function setInvert() { | |
each(['group', 'markerGroup'], function(groupName) { | |
if (series[groupName]) { | |
// VML/HTML needs explicit attributes for flipping | |
if (chart.renderer.isVML) { | |
series[groupName].attr({ | |
width: series.yAxis.len, | |
height: series.xAxis.len | |
}); | |
} | |
series[groupName].width = series.yAxis.len; | |
series[groupName].height = series.xAxis.len; | |
series[groupName].invert(inverted); | |
} | |
}); | |
} | |
// Pie, go away (#1736) | |
if (!series.xAxis) { | |
return; | |
} | |
// A fixed size is needed for inversion to work | |
remover = addEvent(chart, 'resize', setInvert); | |
addEvent(series, 'destroy', remover); | |
// Do it now | |
setInvert(inverted); // do it now | |
// On subsequent render and redraw, just do setInvert without setting up events again | |
series.invertGroups = setInvert; | |
}, | |
/** | |
* General abstraction for creating plot groups like series.group, | |
* series.dataLabelsGroup and series.markerGroup. On subsequent calls, the | |
* group will only be adjusted to the updated plot size. | |
*/ | |
plotGroup: function(prop, name, visibility, zIndex, parent) { | |
var group = this[prop], | |
isNew = !group; | |
// Generate it on first call | |
if (isNew) { | |
this[prop] = group = this.chart.renderer.g() | |
.attr({ | |
zIndex: zIndex || 0.1 // IE8 and pointer logic use this | |
}) | |
.add(parent); | |
} | |
// Add the class names, and replace existing ones as response to | |
// Series.update (#6660) | |
group.addClass( | |
( | |
'highcharts-' + name + | |
' highcharts-series-' + this.index + | |
' highcharts-' + this.type + '-series ' + | |
'highcharts-color-' + this.colorIndex + ' ' + | |
(this.options.className || '') | |
), | |
true | |
); | |
// Place it on first and subsequent (redraw) calls | |
group.attr({ | |
visibility: visibility | |
})[isNew ? 'attr' : 'animate']( | |
this.getPlotBox() | |
); | |
return group; | |
}, | |
/** | |
* Get the translation and scale for the plot area of this series | |
*/ | |
getPlotBox: function() { | |
var chart = this.chart, | |
xAxis = this.xAxis, | |
yAxis = this.yAxis; | |
// Swap axes for inverted (#2339) | |
if (chart.inverted) { | |
xAxis = yAxis; | |
yAxis = this.xAxis; | |
} | |
return { | |
translateX: xAxis ? xAxis.left : chart.plotLeft, | |
translateY: yAxis ? yAxis.top : chart.plotTop, | |
scaleX: 1, // #1623 | |
scaleY: 1 | |
}; | |
}, | |
/** | |
* Render the graph and markers | |
*/ | |
render: function() { | |
var series = this, | |
chart = series.chart, | |
group, | |
options = series.options, | |
// Animation doesn't work in IE8 quirks when the group div is | |
// hidden, and looks bad in other oldIE | |
animDuration = (!!series.animate && | |
chart.renderer.isSVG && | |
animObject(options.animation).duration | |
), | |
visibility = series.visible ? 'inherit' : 'hidden', // #2597 | |
zIndex = options.zIndex, | |
hasRendered = series.hasRendered, | |
chartSeriesGroup = chart.seriesGroup, | |
inverted = chart.inverted; | |
// the group | |
group = series.plotGroup( | |
'group', | |
'series', | |
visibility, | |
zIndex, | |
chartSeriesGroup | |
); | |
series.markerGroup = series.plotGroup( | |
'markerGroup', | |
'markers', | |
visibility, | |
zIndex, | |
chartSeriesGroup | |
); | |
// initiate the animation | |
if (animDuration) { | |
series.animate(true); | |
} | |
// SVGRenderer needs to know this before drawing elements (#1089, #1795) | |
group.inverted = series.isCartesian ? inverted : false; | |
// draw the graph if any | |
if (series.drawGraph) { | |
series.drawGraph(); | |
series.applyZones(); | |
} | |
/* each(series.points, function (point) { | |
if (point.redraw) { | |
point.redraw(); | |
} | |
});*/ | |
// draw the data labels (inn pies they go before the points) | |
if (series.drawDataLabels) { | |
series.drawDataLabels(); | |
} | |
// draw the points | |
if (series.visible) { | |
series.drawPoints(); | |
} | |
// draw the mouse tracking area | |
if ( | |
series.drawTracker && | |
series.options.enableMouseTracking !== false | |
) { | |
series.drawTracker(); | |
} | |
// Handle inverted series and tracker groups | |
series.invertGroups(inverted); | |
// Initial clipping, must be defined after inverting groups for VML. | |
// Applies to columns etc. (#3839). | |
if (options.clip !== false && !series.sharedClipKey && !hasRendered) { | |
group.clip(chart.clipRect); | |
} | |
// Run the animation | |
if (animDuration) { | |
series.animate(); | |
} | |
// Call the afterAnimate function on animation complete (but don't | |
// overwrite the animation.complete option which should be available to | |
// the user). | |
if (!hasRendered) { | |
series.animationTimeout = syncTimeout(function() { | |
series.afterAnimate(); | |
}, animDuration); | |
} | |
series.isDirty = false; // means data is in accordance with what you see | |
// (See #322) series.isDirty = series.isDirtyData = false; // means | |
// data is in accordance with what you see | |
series.hasRendered = true; | |
}, | |
/** | |
* Redraw the series after an update in the axes. | |
*/ | |
redraw: function() { | |
var series = this, | |
chart = series.chart, | |
// cache it here as it is set to false in render, but used after | |
wasDirty = series.isDirty || series.isDirtyData, | |
group = series.group, | |
xAxis = series.xAxis, | |
yAxis = series.yAxis; | |
// reposition on resize | |
if (group) { | |
if (chart.inverted) { | |
group.attr({ | |
width: chart.plotWidth, | |
height: chart.plotHeight | |
}); | |
} | |
group.animate({ | |
translateX: pick(xAxis && xAxis.left, chart.plotLeft), | |
translateY: pick(yAxis && yAxis.top, chart.plotTop) | |
}); | |
} | |
series.translate(); | |
series.render(); | |
if (wasDirty) { // #3868, #3945 | |
delete this.kdTree; | |
} | |
}, | |
/** | |
* KD Tree && PointSearching Implementation | |
*/ | |
kdAxisArray: ['clientX', 'plotY'], | |
searchPoint: function(e, compareX) { | |
var series = this, | |
xAxis = series.xAxis, | |
yAxis = series.yAxis, | |
inverted = series.chart.inverted; | |
return this.searchKDTree({ | |
clientX: inverted ? | |
xAxis.len - e.chartY + xAxis.pos : e.chartX - xAxis.pos, | |
plotY: inverted ? | |
yAxis.len - e.chartX + yAxis.pos : e.chartY - yAxis.pos | |
}, compareX); | |
}, | |
/** | |
* Build the k-d-tree that is used by mouse and touch interaction to get the | |
* closest point. Line-like series typically have a one-dimensional tree | |
* where points are searched along the X axis, while scatter-like series | |
* typically search in two dimensions, X and Y. | |
*/ | |
buildKDTree: function() { | |
// Prevent multiple k-d-trees from being built simultaneously (#6235) | |
this.buildingKdTree = true; | |
var series = this, | |
dimensions = series.options.findNearestPointBy.indexOf('y') > -1 ? | |
2 : 1; | |
// Internal function | |
function _kdtree(points, depth, dimensions) { | |
var axis, | |
median, | |
length = points && points.length; | |
if (length) { | |
// alternate between the axis | |
axis = series.kdAxisArray[depth % dimensions]; | |
// sort point array | |
points.sort(function(a, b) { | |
return a[axis] - b[axis]; | |
}); | |
median = Math.floor(length / 2); | |
// build and return nod | |
return { | |
point: points[median], | |
left: _kdtree( | |
points.slice(0, median), depth + 1, dimensions | |
), | |
right: _kdtree( | |
points.slice(median + 1), depth + 1, dimensions | |
) | |
}; | |
} | |
} | |
// Start the recursive build process with a clone of the points array | |
// and null points filtered out (#3873) | |
function startRecursive() { | |
series.kdTree = _kdtree( | |
series.getValidPoints( | |
null, | |
// For line-type series restrict to plot area, but | |
// column-type series not (#3916, #4511) | |
!series.directTouch | |
), | |
dimensions, | |
dimensions | |
); | |
series.buildingKdTree = false; | |
} | |
delete series.kdTree; | |
// For testing tooltips, don't build async | |
syncTimeout(startRecursive, series.options.kdNow ? 0 : 1); | |
}, | |
searchKDTree: function(point, compareX) { | |
var series = this, | |
kdX = this.kdAxisArray[0], | |
kdY = this.kdAxisArray[1], | |
kdComparer = compareX ? 'distX' : 'dist', | |
kdDimensions = series.options.findNearestPointBy.indexOf('y') > -1 ? | |
2 : 1; | |
// Set the one and two dimensional distance on the point object | |
function setDistance(p1, p2) { | |
var x = (defined(p1[kdX]) && defined(p2[kdX])) ? | |
Math.pow(p1[kdX] - p2[kdX], 2) : | |
null, | |
y = (defined(p1[kdY]) && defined(p2[kdY])) ? | |
Math.pow(p1[kdY] - p2[kdY], 2) : | |
null, | |
r = (x || 0) + (y || 0); | |
p2.dist = defined(r) ? Math.sqrt(r) : Number.MAX_VALUE; | |
p2.distX = defined(x) ? Math.sqrt(x) : Number.MAX_VALUE; | |
} | |
function _search(search, tree, depth, dimensions) { | |
var point = tree.point, | |
axis = series.kdAxisArray[depth % dimensions], | |
tdist, | |
sideA, | |
sideB, | |
ret = point, | |
nPoint1, | |
nPoint2; | |
setDistance(search, point); | |
// Pick side based on distance to splitting point | |
tdist = search[axis] - point[axis]; | |
sideA = tdist < 0 ? 'left' : 'right'; | |
sideB = tdist < 0 ? 'right' : 'left'; | |
// End of tree | |
if (tree[sideA]) { | |
nPoint1 = _search(search, tree[sideA], depth + 1, dimensions); | |
ret = (nPoint1[kdComparer] < ret[kdComparer] ? nPoint1 : point); | |
} | |
if (tree[sideB]) { | |
// compare distance to current best to splitting point to decide | |
// wether to check side B or not | |
if (Math.sqrt(tdist * tdist) < ret[kdComparer]) { | |
nPoint2 = _search( | |
search, | |
tree[sideB], | |
depth + 1, | |
dimensions | |
); | |
ret = nPoint2[kdComparer] < ret[kdComparer] ? | |
nPoint2 : | |
ret; | |
} | |
} | |
return ret; | |
} | |
if (!this.kdTree && !this.buildingKdTree) { | |
this.buildKDTree(); | |
} | |
if (this.kdTree) { | |
return _search(point, this.kdTree, kdDimensions, kdDimensions); | |
} | |
} | |
}); // end Series prototype | |
}(Highcharts)); | |
(function(H) { | |
/** | |
* Exporting module | |
* | |
* (c) 2010-2017 Torstein Honsi | |
* | |
* License: www.highcharts.com/license | |
*/ | |
/* eslint indent:0 */ | |
// create shortcuts | |
var defaultOptions = H.defaultOptions, | |
doc = H.doc, | |
Chart = H.Chart, | |
addEvent = H.addEvent, | |
removeEvent = H.removeEvent, | |
fireEvent = H.fireEvent, | |
createElement = H.createElement, | |
discardElement = H.discardElement, | |
css = H.css, | |
merge = H.merge, | |
pick = H.pick, | |
each = H.each, | |
objectEach = H.objectEach, | |
extend = H.extend, | |
isTouchDevice = H.isTouchDevice, | |
win = H.win, | |
userAgent = win.navigator.userAgent, | |
SVGRenderer = H.SVGRenderer, | |
symbols = H.Renderer.prototype.symbols, | |
isMSBrowser = /Edge\/|Trident\/|MSIE /.test(userAgent), | |
isFirefoxBrowser = /firefox/i.test(userAgent); | |
// Add language | |
extend(defaultOptions.lang, { | |
printChart: 'Print chart', | |
downloadPNG: 'Download PNG image', | |
downloadJPEG: 'Download JPEG image', | |
downloadPDF: 'Download PDF document', | |
downloadSVG: 'Download SVG vector image', | |
contextButtonTitle: 'Chart context menu' | |
}); | |
// Buttons and menus are collected in a separate config option set called 'navigation'. | |
// This can be extended later to add control buttons like zoom and pan right click menus. | |
defaultOptions.navigation = { | |
buttonOptions: { | |
theme: {}, | |
symbolSize: 14, | |
symbolX: 12.5, | |
symbolY: 10.5, | |
align: 'right', | |
buttonSpacing: 3, | |
height: 22, | |
// text: null, | |
verticalAlign: 'top', | |
width: 24 | |
} | |
}; | |
// Add the export related options | |
defaultOptions.exporting = { | |
//enabled: true, | |
//filename: 'chart', | |
type: 'image/png', | |
url: 'https://export.highcharts.com/', | |
//width: undefined, | |
printMaxWidth: 780, | |
scale: 2, | |
buttons: { | |
contextButton: { | |
className: 'highcharts-contextbutton', | |
menuClassName: 'highcharts-contextmenu', | |
//x: -10, | |
symbol: 'menu', | |
_titleKey: 'contextButtonTitle', | |
menuItems: [ | |
'printChart', | |
'separator', | |
'downloadPNG', | |
'downloadJPEG', | |
'downloadPDF', | |
'downloadSVG' | |
] | |
} | |
}, | |
// docs. Created API item with since:next. Add information and link to sample | |
// from menuItems too. | |
menuItemDefinitions: { | |
printChart: { | |
textKey: 'printChart', | |
onclick: function() { | |
this.print(); | |
} | |
}, | |
separator: { | |
separator: true | |
}, | |
downloadPNG: { | |
textKey: 'downloadPNG', | |
onclick: function() { | |
this.exportChart(); | |
} | |
}, | |
downloadJPEG: { | |
textKey: 'downloadJPEG', | |
onclick: function() { | |
this.exportChart({ | |
type: 'image/jpeg' | |
}); | |
} | |
}, | |
downloadPDF: { | |
textKey: 'downloadPDF', | |
onclick: function() { | |
this.exportChart({ | |
type: 'application/pdf' | |
}); | |
} | |
}, | |
downloadSVG: { | |
textKey: 'downloadSVG', | |
onclick: function() { | |
this.exportChart({ | |
type: 'image/svg+xml' | |
}); | |
} | |
} | |
} | |
}; | |
// Add the H.post utility | |
H.post = function(url, data, formAttributes) { | |
// create the form | |
var form = createElement('form', merge({ | |
method: 'post', | |
action: url, | |
enctype: 'multipart/form-data' | |
}, formAttributes), { | |
display: 'none' | |
}, doc.body); | |
// add the data | |
objectEach(data, function(val, name) { | |
createElement('input', { | |
type: 'hidden', | |
name: name, | |
value: val | |
}, null, form); | |
}); | |
// submit | |
form.submit(); | |
// clean up | |
discardElement(form); | |
}; | |
extend(Chart.prototype, /** @lends Highcharts.Chart.prototype */ { | |
/** | |
* A collection of fixes on the produced SVG to account for expando properties, | |
* browser bugs, VML problems and other. Returns a cleaned SVG. | |
*/ | |
sanitizeSVG: function(svg, options) { | |
// Move HTML into a foreignObject | |
if (options && options.exporting && options.exporting.allowHTML) { | |
var html = svg.match(/<\/svg>(.*?$)/); | |
if (html && html[1]) { | |
html = '<foreignObject x="0" y="0" ' + | |
'width="' + options.chart.width + '" ' + | |
'height="' + options.chart.height + '">' + | |
'<body xmlns="http://www.w3.org/1999/xhtml">' + | |
html[1] + | |
'</body>' + | |
'</foreignObject>'; | |
svg = svg.replace('</svg>', html + '</svg>'); | |
} | |
} | |
svg = svg | |
.replace(/zIndex="[^"]+"/g, '') | |
.replace(/isShadow="[^"]+"/g, '') | |
.replace(/symbolName="[^"]+"/g, '') | |
.replace(/jQuery[0-9]+="[^"]+"/g, '') | |
.replace(/url\(("|")(\S+)("|")\)/g, 'url($2)') | |
.replace(/url\([^#]+#/g, 'url(#') | |
.replace(/<svg /, '<svg xmlns:xlink="http://www.w3.org/1999/xlink" ') | |
.replace(/ (NS[0-9]+\:)?href=/g, ' xlink:href=') // #3567 | |
.replace(/\n/, ' ') | |
// Any HTML added to the container after the SVG (#894) | |
.replace(/<\/svg>.*?$/, '</svg>') | |
// Batik doesn't support rgba fills and strokes (#3095) | |
.replace(/(fill|stroke)="rgba\(([ 0-9]+,[ 0-9]+,[ 0-9]+),([ 0-9\.]+)\)"/g, '$1="rgb($2)" $1-opacity="$3"') | |
/* This fails in IE < 8 | |
.replace(/([0-9]+)\.([0-9]+)/g, function(s1, s2, s3) { // round off to save weight | |
return s2 +'.'+ s3[0]; | |
})*/ | |
// Replace HTML entities, issue #347 | |
.replace(/ /g, '\u00A0') // no-break space | |
.replace(/­/g, '\u00AD'); // soft hyphen | |
return svg; | |
}, | |
/** | |
* Return innerHTML of chart. Used as hook for plugins. | |
*/ | |
getChartHTML: function() { | |
this.inlineStyles(); | |
return this.container.innerHTML; | |
}, | |
/** | |
* Return an SVG representation of the chart. | |
* | |
* @param chartOptions {Options} | |
* Additional chart options for the generated SVG representation. | |
* For collections like `xAxis`, `yAxis` or `series`, the additional | |
* options is either merged in to the orininal item of the same | |
* `id`, or to the first item if a common id is not found. | |
* @return {String} | |
* The SVG representation of the rendered chart. | |
* @sample highcharts/members/chart-getsvg/ | |
* View the SVG from a button | |
*/ | |
getSVG: function(chartOptions) { | |
var chart = this, | |
chartCopy, | |
sandbox, | |
svg, | |
seriesOptions, | |
sourceWidth, | |
sourceHeight, | |
cssWidth, | |
cssHeight, | |
options = merge(chart.options, chartOptions); // copy the options and add extra options | |
// IE compatibility hack for generating SVG content that it doesn't really understand | |
if (!doc.createElementNS) { | |
doc.createElementNS = function(ns, tagName) { | |
return doc.createElement(tagName); | |
}; | |
} | |
// create a sandbox where a new chart will be generated | |
sandbox = createElement('div', null, { | |
position: 'absolute', | |
top: '-9999em', | |
width: chart.chartWidth + 'px', | |
height: chart.chartHeight + 'px' | |
}, doc.body); | |
// get the source size | |
cssWidth = chart.renderTo.style.width; | |
cssHeight = chart.renderTo.style.height; | |
sourceWidth = options.exporting.sourceWidth || | |
options.chart.width || | |
(/px$/.test(cssWidth) && parseInt(cssWidth, 10)) || | |
600; | |
sourceHeight = options.exporting.sourceHeight || | |
options.chart.height || | |
(/px$/.test(cssHeight) && parseInt(cssHeight, 10)) || | |
400; | |
// override some options | |
extend(options.chart, { | |
animation: false, | |
renderTo: sandbox, | |
forExport: true, | |
renderer: 'SVGRenderer', | |
width: sourceWidth, | |
height: sourceHeight | |
}); | |
options.exporting.enabled = false; // hide buttons in print | |
delete options.data; // #3004 | |
// prepare for replicating the chart | |
options.series = []; | |
each(chart.series, function(serie) { | |
seriesOptions = merge(serie.userOptions, { // #4912 | |
animation: false, // turn off animation | |
enableMouseTracking: false, | |
showCheckbox: false, | |
visible: serie.visible | |
}); | |
if (!seriesOptions.isInternal) { // used for the navigator series that has its own option set | |
options.series.push(seriesOptions); | |
} | |
}); | |
// Assign an internal key to ensure a one-to-one mapping (#5924) | |
each(chart.axes, function(axis) { | |
if (!axis.userOptions.internalKey) { // #6444 | |
axis.userOptions.internalKey = H.uniqueKey(); | |
} | |
}); | |
// generate the chart copy | |
chartCopy = new H.Chart(options, chart.callback); | |
// Axis options and series options (#2022, #3900, #5982) | |
if (chartOptions) { | |
each(['xAxis', 'yAxis', 'series'], function(coll) { | |
var collOptions = {}; | |
if (chartOptions[coll]) { | |
collOptions[coll] = chartOptions[coll]; | |
chartCopy.update(collOptions); | |
} | |
}); | |
} | |
// Reflect axis extremes in the export (#5924) | |
each(chart.axes, function(axis) { | |
var axisCopy = H.find(chartCopy.axes, function(copy) { | |
return copy.options.internalKey === | |
axis.userOptions.internalKey; | |
}), | |
extremes = axis.getExtremes(), | |
userMin = extremes.userMin, | |
userMax = extremes.userMax; | |
if (axisCopy && (userMin !== undefined || userMax !== undefined)) { | |
axisCopy.setExtremes(userMin, userMax, true, false); | |
} | |
}); | |
// Get the SVG from the container's innerHTML | |
svg = chartCopy.getChartHTML(); | |
svg = chart.sanitizeSVG(svg, options); | |
// free up memory | |
options = null; | |
chartCopy.destroy(); | |
discardElement(sandbox); | |
return svg; | |
}, | |
getSVGForExport: function(options, chartOptions) { | |
var chartExportingOptions = this.options.exporting; | |
return this.getSVG(merge({ | |
chart: { | |
borderRadius: 0 | |
} | |
}, | |
chartExportingOptions.chartOptions, | |
chartOptions, { | |
exporting: { | |
sourceWidth: (options && options.sourceWidth) || chartExportingOptions.sourceWidth, | |
sourceHeight: (options && options.sourceHeight) || chartExportingOptions.sourceHeight | |
} | |
} | |
)); | |
}, | |
/** | |
* Exporting module required. Submit an SVG version of the chart to a server | |
* along with some parameters for conversion. | |
* @param {Object} exportingOptions | |
* Exporting options in addition to those defined in {@link | |
* https://api.highcharts.com/highcharts/exporting|exporting}. | |
* @param {String} exportingOptions.filename | |
* The file name for the export without extension. | |
* @param {String} exportingOptions.url | |
* The URL for the server module to do the conversion. | |
* @param {Number} exportingOptions.width | |
* The width of the PNG or JPG image generated on the server. | |
* @param {String} exportingOptions.type | |
* The MIME type of the converted image. | |
* @param {Number} exportingOptions.sourceWidth | |
* The pixel width of the source (in-page) chart. | |
* @param {Number} exportingOptions.sourceHeight | |
* The pixel height of the source (in-page) chart. | |
* @param {Options} chartOptions | |
* Additional chart options for the exported chart. For example a | |
* different background color can be added here, or `dataLabels` | |
* for export only. | |
* | |
* @sample highcharts/members/chart-exportchart/ | |
* Export with no options | |
* @sample highcharts/members/chart-exportchart-filename/ | |
* PDF type and custom filename | |
* @sample highcharts/members/chart-exportchart-custom-background/ | |
* Different chart background in export | |
* @sample stock/members/chart-exportchart/ | |
* Export with Highstock | |
*/ | |
exportChart: function(exportingOptions, chartOptions) { | |
var svg = this.getSVGForExport(exportingOptions, chartOptions); | |
// merge the options | |
exportingOptions = merge(this.options.exporting, exportingOptions); | |
// do the post | |
H.post(exportingOptions.url, { | |
filename: exportingOptions.filename || 'chart', | |
type: exportingOptions.type, | |
width: exportingOptions.width || 0, // IE8 fails to post undefined correctly, so use 0 | |
scale: exportingOptions.scale, | |
svg: svg | |
}, exportingOptions.formAttributes); | |
}, | |
/** | |
* Exporting module required. Clears away other elements in the page and | |
* prints the chart as it is displayed. By default, when the exporting | |
* module is enabled, a context button with a drop down menu in the upper | |
* right corner accesses this function. | |
* | |
* @sample highcharts/members/chart-print/ | |
* Print from a HTML button | |
*/ | |
print: function() { | |
var chart = this, | |
container = chart.container, | |
origDisplay = [], | |
origParent = container.parentNode, | |
body = doc.body, | |
childNodes = body.childNodes, | |
printMaxWidth = chart.options.exporting.printMaxWidth, | |
resetParams, | |
handleMaxWidth; | |
if (chart.isPrinting) { // block the button while in printing mode | |
return; | |
} | |
chart.isPrinting = true; | |
chart.pointer.reset(null, 0); | |
fireEvent(chart, 'beforePrint'); | |
// Handle printMaxWidth | |
handleMaxWidth = printMaxWidth && chart.chartWidth > printMaxWidth; | |
if (handleMaxWidth) { | |
resetParams = [chart.options.chart.width, undefined, false]; | |
chart.setSize(printMaxWidth, undefined, false); | |
} | |
// hide all body content | |
each(childNodes, function(node, i) { | |
if (node.nodeType === 1) { | |
origDisplay[i] = node.style.display; | |
node.style.display = 'none'; | |
} | |
}); | |
// pull out the chart | |
body.appendChild(container); | |
win.focus(); // #1510 | |
win.print(); | |
// allow the browser to prepare before reverting | |
setTimeout(function() { | |
// put the chart back in | |
origParent.appendChild(container); | |
// restore all body content | |
each(childNodes, function(node, i) { | |
if (node.nodeType === 1) { | |
node.style.display = origDisplay[i]; | |
} | |
}); | |
chart.isPrinting = false; | |
// Reset printMaxWidth | |
if (handleMaxWidth) { | |
chart.setSize.apply(chart, resetParams); | |
} | |
fireEvent(chart, 'afterPrint'); | |
}, 1000); | |
}, | |
/** | |
* Display a popup menu for choosing the export type | |
* | |
* @param {String} className An identifier for the menu | |
* @param {Array} items A collection with text and onclicks for the items | |
* @param {Number} x The x position of the opener button | |
* @param {Number} y The y position of the opener button | |
* @param {Number} width The width of the opener button | |
* @param {Number} height The height of the opener button | |
*/ | |
contextMenu: function(className, items, x, y, width, height, button) { | |
var chart = this, | |
navOptions = chart.options.navigation, | |
chartWidth = chart.chartWidth, | |
chartHeight = chart.chartHeight, | |
cacheName = 'cache-' + className, | |
menu = chart[cacheName], | |
menuPadding = Math.max(width, height), // for mouse leave detection | |
innerMenu, | |
hide, | |
menuStyle; | |
// create the menu only the first time | |
if (!menu) { | |
// create a HTML element above the SVG | |
chart[cacheName] = menu = createElement('div', { | |
className: className | |
}, { | |
position: 'absolute', | |
zIndex: 1000, | |
padding: menuPadding + 'px' | |
}, chart.container); | |
innerMenu = createElement('div', { | |
className: 'highcharts-menu' | |
}, null, menu); | |
// hide on mouse out | |
hide = function() { | |
css(menu, { | |
display: 'none' | |
}); | |
if (button) { | |
button.setState(0); | |
} | |
chart.openMenu = false; | |
}; | |
// Hide the menu some time after mouse leave (#1357) | |
chart.exportEvents.push( | |
addEvent(menu, 'mouseleave', function() { | |
menu.hideTimer = setTimeout(hide, 500); | |
}), | |
addEvent(menu, 'mouseenter', function() { | |
clearTimeout(menu.hideTimer); | |
}), | |
// Hide it on clicking or touching outside the menu (#2258, #2335, | |
// #2407) | |
addEvent(doc, 'mouseup', function(e) { | |
if (!chart.pointer.inClass(e.target, className)) { | |
hide(); | |
} | |
}) | |
); | |
// create the items | |
each(items, function(item) { | |
if (typeof item === 'string') { | |
item = chart.options.exporting.menuItemDefinitions[item]; | |
} | |
if (H.isObject(item, true)) { | |
var element; | |
if (item.separator) { | |
element = createElement('hr', null, null, innerMenu); | |
} else { | |
element = createElement('div', { | |
className: 'highcharts-menu-item', | |
onclick: function(e) { | |
if (e) { // IE7 | |
e.stopPropagation(); | |
} | |
hide(); | |
if (item.onclick) { | |
item.onclick.apply(chart, arguments); | |
} | |
}, | |
innerHTML: item.text || chart.options.lang[item.textKey] | |
}, null, innerMenu); | |
} | |
// Keep references to menu divs to be able to destroy them | |
chart.exportDivElements.push(element); | |
} | |
}); | |
// Keep references to menu and innerMenu div to be able to destroy them | |
chart.exportDivElements.push(innerMenu, menu); | |
chart.exportMenuWidth = menu.offsetWidth; | |
chart.exportMenuHeight = menu.offsetHeight; | |
} | |
menuStyle = { | |
display: 'block' | |
}; | |
// if outside right, right align it | |
if (x + chart.exportMenuWidth > chartWidth) { | |
menuStyle.right = (chartWidth - x - width - menuPadding) + 'px'; | |
} else { | |
menuStyle.left = (x - menuPadding) + 'px'; | |
} | |
// if outside bottom, bottom align it | |
if (y + height + chart.exportMenuHeight > chartHeight && button.alignOptions.verticalAlign !== 'top') { | |
menuStyle.bottom = (chartHeight - y - menuPadding) + 'px'; | |
} else { | |
menuStyle.top = (y + height - menuPadding) + 'px'; | |
} | |
css(menu, menuStyle); | |
chart.openMenu = true; | |
}, | |
/** | |
* Add the export button to the chart | |
*/ | |
addButton: function(options) { | |
var chart = this, | |
renderer = chart.renderer, | |
btnOptions = merge(chart.options.navigation.buttonOptions, options), | |
onclick = btnOptions.onclick, | |
menuItems = btnOptions.menuItems, | |
symbol, | |
button, | |
symbolSize = btnOptions.symbolSize || 12; | |
if (!chart.btnCount) { | |
chart.btnCount = 0; | |
} | |
// Keeps references to the button elements | |
if (!chart.exportDivElements) { | |
chart.exportDivElements = []; | |
chart.exportSVGElements = []; | |
} | |
if (btnOptions.enabled === false) { | |
return; | |
} | |
var attr = btnOptions.theme, | |
states = attr.states, | |
hover = states && states.hover, | |
select = states && states.select, | |
callback; | |
delete attr.states; | |
if (onclick) { | |
callback = function(e) { | |
e.stopPropagation(); | |
onclick.call(chart, e); | |
}; | |
} else if (menuItems) { | |
callback = function() { | |
chart.contextMenu( | |
button.menuClassName, | |
menuItems, | |
button.translateX, | |
button.translateY, | |
button.width, | |
button.height, | |
button | |
); | |
button.setState(2); | |
}; | |
} | |
if (btnOptions.text && btnOptions.symbol) { | |
attr.paddingLeft = pick(attr.paddingLeft, 25); | |
} else if (!btnOptions.text) { | |
extend(attr, { | |
width: btnOptions.width, | |
height: btnOptions.height, | |
padding: 0 | |
}); | |
} | |
button = renderer.button(btnOptions.text, 0, 0, callback, attr, hover, select) | |
.addClass(options.className) | |
.attr({ | |
title: chart.options.lang[btnOptions._titleKey], | |
zIndex: 3 // #4955 | |
}); | |
button.menuClassName = options.menuClassName || 'highcharts-menu-' + chart.btnCount++; | |
if (btnOptions.symbol) { | |
symbol = renderer.symbol( | |
btnOptions.symbol, | |
btnOptions.symbolX - (symbolSize / 2), | |
btnOptions.symbolY - (symbolSize / 2), | |
symbolSize, | |
symbolSize | |
) | |
.addClass('highcharts-button-symbol') | |
.attr({ | |
zIndex: 1 | |
}).add(button); | |
} | |
button.add() | |
.align(extend(btnOptions, { | |
width: button.width, | |
x: pick(btnOptions.x, chart.buttonOffset) // #1654 | |
}), true, 'spacingBox'); | |
chart.buttonOffset += (button.width + btnOptions.buttonSpacing) * (btnOptions.align === 'right' ? -1 : 1); | |
chart.exportSVGElements.push(button, symbol); | |
}, | |
/** | |
* Destroy the buttons. | |
*/ | |
destroyExport: function(e) { | |
var chart = e ? e.target : this, | |
exportSVGElements = chart.exportSVGElements, | |
exportDivElements = chart.exportDivElements, | |
exportEvents = chart.exportEvents, | |
cacheName; | |
// Destroy the extra buttons added | |
if (exportSVGElements) { | |
each(exportSVGElements, function(elem, i) { | |
// Destroy and null the svg/vml elements | |
if (elem) { // #1822 | |
elem.onclick = elem.ontouchstart = null; | |
cacheName = 'cache-' + elem.menuClassName; | |
if (chart[cacheName]) { | |
delete chart[cacheName]; | |
} | |
chart.exportSVGElements[i] = elem.destroy(); | |
} | |
}); | |
exportSVGElements.length = 0; | |
} | |
// Destroy the divs for the menu | |
if (exportDivElements) { | |
each(exportDivElements, function(elem, i) { | |
// Remove the event handler | |
clearTimeout(elem.hideTimer); // #5427 | |
removeEvent(elem, 'mouseleave'); | |
// Remove inline events | |
chart.exportDivElements[i] = elem.onmouseout = elem.onmouseover = elem.ontouchstart = elem.onclick = null; | |
// Destroy the div by moving to garbage bin | |
discardElement(elem); | |
}); | |
exportDivElements.length = 0; | |
} | |
if (exportEvents) { | |
each(exportEvents, function(unbind) { | |
unbind(); | |
}); | |
exportEvents.length = 0; | |
} | |
} | |
}); | |
// These ones are translated to attributes rather than styles | |
SVGRenderer.prototype.inlineToAttributes = [ | |
'fill', | |
'stroke', | |
'strokeLinecap', | |
'strokeLinejoin', | |
'strokeWidth', | |
'textAnchor', | |
'x', | |
'y' | |
]; | |
// These CSS properties are not inlined. Remember camelCase. | |
SVGRenderer.prototype.inlineBlacklist = [ | |
/-/, // In Firefox, both hyphened and camelCased names are listed | |
/^(clipPath|cssText|d|height|width)$/, // Full words | |
/^font$/, // more specific props are set | |
/[lL]ogical(Width|Height)$/, | |
/perspective/, | |
/TapHighlightColor/, | |
/^transition/ | |
// /^text (border|color|cursor|height|webkitBorder)/ | |
]; | |
SVGRenderer.prototype.unstyledElements = [ | |
'clipPath', | |
'defs', | |
'desc' | |
]; | |
/** | |
* Analyze inherited styles from stylesheets and add them inline | |
* | |
* @todo: What are the border styles for text about? In general, text has a lot of properties. | |
* @todo: Make it work with IE9 and IE10. | |
*/ | |
Chart.prototype.inlineStyles = function() { | |
var renderer = this.renderer, | |
inlineToAttributes = renderer.inlineToAttributes, | |
blacklist = renderer.inlineBlacklist, | |
whitelist = renderer.inlineWhitelist, // For IE | |
unstyledElements = renderer.unstyledElements, | |
defaultStyles = {}, | |
dummySVG; | |
/** | |
* Make hyphenated property names out of camelCase | |
*/ | |
function hyphenate(prop) { | |
return prop.replace( | |
/([A-Z])/g, | |
function(a, b) { | |
return '-' + b.toLowerCase(); | |
} | |
); | |
} | |
/** | |
* Call this on all elements and recurse to children | |
*/ | |
function recurse(node) { | |
var styles, | |
parentStyles, | |
cssText = '', | |
dummy, | |
styleAttr, | |
blacklisted, | |
whitelisted, | |
i; | |
// Check computed styles and whether they are in the white/blacklist for | |
// styles or atttributes | |
function filterStyles(val, prop) { | |
// Check against whitelist & blacklist | |
blacklisted = whitelisted = false; | |
if (whitelist) { | |
// Styled mode in IE has a whitelist instead. | |
// Exclude all props not in this list. | |
i = whitelist.length; | |
while (i-- && !whitelisted) { | |
whitelisted = whitelist[i].test(prop); | |
} | |
blacklisted = !whitelisted; | |
} | |
// Explicitly remove empty transforms | |
if (prop === 'transform' && val === 'none') { | |
blacklisted = true; | |
} | |
i = blacklist.length; | |
while (i-- && !blacklisted) { | |
blacklisted = blacklist[i].test(prop) || typeof val === 'function'; | |
} | |
if (!blacklisted) { | |
// If parent node has the same style, it gets inherited, no need to inline it | |
if (parentStyles[prop] !== val && defaultStyles[node.nodeName][prop] !== val) { | |
// Attributes | |
if (inlineToAttributes.indexOf(prop) !== -1) { | |
node.setAttribute(hyphenate(prop), val); | |
// Styles | |
} else { | |
cssText += hyphenate(prop) + ':' + val + ';'; | |
} | |
} | |
} | |
} | |
if (node.nodeType === 1 && unstyledElements.indexOf(node.nodeName) === -1) { | |
styles = win.getComputedStyle(node, null); | |
parentStyles = node.nodeName === 'svg' ? {} : win.getComputedStyle(node.parentNode, null); | |
// Get default styles from the browser so that we don't have to add these | |
if (!defaultStyles[node.nodeName]) { | |
if (!dummySVG) { | |
dummySVG = doc.createElementNS(H.SVG_NS, 'svg'); | |
dummySVG.setAttribute('version', '1.1'); | |
doc.body.appendChild(dummySVG); | |
} | |
dummy = doc.createElementNS(node.namespaceURI, node.nodeName); | |
dummySVG.appendChild(dummy); | |
defaultStyles[node.nodeName] = merge(win.getComputedStyle(dummy, null)); // Copy, so we can remove the node | |
dummySVG.removeChild(dummy); | |
} | |
// Loop through all styles and add them inline if they are ok | |
if (isFirefoxBrowser || isMSBrowser) { | |
// Some browsers put lots of styles on the prototype | |
for (var p in styles) { | |
filterStyles(styles[p], p); | |
} | |
} else { | |
objectEach(styles, filterStyles); | |
} | |
// Apply styles | |
if (cssText) { | |
styleAttr = node.getAttribute('style'); | |
node.setAttribute('style', (styleAttr ? styleAttr + ';' : '') + cssText); | |
} | |
// Set default stroke width (needed at least for IE) | |
if (node.nodeName === 'svg') { | |
node.setAttribute('stroke-width', '1px'); | |
} | |
if (node.nodeName === 'text') { | |
return; | |
} | |
// Recurse | |
each(node.children || node.childNodes, recurse); | |
} | |
} | |
/** | |
* Remove the dummy objects used to get defaults | |
*/ | |
function tearDown() { | |
dummySVG.parentNode.removeChild(dummySVG); | |
} | |
recurse(this.container.querySelector('svg')); | |
tearDown(); | |
}; | |
symbols.menu = function(x, y, width, height) { | |
var arr = [ | |
'M', x, y + 2.5, | |
'L', x + width, y + 2.5, | |
'M', x, y + height / 2 + 0.5, | |
'L', x + width, y + height / 2 + 0.5, | |
'M', x, y + height - 1.5, | |
'L', x + width, y + height - 1.5 | |
]; | |
return arr; | |
}; | |
// Add the buttons on chart load | |
Chart.prototype.renderExporting = function() { | |
var chart = this, | |
exportingOptions = chart.options.exporting, | |
buttons = exportingOptions.buttons, | |
isDirty = chart.isDirtyExporting || !chart.exportSVGElements; | |
chart.buttonOffset = 0; | |
if (chart.isDirtyExporting) { | |
chart.destroyExport(); | |
} | |
if (isDirty && exportingOptions.enabled !== false) { | |
chart.exportEvents = []; | |
objectEach(buttons, function(button) { | |
chart.addButton(button); | |
}); | |
chart.isDirtyExporting = false; | |
} | |
// Destroy the export elements at chart destroy | |
addEvent(chart, 'destroy', chart.destroyExport); | |
}; | |
Chart.prototype.callbacks.push(function(chart) { | |
function update(prop, options, redraw) { | |
chart.isDirtyExporting = true; | |
merge(true, chart.options[prop], options); | |
if (pick(redraw, true)) { | |
chart.redraw(); | |
} | |
} | |
chart.renderExporting(); | |
addEvent(chart, 'redraw', chart.renderExporting); | |
// Add update methods to handle chart.update and chart.exporting.update | |
// and chart.navigation.update. | |
each(['exporting', 'navigation'], function(prop) { | |
chart[prop] = { | |
update: function(options, redraw) { | |
update(prop, options, redraw); | |
} | |
}; | |
}); | |
// Uncomment this to see a button directly below the chart, for quick | |
// testing of export | |
/* | |
if (!chart.renderer.forExport) { | |
var button; | |
// View SVG Image | |
button = doc.createElement('button'); | |
button.innerHTML = 'View SVG Image'; | |
chart.renderTo.parentNode.appendChild(button); | |
button.onclick = function () { | |
var div = doc.createElement('div'); | |
div.innerHTML = chart.getSVGForExport(); | |
chart.renderTo.parentNode.appendChild(div); | |
}; | |
// View SVG Source | |
button = doc.createElement('button'); | |
button.innerHTML = 'View SVG Source'; | |
chart.renderTo.parentNode.appendChild(button); | |
button.onclick = function () { | |
var pre = doc.createElement('pre'); | |
pre.innerHTML = chart.getSVGForExport() | |
.replace(/</g, '\n<') | |
.replace(/>/g, '>'); | |
chart.renderTo.parentNode.appendChild(pre); | |
}; | |
} | |
// */ | |
}); | |
}(Highcharts)); | |
return Highcharts | |
})); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment