app/assets/javascripts/highcharts.js in highcharts-rails-4.0.4.1 vs app/assets/javascripts/highcharts.js in highcharts-rails-4.1.0
- old
+ new
@@ -1,10 +1,10 @@
// ==ClosureCompiler==
// @compilation_level SIMPLE_OPTIMIZATIONS
/**
- * @license Highcharts JS v4.0.4 (2014-09-02)
+ * @license Highcharts JS v4.1.0 (2015-02-16)
*
* (c) 2009-2014 Torstein Honsi
*
* License: www.highcharts.com/license
*/
@@ -31,11 +31,11 @@
// some variables
userAgent = navigator.userAgent,
isOpera = win.opera,
- isIE = /msie/i.test(userAgent) && !isOpera,
+ isIE = /(msie|trident)/i.test(userAgent) && !isOpera,
docMode8 = doc.documentMode === 8,
isWebKit = /AppleWebKit/.test(userAgent),
isFirefox = /Firefox/.test(userAgent),
isTouchDevice = /(Mobile|Android|Windows Phone)/.test(userAgent),
SVG_NS = 'http://www.w3.org/2000/svg',
@@ -50,16 +50,15 @@
defaultOptions,
dateFormat, // function
globalAnimation,
pathAnim,
timeUnits,
- error,
noop = function () { return UNDEFINED; },
charts = [],
chartCount = 0,
PRODUCT = 'Highcharts',
- VERSION = '4.0.4',
+ VERSION = '4.1.0',
// some constants for frequently used strings
DIV = 'div',
ABSOLUTE = 'absolute',
RELATIVE = 'relative',
@@ -72,10 +71,11 @@
L = 'L',
numRegex = /^[0-9]+$/,
NORMAL_STATE = '',
HOVER_STATE = 'hover',
SELECT_STATE = 'select',
+ marginNames = ['plotTop', 'marginRight', 'marginBottom', 'plotLeft'],
// Object for extending Axis
AxisPlotLineOrBandExtension,
// constants for attributes
@@ -83,10 +83,11 @@
// time methods, changed based on whether or not UTC is used
Date, // Allow using a different Date class
makeTime,
timezoneOffset,
+ getTimezoneOffset,
getMinutes,
getHours,
getDay,
getDate,
getMonth,
@@ -101,30 +102,29 @@
// lookup over the types and the associated classes
seriesTypes = {},
Highcharts;
// The Highcharts namespace
-if (win.Highcharts) {
- error(16, true);
-} else {
- Highcharts = win.Highcharts = {};
-}
+Highcharts = win.Highcharts = win.Highcharts ? error(16, true) : {};
+
+Highcharts.seriesTypes = seriesTypes;
+
/**
* 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
*/
-function extend(a, b) {
+var extend = Highcharts.extend = function (a, b) {
var n;
if (!a) {
a = {};
}
for (n in b) {
a[n] = b[n];
}
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.
@@ -290,22 +290,22 @@
/**
* Return the first value that is defined. Like MooTools' $.pick.
*/
-function pick() {
+var pick = Highcharts.pick = function () {
var args = arguments,
i,
arg,
length = args.length;
for (i = 0; i < length; i++) {
arg = args[i];
if (arg !== UNDEFINED && arg !== null) {
return arg;
}
}
-}
+};
/**
* Set CSS on a given element
* @param {Object} el
* @param {Object} styles Style object with camel case property names
@@ -355,37 +355,10 @@
extend(object.prototype, members);
return object;
}
/**
- * 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} decPoint 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
- */
-function numberFormat(number, decimals, decPoint, thousandsSep) {
- var externalFn = Highcharts.numberFormat,
- lang = defaultOptions.lang,
- // http://kevin.vanzonneveld.net/techblog/article/javascript_equivalent_for_phps_number_format/
- n = +number || 0,
- c = decimals === -1 ?
- (n.toString().split('.')[1] || '').length : // preserve decimals
- (isNaN(decimals = mathAbs(decimals)) ? 2 : decimals),
- d = decPoint === undefined ? lang.decimalPoint : decPoint,
- t = thousandsSep === undefined ? lang.thousandsSep : thousandsSep,
- s = n < 0 ? "-" : "",
- i = String(pInt(n = mathAbs(n).toFixed(c))),
- j = i.length > 3 ? i.length % 3 : 0;
-
- return externalFn !== numberFormat ?
- externalFn(number, decimals, decPoint, thousandsSep) :
- (s + (j ? i.substr(0, j) + t : "") + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + t) +
- (c ? d + mathAbs(n - i).toFixed(c).slice(2) : ""));
-}
-
-/**
* Pad a string to a given length by adding 0 to the beginning
* @param {Number} number
* @param {Number} length
*/
function pad(number, length) {
@@ -399,17 +372,22 @@
* @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.
*/
-function wrap(obj, method, func) {
+var wrap = Highcharts.wrap = function (obj, method, func) {
var proceed = obj[method];
obj[method] = function () {
var args = Array.prototype.slice.call(arguments);
args.unshift(proceed);
return func.apply(this, args);
};
+};
+
+
+function getTZOffset(timestamp) {
+ return ((getTimezoneOffset && getTimezoneOffset(timestamp)) || timezoneOffset || 0) * 60000;
}
/**
* Based on http://www.php.net/manual/en/function.strftime.php
* @param {String} format
@@ -420,11 +398,11 @@
if (!defined(timestamp) || isNaN(timestamp)) {
return 'Invalid date';
}
format = pick(format, '%Y-%m-%d %H:%M:%S');
- var date = new Date(timestamp - timezoneOffset),
+ var date = new Date(timestamp - getTZOffset(timestamp)),
key, // used in for constuct below
// get the basic time values
hours = date[getHours](),
day = date[getDay](),
dayOfMonth = date[getDate](),
@@ -439,10 +417,11 @@
// Day
'a': 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': dayOfMonth, // Day of the month, 1 through 31
+ 'w': day,
// Week (none implemented)
//'W': weekNumber(),
// Month
@@ -488,11 +467,11 @@
if (floatRegex.test(format)) { // float
decimals = format.match(decRegex);
decimals = decimals ? decimals[1] : -1;
if (val !== null) {
- val = numberFormat(
+ val = Highcharts.numberFormat(
val,
decimals,
lang.decimalPoint,
format.indexOf(',') > -1 ? lang.thousandsSep : ''
);
@@ -565,12 +544,14 @@
* @param {Number} interval
* @param {Array} multiples
* @param {Number} magnitude
* @param {Object} options
*/
-function normalizeTickInterval(interval, multiples, magnitude, allowDecimals) {
- var normalized, i;
+function normalizeTickInterval(interval, multiples, magnitude, allowDecimals, preventExceed) {
+ var normalized,
+ i,
+ retInterval = interval;
// round to a tenfold of 1, 2, 2.5 or 5
magnitude = pick(magnitude, 1);
normalized = interval / magnitude;
@@ -588,20 +569,21 @@
}
}
// normalize the interval to the nearest multiple
for (i = 0; i < multiples.length; i++) {
- interval = multiples[i];
- if (normalized <= (multiples[i] + (multiples[i + 1] || multiples[i])) / 2) {
+ 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))) {
break;
}
}
// multiply back to the correct magnitude
- interval *= magnitude;
-
- return interval;
+ retInterval *= magnitude;
+
+ return retInterval;
}
/**
* Utility method that sorts an object array and keeping the order of equal items.
@@ -702,20 +684,20 @@
}
/**
* Provide error messages for debugging, with links to online explanation
*/
-error = function (code, stop) {
+function error (code, stop) {
var msg = 'Highcharts error #' + code + ': www.highcharts.com/errors/' + code;
if (stop) {
throw msg;
}
// else ...
if (win.console) {
console.log(msg);
}
-};
+}
/**
* Fix JS round off float errors
* @param {Number} num
*/
@@ -743,14 +725,39 @@
second: 1000,
minute: 60000,
hour: 3600000,
day: 24 * 3600000,
week: 7 * 24 * 3600000,
- month: 31 * 24 * 3600000,
- year: 31556952000
+ 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} decPoint 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
+ */
+Highcharts.numberFormat = function (number, decimals, decPoint, thousandsSep) {
+ var lang = defaultOptions.lang,
+ // http://kevin.vanzonneveld.net/techblog/article/javascript_equivalent_for_phps_number_format/
+ n = +number || 0,
+ c = decimals === -1 ?
+ (n.toString().split('.')[1] || '').length : // preserve decimals
+ (isNaN(decimals = mathAbs(decimals)) ? 2 : decimals),
+ d = decPoint === undefined ? lang.decimalPoint : decPoint,
+ t = thousandsSep === undefined ? lang.thousandsSep : thousandsSep,
+ s = n < 0 ? "-" : "",
+ i = String(pInt(n = mathAbs(n).toFixed(c))),
+ j = i.length > 3 ? i.length % 3 : 0;
+
+ return (s + (j ? i.substr(0, j) + t : "") + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + t) +
+ (c ? d + mathAbs(n - i).toFixed(c).slice(2) : ""));
+};
+/**
* Path interpolation algorithm used across adapters
*/
pathAnim = {
/**
* Prepare start and end values so that the path can be animated one to one
@@ -1215,11 +1222,11 @@
// and all the utility functions will be null. In that case they are populated by the
// default adapters below.
var adapterRun = adapter.adapterRun,
getScript = adapter.getScript,
inArray = adapter.inArray,
- each = adapter.each,
+ each = Highcharts.each = adapter.each,
grep = adapter.grep,
offset = adapter.offset,
map = adapter.map,
addEvent = adapter.addEvent,
removeEvent = adapter.removeEvent,
@@ -1231,31 +1238,13 @@
/* ****************************************************************************
* Handle the options *
*****************************************************************************/
-var
-
-defaultLabelOptions = {
- enabled: true,
- // rotation: 0,
- // align: 'center',
- x: 0,
- y: 15,
- /*formatter: function () {
- return this.value;
- },*/
- style: {
- color: '#606060',
- cursor: 'default',
- fontSize: '11px'
- }
-};
-
defaultOptions = {
colors: ['#7cb5ec', '#434348', '#90ed7d', '#f7a35c',
- '#8085e9', '#f15c80', '#e4d354', '#8085e8', '#8d4653', '#91e8e1'],
+ '#8085e9', '#f15c80', '#e4d354', '#2b908f', '#f45b5b', '#91e8e1'],
symbols: ['circle', 'diamond', 'square', 'triangle', 'triangle-down'],
lang: {
loading: 'Loading...',
months: ['January', 'February', 'March', 'April', 'May', 'June', 'July',
'August', 'September', 'October', 'November', 'December'],
@@ -1263,17 +1252,17 @@
weekdays: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
decimalPoint: '.',
numericSymbols: ['k', 'M', 'G', 'T', 'P', 'E'], // SI prefixes used in axis labels
resetZoom: 'Reset zoom',
resetZoomTitle: 'Reset zoom level 1:1',
- thousandsSep: ','
+ thousandsSep: ' '
},
global: {
useUTC: true,
//timezoneOffset: 0,
- canvasToolsURL: 'http://code.highcharts.com/4.0.4/modules/canvas-tools.js',
- VMLRadialGradientURL: 'http://code.highcharts.com/4.0.4/gfx/vml-radial-gradient.png'
+ canvasToolsURL: 'http://code.highcharts.com/4.1.0/modules/canvas-tools.js',
+ VMLRadialGradientURL: 'http://code.highcharts.com/4.1.0/gfx/vml-radial-gradient.png'
},
chart: {
//animation: true,
//alignTicks: false,
//reflow: true,
@@ -1384,26 +1373,33 @@
}
},
point: {
events: {}
},
- dataLabels: merge(defaultLabelOptions, {
+ dataLabels: {
align: 'center',
- //defer: true,
- enabled: false,
+ // defer: true,
+ // enabled: false,
formatter: function () {
- return this.y === null ? '' : numberFormat(this.y, -1);
+ return this.y === null ? '' : Highcharts.numberFormat(this.y, -1);
},
+ style: {
+ color: 'contrast',
+ fontSize: '11px',
+ fontWeight: 'bold',
+ textShadow: '0 0 6px contrast, 0 0 3px contrast'
+ },
verticalAlign: 'bottom', // above singular point
- y: 0
+ x: 0,
+ y: 0,
// backgroundColor: undefined,
// borderColor: undefined,
// borderRadius: undefined,
// borderWidth: undefined,
- // padding: 3,
+ padding: 5
// shadow: false
- }),
+ },
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
@@ -1424,11 +1420,11 @@
marker: {}
}
},
stickyTracking: true,
//tooltip: {
- //pointFormat: '<span style="color:{series.color}">\u25CF</span> {series.name}: <b>{point.y}</b>'
+ //pointFormat: '<span style="color:{point.color}">\u25CF</span> {series.name}: <b>{point.y}</b>'
//valueDecimals: null,
//xDateFormat: '%A, %b %e, %Y',
//valuePrefix: '',
//ySuffix: ''
//}
@@ -1533,13 +1529,14 @@
day: '%A, %b %e, %Y',
week: 'Week from %A, %b %e, %Y',
month: '%B %Y',
year: '%Y'
},
+ footerFormat: '',
//formatter: defaultFormatter,
headerFormat: '<span style="font-size: 10px">{point.key}</span><br/>',
- pointFormat: '<span style="color:{series.color}">\u25CF</span> {series.name}: <b>{point.y}</b><br/>',
+ pointFormat: '<span style="color:{point.color}">\u25CF</span> {series.name}: <b>{point.y}</b><br/>',
shadow: true,
//shape: 'callout',
//shared: false,
snap: isTouchDevice ? 25 : 10,
style: {
@@ -1588,26 +1585,35 @@
/**
* Set the time methods globally based on the useUTC option. Time method can be either
* local time or UTC (default).
*/
function setTimeMethods() {
- var useUTC = defaultOptions.global.useUTC,
+ var globalOptions = defaultOptions.global,
+ useUTC = globalOptions.useUTC,
GET = useUTC ? 'getUTC' : 'get',
SET = useUTC ? 'setUTC' : 'set';
- Date = defaultOptions.global.Date || window.Date;
- timezoneOffset = ((useUTC && defaultOptions.global.timezoneOffset) || 0) * 60000;
- makeTime = useUTC ? Date.UTC : function (year, month, date, hours, minutes, seconds) {
- return new Date(
- year,
- month,
- pick(date, 1),
- pick(hours, 0),
- pick(minutes, 0),
- pick(seconds, 0)
- ).getTime();
+ Date = globalOptions.Date || window.Date;
+ timezoneOffset = useUTC && globalOptions.timezoneOffset;
+ getTimezoneOffset = useUTC && globalOptions.getTimezoneOffset;
+ makeTime = function (year, month, date, hours, minutes, seconds) {
+ var d;
+ if (useUTC) {
+ d = Date.UTC.apply(0, arguments);
+ d += getTZOffset(d);
+ } else {
+ d = new Date(
+ year,
+ month,
+ pick(date, 1),
+ pick(hours, 0),
+ pick(minutes, 0),
+ pick(seconds, 0)
+ ).getTime();
+ }
+ return d;
};
getMinutes = GET + 'Minutes';
getHours = GET + 'Hours';
getDay = GET + 'Day';
getDate = GET + 'Date';
@@ -1776,11 +1782,11 @@
// Default base for animation
opacity: 1,
// For labels, these CSS properties are applied to the <text> node directly
textProps: ['fontSize', 'fontWeight', 'fontFamily', 'color',
- 'lineHeight', 'width', 'textDecoration', 'textShadow', 'HcTextStroke'],
+ 'lineHeight', 'width', 'textDecoration', 'textShadow'],
/**
* Initialize the SVG renderer
* @param {Object} renderer
* @param {String} nodeName
@@ -1920,10 +1926,94 @@
elem.setAttribute(prop, 'url(' + renderer.url + '#' + id + ')');
}
},
/**
+ * Apply a polyfill to the text-stroke CSS property, by copying the text element
+ * and apply strokes to the copy.
+ *
+ * docs: update default, document the polyfill and the limitations on hex colors and pixel values, document contrast pseudo-color
+ * TODO:
+ * - update defaults
+ */
+ applyTextShadow: function (textShadow) {
+ var elem = this.element,
+ tspans,
+ hasContrast = textShadow.indexOf('contrast') !== -1,
+ // Safari suffers from the double display bug (#3649)
+ isSafari = userAgent.indexOf('Safari') > 0 && userAgent.indexOf('Chrome') === -1,
+ // IE10 and IE11 report textShadow in elem.style even though it doesn't work. Check
+ // this again with new IE release.
+ supports = elem.style.textShadow !== UNDEFINED && !isIE && !isSafari;
+
+ // When the text shadow is set to contrast, use dark stroke for light text and vice versa
+ if (hasContrast) {
+ textShadow = textShadow.replace(/contrast/g, this.renderer.getContrast(elem.style.fill));
+ }
+
+ /* 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;
+ }
+ // */
+
+ // No reason to polyfill, we've got native support
+ if (supports) {
+ if (hasContrast) { // Apply the altered style
+ css(elem, {
+ textShadow: textShadow
+ });
+ }
+ } else {
+
+ // In order to get the right y position of the clones,
+ // copy over the y setter
+ this.ySetter = this.xSetter;
+
+ tspans = [].slice.call(elem.getElementsByTagName('tspan'));
+ each(textShadow.split(/\s?,\s?/g), function (textShadow) {
+ var firstChild = elem.firstChild,
+ color,
+ strokeWidth;
+
+ textShadow = textShadow.split(' ');
+ color = textShadow[textShadow.length - 1];
+
+ // Approximately tune the settings to the text-shadow behaviour
+ strokeWidth = textShadow[textShadow.length - 2];
+
+ if (strokeWidth) {
+ 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, {
+ 'stroke': color,
+ 'stroke-opacity': 1 / mathMax(pInt(strokeWidth), 3),
+ 'stroke-width': strokeWidth,
+ 'stroke-linejoin': 'round'
+ });
+ elem.insertBefore(clone, firstChild);
+ });
+ }
+ });
+ }
+ },
+
+ /**
* Set or get a given attribute
* @param {Object|String} hash
* @param {Mixed|Undefined} val
*/
attr: function (hash, val) {
@@ -2118,11 +2208,13 @@
hasNew = true;
}
}
}
if (hasNew) {
- textWidth = elemWrapper.textWidth = styles && styles.width && elem.nodeName.toLowerCase() === 'text' && pInt(styles.width);
+ textWidth = elemWrapper.textWidth =
+ (styles && styles.width && elem.nodeName.toLowerCase() === 'text' && pInt(styles.width)) ||
+ elemWrapper.textWidth; // #3501
// Merge the new styles with the old ones
if (oldStyles) {
styles = extend(
oldStyles,
@@ -2248,10 +2340,13 @@
// apply rotation
if (inverted) {
transform.push('rotate(90) scale(-1,1)');
} else if (rotation) { // text rotation
transform.push('rotate(' + rotation + ' ' + (element.getAttribute('x') || 0) + ' ' + (element.getAttribute('y') || 0) + ')');
+
+ // Delete bBox memo when the rotation changes
+ //delete wrapper.bBox;
}
// apply scale
if (defined(scaleX) || defined(scaleY)) {
transform.push('scale(' + pick(scaleX, 1) + ' ' + pick(scaleY, 1) + ')');
@@ -2276,13 +2371,13 @@
* 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.
+ * 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: function (alignOptions, alignByTranslate, box) {
var align,
vAlign,
@@ -2344,35 +2439,41 @@
},
/**
* Get the bounding box (width, height, x and y) for the element
*/
- getBBox: function () {
+ getBBox: function (reload) {
var wrapper = this,
- bBox = wrapper.bBox,
+ bBox,// = wrapper.bBox,
renderer = wrapper.renderer,
width,
height,
rotation = wrapper.rotation,
element = wrapper.element,
styles = wrapper.styles,
rad = rotation * deg2rad,
textStr = wrapper.textStr,
cacheKey;
- // 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.
- if (textStr === '' || numRegex.test(textStr)) {
- cacheKey = 'num.' + textStr.toString().length + (styles ? ('|' + styles.fontSize + '|' + styles.fontFamily) : '');
+ if (textStr !== UNDEFINED) {
- } //else { // This code block made demo/waterfall fail, related to buildText
- // Caching all strings reduces rendering time by 4-5%.
- // TODO: Check how this affects places where bBox is found on the element
- //cacheKey = textStr + (styles ? ('|' + styles.fontSize + '|' + styles.fontFamily) : '');
- //}
- if (cacheKey) {
+ // Properties that affect bounding box
+ cacheKey = ['', rotation || 0, styles && styles.fontSize, element.style.width].join(',');
+
+ // 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.
+ if (textStr === '' || numRegex.test(textStr)) {
+ cacheKey = 'num:' + textStr.toString().length + cacheKey;
+
+ // Caching all strings reduces rendering time by 4-5%.
+ } else {
+ cacheKey = textStr + cacheKey;
+ }
+ }
+
+ if (cacheKey && !reload) {
bBox = renderer.cache[cacheKey];
}
// No cache found
if (!bBox) {
@@ -2423,14 +2524,11 @@
bBox.height = mathAbs(height * mathCos(rad)) + mathAbs(width * mathSin(rad));
}
}
// Cache it
- wrapper.bBox = bBox;
- if (cacheKey) {
- renderer.cache[cacheKey] = bBox;
- }
+ renderer.cache[cacheKey] = bBox;
}
return bBox;
},
/**
@@ -2466,23 +2564,16 @@
},
/**
* Add the element
* @param {Object|Undefined} parent Can be an element, an element wrapper or undefined
- * to append the element to the renderer.box.
+ * to append the element to the renderer.box.
*/
add: function (parent) {
var renderer = this.renderer,
- parentWrapper = parent || renderer,
- parentNode = parentWrapper.element || renderer.box,
- childNodes,
element = this.element,
- zIndex = this.zIndex,
- otherElement,
- otherZIndex,
- i,
inserted;
if (parent) {
this.parentGroup = parent;
}
@@ -2493,44 +2584,24 @@
// build formatted text
if (this.textStr !== undefined) {
renderer.buildText(this);
}
- // mark the container as having z indexed children
- if (zIndex) {
- parentWrapper.handleZ = true;
- zIndex = pInt(zIndex);
- }
+ // Mark as added
+ this.added = true;
- // insert according to this and other elements' zIndex
- if (parentWrapper.handleZ) { // this element or any of its siblings has a z index
- childNodes = parentNode.childNodes;
- for (i = 0; i < childNodes.length; i++) {
- otherElement = childNodes[i];
- otherZIndex = attr(otherElement, 'zIndex');
- if (otherElement !== element && (
- // insert before the first element with a higher zIndex
- pInt(otherZIndex) > zIndex ||
- // if no zIndex given, insert before the first element with a zIndex
- (!defined(zIndex) && defined(otherZIndex))
-
- )) {
- parentNode.insertBefore(element, otherElement);
- inserted = true;
- break;
- }
- }
+ // If we're adding to renderer root, or other elements in the group
+ // have a z index, we need to handle it
+ if (!parent || parent.handleZ || this.zIndex) {
+ inserted = this.zIndexSetter();
}
- // default: append at the end
+ // If zIndex is not handled, append at the end
if (!inserted) {
- parentNode.appendChild(element);
+ (parent ? parent.element : renderer.box).appendChild(element);
}
- // mark as added
- this.added = true;
-
// fire an event for internal hooks
if (this.onAdd) {
this.onAdd();
}
@@ -2747,13 +2818,57 @@
element.setAttribute(key, value);
} else if (value) {
this.colorGradient(value, key, element);
}
},
- zIndexSetter: function (value, key, element) {
- element.setAttribute(key, value);
- this[key] = value;
+ zIndexSetter: function (value, key) {
+ var renderer = this.renderer,
+ parentGroup = this.parentGroup,
+ parentWrapper = parentGroup || renderer,
+ parentNode = parentWrapper.element || renderer.box,
+ childNodes,
+ otherElement,
+ otherZIndex,
+ element = this.element,
+ inserted,
+ i;
+
+ if (defined(value)) {
+ element.setAttribute(key, value); // So we can read it for other elements in the group
+ this[key] = +value;
+ }
+
+ // Insert according to this and other elements' zIndex. Before .add() is called,
+ // nothing is done. Then on add, or by later calls to zIndexSetter, the node
+ // is placed on the right place in the DOM.
+ if (this.added) {
+ value = this.zIndex;
+
+ if (value && parentGroup) {
+ parentGroup.handleZ = true;
+ }
+
+ childNodes = parentNode.childNodes;
+ for (i = 0; i < childNodes.length && !inserted; i++) {
+ otherElement = childNodes[i];
+ otherZIndex = attr(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))
+
+ )) {
+ parentNode.insertBefore(element, otherElement);
+ inserted = true;
+ }
+ }
+ if (!inserted) {
+ parentNode.appendChild(element);
+ }
+ }
+ return inserted;
},
_defaultSetter: function (value, key, element) {
element.setAttribute(key, value);
}
};
@@ -2951,42 +3066,47 @@
hrefRegex,
parentX = attr(textNode, 'x'),
textStyles = wrapper.styles,
width = wrapper.textWidth,
textLineHeight = textStyles && textStyles.lineHeight,
- textStroke = textStyles && textStyles.HcTextStroke,
+ textShadow = textStyles && textStyles.textShadow,
+ ellipsis = textStyles && textStyles.textOverflow === 'ellipsis',
i = childNodes.length,
+ tempParent = width && !wrapper.added && this.box,
getLineHeight = function (tspan) {
return textLineHeight ?
pInt(textLineHeight) :
renderer.fontMetrics(
/(px|em)$/.test(tspan && tspan.style.fontSize) ?
tspan.style.fontSize :
((textStyles && textStyles.fontSize) || renderer.style.fontSize || 12),
tspan
).h;
+ },
+ unescapeAngleBrackets = function (inputStr) {
+ return inputStr.replace(/</g, '<').replace(/>/g, '>');
};
/// remove old text
while (i--) {
textNode.removeChild(childNodes[i]);
}
// Skip tspans, add text directly to text node. The forceTSpan is a hook
// used in text outline hack.
- if (!hasMarkup && !textStroke && textStr.indexOf(' ') === -1) {
- textNode.appendChild(doc.createTextNode(textStr));
+ if (!hasMarkup && !textShadow && !ellipsis && textStr.indexOf(' ') === -1) {
+ textNode.appendChild(doc.createTextNode(unescapeAngleBrackets(textStr)));
return;
// Complex strings, add more logic
} else {
styleRegex = /<.*style="([^"]+)".*>/;
hrefRegex = /<.*href="(http[^"]+)".*>/;
- if (width && !wrapper.added) {
- this.box.appendChild(textNode); // attach it to the DOM to read offset width
+ if (tempParent) {
+ tempParent.appendChild(textNode); // attach it to the DOM to read offset width
}
if (hasMarkup) {
lines = textStr
.replace(/<(b|strong)>/g, '<span style="font-weight:bold">')
@@ -3025,13 +3145,11 @@
if (hrefRegex.test(span) && !forExport) { // Not for export - #1529
attr(tspan, 'onclick', 'location.href=\"' + span.match(hrefRegex)[1] + '\"');
css(tspan, { cursor: 'pointer' });
}
- span = (span.replace(/<(.|\n)*?>/g, '') || ' ')
- .replace(/</g, '<')
- .replace(/>/g, '>');
+ span = unescapeAngleBrackets(span.replace(/<(.|\n)*?>/g, '') || ' ');
// Nested tags aren't supported, and cause crash in Safari (#1596)
if (span !== ' ') {
// add the text node
@@ -3066,53 +3184,77 @@
'dy',
getLineHeight(tspan)
);
}
- // check width and apply soft breaks
+ /*if (width) {
+ renderer.breakText(wrapper, width);
+ }*/
+
+ // Check width and apply soft breaks or ellipsis
if (width) {
var words = span.replace(/([^\^])-/g, '$1- ').split(' '), // #1273
- hasWhiteSpace = spans.length > 1 || (words.length > 1 && textStyles.whiteSpace !== 'nowrap'),
+ hasWhiteSpace = spans.length > 1 || lineNo || (words.length > 1 && textStyles.whiteSpace !== 'nowrap'),
tooLong,
+ wasTooLong,
actualWidth,
- hcHeight = textStyles.HcHeight,
rest = [],
dy = getLineHeight(tspan),
softLineNo = 1,
+ rotation = wrapper.rotation,
+ wordStr = span, // for ellipsis
+ cursor = wordStr.length, // binary search cursor
bBox;
- while (hasWhiteSpace && (words.length || rest.length)) {
- delete wrapper.bBox; // delete cache
- bBox = wrapper.getBBox();
+ while ((hasWhiteSpace || ellipsis) && (words.length || rest.length)) {
+ wrapper.rotation = 0; // discard rotation when computing box
+ bBox = wrapper.getBBox(true);
actualWidth = bBox.width;
// Old IE cannot measure the actualWidth for SVG elements (#2314)
if (!hasSVG && renderer.forExport) {
actualWidth = renderer.measureSpanWidth(tspan.firstChild.data, wrapper.styles);
}
tooLong = actualWidth > width;
- if (!tooLong || words.length === 1) { // new line needed
+
+ // For ellipsis, do a binary search for the correct string length
+ if (wasTooLong === undefined) {
+ wasTooLong = tooLong; // First time
+ }
+ if (ellipsis && wasTooLong) {
+ cursor /= 2;
+
+ if (wordStr === '' || (!tooLong && cursor < 0.5)) {
+ words = []; // All ok, break out
+ } else {
+ if (tooLong) {
+ wasTooLong = true;
+ }
+ wordStr = span.substring(0, wordStr.length + (tooLong ? -1 : 1) * mathCeil(cursor));
+ words = [wordStr + '\u2026'];
+ tspan.removeChild(tspan.firstChild);
+ }
+
+ // Looping down, this is the first word sequence that is not too long,
+ // so we can move on to build the next line.
+ } else if (!tooLong || words.length === 1) {
words = rest;
rest = [];
+
if (words.length) {
softLineNo++;
- if (hcHeight && softLineNo * dy > hcHeight) {
- words = ['...'];
- wrapper.attr('title', wrapper.textStr);
- } else {
-
- tspan = doc.createElementNS(SVG_NS, 'tspan');
- attr(tspan, {
- dy: dy,
- x: parentX
- });
- if (spanStyle) { // #390
- attr(tspan, 'style', spanStyle);
- }
- textNode.appendChild(tspan);
+
+ tspan = doc.createElementNS(SVG_NS, 'tspan');
+ attr(tspan, {
+ dy: dy,
+ x: parentX
+ });
+ if (spanStyle) { // #390
+ attr(tspan, 'style', spanStyle);
}
+ textNode.appendChild(tspan);
}
if (actualWidth > width) { // a single word is pressing it out
width = actualWidth;
}
} else { // append to existing line tspan
@@ -3121,20 +3263,75 @@
}
if (words.length) {
tspan.appendChild(doc.createTextNode(words.join(' ').replace(/- /g, '-')));
}
}
+ if (wasTooLong) {
+ wrapper.attr('title', wrapper.textStr);
+ }
+ wrapper.rotation = rotation;
}
spanNo++;
}
}
});
});
+ 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);
+ }
}
},
+
+
+ /*
+ breakText: function (wrapper, width) {
+ var bBox = wrapper.getBBox(),
+ node = wrapper.element,
+ textLength = node.textContent.length,
+ pos = mathRound(width * textLength / bBox.width), // try this position first, based on average character width
+ increment = 0,
+ finalPos;
+
+ if (bBox.width > width) {
+ while (finalPos === undefined) {
+ textLength = node.getSubStringLength(0, pos);
+
+ if (textLength <= width) {
+ if (increment === -1) {
+ finalPos = pos;
+ } else {
+ increment = 1;
+ }
+ } else {
+ if (increment === 1) {
+ finalPos = pos - 1;
+ } else {
+ increment = -1;
+ }
+ }
+ pos += increment;
+ }
+ }
+ console.log(finalPos, node.getSubStringLength(0, finalPos))
+ },
+ */
+
+ /**
+ * Returns white for dark colors and black for bright colors
+ */
+ getContrast: function (color) {
+ color = Color(color).rgba;
+ return color[0] + color[1] + color[2] > 384 ? '#000' : '#FFF';
+ },
+
/**
* Create a button with preset states
* @param {String} text
* @param {Number} x
* @param {Number} y
@@ -3409,11 +3606,11 @@
},
/**
* 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.
+ * This can be used for styling and scripting.
*/
g: function (name) {
var elem = this.createElement('g');
return defined(name) ? elem.attr({ 'class': PREFIX + name }) : elem;
},
@@ -3721,10 +3918,11 @@
}).add(this.defs);
wrapper = this.rect(x, y, width, height, 0).add(clipPath);
wrapper.id = id;
wrapper.clipPath = clipPath;
+ wrapper.count = 0;
return wrapper;
},
@@ -3799,32 +3997,46 @@
}
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/
- var lineHeight = fontSize < 24 ? fontSize + 4 : mathRound(fontSize * 1.2),
+ var lineHeight = fontSize < 24 ? fontSize + 3 : mathRound(fontSize * 1.2),
baseline = mathRound(lineHeight * 0.8);
return {
h: lineHeight,
b: baseline,
f: fontSize
};
},
/**
+ * Correct X and Y positioning of a label for rotation (#1764)
+ */
+ rotCorr: function (baseline, rotation, alterY) {
+ var y = baseline;
+ if (rotation && alterY) {
+ y = mathMax(y * mathCos(rotation * deg2rad), 4);
+ }
+ return {
+ x: (-baseline / 3) * mathSin(rotation * deg2rad),
+ y: y
+ };
+ },
+
+ /**
* Add a label, a text item that can hold a colored or gradient background
* as well as a border and shadow.
* @param {string} str
* @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
- * coordinates it should be pinned to
+ * coordinates it should be pinned to
* @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.
+ * 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) {
var renderer = this,
@@ -3856,12 +4068,12 @@
function updateBoxSize() {
var boxX,
boxY,
style = text.element.style;
- bBox = (width === undefined || height === undefined || wrapper.styles.textAlign) && text.textStr &&
- text.getBBox();
+ bBox = (width === undefined || height === undefined || wrapper.styles.textAlign) && defined(text.textStr) &&
+ text.getBBox(); //#3295 && 3514 box failure when string equals 0
wrapper.width = (width || bBox.width || 0) + 2 * padding + paddingLeft;
wrapper.height = (height || bBox.height || 0) + 2 * padding;
// update the label-scoped y offset
baselineOffset = padding + renderer.fontMetrics(style && style.fontSize, text).b;
@@ -3965,11 +4177,11 @@
wrapper.heightSetter = function (value) {
height = value;
};
wrapper.paddingSetter = function (value) {
if (defined(value) && value !== padding) {
- padding = value;
+ padding = wrapper.padding = value;
updateTextPadding();
}
};
wrapper.paddingLeftSetter = function (value) {
if (defined(value) && value !== paddingLeft) {
@@ -4112,11 +4324,14 @@
if (textWidth) {
delete styles.width;
wrapper.textWidth = textWidth;
wrapper.updateTransform();
}
-
+ if (styles && styles.textOverflow === 'ellipsis') {
+ styles.whiteSpace = 'nowrap';
+ styles.overflow = 'hidden';
+ }
wrapper.styles = extend(wrapper.styles, styles);
css(wrapper.element, styles);
return wrapper;
},
@@ -4129,29 +4344,24 @@
* @return {Object} A hash containing values for x, y, width and height
*/
htmlGetBBox: function () {
var wrapper = this,
- element = wrapper.element,
- bBox = wrapper.bBox;
+ element = wrapper.element;
// faking getBBox in exported SVG in legacy IE
- if (!bBox) {
- // faking getBBox in exported SVG in legacy IE (is this a duplicate of the fix for #1079?)
- if (element.nodeName === 'text') {
- element.style.position = ABSOLUTE;
- }
-
- bBox = wrapper.bBox = {
- x: element.offsetLeft,
- y: element.offsetTop,
- width: element.offsetWidth,
- height: element.offsetHeight
- };
+ // faking getBBox in exported SVG in legacy IE (is this a duplicate of the fix for #1079?)
+ if (element.nodeName === 'text') {
+ element.style.position = ABSOLUTE;
}
- return bBox;
+ return {
+ x: element.offsetLeft,
+ y: element.offsetTop,
+ width: element.offsetWidth,
+ height: element.offsetHeight
+ };
},
/**
* VML override private method to update elements based on internal
* properties based on SVG transform
@@ -4170,11 +4380,12 @@
translateY = wrapper.translateY || 0,
x = wrapper.x || 0,
y = wrapper.y || 0,
align = wrapper.textAlign || 'left',
alignCorrection = { left: 0, center: 0.5, right: 1 }[align],
- shadows = wrapper.shadows;
+ shadows = wrapper.shadows,
+ styles = wrapper.styles;
// apply translate
css(elem, {
marginLeft: translateX,
marginTop: translateY
@@ -4218,11 +4429,11 @@
// Update textWidth
if (width > textWidth && /[ \-]/.test(elem.textContent || elem.innerText)) { // #983, #1254
css(elem, {
width: textWidth + PX,
display: 'block',
- whiteSpace: 'normal'
+ whiteSpace: (styles && styles.whiteSpace) || 'normal' // #3331
});
width = textWidth;
}
wrapper.getSpanCorrection(width, baseline, alignCorrection, rotation, align);
@@ -4303,15 +4514,17 @@
x: mathRound(x),
y: mathRound(y)
})
.css({
position: ABSOLUTE,
- whiteSpace: 'nowrap',
fontFamily: this.style.fontFamily,
fontSize: this.style.fontSize
});
+ // Keep the whiteSpace style outside the wrapper.styles collection
+ element.style.whiteSpace = 'nowrap';
+
// Use the HTML specific .css method
wrapper.css = wrapper.htmlCss;
// This is specific for HTML within SVG
if (renderer.isSVG) {
@@ -4967,10 +5180,11 @@
isObj = isObject(x);
// mimic a rectangle with its style object for automatic updating in attr
return extend(clipRect, {
members: [],
+ count: 0,
left: (isObj ? x.x : x) + 1,
top: (isObj ? x.y : y) + 1,
width: (isObj ? x.width : width) - 1,
height: (isObj ? x.height : height) - 1,
getCSS: function (wrapper) {
@@ -5551,27 +5765,18 @@
addLabel: function () {
var tick = this,
axis = tick.axis,
options = axis.options,
chart = axis.chart,
- horiz = axis.horiz,
categories = axis.categories,
names = axis.names,
pos = tick.pos,
labelOptions = options.labels,
- rotation = labelOptions.rotation,
str,
tickPositions = axis.tickPositions,
- width = (horiz && categories &&
- !labelOptions.step && !labelOptions.staggerLines &&
- !labelOptions.rotation &&
- chart.plotWidth / tickPositions.length) ||
- (!horiz && (chart.margin[3] || chart.chartWidth * 0.33)), // #1580, #1931
isFirst = pos === tickPositions[0],
isLast = pos === tickPositions[tickPositions.length - 1],
- css,
- attr,
value = categories ?
pick(categories[pos], names[pos], pos) :
pos,
label = tick.label,
tickPositionInfo = tickPositions.info,
@@ -5595,166 +5800,100 @@
dateTimeLabelFormat: dateTimeLabelFormat,
value: axis.isLog ? correctFloat(lin2log(value)) : value
});
// prepare CSS
- css = width && { width: mathMax(1, mathRound(width - 2 * (labelOptions.padding || 10))) + PX };
+ //css = width && { width: mathMax(1, mathRound(width - 2 * (labelOptions.padding || 10))) + PX };
// first call
if (!defined(label)) {
- attr = {
- align: axis.labelAlign
- };
- if (isNumber(rotation)) {
- attr.rotation = rotation;
- }
- if (width && labelOptions.ellipsis) {
- css.HcHeight = axis.len / tickPositions.length;
- }
tick.label = label =
defined(str) && labelOptions.enabled ?
chart.renderer.text(
str,
0,
0,
labelOptions.useHTML
)
- .attr(attr)
+ //.attr(attr)
// without position absolute, IE export sometimes is wrong
- .css(extend(css, labelOptions.style))
+ .css(merge(labelOptions.style))
.add(axis.labelGroup) :
null;
+ tick.labelLength = label && label.getBBox().width; // Un-rotated length
+ tick.rotation = 0; // Base value to detect change for new calls to getBBox
- // Set the tick baseline and correct for rotation (#1764)
- axis.tickBaseline = chart.renderer.fontMetrics(labelOptions.style.fontSize, label).b;
- if (rotation && axis.side === 2) {
- axis.tickBaseline *= mathCos(rotation * deg2rad);
- }
-
-
// update
} else if (label) {
- label.attr({
- text: str
- })
- .css(css);
+ label.attr({ text: str });
}
- tick.yOffset = label ? pick(labelOptions.y, axis.tickBaseline + (axis.side === 2 ? 8 : -(label.getBBox().height / 2))) : 0;
},
/**
* Get the offset height or width of the label
*/
getLabelSize: function () {
- var label = this.label,
- axis = this.axis;
- return label ?
- label.getBBox()[axis.horiz ? 'height' : 'width'] :
+ return this.label ?
+ this.label.getBBox()[this.axis.horiz ? 'height' : 'width'] :
0;
},
/**
- * Find how far the labels extend to the right and left of the tick's x position. Used for anti-collision
- * detection with overflow logic.
- */
- getLabelSides: function () {
- var bBox = this.label.getBBox(),
- axis = this.axis,
- horiz = axis.horiz,
- options = axis.options,
- labelOptions = options.labels,
- size = horiz ? bBox.width : bBox.height,
- leftSide = horiz ?
- labelOptions.x - size * { left: 0, center: 0.5, right: 1 }[axis.labelAlign] :
- 0,
- rightSide = horiz ?
- size + leftSide :
- size;
-
- return [leftSide, rightSide];
- },
-
- /**
* Handle the label overflow by adjusting the labels to the left and right edge, or
* hide them if they collide into the neighbour label.
*/
- handleOverflow: function (index, xy) {
- var show = true,
- axis = this.axis,
- isFirst = this.isFirst,
- isLast = this.isLast,
- horiz = axis.horiz,
- pxPos = horiz ? xy.x : xy.y,
- reversed = axis.reversed,
- tickPositions = axis.tickPositions,
- sides = this.getLabelSides(),
- leftSide = sides[0],
- rightSide = sides[1],
- axisLeft,
- axisRight,
- neighbour,
- neighbourEdge,
- line = this.label.line,
- lineIndex = line || 0,
- labelEdge = axis.labelEdge,
- justifyLabel = axis.justifyLabels && (isFirst || isLast),
- justifyToPlot;
+ handleOverflow: function (xy) {
+ var axis = this.axis,
+ pxPos = xy.x,
+ chartWidth = axis.chart.chartWidth,
+ spacing = axis.chart.spacing,
+ leftBound = pick(axis.labelLeft, spacing[3]),
+ rightBound = pick(axis.labelRight, chartWidth - spacing[1]),
+ label = this.label,
+ rotation = this.rotation,
+ factor = { left: 0, center: 0.5, right: 1 }[axis.labelAlign],
+ labelWidth = label.getBBox().width,
+ slotWidth = axis.slotWidth,
+ leftPos,
+ rightPos,
+ textWidth;
- // Hide it if it now overlaps the neighbour label
- if (labelEdge[lineIndex] === UNDEFINED || pxPos + leftSide > labelEdge[lineIndex]) {
- labelEdge[lineIndex] = pxPos + rightSide;
+ // Check if the label overshoots the chart spacing box. If it does, move it.
+ // If it now overshoots the slotWidth, add ellipsis.
+ if (!rotation) {
+ leftPos = pxPos - factor * labelWidth;
+ rightPos = pxPos + factor * labelWidth;
- } else if (!justifyLabel) {
- show = false;
- }
+ if (leftPos < leftBound) {
+ slotWidth -= leftBound - leftPos;
+ xy.x = leftBound;
+ label.attr({ align: 'left' });
+ } else if (rightPos > rightBound) {
+ slotWidth -= rightPos - rightBound;
+ xy.x = rightBound;
+ label.attr({ align: 'right' });
+ }
- if (justifyLabel) {
- justifyToPlot = axis.justifyToPlot;
- axisLeft = justifyToPlot ? axis.pos : 0;
- axisRight = justifyToPlot ? axisLeft + axis.len : axis.chart.chartWidth;
-
- // Find the firsth neighbour on the same line
- do {
- index += (isFirst ? 1 : -1);
- neighbour = axis.ticks[tickPositions[index]];
- } while (tickPositions[index] && (!neighbour || !neighbour.label || neighbour.label.line !== line)); // #3044
-
- neighbourEdge = neighbour && neighbour.label.xy && neighbour.label.xy.x + neighbour.getLabelSides()[isFirst ? 0 : 1];
-
- if ((isFirst && !reversed) || (isLast && reversed)) {
- // Is the label spilling out to the left of the plot area?
- if (pxPos + leftSide < axisLeft) {
-
- // Align it to plot left
- pxPos = axisLeft - leftSide;
-
- // Hide it if it now overlaps the neighbour label
- if (neighbour && pxPos + rightSide > neighbourEdge) {
- show = false;
- }
- }
-
- } else {
- // Is the label spilling out to the right of the plot area?
- if (pxPos + rightSide > axisRight) {
-
- // Align it to plot right
- pxPos = axisRight - rightSide;
-
- // Hide it if it now overlaps the neighbour label
- if (neighbour && pxPos + leftSide < neighbourEdge) {
- show = false;
- }
-
- }
+ if (labelWidth > slotWidth) {
+ textWidth = slotWidth;
}
+
- // Set the modified x position of the label
- xy.x = pxPos;
+ // Add ellipsis to prevent rotated labels to be clipped against the edge of the chart
+ } else if (rotation < 0 && pxPos - factor * labelWidth < leftBound) {
+ textWidth = mathRound(pxPos / mathCos(rotation * deg2rad) - leftBound);
+ } else if (rotation > 0 && pxPos + factor * labelWidth > rightBound) {
+ textWidth = mathRound((chartWidth - pxPos) / mathCos(rotation * deg2rad));
}
- return show;
+
+ if (textWidth) {
+ label.css({
+ width: textWidth,
+ textOverflow: 'ellipsis'
+ });
+ }
},
/**
* Get the x and y position for ticks and labels
*/
@@ -5780,26 +5919,29 @@
*/
getLabelPosition: function (x, y, label, horiz, labelOptions, tickmarkOffset, index, step) {
var axis = this.axis,
transA = axis.transA,
reversed = axis.reversed,
- staggerLines = axis.staggerLines;
+ staggerLines = axis.staggerLines,
+ rotCorr = axis.tickRotCorr || { x: 0, y: 0 },
+ yOffset = pick(labelOptions.y, rotCorr.y + (axis.side === 2 ? 8 : -(label.getBBox().height / 2))),
+ line;
- x = x + labelOptions.x - (tickmarkOffset && horiz ?
+ x = x + labelOptions.x + rotCorr.x - (tickmarkOffset && horiz ?
tickmarkOffset * transA * (reversed ? -1 : 1) : 0);
- y = y + this.yOffset - (tickmarkOffset && !horiz ?
+ y = y + yOffset - (tickmarkOffset && !horiz ?
tickmarkOffset * transA * (reversed ? 1 : -1) : 0);
// Correct for staggered labels
if (staggerLines) {
- label.line = (index / (step || 1) % staggerLines);
- y += label.line * (axis.labelOffset / staggerLines);
+ line = (index / (step || 1) % staggerLines);
+ y += line * (axis.labelOffset / staggerLines);
}
return {
x: x,
- y: y
+ y: mathRound(y)
};
},
/**
* Extendible method to return the path of the marker
@@ -5843,11 +5985,11 @@
tickColor = options[tickPrefix + 'Color'],
tickPosition = options[tickPrefix + 'Position'],
gridLinePath,
mark = tick.mark,
markPath,
- step = labelOptions.step,
+ step = /*axis.labelStep || */labelOptions.step,
attribs,
show = true,
tickmarkOffset = axis.tickmarkOffset,
xy = tick.getPosition(horiz, pos, tickmarkOffset, old),
x = xy.x,
@@ -5929,12 +6071,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 (!axis.isRadial && !labelOptions.step && !labelOptions.rotation && !old && opacity !== 0) {
- show = tick.handleOverflow(index, xy);
+ } else if (horiz && !axis.isRadial && !labelOptions.step && !labelOptions.rotation && !old && opacity !== 0) {
+ tick.handleOverflow(xy);
}
// apply step
if (step && index % step) {
// show those indices dividable by step
@@ -5981,11 +6123,10 @@
*/
render: function () {
var plotLine = this,
axis = plotLine.axis,
horiz = axis.horiz,
- halfPointRange = (axis.pointRange || 0) / 2,
options = plotLine.options,
optionsLabel = options.label,
label = plotLine.label,
width = options.width,
to = options.to,
@@ -6023,15 +6164,11 @@
};
if (dashStyle) {
attribs.dashstyle = dashStyle;
}
} else if (isBand) { // plot band
-
- // keep within plot area
- from = mathMax(from, axis.min - halfPointRange);
- to = mathMin(to, axis.max + halfPointRange);
-
+
path = axis.getPlotBandPath(from, to, options);
if (color) {
attribs.fill = color;
}
if (options.borderWidth) {
@@ -6152,12 +6289,12 @@
/**
* Create the path for a plot band
*/
getPlotBandPath: function (from, to) {
- var toPath = this.getPlotLinePath(to),
- path = this.getPlotLinePath(from);
+ var toPath = this.getPlotLinePath(to, null, null, true),
+ path = this.getPlotLinePath(from, null, null, true);
if (path && toPath) {
path.push(
toPath[4],
toPath[5],
@@ -6228,13 +6365,13 @@
/**
* Create a new axis object
* @param {Object} chart
* @param {Object} options
*/
-function Axis() {
+var Axis = Highcharts.Axis = function () {
this.init.apply(this, arguments);
-}
+};
Axis.prototype = {
/**
* Default options for the X axis - the Y axis has extended defaults
@@ -6252,17 +6389,31 @@
week: '%e. %b',
month: '%b \'%y',
year: '%Y'
},
endOnTick: false,
- gridLineColor: '#C0C0C0',
+ gridLineColor: '#D8D8D8',
// gridLineDashStyle: 'solid',
// gridLineWidth: 0,
// reversed: false,
- labels: defaultLabelOptions,
- // { step: null },
+ labels: {
+ enabled: true,
+ // rotation: 0,
+ // align: 'center',
+ // step: null,
+ style: {
+ color: '#606060',
+ cursor: 'default',
+ fontSize: '11px'
+ },
+ x: 0,
+ y: 15
+ /*formatter: function () {
+ return this.value;
+ },*/
+ },
lineColor: '#C0D0E0',
lineWidth: 1,
//linkedTo: null,
//max: undefined,
//min: undefined,
@@ -6345,13 +6496,13 @@
//x: dynamic,
//verticalAlign: dynamic,
//textAlign: dynamic,
//rotation: 0,
formatter: function () {
- return numberFormat(this.total, -1);
+ return Highcharts.numberFormat(this.total, -1);
},
- style: defaultLabelOptions.style
+ style: defaultPlotOptions.line.dataLabels.style
}
},
/**
* These options extend the defaultOptions for left axes
@@ -6382,24 +6533,26 @@
/**
* These options extend the defaultOptions for bottom axes
*/
defaultBottomAxisOptions: {
labels: {
+ autoRotation: [-45],
x: 0,
y: null // based on font size
// overflow: undefined,
// staggerLines: null
},
title: {
rotation: 0
}
},
/**
- * These options extend the defaultOptions for left axes
+ * These options extend the defaultOptions for top axes
*/
defaultTopAxisOptions: {
labels: {
+ autoRotation: [-45],
x: 0,
y: -15
// overflow: undefined
// staggerLines: null
},
@@ -6474,19 +6627,16 @@
//axis.tickPositions = UNDEFINED; // array containing predefined positions
// Tick intervals
//axis.tickInterval = UNDEFINED;
//axis.minorTickInterval = UNDEFINED;
- axis.tickmarkOffset = (axis.categories && options.tickmarkPlacement === 'between' &&
- pick(options.tickInterval, 1) === 1) ? 0.5 : 0; // #3202
-
+
// Major ticks
axis.ticks = {};
axis.labelEdge = [];
// Minor ticks
axis.minorTicks = {};
- //axis.tickAmount = UNDEFINED;
// List of plotLines/Bands
axis.plotLinesAndBands = [];
// Alternate bands
@@ -6617,21 +6767,21 @@
// 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);
if (numericSymbolDetector >= multi && numericSymbols[i] !== null) {
- ret = numberFormat(value / multi, -1) + numericSymbols[i];
+ ret = Highcharts.numberFormat(value / multi, -1) + numericSymbols[i];
}
}
}
if (ret === UNDEFINED) {
if (mathAbs(value) >= 10000) { // add thousands separators
- ret = numberFormat(value, 0);
+ ret = Highcharts.numberFormat(value, 0);
} else { // small numbers
- ret = numberFormat(value, -1, UNDEFINED, ''); // #2466
+ ret = Highcharts.numberFormat(value, -1, UNDEFINED, ''); // #2466
}
}
return ret;
},
@@ -6719,11 +6869,11 @@
cvsOffset = 0,
localA = old ? axis.oldTransA : axis.transA,
localMin = old ? axis.oldMin : axis.min,
returnValue,
minPixelPadding = axis.minPixelPadding,
- postTranslate = (axis.options.ordinal || (axis.isLog && handleLog)) && axis.lin2val;
+ postTranslate = (axis.postTranslate || (axis.isLog && handleLog)) && axis.lin2val;
if (!localA) {
localA = axis.transA;
}
@@ -6802,11 +6952,25 @@
x2,
y2,
cHeight = (old && chart.oldChartHeight) || chart.chartHeight,
cWidth = (old && chart.oldChartWidth) || chart.chartWidth,
skip,
- transB = axis.transB;
+ transB = axis.transB,
+ /**
+ * Check if x is between a and b. If not, either move to a/b or skip,
+ * depending on the force parameter.
+ */
+ between = function (x, a, b) {
+ if (x < a || x > b) {
+ if (force) {
+ x = mathMin(mathMax(a, x), b);
+ } else {
+ skip = true;
+ }
+ }
+ return x;
+ };
translatedValue = pick(translatedValue, axis.translate(value, null, null, old));
x1 = x2 = mathRound(translatedValue + transB);
y1 = y2 = mathRound(cHeight - translatedValue - transB);
@@ -6814,20 +6978,15 @@
skip = true;
} else if (axis.horiz) {
y1 = axisTop;
y2 = cHeight - axis.bottom;
- if (x1 < axisLeft || x1 > axisLeft + axis.width) {
- skip = true;
- }
+ x1 = x2 = between(x1, axisLeft, axisLeft + axis.width);
} else {
x1 = axisLeft;
x2 = cWidth - axis.right;
-
- if (y1 < axisTop || y1 > axisTop + axis.height) {
- skip = true;
- }
+ y1 = y2 = between(y1, axisTop, axisTop + axis.height);
}
return skip && !force ?
null :
chart.renderer.crispLine([M, x1, y1, L, x2, y2], lineWidth || 1);
},
@@ -6879,36 +7038,51 @@
tickPositions = axis.tickPositions,
minorTickInterval = axis.minorTickInterval,
minorTickPositions = [],
pos,
i,
+ min = axis.min,
+ max = axis.max,
len;
- if (axis.isLog) {
- len = tickPositions.length;
- for (i = 1; i < len; i++) {
+ // If minor ticks get too dense, they are hard to read, and may cause long running script. So we don't draw them.
+ if ((max - min) / minorTickInterval < axis.len / 3) {
+
+ if (axis.isLog) {
+ len = tickPositions.length;
+ for (i = 1; i < len; i++) {
+ minorTickPositions = minorTickPositions.concat(
+ axis.getLogTickPositions(minorTickInterval, tickPositions[i - 1], tickPositions[i], true)
+ );
+ }
+ } else if (axis.isDatetimeAxis && options.minorTickInterval === 'auto') { // #1314
minorTickPositions = minorTickPositions.concat(
- axis.getLogTickPositions(minorTickInterval, tickPositions[i - 1], tickPositions[i], true)
+ axis.getTimeTicks(
+ axis.normalizeTimeTickInterval(minorTickInterval),
+ min,
+ max,
+ options.startOfWeek
+ )
);
+
+ } else if (axis.isDatetimeAxis && options.minorTickInterval === 'auto') { // #1314
+ minorTickPositions = minorTickPositions.concat(
+ axis.getTimeTicks(
+ axis.normalizeTimeTickInterval(minorTickInterval),
+ axis.min,
+ axis.max,
+ options.startOfWeek
+ )
+ );
+ } else {
+ for (pos = axis.min + (tickPositions[0] - axis.min) % minorTickInterval; pos <= axis.max; pos += minorTickInterval) {
+ minorTickPositions.push(pos);
+ }
}
- } else if (axis.isDatetimeAxis && options.minorTickInterval === 'auto') { // #1314
- minorTickPositions = minorTickPositions.concat(
- axis.getTimeTicks(
- axis.normalizeTimeTickInterval(minorTickInterval),
- axis.min,
- axis.max,
- options.startOfWeek
- )
- );
- if (minorTickPositions[0] < axis.min) {
- minorTickPositions.shift();
- }
- } else {
- for (pos = axis.min + (tickPositions[0] - axis.min) % minorTickInterval; pos <= axis.max; pos += minorTickInterval) {
- minorTickPositions.push(pos);
- }
}
+
+ axis.trimTicks(minorTickPositions); // #3652 #3743
return minorTickPositions;
},
/**
* Adjust the min and max for the minimum range. Keep in mind that the series data is
@@ -7017,24 +7191,26 @@
if (seriesPointRange > range) { // #1446
seriesPointRange = 0;
}
pointRange = mathMax(pointRange, seriesPointRange);
- // minPointOffset is the value padding to the left of the axis in order to make
- // room for points with a pointRange, typically columns. When the pointPlacement option
- // is 'between' or 'on', this padding does not apply.
- minPointOffset = mathMax(
- minPointOffset,
- isString(pointPlacement) ? 0 : seriesPointRange / 2
- );
+ if (!axis.single) {
+ // minPointOffset is the value padding to the left of the axis in order to make
+ // room for points with a pointRange, typically columns. When the pointPlacement option
+ // is 'between' or 'on', this padding does not apply.
+ minPointOffset = mathMax(
+ minPointOffset,
+ isString(pointPlacement) ? 0 : seriesPointRange / 2
+ );
- // Determine the total padding needed to the length of the axis to make room for the
- // pointRange. If the series' pointPlacement is 'on', no padding is added.
- pointRangePadding = mathMax(
- pointRangePadding,
- pointPlacement === 'on' ? 0 : seriesPointRange
- );
+ // Determine the total padding needed to the length of the axis to make room for the
+ // pointRange. If the series' pointPlacement is 'on', no padding is added.
+ pointRangePadding = mathMax(
+ pointRangePadding,
+ pointPlacement === 'on' ? 0 : seriesPointRange
+ );
+ }
// Set the closestPointRange
if (!series.noSharedTooltip && defined(seriesClosestPointRange)) {
closestPointRange = defined(closestPointRange) ?
mathMin(closestPointRange, seriesClosestPointRange) :
@@ -7068,32 +7244,31 @@
/**
* Set the tick positions to round values and optionally extend the extremes
* to the nearest tick
*/
- setTickPositions: function (secondPass) {
+ setTickInterval: function (secondPass) {
var axis = this,
chart = axis.chart,
options = axis.options,
- startOnTick = options.startOnTick,
- endOnTick = options.endOnTick,
isLog = axis.isLog,
isDatetimeAxis = axis.isDatetimeAxis,
isXAxis = axis.isXAxis,
isLinked = axis.isLinked,
- tickPositioner = axis.options.tickPositioner,
maxPadding = options.maxPadding,
minPadding = options.minPadding,
length,
linkedParentExtremes,
tickIntervalOption = options.tickInterval,
- minTickIntervalOption = options.minTickInterval,
+ minTickInterval,
tickPixelIntervalOption = options.tickPixelInterval,
- tickPositions,
- keepTwoTicksOnly,
categories = axis.categories;
+ if (!isDatetimeAxis && !categories && !isLinked) {
+ this.getTickAmount();
+ }
+
// linked axis gets the extremes from the parent axis
if (isLinked) {
axis.linkedParent = chart[axis.coll][options.linkedTo];
linkedParentExtremes = axis.linkedParent.getExtremes();
axis.min = pick(linkedParentExtremes.min, linkedParentExtremes.dataMin);
@@ -7159,21 +7334,16 @@
tickPixelIntervalOption === axis.linkedParent.options.tickPixelInterval) {
axis.tickInterval = axis.linkedParent.tickInterval;
} else {
axis.tickInterval = pick(
tickIntervalOption,
+ this.tickAmount ? ((axis.max - axis.min) / mathMax(this.tickAmount - 1, 1)) : undefined,
categories ? // for categoried axis, 1 is default, for linear axis use tickPix
1 :
// don't let it be more than the data range
(axis.max - axis.min) * tickPixelIntervalOption / mathMax(axis.len, tickPixelIntervalOption)
);
- // For squished axes, set only two ticks
- if (!defined(tickIntervalOption) && axis.len < tickPixelIntervalOption && !this.isRadial &&
- !this.isLog && !categories && startOnTick && endOnTick) {
- keepTwoTicksOnly = true;
- axis.tickInterval /= 4; // tick extremes closer to the real values
- }
}
// Now we're finished detecting min and max, crop and group series data. This
// is in turn needed in order to find tick positions in ordinal axes.
if (isXAxis && !secondPass) {
@@ -7199,151 +7369,225 @@
if (axis.pointRange) {
axis.tickInterval = mathMax(axis.pointRange, axis.tickInterval);
}
// Before normalizing the tick interval, handle minimum tick interval. This applies only if tickInterval is not defined.
- if (!tickIntervalOption && axis.tickInterval < minTickIntervalOption) {
- axis.tickInterval = minTickIntervalOption;
+ minTickInterval = pick(options.minTickInterval, axis.isDatetimeAxis && axis.closestPointRange);
+ if (!tickIntervalOption && axis.tickInterval < minTickInterval) {
+ axis.tickInterval = minTickInterval;
}
// for linear axes, get magnitude and normalize the interval
if (!isDatetimeAxis && !isLog) { // linear
if (!tickIntervalOption) {
axis.tickInterval = normalizeTickInterval(
axis.tickInterval,
null,
getMagnitude(axis.tickInterval),
- // If the tick interval is between 1 and 5 and the axis max is in the order of
+ // If the tick interval is between 0.5 and 5 and the axis max is in the order of
// thousands, chances are we are dealing with years. Don't allow decimals. #3363.
- pick(options.allowDecimals, !(axis.tickInterval > 1 && axis.tickInterval < 5 && axis.max > 1000 && axis.max < 9999))
+ pick(options.allowDecimals, !(axis.tickInterval > 0.5 && axis.tickInterval < 5 && axis.max > 1000 && axis.max < 9999)),
+ !!this.tickAmount
);
}
}
+ // Prevent ticks from getting so close that we can't draw the labels
+ if (!this.tickAmount && this.len) { // Color axis with disabled legend has no length
+ axis.tickInterval = axis.unsquish();
+ }
+
+ this.setTickPositions();
+ },
+
+ /**
+ * Now we have computed the normalized tickInterval, get the tick positions
+ */
+ setTickPositions: function () {
+
+ var options = this.options,
+ tickPositions,
+ tickPositionsOption = options.tickPositions,
+ tickPositioner = options.tickPositioner,
+ startOnTick = options.startOnTick,
+ endOnTick = options.endOnTick,
+ single;
+
+ // Set the tickmarkOffset
+ this.tickmarkOffset = (this.categories && options.tickmarkPlacement === 'between' &&
+ this.tickInterval === 1) ? 0.5 : 0; // #3202
+
+
// get minorTickInterval
- axis.minorTickInterval = options.minorTickInterval === 'auto' && axis.tickInterval ?
- axis.tickInterval / 5 : options.minorTickInterval;
+ this.minorTickInterval = options.minorTickInterval === 'auto' && this.tickInterval ?
+ this.tickInterval / 5 : options.minorTickInterval;
- // find the tick positions
- axis.tickPositions = tickPositions = options.tickPositions ?
- [].concat(options.tickPositions) : // Work on a copy (#1565)
- (tickPositioner && tickPositioner.apply(axis, [axis.min, axis.max]));
+ // Find the tick positions
+ this.tickPositions = tickPositions = options.tickPositions && options.tickPositions.slice(); // Work on a copy (#1565)
if (!tickPositions) {
- // Too many ticks
- if (!axis.ordinalPositions && (axis.max - axis.min) / axis.tickInterval > mathMax(2 * axis.len, 200)) {
- error(19, true);
- }
-
- if (isDatetimeAxis) {
- tickPositions = axis.getTimeTicks(
- axis.normalizeTimeTickInterval(axis.tickInterval, options.units),
- axis.min,
- axis.max,
+ if (this.isDatetimeAxis) {
+ tickPositions = this.getTimeTicks(
+ this.normalizeTimeTickInterval(this.tickInterval, options.units),
+ this.min,
+ this.max,
options.startOfWeek,
- axis.ordinalPositions,
- axis.closestPointRange,
+ this.ordinalPositions,
+ this.closestPointRange,
true
);
- } else if (isLog) {
- tickPositions = axis.getLogTickPositions(axis.tickInterval, axis.min, axis.max);
+ } else if (this.isLog) {
+ tickPositions = this.getLogTickPositions(this.tickInterval, this.min, this.max);
} else {
- tickPositions = axis.getLinearTickPositions(axis.tickInterval, axis.min, axis.max);
+ tickPositions = this.getLinearTickPositions(this.tickInterval, this.min, this.max);
}
- if (keepTwoTicksOnly) {
- tickPositions.splice(1, tickPositions.length - 2);
+ this.tickPositions = tickPositions;
+
+ // Run the tick positioner callback, that allows modifying auto tick positions.
+ if (tickPositioner) {
+ tickPositioner = tickPositioner.apply(this, [this.min, this.max]);
+ if (tickPositioner) {
+ this.tickPositions = tickPositions = tickPositioner;
+ }
}
- axis.tickPositions = tickPositions;
}
- if (!isLinked) {
+ if (!this.isLinked) {
// reset min/max or remove extremes based on start/end on tick
- var roundedMin = tickPositions[0],
- roundedMax = tickPositions[tickPositions.length - 1],
- minPointOffset = axis.minPointOffset || 0,
- singlePad;
+ this.trimTicks(tickPositions, startOnTick, endOnTick);
- if (startOnTick) {
- axis.min = roundedMin;
- } else if (axis.min - minPointOffset > roundedMin) {
- tickPositions.shift();
+ // When there is only one point, or all points have the same value on this axis, then min
+ // and max are equal and tickPositions.length is 0 or 1. In this case, add some padding
+ // in order to center the point, but leave it with one tick. #1337.
+ if (this.min === this.max && defined(this.min) && !this.tickAmount) {
+ // Substract half a unit (#2619, #2846, #2515, #3390)
+ single = true;
+ this.min -= 0.5;
+ this.max += 0.5;
}
+ this.single = single;
- if (endOnTick) {
- axis.max = roundedMax;
- } else if (axis.max + minPointOffset < roundedMax) {
- tickPositions.pop();
+ if (!tickPositionsOption && !tickPositioner) {
+ this.adjustTickAmount();
}
+ }
+ },
- // If no tick are left, set one tick in the middle (#3195)
- if (tickPositions.length === 0 && defined(roundedMin)) {
- tickPositions.push((roundedMax + roundedMin) / 2);
- }
+ /**
+ * Handle startOnTick and endOnTick by either adapting to padding min/max or rounded min/max
+ */
+ trimTicks: function (tickPositions, startOnTick, endOnTick) {
+ var roundedMin = tickPositions[0],
+ roundedMax = tickPositions[tickPositions.length - 1],
+ minPointOffset = this.minPointOffset || 0;
+
+ if (startOnTick) {
+ this.min = roundedMin;
+ } else if (this.min - minPointOffset > roundedMin) {
+ tickPositions.shift();
+ }
- // When there is only one point, or all points have the same value on this axis, then min
- // and max are equal and tickPositions.length is 0 or 1. In this case, add some padding
- // in order to center the point, but leave it with one tick. #1337.
- if (tickPositions.length === 1) {
- singlePad = mathAbs(axis.max) > 10e12 ? 1 : 0.001; // The lowest possible number to avoid extra padding on columns (#2619, #2846)
- axis.min -= singlePad;
- axis.max += singlePad;
- }
+ if (endOnTick) {
+ this.max = roundedMax;
+ } else if (this.max + minPointOffset < roundedMax) {
+ tickPositions.pop();
}
+
+ // If no tick are left, set one tick in the middle (#3195)
+ if (tickPositions.length === 0 && defined(roundedMin)) {
+ tickPositions.push((roundedMax + roundedMin) / 2);
+ }
},
/**
* Set the max ticks of either the x and y axis collection
*/
- setMaxTicks: function () {
+ getTickAmount: function () {
+ var others = {}, // Whether there is another axis to pair with this one
+ hasOther,
+ options = this.options,
+ tickAmount = options.tickAmount,
+ tickPixelInterval = options.tickPixelInterval;
- var chart = this.chart,
- maxTicks = chart.maxTicks || {},
- tickPositions = this.tickPositions,
- key = this._maxTicksKey = [this.coll, this.pos, this.len].join('-');
+ if (!defined(options.tickInterval) && this.len < tickPixelInterval && !this.isRadial &&
+ !this.isLog && options.startOnTick && options.endOnTick) {
+ tickAmount = 2;
+ }
- if (!this.isLinked && !this.isDatetimeAxis && tickPositions && tickPositions.length > (maxTicks[key] || 0) && this.options.alignTicks !== false) {
- maxTicks[key] = tickPositions.length;
+ if (!tickAmount && this.chart.options.chart.alignTicks !== false && options.alignTicks !== false) {
+ // Check if there are multiple axes in the same pane
+ each(this.chart[this.coll], function (axis) {
+ var options = axis.options,
+ horiz = axis.horiz,
+ key = [horiz ? options.left : options.top, horiz ? options.width : options.height, options.pane].join(',');
+
+ if (others[key]) {
+ hasOther = true;
+ } else {
+ others[key] = 1;
+ }
+ });
+
+ if (hasOther) {
+ // Add 1 because 4 tick intervals require 5 ticks (including first and last)
+ tickAmount = mathCeil(this.len / tickPixelInterval) + 1;
+ }
}
- chart.maxTicks = maxTicks;
+
+ // For tick amounts of 2 and 3, compute five ticks and remove the intermediate ones. This
+ // prevents the axis from adding ticks that are too far away from the data extremes.
+ if (tickAmount < 4) {
+ this.finalTickAmt = tickAmount;
+ tickAmount = 5;
+ }
+
+ this.tickAmount = tickAmount;
},
/**
* When using multiple axes, adjust the number of ticks to match the highest
* number of ticks in that group
*/
adjustTickAmount: function () {
- var axis = this,
- chart = axis.chart,
- key = axis._maxTicksKey,
- tickPositions = axis.tickPositions,
- maxTicks = chart.maxTicks;
+ var tickInterval = this.tickInterval,
+ tickPositions = this.tickPositions,
+ tickAmount = this.tickAmount,
+ finalTickAmt = this.finalTickAmt,
+ currentTickAmount = tickPositions && tickPositions.length,
+ i,
+ len;
- if (maxTicks && maxTicks[key] && !axis.isDatetimeAxis && !axis.categories && !axis.isLinked &&
- axis.options.alignTicks !== false && this.min !== UNDEFINED) {
- var oldTickAmount = axis.tickAmount,
- calculatedTickAmount = tickPositions.length,
- tickAmount;
+ if (currentTickAmount < tickAmount) { // TODO: Check #3411
+ while (tickPositions.length < tickAmount) {
+ tickPositions.push(correctFloat(
+ tickPositions[tickPositions.length - 1] + tickInterval
+ ));
+ }
+ this.transA *= (currentTickAmount - 1) / (tickAmount - 1);
+ this.max = tickPositions[tickPositions.length - 1];
- // set the axis-level tickAmount to use below
- axis.tickAmount = tickAmount = maxTicks[key];
+ // We have too many ticks, run second pass to try to reduce ticks
+ } else if (currentTickAmount > tickAmount) {
+ this.tickInterval *= 2;
+ this.setTickPositions();
+ }
- if (calculatedTickAmount < tickAmount) {
- while (tickPositions.length < tickAmount) {
- tickPositions.push(correctFloat(
- tickPositions[tickPositions.length - 1] + axis.tickInterval
- ));
- }
- axis.transA *= (calculatedTickAmount - 1) / (tickAmount - 1);
- axis.max = tickPositions[tickPositions.length - 1];
-
+ // The finalTickAmt property is set in getTickAmount
+ if (defined(finalTickAmt)) {
+ i = len = tickPositions.length;
+ while (i--) {
+ if (
+ (finalTickAmt === 3 && i % 2 === 1) || // Remove every other tick
+ (finalTickAmt <= 2 && i > 0 && i < len - 1) // Remove all but first and last
+ ) {
+ tickPositions.splice(i, 1);
+ }
}
- if (defined(oldTickAmount) && tickAmount !== oldTickAmount) {
- axis.isDirty = true;
- }
+ this.finalTickAmt = UNDEFINED;
}
},
/**
* Set the scale based on data min and max, user set min and max or options
@@ -7392,11 +7636,11 @@
// get data extremes if needed
axis.getSeriesExtremes();
// get fixed positions based on tickInterval
- axis.setTickPositions();
+ axis.setTickInterval();
// record old values to decide whether a rescale is necessary later on (#540)
axis.oldUserMin = axis.userMin;
axis.oldUserMax = axis.userMax;
@@ -7414,13 +7658,10 @@
for (i in stacks[type]) {
stacks[type][i].cum = stacks[type][i].total;
}
}
}
-
- // Set the maximum tick amount
- axis.setMaxTicks();
},
/**
* Set the extremes and optionally redraw
* @param {Number} newMin
@@ -7508,14 +7749,14 @@
left = pick(options.left, chart.plotLeft + offsetLeft),
percentRegex = /%$/;
// Check for percentage based input values
if (percentRegex.test(height)) {
- height = parseInt(height, 10) / 100 * chart.plotHeight;
+ height = parseFloat(height) / 100 * chart.plotHeight;
}
if (percentRegex.test(top)) {
- top = parseInt(top, 10) / 100 * chart.plotHeight + chart.plotTop;
+ top = parseFloat(top) / 100 * chart.plotHeight + chart.plotTop;
}
// Expose basic values to use in Series object and navigator
this.left = left;
this.top = top;
@@ -7583,10 +7824,165 @@
}
return ret;
},
/**
+ * Prevent the ticks from getting so close we can't draw the labels. On a horizontal
+ * axis, this is handled by rotating the labels, removing ticks and adding ellipsis.
+ * On a vertical axis remove ticks and add ellipsis.
+ */
+ unsquish: function () {
+ var chart = this.chart,
+ ticks = this.ticks,
+ labelOptions = this.options.labels,
+ horiz = this.horiz,
+ tickInterval = this.tickInterval,
+ newTickInterval = tickInterval,
+ slotSize = this.len / (((this.categories ? 1 : 0) + this.max - this.min) / tickInterval),
+ rotation,
+ rotationOption = labelOptions.rotation,
+ labelMetrics = chart.renderer.fontMetrics(labelOptions.style.fontSize, ticks[0] && ticks[0].label),
+ step,
+ bestScore = Number.MAX_VALUE,
+ autoRotation,
+ // Return the multiple of tickInterval that is needed to avoid collision
+ getStep = function (spaceNeeded) {
+ var step = spaceNeeded / (slotSize || 1);
+ step = step > 1 ? mathCeil(step) : 1;
+ return step * tickInterval;
+ };
+
+ if (horiz) {
+ autoRotation = defined(rotationOption) ?
+ [rotationOption] :
+ slotSize < 80 && !labelOptions.staggerLines && !labelOptions.step && labelOptions.autoRotation;
+
+ if (autoRotation) {
+
+ // Loop over the given autoRotation options, and determine which gives the best score. The
+ // best score is that with the lowest number of steps and a rotation closest to horizontal.
+ each(autoRotation, function (rot) {
+ var score;
+
+ if (rot && rot >= -90 && rot <= 90) {
+
+ step = getStep(mathAbs(labelMetrics.h / mathSin(deg2rad * rot)));
+
+ score = step + mathAbs(rot / 360);
+
+ if (score < bestScore) {
+ bestScore = score;
+ rotation = rot;
+ newTickInterval = step;
+ }
+ }
+ });
+ }
+
+ } else {
+ newTickInterval = getStep(labelMetrics.h);
+ }
+
+ this.autoRotation = autoRotation;
+ this.labelRotation = rotation;
+
+ return newTickInterval;
+ },
+
+ renderUnsquish: function () {
+ var chart = this.chart,
+ renderer = chart.renderer,
+ tickPositions = this.tickPositions,
+ ticks = this.ticks,
+ labelOptions = this.options.labels,
+ horiz = this.horiz,
+ margin = chart.margin,
+ slotWidth = this.slotWidth = (horiz && !labelOptions.step && !labelOptions.rotation &&
+ ((this.staggerLines || 1) * chart.plotWidth) / tickPositions.length) ||
+ (!horiz && ((margin[3] && (margin[3] - chart.spacing[3])) || chart.chartWidth * 0.33)), // #1580, #1931,
+ innerWidth = mathMax(1, mathRound(slotWidth - 2 * (labelOptions.padding || 5))),
+ attr = {},
+ labelMetrics = renderer.fontMetrics(labelOptions.style.fontSize, ticks[0] && ticks[0].label),
+ css,
+ labelLength = 0,
+ label,
+ i,
+ pos;
+
+ // Set rotation option unless it is "auto", like in gauges
+ if (!isString(labelOptions.rotation)) {
+ attr.rotation = labelOptions.rotation;
+ }
+
+ // Handle auto rotation on horizontal axis
+ if (this.autoRotation) {
+
+ // Get the longest label length
+ each(tickPositions, function (tick) {
+ tick = ticks[tick];
+ if (tick && tick.labelLength > labelLength) {
+ labelLength = tick.labelLength;
+ }
+ });
+
+ // Apply rotation only if the label is too wide for the slot, and
+ // the label is wider than its height.
+ if (labelLength > innerWidth && labelLength > labelMetrics.h) {
+ attr.rotation = this.labelRotation;
+ } else {
+ this.labelRotation = 0;
+ }
+
+ // Handle word-wrap or ellipsis on vertical axis
+ } else if (slotWidth) {
+ // For word-wrap or ellipsis
+ css = { width: innerWidth + PX, textOverflow: 'clip' };
+
+ // On vertical axis, only allow word wrap if there is room for more lines.
+ i = tickPositions.length;
+ while (!horiz && i--) {
+ pos = tickPositions[i];
+ label = ticks[pos].label;
+ if (label) {
+ if (this.len / tickPositions.length - 4 < label.getBBox().height) {
+ label.specCss = { textOverflow: 'ellipsis' };
+ }
+ }
+ }
+ }
+
+
+ // Add ellipsis if the label length is significantly longer than ideal
+ if (attr.rotation) {
+ css = {
+ width: (labelLength > chart.chartHeight * 0.5 ? chart.chartHeight * 0.33 : chart.chartHeight) + PX,
+ textOverflow: 'ellipsis'
+ };
+ }
+
+ // Set the explicit or automatic label alignment
+ this.labelAlign = attr.align = labelOptions.align || this.autoLabelAlign(this.labelRotation);
+
+ // Apply general and specific CSS
+ each(tickPositions, function (pos) {
+ var tick = ticks[pos],
+ label = tick && tick.label;
+ if (label) {
+ if (css) {
+ label.css(merge(css, label.specCss));
+ }
+ delete label.specCss;
+ label.attr(attr);
+ tick.rotation = attr.rotation;
+ }
+ });
+
+ // TODO: Why not part of getLabelPosition?
+ this.tickRotCorr = renderer.rotCorr(labelMetrics.b, this.labelRotation || 0, this.side === 2);
+ },
+
+ /**
* Render the tick labels to a preliminary position to get their sizes
*/
getOffset: function () {
var axis = this,
chart = axis.chart,
@@ -7608,21 +8004,10 @@
labelOffsetPadded,
axisOffset = chart.axisOffset,
clipOffset = chart.clipOffset,
directionFactor = [-1, 1, 1, -1][side],
n,
- i,
- autoStaggerLines = 1,
- maxStaggerLines = pick(labelOptions.maxStaggerLines, 5),
- sortedPositions,
- lastRight,
- overlap,
- pos,
- bBox,
- x,
- w,
- lineNo,
lineHeightCorrection;
// For reuse in Axis.render
axis.hasData = hasData = (axis.hasVisibleSeries || (defined(axis.min) && defined(axis.max) && !!tickPositions));
axis.showAxis = showAxis = hasData || pick(options.showEmpty, true);
@@ -7643,57 +8028,22 @@
.addClass(PREFIX + axis.coll.toLowerCase() + '-labels')
.add();
}
if (hasData || axis.isLinked) {
-
- // Set the explicit or automatic label alignment
- axis.labelAlign = pick(labelOptions.align || axis.autoLabelAlign(labelOptions.rotation));
-
+
// Generate ticks
each(tickPositions, function (pos) {
if (!ticks[pos]) {
ticks[pos] = new Tick(axis, pos);
} else {
ticks[pos].addLabel(); // update labels depending on tick interval
}
});
- // Handle automatic stagger lines
- if (axis.horiz && !axis.staggerLines && maxStaggerLines && !labelOptions.rotation) {
- sortedPositions = axis.reversed ? [].concat(tickPositions).reverse() : tickPositions;
- while (autoStaggerLines < maxStaggerLines) {
- lastRight = [];
- overlap = false;
+ axis.renderUnsquish();
- for (i = 0; i < sortedPositions.length; i++) {
- pos = sortedPositions[i];
- bBox = ticks[pos].label && ticks[pos].label.getBBox();
- w = bBox ? bBox.width : 0;
- lineNo = i % autoStaggerLines;
-
- if (w) {
- x = axis.translate(pos); // don't handle log
- if (lastRight[lineNo] !== UNDEFINED && x < lastRight[lineNo]) {
- overlap = true;
- }
- lastRight[lineNo] = x + w;
- }
- }
- if (overlap) {
- autoStaggerLines++;
- } else {
- break;
- }
- }
-
- if (autoStaggerLines > 1) {
- axis.staggerLines = autoStaggerLines;
- }
- }
-
-
each(tickPositions, function (pos) {
// left side must be align: right and right side must have align: left for labels
if (side === 0 || side === 2 || { 1: 'left', 3: 'right' }[side] === axis.labelAlign) {
// get the highest offset
@@ -7749,13 +8099,14 @@
}
// handle automatic or user set offset
axis.offset = directionFactor * pick(options.offset, axisOffset[side]);
- lineHeightCorrection = side === 2 ? axis.tickBaseline : 0;
+ axis.tickRotCorr = axis.tickRotCorr || { x: 0, y: 0 }; // polar
+ lineHeightCorrection = side === 2 ? axis.tickRotCorr.y : 0;
labelOffsetPadded = labelOffset + titleMargin +
- (labelOffset && (directionFactor * (horiz ? pick(labelOptions.y, axis.tickBaseline + 8) : labelOptions.x) - lineHeightCorrection));
+ (labelOffset && (directionFactor * (horiz ? pick(labelOptions.y, axis.tickRotCorr.y + 8) : labelOptions.x) - lineHeightCorrection));
axis.axisTitleMargin = pick(titleOffsetOption, labelOffsetPadded);
axisOffset[side] = mathMax(
axisOffset[side],
axis.axisTitleMargin + titleOffset + directionFactor * axis.offset,
@@ -7840,19 +8191,16 @@
/**
* Render the axis
*/
render: function () {
var axis = this,
- horiz = axis.horiz,
- reversed = axis.reversed,
chart = axis.chart,
renderer = chart.renderer,
options = axis.options,
isLog = axis.isLog,
isLinked = axis.isLinked,
tickPositions = axis.tickPositions,
- sortedPositions,
axisTitle = axis.axisTitle,
ticks = axis.ticks,
minorTicks = axis.minorTicks,
alternateBands = axis.alternateBands,
stackLabelOptions = options.stackLabels,
@@ -7863,17 +8211,16 @@
hasRendered = chart.hasRendered,
slideInTicks = hasRendered && defined(axis.oldMin) && !isNaN(axis.oldMin),
hasData = axis.hasData,
showAxis = axis.showAxis,
from,
- overflow = options.labels.overflow,
- justifyLabels = axis.justifyLabels = horiz && overflow !== false,
to;
// Reset
axis.labelEdge.length = 0;
- axis.justifyToPlot = overflow === 'justify';
+ //axis.justifyToPlot = overflow === 'justify';
+ axis.overlap = false;
// Mark all elements inActive before we go over and mark the active ones
each([ticks, minorTicks, alternateBands], function (coll) {
var pos;
for (pos in coll) {
@@ -7901,24 +8248,12 @@
}
// Major ticks. Pull out the first item and render it last so that
// we can get the position of the neighbour label. #808.
if (tickPositions.length) { // #1300
- sortedPositions = tickPositions.slice();
- if ((horiz && reversed) || (!horiz && !reversed)) {
- sortedPositions.reverse();
- }
- if (justifyLabels) {
- sortedPositions = sortedPositions.slice(1).concat([sortedPositions[0]]);
- }
- each(sortedPositions, function (pos, i) {
+ each(tickPositions, function (pos, i) {
- // Reorganize the indices
- if (justifyLabels) {
- i = (i === sortedPositions.length - 1) ? 0 : i + 1;
- }
-
// linked axes need an extra check to find out if
if (!isLinked || (pos >= axis.min && pos <= axis.max)) {
if (!ticks[pos]) {
ticks[pos] = new Tick(axis, pos);
@@ -7933,11 +8268,11 @@
}
});
// In a categorized axis, the tick marks are displayed between labels. So
// we need to add a tick mark and grid line at the left edge of the X axis.
- if (tickmarkOffset && axis.min === 0) {
+ if (tickmarkOffset && (axis.min === 0 || axis.single)) {
if (!ticks[-1]) {
ticks[-1] = new Tick(axis, -1, null, true);
}
ticks[-1].render(-1);
}
@@ -8114,58 +8449,70 @@
},
/**
* Draw the crosshair
*/
- drawCrosshair: function (e, point) {
- if (!this.crosshair) { return; }// Do not draw crosshairs if you don't have too.
+ drawCrosshair: function (e, point) { // docs: Missing docs for Axis.crosshair. Also for properties.
- if ((defined(point) || !pick(this.crosshair.snap, true)) === false) {
- this.hideCrosshair();
- return;
- }
-
var path,
options = this.crosshair,
animation = options.animation,
- pos;
+ pos,
+ attribs,
+ categorized;
+
+ if (
+ // Disabled in options
+ !this.crosshair ||
+ // snap
+ ((defined(point) || !pick(this.crosshair.snap, true)) === false) ||
+ // Do not draw the crosshair if this axis is not part of the point
+ (defined(point) && pick(this.crosshair.snap, true) && (!point.series || point.series[this.isXAxis ? 'xAxis' : 'yAxis'] !== this))
+ ) {
+ this.hideCrosshair();
+
+ } else {
- // Get the path
- if (!pick(options.snap, true)) {
- pos = (this.horiz ? e.chartX - this.pos : this.len - e.chartY + this.pos);
- } else if (defined(point)) {
- /*jslint eqeq: true*/
- pos = (this.chart.inverted != this.horiz) ? point.plotX : this.len - point.plotY;
- /*jslint eqeq: false*/
- }
+ // Get the path
+ if (!pick(options.snap, true)) {
+ pos = (this.horiz ? e.chartX - this.pos : this.len - e.chartY + this.pos);
+ } else if (defined(point)) {
+ /*jslint eqeq: true*/
+ pos = (this.chart.inverted != this.horiz) ? point.plotX : this.len - point.plotY;
+ /*jslint eqeq: false*/
+ }
- if (this.isRadial) {
- path = this.getPlotLinePath(this.isXAxis ? point.x : pick(point.stackY, point.y));
- } else {
- path = this.getPlotLinePath(null, null, null, null, pos);
- }
+ 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 (path === null) {
- this.hideCrosshair();
- return;
- }
+ if (path === null) {
+ this.hideCrosshair();
+ return;
+ }
- // Draw the cross
- if (this.cross) {
- this.cross
- .attr({ visibility: VISIBLE })[animation ? 'animate' : 'attr']({ d: path }, animation);
- } else {
- var attribs = {
- 'stroke-width': options.width || 1,
- stroke: options.color || '#C0C0C0',
- zIndex: options.zIndex || 2
- };
- if (options.dashStyle) {
- attribs.dashstyle = options.dashStyle;
+ // Draw the cross
+ if (this.cross) {
+ this.cross
+ .attr({ visibility: VISIBLE })[animation ? 'animate' : 'attr']({ d: path }, animation);
+ } else {
+ categorized = this.categories && !this.isRadial;
+ attribs = {
+ 'stroke-width': options.width || (categorized ? this.transA : 1),
+ stroke: options.color || (categorized ? 'rgba(155,200,255,0.2)' : '#C0C0C0'),
+ zIndex: options.zIndex || 2
+ };
+ if (options.dashStyle) {
+ attribs.dashstyle = options.dashStyle;
+ }
+ this.cross = this.chart.renderer.path(path).attr(attribs).add();
}
- this.cross = this.chart.renderer.path(path).attr(attribs).add();
+
}
+
},
/**
* Hide the crosshair.
*/
@@ -8193,17 +8540,19 @@
var tickPositions = [],
i,
higherRanks = {},
useUTC = defaultOptions.global.useUTC,
minYear, // used in months and years as a basis for Date.UTC()
- minDate = new Date(min - timezoneOffset),
+ minDate = new Date(min - getTZOffset(min)),
interval = normalizedInterval.unitRange,
count = normalizedInterval.count;
if (defined(min)) { // #1300
+ minDate.setMilliseconds(interval >= timeUnits.second ? 0 :
+ count * mathFloor(minDate.getMilliseconds() / count)); // #3652, #3654
+
if (interval >= timeUnits.second) { // second
- minDate.setMilliseconds(0);
minDate.setSeconds(interval >= timeUnits.minute ? 0 :
count * mathFloor(minDate.getSeconds() / count));
}
if (interval >= timeUnits.minute) { // minute
@@ -8240,19 +8589,20 @@
}
// get tick positions
i = 1;
- if (timezoneOffset) {
- minDate = new Date(minDate.getTime() + timezoneOffset);
+ if (timezoneOffset || getTimezoneOffset) {
+ minDate = minDate.getTime();
+ minDate = new Date(minDate + getTZOffset(minDate));
}
minYear = minDate[getFullYear]();
var time = minDate.getTime(),
minMonth = minDate[getMonth](),
minDateDate = minDate[getDate](),
localTimezoneOffset = (timeUnits.day +
- (useUTC ? timezoneOffset : minDate.getTimezoneOffset() * 60 * 1000)
+ (useUTC ? getTZOffset(minDate) : minDate.getTimezoneOffset() * 60 * 1000)
) % timeUnits.day; // #950, #3359
// iterate and add tick positions at appropriate values
while (time < max) {
tickPositions.push(time);
@@ -8508,11 +8858,11 @@
// The tooltip is initially hidden
this.isHidden = true;
- // create the label
+ // create the label
this.label = chart.renderer.label('', 0, 0, options.shape || 'callout', null, null, options.useHTML, null, 'tooltip')
.attr({
padding: padding,
fill: options.backgroundColor,
'stroke-width': borderWidth,
@@ -8612,10 +8962,11 @@
point.setState();
});
}
this.chart.hoverPoints = null;
+ this.chart.hoverSeries = null;
}
},
/**
* Extendable method to get the anchor position of the tooltip
@@ -8624,13 +8975,15 @@
getAnchor: function (points, mouseEvent) {
var ret,
chart = this.chart,
inverted = chart.inverted,
plotTop = chart.plotTop,
+ plotLeft = chart.plotLeft,
plotX = 0,
plotY = 0,
- yAxis;
+ yAxis,
+ xAxis;
points = splat(points);
// Pie uses a special tooltipPos
ret = points[0].tooltipPos;
@@ -8647,11 +9000,12 @@
}
// When shared, use the average position
if (!ret) {
each(points, function (point) {
yAxis = point.series.yAxis;
- plotX += point.plotX;
+ xAxis = point.series.xAxis;
+ plotX += point.plotX + (!inverted && xAxis ? xAxis.left - plotLeft : 0);
plotY += (point.plotLow ? (point.plotLow + point.plotHigh) / 2 : point.plotY) +
(!inverted && yAxis ? yAxis.top - plotTop : 0); // #1151
});
plotX /= points.length;
@@ -8679,11 +9033,11 @@
ret = {},
swapped,
first = ['y', chart.chartHeight, boxHeight, point.plotY + chart.plotTop],
second = ['x', chart.chartWidth, boxWidth, point.plotX + chart.plotLeft],
// The far side is right or bottom
- preferFarSide = point.ttBelow || (chart.inverted && !point.negative) || (!chart.inverted && point.negative),
+ preferFarSide = pick(point.ttBelow, (chart.inverted && !point.negative) || (!chart.inverted && point.negative)),
/**
* Handle the preferred dimension. When the preferred dimension is tooltip
* on top or bottom of the point, it will look for space there.
*/
firstDimension = function (dim, outerSize, innerSize, point) {
@@ -8763,25 +9117,20 @@
* In case no user defined formatter is given, this will be used. Note that the context
* here is an object holding point, series, x, y etc.
*/
defaultFormatter: function (tooltip) {
var items = this.points || splat(this),
- series = items[0].series,
s;
// build the header
- s = [tooltip.tooltipHeaderFormatter(items[0])];
+ s = [tooltip.tooltipFooterHeaderFormatter(items[0])]; //#3397: abstraction to enable formatting of footer and header
// build the values
- each(items, function (item) {
- series = item.series;
- s.push((series.tooltipFormatter && series.tooltipFormatter(item)) ||
- item.point.tooltipFormatter(series.tooltipOptions.pointFormat));
- });
+ s = s.concat(tooltip.bodyFormatter(items));
// footer
- s.push(tooltip.options.footerFormat || '');
+ s.push(tooltip.tooltipFooterHeaderFormatter(items[0], true)); //#3397: abstraction to enable formatting of footer and header
return s.join('');
},
/**
@@ -8903,56 +9252,106 @@
point.plotX + chart.plotLeft,
point.plotY + chart.plotTop
);
},
+ /**
+ * Get the best X date format based on the closest point range on the axis.
+ */
+ getXDateFormat: function (point, options, xAxis) {
+ var xDateFormat,
+ dateTimeLabelFormats = options.dateTimeLabelFormats,
+ closestPointRange = xAxis && xAxis.closestPointRange,
+ n,
+ blank = '01-01 00:00:00.000',
+ strpos = {
+ millisecond: 15,
+ second: 12,
+ minute: 9,
+ hour: 6,
+ day: 3
+ },
+ date,
+ lastN;
+ if (closestPointRange) {
+ date = dateFormat('%m-%d %H:%M:%S.%L', point.x);
+ for (n in timeUnits) {
+
+ // If the range is exactly one week and we're looking at a Sunday/Monday, go for the week format
+ if (closestPointRange === timeUnits.week && +dateFormat('%w', point.x) === xAxis.options.startOfWeek &&
+ date.substr(6) === blank.substr(6)) {
+ n = 'week';
+ break;
+
+ // The first format that is too great for the range
+ } else if (timeUnits[n] > closestPointRange) {
+ n = lastN;
+ break;
+
+ // If the point is placed every day at 23:59, we need to show
+ // the minutes as well. #2637.
+ } else if (strpos[n] && date.substr(strpos[n]) !== blank.substr(strpos[n])) {
+ break;
+ }
+
+ // Weeks are outside the hierarchy, only apply them on Mondays/Sundays like in the first condition
+ if (n !== 'week') {
+ lastN = n;
+ }
+ }
+
+ if (n) {
+ xDateFormat = dateTimeLabelFormats[n];
+ }
+ } else {
+ xDateFormat = dateTimeLabelFormats.day;
+ }
+
+ return xDateFormat || dateTimeLabelFormats.year; // #2546, 2581
+ },
+
/**
- * Format the header of the tooltip
+ * Format the footer/header of the tooltip
+ * #3397: abstraction to enable formatting of footer and header
*/
- tooltipHeaderFormatter: function (point) {
- var series = point.series,
+ tooltipFooterHeaderFormatter: function (point, isFooter) {
+ var footOrHead = isFooter ? 'footer' : 'header',
+ series = point.series,
tooltipOptions = series.tooltipOptions,
- dateTimeLabelFormats = tooltipOptions.dateTimeLabelFormats,
xDateFormat = tooltipOptions.xDateFormat,
xAxis = series.xAxis,
isDateTime = xAxis && xAxis.options.type === 'datetime' && isNumber(point.key),
- headerFormat = tooltipOptions.headerFormat,
- closestPointRange = xAxis && xAxis.closestPointRange,
- n;
+ formatString = tooltipOptions[footOrHead+'Format'];
- // Guess the best date format based on the closest point distance (#568)
+ // Guess the best date format based on the closest point distance (#568, #3418)
if (isDateTime && !xDateFormat) {
- if (closestPointRange) {
- for (n in timeUnits) {
- if (timeUnits[n] >= closestPointRange ||
- // If the point is placed every day at 23:59, we need to show
- // the minutes as well. This logic only works for time units less than
- // a day, since all higher time units are dividable by those. #2637.
- (timeUnits[n] <= timeUnits.day && point.key % timeUnits[n] > 0)) {
- xDateFormat = dateTimeLabelFormats[n];
- break;
- }
- }
- } else {
- xDateFormat = dateTimeLabelFormats.day;
- }
-
- xDateFormat = xDateFormat || dateTimeLabelFormats.year; // #2546, 2581
-
+ xDateFormat = this.getXDateFormat(point, tooltipOptions, xAxis);
}
- // Insert the header date format if any
+ // Insert the footer date format if any
if (isDateTime && xDateFormat) {
- headerFormat = headerFormat.replace('{point.key}', '{point.key:' + xDateFormat + '}');
+ formatString = formatString.replace('{point.key}', '{point.key:' + xDateFormat + '}');
}
- return format(headerFormat, {
+ return format(formatString, {
point: point,
series: series
});
- }
+ },
+
+ /**
+ * Build the body (lines) of the tooltip by iterating over the items and returning one entry for each item,
+ * abstracting this functionality allows to easily overwrite and extend it.
+ */
+ bodyFormatter: function (items) {
+ return map(items, function (item) {
+ var tooltipOptions = item.series.tooltipOptions;
+ return (tooltipOptions.pointFormatter || item.point.tooltipFormatter).call(item.point, tooltipOptions.pointFormat);
+ });
+ }
+
};
var hoverChartIndex;
// Global flag for touch support
@@ -8998,11 +9397,11 @@
this.pinchDown = [];
this.lastValidTouch = {};
if (Highcharts.Tooltip && options.tooltip.enabled) {
chart.tooltip = new Tooltip(chart, options.tooltip);
- this.followTouchMove = options.tooltip.followTouchMove;
+ this.followTouchMove = pick(options.tooltip.followTouchMove, true);
}
this.setDOMEvents();
},
@@ -9069,106 +9468,104 @@
});
return coordinates;
},
/**
- * Return the index in the tooltipPoints array, corresponding to pixel position in
- * the plot area.
- */
- getIndex: function (e) {
- var chart = this.chart;
- return chart.inverted ?
- chart.plotHeight + chart.plotTop - e.chartY :
- e.chartX - chart.plotLeft;
- },
-
- /**
* With line type charts with a single tracker, get the point closest to the mouse.
* Run Point.onMouseOver and display tooltip for the point or points.
*/
runPointActions: function (e) {
+
var pointer = this,
chart = pointer.chart,
series = chart.series,
tooltip = chart.tooltip,
+ shared = tooltip ? tooltip.shared : false,
followPointer,
- point,
- points,
+ //point,
+ //points,
hoverPoint = chart.hoverPoint,
hoverSeries = chart.hoverSeries,
i,
- j,
+ trueXkd,
+ trueX,
+ //j,
distance = chart.chartWidth,
- index = pointer.getIndex(e),
- anchor;
+ rdistance = chart.chartWidth,
+ anchor,
- // shared tooltip
- if (tooltip && pointer.options.tooltip.shared && !(hoverSeries && hoverSeries.noSharedTooltip)) {
- points = [];
+ kdpoints = [],
+ kdpoint;
- // loop over all series and find the ones with points closest to the mouse
- i = series.length;
- for (j = 0; j < i; j++) {
- if (series[j].visible &&
- series[j].options.enableMouseTracking !== false &&
- !series[j].noSharedTooltip && series[j].singularTooltips !== true && series[j].tooltipPoints.length) {
- point = series[j].tooltipPoints[index];
- if (point && point.series) { // not a dummy point, #1544
- point._dist = mathAbs(index - point.clientX);
- distance = mathMin(distance, point._dist);
- points.push(point);
- }
+ // For hovering over the empty parts of the plot area (hoverSeries is undefined).
+ // If there is one series with point tracking (combo chart), don't go to nearest neighbour.
+ if (!shared && !hoverSeries) {
+ for (i = 0; i < series.length; i++) {
+ if (series[i].directTouch || !series[i].options.stickyTracking) {
+ series = [];
}
}
- // remove furthest points
- i = points.length;
- while (i--) {
- if (points[i]._dist > distance) {
- points.splice(i, 1);
+ }
+
+ if (shared || !hoverSeries) {
+ // Find nearest points on all series
+ each(series, function (s) {
+ // Skip hidden series
+ if (s.visible && pick(s.options.enableMouseTracking, true)) {
+ kdpoints.push(s.searchPoint(e));
}
- }
- // refresh the tooltip if necessary
- if (points.length && (points[0].clientX !== pointer.hoverX)) {
- tooltip.refresh(points, e);
- pointer.hoverX = points[0].clientX;
- }
+ });
+ // Find absolute nearest point
+ each(kdpoints, function (p) {
+ if (p && defined(p.plotX) && defined(p.plotY)) {
+ if ((p.dist.distX < distance) || ((p.dist.distX === distance || p.series.kdDimensions > 1) && p.dist.distR < rdistance)) {
+ distance = p.dist.distX;
+ rdistance = p.dist.distR;
+ kdpoint = p;
+ }
+ }
+ //point = kdpoints[0];
+ });
+ } else {
+ kdpoint = hoverSeries ? hoverSeries.searchPoint(e) : UNDEFINED;
}
- // Separate tooltip and general mouse events
- followPointer = hoverSeries && hoverSeries.tooltipOptions.followPointer;
- if (hoverSeries && hoverSeries.tracker && !followPointer) { // #2584, #2830
-
- // get the point
- point = hoverSeries.tooltipPoints[index];
-
- // a new point is hovered, refresh the tooltip
- if (point && point !== hoverPoint) {
-
- // trigger the events
- point.onMouseOver(e);
-
+ // Refresh tooltip for kdpoint
+ if (kdpoint && tooltip && kdpoint !== hoverPoint) {
+ // Draw tooltip if necessary
+ if (shared && !kdpoint.series.noSharedTooltip) {
+ i = kdpoints.length;
+ trueXkd = kdpoint.plotX + kdpoint.series.xAxis.left;
+ while (i--) {
+ trueX = kdpoints[i].plotX + kdpoints[i].series.xAxis.left;
+ if (kdpoints[i].x !== kdpoint.x || trueX !== trueXkd || !defined(kdpoints[i].y) || (kdpoints[i].series.noSharedTooltip || false)) {
+ kdpoints.splice(i, 1);
+ }
+ }
+ tooltip.refresh(kdpoints, e);
+ each(kdpoints, function (point) {
+ point.onMouseOver(e);
+ });
+ } else {
+ tooltip.refresh(kdpoint, e);
+ kdpoint.onMouseOver(e);
}
-
- } else if (tooltip && followPointer && !tooltip.isHidden) {
- anchor = tooltip.getAnchor([{}], e);
- tooltip.updatePosition({ plotX: anchor[0], plotY: anchor[1] });
+
+ // Update positions (regardless of kdpoint or hoverPoint)
+ } else {
+ followPointer = hoverSeries && hoverSeries.tooltipOptions.followPointer;
+ if (tooltip && followPointer && !tooltip.isHidden) {
+ anchor = tooltip.getAnchor([{}], e);
+ tooltip.updatePosition({ plotX: anchor[0], plotY: anchor[1] });
+ }
}
-
- // Start the event listener to pick up the tooltip
- if (tooltip && !pointer._onDocumentMouseMove) {
- pointer._onDocumentMouseMove = function (e) {
- if (charts[hoverChartIndex]) {
- charts[hoverChartIndex].pointer.onDocumentMouseMove(e);
- }
- };
- addEvent(doc, 'mousemove', pointer._onDocumentMouseMove);
- }
-
- // Draw independent crosshairs
+
+ // Crosshair
each(chart.axes, function (axis) {
- axis.drawCrosshair(e, pick(point, hoverPoint));
- });
+ axis.drawCrosshair(e, pick(kdpoint, hoverPoint));
+ });
+
},
/**
@@ -9185,20 +9582,27 @@
tooltipPoints = tooltip && tooltip.shared ? chart.hoverPoints : hoverPoint;
// Narrow in allowMove
allowMove = allowMove && tooltip && tooltipPoints;
- // Check if the points have moved outside the plot area, #1003
- if (allowMove && splat(tooltipPoints)[0].plotX === UNDEFINED) {
+ // Check if the points have moved outside the plot area, #1003
+ if (allowMove && splat(tooltipPoints)[0].plotX === UNDEFINED) {
allowMove = false;
}
-
// Just move the tooltip, #349
if (allowMove) {
tooltip.refresh(tooltipPoints);
if (hoverPoint) { // #2500
hoverPoint.setState(hoverPoint.state, true);
+ each(chart.axes, function (axis) {
+ if (pick(axis.options.crosshair && axis.options.crosshair.snap, true)) {
+ axis.drawCrosshair(null, allowMove);
+ } else {
+ axis.hideCrosshair();
+ }
+ });
+
}
// Full reset
} else {
@@ -9357,11 +9761,12 @@
/**
* On mouse up or touch end across the entire document, drop the selection.
*/
drop: function (e) {
- var chart = this.chart,
+ var pointer = this,
+ chart = this.chart,
hasPinched = this.hasPinched;
if (this.selectionMarker) {
var selectionData = {
xAxis: [],
@@ -9378,24 +9783,22 @@
// a selection has been made
if (this.hasDragged || hasPinched) {
// record each axis' min and max
each(chart.axes, function (axis) {
- if (axis.zoomEnabled) {
+ if (axis.zoomEnabled && defined(axis.min) && (hasPinched || pointer[{ xAxis: 'zoomX', yAxis: 'zoomY' }[axis.coll]])) { // #859, #3569
var horiz = axis.horiz,
minPixelPadding = e.type === 'touchend' ? axis.minPixelPadding: 0, // #1207, #3075
selectionMin = axis.toValue((horiz ? selectionLeft : selectionTop) + minPixelPadding),
selectionMax = axis.toValue((horiz ? selectionLeft + selectionWidth : selectionTop + selectionHeight) - minPixelPadding);
- if (!isNaN(selectionMin) && !isNaN(selectionMax)) { // #859
- selectionData[axis.coll].push({
- axis: axis,
- min: mathMin(selectionMin, selectionMax), // for reversed axes,
- max: mathMax(selectionMin, selectionMax)
- });
- runZoom = true;
- }
+ selectionData[axis.coll].push({
+ axis: axis,
+ min: mathMin(selectionMin, selectionMax), // for reversed axes
+ max: mathMax(selectionMin, selectionMax)
+ });
+ runZoom = true;
}
});
if (runZoom) {
fireEvent(chart, 'selection', selectionData, function (args) {
chart.zoom(extend(args, hasPinched ? { animation: false } : null));
@@ -9717,11 +10120,10 @@
pinch: function (e) {
var self = this,
chart = self.chart,
pinchDown = self.pinchDown,
- followTouchMove = self.followTouchMove,
touches = e.touches,
touchesLength = touches.length,
lastValidTouch = self.lastValidTouch,
hasZoom = self.hasZoom,
selectionMarker = self.selectionMarker,
@@ -9729,11 +10131,11 @@
fireClickEvent = touchesLength === 1 && ((self.inClass(e.target, PREFIX + 'tracker') &&
chart.runTrackerClick) || self.runChartClick),
clip = {};
// On touch devices, only proceed to trigger click if a handler is defined
- if ((hasZoom || followTouchMove) && !fireClickEvent) {
+ if (hasZoom && !fireClickEvent) {
e.preventDefault();
}
// Normalize each touch
map(touches, function (e) {
@@ -9782,11 +10184,11 @@
// Scale and translate the groups to provide visual feedback during pinching
self.scaleGroups(transform, clip);
// Optionally move the tooltip on touchmove
- if (!hasZoom && followTouchMove && touchesLength === 1) {
+ if (!hasZoom && self.followTouchMove && touchesLength === 1) {
this.runPointActions(self.normalize(e));
} else if (self.res) {
self.res = false;
this.reset(false, 0);
}
@@ -9800,11 +10202,11 @@
if (e.touches.length === 1) {
e = this.normalize(e);
- if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) {
+ if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop) && !chart.openMenu) {
// Run mouse events and display tooltip etc
this.runPointActions(e);
this.pinch(e);
@@ -9937,11 +10339,11 @@
*/
init: function (chart, options) {
var legend = this,
itemStyle = options.itemStyle,
- padding = pick(options.padding, 8),
+ padding,
itemMarginTop = options.itemMarginTop || 0;
this.options = options;
if (!options.enabled) {
@@ -9949,17 +10351,16 @@
}
legend.itemStyle = itemStyle;
legend.itemHiddenStyle = merge(itemStyle, options.itemHiddenStyle);
legend.itemMarginTop = itemMarginTop;
- legend.padding = padding;
+ legend.padding = padding = pick(options.padding, 8);
legend.initialItemX = padding;
legend.initialItemY = padding - 5; // 5 is the number of pixels above the text
legend.maxItemWidth = 0;
legend.chart = chart;
legend.itemHeight = 0;
- legend.lastLineHeight = 0;
legend.symbolWidth = pick(options.symbolWidth, 16);
legend.pages = [];
// Render it
@@ -10216,11 +10617,10 @@
// if the item exceeds the width, start a new line
if (horizontal && legend.itemX - initialItemX + itemWidth >
(widthOption || (chart.chartWidth - 2 * padding - initialItemX - options.x))) {
legend.itemX = initialItemX;
legend.itemY += itemMarginTop + legend.lastLineHeight + itemMarginBottom;
- legend.lastLineHeight = 0; // reset for next line
}
// If the item exceeds the height, start a new column
/*if (!horizontal && legend.itemY + options.y + itemHeight > chart.chartHeight - spacingTop - spacingBottom) {
legend.itemY = legend.initialItemY;
@@ -10276,10 +10676,43 @@
});
return allItems;
},
/**
+ * Adjust the chart margins by reserving space for the legend on only one side
+ * of the chart. If the position is set to a corner, top or bottom is reserved
+ * for horizontal legends and left or right for vertical ones.
+ */
+ adjustMargins: function (margin, spacing) {
+ var chart = this.chart,
+ options = this.options,
+ // Use the first letter of each alignment option in order to detect the side
+ alignment = options.align[0] + options.verticalAlign[0] + options.layout[0];
+
+ if (this.display && !options.floating) {
+
+ each([
+ /(lth|ct|rth)/,
+ /(rtv|rm|rbv)/,
+ /(rbh|cb|lbh)/,
+ /(lbv|lm|ltv)/
+ ], function (alignments, side) {
+ if (alignments.test(alignment) && !defined(margin[side])) {
+ // Now we have detected on which side of the chart we should reserve space for the legend
+ chart[marginNames[side]] = mathMax(
+ chart[marginNames[side]],
+ chart.legend[(side + 1) % 2 ? 'legendHeight' : 'legendWidth'] +
+ [1, -1, -1, 1][side] * options[(side % 2) ? 'x' : 'y'] +
+ pick(options.margin, 12) +
+ spacing[side]
+ );
+ }
+ });
+ }
+ },
+
+ /**
* Render the legend. This method can be called both before and after
* chart.render. If called after, it will only rearrange items instead
* of creating new ones.
*/
render: function () {
@@ -10330,24 +10763,23 @@
legend.allItems = allItems;
legend.display = display = !!allItems.length;
// render the items
+ legend.lastLineHeight = 0;
each(allItems, function (item) {
legend.renderItem(item);
});
- // Draw the border
- legendWidth = options.width || legend.offsetWidth;
+ // Get the box
+ legendWidth = (options.width || legend.offsetWidth) + padding;
legendHeight = legend.lastItemY + legend.lastLineHeight + legend.titleHeight;
-
-
legendHeight = legend.handleOverflow(legendHeight);
+ legendHeight += padding;
+ // Draw the border and/or background
if (legendBorderWidth || legendBackgroundColor) {
- legendWidth += padding;
- legendHeight += padding;
if (!box) {
legend.box = box = renderer.rect(
0,
0,
@@ -10682,17 +11114,22 @@
/**
* The chart class
* @param {Object} options
* @param {Function} callback Function to run when the chart has loaded
*/
-function Chart() {
+var Chart = Highcharts.Chart = function () {
this.init.apply(this, arguments);
-}
+};
Chart.prototype = {
/**
+ * Hook for modules
+ */
+ callbacks: [],
+
+ /**
* Initialize the chart
*/
init: function (userOptions, callback) {
// Handle regular options
@@ -10821,22 +11258,10 @@
y >= 0 &&
y <= this.plotHeight;
},
/**
- * Adjust all axes tick amounts
- */
- adjustTickAmounts: function () {
- if (this.options.chart.alignTicks !== false) {
- each(this.axes, function (axis) {
- axis.adjustTickAmount();
- });
- }
- this.maxTicks = null;
- },
-
- /**
* Redraw legend, axes or series based on updated data
*
* @param {Boolean|Object} animation Whether to apply animation, and optionally animation
* configuration
*/
@@ -10922,12 +11347,10 @@
// set axes scales
each(axes, function (axis) {
axis.setScale();
});
}
-
- chart.adjustTickAmounts();
}
chart.getMargins(); // #3098
if (hasCartesianSeries) {
@@ -11055,12 +11478,10 @@
optionsArray = xAxisOptions.concat(yAxisOptions);
each(optionsArray, function (axisOptions) {
axis = new Axis(chart, axisOptions);
});
-
- chart.adjustTickAmounts();
},
/**
* Get the currently selected points from all series
@@ -11350,102 +11771,67 @@
if (useCanVG) {
// If we need canvg library, extend and configure the renderer
// to get the tracker for translating mouse events
chart.renderer.create(chart, container, chartWidth, chartHeight);
}
+ // Add a reference to the charts index
+ chart.renderer.chartIndex = chart.index;
},
/**
* Calculate margins by rendering axis labels in a preliminary position. Title,
* subtitle and legend have already been rendered at this stage, but will be
* moved into their final positions
*/
- getMargins: function () {
+ getMargins: function (skipAxes) {
var chart = this,
spacing = chart.spacing,
- axisOffset,
- legend = chart.legend,
margin = chart.margin,
- legendOptions = chart.options.legend,
- legendMargin = pick(legendOptions.margin, 20),
- legendX = legendOptions.x,
- legendY = legendOptions.y,
- align = legendOptions.align,
- verticalAlign = legendOptions.verticalAlign,
titleOffset = chart.titleOffset;
chart.resetMargins();
- axisOffset = chart.axisOffset;
// Adjust for title and subtitle
if (titleOffset && !defined(margin[0])) {
chart.plotTop = mathMax(chart.plotTop, titleOffset + chart.options.title.margin + spacing[0]);
}
// Adjust for legend
- if (legend.display && !legendOptions.floating) {
- if (align === 'right') { // horizontal alignment handled first
- if (!defined(margin[1])) {
- chart.marginRight = mathMax(
- chart.marginRight,
- legend.legendWidth - legendX + legendMargin + spacing[1]
- );
- }
- } else if (align === 'left') {
- if (!defined(margin[3])) {
- chart.plotLeft = mathMax(
- chart.plotLeft,
- legend.legendWidth + legendX + legendMargin + spacing[3]
- );
- }
+ chart.legend.adjustMargins(margin, spacing);
- } else if (verticalAlign === 'top') {
- if (!defined(margin[0])) {
- chart.plotTop = mathMax(
- chart.plotTop,
- legend.legendHeight + legendY + legendMargin + spacing[0]
- );
- }
-
- } else if (verticalAlign === 'bottom') {
- if (!defined(margin[2])) {
- chart.marginBottom = mathMax(
- chart.marginBottom,
- legend.legendHeight - legendY + legendMargin + spacing[2]
- );
- }
- }
- }
-
// adjust for scroller
if (chart.extraBottomMargin) {
chart.marginBottom += chart.extraBottomMargin;
}
if (chart.extraTopMargin) {
chart.plotTop += chart.extraTopMargin;
}
+ if (!skipAxes) {
+ this.getAxisMargins();
+ }
+ },
+ getAxisMargins: function () {
+
+ var chart = this,
+ axisOffset = chart.axisOffset = [0, 0, 0, 0], // top, right, bottom, left
+ margin = chart.margin;
+
// pre-render axes to get labels offset width
if (chart.hasCartesianSeries) {
each(chart.axes, function (axis) {
axis.getOffset();
});
}
-
- if (!defined(margin[3])) {
- chart.plotLeft += axisOffset[3];
- }
- if (!defined(margin[0])) {
- chart.plotTop += axisOffset[0];
- }
- if (!defined(margin[2])) {
- chart.marginBottom += axisOffset[2];
- }
- if (!defined(margin[1])) {
- chart.marginRight += axisOffset[1];
- }
+ // Add the axis offsets
+ each(marginNames, function (m, side) {
+ if (!defined(margin[side])) {
+ chart[m] += axisOffset[side];
+ }
+ });
+
chart.setChartSize();
},
/**
@@ -11639,18 +12025,15 @@
/**
* Initial margins before auto size margins are applied
*/
resetMargins: function () {
- var chart = this,
- spacing = chart.spacing,
- margin = chart.margin;
+ var chart = this;
- chart.plotTop = pick(margin[0], spacing[0]);
- chart.marginRight = pick(margin[1], spacing[1]);
- chart.marginBottom = pick(margin[2], spacing[2]);
- chart.plotLeft = pick(margin[3], spacing[3]);
+ each(marginNames, function (m, side) {
+ chart[m] = pick(chart.margin[side], chart.spacing[side]);
+ });
chart.axisOffset = [0, 0, 0, 0]; // top, right, bottom, left
chart.clipOffset = [0, 0, 0, 0];
},
/**
@@ -11838,13 +12221,10 @@
* Render series for the chart
*/
renderSeries: function () {
each(this.series, function (serie) {
serie.translate();
- if (serie.setTooltipPoints) {
- serie.setTooltipPoints();
- }
serie.render();
});
},
/**
@@ -11881,37 +12261,53 @@
*/
render: function () {
var chart = this,
axes = chart.axes,
renderer = chart.renderer,
- options = chart.options;
+ options = chart.options,
+ tempWidth,
+ tempHeight,
+ redoHorizontal,
+ redoVertical;
// Title
chart.setTitle();
// Legend
chart.legend = new Legend(chart, options.legend);
chart.getStacks(); // render stacks
+ // Get chart margins
+ chart.getMargins(true);
+ chart.setChartSize();
+
+ // Record preliminary dimensions for later comparison
+ tempWidth = chart.plotWidth;
+ tempHeight = chart.plotHeight = chart.plotHeight - 13; // 13 is the most common height of X axis labels
+
// Get margins by pre-rendering axes
- // set axes scales
each(axes, function (axis) {
axis.setScale();
});
+ chart.getAxisMargins();
- chart.getMargins();
+ // If the plot area size has changed significantly, calculate tick positions again
+ redoHorizontal = tempWidth / chart.plotWidth > 1.2;
+ redoVertical = tempHeight / chart.plotHeight > 1.1;
- chart.maxTicks = null; // reset for second pass
- each(axes, function (axis) {
- axis.setTickPositions(true); // update to reflect the new margins
- axis.setMaxTicks();
- });
- chart.adjustTickAmounts();
- chart.getMargins(); // second pass to check for new labels
+ if (redoHorizontal || redoVertical) {
+ chart.maxTicks = null; // reset for second pass
+ each(axes, function (axis) {
+ if ((axis.horiz && redoHorizontal) || (!axis.horiz && redoVertical)) {
+ axis.setTickInterval(true); // update to reflect the new margins
+ }
+ });
+ chart.getMargins(); // second pass to check for new labels
+ }
// Draw the borders and backgrounds
chart.drawChartBox();
@@ -12110,18 +12506,20 @@
// run callbacks
if (callback) {
callback.apply(chart, [chart]);
}
each(chart.callbacks, function (fn) {
- fn.apply(chart, [chart]);
+ if (chart.index !== UNDEFINED) { // Chart destroyed in its own callback (#3600)
+ fn.apply(chart, [chart]);
+ }
});
+ // Fire the load event
+ fireEvent(chart, 'load');
- // If the chart was rendered outside the top container, put it back in
+ // If the chart was rendered outside the top container, put it back in (#3679)
chart.cloneRenderTo(true);
-
- fireEvent(chart, 'load');
},
/**
* Creates arrays for spacing and margin from given options.
@@ -12135,13 +12533,10 @@
pick(options[target + 'Bottom'], tArray[2]),
pick(options[target + 'Left'], tArray[3])];
}
}; // end Chart
-// Hook for exporting module
-Chart.prototype.callbacks = [];
-
var CenteredSeriesMixin = Highcharts.CenteredSeriesMixin = {
/**
* Get the center of the pie based on the size and center options relative to the
* plot area. Borrowed by the polar and gauge series types.
*/
@@ -12187,10 +12582,11 @@
init: function (series, options, x) {
var point = this,
colors;
point.series = series;
+ point.color = series.color; // #3445
point.applyOptions(options, x);
point.pointAttr = {};
if (series.options.colorByPoint) {
colors = series.options.colors || series.chart.options.colors;
@@ -12427,11 +12823,11 @@
* - series.yData and series.processedYData contain clean x values, equivalent to series.data and series.points.
*
* @param {Object} chart
* @param {Object} options
*/
-var Series = function () {};
+var Series = Highcharts.Series = function () {};
Series.prototype = {
isCartesian: true,
type: 'line',
@@ -12587,19 +12983,31 @@
/**
* Return an auto incremented x value based on the pointStart and pointInterval options.
* This is only used if an x value is not given for the point that calls autoIncrement.
*/
autoIncrement: function () {
- var series = this,
- options = series.options,
- xIncrement = series.xIncrement;
+ var options = this.options,
+ xIncrement = this.xIncrement,
+ date,
+ pointInterval,
+ pointIntervalUnit = options.pointIntervalUnit;
+
xIncrement = pick(xIncrement, options.pointStart, 0);
-
- series.pointInterval = pick(series.pointInterval, options.pointInterval, 1);
-
- series.xIncrement = xIncrement + series.pointInterval;
+
+ this.pointInterval = pointInterval = pick(this.pointInterval, options.pointInterval, 1);
+
+ // Added code for pointInterval strings
+ if (pointIntervalUnit === 'month' || pointIntervalUnit === 'year') {
+ date = new Date(xIncrement);
+ date = (pointIntervalUnit === 'month') ?
+ +date[setMonth](date[getMonth]() + pointInterval) :
+ +date[setFullYear](date[getFullYear]() + pointInterval);
+ pointInterval = date - xIncrement;
+ }
+
+ this.xIncrement = xIncrement + pointInterval;
return xIncrement;
},
/**
* Divide the series data into segments divided by null values.
@@ -12654,11 +13062,12 @@
chartOptions = chart.options,
plotOptions = chartOptions.plotOptions,
userOptions = chart.userOptions || {},
userPlotOptions = userOptions.plotOptions || {},
typeOptions = plotOptions[this.type],
- options;
+ options,
+ zones;
this.userOptions = itemOptions;
options = merge(
typeOptions,
@@ -12679,12 +13088,29 @@
// Delete marker object if not allowed (#1125)
if (typeOptions.marker === null) {
delete options.marker;
}
+ // Handle color zones
+ this.zoneAxis = options.zoneAxis;
+ zones = this.zones = (options.zones || []).slice();
+ if ((options.negativeColor || options.negativeFillColor) && !options.zones) {
+ zones.push({
+ value: options[this.zoneAxis + 'Threshold'] || options.threshold || 0,
+ color: options.negativeColor,
+ fillColor: options.negativeFillColor
+ });
+ }
+ if (zones.length) { // Push one extra zone for the rest
+ if (defined(zones[zones.length - 1].value)) {
+ zones.push({
+ color: this.color,
+ fillColor: this.fillColor
+ });
+ }
+ }
return options;
-
},
getCyclic: function (prop, value, defaults) {
var i,
userOptions = this.userOptions,
@@ -12740,11 +13166,10 @@
options = series.options,
chart = series.chart,
firstPoint = null,
xAxis = series.xAxis,
hasCategories = xAxis && !!xAxis.categories,
- tooltipPoints = series.tooltipPoints,
i,
turboThreshold = options.turboThreshold,
pt,
xData = this.xData,
yData = this.yData,
@@ -12755,11 +13180,11 @@
dataLength = data.length;
redraw = pick(redraw, true);
// If the point count is the same as is was, just run Point.update which is
// cheaper, allows animation, and keeps references to points.
- if (updatePoints !== false && dataLength && oldDataLength === dataLength && !series.cropped && !series.hasGroupedData) {
+ if (updatePoints !== false && dataLength && oldDataLength === dataLength && !series.cropped && !series.hasGroupedData && series.visible) {
each(data, function (point, i) {
oldData[i].update(point, false, null, false);
});
} else {
@@ -12843,13 +13268,10 @@
while (i--) {
if (oldData[i] && oldData[i].destroy) {
oldData[i].destroy();
}
}
- if (tooltipPoints) { // #2594
- tooltipPoints.length = 0;
- }
// reset minRange (#878)
if (xAxis) {
xAxis.minRange = xAxis.userMinRange;
}
@@ -12880,11 +13302,10 @@
closestPointRange,
xAxis = series.xAxis,
i, // loop variable
options = series.options,
cropThreshold = options.cropThreshold,
- activePointCount = 0,
isCartesian = series.isCartesian,
xExtremes,
min,
max;
@@ -12913,23 +13334,18 @@
croppedData = this.cropData(series.xData, series.yData, min, max);
processedXData = croppedData.xData;
processedYData = croppedData.yData;
cropStart = croppedData.start;
cropped = true;
- activePointCount = processedXData.length;
}
}
// Find the closest distance between processed points
for (i = processedXData.length - 1; i >= 0; i--) {
distance = processedXData[i] - processedXData[i - 1];
- if (!cropped && processedXData[i] > min && processedXData[i] < max) {
- activePointCount++;
- }
-
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)
@@ -12941,11 +13357,10 @@
// Record the properties
series.cropped = cropped; // undefined or true
series.cropStart = cropStart;
series.processedXData = processedXData;
series.processedYData = processedYData;
- series.activePointCount = activePointCount;
if (options.pointRange === null) { // null means auto, as for columns, candlesticks and OHLC
series.pointRange = closestPointRange || 1;
}
series.closestPointRange = closestPointRange;
@@ -13122,11 +13537,15 @@
dataLength = points.length,
hasModifyValue = !!series.modifyValue,
i,
pointPlacement = options.pointPlacement,
dynamicallyPlaced = pointPlacement === 'between' || isNumber(pointPlacement),
- threshold = options.threshold;
+ threshold = options.threshold,
+ plotX,
+ plotY,
+ lastPlotX,
+ closestPointRangePx = Number.MAX_VALUE;
// Translate each point
for (i = 0; i < dataLength; i++) {
var point = points[i],
xValue = point.x,
@@ -13134,18 +13553,18 @@
yBottom = point.low,
stack = stacking && yAxis.stacks[(series.negStacks && yValue < threshold ? '-' : '') + series.stackKey],
pointStack,
stackValues;
- // Discard disallowed y values for log axes
- if (yAxis.isLog && yValue <= 0) {
+ // Discard disallowed y values for log axes (#3434)
+ if (yAxis.isLog && yValue !== null && yValue <= 0) {
point.y = yValue = null;
error(10);
}
// Get the plotX translation
- point.plotX = xAxis.translate(xValue, 0, 0, 0, 1, pointPlacement, this.type === 'flags'); // Math.round fixes #591
+ point.plotX = plotX = xAxis.translate(xValue, 0, 0, 0, 1, pointPlacement, this.type === 'flags'); // Math.round fixes #591
// Calculate the bottom y value for stacked series
if (stacking && series.visible && stack && stack[xValue]) {
@@ -13179,74 +13598,116 @@
if (hasModifyValue) {
yValue = series.modifyValue(yValue, point);
}
// Set the the plotY value, reset it for redraws
- point.plotY = (typeof yValue === 'number' && yValue !== Infinity) ?
- //mathRound(yAxis.translate(yValue, 0, 1, 0, 1) * 10) / 10 : // Math.round fixes #591
- yAxis.translate(yValue, 0, 1, 0, 1) :
+ point.plotY = plotY = (typeof yValue === 'number' && yValue !== Infinity) ?
+ mathMin(mathMax(-1e5, yAxis.translate(yValue, 0, 1, 0, 1)), 1e5) : // #3201
UNDEFINED;
+ point.isInside = plotY !== UNDEFINED && plotY >= 0 && plotY <= yAxis.len && // #3519
+ plotX >= 0 && plotX <= xAxis.len;
+
// Set client related positions for mouse tracking
- point.clientX = dynamicallyPlaced ? xAxis.translate(xValue, 0, 0, 0, 1) : point.plotX; // #1514
+ point.clientX = dynamicallyPlaced ? xAxis.translate(xValue, 0, 0, 0, 1) : plotX; // #1514
point.negative = point.y < (threshold || 0);
// some API data
point.category = categories && categories[point.x] !== UNDEFINED ?
categories[point.x] : point.x;
+ // Determine auto enabling of markers (#3635)
+ if (i) {
+ closestPointRangePx = mathMin(closestPointRangePx, mathAbs(plotX - lastPlotX));
+ }
+ lastPlotX = plotX;
+
}
+ series.closestPointRangePx = closestPointRangePx;
+
// now that we have the cropped data, build the segments
series.getSegments();
},
/**
+ * 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,
+ 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].join(','),
+ clipRect = chart[sharedClipKey],
+ markerClipRect = chart[sharedClipKey + 'm'];
+
+ // If a clipping rectangle with the same properties is currently present in the chart, use that.
+ if (!clipRect) {
+
+ // When animation is set, prepare the initial positions
+ if (animation) {
+ clipBox.width = 0;
+
+ chart[sharedClipKey + 'm'] = markerClipRect = renderer.clipRect(
+ -99, // include the width of the first marker
+ inverted ? -chart.plotLeft : -chart.plotTop,
+ 99,
+ inverted ? chart.chartWidth : chart.chartHeight
+ );
+ }
+ chart[sharedClipKey] = clipRect = renderer.clipRect(clipBox);
+
+ }
+ clipRect.count += 1;
+
+ if (this.options.clip !== false) {
+ this.group.clip(animation || seriesClipBox ? clipRect : chart.clipRect);
+ this.markerGroup.clip(markerClipRect);
+ this.sharedClipKey = sharedClipKey;
+ }
+
+ // Remove the shared clipping rectancgle when all series are shown
+ if (!animation) {
+ clipRect.count -= 1;
+ if (clipRect.count === 0 && sharedClipKey && chart[sharedClipKey]) {
+ if (!seriesClipBox) {
+ chart[sharedClipKey] = chart[sharedClipKey].destroy();
+ }
+ if (chart[sharedClipKey + 'm']) {
+ chart[sharedClipKey + 'm'] = chart[sharedClipKey + 'm'].destroy();
+ }
+ }
+ }
+ },
+
+ /**
* Animate in the series
*/
animate: function (init) {
var series = this,
chart = series.chart,
- renderer = chart.renderer,
clipRect,
- markerClipRect,
animation = series.options.animation,
- clipBox = series.clipBox || chart.clipBox,
- inverted = chart.inverted,
sharedClipKey;
// Animation option is set to true
if (animation && !isObject(animation)) {
animation = defaultPlotOptions[series.type].animation;
}
- sharedClipKey = ['_sharedClip', animation.duration, animation.easing, clipBox.height].join(',');
// Initialize the animation. Set up the clipping rectangle.
if (init) {
- // If a clipping rectangle with the same properties is currently present in the chart, use that.
- clipRect = chart[sharedClipKey];
- markerClipRect = chart[sharedClipKey + 'm'];
- if (!clipRect) {
- chart[sharedClipKey] = clipRect = renderer.clipRect(
- extend(clipBox, { width: 0 })
- );
+ series.setClip(animation);
- 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
- );
- }
- series.group.clip(clipRect);
- series.markerGroup.clip(markerClipRect);
- series.sharedClipKey = sharedClipKey;
-
// Run the animation
} else {
+ sharedClipKey = this.sharedClipKey;
clipRect = chart[sharedClipKey];
if (clipRect) {
clipRect.animate({
width: chart.plotSizeX
}, animation);
@@ -13265,35 +13726,12 @@
/**
* This runs after animation to land on the final plot clipping
*/
afterAnimate: function () {
- var chart = this.chart,
- sharedClipKey = this.sharedClipKey,
- group = this.group,
- clipBox = this.clipBox;
-
- if (group && this.options.clip !== false) {
- if (!sharedClipKey || !clipBox) {
- group.clip(clipBox ? chart.renderer.clipRect(clipBox) : chart.clipRect);
- }
- this.markerGroup.clip(); // no clip
- }
-
+ this.setClip();
fireEvent(this, 'afterAnimate');
-
- // Remove the shared clipping rectancgle when all series are shown
- setTimeout(function () {
- if (sharedClipKey && chart[sharedClipKey]) {
- if (!clipBox) {
- chart[sharedClipKey] = chart[sharedClipKey].destroy();
- }
- if (chart[sharedClipKey + 'm']) {
- chart[sharedClipKey + 'm'] = chart[sharedClipKey + 'm'].destroy();
- }
- }
- }, 100);
},
/**
* Draw the markers
*/
@@ -13316,13 +13754,15 @@
pointMarkerOptions,
hasPointMarker,
enabled,
isInside,
markerGroup = series.markerGroup,
+ xAxis = series.xAxis,
globallyEnabled = pick(
seriesMarkerOptions.enabled,
- !series.requireSorting || series.activePointCount < (0.5 * series.xAxis.len / seriesMarkerOptions.radius)
+ xAxis.isRadial,
+ series.closestPointRangePx > 2 * seriesMarkerOptions.radius
);
if (seriesMarkerOptions.enabled !== false || series._hasPointMarkers) {
i = points.length;
@@ -13332,11 +13772,11 @@
plotY = point.plotY;
graphic = point.graphic;
pointMarkerOptions = point.marker || {};
hasPointMarker = !!point.marker;
enabled = (globallyEnabled && pointMarkerOptions.enabled === UNDEFINED) || pointMarkerOptions.enabled;
- isInside = chart.isInsidePlot(mathRound(plotX), plotY, chart.inverted); // #1858
+ isInside = point.isInside;
// only draw the point if y is defined
if (enabled && plotY !== UNDEFINED && !isNaN(plotY) && point.y !== null) {
// shortcuts
@@ -13413,10 +13853,11 @@
normalOptions = defaultPlotOptions[series.type].marker ? seriesOptions.marker : seriesOptions,
stateOptions = normalOptions.states,
stateOptionsHover = stateOptions[HOVER_STATE],
pointStateOptionsHover,
seriesColor = series.color,
+ seriesNegativeColor = series.options.negativeColor,
normalDefaults = {
stroke: seriesColor,
fill: seriesColor
},
points = series.points || [], // #927
@@ -13424,14 +13865,15 @@
point,
seriesPointAttr = [],
pointAttr,
pointAttrToOptions = series.pointAttrToOptions,
hasPointSpecificOptions = series.hasPointSpecificOptions,
- negativeColor = seriesOptions.negativeColor,
defaultLineColor = normalOptions.lineColor,
defaultFillColor = normalOptions.fillColor,
turboThreshold = seriesOptions.turboThreshold,
+ zones = series.zones,
+ zoneAxis = series.zoneAxis || 'y',
attr,
key;
// series type specific modifications
if (seriesOptions.marker) { // line, spline, area, areaspline, scatter
@@ -13444,10 +13886,15 @@
// if no hover color is given, brighten the normal color
stateOptionsHover.color = stateOptionsHover.color ||
Color(stateOptionsHover.color || seriesColor)
.brighten(stateOptionsHover.brightness).get();
+
+ // if no hover negativeColor is given, brighten the normal negativeColor
+ stateOptionsHover.negativeColor = stateOptionsHover.negativeColor ||
+ Color(stateOptionsHover.negativeColor || seriesNegativeColor)
+ .brighten(stateOptionsHover.brightness).get();
}
// general point attributes for the series normal state
seriesPointAttr[NORMAL_STATE] = series.convertAttribs(normalOptions, normalDefaults);
@@ -13471,12 +13918,18 @@
normalOptions = (point.options && point.options.marker) || point.options;
if (normalOptions && normalOptions.enabled === false) {
normalOptions.radius = 0;
}
- if (point.negative && negativeColor) {
- point.color = point.fillColor = negativeColor;
+ if (zones.length) {
+ var j = 0,
+ threshold = zones[j];
+ while (point[zoneAxis] >= threshold.value) {
+ threshold = zones[++j];
+ }
+
+ point.color = point.fillColor = threshold.color;
}
hasPointSpecificOptions = seriesOptions.colorByPoint || point.color; // #868
// check if the point has specific visual options
@@ -13497,11 +13950,11 @@
pointStateOptionsHover = stateOptions[HOVER_STATE] = stateOptions[HOVER_STATE] || {};
// Handle colors for column and pies
if (!seriesOptions.marker) { // column, bar, point
// If no hover color is given, brighten the normal color. #1619, #2579
- pointStateOptionsHover.color = pointStateOptionsHover.color || (!point.options.color && stateOptionsHover.color) ||
+ pointStateOptionsHover.color = pointStateOptionsHover.color || (!point.options.color && stateOptionsHover[(point.negative && seriesNegativeColor ? 'negativeColor' : 'color')]) ||
Color(point.color)
.brighten(pointStateOptionsHover.brightness || stateOptionsHover.brightness)
.get();
}
@@ -13707,40 +14160,40 @@
* Draw the actual graph
*/
drawGraph: function () {
var series = this,
options = this.options,
- props = [['graph', options.lineColor || this.color]],
+ props = [['graph', options.lineColor || this.color, options.dashStyle]],
lineWidth = options.lineWidth,
- dashStyle = options.dashStyle,
roundCap = options.linecap !== 'square',
graphPath = this.getGraphPath(),
- negativeColor = options.negativeColor;
+ fillColor = (this.fillGraph && this.color) || NONE, // polygon series use filled graph
+ zones = this.zones;
- if (negativeColor) {
- props.push(['graphNeg', negativeColor]);
- }
-
- // draw the graph
+ each(zones, function (threshold, i) {
+ props.push(['colorGraph' + i, threshold.color || series.color, threshold.dashStyle || options.dashStyle]);
+ });
+
+ // Draw the graph
each(props, function (prop, i) {
var graphKey = prop[0],
graph = series[graphKey],
attribs;
if (graph) {
stop(graph); // cancel running animations, #459
graph.animate({ d: graphPath });
- } else if (lineWidth && graphPath.length) { // #1487
+ } else if ((lineWidth || fillColor) && graphPath.length) { // #1487
attribs = {
stroke: prop[1],
'stroke-width': lineWidth,
- fill: NONE,
+ fill: fillColor,
zIndex: 1 // #1069
};
- if (dashStyle) {
- attribs.dashstyle = dashStyle;
+ if (prop[2]) {
+ attribs.dashstyle = prop[2];
} else if (roundCap) {
attribs['stroke-linecap'] = attribs['stroke-linejoin'] = 'round';
}
series[graphKey] = series.chart.renderer.path(graphPath)
@@ -13752,92 +14205,93 @@
},
/**
* Clip the graphs into the positive and negative coloured graphs
*/
- clipNeg: function () {
- var options = this.options,
+ applyZones: function () {
+ var series = this,
chart = this.chart,
renderer = chart.renderer,
- negativeColor = options.negativeColor || options.negativeFillColor,
- translatedThreshold,
- posAttr,
- negAttr,
+ zones = this.zones,
+ translatedFrom,
+ translatedTo,
+ clips = this.clips || [],
+ clipAttr,
graph = this.graph,
area = this.area,
- posClip = this.posClip,
- negClip = this.negClip,
- chartWidth = chart.chartWidth,
- chartHeight = chart.chartHeight,
- chartSizeMax = mathMax(chartWidth, chartHeight),
- yAxis = this.yAxis,
- above,
- below;
+ chartSizeMax = mathMax(chart.chartWidth, chart.chartHeight),
+ zoneAxis = this.zoneAxis || 'y',
+ axis = this[zoneAxis + 'Axis'],
+ reversed = axis.reversed,
+ horiz = axis.horiz;
- if (negativeColor && (graph || area)) {
- translatedThreshold = mathRound(yAxis.toPixels(options.threshold || 0, true));
- if (translatedThreshold < 0) {
- chartSizeMax -= translatedThreshold; // #2534
- }
- above = {
- x: 0,
- y: 0,
- width: chartSizeMax,
- height: translatedThreshold
- };
- below = {
- x: 0,
- y: translatedThreshold,
- width: chartSizeMax,
- height: chartSizeMax
- };
+ if (zones.length && (graph || area)) {
+ // The use of the Color Threshold assumes there are no gaps
+ // so it is safe to hide the original graph and area
+ graph.hide();
+ if (area) { area.hide(); }
- if (chart.inverted) {
+ // Create the clips
+ each(zones, function (threshold, i) {
+ translatedFrom = pick(translatedTo, (reversed ? (horiz ? chart.plotWidth : 0) : (horiz ? 0 : axis.toPixels(axis.min))));
+ translatedTo = mathRound(axis.toPixels(pick(threshold.value, axis.max), true));
- above.height = below.y = chart.plotWidth - translatedThreshold;
- if (renderer.isVML) {
- above = {
- x: chart.plotWidth - translatedThreshold - chart.plotLeft,
+ if (axis.isXAxis) {
+ clipAttr = {
+ x: reversed ? translatedTo : translatedFrom,
y: 0,
- width: chartWidth,
- height: chartHeight
+ width: Math.abs(translatedFrom - translatedTo),
+ height: chartSizeMax
};
- below = {
- x: translatedThreshold + chart.plotLeft - chartWidth,
- y: 0,
- width: chart.plotLeft + translatedThreshold,
- height: chartWidth
+ if (!horiz) {
+ clipAttr.x = chart.plotHeight - clipAttr.x;
+ }
+ } else {
+ clipAttr = {
+ x: 0,
+ y: reversed ? translatedFrom : translatedTo,
+ width: chartSizeMax,
+ height: Math.abs(translatedFrom - translatedTo)
};
+ if (horiz) {
+ clipAttr.y = chart.plotWidth - clipAttr.y;
+ }
+ }
+
+ /// VML SUPPPORT
+ if (chart.inverted && renderer.isVML) {
+ if (axis.isXAxis) {
+ clipAttr = {
+ x: 0,
+ y: reversed ? translatedFrom : translatedTo,
+ height: clipAttr.width,
+ width: chart.chartWidth
+ };
+ } else {
+ clipAttr = {
+ x: clipAttr.y - chart.plotLeft - chart.spacingBox.x,
+ y: 0,
+ width: clipAttr.height,
+ height: chart.chartHeight
+ };
+ }
}
- }
+ /// END OF VML SUPPORT
- if (yAxis.reversed) {
- posAttr = below;
- negAttr = above;
- } else {
- posAttr = above;
- negAttr = below;
- }
+ if (clips[i]) {
+ clips[i].animate(clipAttr);
+ } else {
+ clips[i] = renderer.clipRect(clipAttr);
- if (posClip) { // update
- posClip.animate(posAttr);
- negClip.animate(negAttr);
- } else {
+ series['colorGraph' + i].clip(clips[i]);
- this.posClip = posClip = renderer.clipRect(posAttr);
- this.negClip = negClip = renderer.clipRect(negAttr);
-
- if (graph && this.graphNeg) {
- graph.clip(posClip);
- this.graphNeg.clip(negClip);
+ if (area) {
+ series['colorArea' + i].clip(clips[i]);
+ }
}
-
- if (area) {
- area.clip(posClip);
- this.areaNeg.clip(negClip);
- }
- }
+ });
+ this.clips = clips;
}
},
/**
* Initialize and perform group inversion on series.group and series.markerGroup
@@ -13966,11 +14420,11 @@
group.inverted = series.isCartesian ? chart.inverted : false;
// draw the graph if any
if (series.drawGraph) {
series.drawGraph();
- series.clipNeg();
+ series.applyZones();
}
each(series.points, function (point) {
if (point.redraw) {
point.redraw();
@@ -13996,15 +14450,10 @@
// Handle inverted series and tracker groups
if (chart.inverted) {
series.invertGroups();
}
- // Initial clipping, must be defined after inverting groups for VML
- if (options.clip !== false && !series.sharedClipKey && !hasRendered) {
- group.clip(chart.clipRect);
- }
-
// Run the animation
if (animDuration) {
series.animate();
}
@@ -14050,19 +14499,144 @@
translateY: pick(yAxis && yAxis.top, chart.plotTop)
});
}
series.translate();
- if (series.setTooltipPoints) {
- series.setTooltipPoints(true);
- }
series.render();
if (wasDirtyData) {
fireEvent(series, 'updatedData');
}
+ },
+
+ /**
+ * KD Tree && PointSearching Implementation
+ */
+
+ kdDimensions: 1,
+ kdTree: null,
+ kdAxisArray: ['plotX', 'plotY'],
+ kdComparer: 'distX',
+
+ searchPoint: function (e) {
+ var series = this,
+ xAxis = series.xAxis,
+ yAxis = series.yAxis,
+ inverted = series.chart.inverted;
+
+ e.plotX = inverted ? xAxis.len - e.chartY + xAxis.pos : e.chartX - xAxis.pos;
+ e.plotY = inverted ? yAxis.len - e.chartX + yAxis.pos : e.chartY - yAxis.pos;
+
+ return this.searchKDTree(e);
+ },
+
+ buildKDTree: function () {
+ var series = this,
+ dimensions = series.kdDimensions;
+
+ // Internal function
+ function _kdtree(points, depth, dimensions) {
+ var axis, median, length = points && points.length;
+
+ if (length) {
+
+ // alternate between the axis
+ axis = series.kdAxisArray[depth % dimensions];
+
+ // sort point array
+ points.sort(function(a, b) {
+ return a[axis] - b[axis];
+ });
+
+ median = Math.floor(length / 2);
+
+ // build and return node
+ return {
+ point: points[median],
+ left: _kdtree(points.slice(0, median), depth + 1, dimensions),
+ right: _kdtree(points.slice(median + 1), depth + 1, dimensions)
+ };
+
+ }
+ }
+
+ function startRecursive() {
+ series.kdTree = _kdtree(series.points, dimensions, dimensions);
+ }
+
+ delete series.kdTree;
+
+ if (series.options.kdSync) { // For testing tooltips, don't build async
+ startRecursive();
+ } else {
+ setTimeout(startRecursive);
+ }
+ },
+
+ searchKDTree: function (point) {
+ var series = this,
+ kdComparer = this.kdComparer,
+ kdX = this.kdAxisArray[0],
+ kdY = this.kdAxisArray[1];
+
+ // Internal function
+ function _distance(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);
+
+ return {
+ distX: defined(x) ? Math.sqrt(x) : Number.MAX_VALUE,
+ distY: defined(y) ? Math.sqrt(y) : Number.MAX_VALUE,
+ distR: defined(r) ? Math.sqrt(r) : Number.MAX_VALUE
+ };
+ }
+ function _search(search, tree, depth, dimensions) {
+ var point = tree.point,
+ axis = series.kdAxisArray[depth % dimensions],
+ tdist,
+ sideA,
+ sideB,
+ ret = point,
+ nPoint1,
+ nPoint2;
+ point.dist = _distance(search, point);
+
+ // Pick side based on distance to splitting point
+ tdist = search[axis] - point[axis];
+ sideA = tdist < 0 ? 'left' : 'right';
+
+ // End of tree
+ if (tree[sideA]) {
+ nPoint1 =_search(search, tree[sideA], depth + 1, dimensions);
+
+ ret = (nPoint1.dist[kdComparer] < ret.dist[kdComparer] ? nPoint1 : point);
+
+ sideB = tdist < 0 ? 'right' : 'left';
+ 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.dist[kdComparer]) {
+ nPoint2 = _search(search, tree[sideB], depth + 1, dimensions);
+ ret = (nPoint2.dist[kdComparer] < ret.dist[kdComparer] ? nPoint2 : ret);
+ }
+ }
+ }
+ return ret;
+ }
+
+ if (!this.kdTree) {
+ this.buildKDTree();
+ }
+
+ if (this.kdTree) {
+ return _search(point,
+ this.kdTree, this.kdDimensions, this.kdDimensions);
+ } else {
+ return UNDEFINED;
+ }
}
+
}; // end Series prototype
/**
* The class for stack items
*/
@@ -14508,11 +15082,12 @@
var point = this,
series = point.series,
graphic = point.graphic,
i,
chart = series.chart,
- seriesOptions = series.options;
+ seriesOptions = series.options,
+ names = series.xAxis.names;
redraw = pick(redraw, true);
function update() {
@@ -14537,10 +15112,13 @@
}
// record changes in the parallel arrays
i = point.index;
series.updateParallelArrays(point, i);
+ if (names && point.name) {
+ names[point.x] = point.name;
+ }
seriesOptions.data[i] = point.options;
// redraw
series.isDirty = series.isDirtyData = true;
@@ -14569,41 +15147,11 @@
* @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
* @param {Boolean|Object} animation Whether to apply animation, and optionally animation
* configuration
*/
remove: function (redraw, animation) {
- var point = this,
- series = point.series,
- points = series.points,
- chart = series.chart,
- i,
- data = series.data;
-
- setAnimation(animation, chart);
- redraw = pick(redraw, true);
-
- // fire the event with a default handler of removing the point
- point.firePointEvent('remove', null, function () {
-
- // splice all the parallel arrays
- i = inArray(point, data);
- if (data.length === points.length) {
- points.splice(i, 1);
- }
- data.splice(i, 1);
- series.options.data.splice(i, 1);
- series.updateParallelArrays(point, 'splice', i, 1);
-
- point.destroy();
-
- // redraw
- series.isDirty = true;
- series.isDirtyData = true;
- if (redraw) {
- chart.redraw();
- }
- });
+ this.series.removePoint(inArray(this, this.series.data), redraw, animation);
}
});
// Extend the series prototype for dynamic methods
extend(Series.prototype, {
@@ -14703,10 +15251,52 @@
chart.redraw();
}
},
/**
+ * Remove a point (rendered or not), by index
+ */
+ removePoint: function (i, redraw, animation) {
+
+ var series = this,
+ data = series.data,
+ point = data[i],
+ points = series.points,
+ chart = series.chart,
+ remove = function () {
+
+ if (data.length === points.length) {
+ points.splice(i, 1);
+ }
+ data.splice(i, 1);
+ series.options.data.splice(i, 1);
+ series.updateParallelArrays(point || { series: series }, 'splice', i, 1);
+
+ if (point) {
+ point.destroy();
+ }
+
+ // redraw
+ series.isDirty = true;
+ series.isDirtyData = true;
+ if (redraw) {
+ chart.redraw();
+ }
+ };
+
+ setAnimation(animation, chart);
+ redraw = pick(redraw, true);
+
+ // Fire the event with a default handler of removing the point
+ if (point) {
+ point.firePointEvent('remove', null, remove);
+ } else {
+ remove();
+ }
+ },
+
+ /**
* Remove a series and optionally redraw the chart
*
* @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
* @param {Boolean|Object} animation Whether to apply animation, and optionally animation
* configuration
@@ -14754,10 +15344,15 @@
oldType = this.type,
proto = seriesTypes[oldType].prototype,
preserve = ['group', 'markerGroup', 'dataLabelsGroup'],
n;
+ // If we're changing type or zIndex, create new groups (#3380, #3404)
+ if ((newOptions.type && newOptions.type !== oldType) || newOptions.zIndex !== undefined) {
+ preserve.length = 0;
+ }
+
// Make sure groups are not destroyed (#3094)
each(preserve, function (prop) {
preserve[prop] = series[prop];
delete series[prop];
});
@@ -14767,25 +15362,23 @@
animation: false,
index: this.index,
pointStart: this.xData[0] // when updating after addPoint
}, { data: this.options.data }, newOptions);
- // Destroy the series and reinsert methods from the type prototype
+ // Destroy the series and delete all properties. Reinsert all methods
+ // and properties from the new type prototype (#2270, #3719)
this.remove(false);
- for (n in proto) { // Overwrite series-type specific methods (#2270)
- if (proto.hasOwnProperty(n)) {
- this[n] = UNDEFINED;
- }
+ for (n in proto) {
+ this[n] = UNDEFINED;
}
extend(this, seriesTypes[newOptions.type || oldType].prototype);
// Re-register groups (#3094)
each(preserve, function (prop) {
series[prop] = preserve[prop];
});
-
this.init(chart, newOptions);
chart.linkSeries(); // Links are lost in this.remove (#3028)
if (pick(redraw, true)) {
chart.redraw(false);
}
@@ -15045,18 +15638,16 @@
// Define local variables
var series = this,
areaPath = this.areaPath,
options = this.options,
- negativeColor = options.negativeColor,
- negativeFillColor = options.negativeFillColor,
+ zones = this.zones,
props = [['area', this.color, options.fillColor]]; // area name, main color, fill color
- if (negativeColor || negativeFillColor) {
- props.push(['areaNeg', negativeColor, negativeFillColor]);
- }
-
+ each(zones, function (threshold, i) {
+ props.push(['colorArea' + i, threshold.color || series.color, threshold.fillColor || options.fillColor]);
+ });
each(props, function (prop) {
var areaKey = prop[0],
area = series[areaKey];
// Create or update the area
@@ -15276,10 +15867,11 @@
stroke: 'borderColor',
fill: 'color',
r: 'borderRadius'
},
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)
/**
@@ -15376,11 +15968,11 @@
var series = this,
chart = series.chart,
options = series.options,
borderWidth = series.borderWidth = pick(
options.borderWidth,
- series.activePointCount > 0.5 * series.xAxis.len ? 0 : 1
+ series.closestPointRange * series.xAxis.transA < 2 ? 0 : 1 // #3635
),
yAxis = series.yAxis,
threshold = options.threshold,
translatedThreshold = series.translatedThreshold = yAxis.getThreshold(threshold),
minPointLength = pick(options.minPointLength, 5),
@@ -15429,22 +16021,22 @@
// Cache for access in polar
point.barX = barX;
point.pointWidth = pointWidth;
- // Fix the tooltip on center of grouped columns (#1216, #424)
+ // Fix the tooltip on center of grouped columns (#1216, #424, #3648)
point.tooltipPos = chart.inverted ?
- [yAxis.len - plotY, series.xAxis.len - barX - barW / 2] :
+ [yAxis.len + yAxis.pos - chart.plotLeft - plotY, series.xAxis.len - barX - barW / 2] :
[barX + barW / 2, plotY + yAxis.pos - chart.plotTop];
// Round off to obtain crisp edges and avoid overlapping with neighbours (#2694)
right = mathRound(barX + barW) + xCrisp;
barX = mathRound(barX) + xCrisp;
barW = right - barX;
fromTop = mathAbs(barY) < 0.5;
- bottom = mathRound(barY + barH) + yCrisp;
+ bottom = mathMin(mathRound(barY + barH) + yCrisp, 9e4); // #3575
barY = mathRound(barY) + yCrisp;
barH = bottom - barY;
// Top edges are exceptions
if (fromTop) {
@@ -15511,12 +16103,12 @@
stop(graphic);
graphic.attr(borderAttr)[chart.pointCount < animationLimit ? 'animate' : 'attr'](merge(shapeArgs));
} else {
point.graphic = graphic = renderer[point.shapeType](shapeArgs)
- .attr(pointAttr)
.attr(borderAttr)
+ .attr(pointAttr)
.add(series.group)
.shadow(options.shadow, null, options.stacking && !options.borderRadius);
}
} else if (graphic) {
@@ -15597,28 +16189,31 @@
/**
* Set the default options for scatter
*/
defaultPlotOptions.scatter = merge(defaultSeriesOptions, {
lineWidth: 0,
+ marker: {
+ enabled: true // Overrides auto-enabling in line series (#3647)
+ },
tooltip: {
headerFormat: '<span style="color:{series.color}">\u25CF</span> <span style="font-size: 10px;"> {series.name}</span><br/>',
pointFormat: 'x: <b>{point.x}</b><br/>y: <b>{point.y}</b><br/>'
- },
- stickyTracking: false
+ }
});
/**
* The scatter series class
*/
var ScatterSeries = extendClass(Series, {
type: 'scatter',
sorted: false,
requireSorting: false,
noSharedTooltip: true,
- trackerGroups: ['markerGroup', 'dataLabelsGroup'],
+ trackerGroups: ['group', 'markerGroup', 'dataLabelsGroup'],
takeOrdinalPosition: false, // #2342
- singularTooltips: true,
+ kdDimensions: 2,
+ kdComparer: 'distR',
drawGraph: function () {
if (this.options.lineWidth) {
Series.prototype.drawGraph.call(this);
}
}
@@ -15642,13 +16237,14 @@
// connectorPadding: 5,
distance: 30,
enabled: true,
formatter: function () { // #2945
return this.point.name;
- }
+ },
// softConnector: true,
- //y: 0
+ x: 0
+ // y: 0
},
ignoreHiddenPoint: true,
//innerSize: 0,
legendType: 'point',
marker: null, // point options are specified in the base options
@@ -15679,16 +16275,10 @@
Point.prototype.init.apply(this, arguments);
var point = this,
toggleSlice;
- // Disallow negative values (#1530)
- if (point.y < 0) {
- point.y = null;
- }
-
- //visible: options.visible !== false,
extend(point, {
visible: point.visible !== false,
name: pick(point.name, 'Slice')
});
@@ -15793,11 +16383,10 @@
pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
stroke: 'borderColor',
'stroke-width': 'borderWidth',
fill: 'color'
},
- singularTooltips: true,
/**
* Pies have one color each point
*/
getColor: noop,
@@ -15868,10 +16457,16 @@
len = points.length;
// Get the total sum
for (i = 0; i < len; i++) {
point = points[i];
+
+ // Disallow negative values (#1530, #3623)
+ if (point.y < 0) {
+ point.y = null;
+ }
+
total += (ignoreHiddenPoint && !point.visible) ? 0 : point.y;
}
this.total = total;
// Set each point's properties
@@ -16064,10 +16659,13 @@
});
},
+
+ searchPoint: noop,
+
/**
* Utility for sorting data labels
*/
sortByAngle: function (points, sign) {
points.sort(function (a, b) {
@@ -16106,11 +16704,12 @@
points = series.points,
pointOptions,
generalOptions,
hasRendered = series.hasRendered || 0,
str,
- dataLabelsGroup;
+ dataLabelsGroup,
+ renderer = series.chart.renderer;
if (options.enabled || series._hasPointLabels) {
// Process default alignment of data labels for columns
if (series.dlProcessOptions) {
@@ -16146,14 +16745,16 @@
labelConfig,
attr,
name,
rotation,
connector = point.connector,
- isNew = true;
+ isNew = true,
+ style,
+ moreStyle = {};
// Determine if each data label is enabled
- pointOptions = point.options && point.options.dataLabels;
+ pointOptions = point.dlOptions || (point.options && point.options.dataLabels); // dlOptions is used in treemaps
enabled = pick(pointOptions && pointOptions.enabled, generalOptions.enabled); // #2282
// If the point is outside the plot area, destroy it. #678, #820
if (dataLabel && !enabled) {
@@ -16164,21 +16765,22 @@
} else if (enabled) {
// Create individual options structure that can be extended without
// affecting others
options = merge(generalOptions, pointOptions);
+ style = options.style;
rotation = options.rotation;
// Get the string
labelConfig = point.getLabelConfig();
str = options.format ?
format(options.format, labelConfig) :
options.formatter.call(labelConfig, options);
// Determine the color
- options.style.color = pick(options.color, options.style.color, series.color, 'black');
+ style.color = pick(options.color, style.color, series.color, 'black');
// update existing label
if (dataLabel) {
@@ -16206,28 +16808,40 @@
r: options.borderRadius || 0,
rotation: rotation,
padding: options.padding,
zIndex: 1
};
+
+ // Get automated contrast color
+ if (style.color === 'contrast') {
+ moreStyle.color = options.inside || options.distance < 0 || !!seriesOptions.stacking ?
+ renderer.getContrast(point.color || series.color) :
+ '#000000';
+ }
+ if (cursor) {
+ moreStyle.cursor = cursor;
+ }
+
+
// Remove unused attributes (#947)
for (name in attr) {
if (attr[name] === UNDEFINED) {
delete attr[name];
}
}
- dataLabel = point.dataLabel = series.chart.renderer[rotation ? 'text' : 'label']( // labels don't support rotation
+ dataLabel = point.dataLabel = renderer[rotation ? 'text' : 'label']( // labels don't support rotation
str,
0,
-999,
null,
null,
null,
options.useHTML
)
.attr(attr)
- .css(extend(options.style, cursor && { cursor: cursor }))
+ .css(extend(style, moreStyle))
.add(dataLabelsGroup)
.shadow(options.shadow);
}
@@ -16247,10 +16861,12 @@
var chart = this.chart,
inverted = chart.inverted,
plotX = pick(point.plotX, -999),
plotY = pick(point.plotY, -999),
bBox = dataLabel.getBBox(),
+ baseline = chart.renderer.fontMetrics(options.style.fontSize).b,
+ rotCorr, // rotation correction
// Math.round for rounding errors (#2683), alignTo to allow column labels (#2700)
visible = this.visible && (point.series.forceDL || chart.isInsidePlot(plotX, mathRound(plotY), inverted) ||
(alignTo && chart.isInsidePlot(plotX, inverted ? alignTo.x + 1 : alignTo.y + alignTo.height - 1, inverted))),
alignAttr; // the final position;
@@ -16270,12 +16886,13 @@
height: bBox.height
});
// Allow a hook for changing alignment in the last moment, then do the alignment
if (options.rotation) { // Fancy box alignment isn't supported for rotated text
+ rotCorr = chart.renderer.rotCorr(baseline, options.rotation); // #3723
dataLabel[isNew ? 'attr' : 'animate']({
- x: alignTo.x + options.x + alignTo.width / 2,
+ x: alignTo.x + options.x + alignTo.width / 2 + rotCorr.x,
y: alignTo.y + options.y + alignTo.height / 2
})
.attr({ // #3003
align: options.align
});
@@ -16310,47 +16927,48 @@
Series.prototype.justifyDataLabel = function (dataLabel, options, alignAttr, bBox, alignTo, isNew) {
var chart = this.chart,
align = options.align,
verticalAlign = options.verticalAlign,
off,
- justified;
+ justified,
+ padding = dataLabel.box ? 0 : (dataLabel.padding || 0);
// Off left
- off = alignAttr.x;
+ off = alignAttr.x + padding;
if (off < 0) {
if (align === 'right') {
options.align = 'left';
} else {
options.x = -off;
}
justified = true;
}
// Off right
- off = alignAttr.x + bBox.width;
+ off = alignAttr.x + bBox.width - padding;
if (off > chart.plotWidth) {
if (align === 'left') {
options.align = 'right';
} else {
options.x = chart.plotWidth - off;
}
justified = true;
}
// Off top
- off = alignAttr.y;
+ off = alignAttr.y + padding;
if (off < 0) {
if (verticalAlign === 'bottom') {
options.verticalAlign = 'top';
} else {
options.y = -off;
}
justified = true;
}
// Off bottom
- off = alignAttr.y + bBox.height;
+ off = alignAttr.y + bBox.height - padding;
if (off > chart.plotHeight) {
if (verticalAlign === 'top') {
options.verticalAlign = 'bottom';
} else {
options.y = chart.plotHeight - off;
@@ -16771,24 +17389,24 @@
/**
* Override the basic data label alignment by adjusting for the position of the column
*/
seriesTypes.column.prototype.alignDataLabel = function (point, dataLabel, options, alignTo, isNew) {
- var chart = this.chart,
- inverted = chart.inverted,
+ var inverted = this.chart.inverted,
+ series = point.series,
dlBox = point.dlBox || point.shapeArgs, // data label box for alignment
- below = point.below || (point.plotY > pick(this.translatedThreshold, chart.plotSizeY)),
+ below = point.below || (point.plotY > pick(this.translatedThreshold, series.yAxis.len)),
inside = pick(options.inside, !!this.options.stacking); // draw it inside the box?
// Align to the column itself, or the top of it
if (dlBox) { // Area range uses this method but not alignTo
alignTo = merge(dlBox);
if (inverted) {
alignTo = {
- x: chart.plotWidth - alignTo.y - alignTo.height,
- y: chart.plotHeight - alignTo.x - alignTo.width,
+ x: series.yAxis.len - alignTo.y - alignTo.height,
+ y: series.xAxis.len - alignTo.x - alignTo.width,
width: alignTo.height,
height: alignTo.width
};
}
@@ -16822,10 +17440,109 @@
}
/**
+ * @license Highcharts JS v4.1.0 (2015-02-16)
+ * Highcharts module to hide overlapping data labels. This module is included by default in Highmaps.
+ *
+ * (c) 2010-2014 Torstein Honsi
+ *
+ * License: www.highcharts.com/license
+ */
+
+/*global Highcharts, HighchartsAdapter */
+(function (H) {
+ var Chart = H.Chart,
+ each = H.each,
+ addEvent = HighchartsAdapter.addEvent;
+
+ // Collect potensial overlapping data labels. Stack labels probably don't need to be
+ // considered because they are usually accompanied by data labels that lie inside the columns.
+ Chart.prototype.callbacks.push(function (chart) {
+ function collectAndHide() {
+ var labels = [];
+
+ each(chart.series, function (series) {
+ var dlOptions = series.options.dataLabels;
+ if ((dlOptions.enabled || series._hasPointLabels) && !dlOptions.allowOverlap) {
+ each(series.points, function (point) {
+ if (point.dataLabel) {
+ point.dataLabel.labelrank = point.labelrank;
+ labels.push(point.dataLabel);
+ }
+ });
+ }
+ });
+ chart.hideOverlappingLabels(labels);
+ }
+
+ // Do it now ...
+ collectAndHide();
+
+ // ... and after each chart redraw
+ addEvent(chart, 'redraw', collectAndHide);
+
+ });
+
+ /**
+ * Hide overlapping labels. Labels are moved and faded in and out on zoom to provide a smooth
+ * visual imression.
+ */
+ Chart.prototype.hideOverlappingLabels = function (labels) {
+
+ var len = labels.length,
+ label,
+ i,
+ j,
+ label1,
+ label2,
+ intersectRect = function (pos1, pos2, size1, size2) {
+ return !(
+ pos2.x > pos1.x + size1.width ||
+ pos2.x + size2.width < pos1.x ||
+ pos2.y > pos1.y + size1.height ||
+ pos2.y + size2.height < pos1.y
+ );
+ };
+
+ // Mark with initial opacity
+ for (i = 0; i < len; i++) {
+ label = labels[i];
+ if (label) {
+ label.oldOpacity = label.opacity;
+ label.newOpacity = 1;
+ }
+ }
+
+ // Detect overlapping labels
+ for (i = 0; i < len; i++) {
+ label1 = labels[i];
+
+ for (j = i + 1; j < len; ++j) {
+ label2 = labels[j];
+ if (label1 && label2 && label1.placed && label2.placed && label1.newOpacity !== 0 && label2.newOpacity !== 0 &&
+ intersectRect(label1.alignAttr, label2.alignAttr, label1, label2)) {
+ (label1.labelrank < label2.labelrank ? label1 : label2).newOpacity = 0;
+ }
+ }
+ }
+
+ // Hide or show
+ for (i = 0; i < len; i++) {
+ label = labels[i];
+ if (label) {
+ if (label.oldOpacity !== label.newOpacity && label.placed) {
+ label.alignAttr.opacity = label.newOpacity;
+ label[label.isOld && label.newOpacity ? 'animate' : 'attr'](label.alignAttr);
+ }
+ label.isOld = true;
+ }
+ }
+ };
+
+}(Highcharts));/**
* TrackerMixin for points and graphs
*/
var TrackerMixin = Highcharts.TrackerMixin = {
@@ -17039,12 +17756,13 @@
defaultChecked: item.selected // required by IE7
}, legend.options.itemCheckboxStyle, legend.chart.container);
addEvent(item.checkbox, 'click', function (event) {
var target = event.target;
- fireEvent(item, 'checkboxClick', {
- checked: target.checked
+ fireEvent(item.series || item, 'checkboxClick', { // #3712
+ checked: target.checked,
+ item: item
},
function () {
item.select();
}
);
@@ -17165,13 +17883,16 @@
axis = chart[isX ? 'xAxis' : 'yAxis'][0],
startPos = chart[isX ? 'mouseDownX' : 'mouseDownY'],
halfPointRange = (axis.pointRange || 0) / 2,
extremes = axis.getExtremes(),
newMin = axis.toValue(startPos - mousePos, true) + halfPointRange,
- newMax = axis.toValue(startPos + chart[isX ? 'plotWidth' : 'plotHeight'] - mousePos, true) - halfPointRange;
+ newMax = axis.toValue(startPos + chart[isX ? 'plotWidth' : 'plotHeight'] - mousePos, true) - halfPointRange,
+ goingLeft = startPos > mousePos; // #3613
- if (axis.series.length && newMin > mathMin(extremes.dataMin, extremes.min) && newMax < mathMax(extremes.dataMax, extremes.max)) {
+ if (axis.series.length &&
+ (goingLeft || newMin > mathMin(extremes.dataMin, extremes.min)) &&
+ (!goingLeft || newMax < mathMax(extremes.dataMax, extremes.max))) {
axis.setExtremes(newMin, newMax, false, false, { trigger: 'pan' });
doRedraw = true;
}
chart[isX ? 'mouseDownX' : 'mouseDownY'] = mousePos; // set new reference for next run
@@ -17390,11 +18111,11 @@
// Show me your halo
haloOptions = stateOptions[state] && stateOptions[state].halo;
if (haloOptions && haloOptions.size) {
if (!halo) {
series.halo = halo = chart.renderer.path()
- .add(series.seriesGroup);
+ .add(chart.seriesGroup);
}
halo.attr(extend({
fill: Color(point.color || series.color).setOpacity(haloOptions.opacity).get()
}, haloOptions.attributes))[move ? 'animate' : 'attr']({
d: point.haloPath(haloOptions.size)
@@ -17502,11 +18223,11 @@
if (stateOptions[state] && stateOptions[state].enabled === false) {
return;
}
if (state) {
- lineWidth = stateOptions[state].lineWidth || lineWidth + (stateOptions[state].lineWidthPlus || 0);
+ lineWidth = (stateOptions[state].lineWidth || lineWidth) + (stateOptions[state].lineWidthPlus || 0);
}
if (graph && !graph.dashstyle) { // hover is turned off for dashed lines in VML
attribs = {
'stroke-width': lineWidth
@@ -17582,78 +18303,10 @@
fireEvent(series, showOrHide);
},
/**
- * Memorize tooltip texts and positions
- */
- setTooltipPoints: function (renew) {
- var series = this,
- points = [],
- pointsLength,
- low,
- high,
- xAxis = series.xAxis,
- xExtremes = xAxis && xAxis.getExtremes(),
- axisLength = xAxis ? (xAxis.tooltipLen || xAxis.len) : series.chart.plotSizeX, // tooltipLen and tooltipPosName used in polar
- point,
- pointX,
- nextPoint,
- i,
- tooltipPoints = []; // a lookup array for each pixel in the x dimension
-
- // don't waste resources if tracker is disabled
- if (series.options.enableMouseTracking === false || series.singularTooltips) {
- return;
- }
-
- // renew
- if (renew) {
- series.tooltipPoints = null;
- }
-
- // concat segments to overcome null values
- each(series.segments || series.points, function (segment) {
- points = points.concat(segment);
- });
-
- // Reverse the points in case the X axis is reversed
- if (xAxis && xAxis.reversed) {
- points = points.reverse();
- }
-
- // Polar needs additional shaping
- if (series.orderTooltipPoints) {
- series.orderTooltipPoints(points);
- }
-
- // Assign each pixel position to the nearest point
- pointsLength = points.length;
- for (i = 0; i < pointsLength; i++) {
- point = points[i];
- pointX = point.x;
- if (pointX >= xExtremes.min && pointX <= xExtremes.max) { // #1149
- nextPoint = points[i + 1];
-
- // Set this range's low to the last range's high plus one
- low = high === UNDEFINED ? 0 : high + 1;
- // Now find the new high
- high = points[i + 1] ?
- mathMin(mathMax(0, mathFloor( // #2070
- (point.clientX + (nextPoint ? (nextPoint.wrappedClientX || nextPoint.clientX) : axisLength)) / 2
- )), axisLength) :
- axisLength;
-
- while (low >= 0 && low <= high) {
- tooltipPoints[low++] = point;
- }
- }
- }
- series.tooltipPoints = tooltipPoints;
- },
-
- /**
* Show the graph
*/
show: function () {
this.setVisible(true);
},
@@ -17688,46 +18341,39 @@
});
// global variables
extend(Highcharts, {
// Constructors
- Axis: Axis,
- Chart: Chart,
Color: Color,
Point: Point,
Tick: Tick,
Renderer: Renderer,
- Series: Series,
SVGElement: SVGElement,
SVGRenderer: SVGRenderer,
// Various
arrayMin: arrayMin,
arrayMax: arrayMax,
charts: charts,
dateFormat: dateFormat,
+ error: error,
format: format,
pathAnim: pathAnim,
getOptions: getOptions,
hasBidiBug: hasBidiBug,
isTouchDevice: isTouchDevice,
- numberFormat: numberFormat,
- seriesTypes: seriesTypes,
setOptions: setOptions,
addEvent: addEvent,
removeEvent: removeEvent,
createElement: createElement,
discardElement: discardElement,
css: css,
each: each,
- extend: extend,
map: map,
merge: merge,
- pick: pick,
splat: splat,
extendClass: extendClass,
pInt: pInt,
- wrap: wrap,
svg: hasSVG,
canvas: useCanVG,
vml: !hasSVG && !useCanVG,
product: PRODUCT,
version: VERSION