app/assets/javascripts/highcharts.js in highcharts-rails-5.0.0 vs app/assets/javascripts/highcharts.js in highcharts-rails-5.0.3
- old
+ new
@@ -1,7 +1,7 @@
/**
- * @license Highcharts JS v5.0.0 (2016-09-29)
+ * @license Highcharts JS v5.0.3 (2016-11-18)
*
* (c) 2009-2016 Torstein Honsi
*
* License: www.highcharts.com/license
*/
@@ -34,22 +34,23 @@
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: '5.0.0',
+ version: '5.0.3',
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,
- idCounter: 0,
chartCount: 0,
seriesTypes: {},
+ symbolSizes: {},
svg: svg,
vml: vml,
win: win,
charts: [],
marginNames: ['plotTop', 'marginRight', 'marginBottom', 'plotLeft'],
@@ -63,59 +64,82 @@
/**
* (c) 2010-2016 Torstein Honsi
*
* License: www.highcharts.com/license
*/
+ /* eslint max-len: ["warn", 80, 4] */
'use strict';
+
+ /**
+ * The Highcharts object is the placeholder for all other members, and various
+ * utility functions.
+ * @namespace Highcharts
+ */
+
var timers = [];
var charts = H.charts,
doc = H.doc,
win = H.win;
/**
- * Provide error messages for debugging, with links to online explanation
+ * 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} code - The error code. See [errors.xml]{@link
+ * https://github.com/highcharts/highcharts/blob/master/errors/errors.xml}
+ * for available codes.
+ * @param {Boolean} [stop=false] - Whether to throw an error or just log a
+ * warning in the console.
*/
H.error = function(code, stop) {
- var msg = 'Highcharts error #' + code + ': www.highcharts.com/errors/' + code;
+ var msg = 'Highcharts error #' + code + ': www.highcharts.com/errors/' +
+ code;
if (stop) {
throw new Error(msg);
}
// else ...
if (win.console) {
console.log(msg); // eslint-disable-line no-console
}
};
/**
- * An animator object. One instance applies to one property (attribute or style prop)
- * on one element.
- *
- * @param {object} elem The element to animate. May be a DOM element or a Highcharts SVGElement wrapper.
- * @param {object} options Animation options, including duration, easing, step and complete.
- * @param {object} prop The property to animate.
+ * An animator object. One instance applies to one property (attribute or style
+ * prop) on one element.
+ *
+ * @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.
*/
H.Fx = function(elem, options, prop) {
this.options = options;
this.elem = elem;
this.prop = prop;
};
H.Fx.prototype = {
/**
- * Animating a path definition on SVGElement
- * @returns {undefined}
+ * 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;
- if (now === 1) { // land on the final path without adjustment points appended in the ends
+ // 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]);
@@ -123,19 +147,22 @@
isNaN(startVal) ? // a letter instruction like M or L
start[i] :
now * (parseFloat(end[i] - startVal)) + startVal;
}
- } else { // if animation is finished or length not matching, land on right value
+ // If animation is finished or length not matching, land on right value
+ } else {
ret = end;
}
this.elem.attr('d', ret);
},
/**
- * Update the element with the current animation step
- * @returns {undefined}
+ * 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,
@@ -161,11 +188,18 @@
}
},
/**
- * Run an animation
+ * 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);
@@ -196,13 +230,17 @@
}, 13);
}
},
/**
- * Run a single step in the animation
- * @param {Boolean} gotoEnd Whether to go to then endpoint of the animation after abort
- * @returns {Boolean} True if animation continues
+ * 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,
@@ -211,11 +249,11 @@
complete = options.complete,
duration = options.duration,
curAnim = options.curAnim,
i;
- if (elem.attr && !elem.element) { // #2616, element including flag is destroyed
+ 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;
@@ -243,11 +281,19 @@
}
return ret;
},
/**
- * Prepare start and end values so that the path can be animated one to one
+ * 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,
@@ -268,11 +314,15 @@
*/
function sixify(arr) {
i = arr.length;
while (i--) {
if (arr[i] === 'M' || arr[i] === 'L') {
- arr.splice(i + 1, 0, arr[i + 1], arr[i + 2], arr[i + 1], arr[i + 2]);
+ arr.splice(
+ i + 1, 0,
+ arr[i + 1], arr[i + 2],
+ arr[i + 1], arr[i + 2]
+ );
}
}
}
/**
@@ -294,14 +344,17 @@
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.
+ // 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);
+ insertSlice(
+ arr,
+ arr.slice(arr.length - numParams), arr.length
+ );
i--;
}
}
arr[0] = 'M';
}
@@ -311,15 +364,16 @@
*/
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.
+ // 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
);
@@ -330,11 +384,12 @@
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)
+ // 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--;
}
@@ -344,18 +399,21 @@
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.
+ // 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++) {
- if (startX[i] === endX[0]) { // Moving left, new points coming in on right
+ // Moving left, new points coming in on right
+ if (startX[i] === endX[0]) {
shift = i;
break;
- } else if (startX[0] === endX[endX.length - startX.length + i]) { // Moving right
+ // Moving right
+ } else if (startX[0] ===
+ endX[endX.length - startX.length + i]) {
shift = i;
reverse = true;
break;
}
}
@@ -383,13 +441,17 @@
}
}; // End of Fx prototype
/**
- * Extend an object with the members of another
- * @param {Object} a The object to be extended
- * @param {Object} b The object to add to the first one
+ * 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 = {};
@@ -399,15 +461,24 @@
}
return a;
};
/**
- * 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.
- * Previously this function redirected to jQuery.extend(true), but this had two limitations.
- * First, it deep merged arrays, which lead to workarounds in Highcharts. Second,
- * it copied properties from extended prototypes.
+ * 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,
@@ -424,11 +495,12 @@
if (original.hasOwnProperty(key)) {
value = original[key];
// Copy the contents of objects, but not arrays or DOM nodes
if (H.isObject(value, true) &&
- key !== 'renderTo' && typeof value.nodeType !== 'number') {
+ key !== 'renderTo' &&
+ typeof value.nodeType !== 'number') {
copy[key] = doCopy(copy[key] || {}, value);
// Primitives and arrays are copied over directly
} else {
copy[key] = original[key];
@@ -436,11 +508,12 @@
}
}
return copy;
};
- // If first argument is true, copy into the existing object. Used in setOptions.
+ // 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);
}
@@ -453,82 +526,111 @@
return ret;
};
/**
* Shortcut for parseInt
+ * @ignore
* @param {Object} s
* @param {Number} mag Magnitude
*/
H.pInt = function(s, mag) {
return parseInt(s, mag || 10);
};
/**
- * Check for string
- * @param {Object} s
+ * 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';
};
/**
- * Check for object
- * @param {Object} obj
- * @param {Boolean} strict Also checks that the object is not an array
+ * 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]';
};
/**
- * Check for array
- * @param {Object} obj
+ * 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));
};
/**
- * Check for number
- * @param {Object} n
+ * 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 last occurence of an item from an array
- * @param {Array} arr
- * @param {Mixed} item
+ * 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;
}
}
- //return arr;
};
/**
- * Returns true if the object is not null or undefined.
- * @param {Object} obj
+ * 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. Can't use jQuery attr because
- * it attempts to set expando properties on the SVG element, which is not allowed.
+ * 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.
*
- * @param {Object} elem The DOM element to receive the attribute(s)
- * @param {String|Object} prop The property or an abject of key-value pairs
- * @param {String} value The value if a single property is set
+ * @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 key,
ret;
@@ -537,11 +639,11 @@
// set the value
if (H.defined(value)) {
elem.setAttribute(prop, value);
// get the value
- } else if (elem && elem.getAttribute) { // elem not defined when printing pie demo...
+ } 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)) {
@@ -549,34 +651,50 @@
elem.setAttribute(key, prop[key]);
}
}
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
- * @param {Function} fn The function to perform
- * @param {Number} delay Delay in milliseconds
- * @param {Ojbect} context The context
- * @returns {Nubmer} An identifier for the timeout
+ * 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 defined.
+ * 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,
@@ -588,31 +706,55 @@
}
}
};
/**
- * Set CSS on a given element
- * @param {Object} el
- * @param {Object} styles Style object with camel case property names
+ * @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);
};
/**
- * Utility function to create element with attributes and styles
- * @param {Object} tag
- * @param {Object} attribs
- * @param {Object} styles
- * @param {Object} parent
- * @param {Object} nopad
+ * 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);
@@ -632,65 +774,118 @@
}
return el;
};
/**
- * Extend a prototyped class by new members
- * @param {Object} parent
- * @param {Object} members
+ * 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) {
+ H.extendClass = function(parent, members) {
var object = function() {};
- object.prototype = new Parent();
+ object.prototype = new parent(); // eslint-disable-line new-cap
H.extend(object.prototype, members);
return object;
};
/**
- * Pad a string to a given length by adding 0 to the beginning
- * @param {Number} number
- * @param {Number} length
+ * 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;
+ 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);
+ return (/%$/).test(value) ?
+ base * parseFloat(value) / 100 :
+ parseFloat(value);
};
/**
- * Wrap a method with extended functionality, preserving the original function
- * @param {Object} obj The context object that the method belongs to
- * @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.
+ * 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);
+ 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);
- return func.apply(this, args);
+ 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;
+ return ((d.hcGetTimezoneOffset && d.hcGetTimezoneOffset(timestamp)) ||
+ d.hcTimezoneOffset || 0) * 60000;
};
/**
- * Based on http://www.php.net/manual/en/function.strftime.php
- * @param {String} format
- * @param {Number} timestamp
- * @param {Boolean} capitalize
+ * Format a date, based on the syntax for PHP's [strftime]{@link
+ * http://www.php.net/manual/en/function.strftime.php} function.
+ *
+ * @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 || '';
}
@@ -711,55 +906,91 @@
pad = H.pad,
// List all format keys. Custom formats can be added from the outside.
replacements = H.extend({
- // Day
- 'a': shortWeekdays ? shortWeekdays[day] : langWeekdays[day].substr(0, 3), // Short weekday, like 'Mon'
- 'A': langWeekdays[day], // Long weekday, like 'Monday'
- 'd': pad(dayOfMonth), // Two digit day of the month, 01 to 31
- 'e': pad(dayOfMonth, 2, ' '), // Day of the month, 1 through 31
+ //-- 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
- 'b': lang.shortMonths[month], // Short month, like 'Jan'
- 'B': lang.months[month], // Long month, like 'January'
- 'm': pad(month + 1), // Two digit month number, 01 through 12
+ //-- 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
- 'y': fullYear.toString().substr(2, 2), // Two digits year, like 09 for 2009
- 'Y': fullYear, // Four digits year, like 2009
+ //-- Year
+ // Two digits year, like 09 for 2009
+ 'y': fullYear.toString().substr(2, 2),
+ // Four digits year, like 2009
+ 'Y': fullYear,
- // Time
- 'H': pad(hours), // Two digits hours in 24h format, 00 through 23
- 'k': hours, // Hours in 24h format, 0 through 23
- 'I': pad((hours % 12) || 12), // Two digits hours in 12h format, 00 through 11
- 'l': (hours % 12) || 12, // Hours in 12h format, 1 through 12
- 'M': pad(date[D.hcGetMinutes]()), // Two digits minutes, 00 through 59
- 'p': hours < 12 ? 'AM' : 'PM', // Upper case AM or PM
- 'P': hours < 12 ? 'am' : 'pm', // Lower case AM or PM
- 'S': pad(date.getSeconds()), // Two digits seconds, 00 through 59
- 'L': pad(Math.round(timestamp % 1000), 3) // Milliseconds (naming from Ruby)
+ //-- 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)
}, H.dateFormats);
- // do the replaces
+ // Do the replaces
for (key in replacements) {
- while (format.indexOf('%' + key) !== -1) { // regex would do it in one line, but this is faster
- format = format.replace('%' + key, typeof replacements[key] === 'function' ? replacements[key](timestamp) : replacements[key]);
+ // Regex would do it in one line, but this is faster
+ while (format.indexOf('%' + key) !== -1) {
+ format = format.replace(
+ '%' + key,
+ typeof replacements[key] === 'function' ?
+ replacements[key](timestamp) :
+ replacements[key]
+ );
}
}
// Optionally capitalize the string and return
- return capitalize ? format.substr(0, 1).toUpperCase() + format.substr(1) : format;
+ 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,
@@ -781,11 +1012,26 @@
}
return val;
};
/**
- * Format a string according to a subset of the rules of Python's String.format method.
+ * 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,
@@ -805,11 +1051,11 @@
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 format
+ path = valueAndFormat.shift().split('.'); // get first and leave
len = path.length;
val = ctx;
// Assign deeper paths
for (i = 0; i < len; i++) {
@@ -835,51 +1081,77 @@
ret.push(str);
return ret.join('');
};
/**
- * Get the magnitude of a number
+ * 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 1, 2, 2.5 and 5
- * @param {Number} interval
- * @param {Array} multiples
- * @param {Number} magnitude
- * @param {Object} options
+ * 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, preventExceed) {
+ 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 = [1, 2, 2.5, 5, 10];
+ 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 = [1, 2, 5, 10];
+ 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];
- if ((preventExceed && retInterval * magnitude >= interval) || // only allow tick amounts smaller than natural
- (!preventExceed && (normalized <= (multiples[i] + (multiples[i + 1] || multiples[i])) / 2))) {
+ // 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
@@ -888,12 +1160,19 @@
return retInterval;
};
/**
- * Utility method that sorts an object array and keeping the order of equal items.
- * ECMA script standard does not specify the behaviour when items are equal.
+ * 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;
@@ -913,13 +1192,18 @@
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.
+ * 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];
@@ -930,13 +1214,18 @@
}
return min;
};
/**
- * 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.
+ * 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];
@@ -947,15 +1236,20 @@
}
return max;
};
/**
- * Utility method that destroys any SVGElement or VMLElement 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'ed.
- * @param {Object} The object to destroy properties on
- * @param {Object} Exception, do not destroy this property, only delete it.
+ * 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) {
var n;
for (n in obj) {
// If the object is non-null and destroy is defined
@@ -969,12 +1263,16 @@
}
};
/**
- * Discard an element by moving it to the bin and delete
- * @param {Object} The HTML node to discard
+ * 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) {
@@ -987,37 +1285,60 @@
}
garbageBin.innerHTML = '';
};
/**
- * Fix JS round off float errors
- * @param {Number} num
+ * 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
- * @param {Object} animation
- * @param {Object} chart
+ * 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);
+ chart.renderer.globalAnimation = H.pick(
+ animation,
+ chart.options.chart.animation,
+ true
+ );
};
/**
* Get the animation in object form, where a disabled animation is always
- * returned with duration: 0
+ * 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
- };
+ return H.isObject(animation) ?
+ H.merge(animation) : {
+ duration: animation ? 500 : 0
+ };
};
/**
* The time unit lookup
*/
@@ -1031,15 +1352,21 @@
month: 28 * 24 * 3600000,
year: 364 * 24 * 3600000
};
/**
- * Format a number and return a string based on input settings
- * @param {Number} number The input number to format
- * @param {Number} decimals The amount of decimals
- * @param {String} decimalPoint The decimal point, defaults to the one given in the lang options
- * @param {String} thousandsSep The thousands separator, defaults to the one given in the lang options
+ * 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.
+ * @param {String} [decimalPoint] - The decimal point, defaults to the one given
+ * in the lang options.
+ * @param {String} [thousandsSep] - The thousands separator, defaults to the one
+ * given in the lang options.
+ * @returns {String} The formatted number.
*/
H.numberFormat = function(number, decimals, decimalPoint, thousandsSep) {
number = +number || 0;
decimals = +decimals;
@@ -1051,11 +1378,12 @@
thousands,
absNumber = Math.abs(number),
ret;
if (decimals === -1) {
- decimals = Math.min(origDec, 20); // Preserve decimals. Not huge numbers (#3793).
+ // Preserve decimals. Not huge numbers (#3793).
+ decimals = Math.min(origDec, 20);
} else if (!H.isNumber(decimals)) {
decimals = 2;
}
// A string containing the positive integer component of the number
@@ -1069,70 +1397,108 @@
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.
+ // 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);
+ 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, and add power to avoid rounding errors with float numbers (#4573)
- decimalComponent = Math.abs(absNumber - strinteger + Math.pow(10, -Math.max(decimals, origDec) - 1));
+ // Get the decimal component, and add power to avoid rounding errors
+ // with float numbers (#4573)
+ decimalComponent = Math.abs(absNumber - strinteger +
+ Math.pow(10, -Math.max(decimals, origDec) - 1));
ret += decimalPoint + decimalComponent.toFixed(decimals).slice(2);
}
return ret;
};
/**
* Easing definition
- * @param {Number} pos Current position, ranging from 0 to 1
+ * @ignore
+ * @param {Number} pos Current position, ranging from 0 to 1.
*/
Math.easeInOutSine = function(pos) {
return -0.5 * (Math.cos(Math.PI * pos) - 1);
};
/**
- * Internal method to return CSS value for given element and property
+ * 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.
+ * @returns {Number} - The numeric value.
*/
H.getStyle = function(el, prop) {
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');
+ 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');
+ 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);
return style && H.pInt(style.getPropertyValue(prop));
};
/**
- * Return the index of an item in an array, or -1 if not found
+ * 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
+ * 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(elements, callback) {
- return [].filter.call(elements, callback);
+ H.grep = function(arr, callback) {
+ return [].filter.call(arr, callback);
};
/**
- * Map an array
+ * 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;
@@ -1143,29 +1509,43 @@
return results;
};
/**
- * Get the element's offset position, corrected by overflow:auto.
+ * 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)
+ top: box.top + (win.pageYOffset || docElem.scrollTop) -
+ (docElem.clientTop || 0),
+ left: box.left + (win.pageXOffset || docElem.scrollLeft) -
+ (docElem.clientLeft || 0)
};
};
/**
* Stop running animation.
- * A possible extension to this would be to stop a single property, when
+ *
+ * @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.
+ * 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.
+ * @returns {void}
*/
H.stop = function(el) {
var i = timers.length;
@@ -1176,20 +1556,35 @@
}
}
};
/**
- * Utility for iterating over an array.
- * @param {Array} arr
- * @param {Function} fn
+ * Iterate over an array.
+ *
+ * @function #each
+ * @memberOf Highcharts
+ * @param {Array} arr - The array to iterate over.
+ * @param {Function} fn - The iterator callback. It passes two arguments:
+ * * item - The array item.
+ * * index - The item's index in the array.
+ * @param {Object} [ctx] The context.
*/
H.each = function(arr, fn, ctx) { // modern browsers
return Array.prototype.forEach.call(arr, fn, ctx);
};
/**
- * Add an event listener
+ * 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 || {};
@@ -1218,14 +1613,28 @@
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 event added with addEvent
+ * 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,
@@ -1287,11 +1696,22 @@
}
}
};
/**
- * Fire an event on a custom object
+ * 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,
@@ -1320,29 +1740,32 @@
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)
+ // 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.
+ // 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).
+ // 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 the event handler return false, prevent the default handler
+ // from executing
if (fn && fn.call(el, eventArguments) === false) {
eventArguments.preventDefault();
}
}
}
@@ -1352,11 +1775,35 @@
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,
@@ -1373,11 +1820,13 @@
};
}
if (!H.isNumber(opt.duration)) {
opt.duration = 400;
}
- opt.easing = typeof opt.easing === 'function' ? opt.easing : (Math[opt.easing] || Math.easeInOutSine);
+ opt.easing = typeof opt.easing === 'function' ?
+ opt.easing :
+ (Math[opt.easing] || Math.easeInOutSine);
opt.curAnim = H.merge(params);
for (prop in params) {
fx = new H.Fx(el, opt, prop);
end = null;
@@ -1409,41 +1858,73 @@
fx.run(start, end, unit);
}
};
/**
- * The series type factory.
+ * Factory to create new series prototypes.
*
- * @param {string} type The series type name.
- * @param {string} parent The parent series type name.
- * @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 Point prototype if needed.
+ * @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.
*/
- H.seriesType = function(type, parent, options, props, pointProps) { // docs: add to API + extending Highcharts
+ // 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] = 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);
+ 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);
@@ -1451,25 +1932,27 @@
if (this[0]) { // this[0] is the renderTo div
// Create the chart
if (args[0]) {
new H[ // eslint-disable-line no-new
- H.isString(args[0]) ? args.shift() : 'Chart' // Constructor defaults to Chart
+ // 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
+ // 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.
+ * 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 = {
@@ -1566,11 +2049,20 @@
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
@@ -1755,46 +2247,82 @@
removeEvent = H.removeEvent,
splat = H.splat,
stop = H.stop,
svg = H.svg,
SVG_NS = H.SVG_NS,
+ symbolSizes = H.symbolSizes,
win = H.win;
/**
- * A wrapper object for SVG elements
+ * @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 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 SVGRenderer} object, like [rect]{@link SVGRenderer#rect},
+ * [path]{@link SVGRenderer#path}, [text]{@link SVGRenderer#text}, [label]{@link
+ * SVGRenderer#label}, [g]{@link SVGRenderer#g} and more.
+ *
+ * @class
+ */
SVGElement = H.SVGElement = function() {
return this;
};
SVGElement.prototype = {
// Default base for animation
opacity: 1,
SVG_NS: SVG_NS,
- // For labels, these CSS properties are applied to the <text> node directly
- textProps: ['direction', 'fontSize', 'fontWeight', 'fontFamily', 'fontStyle', 'color',
- 'lineHeight', 'width', 'textDecoration', 'textOverflow', 'textShadow'
+
+ /**
+ * For labels, these CSS properties are applied to the `text` node directly.
+ * @type {Array.<string>}
+ */
+ textProps: ['direction', 'fontSize', 'fontWeight', 'fontFamily',
+ 'fontStyle', 'color', 'lineHeight', 'width', 'textDecoration',
+ 'textOverflow', 'textOutline'
],
/**
- * Initialize the SVG renderer
- * @param {Object} renderer
- * @param {String} nodeName
+ * Initialize the SVG renderer. This function only exists to make the
+ * initiation process overridable. It should not be called directly.
+ *
+ * @param {SVGRenderer} renderer The SVGRenderer instance to initialize to.
+ * @param {String} nodeName The SVG node name.
+ * @returns {void}
*/
init: function(renderer, nodeName) {
- var wrapper = this;
- wrapper.element = nodeName === 'span' ?
+
+ /**
+ * 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(wrapper.SVG_NS, nodeName);
- wrapper.renderer = renderer;
+ doc.createElementNS(this.SVG_NS, nodeName);
+
+ /**
+ * The renderer that the SVGElement belongs to.
+ * @type {SVGRenderer}
+ */
+ this.renderer = renderer;
},
/**
- * Animate a given attribute
- * @param {Object} params
- * @param {Number} options Options include duration, easing, step and complete
- * @param {Function} complete Function to perform at the end of animation
+ * 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.
+ * @returns {SVGElement} Returns the SVGElement for chaining.
*/
animate: function(params, options, complete) {
var animOptions = pick(options, this.renderer.globalAnimation, true);
stop(this); // stop regardless of animation actually running, or reverting to .attr (#607)
if (animOptions) {
@@ -1807,12 +2335,57 @@
}
return this;
},
/**
- * Build an SVG gradient out of a common JavaScript configuration object
+ * @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,
@@ -1878,11 +2451,11 @@
id = gradients[key].attr('id');
} else {
// Set the id and create the element
- gradAttr.id = id = 'highcharts-' + H.idCounter++;
+ gradAttr.id = id = H.uniqueKey();
gradients[key] = gradientObject = renderer.createElement(gradName)
.attr(gradAttr)
.add(renderer.defs);
gradientObject.radAttr = radAttr;
@@ -1921,102 +2494,165 @@
};
}
},
/**
- * Apply a polyfill to the text-stroke CSS property, by copying the text element
- * and apply strokes to the copy.
+ * 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/ .
*
- * 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
+ * });
*/
- applyTextShadow: function(textShadow) {
+ applyTextOutline: function(textOutline) {
var elem = this.element,
tspans,
- hasContrast = textShadow.indexOf('contrast') !== -1,
+ hasContrast = textOutline.indexOf('contrast') !== -1,
styles = {},
- forExport = this.renderer.forExport,
- // IE10 and IE11 report textShadow in elem.style even though it doesn't work. Check
- // this again with new IE release. In exports, the rendering is passed to PhantomJS.
- supports = this.renderer.forExport || (elem.style.textShadow !== undefined && !isMS);
+ color,
+ strokeWidth;
- // When the text shadow is set to contrast, use dark stroke for light text and vice versa
+ // When the text shadow is set to contrast, use dark stroke for light
+ // text and vice versa.
if (hasContrast) {
- styles.textShadow = textShadow = textShadow.replace(/contrast/g, this.renderer.getContrast(elem.style.fill));
+ styles.textOutline = textOutline = textOutline.replace(
+ /contrast/g,
+ this.renderer.getContrast(elem.style.fill)
+ );
}
- // Safari with retina displays as well as PhantomJS bug (#3974). Firefox does not tolerate this,
- // it removes the text shadows.
- if (isWebKit || forExport) {
- styles.textRendering = 'geometricPrecision';
- }
+ this.fakeTS = true; // Fake text shadow
- /* Selective side-by-side testing in supported browser (http://jsfiddle.net/highcharts/73L1ptrh/)
- if (elem.textContent.indexOf('2.') === 0) {
- elem.style['text-shadow'] = 'none';
- supports = false;
- }
- // */
+ // In order to get the right y position of the clone,
+ // copy over the y setter
+ this.ySetter = this.xSetter;
- // No reason to polyfill, we've got native support
- if (supports) {
- this.css(styles); // Apply altered textShadow or textRendering workaround
- } else {
+ tspans = [].slice.call(elem.getElementsByTagName('tspan'));
- this.fakeTS = true; // Fake text shadow
+ // Extract the stroke width and color
+ textOutline = textOutline.split(' ');
+ color = textOutline[textOutline.length - 1];
+ strokeWidth = textOutline[0];
- // In order to get the right y position of the clones,
- // copy over the y setter
- this.ySetter = this.xSetter;
+ if (strokeWidth && strokeWidth !== 'none') {
- tspans = [].slice.call(elem.getElementsByTagName('tspan'));
- each(textShadow.split(/\s?,\s?/g), function(textShadow) {
- var firstChild = elem.firstChild,
- color,
- strokeWidth;
+ // 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;
+ }
+ );
- textShadow = textShadow.split(' ');
- color = textShadow[textShadow.length - 1];
+ // Remove shadows from previous runs
+ each(tspans, function(tspan) {
+ if (tspan.getAttribute('class') === 'highcharts-text-outline') {
+ // Remove then erase
+ erase(tspans, elem.removeChild(tspan));
+ }
+ });
- // Approximately tune the settings to the text-shadow behaviour
- strokeWidth = textShadow[textShadow.length - 2];
+ this.realBox = elem.getBBox();
- if (strokeWidth) {
- each(tspans, function(tspan, y) {
- var clone;
+ // For each of the tspans, create a stroked copy behind it.
+ 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 shadow properties
- clone = tspan.cloneNode(1);
- attr(clone, {
- 'class': 'highcharts-text-shadow',
- 'fill': color,
- 'stroke': color,
- 'stroke-opacity': 1 / Math.max(pInt(strokeWidth), 3),
- 'stroke-width': strokeWidth,
- 'stroke-linejoin': 'round'
- });
- elem.insertBefore(clone, firstChild);
- });
+ // 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, elem.firstChild);
});
}
},
/**
- * Set or get a given attribute
- * @param {Object|String} hash
- * @param {Mixed|Undefined} val
+ *
+ * @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.
+ *
+ * @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.
+ *
+ * @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) {
var key,
value,
element = this.element,
hasSetSymbolSize,
@@ -2086,15 +2722,18 @@
return ret;
},
/**
- * Update the shadow elements with new attributes
- * @param {String} key The attribute name
- * @param {String|Number} value The value of the attribute
- * @param {Function} setter The setter function, inherited from the parent wrapper
- * @returns {undefined}
+ * Update the shadow elements with new attributes.
+ *
+ * @private
+ * @param {String} key - The attribute name.
+ * @param {String|Number} value - The value of the attribute.
+ * @param {Function} setter - The setter function, inherited from the
+ * parent wrapper
+ * @returns {void}
*/
updateShadows: function(key, value, setter) {
var shadows = this.shadows,
i = shadows.length;
@@ -2110,36 +2749,57 @@
}
},
/**
- * Add a class name to an element
+ * 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(' ', ' ');
+ 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
+ * @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) {
@@ -2156,25 +2816,40 @@
)
});
},
/**
- * Apply a clipping path to this object
- * @param {String} id
+ * 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');
+ 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 {Number} strokeWidth
- * @param {Number} x
- * @param {Number} y
- * @param {Number} width
- * @param {Number} height
+ * 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,
key,
@@ -2201,12 +2876,16 @@
return attribs;
},
/**
- * Set styles for the element
- * @param {Object} styles
+ * 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.
*/
css: function(styles) {
var elemWrapper = this,
oldStyles = elemWrapper.styles,
newStyles = {},
@@ -2263,43 +2942,63 @@
}
attr(elem, 'style', serializedCss); // #1881
}
- // Rebuild text after added
- if (elemWrapper.added && textWidth) {
- elemWrapper.renderer.buildText(elemWrapper);
+ if (elemWrapper.added) {
+ // Rebuild text after added
+ if (textWidth) {
+ elemWrapper.renderer.buildText(elemWrapper);
+ }
+
+ // Apply text outline after added
+ if (styles && styles.textOutline) {
+ elemWrapper.applyTextOutline(styles.textOutline);
+ }
}
}
return elemWrapper;
},
+ /**
+ * Get the current stroke width. In classic mode, the setter registers it
+ * directly on the element.
+ * @returns {number} The stroke width in pixels.
+ * @ignore
+ */
strokeWidth: function() {
return this['stroke-width'] || 0;
},
/**
- * Add an event listener
- * @param {String} eventType
- * @param {Function} handler
+ * 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.
*/
on: function(eventType, handler) {
var svgElement = this,
element = svgElement.element;
// touch
if (hasTouch && eventType === 'click') {
element.ontouchstart = function(e) {
- svgElement.touchEventFired = Date.now();
+ 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) { // #2269
+ 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
@@ -2308,12 +3007,16 @@
return this;
},
/**
* Set the coordinates needed to draw a consistent radial gradient across
- * pie slices regardless of positioning inside the chart. The format is
- * [centerX, centerY, diameter] in pixels.
+ * 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;
@@ -2331,34 +3034,44 @@
return this;
},
/**
- * Move an object and its children by x and y values
- * @param {Number} x
- * @param {Number} y
+ * 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
+ * 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;
},
/**
- * Private method to update the transform attribute based on internal
- * properties
+ * 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,
@@ -2396,31 +3109,44 @@
if (transform.length) {
element.setAttribute('transform', transform.join(' '));
}
},
+
/**
- * Bring the element to the front
+ * Bring the element to the front.
+ *
+ * @returns {SVGElement} Returns the SVGElement for chaining.
*/
toFront: function() {
var element = this.element;
element.parentNode.appendChild(element);
return this;
},
/**
- * Break down alignment options like align, verticalAlign, x and y
- * to x and y relative to the chart.
- *
- * @param {Object} alignOptions
- * @param {Boolean} alignByTranslate
- * @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.
- *
+ * 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,
@@ -2488,11 +3214,24 @@
return this;
},
/**
- * Get the bounding box (width, height, x and y) for the element
+ * 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.
*/
getBBox: function(reload, rot) {
var wrapper = this,
bBox, // = wrapper.bBox,
renderer = wrapper.renderer,
@@ -2502,12 +3241,10 @@
rad,
element = wrapper.element,
styles = wrapper.styles,
fontSize,
textStr = wrapper.textStr,
- textShadow,
- elemStyle = element.style,
toggleTextShadowShim,
cache = renderer.cache,
cacheKeys = renderer.cacheKeys,
cacheKey;
@@ -2518,19 +3255,29 @@
fontSize = styles && styles.fontSize;
if (textStr !== undefined) {
- cacheKey =
+ 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.
- textStr.toString().replace(/[0-9]/g, '0') +
+ // 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
- ['', rotation || 0, fontSize, element.style.width].join(',');
+ // Properties that affect bounding box
+ cacheKey += [
+ '',
+ rotation || 0,
+ fontSize,
+ element.style.width,
+ element.style['text-overflow'] // #5968
+ ]
+ .join(',');
}
if (cacheKey && !reload) {
bBox = cache[cacheKey];
@@ -2544,20 +3291,17 @@
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-shadow'), function(tspan) {
+ each(element.querySelectorAll('.highcharts-text-outline'), function(tspan) {
tspan.style.display = display;
});
};
// Workaround for #3842, Firefox reporting wrong bounding box for shadows
- if (isFirefox && elemStyle.textShadow) {
- textShadow = elemStyle.textShadow;
- elemStyle.textShadow = '';
- } else if (toggleTextShadowShim) {
+ if (toggleTextShadowShim) {
toggleTextShadowShim('none');
}
bBox = element.getBBox ?
// SVG: use extend because IE9 is not allowed to change width and height in case
@@ -2568,13 +3312,11 @@
width: element.offsetWidth,
height: element.offsetHeight
};
// #3842
- if (textShadow) {
- elemStyle.textShadow = textShadow;
- } else if (toggleTextShadowShim) {
+ if (toggleTextShadowShim) {
toggleTextShadowShim('');
}
} catch (e) {}
// If the bBox is not set, the try-catch block above failed. The other condition
@@ -2629,27 +3371,42 @@
}
return bBox;
},
/**
- * Show the element
+ * 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
+ * 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
}, {
@@ -2661,13 +3418,18 @@
}
});
},
/**
- * Add the element
- * @param {Object|Undefined} parent Can be an element, an element wrapper or undefined
- * to append the element to the renderer.box.
+ * 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 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,
@@ -2706,22 +3468,27 @@
return this;
},
/**
- * Removes a child either by removeChild or move to garbageBin.
- * Issue 490; in VML removeChild results in Orphaned nodes according to sIEve, discardElement does not.
+ * 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
+ * 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,
@@ -2772,13 +3539,39 @@
return null;
},
/**
- * Add a shadow to the element. Must be done after the element is added to the DOM
- * @param {Boolean|Object} shadowOptions
+ * @typedef {Object} ShadowOptions
+ * @property {string} [color=#000000] The shadow color.
+ * @property {number} [offsetX=1] The horizontal offset from the element.
+ * @property {number} [offsetY=1] The vertical offset from the element.
+ * @property {number} [opacity=0.15] The shadow opacity.
+ * @property {number} [width=3] The shadow width or distance from the
+ * element.
*/
+ /**
+ * Add a shadow to the element. Must be called after the element is added to
+ * the DOM. In styled mode, this method is not used, instead use `defs` and
+ * filters.
+ *
+ * @param {boolean|ShadowOptions} shadowOptions The shadow options. If
+ * `true`, the default options are applied. If `false`, the current
+ * shadow will be removed.
+ * @param {SVGElement} [group] The SVG group element where the shadows will
+ * be applied. The default is to add it to the same parent as the current
+ * element. Internally, this is ised for pie slices, where all the
+ * shadows are added to an element behind all the slices.
+ * @param {boolean} [cutOff] Used internally for column shadows.
+ *
+ * @returns {SVGElement} Returns the SVGElement for chaining.
+ *
+ * @example
+ * renderer.rect(10, 100, 100, 100)
+ * .attr({ fill: 'red' })
+ * .shadow(true);
+ */
shadow: function(shadowOptions, group, cutOff) {
var shadows = [],
i,
shadow,
element = this.element,
@@ -2827,10 +3620,14 @@
}
return this;
},
+ /**
+ * Destroy shadows on the element.
+ * @private
+ */
destroyShadows: function() {
each(this.shadows || [], function(shadow) {
this.safeRemoveChild(shadow);
}, this);
this.shadows = undefined;
@@ -2849,11 +3646,14 @@
return this._defaultGetter(key);
},
/**
* Get the current value of an attribute or pseudo attribute, used mainly
- * for animation.
+ * for animation. Called internally from the {@link 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
@@ -2913,10 +3713,14 @@
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);
@@ -2997,11 +3801,15 @@
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))
+ (!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;
}
@@ -3023,15 +3831,10 @@
SVGElement.prototype.rotationSetter = SVGElement.prototype.verticalAlignSetter =
SVGElement.prototype.scaleXSetter = SVGElement.prototype.scaleYSetter = function(value, key) {
this[key] = value;
this.doTransform = true;
};
- // These setters both set the key on the instance itself plus as an attribute
- SVGElement.prototype.opacitySetter = SVGElement.prototype.displaySetter = function(value, key, element) {
- this[key] = value;
- element.setAttribute(key, value);
- };
// WebKit and Batik have problems with a stroke-width of zero, so in this case we remove the
// stroke attribute altogether. #1270, #1369, #3065, #3072.
SVGElement.prototype['stroke-widthSetter'] = SVGElement.prototype.strokeSetter = function(value, key, element) {
@@ -3047,24 +3850,49 @@
}
};
/**
- * The default SVG renderer
+ * 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
*/
SVGRenderer = H.SVGRenderer = function() {
this.init.apply(this, arguments);
};
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
- * @param {Object} container
- * @param {Number} width
- * @param {Number} height
- * @param {Boolean} forExport
+ * 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,
@@ -3085,25 +3913,38 @@
attr(element, 'xmlns', this.SVG_NS);
}
// object properties
renderer.isSVG = true;
- renderer.box = element;
- renderer.boxWrapper = boxWrapper;
+
+ /**
+ * 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. #24, #672, #1070
- renderer.url = (isFirefox || isWebKit) && doc.getElementsByTagName('base').length ?
+ /**
+ * 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, '\\$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 5.0.0'));
+ desc.element.appendChild(doc.createTextNode('Created with Highcharts 5.0.3'));
renderer.defs = this.createElement('defs').add();
renderer.allowHTML = allowHTML;
renderer.forExport = forExport;
@@ -3122,11 +3963,11 @@
// 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) {
- renderer.subPixelFix = subPixelFix = function() {
+ subPixelFix = function() {
css(container, {
left: 0,
top: 0
});
rect = container.getBoundingClientRect();
@@ -3138,35 +3979,49 @@
// run the fix now
subPixelFix();
// run it on resize
- addEvent(win, 'resize', subPixelFix);
+ renderer.unSubPixelFix = addEvent(win, 'resize', subPixelFix);
}
},
+ /**
+ * Get the global style setting for the renderer.
+ * @private
+ * @param {CSSObject} style - Style settings.
+ * @return {CSSObject} The style settings mixed with defaults.
+ */
getStyle: function(style) {
this.style = extend({
fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Arial, Helvetica, sans-serif', // default font
fontSize: '12px'
}, style);
return this.style;
},
+ /**
+ * Apply the global style on the renderer, mixed with the default styles.
+ * @param {CSSObject} style - CSS to apply.
+ */
setStyle: function(style) {
this.boxWrapper.css(this.getStyle(style));
},
/**
- * Detect whether the renderer is hidden. This happens when one of the parent elements
- * has display: none. #608.
+ * 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() {
+ isHidden: function() { // #608
return !this.boxWrapper.getBBox().width;
},
/**
* Destroys the renderer and its allocated members.
@@ -3185,52 +4040,64 @@
// Otherwise, destroy them here.
if (rendererDefs) {
renderer.defs = rendererDefs.destroy();
}
- // Remove sub pixel fix handler
- // We need to check that there is a handler, otherwise all functions that are registered for event 'resize' are removed
- // See issue #982
- if (renderer.subPixelFix) {
- removeEvent(win, 'resize', renderer.subPixelFix);
+ // Remove sub pixel fix handler (#982)
+ if (renderer.unSubPixelFix) {
+ renderer.unSubPixelFix();
}
renderer.alignedObjects = null;
return null;
},
/**
- * Create a wrapper for an SVG element
- * @param {Object} nodeName
+ * 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
+ * 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
+ * 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]
};
},
/**
- * Parse a simple HTML string into SVG tspans
- *
- * @param {Object} textNode The parent text SVG node
+ * 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,
@@ -3244,11 +4111,11 @@
wasTooLong,
parentX = attr(textNode, 'x'),
textStyles = wrapper.styles,
width = wrapper.textWidth,
textLineHeight = textStyles && textStyles.lineHeight,
- textShadow = textStyles && textStyles.textShadow,
+ textOutline = textStyles && textStyles.textOutline,
ellipsis = textStyles && textStyles.textOverflow === 'ellipsis',
i = childNodes.length,
tempParent = width && !wrapper.added && this.box,
getLineHeight = function(tspan) {
var fontSizeStyle;
@@ -3260,11 +4127,12 @@
return textLineHeight ?
pInt(textLineHeight) :
renderer.fontMetrics(
fontSizeStyle,
- tspan
+ // Get the computed size from parent if not explicit
+ tspan.getAttribute('style') ? tspan : textNode
).h;
},
unescapeAngleBrackets = function(inputStr) {
return inputStr.replace(/</g, '<').replace(/>/g, '>');
};
@@ -3274,11 +4142,11 @@
textNode.removeChild(childNodes[i]);
}
// Skip tspans, add text directly to text node. The forceTSpan is a hook
// used in text outline hack.
- if (!hasMarkup && !textShadow && !ellipsis && !width && textStr.indexOf(' ') === -1) {
+ if (!hasMarkup && !textOutline && !ellipsis && !width && textStr.indexOf(' ') === -1) {
textNode.appendChild(doc.createTextNode(unescapeAngleBrackets(textStr)));
// Complex strings, add more logic
} else {
@@ -3468,13 +4336,13 @@
}
if (tempParent) {
tempParent.removeChild(textNode); // attach it to the DOM to read offset width
}
- // Apply the text shadow
- if (textShadow && wrapper.applyTextShadow) {
- wrapper.applyTextShadow(textShadow);
+ // Apply the text outline
+ if (textOutline && wrapper.applyTextOutline) {
+ wrapper.applyTextOutline(textOutline);
}
}
},
@@ -3511,26 +4379,36 @@
console.log('width', width, 'stringWidth', node.getSubStringLength(0, finalPos))
},
*/
/**
- * Returns white for dark colors and black for bright colors
+ * 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;
return rgba[0] + rgba[1] + rgba[2] > 2 * 255 ? '#000000' : '#FFFFFF';
},
/**
- * Create a button with preset states
- * @param {String} text
- * @param {Number} x
- * @param {Number} y
- * @param {Function} callback
- * @param {Object} normalState
- * @param {Object} hoverState
- * @param {Object} pressedState
+ * 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;
@@ -3633,16 +4511,19 @@
}
});
},
/**
- * Make a straight line crisper by not spilling out to neighbour pixels
- * @param {Array} points
- * @param {Number} width
+ * 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) {
- // points format: ['M', 0, 0, 'L', 100, 0]
// 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);
}
@@ -3652,13 +4533,26 @@
return points;
},
/**
- * Draw a path
- * @param {Array} path An SVG path in array form
+ * 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.
*/
+ /**
+ * 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 = {
fill: 'none'
@@ -3670,15 +4564,23 @@
}
return this.createElement('path').attr(attribs);
},
/**
- * Draw and return an SVG circle
- * @param {Number} x The x position
- * @param {Number} y The y position
- * @param {Number} r The radius
+ * 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.
*/
+ /**
+ * 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
@@ -3692,18 +4594,26 @@
return wrapper.attr(attribs);
},
/**
- * Draw and return an arc
- * @param {Number} x X position
- * @param {Number} y Y position
- * @param {Number} r Radius
- * @param {Number} innerR Inner radius like used in donut charts
- * @param {Number} start Starting angle
- * @param {Number} end Ending angle
+ * 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.
*/
+ /**
+ * 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;
if (isObject(x)) {
y = x.y;
@@ -3724,18 +4634,26 @@
arc.r = r; // #959
return arc;
},
/**
- * Draw and return a rectangle
- * @param {Number} x Left position
- * @param {Number} y Top position
- * @param {Number} width
- * @param {Number} height
- * @param {Number} r Border corner radius
- * @param {Number} strokeWidth A stroke width can be supplied to allow crisp drawing
+ * 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.
+ * @returns {SVGElement} The generated wrapper element.
+ */
rect: function(x, y, width, height, r, strokeWidth) {
r = isObject(x) ? x.r : r;
var wrapper = this.createElement('rect'),
@@ -3767,15 +4685,15 @@
return wrapper.attr(attribs);
},
/**
- * Resize the box and re-align all aligned elements
- * @param {Object} width
- * @param {Object} height
- * @param {Boolean} animate
- *
+ * 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} animate Whether to animate.
*/
setSize: function(width, height, animate) {
var renderer = this,
alignedObjects = renderer.alignedObjects,
i = alignedObjects.length;
@@ -3799,28 +4717,33 @@
alignedObjects[i].align();
}
},
/**
- * Create a group
- * @param {String} name The group will be given a class name of 'highcharts-{name}'.
- * This can be used for styling and scripting.
+ * Create and return an svg group element.
+ *
+ * @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.
*/
g: function(name) {
var elem = this.createElement('g');
return name ? elem.attr({
'class': 'highcharts-' + name
}) : elem;
},
/**
- * Display an image
- * @param {String} src
- * @param {Number} x
- * @param {Number} y
- * @param {Number} width
- * @param {Number} height
+ * 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.
*/
image: function(src, x, y, width, height) {
var attribs = {
preserveAspectRatio: 'none'
},
@@ -3849,17 +4772,31 @@
}
return elemWrapper;
},
/**
- * Draw a symbol out of pre-defined shape paths from the namespace 'symbol' object.
+ * 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 {Object} symbol
- * @param {Object} x
- * @param {Object} y
- * @param {Object} radius
- * @param {Object} options
+ * @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,
@@ -3875,12 +4812,11 @@
height,
options
),
imageRegex = /^url\((.*?)\)$/,
imageSrc,
- centerImage,
- symbolSizes = {};
+ centerImage;
if (symbolFn) {
obj = this.path(path);
@@ -3907,14 +4843,21 @@
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);
+ // 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({
@@ -3922,24 +4865,26 @@
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.
+ * 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];
+ 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[key === 'width' ? 'translateX' : 'translateY'] = (this[key] - imgSize) / 2;
+ attribs[trans] = ((this[key] || 0) - imgSize) / 2;
this.attr(attribs);
}
}
};
});
@@ -4010,10 +4955,18 @@
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) {
var cpw = 0.166 * w;
@@ -4120,27 +5073,57 @@
'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-right corner
+ 'C', x, y, x, y, x + r, y // top-left corner
];
- if (anchorX && anchorX > w && anchorY > y + safeDistance && anchorY < y + h - safeDistance) { // replace right side
- path.splice(13, 3,
- 'L', x + w, anchorY - halfDistance,
- x + w + arrowLength, anchorY,
- x + w, anchorY + halfDistance,
- x + w, y + h - r
- );
- } else if (anchorX && anchorX < 0 && anchorY > y + safeDistance && anchorY < y + h - safeDistance) { // replace left side
- path.splice(33, 3,
- 'L', x, anchorY + halfDistance,
- x - arrowLength, anchorY,
- x, anchorY - halfDistance,
- x, y + r
- );
+ // 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,
@@ -4152,25 +5135,42 @@
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
+ * @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 = 'highcharts-' + H.idCounter++,
+ id = H.uniqueKey(),
clipPath = this.createElement('clipPath').attr({
id: id
}).add(this.defs);
@@ -4187,12 +5187,12 @@
/**
* Add text to the SVG object
* @param {String} str
- * @param {Number} x Left position
- * @param {Number} y Top position
+ * @param {number} x Left position
+ * @param {number} y Top position
* @param {Boolean} useHTML Use HTML to render the text
*/
text: function(str, x, y, useHTML) {
// declare variables
@@ -4242,24 +5242,47 @@
return wrapper;
},
/**
- * Utility to return the baseline offset and total line height from the font size
+ * 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) { // eslint-disable-line no-unused-vars
+ fontMetrics: function(fontSize, elem) {
var lineHeight,
baseline;
- fontSize = fontSize || (this.style && this.style.fontSize);
+ fontSize = fontSize ||
+ // When the elem is a DOM element (#5932)
+ (elem && elem.style && elem.style.fontSize) ||
+ // Fall back on the renderer style default
+ (this.style && this.style.fontSize);
- fontSize = /px/.test(fontSize) ? pInt(fontSize) : /em/.test(fontSize) ? parseFloat(fontSize) * 12 : 12;
- // Empirical values found by comparing font size and bounding box height.
- // Applies to the default font family. http://jsfiddle.net/highcharts/7xvn7/
+ // 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,
@@ -4282,18 +5305,20 @@
};
},
/**
* Add a label, a text item that can hold a colored or gradient background
- * as well as a border and shadow.
+ * as well as a border and shadow. Supported custom attributes include
+ * `padding`.
+ *
* @param {string} str
- * @param {Number} x
- * @param {Number} y
+ * @param {number} x
+ * @param {number} y
* @param {String} shape
- * @param {Number} anchorX In case the shape has a pointer, like a flag, this is the
+ * @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
+ * @param {number} anchorY
* @param {Boolean} baseline Whether to position the label relative to the text baseline,
* like renderer.text, or to the upper border of the rectangle.
* @param {String} className Class name for the group
*/
label: function(str, x, y, shape, anchorX, anchorY, useHTML, baseline, className) {
@@ -4543,11 +5568,13 @@
// 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
+ * 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)
@@ -4560,11 +5587,12 @@
text.css(textStyles);
}
return baseCss.call(wrapper, styles);
},
/**
- * Return the bounding box of the box, not the group
+ * Return the bounding box of the box, not the group.
+ * @ignore
*/
getBBox: function() {
return {
width: bBox.width + 2 * padding,
height: bBox.height + 2 * padding,
@@ -4572,11 +5600,12 @@
y: bBox.y - padding
};
},
/**
- * Apply the shadow to the box
+ * Apply the shadow to the box.
+ * @ignore
*/
shadow: function(b) {
if (b) {
updateBoxSize();
if (box) {
@@ -4586,10 +5615,11 @@
return wrapper;
},
/**
* Destroy and release memory.
+ * @ignore
*/
destroy: function() {
// Added by button implementation
removeEvent(wrapper.element, 'mouseenter');
@@ -4636,12 +5666,12 @@
SVGElement = H.SVGElement,
SVGRenderer = H.SVGRenderer,
win = H.win,
wrap = H.wrap;
- // extend SvgElement for useHTML option
- extend(SVGElement.prototype, {
+ // Extend SvgElement for useHTML option
+ extend(SVGElement.prototype, /** @lends SVGElement.prototype */ {
/**
* Apply CSS to HTML elements. This is used in text within SVG rendering and
* by the VML renderer
*/
htmlCss: function(styles) {
@@ -4813,11 +5843,11 @@
this.yCorr = -baseline;
}
});
// Extend SvgRenderer for useHTML option.
- extend(SVGRenderer.prototype, {
+ extend(SVGRenderer.prototype, /** @lends SVGRenderer.prototype */ {
/**
* Create HTML text node. This is used by the VML renderer as well as the SVG
* renderer through the useHTML option.
*
* @param {String} str
@@ -4830,11 +5860,11 @@
renderer = wrapper.renderer,
isSVG = renderer.isSVG,
addSetters = function(element, style) {
// These properties are set as attributes on the SVG group, and as
// identical CSS properties on the div. (#3542)
- each(['display', 'opacity', 'visibility'], function(prop) {
+ each(['opacity', 'visibility'], function(prop) {
wrap(element, prop + 'Setter', function(proceed, value, key, elem) {
proceed.call(this, value, key, elem);
style[key] = value;
});
});
@@ -5357,11 +6387,15 @@
if (cutOff) {
shadow.cutOff = strokeWidth + 1;
}
// apply the opacity
- markup = ['<stroke color="', shadowOptions.color || '#000000', '" opacity="', shadowElementOpacity * i, '"/>'];
+ markup = [
+ '<stroke color="',
+ shadowOptions.color || '#000000',
+ '" opacity="', shadowElementOpacity * i, '"/>'
+ ];
createElement(renderer.prepVML(markup), null, null, shadow);
// insert it
if (group) {
@@ -5397,11 +6431,11 @@
var strokeElem = element.getElementsByTagName('stroke')[0] ||
createElement(this.renderer.prepVML(['<stroke/>']), null, null, element);
strokeElem[key] = value || 'solid';
this[key] = value;
/* because changing stroke-width will change the dash length
- and cause an epileptic effect */
+ and cause an epileptic effect */
},
dSetter: function(value, key, element) {
var i,
shadows = this.shadows;
value = value || [];
@@ -5485,24 +6519,21 @@
}
key = 'top';
}
element.style[key] = value;
},
- displaySetter: function(value, key, element) {
- element.style[key] = value;
- },
xSetter: function(value, key, element) {
this[key] = value; // used in getter
if (key === 'x') {
key = 'left';
} else if (key === 'y') {
key = 'top';
}
/* else {
- value = Math.max(0, value); // don't set width or height below zero (#311)
- }*/
+ value = Math.max(0, value); // don't set width or height below zero (#311)
+ }*/
// clipping rectangle special
if (this.updateClipping) {
this[key] = value; // the key is now 'left' or 'top' for 'x' and 'y'
this.updateClipping();
@@ -5648,11 +6679,14 @@
},
// used in attr and animation to update the clipping of all members
updateClipping: function() {
each(clipRect.members, function(member) {
- if (member.element) { // Deleted series, like in stock/members/series-remove demo. Should be removed from members, but this will do.
+ // Member.element is falsy on deleted series, like in
+ // stock/members/series-remove demo. Should be removed
+ // from members, but this will do.
+ if (member.element) {
member.css(clipRect.getCSS(member));
}
});
}
});
@@ -5701,12 +6735,14 @@
lastStop,
colors = [],
addFillNode = function() {
// Add the fill subnode. When colors attribute is used, the meanings of opacity and o:opacity2
// are reversed.
- markup = ['<fill colors="' + colors.join(',') + '" opacity="', opacity2, '" o:opacity2="', opacity1,
- '" type="', fillType, '" ', fillAttr, 'focus="100%" method="any" />'
+ markup = ['<fill colors="' + colors.join(',') +
+ '" opacity="', opacity2, '" o:opacity2="',
+ opacity1, '" type="', fillType, '" ', fillAttr,
+ 'focus="100%" method="any" />'
];
createElement(renderer.prepVML(markup), null, null, elem);
};
// Extend from 0 to 1
@@ -5958,11 +6994,13 @@
/**
* For rectangles, VML uses a shape for rect to overcome bugs and rotation problems
*/
createElement: function(nodeName) {
- return nodeName === 'rect' ? this.symbol(nodeName) : SVGRenderer.prototype.createElement.call(this, nodeName);
+ return nodeName === 'rect' ?
+ this.symbol(nodeName) :
+ SVGRenderer.prototype.createElement.call(this, nodeName);
},
/**
* In the VML renderer, each child of an inverted div (group) is inverted
* @param {Object} element
@@ -5978,11 +7016,12 @@
left: pInt(parentStyle.width) - (imgStyle ? pInt(imgStyle.top) : 1),
top: pInt(parentStyle.height) - (imgStyle ? pInt(imgStyle.left) : 1),
rotation: -90
});
- // Recursively invert child elements, needed for nested composite shapes like box plots and error bars. #1680, #1806.
+ // Recursively invert child elements, needed for nested composite
+ // shapes like box plots and error bars. #1680, #1806.
each(element.childNodes, function(child) {
ren.invertChild(child, element);
});
},
@@ -6156,11 +7195,11 @@
},
global: {
useUTC: true,
//timezoneOffset: 0,
- VMLRadialGradientURL: 'http://code.highcharts.com@product.cdnpath@/5.0.0/gfx/vml-radial-gradient.png'
+ VMLRadialGradientURL: 'http://code.highcharts.com/5.0.3/gfx/vml-radial-gradient.png'
},
chart: {
//animation: true,
//alignTicks: false,
@@ -6210,11 +7249,10 @@
plotBorderColor: '#cccccc'
//plotBorderWidth: 0,
//plotShadow: false
},
-
title: {
text: 'Chart title',
align: 'center',
// floating: false,
margin: 15,
@@ -6403,12 +7441,15 @@
};
/**
- * Set the time methods globally based on the useUTC option. Time method can be either
- * local time or UTC (default).
+ * 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,
@@ -6733,11 +7774,12 @@
path.push(
toPath[4],
toPath[5],
toPath[1],
- toPath[2]
+ toPath[2],
+ 'z' // #5909
);
} else { // outside the axis area
path = null;
}
@@ -6854,11 +7896,14 @@
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];
+ dateTimeLabelFormat =
+ options.dateTimeLabelFormats[
+ tickPositionInfo.higherRanks[pos] || tickPositionInfo.unitName
+ ];
}
// set properties for access in render method
tick.isFirst = isFirst;
tick.isLast = isLast;
@@ -6953,11 +7998,12 @@
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)));
+ 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)) {
@@ -6988,11 +8034,15 @@
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),
+ 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
};
@@ -7086,11 +8136,12 @@
show = true,
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
+ reverseCrisp = ((horiz && x === axis.pos + axis.len) ||
+ (!horiz && y === axis.pos)) ? -1 : 1; // #1480, #1687
var gridPrefix = type ? type + 'Grid' : 'grid',
gridLineWidth = options[gridPrefix + 'LineWidth'],
gridLineColor = options[gridPrefix + 'LineColor'],
@@ -7172,11 +8223,12 @@
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) {
+ } else if (horiz && !axis.isRadial && !labelOptions.step &&
+ !labelOptions.rotation && !old && opacity !== 0) {
tick.handleOverflow(xy);
}
// apply step
if (step && index % step) {
@@ -7210,10 +8262,11 @@
* (c) 2010-2016 Torstein Honsi
*
* License: www.highcharts.com/license
*/
'use strict';
+
var addEvent = H.addEvent,
animObject = H.animObject,
arrayMax = H.arrayMax,
arrayMin = H.arrayMin,
AxisPlotLineOrBandExtension = H.AxisPlotLineOrBandExtension,
@@ -7240,12 +8293,14 @@
PlotLineOrBand = H.PlotLineOrBand,
removeEvent = H.removeEvent,
splat = H.splat,
syncTimeout = H.syncTimeout,
Tick = H.Tick;
+
/**
- * Create a new axis object
+ * Create a new axis object.
+ * @constructor Axis
* @param {Object} chart
* @param {Object} options
*/
H.Axis = function() {
this.init.apply(this, arguments);
@@ -7386,11 +8441,11 @@
style: {
fontSize: '11px',
fontWeight: 'bold',
color: '#000000',
- textShadow: '1px 1px contrast, -1px -1px contrast, -1px 1px contrast, 1px -1px contrast'
+ textOutline: '1px contrast'
}
},
gridLineWidth: 1,
@@ -7635,11 +8690,13 @@
defaultLabelFormatter: function() {
var axis = this.axis,
value = this.value,
categories = axis.categories,
dateTimeLabelFormat = this.dateTimeLabelFormat,
- numericSymbols = defaultOptions.lang.numericSymbols,
+ lang = defaultOptions.lang,
+ numericSymbols = lang.numericSymbols,
+ numSymMagnitude = lang.numericSymbolMagnitude || 1000,
i = numericSymbols && numericSymbols.length,
multi,
ret,
formatOption = axis.options.labels.format,
@@ -7658,11 +8715,11 @@
} 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(1000, i + 1);
+ 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];
}
}
}
@@ -7805,15 +8862,13 @@
// From value to pixels
} else {
if (doPostTranslate) { // log and ordinal axes
val = axis.val2lin(val);
}
- if (pointPlacement === 'between') {
- pointPlacement = 0.5;
- }
- returnValue = sign * (val - localMin) * localA + cvsOffset + (sign * minPixelPadding) +
- (isNumber(pointPlacement) ? localA * pointPlacement * axis.pointRange : 0);
+ returnValue = sign * (val - localMin) * localA + cvsOffset +
+ (sign * minPixelPadding) +
+ (isNumber(pointPlacement) ? localA * pointPlacement : 0);
}
return returnValue;
},
@@ -7965,11 +9020,17 @@
max,
options.startOfWeek
)
);
} else {
- for (pos = min + (tickPositions[0] - min) % minorTickInterval; pos <= max; pos += minorTickInterval) {
+ 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);
}
}
}
@@ -8065,12 +9126,18 @@
if (this.categories) {
ret = 1;
} else {
each(this.series, function(series) {
- var seriesClosest = series.closestPointRange;
- if (!series.noSharedTooltip && defined(seriesClosest)) {
+ 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;
}
});
@@ -8089,11 +9156,11 @@
x;
point.series.requireSorting = false;
if (!defined(nameX)) {
- nameX = this.options.nameToX === false ?
+ 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) {
@@ -8118,12 +9185,15 @@
if (this.names.length > 0) {
this.names.length = 0;
this.minRange = undefined;
each(this.series || [], function(series) {
+ // Reset incrementer (#5928)
+ series.xIncrement = null;
+
// When adding a series, points are not yet generated
- if (!series.processedXData) {
+ if (!series.points || series.isDirtyData) {
series.processData();
series.generatePoints();
}
each(series.points, function(point, i) {
@@ -8332,16 +9402,20 @@
axis.max += length * maxPadding;
}
}
}
- // Stay within floor and ceiling
+ // Handle options for floor, ceiling, softMin and softMax
if (isNumber(options.floor)) {
axis.min = Math.max(axis.min, options.floor);
+ } else if (isNumber(options.softMin)) {
+ axis.min = Math.min(axis.min, options.softMin);
}
if (isNumber(options.ceiling)) {
axis.max = Math.min(axis.max, options.ceiling);
+ } else if (isNumber(options.softMax)) {
+ axis.max = Math.max(axis.max, options.softMax);
}
// 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
@@ -8744,32 +9818,36 @@
dataMax = this.dataMax,
options = this.options,
min = Math.min(dataMin, pick(options.min, dataMin)),
max = Math.max(dataMax, pick(options.max, dataMax));
- // Prevent pinch zooming out of range. Check for defined is for #1946. #1734.
- if (!this.allowZoomOutside) {
- if (defined(dataMin) && newMin <= min) {
- newMin = min;
+ if (newMin !== this.min || newMax !== this.max) { // #5790
+
+ // Prevent pinch zooming out of range. Check for defined is for #1946. #1734.
+ if (!this.allowZoomOutside) {
+ if (defined(dataMin) && newMin <= min) {
+ newMin = min;
+ }
+ if (defined(dataMax) && newMax >= max) {
+ newMax = max;
+ }
}
- if (defined(dataMax) && newMax >= max) {
- newMax = max;
- }
- }
- // In full view, displaying the reset zoom button is not required
- this.displayBtn = newMin !== undefined || newMax !== undefined;
+ // 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'
- }
- );
+ // Do it
+ this.setExtremes(
+ newMin,
+ newMax,
+ false,
+ undefined, {
+ trigger: 'zoom'
+ }
+ );
+ }
+
return true;
},
/**
* Update the axis metrics
@@ -9323,11 +10401,10 @@
], lineWidth);
},
/**
* Render the axis line
- * @returns {[type]} [description]
*/
renderLine: function() {
if (!this.axisLine) {
this.axisLine = this.chart.renderer.path()
.addClass('highcharts-axis-line')
@@ -9587,21 +10664,24 @@
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.
*/
destroy: function(keepEvents) {
var axis = this,
stacks = axis.stacks,
stackKey,
plotLinesAndBands = axis.plotLinesAndBands,
i,
- n,
- keepProps;
+ n;
// Remove the events
if (!keepEvents) {
removeEvent(axis);
}
@@ -9629,16 +10709,13 @@
if (axis[prop]) {
axis[prop] = axis[prop].destroy();
}
});
-
// Delete all properties and fall back to the prototype.
- // Preserve some properties, needed for Axis.update (#4317).
- keepProps = ['names', 'series', 'userMax', 'userMin'];
for (n in axis) {
- if (axis.hasOwnProperty(n) && inArray(n, keepProps) === -1) {
+ if (axis.hasOwnProperty(n) && inArray(n, axis.keepProps) === -1) {
delete axis[n];
}
}
},
@@ -9650,10 +10727,11 @@
*/
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
@@ -9664,29 +10742,34 @@
if (
// Disabled in options
!this.crosshair ||
// Snap
- ((defined(point) || !pick(options.snap, true)) === false)
+ ((defined(point) || !snap) === false)
) {
this.hideCrosshair();
} else {
// Get the path
- if (!pick(options.snap, true)) {
- pos = (this.horiz ? e.chartX - this.pos : this.len - e.chartY + this.pos);
+ 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 (this.isRadial) {
- path = this.getPlotLinePath(this.isXAxis ? point.x : pick(point.stackY, point.y)) || null; // #3189
- } else {
- path = this.getPlotLinePath(null, null, null, null, pos) || null; // #3189
+ 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 (path === null) {
+ if (!defined(path)) {
this.hideCrosshair();
return;
}
categorized = this.categories && !this.isRadial;
@@ -9719,18 +10802,17 @@
graphic.show().attr({
d: path
});
- if (categorized) {
+ if (categorized && !options.width) {
graphic.attr({
'stroke-width': this.transA
});
}
this.cross.e = e;
}
-
},
/**
* Hide the crosshair.
*/
@@ -9751,17 +10833,17 @@
* License: www.highcharts.com/license
*/
'use strict';
var Axis = H.Axis,
Date = H.Date,
+ dateFormat = H.dateFormat,
defaultOptions = H.defaultOptions,
defined = H.defined,
each = H.each,
extend = H.extend,
getMagnitude = H.getMagnitude,
getTZOffset = H.getTZOffset,
- grep = H.grep,
normalizeTickInterval = H.normalizeTickInterval,
pick = H.pick,
timeUnits = H.timeUnits;
/**
* Set the tick positions to a time unit that makes sense, for example
@@ -9781,11 +10863,12 @@
useUTC = defaultOptions.global.useUTC,
minYear, // used in months and years as a basis for Date.UTC()
minDate = new Date(min - getTZOffset(min)),
makeTime = Date.hcMakeTime,
interval = normalizedInterval.unitRange,
- count = normalizedInterval.count;
+ count = normalizedInterval.count,
+ variableDayLength;
if (defined(min)) { // #1300
minDate[Date.hcSetMilliseconds](interval >= timeUnits.second ? 0 : // #3935
count * Math.floor(minDate.getMilliseconds() / count)); // #3652, #3654
@@ -9826,26 +10909,44 @@
minDate[Date.hcSetDate](minDate[Date.hcGetDate]() - minDate[Date.hcGetDay]() +
pick(startOfWeek, 1));
}
- // get tick positions
- i = 1;
+ // Get basics for variable time spans
+ minYear = minDate[Date.hcGetFullYear]();
+ var minMonth = minDate[Date.hcGetMonth](),
+ minDateDate = minDate[Date.hcGetDate](),
+ minHours = minDate[Date.hcGetHours]();
+
+
+ // Handle local timezone offset
if (Date.hcTimezoneOffset || Date.hcGetTimezoneOffset) {
+
+ // Detect whether we need to take the DST crossover into
+ // consideration. If we're crossing over DST, the day length may be
+ // 23h or 25h and we need to compute the exact clock time for each
+ // tick instead of just adding hours. This comes at a cost, so first
+ // we found out if it is needed. #4951.
+ variableDayLength =
+ (!useUTC || !!Date.hcGetTimezoneOffset) &&
+ (
+ // Long range, assume we're crossing over.
+ max - min > 4 * timeUnits.month ||
+ // Short range, check if min and max are in different time
+ // zones.
+ getTZOffset(min) !== getTZOffset(max)
+ );
+
+ // Adjust minDate to the offset date
minDate = minDate.getTime();
minDate = new Date(minDate + getTZOffset(minDate));
}
- minYear = minDate[Date.hcGetFullYear]();
- var time = minDate.getTime(),
- minMonth = minDate[Date.hcGetMonth](),
- minDateDate = minDate[Date.hcGetDate](),
- variableDayLength = !useUTC || !!Date.hcGetTimezoneOffset, // #4951
- localTimezoneOffset = (timeUnits.day +
- (useUTC ? getTZOffset(minDate) : minDate.getTimezoneOffset() * 60 * 1000)
- ) % timeUnits.day; // #950, #3359
- // iterate and add tick positions at appropriate values
+
+ // Iterate and add tick positions at appropriate values
+ var time = minDate.getTime();
+ i = 1;
while (time < max) {
tickPositions.push(time);
// if the interval is years, use Date.UTC to increase years
if (interval === timeUnits.year) {
@@ -9859,10 +10960,13 @@
// one hour at the DST crossover
} else if (variableDayLength && (interval === timeUnits.day || interval === timeUnits.week)) {
time = makeTime(minYear, minMonth, minDateDate +
i * count * (interval === timeUnits.day ? 1 : 7));
+ } else if (variableDayLength && interval === timeUnits.hour) {
+ time = makeTime(minYear, minMonth, minDateDate, minHours + i * count);
+
// else, the interval is fixed and we use simple addition
} else {
time += interval * count;
}
@@ -9871,16 +10975,19 @@
// push the last time
tickPositions.push(time);
- // mark new days if the time is dividible by day (#1649, #1760)
- each(grep(tickPositions, function(time) {
- return interval <= timeUnits.hour && time % timeUnits.day === localTimezoneOffset;
- }), function(time) {
- higherRanks[time] = 'day';
- });
+ // Handle higher ranks. Mark new days if the time is on midnight
+ // (#950, #1649, #1760, #3349)
+ if (interval <= timeUnits.hour) {
+ each(tickPositions, function(time) {
+ if (dateFormat('%H%M%S%L', time) === '000000000') {
+ higherRanks[time] = 'day';
+ }
+ });
+ }
}
// record information on the chosen unit - for dynamic label formatter
tickPositions.info = extend(normalizedInterval, {
@@ -10102,12 +11209,11 @@
* (c) 2010-2016 Torstein Honsi
*
* License: www.highcharts.com/license
*/
'use strict';
- var addEvent = H.addEvent,
- dateFormat = H.dateFormat,
+ var dateFormat = H.dateFormat,
each = H.each,
extend = H.extend,
format = H.format,
isNumber = H.isNumber,
map = H.map,
@@ -10153,47 +11259,85 @@
// Public property for getting the shared state.
this.split = options.split && !chart.inverted;
this.shared = options.shared || this.split;
+ },
- // Create the label
- if (this.split) {
- this.label = this.chart.renderer.g('tooltip');
- } else {
- this.label = chart.renderer.label(
- '',
- 0,
- 0,
- options.shape || 'callout',
- null,
- null,
- options.useHTML,
- null,
- 'tooltip'
- )
- .attr({
- padding: options.padding,
- r: options.borderRadius,
- display: 'none' // #2301, #2657, #3532, #5570
- });
+ /**
+ * 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;
+ }
+ }
+ });
+ },
+
+
+ /**
+ * 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
+ });
+
+
+ this.label
+ .attr({
+ 'fill': options.backgroundColor,
+ 'stroke-width': options.borderWidth
+ })
+ // #2301, #2657
+ .css(options.style)
+ .shadow(options.shadow);
+
+ }
+
+
+
this.label
.attr({
- 'fill': options.backgroundColor,
- 'stroke-width': options.borderWidth
+ zIndex: 8
})
- // #2301, #2657
- .css(options.style)
- .shadow(options.shadow);
-
+ .add();
}
- this.label.attr({
- zIndex: 8
- })
- .add();
+ return this.label;
},
update: function(options) {
this.destroy();
this.init(this.chart, merge(true, this.options, options));
@@ -10205,10 +11349,14 @@
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);
},
/**
@@ -10233,22 +11381,23 @@
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.label.attr(now);
+ 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.
+ // 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);
@@ -10262,11 +11411,11 @@
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.label[delay ? 'fadeOut' : 'hide']();
+ tooltip.getLabel()[delay ? 'fadeOut' : 'hide']();
tooltip.isHidden = true;
}, delay);
}
},
@@ -10333,12 +11482,18 @@
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],
+ 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.
@@ -10354,11 +11509,16 @@
} 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);
+ ret[dim] = Math.max(
+ min,
+ alignedRight + h + innerSize > outerSize ?
+ alignedRight :
+ alignedRight + h
+ );
} else {
return false;
}
},
/**
@@ -10426,18 +11586,18 @@
*/
defaultFormatter: function(tooltip) {
var items = this.points || splat(this),
s;
- // build the header
- s = [tooltip.tooltipFooterHeaderFormatter(items[0])]; //#3397: abstraction to enable formatting of footer and header
+ // 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)); //#3397: abstraction to enable formatting of footer and header
+ s.push(tooltip.tooltipFooterHeaderFormatter(items[0], true));
return s;
},
/**
@@ -10445,11 +11605,11 @@
* @param {Object} point
*/
refresh: function(point, mouseEvent) {
var tooltip = this,
chart = tooltip.chart,
- label = tooltip.label,
+ label,
options = tooltip.options,
x,
y,
anchor,
textConfig = {},
@@ -10507,25 +11667,26 @@
// update the inner HTML
if (text === false) {
this.hide();
} else {
+ label = tooltip.getLabel();
+
// show it
if (tooltip.isHidden) {
stop(label);
label.attr({
- opacity: 1,
- display: 'block'
+ opacity: 1
}).show();
}
// update text
if (tooltip.split) {
this.renderSplit(text, chart.hoverPoints);
} else {
label.attr({
- text: text.join ? text.join('') : text
+ 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));
@@ -10559,20 +11720,13 @@
boxes = [],
chart = this.chart,
ren = chart.renderer,
rightAligned = true,
options = this.options,
- headerHeight;
+ headerHeight,
+ tooltipLabel = this.getLabel();
- /**
- * Destroy a single-series tooltip
- */
- function destroy(tt) {
- tt.connector = tt.connector.destroy();
- tt.destroy();
- }
-
// Create the individual labels
each(labels.slice(0, labels.length - 1), function(str, i) {
var point = points[i - 1] ||
// Item 0 is the header. Instead of this, we could also use the crosshair label
{
@@ -10583,57 +11737,52 @@
tt = owner.tt,
series = point.series || {},
colorClass = 'highcharts-color-' + pick(point.colorIndex, series.colorIndex, 'none'),
target,
x,
- bBox;
+ bBox,
+ boxWidth;
// Store the tooltip referance on the series
if (!tt) {
- owner.tt = tt = ren.label(null, null, null, point.isHeader && 'callout')
+ owner.tt = tt = ren.label(null, null, null, 'callout')
.addClass('highcharts-tooltip-box ' + colorClass)
.attr({
'padding': options.padding,
'r': options.borderRadius,
'fill': options.backgroundColor,
'stroke': point.color || series.color || '#333333',
'stroke-width': options.borderWidth
})
- .add(tooltip.label);
-
- // Add a connector back to the point
- if (point.series) {
- tt.connector = ren.path()
- .addClass('highcharts-tooltip-connector ' + colorClass)
-
- .attr({
- 'stroke-width': series.options.lineWidth || 2,
- 'stroke': point.color || series.color || '#666666'
- })
-
- .add(tooltip.label);
-
- addEvent(point.series, 'hide', function() {
- this.tt = destroy(this.tt);
- });
- }
+ .add(tooltipLabel);
}
+
tt.isActive = true;
tt.attr({
text: str
});
+ tt.css(options.style);
+
+
// 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 = point.plotX + chart.plotLeft - bBox.width / 2;
+ x = Math.max(
+ 0, // No left overflow
+ Math.min(
+ point.plotX + chart.plotLeft - boxWidth / 2,
+ chart.chartWidth - boxWidth // No right overflow (#5794)
+ )
+ );
} else {
x = point.plotX + chart.plotLeft - pick(options.distance, 16) -
- bBox.width;
+ boxWidth;
}
// If overflow left, we don't use this x in the next loop
if (x < 0) {
@@ -10652,64 +11801,37 @@
tt: tt
});
});
// Clean previous run (for missing points)
- each(chart.series, function(series) {
- var tt = series.tt;
- if (tt) {
- if (!tt.isActive) {
- series.tt = destroy(tt);
- } else {
- tt.isActive = false;
- }
- }
- });
+ this.cleanSplit();
// Distribute and put in place
H.distribute(boxes, chart.plotHeight + headerHeight);
each(boxes, function(box) {
- var point = box.point,
- tt = box.tt,
- attr;
+ var point = box.point;
// Put the label in place
- attr = {
- display: box.pos === undefined ? 'none' : '',
- x: (rightAligned || point.isHeader ? box.x : point.plotX + chart.plotLeft + pick(options.distance, 16)),
- y: box.pos + chart.plotTop
- };
- if (point.isHeader) {
- attr.anchorX = point.plotX + chart.plotLeft;
- attr.anchorY = attr.y - 100;
- }
- tt.attr(attr);
-
- // Draw the connector to the point
- if (!point.isHeader) {
- tt.connector.attr({
- d: [
- 'M',
- point.plotX + chart.plotLeft,
- point.plotY + point.series.yAxis.pos,
- 'L',
- rightAligned ?
- point.plotX + chart.plotLeft - pick(options.distance, 16) :
- point.plotX + chart.plotLeft + pick(options.distance, 16),
- box.pos + chart.plotTop + tt.getBBox().height / 2
- ]
- });
- }
+ 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.plotX + chart.plotLeft,
+ anchorY: point.isHeader ?
+ box.pos + chart.plotTop - 15 : point.plotY + chart.plotTop
+ });
});
},
/**
* Find the new position and perform the move
*/
updatePosition: function(point) {
var chart = this.chart,
- label = this.label,
+ label = this.getLabel(),
pos = (this.options.positioner || this.getPosition).call(
this,
label.width,
label.height,
point
@@ -10816,11 +11938,12 @@
* 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);
+ return (tooltipOptions.pointFormatter || item.point.tooltipFormatter)
+ .call(item.point, tooltipOptions.pointFormat);
});
}
};
@@ -10847,16 +11970,16 @@
removeEvent = H.removeEvent,
splat = H.splat,
Tooltip = H.Tooltip,
win = H.win;
- // Global flag for touch support
- H.hasTouch = doc && doc.documentElement.ontouchstart !== undefined;
-
/**
- * 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.
+ * 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 Pointer
* @param {Object} chart The Chart instance
* @param {Object} options The root options object
*/
H.Pointer = function(chart, options) {
this.init(chart, options);
@@ -10885,21 +12008,28 @@
this.setDOMEvents();
},
/**
- * Resolve the zoomType option
+ * Resolve the zoomType option, this is reset on all touch start and mouse
+ * down events.
*/
- zoomOption: function() {
+ zoomOption: function(e) {
var chart = this.chart,
- zoomType = chart.options.chart.zoomType,
- zoomX = /x/.test(zoomType),
- zoomY = /y/.test(zoomType),
- inverted = chart.inverted;
+ options = chart.options.chart,
+ zoomType = options.zoomType || '',
+ inverted = chart.inverted,
+ zoomX,
+ zoomY;
- this.zoomX = zoomX;
- this.zoomY = 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;
},
@@ -11024,46 +12154,49 @@
// Sort kdpoints by distance to mouse pointer
kdpoints.sort(function(p1, p2) {
var isCloserX = p1.distX - p2.distX,
isCloser = p1.dist - p2.dist,
- isAbove = p1.series.group.zIndex > p2.series.group.zIndex ? -1 : 1;
+ isAbove = p2.series.group.zIndex - p1.series.group.zIndex;
+
// We have two points which are not in the same place on xAxis and shared tooltip:
- if (isCloserX !== 0) {
+ if (isCloserX !== 0 && shared) { // #5721
return isCloserX;
}
// Points are not exactly in the same place on x/yAxis:
if (isCloser !== 0) {
return isCloser;
}
// The same xAxis and yAxis position, sort by z-index:
- return isAbove;
+ if (isAbove !== 0) {
+ return isAbove;
+ }
+
+ // The same zIndex, sort by array index:
+ return p1.series.index > p2.series.index ? -1 : 1;
});
}
// Remove points with different x-positions, required for shared tooltip and crosshairs (#4645):
if (shared) {
i = kdpoints.length;
while (i--) {
- if (kdpoints[i].clientX !== kdpoints[0].clientX || kdpoints[i].series.noSharedTooltip) {
+ if (kdpoints[i].x !== kdpoints[0].x || kdpoints[i].series.noSharedTooltip) {
kdpoints.splice(i, 1);
}
}
}
// Refresh tooltip for kdpoint if new hover point or tooltip was hidden // #3926, #4200
- if (kdpoints[0] && (kdpoints[0] !== pointer.hoverPoint || (tooltip && tooltip.isHidden))) {
+ if (kdpoints[0] && (kdpoints[0] !== this.prevKDPoint || (tooltip && tooltip.isHidden))) {
// Draw tooltip if necessary
if (shared && !kdpoints[0].series.noSharedTooltip) {
- // Do mouseover on all points (#3919, #3985, #4410)
- for (i = 0; i >= 0; i--) {
+ // Do mouseover on all points (#3919, #3985, #4410, #5622)
+ for (i = 0; i < kdpoints.length; i++) {
kdpoints[i].onMouseOver(e, kdpoints[i] !== ((hoverSeries && hoverSeries.directTouch && hoverPoint) || kdpoints[0]));
}
- // Make sure that the hoverPoint and hoverSeries are stored for events (e.g. click), #5622
- if (hoverSeries && hoverSeries.directTouch && hoverPoint && hoverPoint !== kdpoints[0]) {
- hoverPoint.onMouseOver(e, false);
- }
+
if (kdpoints.length && tooltip) {
// Keep the order of series in tooltip:
tooltip.refresh(kdpoints.sort(function(p1, p2) {
return p1.series.index - p2.series.index;
}), e);
@@ -11074,11 +12207,11 @@
}
if (!hoverSeries || !hoverSeries.directTouch) { // #4448
kdpoints[0].onMouseOver(e);
}
}
- pointer.prevKDPoint = kdpoints[0];
+ this.prevKDPoint = kdpoints[0];
updatePosition = false;
}
// Update positions (regardless of kdpoint or hoverPoint)
if (updatePosition) {
followPointer = hoverSeries && hoverSeries.tooltipOptions.followPointer;
@@ -11090,17 +12223,16 @@
});
}
}
// Start the event listener to pick up the tooltip and crosshairs
- if (!pointer._onDocumentMouseMove) {
- pointer._onDocumentMouseMove = function(e) {
+ if (!pointer.unDocMouseMove) {
+ pointer.unDocMouseMove = addEvent(doc, 'mousemove', function(e) {
if (charts[H.hoverChartIndex]) {
charts[H.hoverChartIndex].pointer.onDocumentMouseMove(e);
}
- };
- addEvent(doc, 'mousemove', pointer._onDocumentMouseMove);
+ });
}
// Crosshair. For each hover point, loop over axes and draw cross if that point
// belongs to the axis (#4927).
each(shared ? kdpoints : [pick(hoverPoint, kdpoints[0])], function drawPointCrosshair(point) { // #5269
@@ -11169,13 +12301,12 @@
if (tooltip) {
tooltip.hide(delay);
}
- if (pointer._onDocumentMouseMove) {
- removeEvent(doc, 'mousemove', pointer._onDocumentMouseMove);
- pointer._onDocumentMouseMove = null;
+ if (pointer.unDocMouseMove) {
+ pointer.unDocMouseMove = pointer.unDocMouseMove();
}
// Remove crosshairs
each(chart.axes, function(axis) {
axis.hideCrosshair();
@@ -11194,11 +12325,11 @@
seriesAttribs;
// Scale each series
each(chart.series, function(series) {
seriesAttribs = attribs || series.getPlotBox(); // #1701
- if (series.xAxis && series.xAxis.zoomEnabled) {
+ 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);
}
@@ -11394,11 +12525,11 @@
onContainerMouseDown: function(e) {
e = this.normalize(e);
- this.zoomOption();
+ this.zoomOption(e);
// issue #295, dragging not always working in Firefox
if (e.preventDefault) {
e.preventDefault();
}
@@ -11490,11 +12621,14 @@
var series = this.chart.hoverSeries,
relatedTarget = e.relatedTarget || e.toElement;
if (series && relatedTarget && !series.options.stickyTracking &&
!this.inClass(relatedTarget, 'highcharts-tooltip') &&
- !this.inClass(relatedTarget, 'highcharts-series-' + series.index)) { // #2499, #4465
+ (!this.inClass(relatedTarget, 'highcharts-series-' + series.index) || // #2499, #4465
+ !this.inClass(relatedTarget, 'highcharts-tracker') // #5553
+ )
+ ) {
series.onMouseOut();
}
},
onContainerClick: function(e) {
@@ -11607,28 +12741,29 @@
noop = H.noop,
pick = H.pick,
Pointer = H.Pointer;
/* Support for touch devices */
- extend(Pointer.prototype, {
+ extend(Pointer.prototype, /** @lends Pointer.prototype */ {
/**
* Run translation operations
*/
pinchTranslate: function(pinchDown, touches, transform, selectionMarker, clip, lastValidTouch) {
- if (this.zoomHor || this.pinchHor) {
+ if (this.zoomHor) {
this.pinchTranslateDirection(true, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
}
- if (this.zoomVert || this.pinchVert) {
+ if (this.zoomVert) {
this.pinchTranslateDirection(false, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
}
},
/**
* Run translation operations for each direction (horizontal and vertical) independently
*/
- pinchTranslateDirection: function(horiz, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch, forcedScale) {
+ pinchTranslateDirection: function(horiz, pinchDown, touches, transform,
+ selectionMarker, clip, lastValidTouch, forcedScale) {
var chart = this.chart,
xy = horiz ? 'x' : 'y',
XY = horiz ? 'X' : 'Y',
sChartXY = 'chart' + XY,
wh = horiz ? 'width' : 'height',
@@ -11646,11 +12781,12 @@
touch1Now = !singleTouch && touches[1][sChartXY],
outOfBounds,
transformScale,
scaleKey,
setScale = function() {
- if (!singleTouch && Math.abs(touch0Start - touch1Start) > 20) { // Don't zoom if fingers are too close on this axis
+ // Don't zoom if fingers are too close on this axis
+ if (!singleTouch && Math.abs(touch0Start - touch1Start) > 20) {
scale = forcedScale || Math.abs(touch0Now - touch1Now) / Math.abs(touch0Start - touch1Start);
}
clipXY = ((plotLeftTop - touch0Now) / scale) + touch0Start;
selectionWH = chart['plot' + (horiz ? 'Width' : 'Height')] / scale;
@@ -11761,10 +12897,14 @@
bounds.max = Math.max(axis.pos + axis.len, absMax + minPixelPadding);
}
});
self.res = true; // reset on next move
+ // Optionally move the tooltip on touchmove
+ } else if (self.followTouchMove && touchesLength === 1) {
+ this.runPointActions(self.normalize(e));
+
// Event type is touchmove, handle panning and pinching
} else if (pinchDown.length) { // can be 0 when releasing, if touchend fires first
// Set the marker
@@ -11780,14 +12920,11 @@
self.hasPinched = hasZoom;
// Scale and translate the groups to provide visual feedback during pinching
self.scaleGroups(transform, clip);
- // Optionally move the tooltip on touchmove
- if (!hasZoom && self.followTouchMove && touchesLength === 1) {
- this.runPointActions(self.normalize(e));
- } else if (self.res) {
+ if (self.res) {
self.res = false;
this.reset(false, 0);
}
}
},
@@ -11796,19 +12933,24 @@
* General touch handler shared by touchstart and touchmove.
*/
touch: function(e, start) {
var chart = this.chart,
hasMoved,
- pinchDown;
+ pinchDown,
+ isInside;
H.hoverChartIndex = chart.index;
if (e.touches.length === 1) {
e = this.normalize(e);
- if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop) && !chart.openMenu) {
+ isInside = chart.isInsidePlot(
+ e.chartX - chart.plotLeft,
+ e.chartY - chart.plotTop
+ );
+ if (isInside && !chart.openMenu) {
// Run mouse events and display tooltip etc
if (start) {
this.runPointActions(e);
}
@@ -11839,11 +12981,11 @@
this.pinch(e);
}
},
onContainerTouchStart: function(e) {
- this.zoomOption();
+ this.zoomOption(e);
this.touch(e, true);
},
onContainerTouchMove: function(e) {
this.touch(e);
@@ -11913,11 +13055,11 @@
};
/**
* Extend the Pointer prototype with methods for each event handler and more
*/
- extend(Pointer.prototype, {
+ extend(Pointer.prototype, /** @lends Pointer.prototype */ {
onContainerPointerDown: function(e) {
translateMSPointer(e, 'onContainerTouchStart', 'touchstart', function(e) {
touches[e.pointerId] = {
pageX: e.pageX,
pageY: e.pageY,
@@ -12000,11 +13142,12 @@
setAnimation = H.setAnimation,
stableSort = H.stableSort,
win = H.win,
wrap = H.wrap;
/**
- * The overview of the chart's series
+ * The overview of the chart's series.
+ * @class
*/
Legend = H.Legend = function(chart, options) {
this.init(chart, options);
};
@@ -12191,17 +13334,18 @@
});
if (legendGroup) {
legend.group = legendGroup.destroy();
}
+ legend.display = null; // Reset in .render on update.
},
/**
* Position the checkboxes after the width is determined
*/
positionCheckboxes: function(scrollOffset) {
- var alignAttr = this.group.alignAttr,
+ var alignAttr = this.group && this.group.alignAttr,
translateY,
clipHeight = this.clipHeight || this.legendHeight,
titleHeight = this.titleHeight;
if (alignAttr) {
@@ -12300,13 +13444,13 @@
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 + ' ' +
- (item.options.className || '') +
- (isSeries ? 'highcharts-series-' + item.index : '')
+ .addClass('highcharts-' + series.type + '-series highcharts-color-' + item.colorIndex +
+ (item.options.className ? ' ' + item.options.className : '') +
+ (isSeries ? ' highcharts-series-' + item.index : '')
)
.attr({
zIndex: 1
})
.add(legend.scrollGroup);
@@ -12542,11 +13686,12 @@
box.isNew = true;
}
// Presentational
- box.attr({
+ box
+ .attr({
stroke: options.borderColor,
'stroke-width': options.borderWidth || 0,
fill: options.backgroundColor || 'none'
})
.shadow(options.shadow);
@@ -12624,23 +13769,31 @@
pages = this.pages,
padding = this.padding,
lastY,
allItems = this.allItems,
clipToHeight = function(height) {
- clipRect.attr({
- height: height
- });
+ if (height) {
+ 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 = 'rect(' + padding + 'px,9999px,' + (padding + height) + 'px,0)';
+ legend.contentGroup.div.style.clip = height ?
+ 'rect(' + padding + 'px,9999px,' +
+ (padding + height) + 'px,0)' :
+ 'auto';
}
};
// Adjust the height
- if (options.layout === 'horizontal') {
+ if (options.layout === 'horizontal' && options.verticalAlign !== 'middle' && !options.floating) {
spaceHeight /= 2;
}
if (maxHeight) {
spaceHeight = Math.min(spaceHeight, maxHeight);
}
@@ -12707,12 +13860,13 @@
// Set initial position
legend.scroll(0);
legendHeight = spaceHeight;
+ // Reset
} else if (nav) {
- clipToHeight(chart.chartHeight);
+ clipToHeight();
nav.hide();
this.scrollGroup.attr({
translateY: 1
});
this.clipHeight = 0; // #1379
@@ -12808,18 +13962,18 @@
*/
drawRectangle: function(legend, item) {
var options = legend.options,
symbolHeight = options.symbolHeight || legend.fontMetrics.f,
square = options.squareSymbol,
- symbolWidth = square ? symbolHeight : legend.symbolWidth; // docs: square
+ 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) // docs: new default
+ pick(legend.options.symbolRadius, symbolHeight / 2)
)
.addClass('highcharts-point')
.attr({
zIndex: 3
}).add(item.legendGroup);
@@ -12866,11 +14020,11 @@
.attr(attr)
.add(legendItemGroup);
// Draw the marker
if (markerOptions && markerOptions.enabled !== false) {
- radius = markerOptions.radius;
+ radius = this.symbol.indexOf('url') === 0 ? 0 : markerOptions.radius;
this.legendSymbol = legendSymbol = renderer.symbol(
this.symbol,
(symbolWidth / 2) - radius,
verticalCenter - radius,
2 * radius,
@@ -12946,14 +14100,17 @@
svg = H.svg,
syncTimeout = H.syncTimeout,
win = H.win,
Renderer = H.Renderer;
/**
- * The Chart class
- * @param {String|Object} renderTo The DOM element to render to, or its id
- * @param {Object} options
- * @param {Function} callback Function to run when the chart has loaded
+ * The Chart class.
+ * @class Highcharts.Chart
+ * @memberOf Highcharts
+ * @param {String|HTMLDOMElement} renderTo - The DOM element to render to, or its
+ * id.
+ * @param {ChartOptions} options - The chart options structure.
+ * @param {Function} callback - Function to run when the chart has loaded.
*/
var Chart = H.Chart = function() {
this.getArgs.apply(this, arguments);
};
@@ -13110,11 +14267,11 @@
/**
* Redraw legend, axes or series based on updated data
*
* @param {Boolean|Object} animation Whether to apply animation, and optionally animation
- * configuration
+ * configuration
*/
redraw: function(animation) {
var chart = this,
axes = chart.axes,
series = chart.series,
@@ -13235,12 +14392,11 @@
}
// redraw affected series
each(series, function(serie) {
- if (serie.isDirty && serie.visible &&
- (!serie.isCartesian || serie.xAxis)) { // issue #153
+ if ((isDirtyBox || serie.isDirty) && serie.visible) {
serie.redraw();
}
});
// move tooltip or reset
@@ -13543,11 +14699,11 @@
chartHeight,
renderTo = chart.renderTo,
indexAttrName = 'data-highcharts-chart',
oldChartIndex,
Ren,
- containerId = 'highcharts-' + H.idCounter++,
+ containerId = H.uniqueKey(),
containerStyle,
key;
if (!renderTo) {
chart.renderTo = renderTo = optionsChart.renderTo;
@@ -13575,15 +14731,15 @@
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 allowClone option is used in sparklines as a micro optimization,
- // saving about 1-2 ms each chart.
+ // 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.cloneRenderTo();
}
// get the width and height
@@ -13601,13 +14757,14 @@
height: chartHeight + 'px',
textAlign: 'left',
lineHeight: 'normal', // #427
zIndex: 0, // #1072
'-webkit-tap-highlight-color': 'rgba(0,0,0,0)'
- });
+ }, optionsChart.style);
- chart.container = container = createElement('div', {
+ chart.container = container = createElement(
+ 'div', {
id: containerId
},
containerStyle,
chart.renderToClone || renderTo
);
@@ -13729,19 +14886,27 @@
/**
* Add the event handlers necessary for auto resizing
*/
initReflow: function() {
var chart = this,
- reflow = function(e) {
- chart.reflow(e);
- };
+ unbind;
-
- addEvent(win, 'resize', reflow);
- addEvent(chart, 'destroy', function() {
- removeEvent(win, 'resize', reflow);
+ 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
* @param {Number} width
@@ -13785,15 +14950,10 @@
each(chart.axes, function(axis) {
axis.isDirty = true;
axis.setScale();
});
- // make sure non-cartesian series are also handled
- each(chart.series, function(serie) {
- serie.isDirty = true;
- });
-
chart.isDirtyLegend = true; // force legend redraw
chart.isDirtyBox = true; // force redraw of plot and chart border
chart.layOutTitles(); // #2857
chart.getMargins();
@@ -14442,11 +15602,13 @@
}, this);
fireEvent(this, 'load');
// Set up auto resize
- this.initReflow();
+ if (this.options.chart.reflow !== false) {
+ this.initReflow();
+ }
// Don't run again
this.onload = null;
}
@@ -14471,19 +15633,27 @@
isNumber = H.isNumber,
pick = H.pick,
removeEvent = H.removeEvent;
/**
- * The Point object and prototype. Inheritable and used as base for PiePoint
+ * 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.
+ * @constructor Point
*/
Point = H.Point = function() {};
Point.prototype = {
/**
- * Initialize the point
- * @param {Object} series The series object containing this point
- * @param {Object} options The data in either number, array or object format
+ * Initialize the point. Called internally based on the series.data option.
+ * @function #init
+ * @memberOf Point
+ * @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,
@@ -14515,14 +15685,18 @@
series.chart.pointCount++;
return point;
},
/**
- * Apply the options containing the x and y data and possible some extra properties.
- * This is called on point init or from point.update.
+ * 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
+ * @function #applyOptions
+ * @memberOf Point
+ * @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;
@@ -14545,10 +15719,15 @@
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);
}
@@ -14620,10 +15799,11 @@
*/
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 : '');
},
/**
@@ -14817,11 +15997,11 @@
SVGElement = H.SVGElement,
syncTimeout = H.syncTimeout,
win = H.win;
/**
- * @classDescription The base function which all other series types inherit from. The data in the series is stored
+ * The base function which all other series types inherit from. 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
@@ -14830,1900 +16010,1977 @@
* - 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 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.
*
- * @param {Object} chart
- * @param {Object} options
+ * @constructor Series
+ * @param {Object} chart - The chart instance.
+ * @param {Object} options - The series options.
*/
H.Series = H.seriesType('line', null, { // base series options
- //cursor: 'default',
- //dashStyle: null,
- //linecap: 'round',
- lineWidth: 2,
- //shadow: false,
+ //cursor: 'default',
+ //dashStyle: null,
+ //linecap: 'round',
+ lineWidth: 2,
+ //shadow: false,
- allowPointSelect: false,
- showCheckbox: false,
- animation: {
- duration: 1000
- },
- //clip: true,
- //connectNulls: false,
- //enableMouseTracking: true,
- events: {},
- //legendIndex: 0,
- // stacking: null,
- marker: {
+ allowPointSelect: false,
+ showCheckbox: false,
+ animation: {
+ duration: 1000
+ },
+ //clip: true,
+ //connectNulls: false,
+ //enableMouseTracking: true,
+ events: {},
+ //legendIndex: 0,
+ // stacking: null,
+ marker: {
- lineWidth: 0,
- lineColor: '#ffffff',
- //fillColor: null,
+ lineWidth: 0,
+ lineColor: '#ffffff',
+ //fillColor: null,
- //enabled: true,
- //symbol: null,
- radius: 4,
- states: { // states for a single point
- hover: {
- enabled: true,
- radiusPlus: 2,
-
- lineWidthPlus: 1
-
+ //enabled: true,
+ //symbol: null,
+ radius: 4,
+ states: { // states for a single point
+ hover: {
+ animation: {
+ duration: 50
},
+ enabled: true,
+ radiusPlus: 2,
- select: {
- fillColor: '#cccccc',
- lineColor: '#000000',
- lineWidth: 2
- }
+ lineWidthPlus: 1
- }
- },
- point: {
- events: {}
- },
- dataLabels: {
- align: 'center',
- // defer: true,
- // enabled: false,
- formatter: function() {
- return this.y === null ? '' : H.numberFormat(this.y, -1);
},
- style: {
- fontSize: '11px',
- fontWeight: 'bold',
- color: 'contrast',
- textShadow: '1px 1px contrast, -1px -1px contrast, -1px 1px contrast, 1px -1px contrast'
- },
- // backgroundColor: undefined,
- // borderColor: undefined,
- // borderWidth: undefined,
- // shadow: false
+ select: {
+ fillColor: '#cccccc',
+ lineColor: '#000000',
+ lineWidth: 2
+ }
- verticalAlign: 'bottom', // above singular point
- x: 0,
- y: 0,
- // borderRadius: undefined,
- padding: 5
+ }
+ },
+ point: {
+ events: {}
+ },
+ dataLabels: {
+ align: 'center',
+ // defer: true,
+ // enabled: false,
+ formatter: function() {
+ return this.y === null ? '' : H.numberFormat(this.y, -1);
},
- cropThreshold: 300, // draw points outside the plot area when the number of points is less than this
- pointRange: 0,
- //pointStart: 0,
- //pointInterval: 1,
- //showInLegend: null, // auto: true for standalone series, false for linked series
- softThreshold: true,
- states: { // states for the entire series
- hover: {
- //enabled: false,
- lineWidthPlus: 1,
- marker: {
- // lineWidth: base + 1,
- // radius: base + 1
- },
- halo: {
- size: 10,
- opacity: 0.25
+ style: {
+ fontSize: '11px',
+ fontWeight: 'bold',
+ color: 'contrast',
+ textOutline: '1px contrast'
+ },
+ // backgroundColor: undefined,
+ // borderColor: undefined,
+ // borderWidth: undefined,
+ // shadow: false
- }
+ verticalAlign: 'bottom', // above singular point
+ x: 0,
+ y: 0,
+ // borderRadius: undefined,
+ padding: 5
+ },
+ cropThreshold: 300, // draw points outside the plot area when the number of points is less than this
+ pointRange: 0,
+ //pointStart: 0,
+ //pointInterval: 1,
+ //showInLegend: null, // auto: true for standalone series, false for linked series
+ softThreshold: true,
+ states: { // states for the entire series
+ hover: {
+ //enabled: false,
+ lineWidthPlus: 1,
+ marker: {
+ // lineWidth: base + 1,
+ // radius: base + 1
},
- select: {
- marker: {}
+ halo: {
+ size: 10,
+
+ opacity: 0.25
+
}
},
- 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
+ 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
- // Prototype properties
- {
- isCartesian: true,
- pointClass: Point,
- sorted: true, // requires the data to be sorted
- requireSorting: true,
- directTouch: false,
- axisTypes: ['xAxis', 'yAxis'],
- colorCounter: 0,
- parallelArrays: ['x', 'y'], // each point's x and y values are stored in this.xData and this.yData
- coll: 'series',
- init: function(chart, options) {
- var series = this,
- eventType,
- events,
- chartSeries = chart.series,
- sortByIndex = function(a, b) {
- return pick(a.options.index, a._i) - pick(b.options.index, b._i);
- };
- series.chart = chart;
- series.options = options = series.setOptions(options); // merge with plotOptions
- series.linkedSeries = [];
+ }, /** @lends Series.prototype */ {
+ isCartesian: true,
+ pointClass: Point,
+ sorted: true, // requires the data to be sorted
+ requireSorting: true,
+ directTouch: false,
+ axisTypes: ['xAxis', 'yAxis'],
+ colorCounter: 0,
+ parallelArrays: ['x', 'y'], // each point's x and y values are stored in this.xData and this.yData
+ coll: 'series',
+ init: function(chart, options) {
+ var series = this,
+ eventType,
+ events,
+ chartSeries = chart.series,
+ lastSeries,
+ sortByIndex = function(a, b) {
+ return pick(a.options.index, a._i) - pick(b.options.index, b._i);
+ };
- // bind the axes
- series.bindAxes();
+ series.chart = chart;
+ series.options = options = series.setOptions(options); // merge with plotOptions
+ series.linkedSeries = [];
- // set some variables
- extend(series, {
- name: options.name,
- state: '',
- visible: options.visible !== false, // true by default
- selected: options.selected === true // false by default
- });
+ // bind the axes
+ series.bindAxes();
- // register event listeners
- events = options.events;
- for (eventType in events) {
- addEvent(series, eventType, events[eventType]);
- }
- if (
- (events && events.click) ||
- (options.point && options.point.events && options.point.events.click) ||
- options.allowPointSelect
- ) {
- chart.runTrackerClick = true;
- }
+ // set some variables
+ extend(series, {
+ name: options.name,
+ state: '',
+ visible: options.visible !== false, // true by default
+ selected: options.selected === true // false by default
+ });
- series.getColor();
- series.getSymbol();
+ // register event listeners
+ events = options.events;
+ for (eventType in events) {
+ addEvent(series, eventType, events[eventType]);
+ }
+ if (
+ (events && events.click) ||
+ (options.point && options.point.events && options.point.events.click) ||
+ options.allowPointSelect
+ ) {
+ chart.runTrackerClick = true;
+ }
- // Set the data
- each(series.parallelArrays, function(key) {
- series[key + 'Data'] = [];
- });
- series.setData(options.data, false);
+ series.getColor();
+ series.getSymbol();
- // Mark cartesian
- if (series.isCartesian) {
- chart.hasCartesianSeries = true;
- }
+ // Set the data
+ each(series.parallelArrays, function(key) {
+ series[key + 'Data'] = [];
+ });
+ series.setData(options.data, false);
- // Register it in the chart
- chartSeries.push(series);
- series._i = chartSeries.length - 1;
+ // Mark cartesian
+ if (series.isCartesian) {
+ chart.hasCartesianSeries = true;
+ }
- // Sort series according to index option (#248, #1123, #2456)
- stableSort(chartSeries, sortByIndex);
- if (this.yAxis) {
- stableSort(this.yAxis.series, sortByIndex);
- }
+ // 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;
+ chartSeries.push(series);
- each(chartSeries, function(series, i) {
- series.index = i;
- series.name = series.name || 'Series ' + (i + 1);
- });
+ // Sort series according to index option (#248, #1123, #2456)
+ stableSort(chartSeries, sortByIndex);
+ if (this.yAxis) {
+ stableSort(this.yAxis.series, sortByIndex);
+ }
- },
+ each(chartSeries, function(series, i) {
+ series.index = i;
+ series.name = series.name || 'Series ' + (i + 1);
+ });
- /**
- * Set the xAxis and yAxis properties of cartesian series, and register the series
- * in the axis.series array
- */
- bindAxes: function() {
- var series = this,
- seriesOptions = series.options,
- chart = series.chart,
- axisOptions;
+ },
- each(series.axisTypes || [], function(AXIS) { // repeat for xAxis and yAxis
+ /**
+ * 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;
- each(chart[AXIS], function(axis) { // loop through the chart's axis objects
- axisOptions = axis.options;
+ each(series.axisTypes || [], function(AXIS) { // repeat for xAxis and yAxis
- // 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)) {
+ each(chart[AXIS], function(axis) { // loop through the chart's axis objects
+ axisOptions = axis.options;
- // register this series in the axis.series lookup
- axis.series.push(series);
+ // 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)) {
- // set this series.xAxis or series.yAxis reference
- series[AXIS] = axis;
+ // register this series in the axis.series lookup
+ axis.series.push(series);
- // mark dirty for redraw
- axis.isDirty = true;
- }
- });
+ // set this series.xAxis or series.yAxis reference
+ series[AXIS] = axis;
- // The series needs an X and an Y axis
- if (!series[AXIS] && series.optionalAxis !== AXIS) {
- error(18, true);
+ // mark dirty for redraw
+ axis.isDirty = 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));
- };
+ // The series needs an X and an Y axis
+ if (!series[AXIS] && series.optionalAxis !== AXIS) {
+ error(18, true);
+ }
- 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() {
+ /**
+ * 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));
+ };
- var options = this.options,
- xIncrement = this.xIncrement,
- date,
- pointInterval,
- pointIntervalUnit = options.pointIntervalUnit;
+ each(series.parallelArrays, fn);
+ },
- xIncrement = pick(xIncrement, options.pointStart, 0);
+ /**
+ * 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() {
- this.pointInterval = pointInterval = pick(this.pointInterval, options.pointInterval, 1);
+ var options = this.options,
+ xIncrement = this.xIncrement,
+ date,
+ pointInterval,
+ pointIntervalUnit = options.pointIntervalUnit;
- // Added code for pointInterval strings
- if (pointIntervalUnit) {
- date = new Date(xIncrement);
+ xIncrement = pick(xIncrement, options.pointStart, 0);
- 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.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.xIncrement = xIncrement + pointInterval;
+ return xIncrement;
+ },
- this.userOptions = itemOptions;
+ /**
+ * 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;
- // 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
- );
+ this.userOptions = itemOptions;
- // The tooltip options are merged between global and series specific options
- this.tooltipOptions = merge(
- defaultOptions.tooltip,
- defaultOptions.plotOptions[this.type].tooltip,
- userOptions.tooltip,
- userPlotOptions.series && userPlotOptions.series.tooltip,
- userPlotOptions[this.type] && userPlotOptions[this.type].tooltip,
- itemOptions.tooltip
- );
+ // 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
+ );
- // Delete marker object if not allowed (#1125)
- if (typeOptions.marker === null) {
- delete options.marker;
- }
+ // The tooltip options are merged between global and series specific options
+ this.tooltipOptions = merge(
+ defaultOptions.tooltip,
+ defaultOptions.plotOptions[this.type].tooltip,
+ userOptions.tooltip,
+ userPlotOptions.series && userPlotOptions.series.tooltip,
+ userPlotOptions[this.type] && userPlotOptions[this.type].tooltip,
+ itemOptions.tooltip
+ );
- // 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',
+ // Delete marker object if not allowed (#1125)
+ if (typeOptions.marker === null) {
+ delete options.marker;
+ }
- color: options.negativeColor,
- fillColor: options.negativeFillColor
+ // 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({
+ color: options.negativeColor,
+ fillColor: options.negativeFillColor
- color: this.color,
- fillColor: this.fillColor
+ });
+ }
+ if (zones.length) { // Push one extra zone for the rest
+ if (defined(zones[zones.length - 1].value)) {
+ zones.push({
- });
- }
- }
- return options;
- },
+ color: this.color,
+ fillColor: this.fillColor
- getCyclic: function(prop, value, defaults) {
- var i,
- userOptions = this.userOptions,
- indexName = prop + 'Index',
- counterName = prop + 'Counter',
- len = defaults ? defaults.length : pick(this.chart.options.chart[prop + 'Count'], this.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 {
- userOptions['_' + indexName] = i = this.chart[counterName] % len;
- this.chart[counterName] += 1;
- }
- if (defaults) {
- value = defaults[i];
- }
+ });
}
- // Set the colorIndex
- if (i !== undefined) {
- this[indexName] = i;
- }
- this[prop] = value;
- },
+ }
+ return options;
+ },
- /**
- * Get the series' color
- */
+ getCyclic: function(prop, value, defaults) {
+ var i,
+ userOptions = this.userOptions,
+ indexName = prop + 'Index',
+ counterName = prop + 'Counter',
+ len = defaults ? defaults.length : pick(this.chart.options.chart[prop + 'Count'], this.chart[prop + 'Count']),
+ setting;
- getColor: function() {
- if (this.options.colorByPoint) {
- this.options.color = null; // #4359, selected slice got series.color even when colorByPoint was set.
+ 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 {
- this.getCyclic('color', this.options.color || defaultPlotOptions[this.type].color, this.chart.options.colors);
+ userOptions['_' + indexName] = i = this.chart[counterName] % len;
+ this.chart[counterName] += 1;
}
- },
+ if (defaults) {
+ value = defaults[i];
+ }
+ }
+ // Set the colorIndex
+ if (i !== undefined) {
+ this[indexName] = i;
+ }
+ this[prop] = value;
+ },
- /**
- * Get the series' symbol
- */
- getSymbol: function() {
- var seriesMarkerOption = this.options.marker;
+ /**
+ * Get the series' color
+ */
- this.getCyclic('symbol', seriesMarkerOption.symbol, this.chart.options.symbols);
+ getColor: function() {
+ if (this.options.colorByPoint) {
+ this.options.color = null; // #4359, selected slice got series.color even when colorByPoint was set.
+ } else {
+ this.getCyclic('color', this.options.color || defaultPlotOptions[this.type].color, this.chart.options.colors);
+ }
+ },
- // don't substract radius in image symbols (#604)
- if (/^url/.test(this.symbol)) {
- seriesMarkerOption.radius = 0;
- }
- },
+ /**
+ * Get the series' symbol
+ */
+ getSymbol: function() {
+ var seriesMarkerOption = this.options.marker;
- drawLegendSymbol: LegendSymbolMixin.drawLineMarker,
+ this.getCyclic('symbol', seriesMarkerOption.symbol, this.chart.options.symbols);
+ },
- /**
- * Replace the series data with a new set of data
- * @param {Object} data
- * @param {Object} redraw
- */
- 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;
+ drawLegendSymbol: LegendSymbolMixin.drawLineMarker,
- data = data || [];
- dataLength = data.length;
- redraw = pick(redraw, true);
+ /**
+ * Replace the series data with a new set of data
+ * @param {Object} data
+ * @param {Object} redraw
+ */
+ 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;
- // 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);
- }
- });
+ data = data || [];
+ dataLength = data.length;
+ redraw = pick(redraw, true);
- } else {
+ // 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);
+ }
+ });
- // Reset properties
- series.xIncrement = null;
+ } else {
- series.colorCounter = 0; // for series with colorByPoint (#1547)
+ // Reset properties
+ series.xIncrement = null;
- // Update parallel arrays
- each(this.parallelArrays, function(key) {
- series[key + 'Data'].length = 0;
- });
+ series.colorCounter = 0; // for series with colorByPoint (#1547)
- // 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) {
+ // Update parallel arrays
+ each(this.parallelArrays, function(key) {
+ series[key + 'Data'].length = 0;
+ });
- // find the first non-null point
- i = 0;
- while (firstPoint === null && i < dataLength) {
- firstPoint = data[i];
- i++;
- }
+ // 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
+
+ if (isNumber(firstPoint)) { // assume all points are numbers
+ for (i = 0; i < dataLength; i++) {
+ xData[i] = this.autoIncrement();
+ yData[i] = data[i];
+ }
+ } else if (isArray(firstPoint)) { // assume all points are arrays
+ if (valueCount) { // [x, low, high] or [x, o, h, l, c]
for (i = 0; i < dataLength; i++) {
- xData[i] = this.autoIncrement();
- yData[i] = data[i];
+ pt = data[i];
+ xData[i] = pt[0];
+ yData[i] = pt.slice(1, valueCount + 1);
}
- } else if (isArray(firstPoint)) { // assume all points are arrays
- 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 { // [x, y]
+ for (i = 0; i < dataLength; i++) {
+ pt = data[i];
+ xData[i] = pt[0];
+ yData[i] = pt[1];
}
- } else {
- error(12); // Highcharts expects configs to be numbers or arrays in turbo mode
}
} 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);
- }
+ error(12); // Highcharts expects configs to be numbers or arrays in turbo mode
+ }
+ } 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])) {
- error(14, true);
- }
+ // Forgetting to cast strings to numbers is a common caveat when handling CSV or JSON
+ if (isString(yData[0])) {
+ error(14, true);
+ }
- series.data = [];
- series.options.data = series.userOptions.data = data;
+ 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();
- }
+ // 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();
+ // reset minRange (#878)
+ if (xAxis) {
+ xAxis.minRange = xAxis.userMinRange;
}
- if (redraw) {
- chart.redraw(animation);
- }
- },
+ // redraw
+ series.isDirty = chart.isDirtyBox = true;
+ series.isDirtyData = !!oldData;
+ animation = false;
+ }
- /**
- * Process the data by cropping away unused data points if the series is longer
- * than the crop threshold. This saves computing time for lage series.
- */
- processData: function(force) {
- var series = this,
- processedXData = series.xData, // copied during slice operation below
- 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;
+ // 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 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 (redraw) {
+ chart.redraw(animation);
+ }
+ },
- if (xAxis) {
- xExtremes = xAxis.getExtremes(); // corrected for log axis (#3053)
- min = xExtremes.min;
- max = xExtremes.max;
- }
+ /**
+ * Process the data by cropping away unused data points if the series is longer
+ * than the crop threshold. This saves computing time for lage series.
+ */
+ processData: function(force) {
+ var series = this,
+ processedXData = series.xData, // copied during slice operation below
+ 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;
- // optionally filter out points outside the plot area
- if (isCartesian && series.sorted && !getExtremesFromAll && (!cropThreshold || dataLength > cropThreshold || series.forceCrop)) {
+ // 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;
+ }
- // it's outside current extremes
- if (processedXData[dataLength - 1] < min || processedXData[0] > max) {
- processedXData = [];
- processedYData = [];
+ if (xAxis) {
+ xExtremes = xAxis.getExtremes(); // corrected for log axis (#3053)
+ min = xExtremes.min;
+ max = xExtremes.max;
+ }
- // 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;
- }
+ // 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];
+ // 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;
+ 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) {
- error(15);
- }
+ // 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) {
+ error(15);
}
+ }
- // Record the properties
- series.cropped = cropped; // undefined or true
- series.cropStart = cropStart;
- series.processedXData = processedXData;
- series.processedYData = processedYData;
+ // Record the properties
+ series.cropped = cropped; // undefined or true
+ series.cropStart = cropStart;
+ series.processedXData = processedXData;
+ series.processedYData = processedYData;
- series.closestPointRange = closestPointRange;
+ 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,
- cropShoulder = pick(this.cropShoulder, 1), // line-type series need one point outside
- i,
- j;
+ /**
+ * 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,
+ cropShoulder = pick(this.cropShoulder, 1), // line-type series need one point outside
+ 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;
- }
+ // 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;
- }
+ // 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
- };
- },
+ 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,
- point,
- points = [],
- i;
+ /**
+ * 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,
+ point,
+ points = [],
+ i;
- if (!data && !hasGroupedData) {
- var arr = [];
- arr.length = dataOptions.length;
- data = series.data = arr;
- }
+ if (!data && !hasGroupedData) {
+ var arr = [];
+ arr.length = dataOptions.length;
+ data = series.data = arr;
+ }
- for (i = 0; i < processedDataLength; i++) {
- cursor = cropStart + i;
- if (!hasGroupedData) {
- if (data[cursor]) {
- point = data[cursor];
- } else if (dataOptions[cursor] !== undefined) { // #970
- data[cursor] = point = (new PointClass()).init(series, dataOptions[cursor], processedXData[i]);
- }
- points[i] = point;
- } else {
- // splat the y data in case of ohlc data array
- points[i] = (new PointClass()).init(series, [processedXData[i]].concat(splat(processedYData[i])));
- points[i].dataGroup = series.groupMap[i];
+ 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]);
}
- points[i].index = cursor; // For faster access in Point.update
+ } else {
+ // splat the y data in case of ohlc data array
+ point = (new PointClass()).init(series, [processedXData[i]].concat(splat(processedYData[i])));
+ point.dataGroup = series.groupMap[i];
}
+ point.index = cursor; // For faster access in Point.update
+ points[i] = point;
+ }
- // 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++) {
- if (i === cropStart && !hasGroupedData) { // when has grouped data, clear all points
- i += processedDataLength;
- }
- if (data[i]) {
- data[i].destroyElements();
- data[i].plotX = undefined; // #1003
- }
+ // 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++) {
+ if (i === cropStart && !hasGroupedData) { // when has grouped data, clear all points
+ i += processedDataLength;
}
+ if (data[i]) {
+ data[i].destroyElements();
+ data[i].plotX = undefined; // #1003
+ }
}
+ }
- series.data = data;
- series.points = points;
- },
+ 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,
- xExtremes = xAxis.getExtremes(), // #2117, need to compensate for log X axis
- xMin = xExtremes.min,
- xMax = xExtremes.max,
- validValue,
- withinRange,
- x,
- y,
- i,
- j;
+ /**
+ * Calculate Y extremes for visible data
+ */
+ getExtremes: function(yData) {
+ var xAxis = this.xAxis,
+ yAxis = this.yAxis,
+ xData = this.processedXData,
+ yDataLength,
+ activeYData = [],
+ activeCounter = 0,
+ xExtremes = xAxis.getExtremes(), // #2117, need to compensate for log X axis
+ xMin = xExtremes.min,
+ xMax = xExtremes.max,
+ validValue,
+ withinRange,
+ x,
+ y,
+ i,
+ j;
- yData = yData || this.stackedYData || this.processedYData || [];
- yDataLength = yData.length;
+ yData = yData || this.stackedYData || this.processedYData || [];
+ yDataLength = yData.length;
- for (i = 0; i < yDataLength; i++) {
+ for (i = 0; i < yDataLength; i++) {
- x = xData[i];
- y = yData[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.isLog || (y.length || y > 0));
- withinRange = this.getExtremesFromAll || this.options.getExtremesFromAll || this.cropped ||
- ((xData[i + 1] || x) >= xMin && (xData[i - 1] || x) <= xMax);
+ // For points within the visible range, including the first point outside the
+ // visible range, consider y extremes
+ validValue = (isNumber(y, true) || isArray(y)) && (!yAxis.isLog || (y.length || y > 0));
+ withinRange = this.getExtremesFromAll || this.options.getExtremesFromAll || this.cropped ||
+ ((xData[i + 1] || x) >= xMin && (xData[i - 1] || x) <= xMax);
- if (validValue && withinRange) {
+ if (validValue && withinRange) {
- j = y.length;
- if (j) { // array, like ohlc or range data
- while (j--) {
- if (y[j] !== null) {
- activeYData[activeCounter++] = y[j];
- }
+ 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;
}
+ } else {
+ activeYData[activeCounter++] = y;
}
}
- this.dataMin = arrayMin(activeYData);
- this.dataMax = arrayMax(activeYData);
- },
+ }
+ 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.
- */
- 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;
+ /**
+ * 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;
- // 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;
+ // Point placement is relative to each series pointRange (#5889)
+ if (pointPlacement === 'between') {
+ pointPlacement = 0.5;
+ }
+ if (isNumber(pointPlacement)) {
+ pointPlacement *= pick(options.pointRange || xAxis.pointRange);
+ }
- // Discard disallowed y values for log axes (#3434)
- if (yAxis.isLog && yValue !== null && yValue <= 0) {
- point.isNull = true;
- }
+ // 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;
- // 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
- );
+ // Discard disallowed y values for log axes (#3434)
+ if (yAxis.isLog && yValue !== null && yValue <= 0) {
+ point.isNull = true;
+ }
- // 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];
+ // 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
+ );
- if (yBottom === stackThreshold && stackIndicator.key === stack[xValue].base) {
- yBottom = pick(threshold, yAxis.min);
- }
- if (yAxis.isLog && yBottom <= 0) { // #1200, #1232
- yBottom = null;
- }
+ // 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];
- point.total = point.stackTotal = pointStack.total;
- point.percentage = pointStack.total && (point.y / pointStack.total * 100);
- point.stackY = yValue;
+ if (yBottom === stackThreshold && stackIndicator.key === stack[xValue].base) {
+ yBottom = pick(threshold, yAxis.min);
+ }
+ if (yAxis.isLog && yBottom <= 0) { // #1200, #1232
+ yBottom = null;
+ }
- // Place the stack label
- pointStack.setOffset(series.pointXOffset || 0, series.barW || 0);
+ 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 translated yBottom or remove it
+ point.yBottom = defined(yBottom) ?
+ yAxis.translate(yBottom, 0, 1, 0, 1) :
+ null;
- // 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;
+ // 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;
- // Set client related positions for mouse tracking
- point.clientX = dynamicallyPlaced ? correctFloat(xAxis.translate(xValue, 0, 0, 0, 1, pointPlacement)) : plotX; // #1514, #5383, #5518
+ point.isInside = plotY !== undefined && plotY >= 0 && plotY <= yAxis.len && // #3519
+ plotX >= 0 && plotX <= xAxis.len;
- point.negative = point.y < (threshold || 0);
- // some API data
- point.category = categories && categories[point.x] !== undefined ?
- categories[point.x] : point.x;
+ // Set client related positions for mouse tracking
+ point.clientX = dynamicallyPlaced ? correctFloat(xAxis.translate(xValue, 0, 0, 0, 1, pointPlacement)) : plotX; // #1514, #5383, #5518
- // Determine auto enabling of markers (#3635, #5099)
- if (!point.isNull) {
- if (lastPlotX !== undefined) {
- closestPointRangePx = Math.min(closestPointRangePx, Math.abs(plotX - lastPlotX));
- }
- lastPlotX = plotX;
- }
+ point.negative = point.y < (threshold || 0);
- }
- series.closestPointRangePx = closestPointRangePx;
- },
+ // some API data
+ point.category = categories && categories[point.x] !== undefined ?
+ categories[point.x] : point.x;
- /**
- * Return the series points with null points filtered out
- */
- getValidPoints: function(points, insideOnly) {
- var chart = this.chart;
- return grep(points || this.points || [], function isValidPoint(point) { // #3916, #5029
- if (insideOnly && !chart.isInsidePlot(point.plotX, point.plotY, chart.inverted)) { // #5085
- return false;
+ // Determine auto enabling of markers (#3635, #5099)
+ if (!point.isNull) {
+ if (lastPlotX !== undefined) {
+ closestPointRangePx = Math.min(closestPointRangePx, Math.abs(plotX - lastPlotX));
}
- return !point.isNull;
- });
- },
+ lastPlotX = plotX;
+ }
- /**
- * 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'];
+ }
+ series.closestPointRangePx = closestPointRangePx;
+ },
- // If a clipping rectangle with the same properties is currently present in the chart, use that.
- if (!clipRect) {
+ /**
+ * Return the series points with null points filtered out
+ */
+ getValidPoints: function(points, insideOnly) {
+ var chart = this.chart;
+ return grep(points || this.points || [], function isValidPoint(point) { // #3916, #5029
+ if (insideOnly && !chart.isInsidePlot(point.plotX, point.plotY, chart.inverted)) { // #5085
+ return false;
+ }
+ return !point.isNull;
+ });
+ },
- // When animation is set, prepare the initial positions
- if (animation) {
- clipBox.width = 0;
+ /**
+ * 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'];
- 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 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) {
- if (!clipRect.count[this.index]) {
- clipRect.count[this.index] = true;
- clipRect.count.length += 1;
- }
+ 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 (options.clip !== false) {
- this.group.clip(animation || seriesClipBox ? clipRect : chart.clipRect);
- this.markerGroup.clip(markerClipRect);
- this.sharedClipKey = sharedClipKey;
+ }
+ if (animation) {
+ if (!clipRect.count[this.index]) {
+ clipRect.count[this.index] = true;
+ clipRect.count.length += 1;
}
+ }
- // 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 (options.clip !== false) {
+ this.group.clip(animation || seriesClipBox ? clipRect : chart.clipRect);
+ this.markerGroup.clip(markerClipRect);
+ this.sharedClipKey = sharedClipKey;
+ }
- 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();
- }
- }
+ // 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;
}
- },
- /**
- * 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 (clipRect.count.length === 0 && sharedClipKey && chart[sharedClipKey]) {
+ if (!seriesClipBox) {
+ chart[sharedClipKey] = chart[sharedClipKey].destroy();
}
if (chart[sharedClipKey + 'm']) {
- chart[sharedClipKey + 'm'].animate({
- width: chart.plotSizeX + 99
- }, animation);
+ chart[sharedClipKey + 'm'] = chart[sharedClipKey + 'm'].destroy();
}
+ }
+ }
+ },
- // Delete this function to allow it only once
- series.animate = null;
+ /**
+ * 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);
+ }
- /**
- * This runs after animation to land on the final plot clipping
- */
- afterAnimate: function() {
- this.setClip();
- fireEvent(this, 'afterAnimate');
- },
+ // Delete this function to allow it only once
+ series.animate = null;
- /**
- * Draw the markers
- */
- drawPoints: function() {
- var series = this,
- points = series.points,
- chart = series.chart,
- plotX,
- plotY,
- i,
- point,
- radius,
- symbol,
- isImage,
- graphic,
- options = series.options,
- seriesMarkerOptions = options.marker,
- pointMarkerOptions,
- hasPointMarker,
- enabled,
- isInside,
- markerGroup = series.markerGroup,
- xAxis = series.xAxis,
- globallyEnabled = pick(
- seriesMarkerOptions.enabled,
- xAxis.isRadial ? true : null,
- series.closestPointRangePx > 2 * seriesMarkerOptions.radius
- );
+ }
+ },
- if (seriesMarkerOptions.enabled !== false || series._hasPointMarkers) {
+ /**
+ * This runs after animation to land on the final plot clipping
+ */
+ afterAnimate: function() {
+ this.setClip();
+ fireEvent(this, 'afterAnimate');
+ },
- i = points.length;
- while (i--) {
- point = points[i];
- plotX = Math.floor(point.plotX); // #1843
- plotY = point.plotY;
- graphic = point.graphic;
- pointMarkerOptions = point.marker || {};
- hasPointMarker = !!point.marker;
- enabled = (globallyEnabled && pointMarkerOptions.enabled === undefined) || pointMarkerOptions.enabled;
- isInside = point.isInside;
+ /**
+ * 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.markerGroup,
+ xAxis = series.xAxis,
+ markerAttribs,
+ globallyEnabled = pick(
+ seriesMarkerOptions.enabled,
+ xAxis.isRadial ? true : null,
+ series.closestPointRangePx > 2 * seriesMarkerOptions.radius
+ );
- // only draw the point if y is defined
- if (enabled && isNumber(plotY) && point.y !== null) {
+ if (seriesMarkerOptions.enabled !== false || series._hasPointMarkers) {
- // Shortcuts
- radius = seriesMarkerOptions.radius;
- symbol = pick(pointMarkerOptions.symbol, series.symbol);
- isImage = symbol.indexOf('url') === 0;
+ i = points.length;
+ while (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;
- if (graphic) { // update
- graphic[isInside ? 'show' : 'hide'](true) // Since the marker group isn't clipped, each individual marker must be toggled
- //.attr(pointAttr) // #4759
- .animate(extend({
- x: plotX - radius,
- y: plotY - radius
- }, graphic.symbolName ? { // don't apply to image symbols #507
- width: 2 * radius,
- height: 2 * radius
- } : {}));
- } else if (isInside && (radius > 0 || isImage)) {
- point.graphic = graphic = chart.renderer.symbol(
- symbol,
- plotX - radius,
- plotY - radius,
- 2 * radius,
- 2 * radius,
- hasPointMarker ? pointMarkerOptions : seriesMarkerOptions
- )
- .attr({
- r: radius
- })
- .add(markerGroup);
- }
+ // 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;
- // Presentational attributes
- if (graphic) {
- graphic.attr(series.pointAttribs(point, point.selected && 'select'));
- }
+ 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
+ // Presentational attributes
+ if (graphic) {
+ graphic.attr(series.pointAttribs(point, point.selected && 'select'));
}
+
+
+ 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,
+ pointOptions = point && point.options,
+ pointMarkerOptions = (pointOptions && pointOptions.marker) || {},
+ pointStateOptions,
+ radius = pick(
+ pointMarkerOptions.radius,
+ seriesMarkerOptions.radius
+ ),
+ attribs;
- /**
- * Get presentational attributes for marker-based series (line, spline, scatter, bubble, mappoint...)
- */
- pointAttribs: function(point, state) {
- var seriesMarkerOptions = this.options.marker,
- seriesStateOptions,
- pointOptions = point && point.options,
- pointMarkerOptions = (pointOptions && pointOptions.marker) || {},
- pointStateOptions,
- strokeWidth = seriesMarkerOptions.lineWidth,
- color = this.color,
- pointColorOption = pointOptions && pointOptions.color,
- pointColor = point && point.color,
- zoneColor,
- fill,
- stroke,
- zone;
+ // Handle hover and select states
+ if (state) {
+ seriesStateOptions = seriesMarkerOptions.states[state];
+ pointStateOptions = pointMarkerOptions.states &&
+ pointMarkerOptions.states[state];
- if (point && this.zones.length) {
- zone = point.getZone();
- if (zone && zone.color) {
- zoneColor = zone.color;
- }
- }
+ radius = pick(
+ pointStateOptions && pointStateOptions.radius,
+ seriesStateOptions && seriesStateOptions.radius,
+ radius + (seriesStateOptions && seriesStateOptions.radiusPlus || 0)
+ );
+ }
- color = pointColorOption || zoneColor || pointColor || color;
- fill = pointMarkerOptions.fillColor || seriesMarkerOptions.fillColor || color;
- stroke = pointMarkerOptions.lineColor || seriesMarkerOptions.lineColor || color;
+ if (point.hasImage) {
+ radius = 0; // and subsequently width and height is not set
+ }
- // Handle hover and select states
- if (state) {
- seriesStateOptions = seriesMarkerOptions.states[state];
- pointStateOptions = (pointMarkerOptions.states && pointMarkerOptions.states[state]) || {};
- strokeWidth = seriesStateOptions.lineWidth || strokeWidth + seriesStateOptions.lineWidthPlus;
- fill = pointStateOptions.fillColor || seriesStateOptions.fillColor || fill;
- stroke = pointStateOptions.lineColor || seriesStateOptions.lineColor || stroke;
- }
+ attribs = {
+ x: Math.floor(point.plotX) - radius, // Math.floor for #1843
+ y: point.plotY - radius
+ };
- return {
- 'stroke': stroke,
- 'stroke-width': strokeWidth,
- 'fill': fill
- };
- },
+ if (radius) {
+ attribs.width = attribs.height = 2 * radius;
+ }
- /**
- * 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,
- prop,
- axis;
+ return attribs;
- // 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;
- }
- });
+ /**
+ * Get presentational attributes for marker-based series (line, spline, scatter, bubble, mappoint...)
+ */
+ pointAttribs: function(point, state) {
+ var seriesMarkerOptions = this.options.marker,
+ seriesStateOptions,
+ pointOptions = point && point.options,
+ pointMarkerOptions = (pointOptions && pointOptions.marker) || {},
+ pointStateOptions,
+ color = this.color,
+ pointColorOption = pointOptions && pointOptions.color,
+ pointColor = point && point.color,
+ strokeWidth = pick(
+ pointMarkerOptions.lineWidth,
+ seriesMarkerOptions.lineWidth
+ ),
+ zoneColor,
+ fill,
+ stroke,
+ zone;
- // remove legend items
- if (series.legendItem) {
- series.chart.legend.destroyItem(series);
+ if (point && this.zones.length) {
+ zone = point.getZone();
+ if (zone && zone.color) {
+ zoneColor = zone.color;
}
+ }
- // destroy all points with their elements
- i = data.length;
- while (i--) {
- point = data[i];
- if (point && point.destroy) {
- point.destroy();
- }
- }
- series.points = null;
+ color = pointColorOption || zoneColor || pointColor || color;
+ fill = pointMarkerOptions.fillColor || seriesMarkerOptions.fillColor || color;
+ stroke = pointMarkerOptions.lineColor || seriesMarkerOptions.lineColor || color;
- // Clear the animation timeout if we are destroying the series during initial animation
- clearTimeout(series.animationTimeout);
+ // Handle hover and select states
+ if (state) {
+ seriesStateOptions = seriesMarkerOptions.states[state];
+ pointStateOptions = (pointMarkerOptions.states && pointMarkerOptions.states[state]) || {};
+ strokeWidth = pick(
+ pointStateOptions.lineWidth,
+ seriesStateOptions.lineWidth,
+ strokeWidth + pick(
+ pointStateOptions.lineWidthPlus,
+ seriesStateOptions.lineWidthPlus,
+ 0
+ )
+ );
+ fill = pointStateOptions.fillColor || seriesStateOptions.fillColor || fill;
+ stroke = pointStateOptions.lineColor || seriesStateOptions.lineColor || stroke;
+ }
- // Destroy all SVGElements associated to the series
- for (prop in series) {
- if (series[prop] instanceof SVGElement && !series[prop].survive) { // Survive provides a hook for not destroying
+ return {
+ 'stroke': stroke,
+ 'stroke-width': strokeWidth,
+ 'fill': fill
+ };
+ },
- // issue 134 workaround
- destroy = issue134 && prop === 'group' ?
- 'hide' :
- 'destroy';
+ /**
+ * 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,
+ prop,
+ axis;
- series[prop][destroy]();
- }
- }
+ // add event hook
+ fireEvent(series, 'destroy');
- // remove from hoverSeries
- if (chart.hoverSeries === series) {
- chart.hoverSeries = null;
+ // 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;
}
- erase(chart.series, series);
+ });
- // clear all members
- for (prop in series) {
- delete series[prop];
+ // 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;
- /**
- * Get the graph path
- */
- getGraphPath: function(points, nullsAsZeroes, connectCliffs) {
- var series = this,
- options = series.options,
- step = options.step,
- reversed,
- graphPath = [],
- xMap = [],
- gap;
+ // Clear the animation timeout if we are destroying the series during initial animation
+ clearTimeout(series.animationTimeout);
- points = points || series.points;
+ // Destroy all SVGElements associated to the series
+ for (prop in series) {
+ if (series[prop] instanceof SVGElement && !series[prop].survive) { // Survive provides a hook for not destroying
- // 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;
- }
+ // issue 134 workaround
+ destroy = issue134 && prop === 'group' ?
+ 'hide' :
+ 'destroy';
- // Remove invalid points, especially in spline (#5015)
- if (options.connectNulls && !nullsAsZeroes && !connectCliffs) {
- points = this.getValidPoints(points);
+ series[prop][destroy]();
}
+ }
- // Build the line
- each(points, function(point, i) {
+ // remove from hoverSeries
+ if (chart.hoverSeries === series) {
+ chart.hoverSeries = null;
+ }
+ erase(chart.series, series);
- var plotX = point.plotX,
- plotY = point.plotY,
- lastPoint = points[i - 1],
- pathToPoint; // the path to this point from the previous
+ // clear all members
+ for (prop in series) {
+ delete series[prop];
+ }
+ },
- if ((point.leftCliff || (lastPoint && lastPoint.rightCliff)) && !connectCliffs) {
- gap = true; // ... and continue
- }
+ /**
+ * Get the graph path
+ */
+ getGraphPath: function(points, nullsAsZeroes, connectCliffs) {
+ var series = this,
+ options = series.options,
+ step = options.step,
+ reversed,
+ graphPath = [],
+ xMap = [],
+ gap;
- // Line series, nullsAsZeroes is not handled
- if (point.isNull && !defined(nullsAsZeroes) && i > 0) {
- gap = !options.connectNulls;
+ points = points || series.points;
- // Area series, nullsAsZeroes is set
- } else if (point.isNull && !nullsAsZeroes) {
- gap = true;
+ // 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;
+ }
- } else {
+ // Remove invalid points, especially in spline (#5015)
+ if (options.connectNulls && !nullsAsZeroes && !connectCliffs) {
+ points = this.getValidPoints(points);
+ }
- if (i === 0 || gap) {
- pathToPoint = ['M', point.plotX, point.plotY];
+ // Build the line
+ each(points, function(point, i) {
- } else if (series.getPointSpline) { // generate the spline as defined in the SplineSeries object
+ var plotX = point.plotX,
+ plotY = point.plotY,
+ lastPoint = points[i - 1],
+ pathToPoint; // the path to this point from the previous
- pathToPoint = series.getPointSpline(points, point, i);
+ if ((point.leftCliff || (lastPoint && lastPoint.rightCliff)) && !connectCliffs) {
+ gap = true; // ... and continue
+ }
- } else if (step) {
+ // Line series, nullsAsZeroes is not handled
+ if (point.isNull && !defined(nullsAsZeroes) && i > 0) {
+ gap = !options.connectNulls;
- if (step === 1) { // right
- pathToPoint = [
- 'L',
- lastPoint.plotX,
- plotY
- ];
+ // Area series, nullsAsZeroes is set
+ } else if (point.isNull && !nullsAsZeroes) {
+ gap = true;
- } else if (step === 2) { // center
- pathToPoint = [
- 'L',
- (lastPoint.plotX + plotX) / 2,
- lastPoint.plotY,
- 'L',
- (lastPoint.plotX + plotX) / 2,
- plotY
- ];
+ } else {
- } else {
- pathToPoint = [
- 'L',
- plotX,
- lastPoint.plotY
- ];
- }
- pathToPoint.push('L', plotX, plotY);
+ 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 {
- // normal line to next point
pathToPoint = [
'L',
plotX,
- plotY
+ lastPoint.plotY
];
}
+ pathToPoint.push('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);
- }
+ } else {
+ // normal line to next point
+ pathToPoint = [
+ 'L',
+ plotX,
+ plotY
+ ];
+ }
- graphPath.push.apply(graphPath, pathToPoint);
- gap = false;
+ // 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.xMap = xMap;
- series.graphPath = graphPath;
+ graphPath.push.apply(graphPath, pathToPoint);
+ gap = false;
+ }
+ });
- return graphPath;
+ 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',
+ },
- options.lineColor || this.color,
- options.dashStyle
+ /**
+ * Draw the actual graph
+ */
+ drawGraph: function() {
+ var series = this,
+ options = this.options,
+ graphPath = (this.gappedPath || this.getGraphPath).call(this),
+ props = [
+ [
+ 'graph',
+ 'highcharts-graph',
- ]
- ];
+ options.lineColor || this.color,
+ options.dashStyle
- // 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 || ''),
+ ]
+ ];
- zone.color || series.color,
- zone.dashStyle || options.dashStyle
+ // 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 || ''),
- ]);
- });
+ zone.color || series.color,
+ zone.dashStyle || options.dashStyle
- // 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
- });
+ // Draw the graph
+ each(props, function(prop, i) {
+ var graphKey = prop[0],
+ graph = series[graphKey],
+ attribs;
- } else if (graphPath.length) { // #1487
+ if (graph) {
+ graph.endX = graphPath.xMap;
+ graph.animate({
+ d: graphPath
+ });
- series[graphKey] = series.chart.renderer.path(graphPath)
- .addClass(prop[1])
- .attr({
- zIndex: 1
- }) // #1069
- .add(series.group);
+ } else if (graphPath.length) { // #1487
+ series[graphKey] = series.chart.renderer.path(graphPath)
+ .addClass(prop[1])
+ .attr({
+ zIndex: 1
+ }) // #1069
+ .add(series.group);
- attribs = {
- 'stroke': prop[2],
- 'stroke-width': options.lineWidth,
- 'fill': (series.fillGraph && series.color) || 'none' // Polygon series use filled graph
- };
- if (prop[3]) {
- attribs.dashstyle = prop[3];
- } else if (options.linecap !== 'square') {
- attribs['stroke-linecap'] = attribs['stroke-linejoin'] = 'round';
- }
+ attribs = {
+ 'stroke': prop[2],
+ 'stroke-width': options.lineWidth,
+ 'fill': (series.fillGraph && series.color) || 'none' // Polygon series use filled graph
+ };
- graph = series[graphKey]
- .attr(attribs)
- .shadow((i < 2) && options.shadow); // add shadow to normal series (0) or to first zone (1) #3932
-
+ if (prop[3]) {
+ attribs.dashstyle = prop[3];
+ } else if (options.linecap !== 'square') {
+ attribs['stroke-linecap'] = attribs['stroke-linejoin'] = 'round';
}
- // Helpers for animation
- if (graph) {
- graph.startX = graphPath.xMap;
- //graph.shiftUnit = options.step ? 2 : 1;
- graph.isArea = graphPath.isArea; // For arearange animation
- }
- });
- },
+ graph = series[graphKey]
+ .attr(attribs)
+ .shadow((i < 2) && options.shadow); // add shadow to normal series (0) or to first zone (1) #3932
- /**
- * 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();
- }
+ // Helpers for animation
+ if (graph) {
+ graph.startX = graphPath.xMap;
+ //graph.shiftUnit = options.step ? 2 : 1;
+ graph.isArea = graphPath.isArea; // For arearange animation
+ }
+ });
+ },
- // Create the clips
- extremes = axis.getExtremes();
- each(zones, function(threshold, i) {
+ /**
+ * 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;
- 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 (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();
+ }
- if (ignoreZones) {
- translatedFrom = translatedTo = axis.toPixels(extremes.max);
+ // 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;
+ }
+ }
- pxRange = Math.abs(translatedFrom - translatedTo);
- pxPosMin = Math.min(translatedFrom, translatedTo);
- pxPosMax = Math.max(translatedFrom, translatedTo);
+
+ /// VML SUPPPORT
+ if (inverted && renderer.isVML) {
if (axis.isXAxis) {
clipAttr = {
- x: inverted ? pxPosMax : pxPosMin,
- y: 0,
- width: pxRange,
- height: chartSizeMax
+ x: 0,
+ y: reversed ? pxPosMin : pxPosMax,
+ height: clipAttr.width,
+ width: chart.chartWidth
};
- if (!horiz) {
- clipAttr.x = chart.plotHeight - clipAttr.x;
- }
} else {
clipAttr = {
- x: 0,
- y: inverted ? pxPosMax : pxPosMin,
- width: chartSizeMax,
- height: pxRange
+ x: clipAttr.y - chart.plotLeft - chart.spacingBox.x,
+ y: 0,
+ width: clipAttr.height,
+ height: chart.chartHeight
};
- if (horiz) {
- clipAttr.y = chart.plotWidth - clipAttr.y;
- }
}
+ }
+ /// END OF VML SUPPORT
- /// VML SUPPPORT
- if (inverted && renderer.isVML) {
- if (axis.isXAxis) {
- clipAttr = {
- x: 0,
- y: reversed ? pxPosMin : pxPosMax,
- height: clipAttr.width,
- width: chart.chartWidth
- };
- } else {
- clipAttr = {
- x: clipAttr.y - chart.plotLeft - chart.spacingBox.x,
- y: 0,
- width: clipAttr.height,
- height: chart.chartHeight
- };
- }
+ if (clips[i]) {
+ clips[i].animate(clipAttr);
+ } else {
+ clips[i] = renderer.clipRect(clipAttr);
+
+ if (graph) {
+ series['zone-graph-' + i].clip(clips[i]);
}
- /// END OF VML SUPPORT
-
- 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 (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;
- }
- },
+ }
+ // 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;
+ /**
+ * Initialize and perform group inversion on series.group and series.markerGroup
+ */
+ invertGroups: function(inverted) {
+ var series = this,
+ chart = series.chart,
+ remover;
- // Pie, go away (#1736)
- if (!series.xAxis) {
- return;
- }
+ function setInvert() {
+ var size = {
+ width: series.yAxis.len,
+ height: series.xAxis.len
+ };
- // A fixed size is needed for inversion to work
- function setInvert() {
- var size = {
- width: series.yAxis.len,
- height: series.xAxis.len
- };
-
- each(['group', 'markerGroup'], function(groupName) {
- if (series[groupName]) {
- series[groupName].attr(size).invert(inverted);
- }
- });
- }
-
- addEvent(chart, 'resize', setInvert); // do it on resize
- addEvent(series, 'destroy', function() {
- removeEvent(chart, 'resize', setInvert);
+ each(['group', 'markerGroup'], function(groupName) {
+ if (series[groupName]) {
+ series[groupName].attr(size).invert(inverted);
+ }
});
+ }
- // Do it now
- setInvert(inverted); // do it now
+ // Pie, go away (#1736)
+ if (!series.xAxis) {
+ return;
+ }
- // On subsequent render and redraw, just do setInvert without setting up events again
- series.invertGroups = setInvert;
- },
+ // A fixed size is needed for inversion to work
+ remover = addEvent(chart, 'resize', setInvert);
+ addEvent(series, 'destroy', remover);
- /**
- * 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;
+ // Do it now
+ setInvert(inverted); // do it now
- // Generate it on first call
- if (isNew) {
- this[prop] = group = this.chart.renderer.g(name)
- .attr({
- zIndex: zIndex || 0.1 // IE8 and pointer logic use this
- })
- .add(parent);
+ // On subsequent render and redraw, just do setInvert without setting up events again
+ series.invertGroups = setInvert;
+ },
- group.addClass('highcharts-series-' + this.index + ' highcharts-' + this.type + '-series highcharts-color-' + this.colorIndex +
- ' ' + (this.options.className || ''));
- }
+ /**
+ * 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;
- // Place it on first and subsequent (redraw) calls
- group.attr({
- visibility: visibility
- })[isNew ? 'attr' : 'animate'](this.getPlotBox());
- return group;
- },
+ // Generate it on first call
+ if (isNew) {
+ this[prop] = group = this.chart.renderer.g(name)
+ .attr({
+ zIndex: zIndex || 0.1 // IE8 and pointer logic use this
+ })
+ .add(parent);
- /**
- * Get the translation and scale for the plot area of this series
- */
- getPlotBox: function() {
- var chart = this.chart,
- xAxis = this.xAxis,
- yAxis = this.yAxis;
+ group.addClass('highcharts-series-' + this.index + ' highcharts-' + this.type + '-series highcharts-color-' + this.colorIndex +
+ ' ' + (this.options.className || ''));
+ }
- // 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
- };
- },
+ // Place it on first and subsequent (redraw) calls
+ group.attr({
+ visibility: visibility
+ })[isNew ? 'attr' : 'animate'](this.getPlotBox());
+ return group;
+ },
- /**
- * 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;
+ /**
+ * Get the translation and scale for the plot area of this series
+ */
+ getPlotBox: function() {
+ var chart = this.chart,
+ xAxis = this.xAxis,
+ yAxis = this.yAxis;
- // the group
- group = series.plotGroup(
- 'group',
- 'series',
- visibility,
- zIndex,
- chartSeriesGroup
- );
+ // 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
+ };
+ },
- series.markerGroup = series.plotGroup(
- 'markerGroup',
- 'markers',
- visibility,
- zIndex,
- chartSeriesGroup
- );
+ /**
+ * 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;
- // initiate the animation
- if (animDuration) {
- series.animate(true);
- }
+ // the group
+ group = series.plotGroup(
+ 'group',
+ 'series',
+ visibility,
+ zIndex,
+ chartSeriesGroup
+ );
- // SVGRenderer needs to know this before drawing elements (#1089, #1795)
- group.inverted = series.isCartesian ? inverted : false;
+ series.markerGroup = series.plotGroup(
+ 'markerGroup',
+ 'markers',
+ visibility,
+ zIndex,
+ chartSeriesGroup
+ );
- // draw the graph if any
- if (series.drawGraph) {
- series.drawGraph();
- series.applyZones();
- }
+ // initiate the animation
+ if (animDuration) {
+ series.animate(true);
+ }
- /* each(series.points, function (point) {
- if (point.redraw) {
- point.redraw();
- }
- });*/
+ // SVGRenderer needs to know this before drawing elements (#1089, #1795)
+ group.inverted = series.isCartesian ? inverted : false;
- // draw the data labels (inn pies they go before the points)
- if (series.drawDataLabels) {
- series.drawDataLabels();
- }
+ // draw the graph if any
+ if (series.drawGraph) {
+ series.drawGraph();
+ series.applyZones();
+ }
- // draw the points
- if (series.visible) {
- series.drawPoints();
- }
+ /* 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 mouse tracking area
- if (series.drawTracker && series.options.enableMouseTracking !== false) {
- series.drawTracker();
- }
+ // draw the points
+ if (series.visible) {
+ series.drawPoints();
+ }
- // 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);
- }
+ // draw the mouse tracking area
+ if (series.drawTracker && series.options.enableMouseTracking !== false) {
+ series.drawTracker();
+ }
- // Run the animation
- if (animDuration) {
- series.animate();
- }
+ // Handle inverted series and tracker groups
+ series.invertGroups(inverted);
- // 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);
- }
+ // 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);
+ }
- series.isDirty = series.isDirtyData = 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;
- },
+ // Run the animation
+ if (animDuration) {
+ series.animate();
+ }
- /**
- * Redraw the series after an update in the axes.
- */
- redraw: function() {
- var series = this,
- chart = series.chart,
- wasDirty = series.isDirty || series.isDirtyData, // cache it here as it is set to false in render, but used after
- group = series.group,
- xAxis = series.xAxis,
- yAxis = series.yAxis;
+ // 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);
+ }
- // reposition on resize
- if (group) {
- if (chart.inverted) {
- group.attr({
- width: chart.plotWidth,
- height: chart.plotHeight
- });
- }
+ series.isDirty = series.isDirtyData = 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;
+ },
- group.animate({
- translateX: pick(xAxis && xAxis.left, chart.plotLeft),
- translateY: pick(yAxis && yAxis.top, chart.plotTop)
+ /**
+ * Redraw the series after an update in the axes.
+ */
+ redraw: function() {
+ var series = this,
+ chart = series.chart,
+ wasDirty = series.isDirty || series.isDirtyData, // cache it here as it is set to false in render, but used after
+ 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
});
}
- series.translate();
- series.render();
- if (wasDirty) { // #3868, #3945
- delete this.kdTree;
- }
- },
+ group.animate({
+ translateX: pick(xAxis && xAxis.left, chart.plotLeft),
+ translateY: pick(yAxis && yAxis.top, chart.plotTop)
+ });
+ }
- /**
- * KD Tree && PointSearching Implementation
- */
+ series.translate();
+ series.render();
+ if (wasDirty) { // #3868, #3945
+ delete this.kdTree;
+ }
+ },
- kdDimensions: 1,
- kdAxisArray: ['clientX', 'plotY'],
+ /**
+ * KD Tree && PointSearching Implementation
+ */
- searchPoint: function(e, compareX) {
- var series = this,
- xAxis = series.xAxis,
- yAxis = series.yAxis,
- inverted = series.chart.inverted;
+ kdDimensions: 1,
+ kdAxisArray: ['clientX', 'plotY'],
- 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);
- },
+ searchPoint: function(e, compareX) {
+ var series = this,
+ xAxis = series.xAxis,
+ yAxis = series.yAxis,
+ inverted = series.chart.inverted;
- buildKDTree: function() {
- var series = this,
- dimensions = series.kdDimensions;
+ 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);
+ },
- // Internal function
- function _kdtree(points, depth, dimensions) {
- var axis,
- median,
- length = points && points.length;
+ buildKDTree: function() {
+ var series = this,
+ dimensions = series.kdDimensions;
- if (length) {
+ // Internal function
+ function _kdtree(points, depth, dimensions) {
+ var axis,
+ median,
+ length = points && points.length;
- // alternate between the axis
- axis = series.kdAxisArray[depth % dimensions];
+ if (length) {
- // sort point array
- points.sort(function(a, b) {
- return a[axis] - b[axis];
- });
+ // alternate between the axis
+ axis = series.kdAxisArray[depth % dimensions];
- median = Math.floor(length / 2);
+ // sort point array
+ points.sort(function(a, b) {
+ return a[axis] - b[axis];
+ });
- // 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)
- };
+ 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, !series.directTouch // For line-type series restrict to plot area, but column-type series not (#3916, #4511)
- ),
- dimensions,
- dimensions
- );
}
- delete series.kdTree;
+ }
- // For testing tooltips, don't build async
- syncTimeout(startRecursive, series.options.kdNow ? 0 : 1);
- },
+ // 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, !series.directTouch // For line-type series restrict to plot area, but column-type series not (#3916, #4511)
+ ),
+ dimensions,
+ dimensions
+ );
+ }
+ delete series.kdTree;
- searchKDTree: function(point, compareX) {
- var series = this,
- kdX = this.kdAxisArray[0],
- kdY = this.kdAxisArray[1],
- kdComparer = compareX ? 'distX' : 'dist';
+ // For testing tooltips, don't build async
+ syncTimeout(startRecursive, series.options.kdNow ? 0 : 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);
+ searchKDTree: function(point, compareX) {
+ var series = this,
+ kdX = this.kdAxisArray[0],
+ kdY = this.kdAxisArray[1],
+ kdComparer = compareX ? 'distX' : 'dist';
- p2.dist = defined(r) ? Math.sqrt(r) : Number.MAX_VALUE;
- p2.distX = defined(x) ? Math.sqrt(x) : Number.MAX_VALUE;
- }
+ // 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);
- function _search(search, tree, depth, dimensions) {
- var point = tree.point,
- axis = series.kdAxisArray[depth % dimensions],
- tdist,
- sideA,
- sideB,
- ret = point,
- nPoint1,
- nPoint2;
+ p2.dist = defined(r) ? Math.sqrt(r) : Number.MAX_VALUE;
+ p2.distX = defined(x) ? Math.sqrt(x) : Number.MAX_VALUE;
+ }
- setDistance(search, point);
+ function _search(search, tree, depth, dimensions) {
+ var point = tree.point,
+ axis = series.kdAxisArray[depth % dimensions],
+ tdist,
+ sideA,
+ sideB,
+ ret = point,
+ nPoint1,
+ nPoint2;
- // Pick side based on distance to splitting point
- tdist = search[axis] - point[axis];
- sideA = tdist < 0 ? 'left' : 'right';
- sideB = tdist < 0 ? 'right' : 'left';
+ setDistance(search, point);
- // End of tree
- if (tree[sideA]) {
- nPoint1 = _search(search, tree[sideA], depth + 1, dimensions);
+ // Pick side based on distance to splitting point
+ tdist = search[axis] - point[axis];
+ sideA = tdist < 0 ? 'left' : 'right';
+ sideB = tdist < 0 ? 'right' : 'left';
- 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);
- }
- }
+ // End of tree
+ if (tree[sideA]) {
+ nPoint1 = _search(search, tree[sideA], depth + 1, dimensions);
- return ret;
+ ret = (nPoint1[kdComparer] < ret[kdComparer] ? nPoint1 : point);
}
-
- if (!this.kdTree) {
- this.buildKDTree();
+ 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);
+ }
}
- if (this.kdTree) {
- return _search(point,
- this.kdTree, this.kdDimensions, this.kdDimensions);
- }
+ return ret;
}
- }); // end Series prototype
+ if (!this.kdTree) {
+ this.buildKDTree();
+ }
+ if (this.kdTree) {
+ return _search(point,
+ this.kdTree, this.kdDimensions, this.kdDimensions);
+ }
+ }
+
+ }); // end Series prototype
+
}(Highcharts));
(function(H) {
/**
* (c) 2010-2016 Torstein Honsi
*
@@ -16737,12 +17994,16 @@
destroyObjectProperties = H.destroyObjectProperties,
each = H.each,
format = H.format,
pick = H.pick,
Series = H.Series;
+
/**
- * The class for stack items
+ * The class for stacks. Each stack, on a specific X value and either negative
+ * or positive, has its own stack item.
+ *
+ * @class
*/
function StackItem(axis, options, isNegative, x, stackOption) {
var inverted = axis.chart.inverted;
@@ -16758,29 +18019,34 @@
this.x = x;
// Initialize total value
this.total = null;
- // This will keep each points' extremes stored by series.index and point index
+ // This will keep each points' extremes stored by series.index and point
+ // index
this.points = {};
- // Save the stack option on the series configuration object, and whether to treat it as percent
+ // Save the stack option on the series configuration object, and whether to
+ // treat it as percent
this.stack = stackOption;
this.leftCliff = 0;
this.rightCliff = 0;
- // The align options and text align varies on whether the stack is negative and
- // if the chart is inverted or not.
+ // The align options and text align varies on whether the stack is negative
+ // and if the chart is inverted or not.
// First test the user supplied value, then use the dynamic.
this.alignOptions = {
- align: options.align || (inverted ? (isNegative ? 'left' : 'right') : 'center'),
- verticalAlign: options.verticalAlign || (inverted ? 'middle' : (isNegative ? 'bottom' : 'top')),
+ align: options.align ||
+ (inverted ? (isNegative ? 'left' : 'right') : 'center'),
+ verticalAlign: options.verticalAlign ||
+ (inverted ? 'middle' : (isNegative ? 'bottom' : 'top')),
y: pick(options.y, inverted ? 4 : (isNegative ? 14 : -6)),
x: pick(options.x, inverted ? (isNegative ? -6 : 6) : 0)
};
- this.textAlign = options.textAlign || (inverted ? (isNegative ? 'right' : 'left') : 'center');
+ this.textAlign = options.textAlign ||
+ (inverted ? (isNegative ? 'right' : 'left') : 'center');
}
StackItem.prototype = {
destroy: function() {
destroyObjectProperties(this, this.axis);
@@ -16794,60 +18060,77 @@
formatOption = options.format,
str = formatOption ?
format(formatOption, this) :
options.formatter.call(this); // format the text in the label
- // Change the text to reflect the new total and set visibility to hidden in case the serie is hidden
+ // Change the text to reflect the new total and set visibility to hidden
+ // in case the serie is hidden
if (this.label) {
this.label.attr({
text: str,
visibility: 'hidden'
});
// Create new label
} else {
this.label =
- this.axis.chart.renderer.text(str, null, null, options.useHTML) // dummy positions, actual position updated with setOffset method in columnseries
- .css(options.style) // apply style
+ this.axis.chart.renderer.text(str, null, null, options.useHTML)
+ .css(options.style)
.attr({
- align: this.textAlign, // fix the text-anchor
- rotation: options.rotation, // rotation
+ align: this.textAlign,
+ rotation: options.rotation,
visibility: 'hidden' // hidden until setOffset is called
})
.add(group); // add to the labels-group
}
},
/**
- * Sets the offset that the stack has from the x value and repositions the label.
+ * Sets the offset that the stack has from the x value and repositions the
+ * label.
*/
setOffset: function(xOffset, xWidth) {
var stackItem = this,
axis = stackItem.axis,
chart = axis.chart,
inverted = chart.inverted,
reversed = axis.reversed,
- neg = (this.isNegative && !reversed) || (!this.isNegative && reversed), // #4056
- y = axis.translate(axis.usePercentage ? 100 : this.total, 0, 0, 0, 1), // stack value translated mapped to chart coordinates
+ neg = (this.isNegative && !reversed) ||
+ (!this.isNegative && reversed), // #4056
+ // stack value translated mapped to chart coordinates
+ y = axis.translate(
+ axis.usePercentage ? 100 : this.total,
+ 0,
+ 0,
+ 0,
+ 1
+ ),
yZero = axis.translate(0), // stack origin
h = Math.abs(y - yZero), // stack height
x = chart.xAxis[0].translate(this.x) + xOffset, // stack x position
plotHeight = chart.plotHeight,
stackBox = { // this is the box for the complete stack
x: inverted ? (neg ? y : y - h) : x,
- y: inverted ? plotHeight - x - xWidth : (neg ? (plotHeight - y - h) : plotHeight - y),
+ y: inverted ?
+ plotHeight - x - xWidth : (neg ? (plotHeight - y - h) :
+ plotHeight - y),
width: inverted ? h : xWidth,
height: inverted ? xWidth : h
},
label = this.label,
alignAttr;
if (label) {
- label.align(this.alignOptions, null, stackBox); // align the label to the box
+ // Align the label to the box
+ label.align(this.alignOptions, null, stackBox);
// Set visibility (#678)
alignAttr = label.alignAttr;
- label[this.options.crop === false || chart.isInsidePlot(alignAttr.x, alignAttr.y) ? 'show' : 'hide'](true);
+ label[
+ this.options.crop === false || chart.isInsidePlot(
+ alignAttr.x,
+ alignAttr.y
+ ) ? 'show' : 'hide'](true);
}
}
};
/**
@@ -16862,11 +18145,12 @@
axis.oldStacks = axis.stacks;
}
});
each(chart.series, function(series) {
- if (series.options.stacking && (series.visible === true || chart.options.chart.ignoreHiddenSeries === false)) {
+ if (series.options.stacking && (series.visible === true ||
+ chart.options.chart.ignoreHiddenSeries === false)) {
series.stackKey = series.type + pick(series.options.stack, '');
}
});
};
@@ -16924,12 +18208,12 @@
zIndex: 6
})
.add();
}
- // plotLeft/Top will change when y axis gets wider so we need to translate the
- // stackTotalGroup at every render call. See bug #506 and #516
+ // plotLeft/Top will change when y axis gets wider so we need to translate
+ // the stackTotalGroup at every render call. See bug #506 and #516
stackTotalGroup.translate(chart.plotLeft, chart.plotTop);
// Render each stack total
for (stackKey in stacks) {
oneStack = stacks[stackKey];
@@ -16987,11 +18271,12 @@
/**
* Adds series' points value to corresponding stack
*/
Series.prototype.setStackedPoints = function() {
- if (!this.options.stacking || (this.visible !== true && this.chart.options.chart.ignoreHiddenSeries !== false)) {
+ if (!this.options.stacking || (this.visible !== true &&
+ this.chart.options.chart.ignoreHiddenSeries !== false)) {
return;
}
var series = this,
xData = series.processedXData,
@@ -17024,14 +18309,19 @@
// loop over the non-null y values and read them into a local array
for (i = 0; i < yDataLength; i++) {
x = xData[i];
y = yData[i];
- stackIndicator = series.getStackIndicator(stackIndicator, x, series.index);
+ stackIndicator = series.getStackIndicator(
+ stackIndicator,
+ x,
+ series.index
+ );
pointKey = stackIndicator.key;
// Read stacked values into a stack based on the x value,
- // the sign of y and the stack key. Stacking is also handled for null values (#739)
+ // the sign of y and the stack key. Stacking is also handled for null
+ // values (#739)
isNegative = negStacks && y < (stackThreshold ? 0 : threshold);
key = isNegative ? negKey : stackKey;
// Create empty object for this stack if it doesn't exist yet
if (!stacks[key]) {
@@ -17042,11 +18332,17 @@
if (!stacks[key][x]) {
if (oldStacks[key] && oldStacks[key][x]) {
stacks[key][x] = oldStacks[key][x];
stacks[key][x].total = null;
} else {
- stacks[key][x] = new StackItem(yAxis, yAxis.options.stackLabels, isNegative, x, stackOption);
+ stacks[key][x] = new StackItem(
+ yAxis,
+ yAxis.options.stackLabels,
+ isNegative,
+ x,
+ stackOption
+ );
}
}
// If the StackItem doesn't exist, create it first
stack = stacks[key][x];
@@ -17058,25 +18354,28 @@
stack.base = pointKey;
}
stack.touched = yAxis.stacksTouched;
- // In area charts, if there are multiple points on the same X value, let the
- // area fill the full span of those points
+ // In area charts, if there are multiple points on the same X value,
+ // let the area fill the full span of those points
if (stackIndicator.index > 0 && series.singleStacks === false) {
- stack.points[pointKey][0] = stack.points[series.index + ',' + x + ',0'][0];
+ stack.points[pointKey][0] =
+ stack.points[series.index + ',' + x + ',0'][0];
}
}
// Add value to the stack total
if (stacking === 'percent') {
- // Percent stacked column, totals are the same for the positive and negative stacks
+ // Percent stacked column, totals are the same for the positive and
+ // negative stacks
other = isNegative ? stackKey : negKey;
if (negStacks && stacks[other] && stacks[other][x]) {
other = stacks[other][x];
- stack.total = other.total = Math.max(other.total, stack.total) + Math.abs(y) || 0;
+ stack.total = other.total =
+ Math.max(other.total, stack.total) + Math.abs(y) || 0;
// Percent stacked areas
} else {
stack.total = correctFloat(stack.total + (Math.abs(y) || 0));
}
@@ -17120,31 +18419,44 @@
pointExtremes,
totalFactor;
while (i--) {
x = processedXData[i];
- stackIndicator = series.getStackIndicator(stackIndicator, x, series.index);
+ stackIndicator = series.getStackIndicator(
+ stackIndicator,
+ x,
+ series.index,
+ key
+ );
stack = stacks[key] && stacks[key][x];
pointExtremes = stack && stack.points[stackIndicator.key];
if (pointExtremes) {
totalFactor = stack.total ? 100 / stack.total : 0;
- pointExtremes[0] = correctFloat(pointExtremes[0] * totalFactor); // Y bottom value
- pointExtremes[1] = correctFloat(pointExtremes[1] * totalFactor); // Y value
+ // Y bottom value
+ pointExtremes[0] = correctFloat(pointExtremes[0] * totalFactor);
+ // Y value
+ pointExtremes[1] = correctFloat(pointExtremes[1] * totalFactor);
series.stackedYData[i] = pointExtremes[1];
}
}
});
};
/**
- * Get stack indicator, according to it's x-value, to determine points with the same x-value
+ * Get stack indicator, according to it's x-value, to determine points with the
+ * same x-value
*/
- Series.prototype.getStackIndicator = function(stackIndicator, x, index) {
- if (!defined(stackIndicator) || stackIndicator.x !== x) {
+ Series.prototype.getStackIndicator = function(stackIndicator, x, index, key) {
+ // Update stack indicator, when:
+ // first point in a stack || x changed || stack type (negative vs positive)
+ // changed:
+ if (!defined(stackIndicator) || stackIndicator.x !== x ||
+ (key && stackIndicator.key !== key)) {
stackIndicator = {
x: x,
- index: 0
+ index: 0,
+ key: key
};
} else {
stackIndicator.index++;
}
@@ -17171,21 +18483,22 @@
each = H.each,
erase = H.erase,
extend = H.extend,
fireEvent = H.fireEvent,
inArray = H.inArray,
+ isNumber = H.isNumber,
isObject = H.isObject,
merge = H.merge,
pick = H.pick,
Point = H.Point,
Series = H.Series,
seriesTypes = H.seriesTypes,
setAnimation = H.setAnimation,
splat = H.splat;
// Extend the Chart prototype for dynamic methods
- extend(Chart.prototype, {
+ extend(Chart.prototype, /** @lends Highcharts.Chart.prototype */ {
/**
* Add a series dynamically after time
*
* @param {Object} options The config options
@@ -17275,14 +18588,13 @@
null,
loadingDiv
);
addEvent(chart, 'redraw', setLoadingSize); // #1080
}
- setTimeout(function() {
- loadingDiv.className = 'highcharts-loading';
- });
+ loadingDiv.className = 'highcharts-loading';
+
// Update text
chart.loadingSpan.innerHTML = str || options.lang.loading;
// Update visuals
@@ -17342,13 +18654,16 @@
'borderRadius', 'plotBackgroundColor', 'plotBackgroundImage', 'plotBorderColor', 'plotBorderWidth',
'plotShadow', 'shadow'
],
/**
- * These properties cause all series to be updated when updating. Can be extended from plugins.
+ * These properties cause all series to be updated when updating. Can be
+ * extended from plugins.
*/
- propsRequireUpdateSeries: ['chart.polar', 'chart.ignoreHiddenSeries', 'chart.type', 'colors', 'plotOptions'],
+ propsRequireUpdateSeries: ['chart.inverted', 'chart.polar',
+ 'chart.ignoreHiddenSeries', 'chart.type', 'colors', 'plotOptions'
+ ],
/**
* Chart.update function that takes the whole options stucture.
*/
update: function(options, redraw) {
@@ -17358,11 +18673,13 @@
title: 'setTitle',
subtitle: 'setSubtitle'
},
optionsChart = options.chart,
updateAllAxes,
- updateAllSeries;
+ updateAllSeries,
+ newWidth,
+ newHeight;
// If the top-level chart option is present, some special updates are required
if (optionsChart) {
merge(true, this.options.chart, optionsChart);
@@ -17461,12 +18778,15 @@
if (options.loading) {
merge(true, this.options.loading, options.loading);
}
// Update size. Redraw is forced.
- if (optionsChart && ('width' in optionsChart || 'height' in optionsChart)) {
- this.setSize(optionsChart.width, optionsChart.height);
+ newWidth = optionsChart && optionsChart.width;
+ newHeight = optionsChart && optionsChart.height;
+ if ((isNumber(newWidth) && newWidth !== this.chartWidth) ||
+ (isNumber(newHeight) && newHeight !== this.chartHeight)) {
+ this.setSize(newWidth, newHeight);
} else if (pick(redraw, true)) {
this.redraw();
}
},
@@ -17479,11 +18799,11 @@
});
// extend the Point prototype for dynamic methods
- extend(Point.prototype, {
+ extend(Point.prototype, /** @lends Point.prototype */ {
/**
* Point.update with new options (typically x/y data) and optionally redraw the series.
*
* @param {Object} options Point options as defined in the series.data array
* @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
@@ -17562,18 +18882,18 @@
this.series.removePoint(inArray(this, this.series.data), redraw, animation);
}
});
// Extend the series prototype for dynamic methods
- extend(Series.prototype, {
+ extend(Series.prototype, /** @lends Series.prototype */ {
/**
* Add a point dynamically after chart load time
* @param {Object} options Point options as given in series.data
* @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
* @param {Boolean} shift If shift is true, a point is shifted off the start
* of the series as one is appended to the end.
- * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
+ * @param {Boolean|AnimationOptions} animation Whether to apply animation, and optionally animation
* configuration
*/
addPoint: function(options, redraw, shift, animation) {
var series = this,
seriesOptions = series.options,
@@ -17779,11 +19099,11 @@
}
}
});
// Extend the Axis.prototype for dynamic methods
- extend(Axis.prototype, {
+ extend(Axis.prototype, /** @lends Axis.prototype */ {
/**
* Axis.update with a new options structure
*/
update: function(newOptions, redraw) {
@@ -17869,21 +19189,24 @@
LegendSymbolMixin = H.LegendSymbolMixin,
map = H.map,
pick = H.pick,
Series = H.Series,
seriesType = H.seriesType;
+
/**
- * Area series type
+ * Area series type.
+ * @constructor seriesTypes.area
+ * @extends {Series}
*/
seriesType('area', 'line', {
softThreshold: false,
threshold: 0
// trackByArea: false,
// lineColor: null, // overrides color, but lets fillColor be unaltered
// fillOpacity: 0.75,
// fillColor: null
- }, {
+ }, /** @lends seriesTypes.area.prototype */ {
singleStacks: false,
/**
* Return an array of stacked points, where null and missing points are replaced by
* dummy points in order for gaps to be drawn correctly in stacks.
*/
@@ -18189,29 +19512,19 @@
* (c) 2010-2016 Torstein Honsi
*
* License: www.highcharts.com/license
*/
'use strict';
- var defaultPlotOptions = H.defaultPlotOptions,
- defaultSeriesOptions = H.defaultPlotOptions.line,
- extendClass = H.extendClass,
- merge = H.merge,
- pick = H.pick,
- Series = H.Series,
- seriesTypes = H.seriesTypes;
+ var pick = H.pick,
+ seriesType = H.seriesType;
/**
- * Set the default options for spline
+ * Spline series type.
+ * @constructor seriesTypes.spline
+ * @extends {Series}
*/
- defaultPlotOptions.spline = merge(defaultSeriesOptions);
-
- /**
- * SplineSeries object
- */
- seriesTypes.spline = extendClass(Series, {
- type: 'spline',
-
+ seriesType('spline', 'line', {}, /** @lends seriesTypes.spline.prototype */ {
/**
* Get the spline segment from a given point's previous neighbour to the given point
*/
getPointSpline: function(points, point, i) {
var smoothing = 1.5, // 1 means control points midway between points, 2 means 1/3 from the point, 3 is 1/4 etc
@@ -18372,11 +19685,14 @@
Series = H.Series,
seriesType = H.seriesType,
stop = H.stop,
svg = H.svg;
/**
- * The column series type
+ * The column series type.
+ *
+ * @constructor seriesTypes.column
+ * @augments Series
*/
seriesType('column', 'line', {
borderRadius: 0,
//colorByPoint: undefined,
groupPadding: 0.2,
@@ -18418,20 +19734,24 @@
borderColor: '#ffffff'
// borderWidth: 1
- // Prototype members
- }, {
+ }, /** @lends seriesTypes.column.prototype */ {
cropShoulder: 0,
directTouch: true, // When tooltip is not shared, this series (and derivatives) requires direct touch/hover. KD-tree does not apply.
trackerGroups: ['group', 'dataLabelsGroup'],
negStacks: true, // use separate negative stacks, unlike area stacks where a negative
// point is substracted from previous (#1910)
/**
- * Initialize the series
+ * Initialize the series. Extends the basic Series.init method by
+ * marking other series of the same type as dirty.
+ *
+ * @function #init
+ * @memberOf seriesTypes.column
+ * @returns {void}
*/
init: function() {
Series.prototype.init.apply(this, arguments);
var series = this,
@@ -18656,11 +19976,12 @@
ret,
p2o = this.pointAttrToOptions || {},
strokeOption = p2o.stroke || 'borderColor',
strokeWidthOption = p2o['stroke-width'] || 'borderWidth',
fill = (point && point.color) || this.color,
- stroke = options[strokeOption] || this.color || fill, // set to fill when borderColor = null on pies
+ stroke = point[strokeOption] || options[strokeOption] ||
+ this.color || fill, // set to fill when borderColor null
dashstyle = options.dashStyle,
zone,
brightness;
// Handle zone colors
@@ -18813,11 +20134,13 @@
* (c) 2010-2016 Torstein Honsi
*
* License: www.highcharts.com/license
*/
'use strict';
+
var seriesType = H.seriesType;
+
/**
* The Bar series class
*/
seriesType('bar', 'column', null, {
inverted: true
@@ -18931,12 +20254,16 @@
Point = H.Point,
Series = H.Series,
seriesType = H.seriesType,
seriesTypes = H.seriesTypes,
setAnimation = H.setAnimation;
+
/**
- * Pie series type
+ * The pie series type.
+ *
+ * @constructor seriesTypes.pie
+ * @augments Series
*/
seriesType('pie', 'line', {
center: [null, null],
clip: false,
colorByPoint: true, // always true for pies
@@ -18974,12 +20301,11 @@
shadow: false
}
}
- // Prototype members
- }, {
+ }, /** @lends seriesTypes.pie.prototype */ {
isCartesian: false,
requireSorting: false,
directTouch: true,
noSharedTooltip: true,
trackerGroups: ['group', 'dataLabelsGroup'],
@@ -19278,13 +20604,17 @@
/**
* Pies don't have point marker symbols
*/
getSymbol: noop
- // Point class overrides
- }, {
+
/**
+ * @constructor seriesTypes.pie.prototype.pointClass
+ * @extends {Point}
+ */
+ }, /** @lends seriesTypes.pie.prototype.pointClass.prototype */ {
+ /**
* Initiate the pie slice
*/
init: function() {
Point.prototype.init.apply(this, arguments);
@@ -19384,18 +20714,23 @@
}
},
haloPath: function(size) {
- var shapeArgs = this.shapeArgs,
- chart = this.series.chart;
+ var shapeArgs = this.shapeArgs;
- return this.sliced || !this.visible ? [] : this.series.chart.renderer.symbols.arc(chart.plotLeft + shapeArgs.x, chart.plotTop + shapeArgs.y, shapeArgs.r + size, shapeArgs.r + size, {
- innerR: this.shapeArgs.r,
- start: shapeArgs.start,
- end: shapeArgs.end
- });
+ return this.sliced || !this.visible ? [] :
+ this.series.chart.renderer.symbols.arc(
+ shapeArgs.x,
+ shapeArgs.y,
+ shapeArgs.r + size,
+ shapeArgs.r + size, {
+ innerR: this.shapeArgs.r,
+ start: shapeArgs.start,
+ end: shapeArgs.end
+ }
+ );
}
});
}(Highcharts));
(function(H) {
@@ -20088,11 +21423,11 @@
* and the pie slice.
*/
seriesTypes.pie.prototype.connectorPath = function(labelPos) {
var x = labelPos.x,
y = labelPos.y;
- return pick(this.options.softConnector, true) ? [
+ return pick(this.options.dataLabels.softConnector, true) ? [
'M',
x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label
'C',
x, y, // first break, next to the label
2 * labelPos[2] - labelPos[4], 2 * labelPos[3] - labelPos[5],
@@ -20431,15 +21766,21 @@
Point = H.Point,
Series = H.Series,
seriesTypes = H.seriesTypes,
svg = H.svg,
TrackerMixin;
+
/**
- * TrackerMixin for points and graphs
+ * TrackerMixin for points and graphs.
+ *
+ * @mixin
*/
TrackerMixin = H.TrackerMixin = {
+ /**
+ * Draw the tracker for a point.
+ */
drawTrackerPoint: function() {
var series = this,
chart = series.chart,
pointer = chart.pointer,
onMouseOver = function(e) {
@@ -20702,11 +22043,11 @@
/*
* Extend the Chart object with interaction
*/
- extend(Chart.prototype, {
+ extend(Chart.prototype, /** @lends Chart.prototype */ {
/**
* Display the zoom button
*/
showResetZoom: function() {
var chart = this,
@@ -20763,11 +22104,11 @@
each(event.xAxis.concat(event.yAxis), function(axisData) {
var axis = axisData.axis,
isXAxis = axis.isXAxis;
// don't zoom more than minRange
- if (pointer[isXAxis ? 'zoomX' : 'zoomY'] || pointer[isXAxis ? 'pinchX' : 'pinchY']) {
+ if (pointer[isXAxis ? 'zoomX' : 'zoomY']) {
hasZoomed = axis.zoom(axisData.min, axisData.max);
if (axis.displayBtn) {
displayButton = true;
}
}
@@ -20843,11 +22184,11 @@
});
/*
* Extend the Point object with interaction
*/
- extend(Point.prototype, {
+ extend(Point.prototype, /** @lends Point.prototype */ {
/**
* Toggle the selection status of a point
* @param {Boolean} selected Whether to select or unselect the point.
* @param {Boolean} accumulate Whether to add to the previous selection. By default,
* this happens if the control key (Cmd on Mac) was pressed during clicking.
@@ -20894,34 +22235,34 @@
series = point.series,
chart = series.chart,
tooltip = chart.tooltip,
hoverPoint = chart.hoverPoint;
- if (chart.hoverSeries !== series) {
- series.onMouseOver();
- }
-
- // set normal state to previous series
- if (hoverPoint && hoverPoint !== point) {
- hoverPoint.onMouseOut();
- }
-
if (point.series) { // It may have been destroyed, #4130
+ // In shared tooltip, call mouse over when point/series is actually hovered: (#5766)
+ if (!byProximity) {
+ // set normal state to previous series
+ if (hoverPoint && hoverPoint !== point) {
+ hoverPoint.onMouseOut();
+ }
+ if (chart.hoverSeries !== series) {
+ series.onMouseOver();
+ }
+ chart.hoverPoint = point;
+ }
- // trigger the event
- point.firePointEvent('mouseOver');
-
// update the tooltip
if (tooltip && (!tooltip.shared || series.noSharedTooltip)) {
+ // hover point only for non shared points: (#5766)
+ point.setState('hover');
tooltip.refresh(point, e);
+ } else if (!tooltip) {
+ point.setState('hover');
}
- // hover this
- point.setState('hover');
- if (!byProximity) {
- chart.hoverPoint = point;
- }
+ // trigger the event
+ point.firePointEvent('mouseOver');
}
},
/**
* Runs on mouse out from the point
@@ -20967,21 +22308,23 @@
var point = this,
plotX = Math.floor(point.plotX), // #4586
plotY = point.plotY,
series = point.series,
stateOptions = series.options.states[state] || {},
- markerOptions = (defaultPlotOptions[series.type].marker && series.options.marker) || {},
- normalDisabled = markerOptions.enabled === false,
- markerStateOptions = (markerOptions.states && markerOptions.states[state]) || {},
+ markerOptions = defaultPlotOptions[series.type].marker &&
+ series.options.marker,
+ normalDisabled = markerOptions && markerOptions.enabled === false,
+ markerStateOptions = (markerOptions && markerOptions.states &&
+ markerOptions.states[state]) || {},
stateDisabled = markerStateOptions.enabled === false,
stateMarkerGraphic = series.stateMarkerGraphic,
pointMarker = point.marker || {},
chart = series.chart,
- radius,
halo = series.halo,
haloOptions,
- attribs,
+ markerAttribs,
+ hasMarkers = markerOptions && series.markerAttribs,
newSymbol;
state = state || ''; // empty string
if (
@@ -20998,11 +22341,13 @@
) {
return;
}
- radius = markerStateOptions.radius || (markerOptions.radius + (markerStateOptions.radiusPlus || 0));
+ if (hasMarkers) {
+ markerAttribs = series.markerAttribs(point, state);
+ }
// Apply hover styles to the existing point
if (point.graphic) {
if (point.state) {
@@ -21010,22 +22355,32 @@
}
if (state) {
point.graphic.addClass('highcharts-point-' + state);
}
- attribs = radius ? { // new symbol attributes (#507, #612)
- x: plotX - radius,
- y: plotY - radius,
- width: 2 * radius,
- height: 2 * radius
- } : {};
+ /*attribs = radius ? { // new symbol attributes (#507, #612)
+ x: plotX - radius,
+ y: plotY - radius,
+ width: 2 * radius,
+ height: 2 * radius
+ } : {};*/
- attribs = merge(series.pointAttribs(point, state), attribs);
+ //attribs = merge(series.pointAttribs(point, state), attribs);
+ point.graphic.attr(series.pointAttribs(point, state));
- point.graphic.attr(attribs);
+ if (markerAttribs) {
+ point.graphic.animate(
+ markerAttribs,
+ pick(
+ chart.options.chart.animation, // Turn off globally
+ markerStateOptions.animation,
+ markerOptions.animation
+ )
+ );
+ }
// Zooming in from a range with no markers to a range with markers
if (stateMarkerGraphic) {
stateMarkerGraphic.hide();
}
@@ -21044,24 +22399,24 @@
// Add a new state marker graphic
if (!stateMarkerGraphic) {
if (newSymbol) {
series.stateMarkerGraphic = stateMarkerGraphic = chart.renderer.symbol(
newSymbol,
- plotX - radius,
- plotY - radius,
- 2 * radius,
- 2 * radius
+ markerAttribs.x,
+ markerAttribs.y,
+ markerAttribs.width,
+ markerAttribs.height
)
.add(series.markerGroup);
stateMarkerGraphic.currentSymbol = newSymbol;
}
// Move the existing graphic
} else {
stateMarkerGraphic[move ? 'animate' : 'attr']({ // #1054
- x: plotX - radius,
- y: plotY - radius
+ x: markerAttribs.x,
+ y: markerAttribs.y
});
}
if (stateMarkerGraphic) {
stateMarkerGraphic.attr(series.pointAttribs(point, state));
@@ -21078,64 +22433,60 @@
// Show me your halo
haloOptions = stateOptions.halo;
if (haloOptions && haloOptions.size) {
if (!halo) {
series.halo = halo = chart.renderer.path()
- .add(chart.seriesGroup);
+ // #5818, #5903
+ .add(hasMarkers ? series.markerGroup : series.group);
}
+ H.stop(halo);
halo[move ? 'animate' : 'attr']({
d: point.haloPath(haloOptions.size)
});
halo.attr({
'class': 'highcharts-halo highcharts-color-' + pick(point.colorIndex, series.colorIndex)
});
halo.attr(extend({
- 'fill': point.color || series.color,
- 'fill-opacity': haloOptions.opacity,
- 'zIndex': -1 // #4929, IE8 added halo above everything
- },
- haloOptions.attributes))[move ? 'animate' : 'attr']({
- d: point.haloPath(haloOptions.size)
- });
+ 'fill': point.color || series.color,
+ 'fill-opacity': haloOptions.opacity,
+ 'zIndex': -1 // #4929, IE8 added halo above everything
+ }, haloOptions.attributes));
} else if (halo) {
- halo.attr({
- d: []
- });
+ halo.animate({
+ d: point.haloPath(0)
+ }); // Hide
}
point.state = state;
},
/**
* Get the circular path definition for the halo
- * @param {Number} size The radius of the circular halo
+ * @param {Number} size The radius of the circular halo.
* @returns {Array} The path definition
*/
haloPath: function(size) {
var series = this.series,
- chart = series.chart,
- plotBox = series.getPlotBox(),
- inverted = chart.inverted,
- plotX = Math.floor(this.plotX);
+ chart = series.chart;
return chart.renderer.symbols.circle(
- plotBox.translateX + (inverted ? series.yAxis.len - this.plotY : plotX) - size,
- plotBox.translateY + (inverted ? series.xAxis.len - plotX : this.plotY) - size,
+ Math.floor(this.plotX) - size,
+ this.plotY - size,
size * 2,
size * 2
);
}
});
/*
* Extend the Series object with interaction
*/
- extend(Series.prototype, {
+ extend(Series.prototype, /** @lends Series.prototype */ {
/**
* Series mouse over handler
*/
onMouseOver: function() {
var series = this,
@@ -21265,11 +22616,11 @@
// if called without an argument, toggle visibility
series.visible = vis = series.options.visible = series.userOptions.visible = vis === undefined ? !oldVisibility : vis; // #5618
showOrHide = vis ? 'show' : 'hide';
// show or hide elements
- each(['group', 'dataLabelsGroup', 'markerGroup', 'tracker'], function(key) {
+ each(['group', 'dataLabelsGroup', 'markerGroup', 'tracker', 'tt'], function(key) {
if (series[key]) {
series[key][showOrHide]();
}
});
@@ -21380,19 +22731,19 @@
*/
Chart.prototype.matchResponsiveRule = function(rule, redraw) {
var respRules = this.respRules,
condition = rule.condition,
matches,
- fn = rule.callback || function() {
+ fn = condition.callback || function() {
return this.chartWidth <= pick(condition.maxWidth, Number.MAX_VALUE) &&
this.chartHeight <= pick(condition.maxHeight, Number.MAX_VALUE) &&
this.chartWidth >= pick(condition.minWidth, 0) &&
this.chartHeight >= pick(condition.minHeight, 0);
};
if (rule._id === undefined) {
- rule._id = H.idCounter++;
+ rule._id = H.uniqueKey();
}
matches = fn.call(this);
// Apply a rule
if (!respRules[rule._id] && matches) {