initial commit

This commit is contained in:
2021-07-10 16:25:16 +02:00
parent bff57cbf45
commit 20fb7267a8
2426 changed files with 792090 additions and 0 deletions
@@ -0,0 +1,524 @@
/**
* Accessibility module - internationalization support
*
* (c) 2010-2019 Highsoft AS
* Author: Øystein Moseng
*
* License: www.highcharts.com/license
*/
'use strict';
import H from '../parts/Globals.js';
import '../parts/Utilities.js';
var pick = H.pick;
/**
* String trim that works for IE6-8 as well.
*
* @private
* @function stringTrim
*
* @param {string} str
* The input string
*
* @return {string}
* The trimmed string
*/
function stringTrim(str) {
return str.trim && str.trim() || str.replace(/^\s+|\s+$/g, '');
}
/**
* i18n utility function. Format a single array or plural statement in a format
* string. If the statement is not an array or plural statement, returns the
* statement within brackets. Invalid array statements return an empty string.
*
* @private
* @function formatExtendedStatement
*
* @param {string} statement
*
* @param {Highcharts.Dictionary<*>} ctx
* Context to apply to the format string.
*
* @return {string}
*/
function formatExtendedStatement(statement, ctx) {
var eachStart = statement.indexOf('#each('),
pluralStart = statement.indexOf('#plural('),
indexStart = statement.indexOf('['),
indexEnd = statement.indexOf(']'),
arr,
result;
// Dealing with an each-function?
if (eachStart > -1) {
var eachEnd = statement.slice(eachStart).indexOf(')') + eachStart,
preEach = statement.substring(0, eachStart),
postEach = statement.substring(eachEnd + 1),
eachStatement = statement.substring(eachStart + 6, eachEnd),
eachArguments = eachStatement.split(','),
lenArg = Number(eachArguments[1]),
len;
result = '';
arr = ctx[eachArguments[0]];
if (arr) {
lenArg = isNaN(lenArg) ? arr.length : lenArg;
len = lenArg < 0 ?
arr.length + lenArg :
Math.min(lenArg, arr.length); // Overshoot
// Run through the array for the specified length
for (var i = 0; i < len; ++i) {
result += preEach + arr[i] + postEach;
}
}
return result.length ? result : '';
}
// Dealing with a plural-function?
if (pluralStart > -1) {
var pluralEnd = statement.slice(pluralStart).indexOf(')') + pluralStart,
pluralStatement = statement.substring(pluralStart + 8, pluralEnd),
pluralArguments = pluralStatement.split(','),
num = Number(ctx[pluralArguments[0]]);
switch (num) {
case 0:
result = pick(pluralArguments[4], pluralArguments[1]);
break;
case 1:
result = pick(pluralArguments[2], pluralArguments[1]);
break;
case 2:
result = pick(pluralArguments[3], pluralArguments[1]);
break;
default:
result = pluralArguments[1];
}
return result ? stringTrim(result) : '';
}
// Array index
if (indexStart > -1) {
var arrayName = statement.substring(0, indexStart),
ix = Number(statement.substring(indexStart + 1, indexEnd)),
val;
arr = ctx[arrayName];
if (!isNaN(ix) && arr) {
if (ix < 0) {
val = arr[arr.length + ix];
// Handle negative overshoot
if (val === undefined) {
val = arr[0];
}
} else {
val = arr[ix];
// Handle positive overshoot
if (val === undefined) {
val = arr[arr.length - 1];
}
}
}
return val !== undefined ? val : '';
}
// Standard substitution, delegate to H.format or similar
return '{' + statement + '}';
}
/**
* i18n formatting function. Extends Highcharts.format() functionality by also
* handling arrays and plural conditionals. Arrays can be indexed as follows:
*
* - Format: 'This is the first index: {myArray[0]}. The last: {myArray[-1]}.'
*
* - Context: { myArray: [0, 1, 2, 3, 4, 5] }
*
* - Result: 'This is the first index: 0. The last: 5.'
*
*
* They can also be iterated using the #each() function. This will repeat the
* contents of the bracket expression for each element. Example:
*
* - Format: 'List contains: {#each(myArray)cm }'
*
* - Context: { myArray: [0, 1, 2] }
*
* - Result: 'List contains: 0cm 1cm 2cm '
*
*
* The #each() function optionally takes a length parameter. If positive, this
* parameter specifies the max number of elements to iterate through. If
* negative, the function will subtract the number from the length of the array.
* Use this to stop iterating before the array ends. Example:
*
* - Format: 'List contains: {#each(myArray, -1) }and {myArray[-1]}.'
*
* - Context: { myArray: [0, 1, 2, 3] }
*
* - Result: 'List contains: 0, 1, 2, and 3.'
*
*
* Use the #plural() function to pick a string depending on whether or not a
* context object is 1. Arguments are #plural(obj, plural, singular). Example:
*
* - Format: 'Has {numPoints} {#plural(numPoints, points, point}.'
*
* - Context: { numPoints: 5 }
*
* - Result: 'Has 5 points.'
*
*
* Optionally there are additional parameters for dual and none: #plural(obj,
* plural, singular, dual, none). Example:
*
* - Format: 'Has {#plural(numPoints, many points, one point, two points,
* none}.'
*
* - Context: { numPoints: 2 }
*
* - Result: 'Has two points.'
*
*
* The dual or none parameters will take precedence if they are supplied.
*
*
* @function Highcharts.i18nFormat
* @requires a11y-i18n
*
* @param {string} formatString
* The string to format.
*
* @param {Highcharts.Dictionary<*>} context
* Context to apply to the format string.
*
* @param {Highcharts.Time} time
* A `Time` instance for date formatting, passed on to H.format().
*
* @return {string}
* The formatted string.
*/
H.i18nFormat = function (formatString, context, time) {
var getFirstBracketStatement = function (sourceStr, offset) {
var str = sourceStr.slice(offset || 0),
startBracket = str.indexOf('{'),
endBracket = str.indexOf('}');
if (startBracket > -1 && endBracket > startBracket) {
return {
statement: str.substring(startBracket + 1, endBracket),
begin: offset + startBracket + 1,
end: offset + endBracket
};
}
},
tokens = [],
bracketRes,
constRes,
cursor = 0;
// Tokenize format string into bracket statements and constants
do {
bracketRes = getFirstBracketStatement(formatString, cursor);
constRes = formatString.substring(
cursor,
bracketRes && bracketRes.begin - 1
);
// If we have constant content before this bracket statement, add it
if (constRes.length) {
tokens.push({
value: constRes,
type: 'constant'
});
}
// Add the bracket statement
if (bracketRes) {
tokens.push({
value: bracketRes.statement,
type: 'statement'
});
}
cursor = bracketRes && bracketRes.end + 1;
} while (bracketRes);
// Perform the formatting. The formatArrayStatement function returns the
// statement in brackets if it is not an array statement, which means it
// gets picked up by H.format below.
tokens.forEach(function (token) {
if (token.type === 'statement') {
token.value = formatExtendedStatement(token.value, context);
}
});
// Join string back together and pass to H.format to pick up non-array
// statements.
return H.format(tokens.reduce(function (acc, cur) {
return acc + cur.value;
}, ''), context, time);
};
/**
* Apply context to a format string from lang options of the chart.
*
* @function Highcharts.Chart#langFormat
* @requires a11y-i18n
*
* @param {string} langKey
* Key (using dot notation) into lang option structure.
*
* @param {Highcharts.Dictionary<*>} context
* Context to apply to the format string.
*
* @return {string}
* The formatted string.
*/
H.Chart.prototype.langFormat = function (langKey, context, time) {
var keys = langKey.split('.'),
formatString = this.options.lang,
i = 0;
for (; i < keys.length; ++i) {
formatString = formatString && formatString[keys[i]];
}
return typeof formatString === 'string' && H.i18nFormat(
formatString, context, time
);
};
H.setOptions({
lang: {
/* eslint-disable max-len */
/**
* Configure the accessibility strings in the chart. Requires the
* [accessibility module](//code.highcharts.com/modules/accessibility.js)
* to be loaded. For a description of the module and information on its
* features, see [Highcharts Accessibility](
* http://www.highcharts.com/docs/chart-concepts/accessibility).
*
* For more dynamic control over the accessibility functionality, see
* [accessibility.pointDescriptionFormatter](
* accessibility.pointDescriptionFormatter),
* [accessibility.seriesDescriptionFormatter](
* accessibility.seriesDescriptionFormatter), and
* [accessibility.screenReaderSectionFormatter](
* accessibility.screenReaderSectionFormatter).
*
* @since 6.0.6
* @optionparent lang.accessibility
*/
/* eslint-enable max-len */
accessibility: {
/* eslint-disable max-len */
screenReaderRegionLabel: 'Chart screen reader information.',
navigationHint: 'Use regions/landmarks to skip ahead to chart {#plural(numSeries, and navigate between data series,)}',
defaultChartTitle: 'Chart',
longDescriptionHeading: 'Long description.',
noDescription: 'No description available.',
structureHeading: 'Structure.',
viewAsDataTable: 'View as data table.',
chartHeading: 'Chart graphic.',
chartContainerLabel: 'Interactive chart. {title}. Use up and down arrows to navigate with most screen readers.',
rangeSelectorMinInput: 'Select start date.',
rangeSelectorMaxInput: 'Select end date.',
tableSummary: 'Table representation of chart.',
mapZoomIn: 'Zoom chart',
mapZoomOut: 'Zoom out chart',
rangeSelectorButton: 'Select range {buttonText}',
legendLabel: 'Toggle series visibility',
/* eslint-enable max-len */
/**
* Title element text for the chart SVG element. Leave this
* empty to disable adding the title element. Browsers will display
* this content when hovering over elements in the chart. Assistive
* technology may use this element to label the chart.
*
* @since 6.0.8
*/
svgContainerTitle: '{chartTitle}',
/**
* Descriptions of lesser known series types. The relevant
* description is added to the screen reader information region
* when these series types are used.
*
* @since 6.0.6
*/
seriesTypeDescriptions: {
boxplot: 'Box plot charts are typically used to display ' +
'groups of statistical data. Each data point in the ' +
'chart can have up to 5 values: minimum, lower quartile, ' +
'median, upper quartile, and maximum.',
arearange: 'Arearange charts are line charts displaying a ' +
'range between a lower and higher value for each point.',
areasplinerange: 'These charts are line charts displaying a ' +
'range between a lower and higher value for each point.',
bubble: 'Bubble charts are scatter charts where each data ' +
'point also has a size value.',
columnrange: 'Columnrange charts are column charts ' +
'displaying a range between a lower and higher value for ' +
'each point.',
errorbar: 'Errorbar series are used to display the ' +
'variability of the data.',
funnel: 'Funnel charts are used to display reduction of data ' +
'in stages.',
pyramid: 'Pyramid charts consist of a single pyramid with ' +
'item heights corresponding to each point value.',
waterfall: 'A waterfall chart is a column chart where each ' +
'column contributes towards a total end value.'
},
/**
* Chart type description strings. This is added to the chart
* information region.
*
* If there is only a single series type used in the chart, we use
* the format string for the series type, or default if missing.
* There is one format string for cases where there is only a single
* series in the chart, and one for multiple series of the same
* type.
*
* @since 6.0.6
*/
chartTypes: {
/* eslint-disable max-len */
emptyChart: 'Empty chart',
mapTypeDescription: 'Map of {mapTitle} with {numSeries} data series.',
unknownMap: 'Map of unspecified region with {numSeries} data series.',
combinationChart: 'Combination chart with {numSeries} data series.',
defaultSingle: 'Chart with {numPoints} data {#plural(numPoints, points, point)}.',
defaultMultiple: 'Chart with {numSeries} data series.',
splineSingle: 'Line chart with {numPoints} data {#plural(numPoints, points, point)}.',
splineMultiple: 'Line chart with {numSeries} lines.',
lineSingle: 'Line chart with {numPoints} data {#plural(numPoints, points, point)}.',
lineMultiple: 'Line chart with {numSeries} lines.',
columnSingle: 'Bar chart with {numPoints} {#plural(numPoints, bars, bar)}.',
columnMultiple: 'Bar chart with {numSeries} data series.',
barSingle: 'Bar chart with {numPoints} {#plural(numPoints, bars, bar)}.',
barMultiple: 'Bar chart with {numSeries} data series.',
pieSingle: 'Pie chart with {numPoints} {#plural(numPoints, slices, slice)}.',
pieMultiple: 'Pie chart with {numSeries} pies.',
scatterSingle: 'Scatter chart with {numPoints} {#plural(numPoints, points, point)}.',
scatterMultiple: 'Scatter chart with {numSeries} data series.',
boxplotSingle: 'Boxplot with {numPoints} {#plural(numPoints, boxes, box)}.',
boxplotMultiple: 'Boxplot with {numSeries} data series.',
bubbleSingle: 'Bubble chart with {numPoints} {#plural(numPoints, bubbles, bubble)}.',
bubbleMultiple: 'Bubble chart with {numSeries} data series.'
}, /* eslint-enable max-len */
/**
* Axis description format strings.
*
* @since 6.0.6
*/
axis: {
/* eslint-disable max-len */
xAxisDescriptionSingular: 'The chart has 1 X axis displaying {names[0]}.',
xAxisDescriptionPlural: 'The chart has {numAxes} X axes displaying {#names.forEach(-1) }and {names[-1]}',
yAxisDescriptionSingular: 'The chart has 1 Y axis displaying {names[0]}.',
yAxisDescriptionPlural: 'The chart has {numAxes} Y axes displaying {#names.forEach(-1) }and {names[-1]}'
}, /* eslint-enable max-len */
/**
* Exporting menu format strings for accessibility module.
*
* @since 6.0.6
*/
exporting: {
chartMenuLabel: 'Chart export',
menuButtonLabel: 'View export menu',
exportRegionLabel: 'Chart export menu'
},
/**
* Lang configuration for different series types. For more dynamic
* control over the series element descriptions, see
* [accessibility.seriesDescriptionFormatter](
* accessibility.seriesDescriptionFormatter).
*
* @since 6.0.6
*/
series: {
/**
* Lang configuration for the series main summary. Each series
* type has two modes:
*
* 1. This series type is the only series type used in the
* chart
*
* 2. This is a combination chart with multiple series types
*
* If a definition does not exist for the specific series type
* and mode, the 'default' lang definitions are used.
*
* @since 6.0.6
*/
summary: {
/* eslint-disable max-len */
'default': '{name}, series {ix} of {numSeries} with {numPoints} data {#plural(numPoints, points, point)}.',
defaultCombination: '{name}, series {ix} of {numSeries} with {numPoints} data {#plural(numPoints, points, point)}.',
line: '{name}, line {ix} of {numSeries} with {numPoints} data {#plural(numPoints, points, point)}.',
lineCombination: '{name}, series {ix} of {numSeries}. Line with {numPoints} data {#plural(numPoints, points, point)}.',
spline: '{name}, line {ix} of {numSeries} with {numPoints} data {#plural(numPoints, points, point)}.',
splineCombination: '{name}, series {ix} of {numSeries}. Line with {numPoints} data {#plural(numPoints, points, point)}.',
column: '{name}, bar series {ix} of {numSeries} with {numPoints} {#plural(numPoints, bars, bar)}.',
columnCombination: '{name}, series {ix} of {numSeries}. Bar series with {numPoints} {#plural(numPoints, bars, bar)}.',
bar: '{name}, bar series {ix} of {numSeries} with {numPoints} {#plural(numPoints, bars, bar)}.',
barCombination: '{name}, series {ix} of {numSeries}. Bar series with {numPoints} {#plural(numPoints, bars, bar)}.',
pie: '{name}, pie {ix} of {numSeries} with {numPoints} {#plural(numPoints, slices, slice)}.',
pieCombination: '{name}, series {ix} of {numSeries}. Pie with {numPoints} {#plural(numPoints, slices, slice)}.',
scatter: '{name}, scatter plot {ix} of {numSeries} with {numPoints} {#plural(numPoints, points, point)}.',
scatterCombination: '{name}, series {ix} of {numSeries}, scatter plot with {numPoints} {#plural(numPoints, points, point)}.',
boxplot: '{name}, boxplot {ix} of {numSeries} with {numPoints} {#plural(numPoints, boxes, box)}.',
boxplotCombination: '{name}, series {ix} of {numSeries}. Boxplot with {numPoints} {#plural(numPoints, boxes, box)}.',
bubble: '{name}, bubble series {ix} of {numSeries} with {numPoints} {#plural(numPoints, bubbles, bubble)}.',
bubbleCombination: '{name}, series {ix} of {numSeries}. Bubble series with {numPoints} {#plural(numPoints, bubbles, bubble)}.',
map: '{name}, map {ix} of {numSeries} with {numPoints} {#plural(numPoints, areas, area)}.',
mapCombination: '{name}, series {ix} of {numSeries}. Map with {numPoints} {#plural(numPoints, areas, area)}.',
mapline: '{name}, line {ix} of {numSeries} with {numPoints} data {#plural(numPoints, points, point)}.',
maplineCombination: '{name}, series {ix} of {numSeries}. Line with {numPoints} data {#plural(numPoints, points, point)}.',
mapbubble: '{name}, bubble series {ix} of {numSeries} with {numPoints} {#plural(numPoints, bubbles, bubble)}.',
mapbubbleCombination: '{name}, series {ix} of {numSeries}. Bubble series with {numPoints} {#plural(numPoints, bubbles, bubble)}.'
}, /* eslint-enable max-len */
/**
* User supplied description text. This is added after the main
* summary if present.
*
* @since 6.0.6
*/
description: '{description}',
/**
* xAxis description for series if there are multiple xAxes in
* the chart.
*
* @since 6.0.6
*/
xAxisDescription: 'X axis, {name}',
/**
* yAxis description for series if there are multiple yAxes in
* the chart.
*
* @since 6.0.6
*/
yAxisDescription: 'Y axis, {name}'
}
}
}
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,173 @@
/* *
* (c) 2010-2019 Highsoft AS
*
* Author: Sebastian Domas
*
* License: www.highcharts.com/license
*/
'use strict';
import H from '../parts/Globals.js';
import '../parts/Utilities.js';
import derivedSeriesMixin from '../mixins/derived-series.js';
var seriesType = H.seriesType,
correctFloat = H.correctFloat,
isNumber = H.isNumber,
merge = H.merge;
/* ************************************************************************** *
* BELL CURVE *
* ************************************************************************** */
function mean(data) {
var length = data.length,
sum = data.reduce(function (sum, value) {
return (sum += value);
}, 0);
return length > 0 && sum / length;
}
function standardDeviation(data, average) {
var len = data.length,
sum;
average = isNumber(average) ? average : mean(data);
sum = data.reduce(function (sum, value) {
var diff = value - average;
return (sum += diff * diff);
}, 0);
return len > 1 && Math.sqrt(sum / (len - 1));
}
function normalDensity(x, mean, standardDeviation) {
var translation = x - mean;
return Math.exp(
-(translation * translation) /
(2 * standardDeviation * standardDeviation)
) / (standardDeviation * Math.sqrt(2 * Math.PI));
}
/**
* Bell curve class
*
* @private
* @class
* @name Highcharts.seriesTypes.bellcurve
*
* @augments Highcharts.Series
*
* @mixes DerivedSeriesMixin
*/
seriesType('bellcurve', 'areaspline'
/**
* A bell curve is an areaspline series which represents the probability density
* function of the normal distribution. It calculates mean and standard
* deviation of the base series data and plots the curve according to the
* calculated parameters.
*
* @sample {highcharts} highcharts/demo/bellcurve/
* Bell curve
*
* @extends plotOptions.areaspline
* @since 6.0.0
* @product highcharts
* @excluding boostThreshold, connectNulls, stacking, pointInterval,
* pointIntervalUnit
* @optionparent plotOptions.bellcurve
*/
, {
/**
* This option allows to define the length of the bell curve. A unit of the
* length of the bell curve is standard deviation.
*
* @sample highcharts/plotoptions/bellcurve-intervals-pointsininterval
* Intervals and points in interval
*/
intervals: 3,
/**
* Defines how many points should be plotted within 1 interval. See
* `plotOptions.bellcurve.intervals`.
*
* @sample highcharts/plotoptions/bellcurve-intervals-pointsininterval
* Intervals and points in interval
*/
pointsInInterval: 3,
marker: {
enabled: false
}
}, merge(derivedSeriesMixin, {
setMean: function () {
this.mean = correctFloat(mean(this.baseSeries.yData));
},
setStandardDeviation: function () {
this.standardDeviation = correctFloat(
standardDeviation(this.baseSeries.yData, this.mean)
);
},
setDerivedData: function () {
if (this.baseSeries.yData.length > 1) {
this.setMean();
this.setStandardDeviation();
this.setData(
this.derivedData(this.mean, this.standardDeviation), false
);
}
},
derivedData: function (mean, standardDeviation) {
var intervals = this.options.intervals,
pointsInInterval = this.options.pointsInInterval,
x = mean - intervals * standardDeviation,
stop = intervals * pointsInInterval * 2 + 1,
increment = standardDeviation / pointsInInterval,
data = [],
i;
for (i = 0; i < stop; i++) {
data.push([x, normalDensity(x, mean, standardDeviation)]);
x += increment;
}
return data;
}
}));
/**
* A `bellcurve` series. If the [type](#series.bellcurve.type) option is not
* specified, it is inherited from [chart.type](#chart.type).
*
* For options that apply to multiple series, it is recommended to add
* them to the [plotOptions.series](#plotOptions.series) options structure.
* To apply to all series of this specific type, apply it to
* [plotOptions.bellcurve](#plotOptions.bellcurve).
*
* @extends series,plotOptions.bellcurve
* @since 6.0.0
* @product highcharts
* @excluding dataParser, dataURL, data
* @apioption series.bellcurve
*/
/**
* An integer identifying the index to use for the base series, or a string
* representing the id of the series.
*
* @type {number|string}
* @apioption series.bellcurve.baseSeries
*/
@@ -0,0 +1,732 @@
/* *
* License: www.highcharts.com/license
* Author: Torstein Honsi, Christer Vasseng
*
* This module serves as a fallback for the Boost module in IE9 and IE10. Newer
* browsers support WebGL which is faster.
*
* It is recommended to include this module in conditional comments targeting
* IE9 and IE10.
*/
'use strict';
import H from '../parts/Globals.js';
import '../parts/Utilities.js';
import '../parts/Color.js';
import '../parts/Series.js';
import '../parts/Options.js';
var win = H.win,
doc = win.document,
noop = function () {},
Color = H.Color,
Series = H.Series,
seriesTypes = H.seriesTypes,
extend = H.extend,
addEvent = H.addEvent,
fireEvent = H.fireEvent,
isNumber = H.isNumber,
merge = H.merge,
pick = H.pick,
wrap = H.wrap,
CHUNK_SIZE = 50000,
destroyLoadingDiv;
/**
* Initialize the canvas boost.
*
* @function Highcharts.initCanvasBoost
*/
H.initCanvasBoost = function () {
if (H.seriesTypes.heatmap) {
H.wrap(H.seriesTypes.heatmap.prototype, 'drawPoints', function () {
var chart = this.chart,
ctx = this.getContext(),
inverted = this.chart.inverted,
xAxis = this.xAxis,
yAxis = this.yAxis;
if (ctx) {
// draw the columns
this.points.forEach(function (point) {
var plotY = point.plotY,
shapeArgs,
pointAttr;
if (
plotY !== undefined &&
!isNaN(plotY) &&
point.y !== null
) {
shapeArgs = point.shapeArgs;
if (!chart.styledMode) {
pointAttr = point.series.pointAttribs(point);
} else {
pointAttr = point.series.colorAttribs(point);
}
ctx.fillStyle = pointAttr.fill;
if (inverted) {
ctx.fillRect(
yAxis.len - shapeArgs.y + xAxis.left,
xAxis.len - shapeArgs.x + yAxis.top,
-shapeArgs.height,
-shapeArgs.width
);
} else {
ctx.fillRect(
shapeArgs.x + xAxis.left,
shapeArgs.y + yAxis.top,
shapeArgs.width,
shapeArgs.height
);
}
}
});
this.canvasToSVG();
} else {
this.chart.showLoading(
'Your browser doesn\'t support HTML5 canvas, <br>' +
'please use a modern browser'
);
// Uncomment this to provide low-level (slow) support in oldIE.
// It will cause script errors on charts with more than a few
// thousand points.
// arguments[0].call(this);
}
});
}
H.extend(Series.prototype, {
/**
* Create a hidden canvas to draw the graph on. The contents is later
* copied over to an SVG image element.
*
* @private
* @function Highcharts.Series#getContext
*/
getContext: function () {
var chart = this.chart,
width = chart.chartWidth,
height = chart.chartHeight,
targetGroup = chart.seriesGroup || this.group,
target = this,
ctx,
swapXY = function (proceed, x, y, a, b, c, d) {
proceed.call(this, y, x, a, b, c, d);
};
if (chart.isChartSeriesBoosting()) {
target = chart;
targetGroup = chart.seriesGroup;
}
ctx = target.ctx;
if (!target.canvas) {
target.canvas = doc.createElement('canvas');
target.renderTarget = chart.renderer.image(
'',
0,
0,
width,
height
)
.addClass('highcharts-boost-canvas')
.add(targetGroup);
target.ctx = ctx = target.canvas.getContext('2d');
if (chart.inverted) {
['moveTo', 'lineTo', 'rect', 'arc'].forEach(function (fn) {
wrap(ctx, fn, swapXY);
});
}
target.boostCopy = function () {
target.renderTarget.attr({
href: target.canvas.toDataURL('image/png')
});
};
target.boostClear = function () {
ctx.clearRect(
0,
0,
target.canvas.width,
target.canvas.height
);
if (target === this) {
target.renderTarget.attr({ href: '' });
}
};
target.boostClipRect = chart.renderer.clipRect();
target.renderTarget.clip(target.boostClipRect);
} else if (!(target instanceof H.Chart)) {
// ctx.clearRect(0, 0, width, height);
}
if (target.canvas.width !== width) {
target.canvas.width = width;
}
if (target.canvas.height !== height) {
target.canvas.height = height;
}
target.renderTarget.attr({
x: 0,
y: 0,
width: width,
height: height,
style: 'pointer-events: none',
href: ''
});
target.boostClipRect.attr(chart.getBoostClipRect(target));
return ctx;
},
/**
* Draw the canvas image inside an SVG image
*
* @private
* @function Highcharts.Series#canvasToSVG
*/
canvasToSVG: function () {
if (!this.chart.isChartSeriesBoosting()) {
if (this.boostCopy || this.chart.boostCopy) {
(this.boostCopy || this.chart.boostCopy)();
}
} else {
if (this.boostClear) {
this.boostClear();
}
}
},
cvsLineTo: function (ctx, clientX, plotY) {
ctx.lineTo(clientX, plotY);
},
renderCanvas: function () {
var series = this,
options = series.options,
chart = series.chart,
xAxis = this.xAxis,
yAxis = this.yAxis,
activeBoostSettings = chart.options.boost || {},
boostSettings = {
timeRendering: activeBoostSettings.timeRendering || false,
timeSeriesProcessing:
activeBoostSettings.timeSeriesProcessing || false,
timeSetup: activeBoostSettings.timeSetup || false
},
ctx,
c = 0,
xData = series.processedXData,
yData = series.processedYData,
rawData = options.data,
xExtremes = xAxis.getExtremes(),
xMin = xExtremes.min,
xMax = xExtremes.max,
yExtremes = yAxis.getExtremes(),
yMin = yExtremes.min,
yMax = yExtremes.max,
pointTaken = {},
lastClientX,
sampling = !!series.sampling,
points,
r = options.marker && options.marker.radius,
cvsDrawPoint = this.cvsDrawPoint,
cvsLineTo = options.lineWidth ? this.cvsLineTo : false,
cvsMarker = r && r <= 1 ?
this.cvsMarkerSquare :
this.cvsMarkerCircle,
strokeBatch = this.cvsStrokeBatch || 1000,
enableMouseTracking = options.enableMouseTracking !== false,
lastPoint,
threshold = options.threshold,
yBottom = yAxis.getThreshold(threshold),
hasThreshold = isNumber(threshold),
translatedThreshold = yBottom,
doFill = this.fill,
isRange = series.pointArrayMap &&
series.pointArrayMap.join(',') === 'low,high',
isStacked = !!options.stacking,
cropStart = series.cropStart || 0,
loadingOptions = chart.options.loading,
requireSorting = series.requireSorting,
wasNull,
connectNulls = options.connectNulls,
useRaw = !xData,
minVal,
maxVal,
minI,
maxI,
index,
sdata = isStacked ? series.data : (xData || rawData),
fillColor = series.fillOpacity ?
new Color(series.color).setOpacity(
pick(options.fillOpacity, 0.75)
).get() :
series.color,
stroke = function () {
if (doFill) {
ctx.fillStyle = fillColor;
ctx.fill();
} else {
ctx.strokeStyle = series.color;
ctx.lineWidth = options.lineWidth;
ctx.stroke();
}
},
drawPoint = function (clientX, plotY, yBottom, i) {
if (c === 0) {
ctx.beginPath();
if (cvsLineTo) {
ctx.lineJoin = 'round';
}
}
if (
chart.scroller &&
series.options.className ===
'highcharts-navigator-series'
) {
plotY += chart.scroller.top;
if (yBottom) {
yBottom += chart.scroller.top;
}
} else {
plotY += chart.plotTop;
}
clientX += chart.plotLeft;
if (wasNull) {
ctx.moveTo(clientX, plotY);
} else {
if (cvsDrawPoint) {
cvsDrawPoint(
ctx,
clientX,
plotY,
yBottom,
lastPoint
);
} else if (cvsLineTo) {
cvsLineTo(ctx, clientX, plotY);
} else if (cvsMarker) {
cvsMarker.call(series, ctx, clientX, plotY, r, i);
}
}
// We need to stroke the line for every 1000 pixels. It will
// crash the browser memory use if we stroke too
// infrequently.
c = c + 1;
if (c === strokeBatch) {
stroke();
c = 0;
}
// Area charts need to keep track of the last point
lastPoint = {
clientX: clientX,
plotY: plotY,
yBottom: yBottom
};
},
compareX = options.findNearestPointBy === 'x',
xDataFull = (
this.xData ||
this.options.xData ||
this.processedXData ||
false
),
addKDPoint = function (clientX, plotY, i) {
// Shaves off about 60ms compared to repeated concatenation
index = compareX ? clientX : clientX + ',' + plotY;
// The k-d tree requires series points.
// Reduce the amount of points, since the time to build the
// tree increases exponentially.
if (enableMouseTracking && !pointTaken[index]) {
pointTaken[index] = true;
if (chart.inverted) {
clientX = xAxis.len - clientX;
plotY = yAxis.len - plotY;
}
points.push({
x: xDataFull ? xDataFull[cropStart + i] : false,
clientX: clientX,
plotX: clientX,
plotY: plotY,
i: cropStart + i
});
}
};
if (this.renderTarget) {
this.renderTarget.attr({ 'href': '' });
}
// If we are zooming out from SVG mode, destroy the graphics
if (this.points || this.graph) {
this.destroyGraphics();
}
// The group
series.plotGroup(
'group',
'series',
series.visible ? 'visible' : 'hidden',
options.zIndex,
chart.seriesGroup
);
series.markerGroup = series.group;
addEvent(series, 'destroy', function () { // Prevent destroy twice
series.markerGroup = null;
});
points = this.points = [];
ctx = this.getContext();
series.buildKDTree = noop; // Do not start building while drawing
if (this.boostClear) {
this.boostClear();
}
// if (this.canvas) {
// ctx.clearRect(
// 0,
// 0,
// this.canvas.width,
// this.canvas.height
// );
// }
if (!this.visible) {
return;
}
// Display a loading indicator
if (rawData.length > 99999) {
chart.options.loading = merge(loadingOptions, {
labelStyle: {
backgroundColor: H.color('#ffffff')
.setOpacity(0.75).get(),
padding: '1em',
borderRadius: '0.5em'
},
style: {
backgroundColor: 'none',
opacity: 1
}
});
H.clearTimeout(destroyLoadingDiv);
chart.showLoading('Drawing...');
chart.options.loading = loadingOptions; // reset
}
if (boostSettings.timeRendering) {
console.time('canvas rendering'); // eslint-disable-line no-console
}
// Loop over the points
H.eachAsync(sdata, function (d, i) {
var x,
y,
clientX,
plotY,
isNull,
low,
isNextInside = false,
isPrevInside = false,
nx = false,
px = false,
chartDestroyed = typeof chart.index === 'undefined',
isYInside = true;
if (!chartDestroyed) {
if (useRaw) {
x = d[0];
y = d[1];
if (sdata[i + 1]) {
nx = sdata[i + 1][0];
}
if (sdata[i - 1]) {
px = sdata[i - 1][0];
}
} else {
x = d;
y = yData[i];
if (sdata[i + 1]) {
nx = sdata[i + 1];
}
if (sdata[i - 1]) {
px = sdata[i - 1];
}
}
if (nx && nx >= xMin && nx <= xMax) {
isNextInside = true;
}
if (px && px >= xMin && px <= xMax) {
isPrevInside = true;
}
// Resolve low and high for range series
if (isRange) {
if (useRaw) {
y = d.slice(1, 3);
}
low = y[0];
y = y[1];
} else if (isStacked) {
x = d.x;
y = d.stackY;
low = y - d.y;
}
isNull = y === null;
// Optimize for scatter zooming
if (!requireSorting) {
isYInside = y >= yMin && y <= yMax;
}
if (!isNull &&
(
(x >= xMin && x <= xMax && isYInside) ||
(isNextInside || isPrevInside)
)) {
clientX = Math.round(xAxis.toPixels(x, true));
if (sampling) {
if (minI === undefined || clientX === lastClientX) {
if (!isRange) {
low = y;
}
if (maxI === undefined || y > maxVal) {
maxVal = y;
maxI = i;
}
if (minI === undefined || low < minVal) {
minVal = low;
minI = i;
}
}
// Add points and reset
if (clientX !== lastClientX) {
if (minI !== undefined) { // maxI also a number
plotY = yAxis.toPixels(maxVal, true);
yBottom = yAxis.toPixels(minVal, true);
drawPoint(
clientX,
hasThreshold ?
Math.min(
plotY,
translatedThreshold
) : plotY,
hasThreshold ?
Math.max(
yBottom,
translatedThreshold
) : yBottom,
i
);
addKDPoint(clientX, plotY, maxI);
if (yBottom !== plotY) {
addKDPoint(clientX, yBottom, minI);
}
}
minI = maxI = undefined;
lastClientX = clientX;
}
} else {
plotY = Math.round(yAxis.toPixels(y, true));
drawPoint(clientX, plotY, yBottom, i);
addKDPoint(clientX, plotY, i);
}
}
wasNull = isNull && !connectNulls;
if (i % CHUNK_SIZE === 0) {
if (series.boostCopy || series.chart.boostCopy) {
(series.boostCopy || series.chart.boostCopy)();
}
}
}
return !chartDestroyed;
}, function () {
var loadingDiv = chart.loadingDiv,
loadingShown = chart.loadingShown;
stroke();
// if (series.boostCopy || series.chart.boostCopy) {
// (series.boostCopy || series.chart.boostCopy)();
// }
series.canvasToSVG();
if (boostSettings.timeRendering) {
console.timeEnd('canvas rendering'); // eslint-disable-line no-console
}
fireEvent(series, 'renderedCanvas');
// Do not use chart.hideLoading, as it runs JS animation and
// will be blocked by buildKDTree. CSS animation looks good, but
// then it must be deleted in timeout. If we add the module to
// core, change hideLoading so we can skip this block.
if (loadingShown) {
extend(loadingDiv.style, {
transition: 'opacity 250ms',
opacity: 0
});
chart.loadingShown = false;
destroyLoadingDiv = setTimeout(function () {
if (loadingDiv.parentNode) { // In exporting it is falsy
loadingDiv.parentNode.removeChild(loadingDiv);
}
chart.loadingDiv = chart.loadingSpan = null;
}, 250);
}
// Go back to prototype, ready to build
delete series.buildKDTree;
series.buildKDTree();
// Don't do async on export, the exportChart, getSVGForExport and
// getSVG methods are not chained for it.
}, chart.renderer.forExport ? Number.MAX_VALUE : undefined);
}
});
seriesTypes.scatter.prototype.cvsMarkerCircle = function (
ctx,
clientX,
plotY,
r
) {
ctx.moveTo(clientX, plotY);
ctx.arc(clientX, plotY, r, 0, 2 * Math.PI, false);
};
// Rect is twice as fast as arc, should be used for small markers
seriesTypes.scatter.prototype.cvsMarkerSquare = function (
ctx,
clientX,
plotY,
r
) {
ctx.rect(clientX - r, plotY - r, r * 2, r * 2);
};
seriesTypes.scatter.prototype.fill = true;
if (seriesTypes.bubble) {
seriesTypes.bubble.prototype.cvsMarkerCircle = function (
ctx,
clientX,
plotY,
r,
i
) {
ctx.moveTo(clientX, plotY);
ctx.arc(
clientX,
plotY,
this.radii && this.radii[i], 0, 2 * Math.PI,
false
);
};
seriesTypes.bubble.prototype.cvsStrokeBatch = 1;
}
extend(seriesTypes.area.prototype, {
cvsDrawPoint: function (ctx, clientX, plotY, yBottom, lastPoint) {
if (lastPoint && clientX !== lastPoint.clientX) {
ctx.moveTo(lastPoint.clientX, lastPoint.yBottom);
ctx.lineTo(lastPoint.clientX, lastPoint.plotY);
ctx.lineTo(clientX, plotY);
ctx.lineTo(clientX, yBottom);
}
},
fill: true,
fillOpacity: true,
sampling: true
});
extend(seriesTypes.column.prototype, {
cvsDrawPoint: function (ctx, clientX, plotY, yBottom) {
ctx.rect(clientX - 1, plotY, 1, yBottom - plotY);
},
fill: true,
sampling: true
});
H.Chart.prototype.callbacks.push(function (chart) {
function canvasToSVG() {
if (chart.boostCopy) {
chart.boostCopy();
}
}
function clear() {
if (chart.renderTarget) {
chart.renderTarget.attr({ href: '' });
}
if (chart.canvas) {
chart.canvas.getContext('2d').clearRect(
0,
0,
chart.canvas.width,
chart.canvas.height
);
}
}
addEvent(chart, 'predraw', clear);
addEvent(chart, 'render', canvasToSVG);
});
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,516 @@
/**
* (c) 2009-2019 Torstein Honsi
*
* License: www.highcharts.com/license
*/
'use strict';
import H from '../parts/Globals.js';
import '../parts/Utilities.js';
import '../parts/Axis.js';
import '../parts/Series.js';
var addEvent = H.addEvent,
pick = H.pick,
extend = H.extend,
isArray = H.isArray,
fireEvent = H.fireEvent,
Axis = H.Axis,
Series = H.Series;
extend(Axis.prototype, {
isInBreak: function (brk, val) {
var ret,
repeat = brk.repeat || Infinity,
from = brk.from,
length = brk.to - brk.from,
test = (
val >= from ?
(val - from) % repeat :
repeat - ((from - val) % repeat)
);
if (!brk.inclusive) {
ret = test < length && test !== 0;
} else {
ret = test <= length;
}
return ret;
},
isInAnyBreak: function (val, testKeep) {
var breaks = this.options.breaks,
i = breaks && breaks.length,
inbrk,
keep,
ret;
if (i) {
while (i--) {
if (this.isInBreak(breaks[i], val)) {
inbrk = true;
if (!keep) {
keep = pick(
breaks[i].showPoints,
!this.isXAxis
);
}
}
}
if (inbrk && testKeep) {
ret = inbrk && !keep;
} else {
ret = inbrk;
}
}
return ret;
}
});
addEvent(Axis, 'afterInit', function () {
if (typeof this.setBreaks === 'function') {
this.setBreaks(this.options.breaks, false);
}
});
addEvent(Axis, 'afterSetTickPositions', function () {
if (this.isBroken) {
var axis = this,
tickPositions = this.tickPositions,
info = this.tickPositions.info,
newPositions = [],
i;
for (i = 0; i < tickPositions.length; i++) {
if (!axis.isInAnyBreak(tickPositions[i])) {
newPositions.push(tickPositions[i]);
}
}
this.tickPositions = newPositions;
this.tickPositions.info = info;
}
});
// Force Axis to be not-ordinal when breaks are defined
addEvent(Axis, 'afterSetOptions', function () {
if (this.isBroken) {
this.options.ordinal = false;
}
});
/**
* Dynamically set or unset breaks in an axis. This function in lighter than
* usin Axis.update, and it also preserves animation.
*
* @private
* @function Highcharts.Axis#setBreaks
*
* @param {Array<*>} [breaks]
* The breaks to add. When `undefined` it removes existing breaks.
*
* @param {boolean} [redraw=true]
* Whether to redraw the chart immediately.
*/
Axis.prototype.setBreaks = function (breaks, redraw) {
var axis = this,
isBroken = (isArray(breaks) && !!breaks.length);
function breakVal2Lin(val) {
var nval = val,
brk,
i;
for (i = 0; i < axis.breakArray.length; i++) {
brk = axis.breakArray[i];
if (brk.to <= val) {
nval -= brk.len;
} else if (brk.from >= val) {
break;
} else if (axis.isInBreak(brk, val)) {
nval -= (val - brk.from);
break;
}
}
return nval;
}
function breakLin2Val(val) {
var nval = val,
brk,
i;
for (i = 0; i < axis.breakArray.length; i++) {
brk = axis.breakArray[i];
if (brk.from >= nval) {
break;
} else if (brk.to < nval) {
nval += brk.len;
} else if (axis.isInBreak(brk, nval)) {
nval += brk.len;
}
}
return nval;
}
axis.isDirty = axis.isBroken !== isBroken;
axis.isBroken = isBroken;
axis.options.breaks = axis.userOptions.breaks = breaks;
axis.forceRedraw = true; // Force recalculation in setScale
if (!isBroken && axis.val2lin === breakVal2Lin) {
// Revert to prototype functions
delete axis.val2lin;
delete axis.lin2val;
}
if (isBroken) {
axis.userOptions.ordinal = false;
axis.val2lin = breakVal2Lin;
axis.lin2val = breakLin2Val;
axis.setExtremes = function (
newMin,
newMax,
redraw,
animation,
eventArguments
) {
// If trying to set extremes inside a break, extend it to before and
// after the break ( #3857 )
if (this.isBroken) {
while (this.isInAnyBreak(newMin)) {
newMin -= this.closestPointRange;
}
while (this.isInAnyBreak(newMax)) {
newMax -= this.closestPointRange;
}
}
Axis.prototype.setExtremes.call(
this,
newMin,
newMax,
redraw,
animation,
eventArguments
);
};
axis.setAxisTranslation = function (saveOld) {
Axis.prototype.setAxisTranslation.call(this, saveOld);
this.unitLength = null;
if (this.isBroken) {
var breaks = axis.options.breaks,
breakArrayT = [], // Temporary one
breakArray = [],
length = 0,
inBrk,
repeat,
min = axis.userMin || axis.min,
max = axis.userMax || axis.max,
pointRangePadding = pick(axis.pointRangePadding, 0),
start,
i;
// Min & max check (#4247)
breaks.forEach(function (brk) {
repeat = brk.repeat || Infinity;
if (axis.isInBreak(brk, min)) {
min += (brk.to % repeat) - (min % repeat);
}
if (axis.isInBreak(brk, max)) {
max -= (max % repeat) - (brk.from % repeat);
}
});
// Construct an array holding all breaks in the axis
breaks.forEach(function (brk) {
start = brk.from;
repeat = brk.repeat || Infinity;
while (start - repeat > min) {
start -= repeat;
}
while (start < min) {
start += repeat;
}
for (i = start; i < max; i += repeat) {
breakArrayT.push({
value: i,
move: 'in'
});
breakArrayT.push({
value: i + (brk.to - brk.from),
move: 'out',
size: brk.breakSize
});
}
});
breakArrayT.sort(function (a, b) {
return (
(a.value === b.value) ?
(
(a.move === 'in' ? 0 : 1) -
(b.move === 'in' ? 0 : 1)
) :
a.value - b.value
);
});
// Simplify the breaks
inBrk = 0;
start = min;
breakArrayT.forEach(function (brk) {
inBrk += (brk.move === 'in' ? 1 : -1);
if (inBrk === 1 && brk.move === 'in') {
start = brk.value;
}
if (inBrk === 0) {
breakArray.push({
from: start,
to: brk.value,
len: brk.value - start - (brk.size || 0)
});
length += brk.value - start - (brk.size || 0);
}
});
axis.breakArray = breakArray;
// Used with staticScale, and below, the actual axis length when
// breaks are substracted.
axis.unitLength = max - min - length + pointRangePadding;
fireEvent(axis, 'afterBreaks');
if (axis.staticScale) {
axis.transA = axis.staticScale;
} else if (axis.unitLength) {
axis.transA *= (max - axis.min + pointRangePadding) /
axis.unitLength;
}
if (pointRangePadding) {
axis.minPixelPadding = axis.transA * axis.minPointOffset;
}
axis.min = min;
axis.max = max;
}
};
}
if (pick(redraw, true)) {
this.chart.redraw();
}
};
addEvent(Series, 'afterGeneratePoints', function () {
var series = this,
xAxis = series.xAxis,
yAxis = series.yAxis,
points = series.points,
point,
i = points.length,
connectNulls = series.options.connectNulls,
nullGap;
if (xAxis && yAxis && (xAxis.options.breaks || yAxis.options.breaks)) {
while (i--) {
point = points[i];
// Respect nulls inside the break (#4275)
nullGap = point.y === null && connectNulls === false;
if (
!nullGap &&
(
xAxis.isInAnyBreak(point.x, true) ||
yAxis.isInAnyBreak(point.y, true)
)
) {
points.splice(i, 1);
if (this.data[i]) {
// Removes the graphics for this point if they exist
this.data[i].destroyElements();
}
}
}
}
});
addEvent(Series, 'afterRender', function drawPointsWrapped() {
this.drawBreaks(this.xAxis, ['x']);
this.drawBreaks(this.yAxis, pick(this.pointArrayMap, ['y']));
});
H.Series.prototype.drawBreaks = function (axis, keys) {
var series = this,
points = series.points,
breaks,
threshold,
eventName,
y;
if (!axis) {
return; // #5950
}
keys.forEach(function (key) {
breaks = axis.breakArray || [];
threshold = axis.isXAxis ?
axis.min :
pick(series.options.threshold, axis.min);
points.forEach(function (point) {
y = pick(point['stack' + key.toUpperCase()], point[key]);
breaks.forEach(function (brk) {
eventName = false;
if (
(threshold < brk.from && y > brk.to) ||
(threshold > brk.from && y < brk.from)
) {
eventName = 'pointBreak';
} else if (
(threshold < brk.from && y > brk.from && y < brk.to) ||
(threshold > brk.from && y > brk.to && y < brk.from)
) {
eventName = 'pointInBreak';
}
if (eventName) {
fireEvent(axis, eventName, { point: point, brk: brk });
}
});
});
});
};
/**
* Extend getGraphPath by identifying gaps in the data so that we can draw a gap
* in the line or area. This was moved from ordinal axis module to broken axis
* module as of #5045.
*
* @private
* @function Highcharts.Series#gappedPath
*/
H.Series.prototype.gappedPath = function () {
var currentDataGrouping = this.currentDataGrouping,
groupingSize = currentDataGrouping && currentDataGrouping.gapSize,
gapSize = this.options.gapSize,
points = this.points.slice(),
i = points.length - 1,
yAxis = this.yAxis,
xRange,
stack;
/**
* Defines when to display a gap in the graph, together with the
* [gapUnit](plotOptions.series.gapUnit) option.
*
* In case when `dataGrouping` is enabled, points can be grouped into a
* larger time span. This can make the grouped points to have a greater
* distance than the absolute value of `gapSize` property, which will result
* in disappearing graph completely. To prevent this situation the mentioned
* distance between grouped points is used instead of previously defined
* `gapSize`.
*
* In practice, this option is most often used to visualize gaps in
* time series. In a stock chart, intraday data is available for daytime
* hours, while gaps will appear in nights and weekends.
*
* @see [gapUnit](plotOptions.series.gapUnit)
* @see [xAxis.breaks](#xAxis.breaks)
*
* @sample {highstock} stock/plotoptions/series-gapsize/
* Setting the gap size to 2 introduces gaps for weekends in daily
* datasets.
*
* @type {number}
* @default 0
* @product highstock
* @apioption plotOptions.series.gapSize
*/
/**
* Together with [gapSize](plotOptions.series.gapSize), this option defines
* where to draw gaps in the graph.
*
* When the `gapUnit` is `relative` (default), a gap size of 5 means
* that if the distance between two points is greater than five times
* that of the two closest points, the graph will be broken.
*
* When the `gapUnit` is `value`, the gap is based on absolute axis values,
* which on a datetime axis is milliseconds. This also applies to the
* navigator series that inherits gap options from the base series.
*
* @see [gapSize](plotOptions.series.gapSize)
*
* @type {string}
* @default relative
* @since 5.0.13
* @product highstock
* @validvalue ["relative", "value"]
* @apioption plotOptions.series.gapUnit
*/
if (gapSize && i > 0) { // #5008
// Gap unit is relative
if (this.options.gapUnit !== 'value') {
gapSize *= this.closestPointRange;
}
// Setting a new gapSize in case dataGrouping is enabled (#7686)
if (groupingSize && groupingSize > gapSize) {
gapSize = groupingSize;
}
// extension for ordinal breaks
while (i--) {
if (points[i + 1].x - points[i].x > gapSize) {
xRange = (points[i].x + points[i + 1].x) / 2;
points.splice( // insert after this one
i + 1,
0,
{
isNull: true,
x: xRange
}
);
// For stacked chart generate empty stack items, #6546
if (this.options.stacking) {
stack = yAxis.stacks[this.stackKey][xRange] =
new H.StackItem(
yAxis,
yAxis.options.stackLabels,
false,
xRange,
this.stack
);
stack.total = 0;
}
}
}
}
// Call base method
return this.getGraphPath(points);
};
@@ -0,0 +1,361 @@
/* *
* (c) 2010-2019 Kacper Madej
*
* License: www.highcharts.com/license
*/
'use strict';
import H from '../parts/Globals.js';
import '../parts/Utilities.js';
var pick = H.pick,
isNumber = H.isNumber,
relativeLength = H.relativeLength,
seriesType = H.seriesType,
columnProto = H.seriesTypes.column.prototype;
/**
* The bullet series type.
*
* @private
* @class
* @name Highcharts.seriesTypes.bullet
*
* @augments Highcharts.Series
*/
seriesType('bullet', 'column'
/**
* A bullet graph is a variation of a bar graph. The bullet graph features a
* single measure, compares it to a target, and displays it in the context of
* qualitative ranges of performance that could be set using
* [plotBands](#yAxis.plotBands) on [yAxis](#yAxis).
*
* @sample {highcharts} highcharts/demo/bullet-graph/
* Bullet graph
*
* @extends plotOptions.column
* @since 6.0.0
* @product highcharts
* @excluding allAreas, boostThreshold, colorAxis, compare, compareBase
* @optionparent plotOptions.bullet
*/
, {
/**
* All options related with look and positiong of targets.
*
* @since 6.0.0
*/
targetOptions: {
/**
* The width of the rectangle representing the target. Could be set
* as a pixel value or as a percentage of a column width.
*
* @type {number|string}
* @since 6.0.0
*/
width: '140%',
/**
* The height of the rectangle representing the target.
*
* @since 6.0.0
*/
height: 3,
/**
* The border color of the rectangle representing the target. When
* not set, the point's border color is used.
*
* In styled mode, use class `highcharts-bullet-target` instead.
*
* @type {Highcharts.ColorString}
* @since 6.0.0
* @product highcharts
* @apioption plotOptions.bullet.targetOptions.borderColor
*/
/**
* The color of the rectangle representing the target. When not set,
* point's color (if set in point's options -
* [`color`](#series.bullet.data.color)) or zone of the target value
* (if [`zones`](#plotOptions.bullet.zones) or
* [`negativeColor`](#plotOptions.bullet.negativeColor) are set)
* or the same color as the point has is used.
*
* In styled mode, use class `highcharts-bullet-target` instead.
*
* @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
* @since 6.0.0
* @product highcharts
* @apioption plotOptions.bullet.targetOptions.color
*/
/**
* The border width of the rectangle representing the target.
*
* In styled mode, use class `highcharts-bullet-target` instead.
*
* @since 6.0.0
*/
borderWidth: 0
},
tooltip: {
pointFormat: '<span style="color:{series.color}">\u25CF</span>' +
' {series.name}: <b>{point.y}</b>. Target: <b>{point.target}' +
'</b><br/>'
}
}, {
pointArrayMap: ['y', 'target'],
parallelArrays: ['x', 'y', 'target'],
/**
* Draws the targets. For inverted chart, the `series.group` is rotated, so
* the same coordinates apply. This method is based on column series
* drawPoints function.
*
* @ignore
* @function Highcharts.Series#drawPoints
*/
drawPoints: function () {
var series = this,
chart = series.chart,
options = series.options,
animationLimit = options.animationLimit || 250;
columnProto.drawPoints.apply(this);
series.points.forEach(function (point) {
var pointOptions = point.options,
shapeArgs,
targetGraphic = point.targetGraphic,
targetShapeArgs,
targetVal = point.target,
pointVal = point.y,
width,
height,
targetOptions,
y;
if (isNumber(targetVal) && targetVal !== null) {
targetOptions = H.merge(
options.targetOptions,
pointOptions.targetOptions
);
height = targetOptions.height;
shapeArgs = point.shapeArgs;
width = relativeLength(
targetOptions.width,
shapeArgs.width
);
y = series.yAxis.translate(
targetVal,
false,
true,
false,
true
) - targetOptions.height / 2 - 0.5;
targetShapeArgs = series.crispCol.apply({
// Use fake series object to set borderWidth of target
chart: chart,
borderWidth: targetOptions.borderWidth,
options: {
crisp: options.crisp
}
}, [
shapeArgs.x + shapeArgs.width / 2 - width / 2,
y,
width,
height
]);
if (targetGraphic) {
// Update
targetGraphic[
chart.pointCount < animationLimit ?
'animate' :
'attr'
](targetShapeArgs);
// Add or remove tooltip reference
if (isNumber(pointVal) && pointVal !== null) {
targetGraphic.element.point = point;
} else {
targetGraphic.element.point = undefined;
}
} else {
point.targetGraphic = targetGraphic = chart.renderer
.rect()
.attr(targetShapeArgs)
.add(series.group);
}
// Presentational
if (!chart.styledMode) {
targetGraphic.attr({
fill: pick(
targetOptions.color,
pointOptions.color,
(series.zones.length && (point.getZone.call({
series: series,
x: point.x,
y: targetVal,
options: {}
}).color || series.color)) || undefined,
point.color,
series.color
),
stroke: pick(
targetOptions.borderColor,
point.borderColor,
series.options.borderColor
),
'stroke-width': targetOptions.borderWidth
});
}
// Add tooltip reference
if (isNumber(pointVal) && pointVal !== null) {
targetGraphic.element.point = point;
}
targetGraphic.addClass(point.getClassName() +
' highcharts-bullet-target', true);
} else if (targetGraphic) {
point.targetGraphic = targetGraphic.destroy(); // #1269
}
});
},
/**
* Includes target values to extend extremes from y values.
*
* @ignore
* @function Highcharts.Series#getExtremes
*/
getExtremes: function (yData) {
var series = this,
targetData = series.targetData,
yMax,
yMin;
columnProto.getExtremes.call(this, yData);
if (targetData && targetData.length) {
yMax = series.dataMax;
yMin = series.dataMin;
columnProto.getExtremes.call(this, targetData);
series.dataMax = Math.max(series.dataMax, yMax);
series.dataMin = Math.min(series.dataMin, yMin);
}
}
},
/** @lends Highcharts.seriesTypes.ohlc.prototype.pointClass.prototype */
{
/**
* Destroys target graphic.
*
* @private
* @function
*/
destroy: function () {
if (this.targetGraphic) {
this.targetGraphic = this.targetGraphic.destroy();
}
columnProto.pointClass.prototype.destroy.apply(this, arguments);
}
});
/**
* A `bullet` series. If the [type](#series.bullet.type) option is not
* specified, it is inherited from [chart.type](#chart.type).
*
* @extends series,plotOptions.bullet
* @since 6.0.0
* @product highcharts
* @excluding dataParser, dataURL, marker
* @apioption series.bullet
*/
/**
* An array of data points for the series. For the `bullet` series type,
* points can be given in the following ways:
*
* 1. An array of arrays with 3 or 2 values. In this case, the values correspond
* to `x,y,target`. If the first value is a string, it is applied as the name
* of the point, and the `x` value is inferred. The `x` value can also be
* omitted, in which case the inner arrays should be of length 2\. Then the
* `x` value is automatically calculated, either starting at 0 and
* incremented by 1, or from `pointStart` and `pointInterval` given in the
* series options.
* ```js
* data: [
* [0, 40, 75],
* [1, 50, 50],
* [2, 60, 40]
* ]
* ```
*
* 2. An array of objects with named values. The following snippet shows only a
* few settings, see the complete options set below. If the total number of
* data points exceeds the series'
* [turboThreshold](#series.bullet.turboThreshold), this option is not
* available.
* ```js
* data: [{
* x: 0,
* y: 40,
* target: 75,
* name: "Point1",
* color: "#00FF00"
* }, {
* x: 1,
* y: 60,
* target: 40,
* name: "Point2",
* color: "#FF00FF"
* }]
* ```
*
* @type {Array<Array<(number|string),number>|Array<(number|string),number,number>|*>}
* @extends series.column.data
* @since 6.0.0
* @product highcharts
* @apioption series.bullet.data
*/
/**
* The target value of a point.
*
* @type {number}
* @since 6.0.0
* @product highcharts
* @apioption series.bullet.data.target
*/
/**
* Individual target options for each point.
*
* @extends plotOptions.bullet.targetOptions
* @product highcharts
* @apioption series.bullet.data.targetOptions
*/
/**
* @product highcharts
* @excluding halo, lineWidth, lineWidthPlus, marker
* @apioption series.bullet.states.hover
*/
/**
* @product highcharts
* @excluding halo, lineWidth, lineWidthPlus, marker
* @apioption series.bullet.states.select
*/
@@ -0,0 +1,389 @@
/* *
* (c) 2010-2019 Kacper Madej
*
* License: www.highcharts.com/license
*/
'use strict';
import H from '../parts/Globals.js';
import '../parts/Utilities.js';
import '../parts/ColumnSeries.js';
import '../parts/SvgRenderer.js';
var charts = H.charts,
color = H.color,
deg2rad = H.deg2rad,
perspective = H.perspective,
seriesType = H.seriesType,
// Work on H.Renderer instead of H.SVGRenderer for VML support.
RendererProto = H.Renderer.prototype,
cuboidPath = RendererProto.cuboidPath,
cylinderMethods;
/**
* The cylinder series type.
*
* @requires module:highcharts-3d
* @requires module:modules/cylinder
*
* @private
* @class
* @name Highcharts.seriesTypes.cylinder
*
* @augments Highcharts.Series
*/
seriesType(
'cylinder',
'column',
/**
* A cylinder graph is a variation of a 3d column graph. The cylinder graph
* features cylindrical points.
*
* @sample {highcharts} highcharts/demo/cylinder/
* Cylinder graph
*
* @extends plotOptions.column
* @since 7.0.0
* @product highcharts
* @excluding allAreas, boostThreshold, colorAxis, compare, compareBase
* @optionparent plotOptions.cylinder
*/
{},
{}, /** @lends Highcharts.seriesTypes.cylinder#pointClass# */ {
shapeType: 'cylinder'
}
);
/**
* A `cylinder` series. If the [type](#series.cylinder.type) option is not
* specified, it is inherited from [chart.type](#chart.type).
*
* @extends series,plotOptions.cylinder
* @since 7.0.0
* @product highcharts
* @excluding allAreas, boostThreshold, colorAxis, compare, compareBase
* @apioption series.cylinder
*/
/**
* An array of data points for the series. For the `cylinder` series type,
* points can be given in the following ways:
*
* 1. An array of numerical values. In this case, the numerical values will be
* interpreted as `y` options. The `x` values will be automatically
* calculated, either starting at 0 and incremented by 1, or from
* `pointStart` and `pointInterval` given in the series options. If the axis
* has categories, these will be used. Example:
* ```js
* data: [0, 5, 3, 5]
* ```
*
* 2. An array of arrays with 2 values. In this case, the values correspond to
* `x,y`. If the first value is a string, it is applied as the name of the
* point, and the `x` value is inferred.
* ```js
* data: [
* [0, 0],
* [1, 8],
* [2, 9]
* ]
* ```
*
* 3. An array of objects with named values. The following snippet shows only a
* few settings, see the complete options set below. If the total number of
* data points exceeds the series'
* [turboThreshold](#series.cylinder.turboThreshold), this option is not
* available.
*
* ```js
* data: [{
* x: 1,
* y: 2,
* name: "Point2",
* color: "#00FF00"
* }, {
* x: 1,
* y: 4,
* name: "Point1",
* color: "#FF00FF"
* }]
* ```
*
* @sample {highcharts} highcharts/chart/reflow-true/
* Numerical values
* @sample {highcharts} highcharts/series/data-array-of-arrays/
* Arrays of numeric x and y
* @sample {highcharts} highcharts/series/data-array-of-arrays-datetime/
* Arrays of datetime x and y
* @sample {highcharts} highcharts/series/data-array-of-name-value/
* Arrays of point.name and y
* @sample {highcharts} highcharts/series/data-array-of-objects/
* Config objects
*
* @type {Array<number|Array<(number|string),number>|*>}
* @extends series.column.data
* @product highcharts highstock
* @apioption series.cylinder.data
*/
// cylinder extends cuboid
cylinderMethods = H.merge(RendererProto.elements3d.cuboid, {
parts: ['top', 'bottom', 'front', 'back'],
pathType: 'cylinder',
fillSetter: function (fill) {
this.singleSetterForParts('fill', null, {
front: fill,
back: fill,
top: color(fill).brighten(0.1).get(),
bottom: color(fill).brighten(-0.1).get()
});
// fill for animation getter (#6776)
this.color = this.fill = fill;
return this;
}
});
RendererProto.elements3d.cylinder = cylinderMethods;
RendererProto.cylinder = function (shapeArgs) {
return this.element3d('cylinder', shapeArgs);
};
// Generates paths and zIndexes.
RendererProto.cylinderPath = function (shapeArgs) {
var renderer = this,
chart = charts[renderer.chartIndex],
// decide zIndexes of parts based on cubiod logic, for consistency.
cuboidData = cuboidPath.call(renderer, shapeArgs),
isTopFirst = !cuboidData.isTop,
isFronFirst = !cuboidData.isFront,
top = renderer.getCylinderEnd(chart, shapeArgs),
bottom = renderer.getCylinderEnd(chart, shapeArgs, true);
return {
front: renderer.getCylinderFront(top, bottom),
back: renderer.getCylinderBack(top, bottom),
top: top,
bottom: bottom,
zIndexes: {
top: isTopFirst ? 3 : 0,
bottom: isTopFirst ? 0 : 3,
front: isFronFirst ? 2 : 1,
back: isFronFirst ? 1 : 2,
group: cuboidData.zIndexes.group
}
};
};
// Returns cylinder Front path
RendererProto.getCylinderFront = function (topPath, bottomPath) {
var path = topPath.slice(0, topPath.simplified ? 9 : 17);
path.push('L');
if (bottomPath.simplified) {
path = path
.concat(bottomPath.slice(7, 9))
.concat(bottomPath.slice(3, 6))
.concat(bottomPath.slice(0, 3));
// change 'M' into 'L'
path[path.length - 3] = 'L';
} else {
path.push(
bottomPath[15], bottomPath[16],
'C',
bottomPath[13], bottomPath[14],
bottomPath[11], bottomPath[12],
bottomPath[8], bottomPath[9],
'C',
bottomPath[6], bottomPath[7],
bottomPath[4], bottomPath[5],
bottomPath[1], bottomPath[2]
);
}
path.push('Z');
return path;
};
// Returns cylinder Back path
RendererProto.getCylinderBack = function (topPath, bottomPath) {
var path = ['M'];
if (bottomPath.simplified) {
path = path.concat(topPath.slice(7, 12));
// end at start
path.push(
'L',
topPath[1], topPath[2]
);
} else {
path = path.concat(topPath.slice(15));
}
path.push('L');
if (bottomPath.simplified) {
path = path
.concat(bottomPath.slice(1, 3))
.concat(bottomPath.slice(9, 12))
.concat(bottomPath.slice(6, 9));
} else {
path.push(
bottomPath[29], bottomPath[30],
'C',
bottomPath[27], bottomPath[28],
bottomPath[25], bottomPath[26],
bottomPath[22], bottomPath[23],
'C',
bottomPath[20], bottomPath[21],
bottomPath[18], bottomPath[19],
bottomPath[15], bottomPath[16]
);
}
path.push('Z');
return path;
};
// Retruns cylinder path for top or bottom
RendererProto.getCylinderEnd = function (chart, shapeArgs, isBottom) {
// A half of the smaller one out of width or depth
var radius = Math.min(shapeArgs.width, shapeArgs.depth) / 2,
// Approximated longest diameter
angleOffset = deg2rad * (chart.options.chart.options3d.beta - 90),
// Could be top or bottom of the cylinder
y = shapeArgs.y + (isBottom ? shapeArgs.height : 0),
// Use cubic Bezier curve to draw a cricle in x,z (y is constant).
// More math. at spencermortensen.com/articles/bezier-circle/
c = 0.5519 * radius,
centerX = shapeArgs.width / 2 + shapeArgs.x,
centerZ = shapeArgs.depth / 2 + shapeArgs.z,
// points could be generated in a loop, but readability will plummet
points = [{ // M - starting point
x: 0,
y: y,
z: radius
}, { // C1 - control point 1
x: c,
y: y,
z: radius
}, { // C1 - control point 2
x: radius,
y: y,
z: c
}, { // C1 - end point
x: radius,
y: y,
z: 0
}, { // C2 - control point 1
x: radius,
y: y,
z: -c
}, { // C2 - control point 2
x: c,
y: y,
z: -radius
}, { // C2 - end point
x: 0,
y: y,
z: -radius
}, { // C3 - control point 1
x: -c,
y: y,
z: -radius
}, { // C3 - control point 2
x: -radius,
y: y,
z: -c
}, { // C3 - end point
x: -radius,
y: y,
z: 0
}, { // C4 - control point 1
x: -radius,
y: y,
z: c
}, { // C4 - control point 2
x: -c,
y: y,
z: radius
}, { // C4 - end point
x: 0,
y: y,
z: radius
}],
cosTheta = Math.cos(angleOffset),
sinTheta = Math.sin(angleOffset),
perspectivePoints,
path,
x, z;
// rotete to match chart's beta and translate to the shape center
points.forEach(function (point, i) {
x = point.x;
z = point.z;
// x′ = (x * cosθ − z * sinθ) + centerX
// z′ = (z * cosθ + x * sinθ) + centerZ
points[i].x = (x * cosTheta - z * sinTheta) + centerX;
points[i].z = (z * cosTheta + x * sinTheta) + centerZ;
});
perspectivePoints = perspective(points, chart, true);
// check for sub-pixel curve issue, compare front and back edges
if (Math.abs(perspectivePoints[3].y - perspectivePoints[9].y) < 2.5) {
// use simplied shape
path = this.toLinePath([
perspectivePoints[0],
perspectivePoints[3],
perspectivePoints[6],
perspectivePoints[9]
], true);
path.simplified = true;
} else {
// or default curved path to imitate ellipse (2D circle)
path = this.getCurvedPath(perspectivePoints);
}
return path;
};
// Returns curved path in format of:
// [ M, x, y, [C, cp1x, cp2y, cp2x, cp2y, epx, epy]*n_times ]
// (cp - control point, ep - end point)
RendererProto.getCurvedPath = function (points) {
var path = [
'M',
points[0].x, points[0].y
],
limit = points.length - 2,
i;
for (i = 1; i < limit; i += 3) {
path.push(
'C',
points[i].x, points[i].y,
points[i + 1].x, points[i + 1].y,
points[i + 2].x, points[i + 2].y
);
}
return path;
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,109 @@
/* *
*
* (c) 2010-2019 Torstein Honsi
*
* License: www.highcharts.com/license
*
* */
'use strict';
import H from '../parts/Globals.js';
var addEvent = H.addEvent,
isNumber = H.isNumber,
setOptions = H.setOptions,
each = H.each;
setOptions({
/**
* @optionparent chart
*/
chart: {
/**
* Whether to display errors on the chart. When `false`, the errors will
* be shown only in the console.
*
* Requires `debugger.js` module.
*
* @sample highcharts/chart/display-errors/
* Show errors on chart
*
* @since 7.0.0
*/
displayErrors: true
}
});
addEvent(H.Chart, 'displayError', function (e) {
var chart = this,
code = e.code,
msg,
options = chart.options.chart,
renderer = chart.renderer,
chartWidth,
chartHeight;
if (chart.errorElements) {
each(chart.errorElements, function (el) {
if (el) {
el.destroy();
}
});
}
if (options && options.displayErrors) {
chart.errorElements = [];
msg = isNumber(code) ? 'Highcharts error #' + code + ': ' +
H.errorMessages[code].title + H.errorMessages[code].text : code;
chartWidth = chart.chartWidth;
chartHeight = chart.chartHeight;
// Render red chart frame.
chart.errorElements[0] = renderer.rect(
2,
2,
chartWidth - 4,
chartHeight - 4
).attr({
'stroke-width': 4,
stroke: '#ff0000',
zIndex: 3
}).add();
// Render error message.
chart.errorElements[1] = renderer.label(
msg,
0,
0,
'rect',
null,
null,
true
).css({
color: '#ffffff',
width: chartWidth - 16,
padding: 0
}).attr({
fill: '#ff0000',
width: chartWidth,
padding: 8,
zIndex: 10
}).add();
chart.errorElements[1].attr({
y: chartHeight - this.errorElements[1].getBBox().height
});
}
});
addEvent(H.Chart, 'beforeRedraw', function () {
var errorElements = this.errorElements;
if (errorElements && errorElements.length) {
each(errorElements, function (el) {
el.destroy();
});
}
this.errorElements = null;
});
@@ -0,0 +1,199 @@
/* *
* Experimental dependency wheel module
*
* (c) 2018 Torstein Honsi
*
* License: www.highcharts.com/license
*/
'use strict';
import H from '../parts/Globals.js';
import '../parts/Utilities.js';
import '../parts/Options.js';
import '../mixins/nodes.js';
var unsupportedSeriesType = H.seriesType;
var base = H.seriesTypes.sankey.prototype;
unsupportedSeriesType('dependencywheel', 'sankey', {
center: [],
curveFactor: 0.6,
startAngle: 0
}, {
orderNodes: false,
getCenter: H.seriesTypes.pie.prototype.getCenter,
// Dependency wheel has only one column, it runs along the perimeter
createNodeColumns: function () {
var columns = [this.createNodeColumn()];
this.nodes.forEach(function (node) {
node.column = 0;
columns[0].push(node);
});
return columns;
},
// Translate from vertical pixels to perimeter
getNodePadding: function () {
return this.options.nodePadding / Math.PI;
},
createNode: function (id) {
var node = base.createNode.call(this, id);
node.index = this.nodes.length - 1;
// Return the sum of incoming and outgoing links
node.getSum = function () {
return node.linksFrom.concat(node.linksTo)
.reduce(function (acc, link) {
return acc + link.weight;
}, 0);
};
// Get the offset in weight values of a point/link.
node.offset = function (point) {
var offset = 0,
i,
links = node.linksFrom.concat(node.linksTo),
sliced;
function otherNode(link) {
if (link.fromNode === node) {
return link.toNode;
}
return link.fromNode;
}
// Sort and slice the links to avoid links going out of each node
// crossing each other.
links.sort(function (a, b) {
return otherNode(a).index - otherNode(b).index;
});
for (i = 0; i < links.length; i++) {
if (otherNode(links[i]).index > node.index) {
links = links.slice(0, i).reverse().concat(
links.slice(i).reverse()
);
sliced = true;
break;
}
}
if (!sliced) {
links.reverse();
}
for (i = 0; i < links.length; i++) {
if (links[i] === point) {
return offset;
}
offset += links[i].weight;
}
};
return node;
},
// @todo: Override the refactored sankey translateLink and translateNode
// functions instead of the whole translate function
translate: function () {
var options = this.options,
factor = 2 * Math.PI /
(this.chart.plotHeight + this.getNodePadding()),
center = this.getCenter(),
startAngle = (options.startAngle - 90) * H.deg2rad;
base.translate.call(this);
this.nodeColumns[0].forEach(function (node) {
var shapeArgs = node.shapeArgs,
centerX = center[0],
centerY = center[1],
r = center[2] / 2,
innerR = r - options.nodeWidth,
start = startAngle + factor * shapeArgs.y,
end = startAngle + factor * (shapeArgs.y + shapeArgs.height);
node.shapeType = 'arc';
node.shapeArgs = {
x: centerX,
y: centerY,
r: r,
innerR: innerR,
start: start,
end: end
};
node.dlBox = {
x: centerX + Math.cos((start + end) / 2) * (r + innerR) / 2,
y: centerY + Math.sin((start + end) / 2) * (r + innerR) / 2,
width: 1,
height: 1
};
// Draw the links from this node
node.linksFrom.forEach(function (point) {
var distance;
var corners = point.linkBase.map(function (top, i) {
var angle = factor * top,
x = Math.cos(startAngle + angle) * (innerR + 1),
y = Math.sin(startAngle + angle) * (innerR + 1),
curveFactor = options.curveFactor;
// The distance between the from and to node along the
// perimeter. This affect how curved the link is, so that
// links between neighbours don't extend too far towards the
// center.
distance = Math.abs(point.linkBase[3 - i] * factor - angle);
if (distance > Math.PI) {
distance = 2 * Math.PI - distance;
}
distance = distance * innerR;
if (distance < innerR) {
curveFactor *= (distance / innerR);
}
return {
x: centerX + x,
y: centerY + y,
cpX: centerX + (1 - curveFactor) * x,
cpY: centerY + (1 - curveFactor) * y
};
});
point.shapeArgs = {
d: [
'M',
corners[0].x, corners[0].y,
'A',
innerR, innerR,
0,
0, // long arc
1, // clockwise
corners[1].x, corners[1].y,
'C',
corners[1].cpX, corners[1].cpY,
corners[2].cpX, corners[2].cpY,
corners[2].x, corners[2].y,
'A',
innerR, innerR,
0,
0,
1,
corners[3].x, corners[3].y,
'C',
corners[3].cpX, corners[3].cpY,
corners[0].cpX, corners[0].cpY,
corners[0].x, corners[0].y
]
};
});
});
}
});
@@ -0,0 +1,624 @@
/* *
* Plugin for resizing axes / panes in a chart.
*
* (c) 2010-2017 Highsoft AS
* Author: Kacper Madej
*
* License: www.highcharts.com/license
*/
'use strict';
import H from '../parts/Globals.js';
import '../parts/Utilities.js';
import '../parts/Axis.js';
import '../parts/Pointer.js';
var hasTouch = H.hasTouch,
merge = H.merge,
wrap = H.wrap,
isNumber = H.isNumber,
addEvent = H.addEvent,
relativeLength = H.relativeLength,
objectEach = H.objectEach,
Axis = H.Axis,
Pointer = H.Pointer,
// Default options for AxisResizer.
resizerOptions = {
/**
* Minimal size of a resizable axis. Could be set as a percent
* of plot area or pixel size.
*
* This feature requires the `drag-panes.js` module.
*
* @type {Number|String}
* @product highstock
* @sample {highstock} stock/yaxis/resize-min-max-length
* minLength and maxLength
* @apioption yAxis.minLength
*/
minLength: '10%',
/**
* Maximal size of a resizable axis. Could be set as a percent
* of plot area or pixel size.
*
* This feature requires the `drag-panes.js` module.
*
* @type {String|Number}
* @product highstock
* @sample {highstock} stock/yaxis/resize-min-max-length
* minLength and maxLength
* @apioption yAxis.maxLength
*/
maxLength: '100%',
/**
* Options for axis resizing. This feature requires the
* [drag-panes.js](http://code.highcharts.com/stock/modules/drag-panes.js)
* module. It adds a thick line between panes which the user can drag
* in order to resize the panes.
*
* @product highstock
* @sample {highstock} stock/demo/candlestick-and-volume
* Axis resizing enabled
* @optionparent yAxis.resize
*/
resize: {
/**
* Contains two arrays of axes that are controlled by control line
* of the axis.
*
* This feature requires the `drag-panes.js` module.
*/
controlledAxis: {
/**
* Array of axes that should move out of the way of resizing
* being done for the current axis. If not set, the next axis
* will be used.
*
* This feature requires the `drag-panes.js` module.
*
* @type {Array<String|Number>}
* @default []
* @sample {highstock} stock/yaxis/multiple-resizers
* Three panes with resizers
* @sample {highstock} stock/yaxis/resize-multiple-axes
* One resizer controlling multiple axes
*/
next: [],
/**
* Array of axes that should move with the current axis
* while resizing.
*
* This feature requires the `drag-panes.js` module.
*
* @type {Array<String|Number>}
* @sample {highstock} stock/yaxis/multiple-resizers
* Three panes with resizers
* @sample {highstock} stock/yaxis/resize-multiple-axes
* One resizer controlling multiple axes
*/
prev: []
},
/**
* Enable or disable resize by drag for the axis.
*
* This feature requires the `drag-panes.js` module.
*
* @sample {highstock} stock/demo/candlestick-and-volume
* Enabled resizer
*/
enabled: false,
/**
* Cursor style for the control line.
*
* In styled mode use class `highcharts-axis-resizer` instead.
*
* This feature requires the `drag-panes.js` module.
*/
cursor: 'ns-resize',
/**
* Color of the control line.
*
* In styled mode use class `highcharts-axis-resizer` instead.
*
* This feature requires the `drag-panes.js` module.
*
* @type {Color}
* @sample {highstock} stock/yaxis/styled-resizer Styled resizer
*/
lineColor: '#cccccc',
/**
* Dash style of the control line.
*
* In styled mode use class `highcharts-axis-resizer` instead.
*
* This feature requires the `drag-panes.js` module.
*
* @sample {highstock} stock/yaxis/styled-resizer Styled resizer
* @see For supported options check
* [dashStyle](#plotOptions.series.dashStyle)
*/
lineDashStyle: 'Solid',
/**
* Width of the control line.
*
* In styled mode use class `highcharts-axis-resizer` instead.
*
* This feature requires the `drag-panes.js` module.
*
* @sample {highstock} stock/yaxis/styled-resizer Styled resizer
*/
lineWidth: 4,
/**
* Horizontal offset of the control line.
*
* This feature requires the `drag-panes.js` module.
*
* @sample {highstock} stock/yaxis/styled-resizer Styled resizer
*/
x: 0,
/**
* Vertical offset of the control line.
*
* This feature requires the `drag-panes.js` module.
*
* @sample {highstock} stock/yaxis/styled-resizer Styled resizer
*/
y: 0
}
};
merge(true, Axis.prototype.defaultYAxisOptions, resizerOptions);
/**
* The AxisResizer class.
*
* @private
* @class
* @name Highcharts.AxisResizer
*
* @param {Highcharts.Axis} axis
* Main axis for the AxisResizer.
*/
H.AxisResizer = function (axis) {
this.init(axis);
};
H.AxisResizer.prototype = {
/**
* Initialize the AxisResizer object.
*
* @function Highcharts.AxisResizer#init
*
* @param {Highcharts.Axis} axis
* Main axis for the AxisResizer.
*/
init: function (axis, update) {
this.axis = axis;
this.options = axis.options.resize;
this.render();
if (!update) {
// Add mouse events.
this.addMouseEvents();
}
},
/**
* Render the AxisResizer
*
* @function Highcharts.AxisResizer#render
*/
render: function () {
var resizer = this,
axis = resizer.axis,
chart = axis.chart,
options = resizer.options,
x = options.x,
y = options.y,
// Normalize control line position according to the plot area
pos = Math.min(
Math.max(
axis.top + axis.height + y,
chart.plotTop
),
chart.plotTop + chart.plotHeight
),
attr = {},
lineWidth;
if (!chart.styledMode) {
attr = {
cursor: options.cursor,
stroke: options.lineColor,
'stroke-width': options.lineWidth,
dashstyle: options.lineDashStyle
};
}
// Register current position for future reference.
resizer.lastPos = pos - y;
if (!resizer.controlLine) {
resizer.controlLine = chart.renderer.path()
.addClass('highcharts-axis-resizer');
}
// Add to axisGroup after axis update, because the group is recreated
// Do .add() before path is calculated because strokeWidth() needs it.
resizer.controlLine.add(axis.axisGroup);
lineWidth = chart.styledMode ?
resizer.controlLine.strokeWidth() :
options.lineWidth;
attr.d = chart.renderer.crispLine(
[
'M', axis.left + x, pos,
'L', axis.left + axis.width + x, pos
],
lineWidth
);
resizer.controlLine.attr(attr);
},
/**
* Set up the mouse and touch events for the control line.
*
* @function Highcharts.AxisResizer#addMouseEvents
*/
addMouseEvents: function () {
var resizer = this,
ctrlLineElem = resizer.controlLine.element,
container = resizer.axis.chart.container,
eventsToUnbind = [],
mouseMoveHandler,
mouseUpHandler,
mouseDownHandler;
/**
* Create mouse events' handlers.
* Make them as separate functions to enable wrapping them:
*/
resizer.mouseMoveHandler = mouseMoveHandler = function (e) {
resizer.onMouseMove(e);
};
resizer.mouseUpHandler = mouseUpHandler = function (e) {
resizer.onMouseUp(e);
};
resizer.mouseDownHandler = mouseDownHandler = function (e) {
resizer.onMouseDown(e);
};
/**
* Add mouse move and mouseup events. These are bind to doc/container,
* because resizer.grabbed flag is stored in mousedown events.
*/
eventsToUnbind.push(
addEvent(container, 'mousemove', mouseMoveHandler),
addEvent(container.ownerDocument, 'mouseup', mouseUpHandler),
addEvent(ctrlLineElem, 'mousedown', mouseDownHandler)
);
// Touch events.
if (hasTouch) {
eventsToUnbind.push(
addEvent(container, 'touchmove', mouseMoveHandler),
addEvent(container.ownerDocument, 'touchend', mouseUpHandler),
addEvent(ctrlLineElem, 'touchstart', mouseDownHandler)
);
}
resizer.eventsToUnbind = eventsToUnbind;
},
/**
* Mouse move event based on x/y mouse position.
*
* @function Highcharts.AxisResizer#onMouseMove
*
* @param {global.PointerEvent} e
* Mouse event.
*/
onMouseMove: function (e) {
/*
* In iOS, a mousemove event with e.pageX === 0 is fired when holding
* the finger down in the center of the scrollbar. This should
* be ignored. Borrowed from Navigator.
*/
if (!e.touches || e.touches[0].pageX !== 0) {
// Drag the control line
if (this.grabbed) {
this.hasDragged = true;
this.updateAxes(this.axis.chart.pointer.normalize(e).chartY -
this.options.y);
}
}
},
/**
* Mouse up event based on x/y mouse position.
*
* @function Highcharts.AxisResizer#onMouseUp
*
* @param {global.PointerEvent} e
* Mouse event.
*/
onMouseUp: function (e) {
if (this.hasDragged) {
this.updateAxes(this.axis.chart.pointer.normalize(e).chartY -
this.options.y);
}
// Restore runPointActions.
this.grabbed = this.hasDragged = this.axis.chart.activeResizer = null;
},
/**
* Mousedown on a control line.
* Will store necessary information for drag&drop.
*
* @function Highcharts.AxisResizer#onMouseDown
*/
onMouseDown: function () {
// Clear all hover effects.
this.axis.chart.pointer.reset(false, 0);
// Disable runPointActions.
this.grabbed = this.axis.chart.activeResizer = true;
},
/**
* Update all connected axes after a change of control line position
*
* @function Highcharts.AxisResizer#updateAxes
*
* @param {number} chartY
*/
updateAxes: function (chartY) {
var resizer = this,
chart = resizer.axis.chart,
axes = resizer.options.controlledAxis,
nextAxes = axes.next.length === 0 ?
[chart.yAxis.indexOf(resizer.axis) + 1] : axes.next,
// Main axis is included in the prev array by default
prevAxes = [resizer.axis].concat(axes.prev),
axesConfigs = [], // prev and next configs
stopDrag = false,
plotTop = chart.plotTop,
plotHeight = chart.plotHeight,
plotBottom = plotTop + plotHeight,
yDelta,
calculatePercent = function (value) {
return value * 100 / plotHeight + '%';
},
normalize = function (val, min, max) {
return Math.round(Math.min(Math.max(val, min), max));
};
// Normalize chartY to plot area limits
chartY = Math.max(Math.min(chartY, plotBottom), plotTop);
yDelta = chartY - resizer.lastPos;
// Update on changes of at least 1 pixel in the desired direction
if (yDelta * yDelta < 1) {
return;
}
// First gather info how axes should behave
[prevAxes, nextAxes].forEach(function (axesGroup, isNext) {
axesGroup.forEach(function (axisInfo, i) {
// Axes given as array index, axis object or axis id
var axis = isNumber(axisInfo) ?
// If it's a number - it's an index
chart.yAxis[axisInfo] :
(
// If it's first elem. in first group
(!isNext && !i) ?
// then it's an Axis object
axisInfo :
// else it should be an id
chart.get(axisInfo)
),
axisOptions = axis && axis.options,
optionsToUpdate = {},
hDelta = 0,
height, top,
minLength, maxLength;
// Skip if axis is not found
// or it is navigator's yAxis (#7732)
if (
!axisOptions ||
axisOptions.id === 'navigator-y-axis'
) {
return;
}
top = axis.top;
minLength = Math.round(
relativeLength(
axisOptions.minLength,
plotHeight
)
);
maxLength = Math.round(
relativeLength(
axisOptions.maxLength,
plotHeight
)
);
if (isNext) {
// Try to change height first. yDelta could had changed
yDelta = chartY - resizer.lastPos;
// Normalize height to option limits
height = normalize(axis.len - yDelta, minLength, maxLength);
// Adjust top, so the axis looks like shrinked from top
top = axis.top + yDelta;
// Check for plot area limits
if (top + height > plotBottom) {
hDelta = plotBottom - height - top;
chartY += hDelta;
top += hDelta;
}
// Fit to plot - when overflowing on top
if (top < plotTop) {
top = plotTop;
if (top + height > plotBottom) {
height = plotHeight;
}
}
// If next axis meets min length, stop dragging:
if (height === minLength) {
stopDrag = true;
}
axesConfigs.push({
axis: axis,
options: {
top: calculatePercent(top - plotTop),
height: calculatePercent(height)
}
});
} else {
// Normalize height to option limits
height = normalize(chartY - top, minLength, maxLength);
// If prev axis meets max length, stop dragging:
if (height === maxLength) {
stopDrag = true;
}
// Check axis size limits
chartY = top + height;
axesConfigs.push({
axis: axis,
options: {
height: calculatePercent(height)
}
});
}
optionsToUpdate.height = height;
});
});
// If we hit the min/maxLength with dragging, don't do anything:
if (!stopDrag) {
// Now update axes:
axesConfigs.forEach(function (config) {
config.axis.update(config.options, false);
});
chart.redraw(false);
}
},
/**
* Destroy AxisResizer. Clear outside references, clear events,
* destroy elements, nullify properties.
*
* @function Highcharts.AxisResizer#destroy
*/
destroy: function () {
var resizer = this,
axis = resizer.axis;
// Clear resizer in axis
delete axis.resizer;
// Clear control line events
if (this.eventsToUnbind) {
this.eventsToUnbind.forEach(function (unbind) {
unbind();
});
}
// Destroy AxisResizer elements
resizer.controlLine.destroy();
// Nullify properties
objectEach(resizer, function (val, key) {
resizer[key] = null;
});
}
};
// Keep resizer reference on axis update
Axis.prototype.keepProps.push('resizer');
// Add new AxisResizer, update or remove it
addEvent(Axis, 'afterRender', function () {
var axis = this,
resizer = axis.resizer,
resizerOptions = axis.options.resize,
enabled;
if (resizerOptions) {
enabled = resizerOptions.enabled !== false;
if (resizer) {
// Resizer present and enabled
if (enabled) {
// Update options
resizer.init(axis, true);
// Resizer present, but disabled
} else {
// Destroy the resizer
resizer.destroy();
}
} else {
// Resizer not present and enabled
if (enabled) {
// Add new resizer
axis.resizer = new H.AxisResizer(axis);
}
// Resizer not present and disabled, so do nothing
}
}
});
// Clear resizer on axis remove.
addEvent(Axis, 'destroy', function (e) {
if (!e.keepEvents && this.resizer) {
this.resizer.destroy();
}
});
// Prevent any hover effects while dragging a control line of AxisResizer.
wrap(Pointer.prototype, 'runPointActions', function (proceed) {
if (!this.chart.activeResizer) {
proceed.apply(this, Array.prototype.slice.call(arguments, 1));
}
});
// Prevent default drag action detection while dragging a control line of
// AxisResizer. (#7563)
wrap(Pointer.prototype, 'drag', function (proceed) {
if (!this.chart.activeResizer) {
proceed.apply(this, Array.prototype.slice.call(arguments, 1));
}
});
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,983 @@
/**
* Experimental data export module for Highcharts
*
* (c) 2010-2019 Torstein Honsi
*
* License: www.highcharts.com/license
*/
// @todo
// - Set up systematic tests for all series types, paired with tests of the data
// module importing the same data.
'use strict';
import Highcharts from '../parts/Globals.js';
import '../parts/Utilities.js';
import '../parts/Chart.js';
import '../mixins/ajax.js';
import '../mixins/download-url.js';
var defined = Highcharts.defined,
pick = Highcharts.pick,
win = Highcharts.win,
doc = win.document,
seriesTypes = Highcharts.seriesTypes,
downloadURL = Highcharts.downloadURL;
// Can we add this to utils? Also used in screen-reader.js
/**
* HTML encode some characters vulnerable for XSS.
* @param {string} html The input string
* @return {string} The excaped string
*/
function htmlencode(html) {
return html
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;')
.replace(/\//g, '&#x2F;');
}
Highcharts.setOptions({
/**
* @optionparent exporting
*/
exporting: {
/**
* Export-data module required. Caption for the data table. Same as
* chart title by default. Set to `false` to disable.
*
* @sample highcharts/export-data/multilevel-table
* Multiple table headers
*
* @type {boolean|string}
* @since 6.0.4
* @apioption exporting.tableCaption
*/
/**
* Options for exporting data to CSV or ExCel, or displaying the data
* in a HTML table or a JavaScript structure. Requires the
* `export-data.js` module. This module adds data export options to the
* export menu and provides functions like `Chart.getCSV`,
* `Chart.getTable`, `Chart.getDataRows` and `Chart.viewData`.
*
* The XLS converter is limited and only creates a HTML string that is
* passed for download, which works but creates a warning before
* opening. The workaround for this is to use a third party XLSX
* converter, as demonstrated in the sample below.
*
* @sample highcharts/export-data/categorized/ Categorized data
* @sample highcharts/export-data/stock-timeaxis/ Highstock time axis
* @sample highcharts/export-data/xlsx/
* Using a third party XLSX converter
*
* @since 6.0.0
*/
csv: {
/**
* Formatter callback for the column headers. Parameters are:
* - `item` - The series or axis object)
* - `key` - The point key, for example y or z
* - `keyLength` - The amount of value keys for this item, for
* example a range series has the keys `low` and `high` so the
* key length is 2.
*
* If [useMultiLevelHeaders](#exporting.useMultiLevelHeaders) is
* true, columnHeaderFormatter by default returns an object with
* columnTitle and topLevelColumnTitle for each key. Columns with
* the same topLevelColumnTitle have their titles merged into a
* single cell with colspan for table/Excel export.
*
* If `useMultiLevelHeaders` is false, or for CSV export, it returns
* the series name, followed by the key if there is more than one
* key.
*
* For the axis it returns the axis title or "Category" or
* "DateTime" by default.
*
* Return `false` to use Highcharts' proposed header.
*
* @sample highcharts/export-data/multilevel-table
* Multiple table headers
*
* @type {Function|null}
*/
columnHeaderFormatter: null,
/**
* Which date format to use for exported dates on a datetime X axis.
* See `Highcharts.dateFormat`.
*/
dateFormat: '%Y-%m-%d %H:%M:%S',
/**
* Which decimal point to use for exported CSV. Defaults to the same
* as the browser locale, typically `.` (English) or `,` (German,
* French etc).
*
* @type {string|null}
* @since 6.0.4
*/
decimalPoint: null,
/**
* The item delimiter in the exported data. Use `;` for direct
* exporting to Excel. Defaults to a best guess based on the browser
* locale. If the locale _decimal point_ is `,`, the `itemDelimiter`
* defaults to `;`, otherwise the `itemDelimiter` defaults to `,`.
*
* @type {string|null}
*/
itemDelimiter: null,
/**
* The line delimiter in the exported data, defaults to a newline.
*/
lineDelimiter: '\n'
},
/**
* Export-data module required. Show a HTML table below the chart with
* the chart's current data.
*
* @sample highcharts/export-data/showtable/
* Show the table
* @sample highcharts/studies/exporting-table-html
* Experiment with putting the table inside the subtitle to
* allow exporting it.
*
* @since 6.0.0
*/
showTable: false,
/**
* Export-data module required. Use multi level headers in data table.
* If [csv.columnHeaderFormatter](#exporting.csv.columnHeaderFormatter)
* is defined, it has to return objects in order for multi level headers
* to work.
*
* @sample highcharts/export-data/multilevel-table
* Multiple table headers
*
* @since 6.0.4
*/
useMultiLevelHeaders: true,
/**
* Export-data module required. If using multi level table headers, use
* rowspans for headers that have only one level.
*
* @sample highcharts/export-data/multilevel-table
* Multiple table headers
*
* @since 6.0.4
*/
useRowspanHeaders: true
},
/**
* @optionparent lang
*/
lang: {
/**
* Export-data module only. The text for the menu item.
*
* @since 6.0.0
*/
downloadCSV: 'Download CSV',
/**
* Export-data module only. The text for the menu item.
*
* @since 6.0.0
*/
downloadXLS: 'Download XLS',
/**
* Export-data module only. The text for the menu item.
*
* @since 6.1.0
*/
openInCloud: 'Open in Highcharts Cloud',
/**
* Export-data module only. The text for the menu item.
*
* @since 6.0.0
*/
viewData: 'View data table'
}
});
// Add an event listener to handle the showTable option
Highcharts.addEvent(Highcharts.Chart, 'render', function () {
if (
this.options &&
this.options.exporting &&
this.options.exporting.showTable
) {
this.viewData();
}
});
/**
* Set up key-to-axis bindings. This is used when the Y axis is datetime or
* categorized. For example in an arearange series, the low and high values
* should be formatted according to the Y axis type, and in order to link them
* we need this map.
*
* @private
* @function Highcharts.Chart#setUpKeyToAxis
*/
Highcharts.Chart.prototype.setUpKeyToAxis = function () {
if (seriesTypes.arearange) {
seriesTypes.arearange.prototype.keyToAxis = {
low: 'y',
high: 'y'
};
}
if (seriesTypes.gantt) {
seriesTypes.gantt.prototype.keyToAxis = {
start: 'x',
end: 'x'
};
}
};
/**
* Export-data module required. Returns a two-dimensional array containing the
* current chart data.
*
* @function Highcharts.Chart#getDataRows
*
* @param {boolean} multiLevelHeaders
* Use multilevel headers for the rows by default. Adds an extra row with
* top level headers. If a custom columnHeaderFormatter is defined, this
* can override the behavior.
*
* @return {Array<Array<number|string>>}
* The current chart data
*/
Highcharts.Chart.prototype.getDataRows = function (multiLevelHeaders) {
var time = this.time,
csvOptions = (this.options.exporting && this.options.exporting.csv) ||
{},
xAxis,
xAxes = this.xAxis,
rows = {},
rowArr = [],
dataRows,
topLevelColumnTitles = [],
columnTitles = [],
columnTitleObj,
i,
x,
xTitle,
// Options
columnHeaderFormatter = function (item, key, keyLength) {
if (csvOptions.columnHeaderFormatter) {
var s = csvOptions.columnHeaderFormatter(item, key, keyLength);
if (s !== false) {
return s;
}
}
if (!item) {
return 'Category';
}
if (item instanceof Highcharts.Axis) {
return (item.options.title && item.options.title.text) ||
(item.isDatetimeAxis ? 'DateTime' : 'Category');
}
if (multiLevelHeaders) {
return {
columnTitle: keyLength > 1 ? key : item.name,
topLevelColumnTitle: item.name
};
}
return item.name + (keyLength > 1 ? ' (' + key + ')' : '');
},
xAxisIndices = [];
// Loop the series and index values
i = 0;
this.setUpKeyToAxis();
this.series.forEach(function (series) {
var keys = series.options.keys,
pointArrayMap = keys || series.pointArrayMap || ['y'],
valueCount = pointArrayMap.length,
xTaken = !series.requireSorting && {},
categoryMap = {},
datetimeValueAxisMap = {},
xAxisIndex = xAxes.indexOf(series.xAxis),
mockSeries,
j;
// Map the categories for value axes
pointArrayMap.forEach(function (prop) {
var axisName = (
(series.keyToAxis && series.keyToAxis[prop]) ||
prop
) + 'Axis';
categoryMap[prop] = (
series[axisName] &&
series[axisName].categories
) || [];
datetimeValueAxisMap[prop] = (
series[axisName] &&
series[axisName].isDatetimeAxis
);
});
if (
series.options.includeInCSVExport !== false &&
!series.options.isInternal &&
series.visible !== false // #55
) {
// Build a lookup for X axis index and the position of the first
// series that belongs to that X axis. Includes -1 for non-axis
// series types like pies.
if (!Highcharts.find(xAxisIndices, function (index) {
return index[0] === xAxisIndex;
})) {
xAxisIndices.push([xAxisIndex, i]);
}
// Compute the column headers and top level headers, usually the
// same as series names
j = 0;
while (j < valueCount) {
columnTitleObj = columnHeaderFormatter(
series,
pointArrayMap[j],
pointArrayMap.length
);
columnTitles.push(
columnTitleObj.columnTitle || columnTitleObj
);
if (multiLevelHeaders) {
topLevelColumnTitles.push(
columnTitleObj.topLevelColumnTitle || columnTitleObj
);
}
j++;
}
mockSeries = {
chart: series.chart,
autoIncrement: series.autoIncrement,
options: series.options,
pointArrayMap: series.pointArrayMap
};
// Export directly from options.data because we need the uncropped
// data (#7913), and we need to support Boost (#7026).
series.options.data.forEach(function eachData(options, pIdx) {
var key,
prop,
val,
name,
point;
point = { series: mockSeries };
series.pointClass.prototype.applyOptions.apply(
point,
[options]
);
key = point.x;
name = series.data[pIdx] && series.data[pIdx].name;
if (xTaken) {
if (xTaken[key]) {
key += '|' + pIdx;
}
xTaken[key] = true;
}
j = 0;
// Pies, funnels, geo maps etc. use point name in X row
if (!series.xAxis || series.exportKey === 'name') {
key = name;
}
if (!rows[key]) {
// Generate the row
rows[key] = [];
// Contain the X values from one or more X axes
rows[key].xValues = [];
}
rows[key].x = point.x;
rows[key].name = name;
rows[key].xValues[xAxisIndex] = point.x;
while (j < valueCount) {
prop = pointArrayMap[j]; // y, z etc
val = point[prop];
rows[key][i + j] = pick(
categoryMap[prop][val], // Y axis category if present
datetimeValueAxisMap[prop] ?
time.dateFormat(csvOptions.dateFormat, val) :
null,
val
);
j++;
}
});
i = i + j;
}
});
// Make a sortable array
for (x in rows) {
if (rows.hasOwnProperty(x)) {
rowArr.push(rows[x]);
}
}
var xAxisIndex, column;
// Add computed column headers and top level headers to final row set
dataRows = multiLevelHeaders ? [topLevelColumnTitles, columnTitles] :
[columnTitles];
i = xAxisIndices.length;
while (i--) { // Start from end to splice in
xAxisIndex = xAxisIndices[i][0];
column = xAxisIndices[i][1];
xAxis = xAxes[xAxisIndex];
// Sort it by X values
rowArr.sort(function (a, b) { // eslint-disable-line no-loop-func
return a.xValues[xAxisIndex] - b.xValues[xAxisIndex];
});
// Add header row
xTitle = columnHeaderFormatter(xAxis);
dataRows[0].splice(column, 0, xTitle);
if (multiLevelHeaders && dataRows[1]) {
// If using multi level headers, we just added top level header.
// Also add for sub level
dataRows[1].splice(column, 0, xTitle);
}
// Add the category column
rowArr.forEach(function (row) { // eslint-disable-line no-loop-func
var category = row.name;
if (xAxis && !defined(category)) {
if (xAxis.isDatetimeAxis) {
if (row.x instanceof Date) {
row.x = row.x.getTime();
}
category = time.dateFormat(
csvOptions.dateFormat,
row.x
);
} else if (xAxis.categories) {
category = pick(
xAxis.names[row.x],
xAxis.categories[row.x],
row.x
);
} else {
category = row.x;
}
}
// Add the X/date/category
row.splice(column, 0, category);
});
}
dataRows = dataRows.concat(rowArr);
Highcharts.fireEvent(this, 'exportData', { dataRows: dataRows });
return dataRows;
};
/**
* Export-data module required. Returns the current chart data as a CSV string.
*
* @function Highcharts.Chart#getCSV
*
* @param {boolean} useLocalDecimalPoint
* Whether to use the local decimal point as detected from the browser.
* This makes it easier to export data to Excel in the same locale as the
* user is.
*
* @return {string}
* CSV representation of the data
*/
Highcharts.Chart.prototype.getCSV = function (useLocalDecimalPoint) {
var csv = '',
rows = this.getDataRows(),
csvOptions = this.options.exporting.csv,
decimalPoint = pick(
csvOptions.decimalPoint,
csvOptions.itemDelimiter !== ',' && useLocalDecimalPoint ?
(1.1).toLocaleString()[1] :
'.'
),
// use ';' for direct to Excel
itemDelimiter = pick(
csvOptions.itemDelimiter,
decimalPoint === ',' ? ';' : ','
),
// '\n' isn't working with the js csv data extraction
lineDelimiter = csvOptions.lineDelimiter;
// Transform the rows to CSV
rows.forEach(function (row, i) {
var val = '',
j = row.length;
while (j--) {
val = row[j];
if (typeof val === 'string') {
val = '"' + val + '"';
}
if (typeof val === 'number') {
if (decimalPoint !== '.') {
val = val.toString().replace('.', decimalPoint);
}
}
row[j] = val;
}
// Add the values
csv += row.join(itemDelimiter);
// Add the line delimiter
if (i < rows.length - 1) {
csv += lineDelimiter;
}
});
return csv;
};
/**
* Export-data module required. Build a HTML table with the chart's current
* data.
*
* @sample highcharts/export-data/viewdata/
* View the data from the export menu
*
* @function Highcharts.Chart#getTable
*
* @param {boolean} useLocalDecimalPoint
* Whether to use the local decimal point as detected from the browser.
* This makes it easier to export data to Excel in the same locale as the
* user is.
*
* @return {string}
* HTML representation of the data.
*/
Highcharts.Chart.prototype.getTable = function (useLocalDecimalPoint) {
var html = '<table id="highcharts-data-table-' + this.index + '">',
options = this.options,
decimalPoint = useLocalDecimalPoint ? (1.1).toLocaleString()[1] : '.',
useMultiLevelHeaders = pick(
options.exporting.useMultiLevelHeaders, true
),
rows = this.getDataRows(useMultiLevelHeaders),
rowLength = 0,
topHeaders = useMultiLevelHeaders ? rows.shift() : null,
subHeaders = rows.shift(),
// Compare two rows for equality
isRowEqual = function (row1, row2) {
var i = row1.length;
if (row2.length === i) {
while (i--) {
if (row1[i] !== row2[i]) {
return false;
}
}
} else {
return false;
}
return true;
},
// Get table cell HTML from value
getCellHTMLFromValue = function (tag, classes, attrs, value) {
var val = pick(value, ''),
className = 'text' + (classes ? ' ' + classes : '');
// Convert to string if number
if (typeof val === 'number') {
val = val.toString();
if (decimalPoint === ',') {
val = val.replace('.', decimalPoint);
}
className = 'number';
} else if (!value) {
className = 'empty';
}
return '<' + tag + (attrs ? ' ' + attrs : '') +
' class="' + className + '">' +
val + '</' + tag + '>';
},
// Get table header markup from row data
getTableHeaderHTML = function (topheaders, subheaders, rowLength) {
var html = '<thead>',
i = 0,
len = rowLength || subheaders && subheaders.length,
next,
cur,
curColspan = 0,
rowspan;
// Clean up multiple table headers. Chart.getDataRows() returns two
// levels of headers when using multilevel, not merged. We need to
// merge identical headers, remove redundant headers, and keep it
// all marked up nicely.
if (
useMultiLevelHeaders &&
topheaders &&
subheaders &&
!isRowEqual(topheaders, subheaders)
) {
html += '<tr>';
for (; i < len; ++i) {
cur = topheaders[i];
next = topheaders[i + 1];
if (cur === next) {
++curColspan;
} else if (curColspan) {
// Ended colspan
// Add cur to HTML with colspan.
html += getCellHTMLFromValue(
'th',
'highcharts-table-topheading',
'scope="col" ' +
'colspan="' + (curColspan + 1) + '"',
cur
);
curColspan = 0;
} else {
// Cur is standalone. If it is same as sublevel,
// remove sublevel and add just toplevel.
if (cur === subheaders[i]) {
if (options.exporting.useRowspanHeaders) {
rowspan = 2;
delete subheaders[i];
} else {
rowspan = 1;
subheaders[i] = '';
}
} else {
rowspan = 1;
}
html += getCellHTMLFromValue(
'th',
'highcharts-table-topheading',
'scope="col"' +
(rowspan > 1 ?
' valign="top" rowspan="' + rowspan + '"' :
''),
cur
);
}
}
html += '</tr>';
}
// Add the subheaders (the only headers if not using multilevels)
if (subheaders) {
html += '<tr>';
for (i = 0, len = subheaders.length; i < len; ++i) {
if (subheaders[i] !== undefined) {
html += getCellHTMLFromValue(
'th', null, 'scope="col"', subheaders[i]
);
}
}
html += '</tr>';
}
html += '</thead>';
return html;
};
// Add table caption
if (options.exporting.tableCaption !== false) {
html += '<caption class="highcharts-table-caption">' + pick(
options.exporting.tableCaption,
(
options.title.text ?
htmlencode(options.title.text) :
'Chart'
)
) + '</caption>';
}
// Find longest row
for (var i = 0, len = rows.length; i < len; ++i) {
if (rows[i].length > rowLength) {
rowLength = rows[i].length;
}
}
// Add header
html += getTableHeaderHTML(
topHeaders,
subHeaders,
Math.max(rowLength, subHeaders.length)
);
// Transform the rows to HTML
html += '<tbody>';
rows.forEach(function (row) {
html += '<tr>';
for (var j = 0; j < rowLength; j++) {
// Make first column a header too. Especially important for
// category axes, but also might make sense for datetime? Should
// await user feedback on this.
html += getCellHTMLFromValue(
j ? 'td' : 'th',
null,
j ? '' : 'scope="row"',
row[j]
);
}
html += '</tr>';
});
html += '</tbody></table>';
var e = { html: html };
Highcharts.fireEvent(this, 'afterGetTable', e);
return e.html;
};
/**
* Get a blob object from content, if blob is supported
*
* @private
*
* @param {String} content The content to create the blob from.
* @param {String} type The type of the content.
* @return {object} The blob object, or undefined if not supported.
*/
function getBlobFromContent(content, type) {
if (win.Blob && win.navigator.msSaveOrOpenBlob) {
return new win.Blob(
['\uFEFF' + content], // #7084
{ type: type }
);
}
}
/**
* Call this on click of 'Download CSV' button
*
* @private
* @function Highcharts.Chart#downloadCSV
*/
Highcharts.Chart.prototype.downloadCSV = function () {
var csv = this.getCSV(true);
downloadURL(
getBlobFromContent(csv, 'text/csv') ||
'data:text/csv,\uFEFF' + encodeURIComponent(csv),
this.getFilename() + '.csv'
);
};
/**
* Call this on click of 'Download XLS' button
*
* @private
* @function Highcharts.Chart#downloadXLS
*/
Highcharts.Chart.prototype.downloadXLS = function () {
var uri = 'data:application/vnd.ms-excel;base64,',
template = '<html xmlns:o="urn:schemas-microsoft-com:office:office" ' +
'xmlns:x="urn:schemas-microsoft-com:office:excel" ' +
'xmlns="http://www.w3.org/TR/REC-html40">' +
'<head><!--[if gte mso 9]><xml><x:ExcelWorkbook>' +
'<x:ExcelWorksheets><x:ExcelWorksheet>' +
'<x:Name>Ark1</x:Name>' +
'<x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions>' +
'</x:ExcelWorksheet></x:ExcelWorksheets></x:ExcelWorkbook>' +
'</xml><![endif]-->' +
'<style>td{border:none;font-family: Calibri, sans-serif;} ' +
'.number{mso-number-format:"0.00";} ' +
'.text{ mso-number-format:"\@";}</style>' +
'<meta name=ProgId content=Excel.Sheet>' +
'<meta charset=UTF-8>' +
'</head><body>' +
this.getTable(true) +
'</body></html>',
base64 = function (s) {
return win.btoa(unescape(encodeURIComponent(s))); // #50
};
downloadURL(
getBlobFromContent(template, 'application/vnd.ms-excel') ||
uri + base64(template),
this.getFilename() + '.xls'
);
};
/**
* Export-data module required. View the data in a table below the chart.
*
* @function Highcharts.Chart#viewData
*/
Highcharts.Chart.prototype.viewData = function () {
if (!this.dataTableDiv) {
this.dataTableDiv = doc.createElement('div');
this.dataTableDiv.className = 'highcharts-data-table';
// Insert after the chart container
this.renderTo.parentNode.insertBefore(
this.dataTableDiv,
this.renderTo.nextSibling
);
}
this.dataTableDiv.innerHTML = this.getTable();
};
/**
* Experimental function to send a chart's config to the Cloud for editing.
*
* Limitations
* - All functions (formatters and callbacks) are removed since they're not
* JSON.
*
* @function Highcharts.Chart#openInCloud
*
* @todo
* - Let the Cloud throw a friendly warning about unsupported structures like
* formatters.
* - Dynamically updated charts probably fail, we need a generic
* Chart.getOptions function that returns all non-default options. Should also
* be used by the export module.
*/
Highcharts.Chart.prototype.openInCloud = function () {
var options,
paramObj,
params;
// Recursively remove function callbacks
function removeFunctions(ob) {
Object.keys(ob).forEach(function (key) {
if (typeof ob[key] === 'function') {
delete ob[key];
}
if (Highcharts.isObject(ob[key])) { // object and not an array
removeFunctions(ob[key]);
}
});
}
function openInCloud() {
var form = doc.createElement('form');
doc.body.appendChild(form);
form.method = 'post';
form.action = 'https://cloud-api.highcharts.com/openincloud';
form.target = '_blank';
var input = doc.createElement('input');
input.type = 'hidden';
input.name = 'chart';
input.value = params;
form.appendChild(input);
form.submit();
doc.body.removeChild(form);
}
options = Highcharts.merge(this.userOptions);
removeFunctions(options);
paramObj = {
name: (options.title && options.title.text) || 'Chart title',
options: options,
settings: {
constructor: 'Chart',
dataProvider: {
csv: this.getCSV()
}
}
};
params = JSON.stringify(paramObj);
openInCloud();
};
// Add "Download CSV" to the exporting menu.
var exportingOptions = Highcharts.getOptions().exporting;
if (exportingOptions) {
Highcharts.extend(exportingOptions.menuItemDefinitions, {
downloadCSV: {
textKey: 'downloadCSV',
onclick: function () {
this.downloadCSV();
}
},
downloadXLS: {
textKey: 'downloadXLS',
onclick: function () {
this.downloadXLS();
}
},
viewData: {
textKey: 'viewData',
onclick: function () {
this.viewData();
}
},
openInCloud: {
textKey: 'openInCloud',
onclick: function () {
this.openInCloud();
}
}
});
exportingOptions.buttons.contextButton.menuItems.push(
'separator',
'downloadCSV',
'downloadXLS',
'viewData',
'openInCloud'
);
}
// Series specific
if (seriesTypes.map) {
seriesTypes.map.prototype.exportKey = 'name';
}
if (seriesTypes.mapbubble) {
seriesTypes.mapbubble.prototype.exportKey = 'name';
}
if (seriesTypes.treemap) {
seriesTypes.treemap.prototype.exportKey = 'name';
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,40 @@
/**
* (c) 2009-2019 Sebastian Bochann
*
* Full screen for Highcharts
*
* License: www.highcharts.com/license
*/
'use strict';
import H from '../parts/Globals.js';
H.FullScreen = function (container) {
this.init(container.parentNode); // main div of the chart
};
/**
* The module allows user to enable full screen mode in StockTools.
* Based on default solutions in browsers.
*
*/
H.FullScreen.prototype = {
/**
* Init function
*
* @param {HTMLDOMElement} - chart div
*
*/
init: function (container) {
if (container.requestFullscreen) {
container.requestFullscreen();
} else if (container.mozRequestFullScreen) {
container.mozRequestFullScreen();
} else if (container.webkitRequestFullscreen) {
container.webkitRequestFullscreen();
} else if (container.msRequestFullscreen) {
container.msRequestFullscreen();
}
}
};
@@ -0,0 +1,552 @@
/* *
* Highcharts funnel module
*
* (c) 2010-2019 Torstein Honsi
*
* License: www.highcharts.com/license
*/
/* eslint indent: 0 */
'use strict';
import Highcharts from '../parts/Globals.js';
import '../parts/Utilities.js';
import '../parts/Options.js';
import '../parts/Series.js';
// create shortcuts
var seriesType = Highcharts.seriesType,
seriesTypes = Highcharts.seriesTypes,
noop = Highcharts.noop,
pick = Highcharts.pick;
/**
* @private
* @class
* @name Highcharts.seriesTypes.funnel
*
* @augments Highcharts.Series
*/
seriesType('funnel', 'pie',
/**
* Funnel charts are a type of chart often used to visualize stages in a sales
* project, where the top are the initial stages with the most clients.
* It requires that the modules/funnel.js file is loaded.
*
* @sample highcharts/demo/funnel/
* Funnel demo
*
* @extends plotOptions.pie
* @excluding size
* @product highcharts
* @optionparent plotOptions.funnel
*/
{
/**
* Initial animation is by default disabled for the funnel chart.
*/
animation: false,
/**
* The center of the series. By default, it is centered in the middle
* of the plot area, so it fills the plot area height.
*
* @type {Array<number|string>}
* @default ["50%", "50%"]
* @since 3.0
*/
center: ['50%', '50%'],
/**
* The width of the funnel compared to the width of the plot area,
* or the pixel width if it is a number.
*
* @type {number|string}
* @since 3.0
*/
width: '90%',
/**
* The width of the neck, the lower part of the funnel. A number defines
* pixel width, a percentage string defines a percentage of the plot
* area width.
*
* @sample {highcharts} highcharts/demo/funnel/
* Funnel demo
*
* @type {number|string}
* @since 3.0
*/
neckWidth: '30%',
/**
* The height of the funnel or pyramid. If it is a number it defines
* the pixel height, if it is a percentage string it is the percentage
* of the plot area height.
*
* @sample {highcharts} highcharts/demo/funnel/
* Funnel demo
*
* @type {number|string}
* @since 3.0
*/
height: '100%',
/**
* The height of the neck, the lower part of the funnel. A number defines
* pixel width, a percentage string defines a percentage of the plot
* area height.
*
* @type {number|string}
*/
neckHeight: '25%',
/**
* A reversed funnel has the widest area down. A reversed funnel with
* no neck width and neck height is a pyramid.
*
* @since 3.0.10
*/
reversed: false,
/** @ignore */
size: true, // to avoid adapting to data label size in Pie.drawDataLabels
dataLabels: {
connectorWidth: 1
},
/**
* Options for the series states.
*/
states: {
/**
* @excluding halo, marker, lineWidth, lineWidthPlus
* @apioption plotOptions.funnel.states.hover
*/
/**
* Options for a selected funnel item.
*
* @excluding halo, marker, lineWidth, lineWidthPlus
*/
select: {
/**
* A specific color for the selected point.
*
* @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
*/
color: '#cccccc',
/**
* A specific border color for the selected point.
*
* @type {Highcharts.ColorString}
*/
borderColor: '#000000'
}
}
},
// Properties
{
animate: noop,
// Overrides the pie translate method
translate: function () {
var
// Get positions - either an integer or a percentage string
// must be given
getLength = function (length, relativeTo) {
return (/%$/).test(length) ?
relativeTo * parseInt(length, 10) / 100 :
parseInt(length, 10);
},
sum = 0,
series = this,
chart = series.chart,
options = series.options,
reversed = options.reversed,
ignoreHiddenPoint = options.ignoreHiddenPoint,
plotWidth = chart.plotWidth,
plotHeight = chart.plotHeight,
cumulative = 0, // start at top
center = options.center,
centerX = getLength(center[0], plotWidth),
centerY = getLength(center[1], plotHeight),
width = getLength(options.width, plotWidth),
tempWidth,
getWidthAt,
height = getLength(options.height, plotHeight),
neckWidth = getLength(options.neckWidth, plotWidth),
neckHeight = getLength(options.neckHeight, plotHeight),
neckY = (centerY - height / 2) + height - neckHeight,
data = series.data,
path,
fraction,
half = options.dataLabels.position === 'left' ? 1 : 0,
x1,
y1,
x2,
x3,
y3,
x4,
y5;
// Return the width at a specific y coordinate
series.getWidthAt = getWidthAt = function (y) {
var top = (centerY - height / 2);
return (y > neckY || height === neckHeight) ?
neckWidth :
neckWidth + (width - neckWidth) *
(1 - (y - top) / (height - neckHeight));
};
series.getX = function (y, half, point) {
return centerX + (half ? -1 : 1) *
((getWidthAt(reversed ? 2 * centerY - y : y) / 2) +
point.labelDistance);
};
// Expose
series.center = [centerX, centerY, height];
series.centerX = centerX;
/*
Individual point coordinate naming:
x1,y1 _________________ x2,y1
\ /
\ /
\ /
\ /
\ /
x3,y3 _________ x4,y3
Additional for the base of the neck:
| |
| |
| |
x3,y5 _________ x4,y5
*/
// get the total sum
data.forEach(function (point) {
if (!ignoreHiddenPoint || point.visible !== false) {
sum += point.y;
}
});
data.forEach(function (point) {
// set start and end positions
y5 = null;
fraction = sum ? point.y / sum : 0;
y1 = centerY - height / 2 + cumulative * height;
y3 = y1 + fraction * height;
tempWidth = getWidthAt(y1);
x1 = centerX - tempWidth / 2;
x2 = x1 + tempWidth;
tempWidth = getWidthAt(y3);
x3 = centerX - tempWidth / 2;
x4 = x3 + tempWidth;
// the entire point is within the neck
if (y1 > neckY) {
x1 = x3 = centerX - neckWidth / 2;
x2 = x4 = centerX + neckWidth / 2;
// the base of the neck
} else if (y3 > neckY) {
y5 = y3;
tempWidth = getWidthAt(neckY);
x3 = centerX - tempWidth / 2;
x4 = x3 + tempWidth;
y3 = neckY;
}
if (reversed) {
y1 = 2 * centerY - y1;
y3 = 2 * centerY - y3;
if (y5 !== null) {
y5 = 2 * centerY - y5;
}
}
// save the path
path = [
'M',
x1, y1,
'L',
x2, y1,
x4, y3
];
if (y5 !== null) {
path.push(x4, y5, x3, y5);
}
path.push(x3, y3, 'Z');
// prepare for using shared dr
point.shapeType = 'path';
point.shapeArgs = { d: path };
// for tooltips and data labels
point.percentage = fraction * 100;
point.plotX = centerX;
point.plotY = (y1 + (y5 || y3)) / 2;
// Placement of tooltips and data labels
point.tooltipPos = [
centerX,
point.plotY
];
// Slice is a noop on funnel points
point.slice = noop;
// Mimicking pie data label placement logic
point.half = half;
if (!ignoreHiddenPoint || point.visible !== false) {
cumulative += fraction;
}
});
},
// Funnel items don't have angles (#2289)
sortByAngle: function (points) {
points.sort(function (a, b) {
return a.plotY - b.plotY;
});
},
// Extend the pie data label method
drawDataLabels: function () {
var series = this,
data = series.data,
labelDistance = series.options.dataLabels.distance,
leftSide,
sign,
point,
i = data.length,
x,
y;
// In the original pie label anticollision logic, the slots are
// distributed from one labelDistance above to one labelDistance below
// the pie. In funnels we don't want this.
series.center[2] -= 2 * labelDistance;
// Set the label position array for each point.
while (i--) {
point = data[i];
leftSide = point.half;
sign = leftSide ? 1 : -1;
y = point.plotY;
point.labelDistance = pick(
point.options.dataLabels && point.options.dataLabels.distance,
labelDistance
);
series.maxLabelDistance = Math.max(
point.labelDistance,
series.maxLabelDistance || 0
);
x = series.getX(y, leftSide, point);
// set the anchor point for data labels
point.labelPosition = {
// initial position of the data label - it's utilized for
// finding the final position for the label
natural: {
x: 0,
y: y
},
'final': {
// used for generating connector path -
// initialized later in drawDataLabels function
// x: undefined,
// y: undefined
},
// left - funnel on the left side of the data label
// right - funnel on the right side of the data label
alignment: leftSide ? 'right' : 'left',
connectorPosition: {
breakAt: { // used in connectorShapes.fixedOffset
x: x + (point.labelDistance - 5) * sign,
y: y
},
touchingSliceAt: {
x: x + point.labelDistance * sign,
y: y
}
}
};
}
seriesTypes.pie.prototype.drawDataLabels.call(this);
}
});
/**
* A `funnel` series. If the [type](#series.funnel.type) option is
* not specified, it is inherited from [chart.type](#chart.type).
*
* @extends series,plotOptions.funnel
* @excluding dataParser, dataURL, stack, xAxis, yAxis
* @product highcharts
* @apioption series.funnel
*/
/**
* An array of data points for the series. For the `funnel` series type,
* points can be given in the following ways:
*
* 1. An array of numerical values. In this case, the numerical values
* will be interpreted as `y` options. Example:
*
* ```js
* data: [0, 5, 3, 5]
* ```
*
* 2. An array of objects with named values. The following snippet shows only a
* few settings, see the complete options set below. If the total number of data
* points exceeds the series' [turboThreshold](#series.funnel.turboThreshold),
* this option is not available.
*
* ```js
* data: [{
* y: 3,
* name: "Point2",
* color: "#00FF00"
* }, {
* y: 1,
* name: "Point1",
* color: "#FF00FF"
* }]
* ```
*
* @sample {highcharts} highcharts/chart/reflow-true/
* Numerical values
* @sample {highcharts} highcharts/series/data-array-of-arrays/
* Arrays of numeric x and y
* @sample {highcharts} highcharts/series/data-array-of-arrays-datetime/
* Arrays of datetime x and y
* @sample {highcharts} highcharts/series/data-array-of-name-value/
* Arrays of point.name and y
* @sample {highcharts} highcharts/series/data-array-of-objects/
* Config objects
*
* @type {Array<number|*>}
* @extends series.pie.data
* @excluding sliced
* @product highcharts
* @apioption series.funnel.data
*/
/**
* Pyramid series type.
*
* @private
* @class
* @name Highcharts.seriesTypes.pyramid
*
* @augments Highcharts.Series
*/
seriesType('pyramid', 'funnel',
/**
* A pyramid series is a special type of funnel, without neck and reversed by
* default. Requires the funnel module.
*
* @sample highcharts/demo/pyramid/
* Pyramid chart
*
* @extends plotOptions.funnel
* @product highcharts
* @optionparent plotOptions.pyramid
*/
{
/**
* The pyramid neck width is zero by default, as opposed to the funnel,
* which shares the same layout logic.
*
* @since 3.0.10
*/
neckWidth: '0%',
/**
* The pyramid neck width is zero by default, as opposed to the funnel,
* which shares the same layout logic.
*
* @since 3.0.10
*/
neckHeight: '0%',
/**
* The pyramid is reversed by default, as opposed to the funnel, which
* shares the layout engine, and is not reversed.
*
* @since 3.0.10
*/
reversed: true
});
/**
* A `pyramid` series. If the [type](#series.pyramid.type) option is
* not specified, it is inherited from [chart.type](#chart.type).
*
* @extends series,plotOptions.pyramid
* @excluding dataParser, dataURL, stack, xAxis, yAxis
* @product highcharts
* @apioption series.pyramid
*/
/**
* An array of data points for the series. For the `pyramid` series
* type, points can be given in the following ways:
*
* 1. An array of numerical values. In this case, the numerical values will be
* interpreted as `y` options. Example:
* ```js
* data: [0, 5, 3, 5]
* ```
*
* 2. An array of objects with named values. The following snippet shows only a
* few settings, see the complete options set below. If the total number of
* data points exceeds the series'
* [turboThreshold](#series.pyramid.turboThreshold), this option is not
* available.
* ```js
* data: [{
* y: 9,
* name: "Point2",
* color: "#00FF00"
* }, {
* y: 6,
* name: "Point1",
* color: "#FF00FF"
* }]
* ```
*
* @sample {highcharts} highcharts/chart/reflow-true/
* Numerical values
* @sample {highcharts} highcharts/series/data-array-of-objects/
* Config objects
*
* @type {Array<number|*>}
* @extends series.pie.data
* @excluding sliced
* @product highcharts
* @apioption series.pyramid.data
*/
@@ -0,0 +1,229 @@
/**
* @license @product.name@ JS v@product.version@ (@product.date@)
*
* (c) 2010-2017 Highsoft AS
* Author: Sebastian Domas
*
* License: www.highcharts.com/license
*/
'use strict';
import H from '../parts/Globals.js';
import derivedSeriesMixin from '../mixins/derived-series.js';
var objectEach = H.objectEach,
seriesType = H.seriesType,
correctFloat = H.correctFloat,
isNumber = H.isNumber,
arrayMax = H.arrayMax,
arrayMin = H.arrayMin,
merge = H.merge;
/* ***************************************************************************
*
* HISTOGRAM
*
**************************************************************************** */
/**
* A dictionary with formulas for calculating number of bins based on the
* base series
**/
var binsNumberFormulas = {
'square-root': function (baseSeries) {
return Math.round(Math.sqrt(baseSeries.options.data.length));
},
'sturges': function (baseSeries) {
return Math.ceil(Math.log(baseSeries.options.data.length) * Math.LOG2E);
},
'rice': function (baseSeries) {
return Math.ceil(2 * Math.pow(baseSeries.options.data.length, 1 / 3));
}
};
/**
* Returns a function for mapping number to the closed (right opened) bins
*
* @param {number} binWidth - width of the bin
* @returns {function}
**/
function fitToBinLeftClosed(bins) {
return function (y) {
var i = 1;
while (bins[i] <= y) {
i++;
}
return bins[--i];
};
}
/**
* Histogram class
*
* @constructor seriesTypes.histogram
* @augments seriesTypes.column
* @mixes DerivedSeriesMixin
**/
/**
* A histogram is a column series which represents the distribution of the data
* set in the base series. Histogram splits data into bins and shows their
* frequencies.
*
* @product highcharts
* @sample {highcharts} highcharts/demo/histogram/ Histogram
* @since 6.0.0
* @extends plotOptions.column
* @excluding boostThreshold, pointInterval, pointIntervalUnit, stacking
* @optionparent plotOptions.histogram
**/
seriesType('histogram', 'column', {
/**
* A preferable number of bins. It is a suggestion, so a histogram may have
* a different number of bins. By default it is set to the square root
* of the base series' data length. Available options are: `square-root`,
* `sturges`, `rice`. You can also define a function which takes a
* `baseSeries` as a parameter and should return a positive integer.
*
* @type {String|Number|Function}
* @validvalue ["square-root", "sturges", "rice"]
*/
binsNumber: 'square-root',
/**
* Width of each bin. By default the bin's width is calculated as
* `(max - min) / number of bins`. This option takes precedence over
* [binsNumber](#plotOptions.histogram.binsNumber).
*
* @type {Number}
*/
binWidth: undefined,
pointPadding: 0,
groupPadding: 0,
grouping: false,
pointPlacement: 'between',
tooltip: {
headerFormat: '',
pointFormat: '<span style="font-size: 10px">{point.x} - {point.x2}' +
'</span><br/>' +
'<span style="color:{point.color}">\u25CF</span>' +
' {series.name} <b>{point.y}</b><br/>'
}
}, merge(derivedSeriesMixin, {
setDerivedData: function () {
var data = this.derivedData(
this.baseSeries.yData,
this.binsNumber(),
this.options.binWidth
);
this.setData(data, false);
},
derivedData: function (baseData, binsNumber, binWidth) {
var max = arrayMax(baseData),
min = arrayMin(baseData),
frequencies = [],
bins = {},
data = [],
x,
fitToBin;
binWidth = this.binWidth = correctFloat(
isNumber(binWidth) ?
(binWidth || 1) :
(max - min) / binsNumber
);
// If binWidth is 0 then max and min are equaled,
// increment the x with some positive value to quit the loop
for (x = min; x < max; x = correctFloat(x + binWidth)) {
frequencies.push(x);
bins[x] = 0;
}
if (bins[min] !== 0) {
frequencies.push(correctFloat(min));
bins[correctFloat(min)] = 0;
}
fitToBin = fitToBinLeftClosed(
frequencies.map(function (elem) {
return parseFloat(elem);
})
);
baseData.forEach(function (y) {
var x = correctFloat(fitToBin(y));
bins[x]++;
});
objectEach(bins, function (frequency, x) {
data.push({
x: Number(x),
y: frequency,
x2: correctFloat(Number(x) + binWidth)
});
});
data.sort(function (a, b) {
return a.x - b.x;
});
return data;
},
binsNumber: function () {
var binsNumberOption = this.options.binsNumber;
var binsNumber = binsNumberFormulas[binsNumberOption] ||
// #7457
(typeof binsNumberOption === 'function' && binsNumberOption);
return Math.ceil(
(binsNumber && binsNumber(this.baseSeries)) ||
(
isNumber(binsNumberOption) ?
binsNumberOption :
binsNumberFormulas['square-root'](this.baseSeries)
)
);
}
}));
/**
* A `histogram` series. If the [type](#series.histogram.type) option is not
* specified, it is inherited from [chart.type](#chart.type).
*
* @type {Object}
* @since 6.0.0
* @extends series,plotOptions.histogram
* @excluding dataParser,dataURL,data
* @product highcharts
* @apioption series.histogram
*/
/**
* An integer identifying the index to use for the base series, or a string
* representing the id of the series.
*
* @type {Number|String}
* @default undefined
* @apioption series.histogram.baseSeries
*/
/**
* An array of data points for the series. For the `histogram` series type,
* points are calculated dynamically. See
* [histogram.baseSeries](#series.histogram.baseSeries).
*
* @type {Array<Object|Array>}
* @since 6.0.0
* @extends series.column.data
* @product highcharts
* @apioption series.histogram.data
*/
@@ -0,0 +1,150 @@
/* *
*
* (c) 2009-2019 Torstein Honsi
*
* Item series type for Highcharts
*
* License: www.highcharts.com/license
*
* */
/**
* @private
* @todo
* - Check update, remove etc.
* - Custom icons like persons, carts etc. Either as images, font icons or
* Highcharts symbols.
*/
'use strict';
import H from '../parts/Globals.js';
import '../parts/Utilities.js';
import '../parts/Series.js';
var extend = H.extend,
pick = H.pick,
seriesType = H.seriesType;
/**
* @private
* @class
* @name Highcharts.seriesTypes.item
*
* @augments Highcharts.Series
*/
seriesType('item', 'column', {
itemPadding: 0.2,
marker: {
symbol: 'circle',
states: {
hover: {},
select: {}
}
}
}, {
drawPoints: function () {
var series = this,
renderer = series.chart.renderer,
seriesMarkerOptions = this.options.marker,
itemPaddingTranslated = this.yAxis.transA *
series.options.itemPadding,
borderWidth = this.borderWidth,
crisp = borderWidth % 2 ? 0.5 : 1;
this.points.forEach(function (point) {
var yPos,
attr,
graphics,
itemY,
pointAttr,
pointMarkerOptions = point.marker || {},
symbol = (
pointMarkerOptions.symbol ||
seriesMarkerOptions.symbol
),
radius = pick(
pointMarkerOptions.radius,
seriesMarkerOptions.radius
),
size,
yTop,
isSquare = symbol !== 'rect',
x,
y;
point.graphics = graphics = point.graphics || {};
pointAttr = point.pointAttr ?
(
point.pointAttr[point.selected ? 'selected' : ''] ||
series.pointAttr['']
) :
series.pointAttribs(point, point.selected && 'select');
delete pointAttr.r;
if (series.chart.styledMode) {
delete pointAttr.stroke;
delete pointAttr['stroke-width'];
}
if (point.y !== null) {
if (!point.graphic) {
point.graphic = renderer.g('point').add(series.group);
}
itemY = point.y;
yTop = pick(point.stackY, point.y);
size = Math.min(
point.pointWidth,
series.yAxis.transA - itemPaddingTranslated
);
for (yPos = yTop; yPos > yTop - point.y; yPos--) {
x = point.barX + (
isSquare ?
point.pointWidth / 2 - size / 2 :
0
);
y = series.yAxis.toPixels(yPos, true) +
itemPaddingTranslated / 2;
if (series.options.crisp) {
x = Math.round(x) - crisp;
y = Math.round(y) + crisp;
}
attr = {
x: x,
y: y,
width: Math.round(isSquare ? size : point.pointWidth),
height: Math.round(size),
r: radius
};
if (graphics[itemY]) {
graphics[itemY].animate(attr);
} else {
graphics[itemY] = renderer.symbol(symbol)
.attr(extend(attr, pointAttr))
.add(point.graphic);
}
graphics[itemY].isActive = true;
itemY--;
}
}
H.objectEach(graphics, function (graphic, key) {
if (!graphic.isActive) {
graphic.destroy();
delete graphic[key];
} else {
graphic.isActive = false;
}
});
});
}
});
H.SVGRenderer.prototype.symbols.rect = function (x, y, w, h, options) {
return H.SVGRenderer.prototype.symbols.callout(x, y, w, h, options);
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,175 @@
/**
* Networkgraph series
*
* (c) 2010-2019 Paweł Fus
*
* License: www.highcharts.com/license
*/
'use strict';
import H from '../../parts/Globals.js';
var QuadTreeNode = H.QuadTreeNode = function (box) {
this.box = box;
this.nodes = []; // Array of 4 -> quad
this.children = []; // Deferred leafs
this.mass = 1;
this.centerX = 0;
this.centerY = 0;
};
H.extend(
QuadTreeNode.prototype,
{
insert: function (node) {
this.mass++;
if (!this.centerX) {
this.centerX = node.plotX;
this.centerY = node.plotY;
} else {
this.centerX = (this.centerX + node.plotX) / 2;
this.centerY = (this.centerY + node.plotY) / 2;
}
if (this.nodes.length) {
this.nodes[this.getBoxPosition(node)].insert(node);
} else {
if (this.children.length < 3) {
this.children.push(node);
} else {
this.divideBox();
this.children.forEach(function (child) {
this.insert(child);
}, this);
this.insert(node);
}
}
},
divideBox: function () {
var halfWidth = this.box.width / 2,
halfHeight = this.box.height / 2;
this.nodes[0] = new QuadTreeNode({
left: this.box.left,
top: this.box.top,
width: halfWidth,
height: halfHeight
});
this.nodes[1] = new QuadTreeNode({
left: this.box.left + halfWidth,
top: this.box.top,
width: halfWidth,
height: halfHeight
});
this.nodes[2] = new QuadTreeNode({
left: this.box.left + halfWidth,
top: this.box.top + halfHeight,
width: halfWidth,
height: halfHeight
});
this.nodes[3] = new QuadTreeNode({
left: this.box.left,
top: this.box.top + halfHeight,
width: halfWidth,
height: halfHeight
});
},
getBoxPosition: function (node) {
var left = node.plotX < this.box.left + this.box.width / 2,
top = node.plotY < this.box.top + this.box.height / 2,
index;
if (left) {
if (top) {
// Top left
index = 0;
} else {
// Bottom left
index = 3;
}
} else {
if (top) {
// Top right
index = 1;
} else {
// Bottom right
index = 2;
}
}
return index;
}
}
);
var QuadTree = H.QuadTree = function (x, y, width, height) {
// Boundary rectangle:
this.rect = {
left: x,
top: y,
width: width,
height: height
};
this.root = new QuadTreeNode(this.rect);
};
H.extend(
QuadTree.prototype,
{
insertNodes: function (nodes) {
nodes.forEach(function (node) {
this.root.insert(node);
}, this);
},
clear: function (chart) {
this.render(chart, true);
},
visitNodeRecursive: function (node, chart, clear) {
node.nodes.forEach(
function (qtNode) {
if (qtNode.children.length) {
this.renderBox(qtNode, chart, clear);
this.visitNodeRecursive(qtNode, chart, clear);
}
},
this
);
},
render: function (chart, clear) {
this.visitNodeRecursive(this.root, chart, clear);
},
renderBox: function (qtNode, chart, clear) {
if (!qtNode.graphic) {
qtNode.graphic = chart.renderer
.rect(
qtNode.box.left + chart.plotLeft,
qtNode.box.top + chart.plotTop,
qtNode.box.width,
qtNode.box.height
)
.attr({
stroke: 'red',
'stroke-width': 2
})
.add();
} else if (clear) {
qtNode.graphic = qtNode.graphic.destroy();
}
if (qtNode.graphic) {
qtNode.graphic.animate({
x: qtNode.box.left + chart.plotLeft,
y: qtNode.box.top + chart.plotTop,
width: qtNode.box.width,
height: qtNode.box.height
});
}
}
}
);
@@ -0,0 +1,506 @@
/**
* Networkgraph series
*
* (c) 2010-2019 Paweł Fus
*
* License: www.highcharts.com/license
*/
'use strict';
import H from '../../parts/Globals.js';
var pick = H.pick;
H.layouts = {
'reingold-fruchterman': function (options) {
this.options = options;
this.nodes = [];
this.links = [];
this.series = [];
this.box = {
x: 0,
y: 0,
width: 0,
height: 0
};
this.setInitialRendering(true);
}
};
H.extend(
/**
* Reingold-Fruchterman algorithm from
* "Graph Drawing by Force-directed Placement" paper.
*/
H.layouts['reingold-fruchterman'].prototype,
{
run: function () {
var layout = this,
series = this.series,
options = this.options;
if (layout.initialRendering) {
layout.initPositions();
// Render elements in initial positions:
series.forEach(function (s) {
s.render();
});
}
// Algorithm:
function localLayout() {
// Barycenter forces:
layout.applyBarycenterForces();
// Repulsive forces:
layout.applyRepulsiveForces();
// Attractive forces:
layout.applyAttractiveForces();
// Limit to the plotting area and cool down:
layout.applyLimits(layout.temperature);
// Cool down:
layout.temperature -= layout.diffTemperature;
layout.prevSystemTemperature = layout.systemTemperature;
layout.systemTemperature = layout.getSystemTemperature();
if (options.enableSimulation) {
series.forEach(function (s) {
s.render();
});
if (
layout.maxIterations-- &&
!layout.isStable()
) {
layout.simulation = H.win.requestAnimationFrame(
localLayout
);
} else {
layout.simulation = false;
}
}
}
layout.setK();
layout.resetSimulation(options);
if (options.enableSimulation) {
// Animate it:
layout.simulation = H.win.requestAnimationFrame(localLayout);
} else {
// Synchronous rendering:
while (
layout.maxIterations-- &&
!layout.isStable()
) {
localLayout();
}
series.forEach(function (s) {
s.render();
});
}
},
stop: function () {
if (this.simulation) {
H.win.cancelAnimationFrame(this.simulation);
}
},
setArea: function (x, y, w, h) {
this.box = {
left: x,
top: y,
width: w,
height: h
};
},
setK: function () {
// Optimal distance between nodes,
// available space around the node:
this.k = this.options.linkLength ||
Math.pow(
this.box.width * this.box.height / this.nodes.length,
0.4
);
},
addNodes: function (nodes) {
nodes.forEach(function (node) {
if (this.nodes.indexOf(node) === -1) {
this.nodes.push(node);
}
}, this);
},
removeNode: function (node) {
var index = this.nodes.indexOf(node);
if (index !== -1) {
this.nodes.splice(index, 1);
}
},
removeLink: function (link) {
var index = this.links.indexOf(link);
if (index !== -1) {
this.links.splice(index, 1);
}
},
addLinks: function (links) {
links.forEach(function (link) {
if (this.links.indexOf(link) === -1) {
this.links.push(link);
}
}, this);
},
addSeries: function (series) {
if (this.series.indexOf(series) === -1) {
this.series.push(series);
}
},
clear: function () {
this.nodes.length = 0;
this.links.length = 0;
this.series.length = 0;
this.resetSimulation();
},
resetSimulation: function () {
this.forcedStop = false;
this.systemTemperature = 0;
this.setMaxIterations();
this.setTemperature();
this.setDiffTemperature();
},
setMaxIterations: function (maxIterations) {
this.maxIterations = pick(
maxIterations,
this.options.maxIterations
);
},
setTemperature: function () {
this.temperature = Math.sqrt(this.nodes.length);
},
setDiffTemperature: function () {
this.diffTemperature = this.temperature /
(this.options.maxIterations + 1);
},
setInitialRendering: function (enable) {
this.initialRendering = enable;
},
initPositions: function () {
var initialPositions = this.options.initialPositions;
if (H.isFunction(initialPositions)) {
initialPositions.call(this);
} else if (initialPositions === 'circle') {
this.setCircularPositions();
} else {
this.setRandomPositions();
}
},
setCircularPositions: function () {
var box = this.box,
nodes = this.nodes,
nodesLength = nodes.length + 1,
angle = 2 * Math.PI / nodesLength,
rootNodes = nodes.filter(function (node) {
return node.linksTo.length === 0;
}),
sortedNodes = [],
visitedNodes = {};
function addToNodes(node) {
node.linksFrom.forEach(function (link) {
if (!visitedNodes[link.toNode.id]) {
visitedNodes[link.toNode.id] = true;
sortedNodes.push(link.toNode);
addToNodes(link.toNode);
}
});
}
// Start with identified root nodes an sort the nodes by their
// hierarchy. In trees, this ensures that branches don't cross
// eachother.
rootNodes.forEach(function (rootNode) {
sortedNodes.push(rootNode);
addToNodes(rootNode);
});
// Cyclic tree, no root node found
if (!sortedNodes.length) {
sortedNodes = nodes;
// Dangling, cyclic trees
} else {
nodes.forEach(function (node) {
if (sortedNodes.indexOf(node) === -1) {
sortedNodes.push(node);
}
});
}
// Initial positions are laid out along a small circle, appearing
// as a cluster in the middle
sortedNodes.forEach(function (node, index) {
node.plotX = pick(
node.plotX,
box.width / 2 + Math.cos(index * angle)
);
node.plotY = pick(
node.plotY,
box.height / 2 + Math.sin(index * angle)
);
node.dispX = 0;
node.dispY = 0;
});
},
setRandomPositions: function () {
var box = this.box,
nodes = this.nodes,
nodesLength = nodes.length + 1;
// Return a repeatable, quasi-random number based on an integer
// input. For the initial positions
function unrandom(n) {
var rand = n * n / Math.PI;
rand = rand - Math.floor(rand);
return rand;
}
// Initial positions:
nodes.forEach(
function (node, index) {
node.plotX = pick(
node.plotX,
box.width * unrandom(index)
);
node.plotY = pick(
node.plotY,
box.height * unrandom(nodesLength + index)
);
node.dispX = 0;
node.dispY = 0;
}
);
},
applyBarycenterForces: function () {
var nodesLength = this.nodes.length,
gravitationalConstant = this.options.gravitationalConstant,
cx = 0,
cy = 0;
// Calculate center:
this.nodes.forEach(function (node) {
cx += node.plotX;
cy += node.plotY;
});
this.barycenter = {
x: cx,
y: cy
};
// Apply forces:
this.nodes.forEach(function (node) {
var degree = node.getDegree(),
phi = degree * (1 + degree / 2);
node.dispX = (cx / nodesLength - node.plotX) *
gravitationalConstant * phi;
node.dispY = (cy / nodesLength - node.plotY) *
gravitationalConstant * phi;
});
},
applyRepulsiveForces: function () {
var layout = this,
nodes = layout.nodes,
options = layout.options,
k = this.k;
nodes.forEach(function (node) {
nodes.forEach(function (repNode) {
var force,
distanceR,
distanceXY;
if (
// Node can not repulse itself:
node !== repNode &&
// Only close nodes affect each other:
/* layout.getDistR(node, repNode) < 2 * k && */
// Not dragged:
!node.fixedPosition
) {
distanceXY = layout.getDistXY(node, repNode);
distanceR = layout.vectorLength(distanceXY);
if (distanceR !== 0) {
force = options.repulsiveForce.call(
layout, distanceR, k
);
node.dispX += (distanceXY.x / distanceR) * force;
node.dispY += (distanceXY.y / distanceR) * force;
}
}
});
});
},
applyAttractiveForces: function () {
var layout = this,
links = layout.links,
options = this.options,
k = this.k;
links.forEach(function (link) {
if (link.fromNode && link.toNode) {
var distanceXY = layout.getDistXY(
link.fromNode,
link.toNode
),
distanceR = layout.vectorLength(distanceXY),
force = options.attractiveForce.call(
layout, distanceR, k
);
if (distanceR !== 0) {
if (!link.fromNode.fixedPosition) {
link.fromNode.dispX -= (distanceXY.x / distanceR) *
force;
link.fromNode.dispY -= (distanceXY.y / distanceR) *
force;
}
if (!link.toNode.fixedPosition) {
link.toNode.dispX += (distanceXY.x / distanceR) *
force;
link.toNode.dispY += (distanceXY.y / distanceR) *
force;
}
}
}
});
},
applyLimits: function (temperature) {
var layout = this,
options = layout.options,
nodes = layout.nodes,
box = layout.box,
distanceR;
nodes.forEach(function (node) {
if (node.fixedPosition) {
return;
}
// Friction:
node.dispX += options.friction * node.dispX;
node.dispY += options.friction * node.dispY;
distanceR = node.temperature = layout.vectorLength({
x: node.dispX,
y: node.dispY
});
// Place nodes:
if (distanceR !== 0) {
node.plotX += node.dispX / distanceR *
Math.min(Math.abs(node.dispX), temperature);
node.plotY += node.dispY / distanceR *
Math.min(Math.abs(node.dispY), temperature);
}
/*
TO DO: Consider elastic collision instead of stopping.
o' means end position when hitting plotting area edge:
- "inealstic":
o
\
______
| o'
| \
| \
- "elastic"/"bounced":
o
\
______
| ^
| / \
|o' \
*/
// Limit X-coordinates:
node.plotX = Math.round(
Math.max(
Math.min(
node.plotX,
box.width
),
box.left
)
);
// Limit Y-coordinates:
node.plotY = Math.round(
Math.max(
Math.min(
node.plotY,
box.height
),
box.top
)
);
// Reset displacement:
node.dispX = 0;
node.dispY = 0;
});
},
isStable: function () {
return Math.abs(
this.systemTemperature -
this.prevSystemTemperature
) === 0;
},
getSystemTemperature: function () {
return this.nodes.reduce(function (value, node) {
return value + node.temperature;
}, 0);
},
vectorLength: function (vector) {
return Math.sqrt(vector.x * vector.x + vector.y * vector.y);
},
getDistR: function (nodeA, nodeB) {
var distance = this.getDistXY(nodeA, nodeB);
return Math.sqrt(
distance.x * distance.x +
distance.y * distance.y
);
},
getDistXY: function (nodeA, nodeB) {
var xDist = nodeA.plotX - nodeB.plotX,
yDist = nodeA.plotY - nodeB.plotY;
return {
x: xDist,
y: yDist,
absX: Math.abs(xDist),
absY: Math.abs(yDist)
};
}
}
);
@@ -0,0 +1,740 @@
/**
* Networkgraph series
*
* (c) 2010-2019 Paweł Fus
*
* License: www.highcharts.com/license
*/
'use strict';
import H from '../../parts/Globals.js';
import '../../parts/Utilities.js';
import '../../parts/Options.js';
import '../../mixins/nodes.js';
import '/layouts.js';
var addEvent = H.addEvent,
defined = H.defined,
seriesType = H.seriesType,
seriesTypes = H.seriesTypes,
pick = H.pick,
Chart = H.Chart,
Point = H.Point,
Series = H.Series;
/**
* A networkgraph is a type of relationship chart, where connnections
* (links) attracts nodes (points) and other nodes repulse each other.
*
* @extends plotOptions.line
* @product highcharts
* @sample highcharts/demo/network-graph/
* Networkgraph
* @since 7.0.0
* @excluding boostThreshold, animation, animationLimit, connectEnds,
* connectNulls, dragDrop, getExtremesFromAll, label, linecap,
* negativeColor, pointInterval, pointIntervalUnit,
* pointPlacement, pointStart, softThreshold, stack, stacking,
* step, threshold, xAxis, yAxis, zoneAxis
* @optionparent plotOptions.networkgraph
*/
seriesType('networkgraph', 'line', {
marker: {
enabled: true
},
dataLabels: {
format: '{key}'
},
/**
* Link style options
*/
link: {
/**
* A name for the dash style to use for links.
*
* @type {String}
* @apioption plotOptions.networkgraph.link.dashStyle
* @defaults undefined
*/
/**
* Color of the link between two nodes.
*/
color: 'rgba(100, 100, 100, 0.5)',
/**
* Width (px) of the link between two nodes.
*/
width: 1
},
/**
* Flag to determine if nodes are draggable or not.
*/
draggable: true,
layoutAlgorithm: {
/**
* Ideal length (px) of the link between two nodes. When not defined,
* length is calculated as:
* `Math.pow(availableWidth * availableHeight / nodesLength, 0.4);`
*
* Note: Because of the algorithm specification, length of each link
* might be not exactly as specified.
*
* @type {number}
* @apioption series.networkgraph.layoutAlgorithm.linkLength
* @sample highcharts/series-networkgraph/styled-links/
* Numerical values
* @defaults undefined
*/
/**
* Initial layout algorithm for positioning nodes. Can be one of
* built-in options ("circle", "random") or a function where positions
* should be set on each node (`this.nodes`) as `node.plotX` and
* `node.plotY`
*
* @sample highcharts/series-networkgraph/initial-positions/
* Initial positions with callback
* @type {String|Function}
* @validvalue ["circle", "random"]
*/
initialPositions: 'circle',
/**
* Experimental. Enables live simulation of the algorithm
* implementation. All nodes are animated as the forces applies on
* them.
*
* @sample highcharts/demo/network-graph/
* Live simulation enabled
*/
enableSimulation: false,
/**
* Type of the algorithm used when positioning nodes.
*
* @validvalue ["reingold-fruchterman"]
*/
type: 'reingold-fruchterman',
/**
* Max number of iterations before algorithm will stop. In general,
* algorithm should find positions sooner, but when rendering huge
* number of nodes, it is recommended to increase this value as
* finding perfect graph positions can require more time.
*/
maxIterations: 1000,
/**
* Gravitational const used in the barycenter force of the algorithm.
*
* @sample highcharts/series-networkgraph/forces/
* Custom forces
*/
gravitationalConstant: 0.0625,
/**
* Friction applied on forces to prevent nodes rushing to fast to the
* desired positions.
*/
friction: -0.981,
/**
* Repulsive force applied on a node. Passed are two arguments:
* - `d` - which is current distance between two nodes
* - `k` - which is desired distance between two nodes
*
* @sample highcharts/series-networkgraph/forces/
* Custom forces
* @type {Function}
* @default function (d, k) { return k * k / d; }
*/
repulsiveForce: function (d, k) {
/*
basic, not recommended:
return k / d;
*/
/*
standard:
return k * k / d;
*/
/*
grid-variant:
return k * k / d * (2 * k - d > 0 ? 1 : 0);
*/
return k * k / d;
},
/**
* Attraction force applied on a node which is conected to another node
* by a link. Passed are two arguments:
* - `d` - which is current distance between two nodes
* - `k` - which is desired distance between two nodes
*
* @sample highcharts/series-networkgraph/forces/
* Custom forces
* @type {Function}
* @default function (d, k) { return k * k / d; }
*/
attractiveForce: function (d, k) {
/*
basic, not recommended:
return d / k;
*/
return d * d / k;
}
},
showInLegend: false
}, {
isNetworkgraph: true,
drawGraph: null,
isCartesian: false,
requireSorting: false,
directTouch: true,
noSharedTooltip: true,
trackerGroups: ['group', 'markerGroup', 'dataLabelsGroup'],
drawTracker: H.TrackerMixin.drawTrackerPoint,
// Animation is run in `series.simulation`.
animate: null,
/**
* Create a single node that holds information on incoming and outgoing
* links.
*/
createNode: H.NodesMixin.createNode,
/**
* Extend generatePoints by adding the nodes, which are Point objects
* but pushed to the this.nodes array.
*/
generatePoints: function () {
var nodeLookup = {},
chart = this.chart;
H.Series.prototype.generatePoints.call(this);
if (!this.nodes) {
this.nodes = []; // List of Point-like node items
}
this.colorCounter = 0;
// Reset links from previous run
this.nodes.forEach(function (node) {
node.linksFrom.length = 0;
node.linksTo.length = 0;
});
// Create the node list and set up links
this.points.forEach(function (point) {
if (defined(point.from)) {
if (!nodeLookup[point.from]) {
nodeLookup[point.from] = this.createNode(point.from);
}
nodeLookup[point.from].linksFrom.push(point);
point.fromNode = nodeLookup[point.from];
// Point color defaults to the fromNode's color
if (chart.styledMode) {
point.colorIndex = pick(
point.options.colorIndex,
nodeLookup[point.from].colorIndex
);
} else {
point.color =
point.options.color || nodeLookup[point.from].color;
}
}
if (defined(point.to)) {
if (!nodeLookup[point.to]) {
nodeLookup[point.to] = this.createNode(point.to);
}
nodeLookup[point.to].linksTo.push(point);
point.toNode = nodeLookup[point.to];
}
point.name = point.name || point.id; // for use in formats
}, this);
if (this.options.nodes) {
this.options.nodes.forEach(
function (nodeOptions) {
if (!nodeLookup[nodeOptions.id]) {
nodeLookup[nodeOptions.id] = this
.createNode(nodeOptions.id);
}
},
this
);
}
},
/**
* Run pre-translation by generating the nodeColumns.
*/
translate: function () {
if (!this.processedXData) {
this.processData();
}
this.generatePoints();
this.deferLayout();
this.nodes.forEach(function (node) {
// Draw the links from this node
node.isInside = true;
node.linksFrom.forEach(function (point) {
point.shapeType = 'path';
// Pass test in drawPoints
point.y = 1;
});
});
},
deferLayout: function () {
var layoutOptions = this.options.layoutAlgorithm,
graphLayoutsStorage = this.chart.graphLayoutsStorage,
chartOptions = this.chart.options.chart,
layout;
if (!this.visible) {
return;
}
if (!graphLayoutsStorage) {
this.chart.graphLayoutsStorage = graphLayoutsStorage = {};
}
layout = graphLayoutsStorage[layoutOptions.type];
if (!layout) {
layoutOptions.enableSimulation = !defined(chartOptions.forExport) ?
layoutOptions.enableSimulation :
!chartOptions.forExport;
graphLayoutsStorage[layoutOptions.type] = layout =
new H.layouts[layoutOptions.type](layoutOptions);
}
this.layout = layout;
layout.setArea(0, 0, this.chart.plotWidth, this.chart.plotHeight);
layout.addSeries(this);
layout.addNodes(this.nodes);
layout.addLinks(this.points);
},
/**
* Extend the render function to also render this.nodes together with
* the points.
*/
render: function () {
var points = this.points,
hoverPoint = this.chart.hoverPoint,
dataLabels = [];
// Render markers:
this.points = this.nodes;
seriesTypes.line.prototype.render.call(this);
this.points = points;
points.forEach(function (point) {
if (point.fromNode && point.toNode) {
point.renderLink();
point.redrawLink();
}
});
if (hoverPoint && hoverPoint.series === this) {
this.redrawHalo(hoverPoint);
}
this.nodes.forEach(function (node) {
if (node.dataLabel) {
dataLabels.push(node.dataLabel);
}
});
H.Chart.prototype.hideOverlappingLabels(dataLabels);
},
/*
* Draggable mode:
*/
redrawHalo: function (point) {
if (point && this.halo) {
this.halo.attr({
d: point.haloPath(
this.options.states.hover.halo.size
)
});
}
},
onMouseDown: function (point, event) {
var normalizedEvent = this.chart.pointer.normalize(event);
point.fixedPosition = {
chartX: normalizedEvent.chartX,
chartY: normalizedEvent.chartY,
plotX: point.plotX,
plotY: point.plotY
};
},
onMouseMove: function (point, event) {
if (point.fixedPosition) {
var series = this,
chart = series.chart,
normalizedEvent = chart.pointer.normalize(event),
diffX = point.fixedPosition.chartX - normalizedEvent.chartX,
diffY = point.fixedPosition.chartY - normalizedEvent.chartY,
newPlotX,
newPlotY;
// At least 5px to apply change (avoids simple click):
if (Math.abs(diffX) > 5 || Math.abs(diffY) > 5) {
newPlotX = point.fixedPosition.plotX - diffX;
newPlotY = point.fixedPosition.plotY - diffY;
if (chart.isInsidePlot(newPlotX, newPlotY)) {
point.plotX = newPlotX;
point.plotY = newPlotY;
series.redrawHalo();
if (!series.layout.simulation) {
// Start new simulation:
if (!series.layout.enableSimulation) {
// Run only one iteration to speed things up:
series.layout.setMaxIterations(1);
}
// When dragging nodes, we don't need to calculate
// initial positions and rendering nodes:
series.layout.setInitialRendering(false);
series.layout.run();
// Restore defaults:
series.layout.setInitialRendering(true);
} else {
// Extend current simulation:
series.layout.resetSimulation();
}
}
}
}
},
onMouseUp: function (point) {
if (point.fixedPosition) {
this.layout.run();
delete point.fixedPosition;
}
},
destroy: function () {
this.nodes.forEach(function (node) {
node.destroy();
});
return Series.prototype.destroy.apply(this, arguments);
}
}, {
getDegree: function () {
var deg = this.isNode ? this.linksFrom.length + this.linksTo.length : 0;
return deg === 0 ? 1 : deg;
},
// Links:
getLinkAttribues: function () {
var linkOptions = this.series.options.link,
pointOptions = this.options;
return {
'stroke-width': pick(pointOptions.width, linkOptions.width),
stroke: pointOptions.color || linkOptions.color,
dashstyle: pointOptions.dashStyle || linkOptions.dashStyle
};
},
renderLink: function () {
if (!this.graphic) {
this.graphic = this.series.chart.renderer
.path(
this.getLinkPath(this.fromNode, this.toNode)
)
.attr(this.getLinkAttribues())
.add(this.series.group);
}
},
redrawLink: function () {
if (this.graphic) {
this.graphic.animate({
d: this.getLinkPath(this.fromNode, this.toNode)
});
}
},
getLinkPath: function (from, to) {
return [
'M',
from.plotX,
from.plotY,
'L',
to.plotX,
to.plotY
];
/*
IDEA: different link shapes?
return [
'M',
from.plotX,
from.plotY,
'Q',
(to.plotX + from.plotX) / 2,
(to.plotY + from.plotY) / 2 + 15,
to.plotX,
to.plotY
];*/
},
// Default utils:
destroy: function () {
if (this.isNode) {
this.linksFrom.forEach(
function (linkFrom) {
if (linkFrom.graphic) {
linkFrom.graphic = linkFrom.graphic.destroy();
}
}
);
}
return Point.prototype.destroy.apply(this, arguments);
}
});
addEvent(seriesTypes.networkgraph, 'updatedData', function () {
if (this.layout) {
this.layout.stop();
}
});
addEvent(seriesTypes.networkgraph.prototype.pointClass, 'remove', function () {
if (this.series.layout) {
if (this.isNode) {
this.series.layout.removeNode(this);
} else {
this.series.layout.removeLink(this);
}
}
});
/*
* Multiple series support:
*/
// Clear previous layouts
addEvent(Chart, 'predraw', function () {
if (this.graphLayoutsStorage) {
H.objectEach(
this.graphLayoutsStorage,
function (layout) {
layout.stop();
}
);
}
});
addEvent(Chart, 'render', function () {
if (this.graphLayoutsStorage) {
H.setAnimation(false, this);
H.objectEach(
this.graphLayoutsStorage,
function (layout) {
layout.run();
}
);
}
});
/*
* Draggable mode:
*/
addEvent(
seriesTypes.networkgraph.prototype.pointClass,
'mouseOver',
function () {
H.css(this.series.chart.container, { cursor: 'move' });
}
);
addEvent(
seriesTypes.networkgraph.prototype.pointClass,
'mouseOut',
function () {
H.css(this.series.chart.container, { cursor: 'default' });
}
);
addEvent(
Chart,
'load',
function () {
var chart = this,
unbinders = [];
if (chart.container) {
unbinders.push(
addEvent(
chart.container,
'mousedown',
function (event) {
var point = chart.hoverPoint;
if (
point &&
point.series &&
point.series.isNetworkgraph &&
point.series.options.draggable
) {
point.series.onMouseDown(point, event);
unbinders.push(addEvent(
chart.container,
'mousemove',
function (e) {
return point.series.onMouseMove(point, e);
}
));
unbinders.push(addEvent(
chart.container.ownerDocument,
'mouseup',
function (e) {
return point.series.onMouseUp(point, e);
}
));
}
}
)
);
}
addEvent(chart, 'destroy', function () {
unbinders.forEach(function (unbind) {
unbind();
});
});
}
);
/**
* A `networkgraph` series. If the [type](#series.networkgraph.type) option is
* not specified, it is inherited from [chart.type](#chart.type).
*
* @type {Object}
* @extends series,plotOptions.networkgraph
* @excluding boostThreshold, animation, animationLimit, connectEnds,
* connectNulls, dragDrop, getExtremesFromAll, label, linecap,
* negativeColor, pointInterval, pointIntervalUnit,
* pointPlacement, pointStart, softThreshold, stack, stacking,
* step, threshold, xAxis, yAxis, zoneAxis
* @product highcharts
* @apioption series.networkgraph
*/
/**
* An array of data points for the series. For the `networkgraph` series type,
* points can be given in the following way:
*
* An array of objects with named values. The following snippet shows only a
* few settings, see the complete options set below. If the total number of
* data points exceeds the series'
* [turboThreshold](#series.area.turboThreshold), this option is not available.
*
* ```js
* data: [{
* from: 'Category1',
* to: 'Category2'
* }, {
* from: 'Category1',
* to: 'Category3'
* }]
* ```
*
* @type {Array<Object|Array|Number>}
* @extends series.line.data
* @excluding drilldown,marker,x,y,draDrop
* @sample {highcharts} highcharts/chart/reflow-true/
* Numerical values
* @sample {highcharts} highcharts/series/data-array-of-arrays/
* Arrays of numeric x and y
* @sample {highcharts} highcharts/series/data-array-of-arrays-datetime/
* Arrays of datetime x and y
* @sample {highcharts} highcharts/series/data-array-of-name-value/
* Arrays of point.name and y
* @sample {highcharts} highcharts/series/data-array-of-objects/
* Config objects
* @product highcharts
* @apioption series.networkgraph.data
*/
/**
* The node that the link runs from.
*
* @type {String}
* @product highcharts
* @apioption series.networkgraph.data.from
*/
/**
* The node that the link runs to.
*
* @type {String}
* @product highcharts
* @apioption series.networkgraph.data.to
*/
/**
* The weight of the link.
*
* @type {Number}
* @product highcharts
* @apioption series.networkgraph.data.weight
*/
/**
* A collection of options for the individual nodes. The nodes in a
* networkgraph diagram are auto-generated instances of `Highcharts.Point`,
* but options can be applied here and linked by the `id`.
*
* @sample highcharts/series-networkgraph/data-options/
* Networkgraph diagram with node options
*
* @type {Array<*>}
* @product highcharts
* @apioption series.networkgraph.nodes
*/
/**
* The id of the auto-generated node, refering to the `from` or `to` setting of
* the link.
*
* @type {string}
* @product highcharts
* @apioption series.networkgraph.nodes.id
*/
/**
* The color of the auto generated node.
*
* @type {Highcharts.ColorString}
* @product highcharts
* @apioption series.networkgraph.nodes.color
*/
/**
* The color index of the auto generated node, especially for use in styled
* mode.
*
* @type {number}
* @product highcharts
* @apioption series.networkgraph.nodes.colorIndex
*/
/**
* The name to display for the node in data labels and tooltips. Use this when
* the name is different from the `id`. Where the id must be unique for each
* node, this is not necessary for the name.
*
* @sample highcharts/series-networkgraph/data-options/
* Networkgraph diagram with node options
*
* @type {string}
* @product highcharts
* @apioption series.networkgraph.nodes.name
*/
@@ -0,0 +1,260 @@
/* *
*
* Plugin for displaying a message when there is no data visible in chart.
*
* (c) 2010-2019 Highsoft AS
*
* Author: Oystein Moseng
*
* License: www.highcharts.com/license
*
* */
'use strict';
import H from '../parts/Globals.js';
import '../parts/Utilities.js';
import '../parts/Series.js';
import '../parts/Options.js';
var seriesTypes = H.seriesTypes,
chartPrototype = H.Chart.prototype,
defaultOptions = H.getOptions(),
extend = H.extend;
// Add language option
extend(
defaultOptions.lang,
/**
* @optionparent lang
*/
{
/**
* The text to display when the chart contains no data. Requires the
* no-data module, see [noData](#noData).
*
* @sample highcharts/no-data-to-display/no-data-line
* No-data text
*
* @since 3.0.8
* @product highcharts highstock
*/
noData: 'No data to display'
}
);
// Add default display options for message
/**
* Options for displaying a message like "No data to display".
* This feature requires the file no-data-to-display.js to be loaded in the
* page. The actual text to display is set in the lang.noData option.
*
* @sample highcharts/no-data-to-display/no-data-line
* Line chart with no-data module
* @sample highcharts/no-data-to-display/no-data-pie
* Pie chart with no-data module
*
* @product highcharts highstock gantt
* @optionparent noData
*/
defaultOptions.noData = {
/**
* An object of additional SVG attributes for the no-data label.
*
* @type {Highcharts.SVGAttributes}
* @since 3.0.8
* @product highcharts highstock gantt
* @apioption noData.attr
*/
/**
* Whether to insert the label as HTML, or as pseudo-HTML rendered with
* SVG.
*
* @type {boolean}
* @default false
* @since 4.1.10
* @product highcharts highstock gantt
* @apioption noData.useHTML
*/
/**
* The position of the no-data label, relative to the plot area.
*
* @type {Highcharts.AlignObject}
* @since 3.0.8
*/
position: {
/**
* Horizontal offset of the label, in pixels.
*/
x: 0,
/**
* Vertical offset of the label, in pixels.
*/
y: 0,
/**
* Horizontal alignment of the label.
*
* @type {Highcharts.AlignType}
*/
align: 'center',
/**
* Vertical alignment of the label.
*
* @type {Highcharts.VerticalAlignType}
*/
verticalAlign: 'middle'
},
/**
* CSS styles for the no-data label.
*
* @sample highcharts/no-data-to-display/no-data-line
* Styled no-data text
*
* @type {Highcharts.CSSObject}
*/
style: {
/** @ignore */
fontWeight: 'bold',
/** @ignore */
fontSize: '12px',
/** @ignore */
color: '#666666'
}
};
// Define hasData function for non-cartesian seris. Returns true if the series
// has points at all.
[
'bubble',
'gauge',
'heatmap',
'networkgraph',
'pie',
'sankey',
'treemap',
'waterfall'
].forEach(function (type) {
if (seriesTypes[type]) {
seriesTypes[type].prototype.hasData = function () {
return !!this.points.length; // != 0
};
}
});
/**
* Define hasData functions for series. These return true if there are data
* points on this series within the plot area.
*
* @private
* @function Highcharts.Series#hasData
*
* @return {boolean}
*/
H.Series.prototype.hasData = function () {
return (
(
this.visible &&
this.dataMax !== undefined &&
this.dataMin !== undefined
) || // #3703
(this.visible && this.yData && this.yData.length > 0) // #9758
);
};
/**
* Display a no-data message.
*
* @private
* @function Highcharts.Chart#showNoData
*
* @param {string} str
* An optional message to show in place of the default one
*/
chartPrototype.showNoData = function (str) {
var chart = this,
options = chart.options,
text = str || (options && options.lang.noData),
noDataOptions = options && options.noData;
if (!chart.noDataLabel && chart.renderer) {
chart.noDataLabel = chart.renderer
.label(
text,
0,
0,
null,
null,
null,
noDataOptions.useHTML,
null,
'no-data'
);
if (!chart.styledMode) {
chart.noDataLabel
.attr(noDataOptions.attr)
.css(noDataOptions.style);
}
chart.noDataLabel.add();
chart.noDataLabel.align(
extend(chart.noDataLabel.getBBox(), noDataOptions.position),
false,
'plotBox'
);
}
};
/**
* Hide no-data message.
*
* @private
* @function Highcharts.Chart#hideNoData
*/
chartPrototype.hideNoData = function () {
var chart = this;
if (chart.noDataLabel) {
chart.noDataLabel = chart.noDataLabel.destroy();
}
};
/**
* Returns true if there are data points within the plot area now.
*
* @private
* @function Highcharts.Chart#hasData
*/
chartPrototype.hasData = function () {
var chart = this,
series = chart.series || [],
i = series.length;
while (i--) {
if (series[i].hasData() && !series[i].options.isInternal) {
return true;
}
}
return chart.loadingShown; // #4588
};
// Add event listener to handle automatic show or hide no-data message.
H.addEvent(H.Chart, 'render', function handleNoData() {
if (this.hasData()) {
this.hideNoData();
} else {
this.showNoData();
}
});
@@ -0,0 +1,710 @@
/**
* Client side exporting module
*
* (c) 2015 Torstein Honsi / Oystein Moseng
*
* License: www.highcharts.com/license
*/
'use strict';
/* global MSBlobBuilder */
import Highcharts from '../parts/Globals.js';
import '../parts/Chart.js';
import '../parts/Options.js';
import '../mixins/download-url.js';
var addEvent = Highcharts.addEvent,
merge = Highcharts.merge,
win = Highcharts.win,
nav = win.navigator,
doc = win.document,
domurl = win.URL || win.webkitURL || win,
isMSBrowser = /Edge\/|Trident\/|MSIE /.test(nav.userAgent),
// Milliseconds to defer image load event handlers to offset IE bug
loadEventDeferDelay = isMSBrowser ? 150 : 0;
// Dummy object so we can reuse our canvas-tools.js without errors
Highcharts.CanVGRenderer = {};
/**
* Downloads a script and executes a callback when done.
*
* @private
* @function getScript
*
* @param {string} scriptLocation
*
* @param {Function} callback
*/
function getScript(scriptLocation, callback) {
var head = doc.getElementsByTagName('head')[0],
script = doc.createElement('script');
script.type = 'text/javascript';
script.src = scriptLocation;
script.onload = callback;
script.onerror = function () {
Highcharts.error('Error loading script ' + scriptLocation);
};
head.appendChild(script);
}
/**
* Get blob URL from SVG code. Falls back to normal data URI.
*
* @private
* @function Highcharts.svgToDataURL
*
* @param {string} svg
*
* @return {string}
*/
Highcharts.svgToDataUrl = function (svg) {
// Webkit and not chrome
var webKit = (
nav.userAgent.indexOf('WebKit') > -1 &&
nav.userAgent.indexOf('Chrome') < 0
);
try {
// Safari requires data URI since it doesn't allow navigation to blob
// URLs. Firefox has an issue with Blobs and internal references,
// leading to gradients not working using Blobs (#4550)
if (!webKit && nav.userAgent.toLowerCase().indexOf('firefox') < 0) {
return domurl.createObjectURL(new win.Blob([svg], {
type: 'image/svg+xml;charset-utf-16'
}));
}
} catch (e) {
// Ignore
}
return 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(svg);
};
/**
* Get data:URL from image URL. Pass in callbacks to handle results.
*
* @private
* @function Highcharts.imageToDataUrl
*
* @param {string} imageURL
*
* @param {string} imageType
*
* @param {*} callbackArgs
* callbackArgs is used only by callbacks.
*
* @param {number} scale
*
* @param {Function} successCallback
* Receives four arguments: imageURL, imageType, callbackArgs, and scale.
*
* @param {Function} taintedCallback
* Receives four arguments: imageURL, imageType, callbackArgs, and scale.
*
* @param {Function} noCanvasSupportCallback
* Receives four arguments: imageURL, imageType, callbackArgs, and scale.
*
* @param {Function} failedLoadCallback
* Receives four arguments: imageURL, imageType, callbackArgs, and scale.
*
* @param {Function} [finallyCallback]
* finallyCallback is always called at the end of the process. All
* callbacks receive four arguments: imageURL, imageType, callbackArgs,
* and scale.
*/
Highcharts.imageToDataUrl = function (
imageURL,
imageType,
callbackArgs,
scale,
successCallback,
taintedCallback,
noCanvasSupportCallback,
failedLoadCallback,
finallyCallback
) {
var img = new win.Image(),
taintedHandler,
loadHandler = function () {
setTimeout(function () {
var canvas = doc.createElement('canvas'),
ctx = canvas.getContext && canvas.getContext('2d'),
dataURL;
try {
if (!ctx) {
noCanvasSupportCallback(
imageURL,
imageType,
callbackArgs,
scale
);
} else {
canvas.height = img.height * scale;
canvas.width = img.width * scale;
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
// Now we try to get the contents of the canvas.
try {
dataURL = canvas.toDataURL(imageType);
successCallback(
dataURL,
imageType,
callbackArgs,
scale
);
} catch (e) {
taintedHandler(
imageURL,
imageType,
callbackArgs,
scale
);
}
}
} finally {
if (finallyCallback) {
finallyCallback(
imageURL,
imageType,
callbackArgs,
scale
);
}
}
// IE bug where image is not always ready despite calling load
// event.
}, loadEventDeferDelay);
},
// Image load failed (e.g. invalid URL)
errorHandler = function () {
failedLoadCallback(imageURL, imageType, callbackArgs, scale);
if (finallyCallback) {
finallyCallback(imageURL, imageType, callbackArgs, scale);
}
};
// This is called on load if the image drawing to canvas failed with a
// security error. We retry the drawing with crossOrigin set to Anonymous.
taintedHandler = function () {
img = new win.Image();
taintedHandler = taintedCallback;
// Must be set prior to loading image source
img.crossOrigin = 'Anonymous';
img.onload = loadHandler;
img.onerror = errorHandler;
img.src = imageURL;
};
img.onload = loadHandler;
img.onerror = errorHandler;
img.src = imageURL;
};
/**
* Get data URL to an image of an SVG and call download on it options object:
*
* - **filename:** Name of resulting downloaded file without extension. Default
* is `chart`.
*
* - **type:** File type of resulting download. Default is `image/png`.
*
* - **scale:** Scaling factor of downloaded image compared to source. Default
* is `1`.
*
* - **libURL:** URL pointing to location of dependency scripts to download on
* demand. Default is the exporting.libURL option of the global Highcharts
* options pointing to our server.
*
* @function Highcharts.downloadSVGLocal
*
* @param {string} svg
*
* @param {Highcharts.ExportingOptions} options
*
* @param {Function} failCallback
*
* @param {Function} successCallback
*/
Highcharts.downloadSVGLocal = function (
svg,
options,
failCallback,
successCallback
) {
var svgurl,
blob,
objectURLRevoke = true,
finallyHandler,
libURL = options.libURL || Highcharts.getOptions().exporting.libURL,
dummySVGContainer = doc.createElement('div'),
imageType = options.type || 'image/png',
filename = (
(options.filename || 'chart') +
'.' +
(imageType === 'image/svg+xml' ? 'svg' : imageType.split('/')[1])
),
scale = options.scale || 1;
// Allow libURL to end with or without fordward slash
libURL = libURL.slice(-1) !== '/' ? libURL + '/' : libURL;
function svgToPdf(svgElement, margin) {
var width = svgElement.width.baseVal.value + 2 * margin,
height = svgElement.height.baseVal.value + 2 * margin,
pdf = new win.jsPDF( // eslint-disable-line new-cap
'l',
'pt',
[width, height]
);
// Workaround for #7090, hidden elements were drawn anyway. It comes
// down to https://github.com/yWorks/svg2pdf.js/issues/28. Check this
// later.
[].forEach.call(
svgElement.querySelectorAll('*[visibility="hidden"]'),
function (node) {
node.parentNode.removeChild(node);
}
);
win.svg2pdf(svgElement, pdf, { removeInvalid: true });
return pdf.output('datauristring');
}
function downloadPDF() {
dummySVGContainer.innerHTML = svg;
var textElements = dummySVGContainer.getElementsByTagName('text'),
titleElements,
svgData,
// Copy style property to element from parents if it's not there.
// Searches up hierarchy until it finds prop, or hits the chart
// container.
setStylePropertyFromParents = function (el, propName) {
var curParent = el;
while (curParent && curParent !== dummySVGContainer) {
if (curParent.style[propName]) {
el.style[propName] = curParent.style[propName];
break;
}
curParent = curParent.parentNode;
}
};
// Workaround for the text styling. Making sure it does pick up settings
// for parent elements.
[].forEach.call(textElements, function (el) {
// Workaround for the text styling. making sure it does pick up the
// root element
['font-family', 'font-size'].forEach(function (property) {
setStylePropertyFromParents(el, property);
});
el.style['font-family'] = (
el.style['font-family'] &&
el.style['font-family'].split(' ').splice(-1)
);
// Workaround for plotband with width, removing title from text
// nodes
titleElements = el.getElementsByTagName('title');
[].forEach.call(titleElements, function (titleElement) {
el.removeChild(titleElement);
});
});
svgData = svgToPdf(dummySVGContainer.firstChild, 0);
try {
Highcharts.downloadURL(svgData, filename);
if (successCallback) {
successCallback();
}
} catch (e) {
failCallback(e);
}
}
// Initiate download depending on file type
if (imageType === 'image/svg+xml') {
// SVG download. In this case, we want to use Microsoft specific Blob if
// available
try {
if (nav.msSaveOrOpenBlob) {
blob = new MSBlobBuilder();
blob.append(svg);
svgurl = blob.getBlob('image/svg+xml');
} else {
svgurl = Highcharts.svgToDataUrl(svg);
}
Highcharts.downloadURL(svgurl, filename);
if (successCallback) {
successCallback();
}
} catch (e) {
failCallback(e);
}
} else if (imageType === 'application/pdf') {
if (win.jsPDF && win.svg2pdf) {
downloadPDF();
} else {
// Must load pdf libraries first. // Don't destroy the object URL
// yet since we are doing things asynchronously. A cleaner solution
// would be nice, but this will do for now.
objectURLRevoke = true;
getScript(libURL + 'jspdf.js', function () {
getScript(libURL + 'svg2pdf.js', function () {
downloadPDF();
});
});
}
} else {
// PNG/JPEG download - create bitmap from SVG
svgurl = Highcharts.svgToDataUrl(svg);
finallyHandler = function () {
try {
domurl.revokeObjectURL(svgurl);
} catch (e) {
// Ignore
}
};
// First, try to get PNG by rendering on canvas
Highcharts.imageToDataUrl(
svgurl,
imageType,
{},
scale,
function (imageURL) {
// Success
try {
Highcharts.downloadURL(imageURL, filename);
if (successCallback) {
successCallback();
}
} catch (e) {
failCallback(e);
}
}, function () {
// Failed due to tainted canvas
// Create new and untainted canvas
var canvas = doc.createElement('canvas'),
ctx = canvas.getContext('2d'),
imageWidth = svg.match(
/^<svg[^>]*width\s*=\s*\"?(\d+)\"?[^>]*>/
)[1] * scale,
imageHeight = svg.match(
/^<svg[^>]*height\s*=\s*\"?(\d+)\"?[^>]*>/
)[1] * scale,
downloadWithCanVG = function () {
ctx.drawSvg(svg, 0, 0, imageWidth, imageHeight);
try {
Highcharts.downloadURL(
nav.msSaveOrOpenBlob ?
canvas.msToBlob() :
canvas.toDataURL(imageType),
filename
);
if (successCallback) {
successCallback();
}
} catch (e) {
failCallback(e);
} finally {
finallyHandler();
}
};
canvas.width = imageWidth;
canvas.height = imageHeight;
if (win.canvg) {
// Use preloaded canvg
downloadWithCanVG();
} else {
// Must load canVG first. // Don't destroy the object URL
// yet since we are doing things asynchronously. A cleaner
// solution would be nice, but this will do for now.
objectURLRevoke = true;
// Get RGBColor.js first, then canvg
getScript(libURL + 'rgbcolor.js', function () {
getScript(libURL + 'canvg.js', function () {
downloadWithCanVG();
});
});
}
},
// No canvas support
failCallback,
// Failed to load image
failCallback,
// Finally
function () {
if (objectURLRevoke) {
finallyHandler();
}
}
);
}
};
/**
* Get SVG of chart prepared for client side export. This converts embedded
* images in the SVG to data URIs. It requires the regular exporting module. The
* options and chartOptions arguments are passed to the getSVGForExport
* function.
*
* @private
* @function Highcharts.Chart#getSVGForLocalExport
*
* @param {Highcharts.ExportingOptions} options
*
* @param {Highcharts.Options} chartOptions
*
* @param {Function} failCallback
*
* @param {Function} successCallback
*/
Highcharts.Chart.prototype.getSVGForLocalExport = function (
options,
chartOptions,
failCallback,
successCallback
) {
var chart = this,
images,
imagesEmbedded = 0,
chartCopyContainer,
chartCopyOptions,
el,
i,
l,
// After grabbing the SVG of the chart's copy container we need to do
// sanitation on the SVG
sanitize = function (svg) {
return chart.sanitizeSVG(svg, chartCopyOptions);
},
// Success handler, we converted image to base64!
embeddedSuccess = function (imageURL, imageType, callbackArgs) {
++imagesEmbedded;
// Change image href in chart copy
callbackArgs.imageElement.setAttributeNS(
'http://www.w3.org/1999/xlink',
'href',
imageURL
);
// When done with last image we have our SVG
if (imagesEmbedded === images.length) {
successCallback(sanitize(chartCopyContainer.innerHTML));
}
};
// Hook into getSVG to get a copy of the chart copy's container (#8273)
chart.unbindGetSVG = addEvent(chart, 'getSVG', function (e) {
chartCopyOptions = e.chartCopy.options;
chartCopyContainer = e.chartCopy.container.cloneNode(true);
});
// Trigger hook to get chart copy
chart.getSVGForExport(options, chartOptions);
images = chartCopyContainer.getElementsByTagName('image');
try {
// If there are no images to embed, the SVG is okay now.
if (!images.length) {
// Use SVG of chart copy
successCallback(sanitize(chartCopyContainer.innerHTML));
return;
}
// Go through the images we want to embed
for (i = 0, l = images.length; i < l; ++i) {
el = images[i];
Highcharts.imageToDataUrl(
el.getAttributeNS(
'http://www.w3.org/1999/xlink',
'href'
),
'image/png',
{ imageElement: el }, options.scale,
embeddedSuccess,
// Tainted canvas
failCallback,
// No canvas support
failCallback,
// Failed to load source
failCallback
);
}
} catch (e) {
failCallback(e);
}
// Clean up
chart.unbindGetSVG();
};
/**
* Exporting and offline-exporting modules required. Export a chart to an image
* locally in the user's browser. Requires the regular exporting module.
*
* @function Highcharts.Chart#exportChartLocal
*
* @param {Highcharts.ExportingOptions} exportingOptions
* Exporting options, the same as in
* {@link Highcharts.Chart#exportChart}.
*
* @param {Highcharts.Options} chartOptions
* Additional chart options for the exported chart. For example a
* different background color can be added here, or `dataLabels`
* for export only.
*/
Highcharts.Chart.prototype.exportChartLocal = function (
exportingOptions,
chartOptions
) {
var chart = this,
options = Highcharts.merge(chart.options.exporting, exportingOptions),
fallbackToExportServer = function (err) {
if (options.fallbackToExportServer === false) {
if (options.error) {
options.error(options, err);
} else {
Highcharts.error(28, true); // Fallback disabled
}
} else {
chart.exportChart(options);
}
},
svgSuccess = function (svg) {
// If SVG contains foreignObjects all exports except SVG will fail,
// as both CanVG and svg2pdf choke on this. Gracefully fall back.
if (
svg.indexOf('<foreignObject') > -1 &&
options.type !== 'image/svg+xml'
) {
fallbackToExportServer(
'Image type not supported for charts with embedded HTML'
);
} else {
Highcharts.downloadSVGLocal(
svg,
Highcharts.extend(
{ filename: chart.getFilename() },
options
),
fallbackToExportServer
);
}
};
// If we are on IE and in styled mode, add a whitelist to the renderer for
// inline styles that we want to pass through. There are so many styles by
// default in IE that we don't want to blacklist them all.
if (isMSBrowser && chart.styledMode) {
Highcharts.SVGRenderer.prototype.inlineWhitelist = [
/^blockSize/,
/^border/,
/^caretColor/,
/^color/,
/^columnRule/,
/^columnRuleColor/,
/^cssFloat/,
/^cursor/,
/^fill$/,
/^fillOpacity/,
/^font/,
/^inlineSize/,
/^length/,
/^lineHeight/,
/^opacity/,
/^outline/,
/^parentRule/,
/^rx$/,
/^ry$/,
/^stroke/,
/^textAlign/,
/^textAnchor/,
/^textDecoration/,
/^transform/,
/^vectorEffect/,
/^visibility/,
/^x$/,
/^y$/
];
}
// Always fall back on:
// - MS browsers: Embedded images JPEG/PNG, or any PDF
// - Embedded images and PDF
if (
(
isMSBrowser &&
(
options.type === 'application/pdf' ||
chart.container.getElementsByTagName('image').length &&
options.type !== 'image/svg+xml'
)
) || (
options.type === 'application/pdf' &&
chart.container.getElementsByTagName('image').length
)
) {
fallbackToExportServer(
'Image type not supported for this chart/browser.'
);
return;
}
chart.getSVGForLocalExport(
options,
chartOptions,
fallbackToExportServer,
svgSuccess
);
};
// Extend the default options to use the local exporter logic
merge(true, Highcharts.getOptions().exporting, {
libURL: 'https://code.highcharts.com/@product.version@/lib/',
// When offline-exporting is loaded, redefine the menu item definitions
// related to download.
menuItemDefinitions: {
downloadPNG: {
textKey: 'downloadPNG',
onclick: function () {
this.exportChartLocal();
}
},
downloadJPEG: {
textKey: 'downloadJPEG',
onclick: function () {
this.exportChartLocal({
type: 'image/jpeg'
});
}
},
downloadSVG: {
textKey: 'downloadSVG',
onclick: function () {
this.exportChartLocal({
type: 'image/svg+xml'
});
}
},
downloadPDF: {
textKey: 'downloadPDF',
onclick: function () {
this.exportChartLocal({
type: 'application/pdf'
});
}
}
}
});
@@ -0,0 +1,121 @@
/**
* (c) 2010-2019 Torstein Honsi
*
* Simple polyfills for array functions in old IE browsers (6, 7 and 8) in
* Highcharts v7+. These polyfills are sufficient for Highcharts to work, but
* for fully compatible polyfills, see MDN.
*
* License: www.highcharts.com/license
*/
/* eslint no-extend-native: 0 */
'use strict';
if (!Array.prototype.forEach) {
Array.prototype.forEach = function (fn, ctx) {
var i = 0,
len = this.length;
for (; i < len; i++) {
if (
this[i] !== undefined && // added check
fn.call(ctx, this[i], i, this) === false
) {
return i;
}
}
};
}
if (!Array.prototype.map) {
Array.prototype.map = function (fn) {
var results = [],
i = 0,
len = this.length;
for (; i < len; i++) {
results[i] = fn.call(this[i], this[i], i, this);
}
return results;
};
}
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function (member, fromIndex) {
var arr = this, // #8874
len,
i = fromIndex || 0; // #8346
if (arr) {
len = arr.length;
for (; i < len; i++) {
if (arr[i] === member) {
return i;
}
}
}
return -1;
};
}
if (!Array.prototype.filter) {
Array.prototype.filter = function (fn) {
var ret = [],
i = 0,
length = this.length;
for (; i < length; i++) {
if (fn(this[i], i)) {
ret.push(this[i]);
}
}
return ret;
};
}
if (!Array.prototype.some) {
Array.prototype.some = function (fn, ctx) { // legacy
var i = 0,
len = this.length;
for (; i < len; i++) {
if (fn.call(ctx, this[i], i, this) === true) {
return true;
}
}
return false;
};
}
if (!Array.prototype.reduce) {
Array.prototype.reduce = function (func, initialValue) {
var context = this,
i = arguments.length > 1 ? 0 : 1,
accumulator = arguments.length > 1 ? initialValue : this[0],
len = this.length;
for (; i < len; ++i) {
accumulator = func.call(context, accumulator, this[i], i, this);
}
return accumulator;
};
}
if (!Object.keys) {
Object.keys = function (obj) {
var result = [],
hasOwnProperty = Object.prototype.hasOwnProperty,
prop;
for (prop in obj) {
if (hasOwnProperty.call(obj, prop)) {
result.push(prop);
}
}
return result;
};
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,250 @@
/* *
* Highcharts module to hide overlapping data labels. This module is included in
* Highcharts.
*
* (c) 2009-2019 Torstein Honsi
*
* License: www.highcharts.com/license
*/
'use strict';
import H from '../parts/Globals.js';
import '../parts/Utilities.js';
import '../parts/Chart.js';
var Chart = H.Chart,
isArray = H.isArray,
objectEach = H.objectEach,
pick = H.pick,
addEvent = H.addEvent,
fireEvent = H.fireEvent;
// 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.
addEvent(Chart, 'render', function collectAndHide() {
var labels = [];
// Consider external label collectors
(this.labelCollectors || []).forEach(function (collector) {
labels = labels.concat(collector());
});
(this.yAxis || []).forEach(function (yAxis) {
if (
yAxis.options.stackLabels &&
!yAxis.options.stackLabels.allowOverlap
) {
objectEach(yAxis.stacks, function (stack) {
objectEach(stack, function (stackItem) {
labels.push(stackItem.label);
});
});
}
});
(this.series || []).forEach(function (series) {
var dlOptions = series.options.dataLabels;
if (
series.visible &&
!(dlOptions.enabled === false && !series._hasPointLabels)
) { // #3866
series.points.forEach(function (point) {
if (point.visible) {
var dataLabels = (
isArray(point.dataLabels) ?
point.dataLabels :
(point.dataLabel ? [point.dataLabel] : [])
);
dataLabels.forEach(function (label) {
var options = label.options;
label.labelrank = pick(
options.labelrank,
point.labelrank,
point.shapeArgs && point.shapeArgs.height
); // #4118
if (!options.allowOverlap) {
labels.push(label);
}
});
}
});
}
});
this.hideOverlappingLabels(labels);
});
/**
* Hide overlapping labels. Labels are moved and faded in and out on zoom to
* provide a smooth visual imression.
*
* @private
* @function Highcharts.Chart#hideOverlappingLabels
*
* @param {Array<Highcharts.SVGElement>} labels
*/
Chart.prototype.hideOverlappingLabels = function (labels) {
var chart = this,
len = labels.length,
ren = chart.renderer,
label,
i,
j,
label1,
label2,
isIntersecting,
box1,
box2,
intersectRect = function (x1, y1, w1, h1, x2, y2, w2, h2) {
return !(
x2 > x1 + w1 ||
x2 + w2 < x1 ||
y2 > y1 + h1 ||
y2 + h2 < y1
);
},
// Get the box with its position inside the chart, as opposed to getBBox
// that only reports the position relative to the parent.
getAbsoluteBox = function (label) {
var pos,
parent,
bBox,
// Substract the padding if no background or border (#4333)
padding = label.box ? 0 : (label.padding || 0),
lineHeightCorrection = 0;
if (
label &&
(!label.alignAttr || label.placed)
) {
pos = label.alignAttr || {
x: label.attr('x'),
y: label.attr('y')
};
parent = label.parentGroup;
// Get width and height if pure text nodes (stack labels)
if (!label.width) {
bBox = label.getBBox();
label.width = bBox.width;
label.height = bBox.height;
// Labels positions are computed from top left corner, so
// we need to substract the text height from text nodes too.
lineHeightCorrection = ren
.fontMetrics(null, label.element).h;
}
return {
x: pos.x + (parent.translateX || 0) + padding,
y: pos.y + (parent.translateY || 0) + padding -
lineHeightCorrection,
width: label.width - 2 * padding,
height: label.height - 2 * padding
};
}
};
for (i = 0; i < len; i++) {
label = labels[i];
if (label) {
// Mark with initial opacity
label.oldOpacity = label.opacity;
label.newOpacity = 1;
label.absoluteBox = getAbsoluteBox(label);
}
}
// Prevent a situation in a gradually rising slope, that each label will
// hide the previous one because the previous one always has lower rank.
labels.sort(function (a, b) {
return (b.labelrank || 0) - (a.labelrank || 0);
});
// Detect overlapping labels
for (i = 0; i < len; i++) {
label1 = labels[i];
box1 = label1 && label1.absoluteBox;
for (j = i + 1; j < len; ++j) {
label2 = labels[j];
box2 = label2 && label2.absoluteBox;
if (
box1 &&
box2 &&
label1 !== label2 && // #6465, polar chart with connectEnds
label1.newOpacity !== 0 &&
label2.newOpacity !== 0
) {
isIntersecting = intersectRect(
box1.x,
box1.y,
box1.width,
box1.height,
box2.x,
box2.y,
box2.width,
box2.height
);
if (isIntersecting) {
(label1.labelrank < label2.labelrank ? label1 : label2)
.newOpacity = 0;
}
}
}
}
// Hide or show
labels.forEach(function (label) {
var complete,
newOpacity;
if (label) {
newOpacity = label.newOpacity;
if (label.oldOpacity !== newOpacity) {
// Make sure the label is completely hidden to avoid catching
// clicks (#4362)
if (label.alignAttr && label.placed) { // data labels
if (newOpacity) {
label.show(true);
} else {
complete = function () {
label.hide();
};
}
// Animate or set the opacity
label.alignAttr.opacity = newOpacity;
label[label.isOld ? 'animate' : 'attr'](
label.alignAttr,
null,
complete
);
fireEvent(chart, 'afterHideOverlappingLabels');
} else { // other labels, tick labels
label.attr({
opacity: newOpacity
});
}
}
label.isOld = true;
}
});
};
@@ -0,0 +1,492 @@
/* *
* Parallel coordinates module
*
* (c) 2010-2019 Pawel Fus
*
* License: www.highcharts.com/license
*/
'use strict';
import H from '../parts/Globals.js';
import '../parts/Axis.js';
import '../parts/Chart.js';
import '../parts/Series.js';
// Extensions for parallel coordinates plot.
var Axis = H.Axis,
Chart = H.Chart,
ChartProto = Chart.prototype,
AxisProto = H.Axis.prototype;
var addEvent = H.addEvent,
pick = H.pick,
wrap = H.wrap,
merge = H.merge,
erase = H.erase,
splat = H.splat,
extend = H.extend,
defined = H.defined,
arrayMin = H.arrayMin,
arrayMax = H.arrayMax;
var defaultXAxisOptions = {
lineWidth: 0,
tickLength: 0,
opposite: true,
type: 'category'
};
/**
* @optionparent chart
*/
var defaultParallelOptions = {
/**
* Flag to render charts as a parallel coordinates plot. In a parallel
* coordinates plot (||-coords) by default all required yAxes are generated
* and the legend is disabled. This feature requires
* `modules/parallel-coordinates.js`.
*
* @sample {highcharts} /highcharts/demo/parallel-coordinates/
* Parallel coordinates demo
* @sample {highcharts} highcharts/parallel-coordinates/polar/
* Star plot, multivariate data in a polar chart
*
* @since 6.0.0
* @product highcharts
*/
parallelCoordinates: false,
/**
* Common options for all yAxes rendered in a parallel coordinates plot.
* This feature requires `modules/parallel-coordinates.js`.
*
* The default options are:
* <pre>
* parallelAxes: {
* lineWidth: 1, // classic mode only
* gridlinesWidth: 0, // classic mode only
* title: {
* text: '',
* reserveSpace: false
* },
* labels: {
* x: 0,
* y: 0,
* align: 'center',
* reserveSpace: false
* },
* offset: 0
* }</pre>
*
* @sample {highcharts} highcharts/parallel-coordinates/parallelaxes/
* Set the same tickAmount for all yAxes
*
* @extends yAxis
* @since 6.0.0
* @product highcharts
* @excluding alternateGridColor, breaks, id, gridLineColor,
* gridLineDashStyle, gridLineWidth, minorGridLineColor,
* minorGridLineDashStyle, minorGridLineWidth, plotBands,
* plotLines, angle, gridLineInterpolation, maxColor, maxZoom,
* minColor, scrollbar, stackLabels, stops
*/
parallelAxes: {
lineWidth: 1,
/**
* Titles for yAxes are taken from
* [xAxis.categories](#xAxis.categories). All options for `xAxis.labels`
* applies to parallel coordinates titles. For example, to style
* categories, use [xAxis.labels.style](#xAxis.labels.style).
*
* @excluding align, enabled, margin, offset, position3d, reserveSpace,
* rotation, skew3d, style, text, useHTML, x, y
*/
title: {
text: '',
reserveSpace: false
},
labels: {
x: 0,
y: 4,
align: 'center',
reserveSpace: false
},
offset: 0
}
};
H.setOptions({
chart: defaultParallelOptions
});
// Initialize parallelCoordinates
addEvent(Chart, 'init', function (e) {
var options = e.args[0],
defaultyAxis = splat(options.yAxis || {}),
yAxisLength = defaultyAxis.length,
newYAxes = [];
/**
* Flag used in parallel coordinates plot to check if chart has ||-coords
* (parallel coords).
*
* @requires module:modules/parallel-coordinates
*
* @name Highcharts.Chart#hasParallelCoordinates
* @type {boolean}
*/
this.hasParallelCoordinates = options.chart &&
options.chart.parallelCoordinates;
if (this.hasParallelCoordinates) {
this.setParallelInfo(options);
// Push empty yAxes in case user did not define them:
for (; yAxisLength <= this.parallelInfo.counter; yAxisLength++) {
newYAxes.push({});
}
if (!options.legend) {
options.legend = {};
}
if (options.legend.enabled === undefined) {
options.legend.enabled = false;
}
merge(
true,
options,
// Disable boost
{
boost: {
seriesThreshold: Number.MAX_VALUE
},
plotOptions: {
series: {
boostThreshold: Number.MAX_VALUE
}
}
}
);
options.yAxis = defaultyAxis.concat(newYAxes);
options.xAxis = merge(
defaultXAxisOptions, // docs
splat(options.xAxis || {})[0]
);
}
});
// Initialize parallelCoordinates
addEvent(Chart, 'update', function (e) {
var options = e.options;
if (options.chart) {
if (defined(options.chart.parallelCoordinates)) {
this.hasParallelCoordinates = options.chart.parallelCoordinates;
}
if (this.hasParallelCoordinates && options.chart.parallelAxes) {
this.options.chart.parallelAxes = merge(
this.options.chart.parallelAxes,
options.chart.parallelAxes
);
this.yAxis.forEach(function (axis) {
axis.update({}, false);
});
}
}
});
extend(ChartProto, /** @lends Highcharts.Chart.prototype */ {
/**
* Define how many parellel axes we have according to the longest dataset.
* This is quite heavy - loop over all series and check series.data.length
* Consider:
*
* - make this an option, so user needs to set this to get better
* performance
*
* - check only first series for number of points and assume the rest is the
* same
*
* @private
* @function Highcharts.Chart#setParallelInfo
*
* @param {Highcharts.Options} options
* User options
*/
setParallelInfo: function (options) {
var chart = this,
seriesOptions = options.series;
chart.parallelInfo = {
counter: 0
};
seriesOptions.forEach(function (series) {
if (series.data) {
chart.parallelInfo.counter = Math.max(
chart.parallelInfo.counter,
series.data.length - 1
);
}
});
}
});
// On update, keep parallelPosition.
AxisProto.keepProps.push('parallelPosition');
// Update default options with predefined for a parallel coords.
addEvent(Axis, 'afterSetOptions', function (e) {
var axis = this,
chart = axis.chart,
axisPosition = ['left', 'width', 'height', 'top'];
if (chart.hasParallelCoordinates) {
if (chart.inverted) {
axisPosition = axisPosition.reverse();
}
if (axis.isXAxis) {
axis.options = merge(
axis.options,
defaultXAxisOptions,
e.userOptions
);
} else {
axis.options = merge(
axis.options,
axis.chart.options.chart.parallelAxes,
e.userOptions
);
axis.parallelPosition = pick(
axis.parallelPosition,
chart.yAxis.length
);
axis.setParallelPosition(axisPosition, axis.options);
}
}
});
/* Each axis should gather extremes from points on a particular position in
series.data. Not like the default one, which gathers extremes from all series
bind to this axis. Consider using series.points instead of series.yData. */
addEvent(Axis, 'getSeriesExtremes', function (e) {
if (this.chart && this.chart.hasParallelCoordinates && !this.isXAxis) {
var index = this.parallelPosition,
currentPoints = [];
this.series.forEach(function (series) {
if (series.visible && defined(series.yData[index])) {
// We need to use push() beacause of null points
currentPoints.push(series.yData[index]);
}
});
this.dataMin = arrayMin(currentPoints);
this.dataMax = arrayMax(currentPoints);
e.preventDefault();
}
});
extend(AxisProto, /** @lends Highcharts.Axis.prototype */ {
/**
* Set predefined left+width and top+height (inverted) for yAxes. This
* method modifies options param.
*
* @function Highcharts.Axis#setParallelPosition
*
* @param {Array<string>} axisPosition
* ['left', 'width', 'height', 'top'] or
* ['top', 'height', 'width', 'left'] for an inverted chart.
*
* @param {Highcharts.AxisOptions} options
* {@link Highcharts.Axis#options}.
*/
setParallelPosition: function (axisPosition, options) {
var fraction = (this.parallelPosition + 0.5) /
(this.chart.parallelInfo.counter + 1);
if (this.chart.polar) {
options.angle = 360 * fraction;
} else {
options[axisPosition[0]] = 100 * fraction + '%';
this[axisPosition[1]] = options[axisPosition[1]] = 0;
// In case of chart.update(inverted), remove old options:
this[axisPosition[2]] = options[axisPosition[2]] = null;
this[axisPosition[3]] = options[axisPosition[3]] = null;
}
}
});
// Bind each series to each yAxis. yAxis needs a reference to all series to
// calculate extremes.
addEvent(H.Series, 'bindAxes', function (e) {
if (this.chart.hasParallelCoordinates) {
var series = this;
this.chart.axes.forEach(function (axis) {
series.insert(axis.series);
axis.isDirty = true;
});
series.xAxis = this.chart.xAxis[0];
series.yAxis = this.chart.yAxis[0];
e.preventDefault();
}
});
// Translate each point using corresponding yAxis.
addEvent(H.Series, 'afterTranslate', function () {
var series = this,
chart = this.chart,
points = series.points,
dataLength = points && points.length,
closestPointRangePx = Number.MAX_VALUE,
lastPlotX,
point,
i;
if (this.chart.hasParallelCoordinates) {
for (i = 0; i < dataLength; i++) {
point = points[i];
if (defined(point.y)) {
if (chart.polar) {
point.plotX = chart.yAxis[i].angleRad || 0;
} else if (chart.inverted) {
point.plotX = (
chart.plotHeight -
chart.yAxis[i].top +
chart.plotTop
);
} else {
point.plotX = chart.yAxis[i].left - chart.plotLeft;
}
point.clientX = point.plotX;
point.plotY = chart.yAxis[i]
.translate(point.y, false, true, null, true);
if (lastPlotX !== undefined) {
closestPointRangePx = Math.min(
closestPointRangePx,
Math.abs(point.plotX - lastPlotX)
);
}
lastPlotX = point.plotX;
point.isInside = chart.isInsidePlot(
point.plotX,
point.plotY,
chart.inverted
);
} else {
point.isNull = true;
}
}
this.closestPointRangePx = closestPointRangePx;
}
}, { order: 1 });
// On destroy, we need to remove series from each axis.series
H.addEvent(H.Series, 'destroy', function () {
if (this.chart.hasParallelCoordinates) {
(this.chart.axes || []).forEach(function (axis) {
if (axis && axis.series) {
erase(axis.series, this);
axis.isDirty = axis.forceRedraw = true;
}
}, this);
}
});
function addFormattedValue(proceed) {
var chart = this.series && this.series.chart,
config = proceed.apply(this, Array.prototype.slice.call(arguments, 1)),
formattedValue,
yAxisOptions,
labelFormat,
yAxis;
if (
chart &&
chart.hasParallelCoordinates &&
!defined(config.formattedValue)
) {
yAxis = chart.yAxis[this.x];
yAxisOptions = yAxis.options;
labelFormat = pick(
/**
* Parallel coordinates only. Format that will be used for point.y
* and available in [tooltip.pointFormat](#tooltip.pointFormat) as
* `{point.formattedValue}`. If not set, `{point.formattedValue}`
* will use other options, in this order:
*
* 1. [yAxis.labels.format](#yAxis.labels.format) will be used if
* set
*
* 2. If yAxis is a category, then category name will be displayed
*
* 3. If yAxis is a datetime, then value will use the same format as
* yAxis labels
*
* 4. If yAxis is linear/logarithmic type, then simple value will be
* used
*
* @sample {highcharts}
* /highcharts/parallel-coordinates/tooltipvalueformat/
* Different tooltipValueFormats's
*
* @type {string}
* @default undefined
* @since 6.0.0
* @product highcharts
* @apioption yAxis.tooltipValueFormat
*/
yAxisOptions.tooltipValueFormat,
yAxisOptions.labels.format
);
if (labelFormat) {
formattedValue = H.format(
labelFormat,
extend(
this,
{ value: this.y }
),
chart.time
);
} else if (yAxis.isDatetimeAxis) {
formattedValue = chart.time.dateFormat(
chart.time.resolveDTLFormat(yAxisOptions.dateTimeLabelFormats[
yAxis.tickPositions.info.unitName
]).main,
this.y
);
} else if (yAxisOptions.categories) {
formattedValue = yAxisOptions.categories[this.y];
} else {
formattedValue = this.y;
}
config.formattedValue = config.point.formattedValue = formattedValue;
}
return config;
}
['line', 'spline'].forEach(function (seriesName) {
wrap(
H.seriesTypes[seriesName].prototype.pointClass.prototype,
'getLabelConfig',
addFormattedValue
);
});
@@ -0,0 +1,152 @@
/* *
* (c) 2010-2017 Sebastian Bochan
*
* License: www.highcharts.com/license
*/
'use strict';
import H from '../parts/Globals.js';
import '../parts/Utilities.js';
import '../parts/Options.js';
import derivedSeriesMixin from '../mixins/derived-series.js';
var correctFloat = H.correctFloat,
seriesType = H.seriesType,
merge = H.merge;
/**
* The pareto series type.
*
* @private
* @class
* @name Highcharts.seriesTypes.pareto
*
* @augments Highcharts.Series
*/
seriesType('pareto', 'line'
/**
* A pareto diagram is a type of chart that contains both bars and a line graph,
* where individual values are represented in descending order by bars, and the
* cumulative total is represented by the line.
*
* @sample {highcharts} highcharts/demo/pareto/
* Pareto diagram
*
* @extends plotOptions.line
* @since 6.0.0
* @product highcharts
* @excluding allAreas, boostThreshold, borderColor, borderRadius,
* borderWidth, crisp, colorAxis, depth, data, edgeColor,
* edgeWidth, findNearestPointBy, gapSize, gapUnit, grouping,
* groupPadding, groupZPadding, maxPointWidth, keys,
* negativeColor, pointInterval, pointIntervalUnit, pointPadding,
* pointPlacement, pointRange, pointStart, pointWidth, shadow,
* step, softThreshold, stacking, threshold, zoneAxis, zones
* @optionparent plotOptions.pareto
*/
, {
/**
* Higher zIndex than column series to draw line above shapes.
*/
zIndex: 3
}, merge(derivedSeriesMixin, {
/**
* Calculate sum and return percent points.
*
* @private
* @function Highcharts.Series#setDerivedData
*
* @return {Array<Array<number,number>>}
* Returns array of points [x,y]
*/
setDerivedData: function () {
if (this.baseSeries.yData.length > 1) {
var xValues = this.baseSeries.xData,
yValues = this.baseSeries.yData,
sum = this.sumPointsPercents(yValues, xValues, null, true);
this.setData(
this.sumPointsPercents(yValues, xValues, sum, false),
false
);
}
},
/**
* Calculate y sum and each percent point.
*
* @private
* @function Highcharts.Series#sumPointsPercents
*
* @param {Array<number>} yValues
* Y values
*
* @param {Array<number>} xValues
* X values
*
* @param {number} sum
* Sum of all y values
*
* @param {boolean} [isSum]
* Declares if calculate sum of all points
*
* @return {number|Array<number,number>}
* Returns sum of points or array of points [x,sum]
*/
sumPointsPercents: function (yValues, xValues, sum, isSum) {
var sumY = 0,
sumPercent = 0,
percentPoints = [],
percentPoint;
yValues.forEach(function (point, i) {
if (point !== null) {
if (isSum) {
sumY += point;
} else {
percentPoint = (point / sum) * 100;
percentPoints.push([
xValues[i],
correctFloat(sumPercent + percentPoint)
]);
sumPercent += percentPoint;
}
}
});
return isSum ? sumY : percentPoints;
}
}));
/**
* A `pareto` series. If the [type](#series.pareto.type) option is not
* specified, it is inherited from [chart.type](#chart.type).
*
* @extends series,plotOptions.pareto
* @since 6.0.0
* @product highcharts
* @excluding data, dataParser, dataURL
* @apioption series.pareto
*/
/**
* An integer identifying the index to use for the base series, or a string
* representing the id of the series.
*
* @type {number|string}
* @default undefined
* @apioption series.pareto.baseSeries
*/
/**
* An array of data points for the series. For the `pareto` series type,
* points are calculated dynamically.
*
* @type {Array<Array<number|string>|*>}
* @extends series.column.data
* @since 6.0.0
* @product highcharts
* @apioption series.pareto.data
*/
@@ -0,0 +1,607 @@
/* *
* Module for using patterns or images as point fills.
*
* (c) 2010-2019 Highsoft AS
* Author: Torstein Hønsi, Øystein Moseng
*
* License: www.highcharts.com/license
*/
/**
* Pattern options
*
* @interface Highcharts.PatternOptionsObject
*//**
* URL to an image to use as the pattern.
* @name Highcharts.PatternOptionsObject#image
* @type {string}
*//**
* Width of the pattern. For images this is automatically set to the width of
* the element bounding box if not supplied. For non-image patterns the default
* is 32px. Note that automatic resizing of image patterns to fill a bounding
* box dynamically is only supported for patterns with an automatically
* calculated ID.
* @name Highcharts.PatternOptionsObject#with
* @type {number}
*//**
* Analogous to pattern.width.
* @name Highcharts.PatternOptionsObject#height
* @type {number}
*//**
* For automatically calculated width and height on images, it is possible to
* set an aspect ratio. The image will be zoomed to fill the bounding box,
* maintaining the aspect ratio defined.
* @name Highcharts.PatternOptionsObject#aspectRatio
* @type {number}
*//**
* Horizontal offset of the pattern. Defaults to 0.
* @name Highcharts.PatternOptionsObject#x
* @type {number|undefined}
*//**
* Vertical offset of the pattern. Defaults to 0.
* @name Highcharts.PatternOptionsObject#y
* @type {number|undefined}
*//**
* Either an SVG path as string, or an object. As an object, supply the path
* string in the `path.d` property. Other supported properties are standard SVG
* attributes like `path.stroke` and `path.fill`. If a path is supplied for the
* pattern, the `image` property is ignored.
* @name Highcharts.PatternOptionsObject#path
* @type {string|Highcharts.SVGAttributes}
*//**
* Pattern color, used as default path stroke.
* @name Highcharts.PatternOptionsObject#color
* @type {Highcharts.ColorString}
*//**
* Opacity of the pattern as a float value from 0 to 1.
* @name Highcharts.PatternOptionsObject#opacity
* @type {number}
*//**
* ID to assign to the pattern. This is automatically computed if not added, and
* identical patterns are reused. To refer to an existing pattern for a
* Highcharts color, use `color: "url(#pattern-id)"`.
* @name Highcharts.PatternOptionsObject#id
* @type {string}
*/
/**
* Holds a pattern definition.
*
* @sample highcharts/series/pattern-fill-area/
* Define a custom path pattern
* @sample highcharts/series/pattern-fill-pie/
* Default patterns and a custom image pattern
* @sample maps/demo/pattern-fill-map/
* Custom images on map
*
* @example
* // Pattern used as a color option
* color: {
* pattern: {
* path: {
* d: 'M 3 3 L 8 3 L 8 8 Z',
* fill: '#102045'
* },
* width: 12,
* height: 12,
* color: '#907000',
* opacity: 0.5
* }
* }
*
* @interface Highcharts.PatternObject
*//**
* Pattern options
* @name Highcharts.PatternObject#pattern
* @type {Highcharts.PatternOptionsObject}
*//**
* Animation options for the image pattern loading.
* @name Highcharts.PatternObject#animation
* @type {boolean|Highcharts.AnimationOptionsObject|undefined}
*/
'use strict';
import H from '../parts/Globals.js';
import '../parts/Utilities.js';
var addEvent = H.addEvent,
wrap = H.wrap,
merge = H.merge,
pick = H.pick;
/**
* Utility function to compute a hash value from an object. Modified Java
* String.hashCode implementation in JS. Use the preSeed parameter to add an
* additional seeding step.
*
* @private
* @function hashFromObject
*
* @param {object} obj
* The javascript object to compute the hash from.
*
* @param {boolean} [preSeed=false]
* Add an optional preSeed stage.
*
* @return {string}
* The computed hash.
*/
function hashFromObject(obj, preSeed) {
var str = JSON.stringify(obj),
strLen = str.length || 0,
hash = 0,
i = 0,
char,
seedStep;
if (preSeed) {
seedStep = Math.max(Math.floor(strLen / 500), 1);
for (var a = 0; a < strLen; a += seedStep) {
hash += str.charCodeAt(a);
}
hash = hash & hash;
}
for (; i < strLen; ++i) {
char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return hash.toString(16).replace('-', '1');
}
/**
* Set dimensions on pattern from point. This function will set internal
* pattern._width/_height properties if width and height are not both already
* set. We only do this on image patterns. The _width/_height properties are set
* to the size of the bounding box of the point, optionally taking aspect ratio
* into account. If only one of width or height are supplied as options, the
* undefined option is calculated as above.
*
* @private
* @function Highcharts.Point#calculatePatternDimensions
*
* @param {Highcharts.PatternObject} pattern
* The pattern to set dimensions on.
*/
H.Point.prototype.calculatePatternDimensions = function (pattern) {
if (pattern.width && pattern.height) {
return;
}
var bBox = this.graphic && (
this.graphic.getBBox &&
this.graphic.getBBox(true) ||
this.graphic.element &&
this.graphic.element.getBBox()
) || {},
shapeArgs = this.shapeArgs;
// Prefer using shapeArgs, as it is animation agnostic
if (shapeArgs) {
bBox.width = shapeArgs.width || bBox.width;
bBox.height = shapeArgs.height || bBox.height;
bBox.x = shapeArgs.x || bBox.x;
bBox.y = shapeArgs.y || bBox.y;
}
// For images we stretch to bounding box
if (pattern.image) {
// If we do not have a bounding box at this point, simply add a defer
// key and pick this up in the fillSetter handler, where the bounding
// box should exist.
if (!bBox.width || !bBox.height) {
pattern._width = 'defer';
pattern._height = 'defer';
return;
}
// Handle aspect ratio filling
if (pattern.aspectRatio) {
bBox.aspectRatio = bBox.width / bBox.height;
if (pattern.aspectRatio > bBox.aspectRatio) {
// Height of bBox will determine width
bBox.aspectWidth = bBox.height * pattern.aspectRatio;
} else {
// Width of bBox will determine height
bBox.aspectHeight = bBox.width / pattern.aspectRatio;
}
}
// We set the width/height on internal properties to differentiate
// between the options set by a user and by this function.
pattern._width = pattern.width ||
Math.ceil(bBox.aspectWidth || bBox.width);
pattern._height = pattern.height ||
Math.ceil(bBox.aspectHeight || bBox.height);
}
// Set x/y accordingly, centering if using aspect ratio, otherwise adjusting
// so bounding box corner is 0,0 of pattern.
if (!pattern.width) {
pattern._x = pattern.x || 0;
pattern._x += bBox.x - Math.round(
bBox.aspectWidth ?
Math.abs(bBox.aspectWidth - bBox.width) / 2 :
0
);
}
if (!pattern.height) {
pattern._y = pattern.y || 0;
pattern._y += bBox.y - Math.round(
bBox.aspectHeight ?
Math.abs(bBox.aspectHeight - bBox.height) / 2 :
0
);
}
};
/**
* Add a pattern to the renderer.
*
* @private
* @function Highcharts.SVGRenderer#addPattern
*
* @param {Highcharts.PatternObject} options
* The pattern options.
*
* @return {Highcharts.SVGElement|undefined}
* The added pattern. Undefined if the pattern already exists.
*/
H.SVGRenderer.prototype.addPattern = function (options, animation) {
var pattern,
animate = H.pick(animation, true),
animationOptions = H.animObject(animate),
path,
defaultSize = 32,
width = options.width || options._width || defaultSize,
height = options.height || options._height || defaultSize,
color = options.color || '#343434',
id = options.id,
ren = this,
rect = function (fill) {
ren.rect(0, 0, width, height)
.attr({
fill: fill
})
.add(pattern);
},
attribs;
if (!id) {
this.idCounter = this.idCounter || 0;
id = 'highcharts-pattern-' + this.idCounter;
++this.idCounter;
}
// Do nothing if ID already exists
this.defIds = this.defIds || [];
if (this.defIds.indexOf(id) > -1) {
return;
}
// Store ID in list to avoid duplicates
this.defIds.push(id);
// Create pattern element
pattern = this.createElement('pattern').attr({
id: id,
patternUnits: 'userSpaceOnUse',
width: width,
height: height,
x: options._x || options.x || 0,
y: options._y || options.y || 0
}).add(this.defs);
// Set id on the SVGRenderer object
pattern.id = id;
// Use an SVG path for the pattern
if (options.path) {
path = options.path;
// The background
if (path.fill) {
rect(path.fill);
}
// The pattern
attribs = {
'd': path.d || path
};
if (!this.styledMode) {
attribs.stroke = path.stroke || color;
attribs['stroke-width'] = path.strokeWidth || 2;
}
this.createElement('path').attr(attribs).add(pattern);
pattern.color = color;
// Image pattern
} else if (options.image) {
if (animate) {
this.image(
options.image, 0, 0, width, height, function () {
// Onload
this.animate({
opacity: pick(options.opacity, 1)
}, animationOptions);
H.removeEvent(this.element, 'load');
}
).attr({ opacity: 0 }).add(pattern);
} else {
this.image(options.image, 0, 0, width, height).add(pattern);
}
}
// For non-animated patterns, set opacity now
if (!(options.image && animate) && options.opacity !== undefined) {
[].forEach.call(pattern.element.childNodes, function (child) {
child.setAttribute('opacity', options.opacity);
});
}
// Store for future reference
this.patternElements = this.patternElements || {};
this.patternElements[id] = pattern;
return pattern;
};
// Make sure we have a series color
wrap(H.Series.prototype, 'getColor', function (proceed) {
var oldColor = this.options.color;
// Temporarely remove color options to get defaults
if (oldColor && oldColor.pattern && !oldColor.pattern.color) {
delete this.options.color;
// Get default
proceed.apply(this, Array.prototype.slice.call(arguments, 1));
// Replace with old, but add default color
oldColor.pattern.color = this.color;
this.color = this.options.color = oldColor;
} else {
// We have a color, no need to do anything special
proceed.apply(this, Array.prototype.slice.call(arguments, 1));
}
});
// Calculate pattern dimensions on points that have their own pattern.
addEvent(H.Series, 'render', function () {
var isResizing = this.chart.isResizing;
if (this.isDirtyData || isResizing || !this.chart.hasRendered) {
(this.points || []).forEach(function (point) {
var colorOptions = point.options && point.options.color;
if (colorOptions && colorOptions.pattern) {
// For most points we want to recalculate the dimensions on
// render, where we have the shape args and bbox. But if we
// are resizing and don't have the shape args, defer it, since
// the bounding box is still not resized.
if (
isResizing &&
!(
point.shapeArgs &&
point.shapeArgs.width &&
point.shapeArgs.height
)
) {
colorOptions.pattern._width = 'defer';
colorOptions.pattern._height = 'defer';
} else {
point.calculatePatternDimensions(colorOptions.pattern);
}
}
});
}
});
// Merge series color options to points
addEvent(H.Point, 'afterInit', function () {
var point = this,
colorOptions = point.options.color;
// Only do this if we have defined a specific color on this point. Otherwise
// we will end up trying to re-add the series color for each point.
if (colorOptions && colorOptions.pattern) {
// Move path definition to object, allows for merge with series path
// definition
if (typeof colorOptions.pattern.path === 'string') {
colorOptions.pattern.path = {
d: colorOptions.pattern.path
};
}
// Merge with series options
point.color = point.options.color = merge(
point.series.options.color, colorOptions
);
}
});
// Add functionality to SVG renderer to handle patterns as complex colors
H.addEvent(H.SVGRenderer, 'complexColor', function (args) {
var color = args.args[0],
prop = args.args[1],
element = args.args[2],
pattern = color.pattern,
value = '#343434',
forceHashId;
// Skip and call default if there is no pattern
if (!pattern) {
return true;
}
// We have a pattern.
if (
pattern.image ||
typeof pattern.path === 'string' ||
pattern.path && pattern.path.d
) {
// Real pattern. Add it and set the color value to be a reference.
// Force Hash-based IDs for legend items, as they are drawn before
// point render, meaning they are drawn before autocalculated image
// width/heights. We don't want them to highjack the width/height for
// this ID if it is defined by users.
forceHashId = element.parentNode &&
element.parentNode.getAttribute('class');
forceHashId = forceHashId &&
forceHashId.indexOf('highcharts-legend') > -1;
// If we don't have a width/height yet, handle it. Try faking a point
// and running the algorithm again.
if (pattern._width === 'defer' || pattern._height === 'defer') {
H.Point.prototype.calculatePatternDimensions.call(
{ graphic: { element: element } }, pattern
);
}
// If we don't have an explicit ID, compute a hash from the
// definition and use that as the ID. This ensures that points with
// the same pattern definition reuse existing pattern elements by
// default. We combine two hashes, the second with an additional
// preSeed algorithm, to minimize collision probability.
if (forceHashId || !pattern.id) {
// Make a copy so we don't accidentally edit options when setting ID
pattern = merge({}, pattern);
pattern.id = 'highcharts-pattern-' + hashFromObject(pattern) +
hashFromObject(pattern, true);
}
// Add it. This function does nothing if an element with this ID
// already exists.
this.addPattern(pattern, !this.forExport && H.pick(
pattern.animation,
this.globalAnimation,
{ duration: 100 }
));
value = 'url(' + this.url + '#' + pattern.id + ')';
} else {
// Not a full pattern definition, just add color
value = pattern.color || value;
}
// Set the fill/stroke prop on the element
element.setAttribute(prop, value);
// Allow the color to be concatenated into tooltips formatters etc.
color.toString = function () {
return value;
};
// Skip default handler
return false;
});
// When animation is used, we have to recalculate pattern dimensions after
// resize, as the bounding boxes are not available until then.
H.addEvent(H.Chart, 'endResize', function () {
if (
(this.renderer.defIds || []).filter(function (id) {
return id && id.indexOf && id.indexOf('highcharts-pattern-') === 0;
}).length
) {
// We have non-default patterns to fix. Find them by looping through
// all points.
this.series.forEach(function (series) {
series.points.forEach(function (point) {
var colorOptions = point.options && point.options.color;
if (colorOptions && colorOptions.pattern) {
colorOptions.pattern._width = 'defer';
colorOptions.pattern._height = 'defer';
}
});
});
// Redraw without animation
this.redraw(false);
}
});
// Add a garbage collector to delete old patterns with autogenerated hashes that
// are no longer being referenced.
H.addEvent(H.Chart, 'redraw', function () {
var usedIds = [],
renderer = this.renderer,
// Get the autocomputed patterns - these are the ones we might delete
patterns = (renderer.defIds || []).filter(function (pattern) {
return pattern.indexOf &&
pattern.indexOf('highcharts-pattern-') === 0;
});
if (patterns.length) {
// Look through the DOM for usage of the patterns. This can be points,
// series, tooltips etc.
[].forEach.call(
this.renderTo.querySelectorAll(
'[color^="url(#"], [fill^="url(#"], [stroke^="url(#"]'
),
function (node) {
var id = node.getAttribute('fill') ||
node.getAttribute('color') ||
node.getAttribute('stroke');
if (id) {
usedIds.push(
id
.substring(id.indexOf('url(#') + 5)
.replace(')', '')
);
}
}
);
// Loop through the patterns that exist and see if they are used
patterns.forEach(function (id) {
if (usedIds.indexOf(id) === -1) {
// Remove id from used id list
H.erase(renderer.defIds, id);
// Remove pattern element
if (renderer.patternElements[id]) {
renderer.patternElements[id].destroy();
delete renderer.patternElements[id];
}
}
});
}
});
// Add the predefined patterns
H.Chart.prototype.callbacks.push(function (chart) {
var colors = H.getOptions().colors;
[
'M 0 0 L 10 10 M 9 -1 L 11 1 M -1 9 L 1 11',
'M 0 10 L 10 0 M -1 1 L 1 -1 M 9 11 L 11 9',
'M 3 0 L 3 10 M 8 0 L 8 10',
'M 0 3 L 10 3 M 0 8 L 10 8',
'M 0 3 L 5 3 L 5 0 M 5 10 L 5 7 L 10 7',
'M 3 3 L 8 3 L 8 8 L 3 8 Z',
'M 5 5 m -4 0 a 4 4 0 1 1 8 0 a 4 4 0 1 1 -8 0',
'M 10 3 L 5 3 L 5 0 M 5 10 L 5 7 L 0 7',
'M 2 5 L 5 2 L 8 5 L 5 8 Z',
'M 0 0 L 5 10 L 10 0'
].forEach(function (pattern, i) {
chart.renderer.addPattern({
id: 'highcharts-default-pattern-' + i,
path: pattern,
color: colors[i],
width: 10,
height: 10
});
});
});
@@ -0,0 +1,149 @@
/**
* (c) 2009-2019 Sebastian Bochann
*
* Price indicator for Highcharts
*
* License: www.highcharts.com/license
*/
'use strict';
import H from '../parts/Globals.js';
import '../parts/Utilities.js';
var addEvent = H.addEvent,
merge = H.merge,
isArray = H.isArray;
/**
* The line marks the last price from visible range of points.
*
* @product highstock
* @sample {highstock} stock/indicators/last-visible-price
* Last visible price
* @apioption plotOptions.series.lastVisiblePrice
*/
/**
* Enable or disable the indicator.
*
* @type {boolean}
* @product highstock
* @default true
* @apioption plotOptions.series.lastVisiblePrice.enabled
*/
/**
* Enable or disable the label.
*
* @type {boolean}
* @product highstock
* @default true
* @apioption plotOptions.series.lastVisiblePrice.label.enabled
*
*/
/**
* The line marks the last price from all points.
*
* @product highstock
* @sample {highstock} stock/indicators/last-price
* Last price
* @apioption plotOptions.series.lastPrice
*/
/**
* Enable or disable the indicator.
*
* @type {boolean}
* @product highstock
* @default true
* @apioption plotOptions.series.lastPrice.enabled
*/
/**
* The color of the line of last price.
*
* @type {string}
* @product highstock
* @default red
* @apioption plotOptions.series.lastPrice.color
*
*/
addEvent(H.Series, 'afterRender', function () {
var serie = this,
seriesOptions = serie.options,
lastVisiblePrice = seriesOptions.lastVisiblePrice,
lastPrice = seriesOptions.lastPrice;
if ((lastVisiblePrice || lastPrice) &&
seriesOptions.id !== 'highcharts-navigator-series') {
var xAxis = serie.xAxis,
yAxis = serie.yAxis,
origOptions = yAxis.crosshair,
origGraphic = yAxis.cross,
origLabel = yAxis.crossLabel,
points = serie.points,
x = serie.xData[serie.xData.length - 1],
y = serie.yData[serie.yData.length - 1],
lastPoint,
yValue,
crop;
if (lastPrice && lastPrice.enabled) {
yAxis.crosshair = yAxis.options.crosshair = seriesOptions.lastPrice;
yAxis.cross = serie.lastPrice;
yValue = isArray(y) ? y[3] : y;
yAxis.drawCrosshair(null, {
x: x,
y: yValue,
plotX: xAxis.toPixels(x, true),
plotY: yAxis.toPixels(yValue, true)
});
// Save price
if (serie.yAxis.cross) {
serie.lastPrice = serie.yAxis.cross;
serie.lastPrice.y = yValue;
}
}
if (lastVisiblePrice &&
lastVisiblePrice.enabled &&
points.length > 0
) {
crop = points[points.length - 1].x === x ? 1 : 2;
yAxis.crosshair = yAxis.options.crosshair = merge({
color: 'transparent'
}, seriesOptions.lastVisiblePrice);
yAxis.cross = serie.lastVisiblePrice;
lastPoint = points[points.length - crop];
// Save price
yAxis.drawCrosshair(null, lastPoint);
if (yAxis.cross) {
serie.lastVisiblePrice = yAxis.cross;
serie.lastVisiblePrice.y = lastPoint.y;
}
if (serie.crossLabel) {
serie.crossLabel.destroy();
}
serie.crossLabel = yAxis.crossLabel;
}
// Restore crosshair:
yAxis.crosshair = origOptions;
yAxis.cross = origGraphic;
yAxis.crossLabel = origLabel;
}
});
@@ -0,0 +1,809 @@
/* *
* Sankey diagram module
*
* (c) 2010-2019 Torstein Honsi
*
* License: www.highcharts.com/license
*/
'use strict';
import H from '../parts/Globals.js';
import '../parts/Utilities.js';
import '../parts/Options.js';
import '../mixins/nodes.js';
var defined = H.defined,
seriesType = H.seriesType,
pick = H.pick,
Point = H.Point;
/**
* @private
* @class
* @name Highcharts.seriesTypes.sankey
*
* @augments Highcharts.Series
*/
seriesType('sankey', 'column'
/**
* A sankey diagram is a type of flow diagram, in which the width of the
* link between two nodes is shown proportionally to the flow quantity.
*
* @sample highcharts/demo/sankey-diagram/
* Sankey diagram
* @sample highcharts/plotoptions/sankey-inverted/
* Inverted sankey diagram
* @sample highcharts/plotoptions/sankey-outgoing
* Sankey diagram with outgoing links
*
* @extends plotOptions.column
* @since 6.0.0
* @product highcharts
* @excluding animationLimit, boostThreshold, borderColor, borderRadius,
* borderWidth, crisp, cropThreshold, depth, edgeColor, edgeWidth,
* findNearestPointBy, grouping, groupPadding, groupZPadding,
* maxPointWidth, negativeColor, pointInterval, pointIntervalUnit,
* pointPadding, pointPlacement, pointRange, pointStart,
* pointWidth, shadow, softThreshold, stacking, threshold,
* zoneAxis, zones
* @optionparent plotOptions.sankey
*/
, {
colorByPoint: true,
/**
* Higher numbers makes the links in a sankey diagram render more curved.
* A `curveFactor` of 0 makes the lines straight.
*/
curveFactor: 0.33,
/**
* Options for the data labels appearing on top of the nodes and links. For
* sankey charts, data labels are visible for the nodes by default, but
* hidden for links. This is controlled by modifying the `nodeFormat`, and
* the `format` that applies to links and is an empty string by default.
*/
dataLabels: {
enabled: true,
backgroundColor: 'none', // enable padding
crop: false,
/**
* The
* [format string](https://www.highcharts.com/docs/chart-concepts/labels-and-string-formatting)
* specifying what to show for _nodes_ in the sankey diagram. By default
* the `nodeFormatter` returns `{point.name}`.
*
* @sample highcharts/plotoptions/sankey-link-datalabels
* Node and link data labels
* @type {string}
*/
nodeFormat: undefined,
/**
* Callback to format data labels for _nodes_ in the sankey diagram.
* The `nodeFormat` option takes precedence over the `nodeFormatter`.
*
* @type {Highcharts.FormatterCallbackFunction<object>}
* @since 6.0.2
*/
nodeFormatter: function () {
return this.point.name;
},
/**
* The
* [format string](https://www.highcharts.com/docs/chart-concepts/labels-and-string-formatting)
* specifying what to show for _links_ in the sankey diagram. Defaults
* to an empty string returned from the `formatter`, in effect disabling
* the labels.
*
* @type {string}
*/
format: undefined,
/**
* Callback to format data labels for _links_ in the sankey diagram.
* The `format` option takes precedence over the `formatter`.
*
* @type {Highcharts.FormatterCallbackFunction<Highcharts.SeriesDataLabelsFormatterContextObject>}
* @since 6.0.2
*/
formatter: function () {
return '';
},
inside: true
},
/**
* Opacity for the links between nodes in the sankey diagram.
*/
linkOpacity: 0.5,
/**
* The pixel width of each node in a sankey diagram, or the height in case
* the chart is inverted.
*/
nodeWidth: 20,
/**
* The padding between nodes in a sankey diagram, in pixels.
*/
nodePadding: 10,
showInLegend: false,
states: {
hover: {
/**
* Opacity for the links between nodes in the sankey diagram in
* hover mode.
*/
linkOpacity: 1
}
},
tooltip: {
/**
* A callback for defining the format for _nodes_ in the sankey chart's
* tooltip, as opposed to links.
*
* @type {Highcharts.FormatterCallbackFunction<object>}
* @since 6.0.2
* @apioption plotOptions.sankey.tooltip.nodeFormatter
*/
/**
* Whether the tooltip should follow the pointer or stay fixed on the
* item.
*/
followPointer: true,
headerFormat:
'<span style="font-size: 10px">{series.name}</span><br/>',
pointFormat: '{point.fromNode.name} \u2192 {point.toNode.name}: <b>{point.weight}</b><br/>',
/**
* The
* [format string](https://www.highcharts.com/docs/chart-concepts/labels-and-string-formatting)
* specifying what to show for _nodes_ in tooltip of a sankey diagram
* series, as opposed to links.
*/
nodeFormat: '{point.name}: <b>{point.sum}</b><br/>'
}
}, {
isCartesian: false,
forceDL: true,
orderNodes: true,
// Create a single node that holds information on incoming and outgoing
// links.
createNode: H.NodesMixin.createNode,
getNodePadding: function () {
return this.options.nodePadding;
},
// Create a node column.
createNodeColumn: function () {
var chart = this.chart,
column = [],
nodePadding = this.getNodePadding();
column.sum = function () {
var sum = 0;
this.forEach(function (node) {
sum += node.getSum();
});
return sum;
};
// Get the offset in pixels of a node inside the column.
column.offset = function (node, factor) {
var offset = 0;
for (var i = 0; i < column.length; i++) {
if (column[i] === node) {
return offset + (node.options.offset || 0);
}
offset += column[i].getSum() * factor + nodePadding;
}
};
// Get the column height in pixels.
column.top = function (factor) {
var height = 0;
for (var i = 0; i < column.length; i++) {
if (i > 0) {
height += nodePadding;
}
height += column[i].getSum() * factor;
}
return (chart.plotSizeY - height) / 2;
};
return column;
},
// Create node columns by analyzing the nodes and the relations between
// incoming and outgoing links.
createNodeColumns: function () {
var columns = [];
this.nodes.forEach(function (node) {
var fromColumn = -1,
i,
point;
if (!H.defined(node.options.column)) {
// No links to this node, place it left
if (node.linksTo.length === 0) {
node.column = 0;
// There are incoming links, place it to the right of the
// highest order column that links to this one.
} else {
for (i = 0; i < node.linksTo.length; i++) {
point = node.linksTo[0];
if (point.fromNode.column > fromColumn) {
fromColumn = point.fromNode.column;
}
}
node.column = fromColumn + 1;
}
}
if (!columns[node.column]) {
columns[node.column] = this.createNodeColumn();
}
columns[node.column].push(node);
}, this);
// Fill in empty columns (#8865)
for (var i = 0; i < columns.length; i++) {
if (columns[i] === undefined) {
columns[i] = this.createNodeColumn();
}
}
return columns;
},
// Return the presentational attributes.
pointAttribs: function (point, state) {
var opacity = this.options.linkOpacity,
color = point.color;
if (state) {
opacity = this.options.states[state].linkOpacity || opacity;
color = this.options.states[state].color || point.color;
}
return {
fill: point.isNode ?
color :
H.color(color).setOpacity(opacity).get()
};
},
// Extend generatePoints by adding the nodes, which are Point objects
// but pushed to the this.nodes array.
generatePoints: function () {
var nodeLookup = {},
chart = this.chart;
H.Series.prototype.generatePoints.call(this);
if (!this.nodes) {
this.nodes = []; // List of Point-like node items
}
this.colorCounter = 0;
// Reset links from previous run
this.nodes.forEach(function (node) {
node.linksFrom.length = 0;
node.linksTo.length = 0;
node.level = undefined;
});
// Create the node list and set up links
this.points.forEach(function (point) {
if (defined(point.from)) {
if (!nodeLookup[point.from]) {
nodeLookup[point.from] = this.createNode(point.from);
}
nodeLookup[point.from].linksFrom.push(point);
point.fromNode = nodeLookup[point.from];
// Point color defaults to the fromNode's color
if (chart.styledMode) {
point.colorIndex = pick(
point.options.colorIndex,
nodeLookup[point.from].colorIndex
);
} else {
point.color =
point.options.color || nodeLookup[point.from].color;
}
}
if (defined(point.to)) {
if (!nodeLookup[point.to]) {
nodeLookup[point.to] = this.createNode(point.to);
}
nodeLookup[point.to].linksTo.push(point);
point.toNode = nodeLookup[point.to];
}
// for use in formats
point.name = point.name || point.options.id;
}, this);
// Order the nodes, starting with the root node(s) (#9818)
function order(node, level) {
if (node.level === undefined) { // Prevents circular recursion
node.level = level;
node.linksFrom.forEach(function (link) {
order(link.toNode, level + 1);
});
}
}
if (this.orderNodes) {
this.nodes
// Identify the root node(s)
.filter(function (node) {
return node.linksTo.length === 0;
})
// Start by the root node(s) and recursively set the level
// on all following nodes.
.forEach(function (node) {
order(node, 0);
});
H.stableSort(this.nodes, function (a, b) {
return a.level - b.level;
});
}
},
// Destroy all nodes on setting new data
setData: function () {
if (this.nodes) {
this.nodes.forEach(function (node) {
node.destroy();
});
this.nodes.length = 0;
}
H.Series.prototype.setData.apply(this, arguments);
},
// Run translation operations for one node
translateNode: function (node, column) {
var translationFactor = this.translationFactor,
chart = this.chart,
sum = node.getSum(),
height = sum * translationFactor,
fromNodeTop = (
column.top(translationFactor) +
column.offset(node, translationFactor)
),
left = this.colDistance * node.column,
nodeLeft = chart.inverted ?
chart.plotSizeX - left :
left,
nodeWidth = this.options.nodeWidth;
node.sum = sum;
// Draw the node
node.shapeType = 'rect';
node.nodeX = nodeLeft;
node.nodeY = fromNodeTop;
if (!chart.inverted) {
node.shapeArgs = {
x: nodeLeft,
y: fromNodeTop,
width: nodeWidth,
height: height
};
} else {
node.shapeArgs = {
x: nodeLeft - nodeWidth,
y: chart.plotSizeY - fromNodeTop - height,
width: nodeWidth,
height: height
};
}
node.shapeArgs.display = node.hasShape() ? '' : 'none';
// Pass test in drawPoints
node.plotY = 1;
},
// Run translation operations for one link
translateLink: function (point) {
var fromNode = point.fromNode,
toNode = point.toNode,
chart = this.chart,
translationFactor = this.translationFactor,
linkHeight = point.weight * translationFactor,
options = this.options,
fromLinkTop = fromNode.offset(point, 'linksFrom') *
translationFactor,
curvy = (
(chart.inverted ? -this.colDistance : this.colDistance) *
options.curveFactor
),
fromY = fromNode.nodeY + fromLinkTop,
nodeLeft = fromNode.nodeX,
toColTop = this.nodeColumns[toNode.column]
.top(translationFactor),
toY = (
toColTop +
(toNode.offset(point, 'linksTo') * translationFactor) +
this.nodeColumns[toNode.column].offset(
toNode,
translationFactor
)
),
nodeW = options.nodeWidth,
right = toNode.column * this.colDistance,
outgoing = point.outgoing,
straight = right > nodeLeft;
if (chart.inverted) {
fromY = chart.plotSizeY - fromY;
toY = chart.plotSizeY - toY;
right = chart.plotSizeX - right;
nodeW = -nodeW;
linkHeight = -linkHeight;
straight = nodeLeft > right;
}
point.shapeType = 'path';
point.linkBase = [
fromY,
fromY + linkHeight,
toY,
toY + linkHeight
];
// Links going from left to right
if (straight) {
point.shapeArgs = {
d: [
'M', nodeLeft + nodeW, fromY,
'C', nodeLeft + nodeW + curvy, fromY,
right - curvy, toY,
right, toY,
'L',
right + (outgoing ? nodeW : 0),
toY + linkHeight / 2,
'L',
right,
toY + linkHeight,
'C', right - curvy, toY + linkHeight,
nodeLeft + nodeW + curvy,
fromY + linkHeight,
nodeLeft + nodeW, fromY + linkHeight,
'z'
]
};
// Experimental: Circular links pointing backwards. In
// v6.1.0 this breaks the rendering completely, so even
// this experimental rendering is an improvement. #8218.
// @todo
// - Make room for the link in the layout
// - Automatically determine if the link should go up or
// down.
} else {
var bend = 20,
vDist = chart.plotHeight - fromY - linkHeight,
x1 = right - bend - linkHeight,
x2 = right - bend,
x3 = right,
x4 = nodeLeft + nodeW,
x5 = x4 + bend,
x6 = x5 + linkHeight,
fy1 = fromY,
fy2 = fromY + linkHeight,
fy3 = fy2 + bend,
y4 = fy3 + vDist,
y5 = y4 + bend,
y6 = y5 + linkHeight,
ty1 = toY,
ty2 = ty1 + linkHeight,
ty3 = ty2 + bend,
cfy1 = fy2 - linkHeight * 0.7,
cy2 = y5 + linkHeight * 0.7,
cty1 = ty2 - linkHeight * 0.7,
cx1 = x3 - linkHeight * 0.7,
cx2 = x4 + linkHeight * 0.7;
point.shapeArgs = {
d: [
'M', x4, fy1,
'C', cx2, fy1, x6, cfy1, x6, fy3,
'L', x6, y4,
'C', x6, cy2, cx2, y6, x4, y6,
'L', x3, y6,
'C', cx1, y6, x1, cy2, x1, y4,
'L', x1, ty3,
'C', x1, cty1, cx1, ty1, x3, ty1,
'L', x3, ty2,
'C', x2, ty2, x2, ty2, x2, ty3,
'L', x2, y4,
'C', x2, y5, x2, y5, x3, y5,
'L', x4, y5,
'C', x5, y5, x5, y5, x5, y4,
'L', x5, fy3,
'C', x5, fy2, x5, fy2, x4, fy2,
'z'
]
};
}
// Place data labels in the middle
point.dlBox = {
x: nodeLeft + (right - nodeLeft + nodeW) / 2,
y: fromY + (toY - fromY) / 2,
height: linkHeight,
width: 0
};
// Pass test in drawPoints
point.y = point.plotY = 1;
if (!point.color) {
point.color = fromNode.color;
}
},
// Run pre-translation by generating the nodeColumns.
translate: function () {
if (!this.processedXData) {
this.processData();
}
this.generatePoints();
this.nodeColumns = this.createNodeColumns();
var series = this,
chart = this.chart,
options = this.options,
nodeWidth = options.nodeWidth,
nodeColumns = this.nodeColumns,
nodePadding = this.getNodePadding();
// Find out how much space is needed. Base it on the translation
// factor of the most spaceous column.
this.translationFactor = nodeColumns.reduce(
function (translationFactor, column) {
var height = chart.plotSizeY -
(column.length - 1) * nodePadding;
return Math.min(translationFactor, height / column.sum());
},
Infinity
);
this.colDistance = (chart.plotSizeX - nodeWidth) /
(nodeColumns.length - 1);
// First translate all nodes so we can use them when drawing links
nodeColumns.forEach(function (column) {
column.forEach(function (node) {
series.translateNode(node, column);
});
}, this);
// Then translate links
this.nodes.forEach(function (node) {
// Translate the links from this node
node.linksFrom.forEach(function (point) {
series.translateLink(point);
});
});
},
// Extend the render function to also render this.nodes together with
// the points.
render: function () {
var points = this.points;
this.points = this.points.concat(this.nodes);
H.seriesTypes.column.prototype.render.call(this);
this.points = points;
},
animate: H.Series.prototype.animate,
destroy: function () {
// Nodes must also be destroyed (#8682, #9300)
this.data = [].concat(this.points, this.nodes);
H.Series.prototype.destroy.call(this);
}
}, {
getClassName: function () {
return (this.isNode ? 'highcharts-node ' : 'highcharts-link ') +
Point.prototype.getClassName.call(this);
},
isValid: function () {
return this.isNode || typeof this.weight === 'number';
}
});
/**
* A `sankey` series. If the [type](#series.sankey.type) option is not
* specified, it is inherited from [chart.type](#chart.type).
*
* @extends series,plotOptions.sankey
* @excluding animationLimit, boostThreshold, borderColor, borderRadius,
* borderWidth, crisp, cropThreshold, dataParser, dataURL, depth,
* edgeColor, edgeWidth, findNearestPointBy, grouping, groupPadding,
* groupZPadding, maxPointWidth, negativeColor, pointInterval,
* pointIntervalUnit, pointPadding, pointPlacement, pointRange,
* pointStart, pointWidth, shadow, softThreshold, stacking,
* threshold, zoneAxis, zones
* @product highcharts
* @apioption series.sankey
*/
/**
* A collection of options for the individual nodes. The nodes in a sankey
* diagram are auto-generated instances of `Highcharts.Point`, but options can
* be applied here and linked by the `id`.
*
* @sample highcharts/css/sankey/
* Sankey diagram with node options
*
* @type {Array<*>}
* @product highcharts
* @apioption series.sankey.nodes
*/
/**
* The id of the auto-generated node, refering to the `from` or `to` setting of
* the link.
*
* @type {string}
* @product highcharts
* @apioption series.sankey.nodes.id
*/
/**
* The color of the auto generated node.
*
* @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
* @product highcharts
* @apioption series.sankey.nodes.color
*/
/**
* The color index of the auto generated node, especially for use in styled
* mode.
*
* @type {number}
* @product highcharts
* @apioption series.sankey.nodes.colorIndex
*/
/**
* An optional column index of where to place the node. The default behaviour is
* to place it next to the preceding node.
*
* @sample highcharts/plotoptions/sankey-node-column/
* Specified node column
*
* @type {number}
* @since 6.0.5
* @product highcharts
* @apioption series.sankey.nodes.column
*/
/**
* The name to display for the node in data labels and tooltips. Use this when
* the name is different from the `id`. Where the id must be unique for each
* node, this is not necessary for the name.
*
* @sample highcharts/css/sankey/
* Sankey diagram with node options
*
* @type {string}
* @product highcharts
* @apioption series.sankey.nodes.name
*/
/**
* The vertical offset of a node in terms of weight. Positive values shift the
* node downwards, negative shift it upwards.
*
* @sample highcharts/plotoptions/sankey-node-column/
* Specified node offset
*
* @type {number}
* @default 0
* @since 6.0.5
* @product highcharts
* @apioption series.sankey.nodes.offset
*/
/**
* An array of data points for the series. For the `sankey` series type,
* points can be given in the following way:
*
* An array of objects with named values. The following snippet shows only a
* few settings, see the complete options set below. If the total number of data
* points exceeds the series' [turboThreshold](#series.area.turboThreshold),
* this option is not available.
*
* ```js
* data: [{
* from: 'Category1',
* to: 'Category2',
* weight: 2
* }, {
* from: 'Category1',
* to: 'Category3',
* weight: 5
* }]
* ```
*
* @sample {highcharts} highcharts/series/data-array-of-objects/
* Config objects
*
* @type {Array<*>}
* @extends series.line.data
* @excluding drilldown, marker, x, y
* @product highcharts
* @apioption series.sankey.data
*/
/**
* The color for the individual _link_. By default, the link color is the same
* as the node it extends from. The `series.fillOpacity` option also applies to
* the points, so when setting a specific link color, consider setting the
* `fillOpacity` to 1.
*
* @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
* @product highcharts
* @apioption series.sankey.data.color
*/
/**
* The node that the link runs from.
*
* @type {string}
* @product highcharts
* @apioption series.sankey.data.from
*/
/**
* The node that the link runs to.
*
* @type {string}
* @product highcharts
* @apioption series.sankey.data.to
*/
/**
* Whether the link goes out of the system.
*
* @sample highcharts/plotoptions/sankey-outgoing
* Sankey chart with outgoing links
*
* @type {boolean}
* @default false
* @product highcharts
* @apioption series.sankey.data.outgoing
*/
/**
* The weight of the link.
*
* @type {number}
* @product highcharts
* @apioption series.sankey.data.weight
*/
@@ -0,0 +1,954 @@
/**
* Accessibility module - Screen Reader support
*
* (c) 2010-2017 Highsoft AS
* Author: Oystein Moseng
*
* License: www.highcharts.com/license
*/
'use strict';
import H from '../parts/Globals.js';
import '../parts/Utilities.js';
import '../parts/Chart.js';
import '../parts/Series.js';
import '../parts/Point.js';
var win = H.win,
doc = win.document,
erase = H.erase,
addEvent = H.addEvent,
merge = H.merge,
// CSS style to hide element from visual users while still exposing it to
// screen readers
hiddenStyle = {
position: 'absolute',
top: '-999em',
width: '1px',
height: '1px',
overflow: 'hidden'
};
// If a point has one of the special keys defined, we expose all keys to the
// screen reader.
H.Series.prototype.commonKeys = ['name', 'id', 'category', 'x', 'value', 'y'];
H.Series.prototype.specialKeys = [
'z', 'open', 'high', 'q3', 'median', 'q1', 'low', 'close'
];
if (H.seriesTypes.pie) {
// A pie is always simple. Don't quote me on that.
H.seriesTypes.pie.prototype.specialKeys = [];
}
/**
* HTML encode some characters vulnerable for XSS.
*
* @private
* @function htmlencode
*
* @param {string} html
* The input string.
*
* @return {string}
* The excaped string.
*/
function htmlencode(html) {
return html
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;')
.replace(/\//g, '&#x2F;');
}
/**
* Strip HTML tags away from a string. Used for aria-label attributes, painting
* on a canvas will fail if the text contains tags.
*
* @private
* @function stripTags
*
* @param {string} s
* The input string.
*
* @return {string}
* The filtered string.
*/
function stripTags(s) {
return typeof s === 'string' ? s.replace(/<\/?[^>]+(>|$)/g, '') : s;
}
// Accessibility options
H.setOptions({
/**
* Options for configuring accessibility for the chart. Requires the
* [accessibility module](https://code.highcharts.com/modules/accessibility.js)
* to be loaded. For a description of the module and information
* on its features, see
* [Highcharts Accessibility](http://www.highcharts.com/docs/chart-concepts/accessibility).
*
* @since 5.0.0
* @optionparent accessibility
*/
accessibility: {
/**
* Whether or not to add series descriptions to charts with a single
* series.
*
* @type {boolean}
* @default false
* @since 5.0.0
* @apioption accessibility.describeSingleSeries
*/
/**
* Function to run upon clicking the "View as Data Table" link in the
* screen reader region.
*
* By default Highcharts will insert and set focus to a data table
* representation of the chart.
*
* @type {Function}
* @since 5.0.0
* @apioption accessibility.onTableAnchorClick
*/
/**
* Date format to use for points on datetime axes when describing them
* to screen reader users.
*
* Defaults to the same format as in tooltip.
*
* For an overview of the replacement codes, see
* [dateFormat](/class-reference/Highcharts#dateFormat).
*
* @see [pointDateFormatter](#accessibility.pointDateFormatter)
*
* @type {string}
* @since 5.0.0
* @apioption accessibility.pointDateFormat
*/
/**
* Formatter function to determine the date/time format used with
* points on datetime axes when describing them to screen reader users.
* Receives one argument, `point`, referring to the point to describe.
* Should return a date format string compatible with
* [dateFormat](/class-reference/Highcharts#dateFormat).
*
* @see [pointDateFormat](#accessibility.pointDateFormat)
*
* @type {Function}
* @since 5.0.0
* @apioption accessibility.pointDateFormatter
*/
/**
* Formatter function to use instead of the default for point
* descriptions.
* Receives one argument, `point`, referring to the point to describe.
* Should return a String with the description of the point for a screen
* reader user.
*
* @see [point.description](#series.line.data.description)
*
* @type {Function}
* @since 5.0.0
* @apioption accessibility.pointDescriptionFormatter
*/
/**
* Formatter function to use instead of the default for series
* descriptions. Receives one argument, `series`, referring to the
* series to describe. Should return a String with the description of
* the series for a screen reader user.
*
* @see [series.description](#plotOptions.series.description)
*
* @type {Function}
* @since 5.0.0
* @apioption accessibility.seriesDescriptionFormatter
*/
/**
* Enable accessibility features for the chart.
*
* @since 5.0.0
*/
enabled: true,
/**
* When a series contains more points than this, we no longer expose
* information about individual points to screen readers.
*
* Set to `false` to disable.
*
* @type {false|number}
* @since 5.0.0
*/
pointDescriptionThreshold: false, // set to false to disable
/**
* A formatter function to create the HTML contents of the hidden screen
* reader information region. Receives one argument, `chart`, referring
* to the chart object. Should return a String with the HTML content
* of the region.
*
* The link to view the chart as a data table will be added
* automatically after the custom HTML content.
*
* @type {Function}
* @default undefined
* @since 5.0.0
*/
screenReaderSectionFormatter: function (chart) {
var options = chart.options,
chartTypes = chart.types || [],
formatContext = {
chart: chart,
numSeries: chart.series && chart.series.length
},
// Build axis info - but not for pies and maps. Consider not
// adding for certain other types as well (funnel, pyramid?)
axesDesc = (
chartTypes.length === 1 && chartTypes[0] === 'pie' ||
chartTypes[0] === 'map'
) && {} || chart.getAxesDescription();
return '<div>' + chart.langFormat(
'accessibility.navigationHint', formatContext
) + '</div><h3>' +
(
options.title.text ?
htmlencode(options.title.text) :
chart.langFormat(
'accessibility.defaultChartTitle', formatContext
)
) +
(
options.subtitle && options.subtitle.text ?
'. ' + htmlencode(options.subtitle.text) :
''
) +
'</h3>' + (
options.chart.description ? (
'<h4>' + chart.langFormat(
'accessibility.longDescriptionHeading',
formatContext
) +
'</h4><div>' + options.chart.description + '</div>'
) : ''
) + '<h4>' + chart.langFormat(
'accessibility.structureHeading', formatContext
) + '</h4><div>' +
(
options.chart.typeDescription ||
chart.getTypeDescription()
) + '</div>' +
(axesDesc.xAxis ? (
'<div>' + axesDesc.xAxis + '</div>'
) : '') +
(axesDesc.yAxis ? (
'<div>' + axesDesc.yAxis + '</div>'
) : '');
}
}
});
/**
* A text description of the chart.
*
* If the Accessibility module is loaded, this is included by default
* as a long description of the chart and its contents in the hidden
* screen reader information region.
*
* @see [typeDescription](#chart.typeDescription)
*
* @type {string}
* @since 5.0.0
* @apioption chart.description
*/
/**
* A text description of the chart type.
*
* If the Accessibility module is loaded, this will be included in the
* description of the chart in the screen reader information region.
*
*
* Highcharts will by default attempt to guess the chart type, but for
* more complex charts it is recommended to specify this property for
* clarity.
*
* @type {string}
* @since 5.0.0
* @apioption chart.typeDescription
*/
/**
* Utility function. Reverses child nodes of a DOM element.
*
* @private
* @function reverseChildNodes
*
* @param {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement} node
*/
function reverseChildNodes(node) {
var i = node.childNodes.length;
while (i--) {
node.appendChild(node.childNodes[i]);
}
}
// Whenever drawing series, put info on DOM elements
H.addEvent(H.Series, 'afterRender', function () {
if (this.chart.options.accessibility.enabled) {
this.setA11yDescription();
}
});
/**
* Put accessible info on series and points of a series.
*
* @private
* @function Highcharts.Series#setA11yDescription
*/
H.Series.prototype.setA11yDescription = function () {
var a11yOptions = this.chart.options.accessibility,
firstPointEl = (
this.points &&
this.points.length &&
this.points[0].graphic &&
this.points[0].graphic.element
),
seriesEl = (
firstPointEl &&
firstPointEl.parentNode || this.graph &&
this.graph.element || this.group &&
this.group.element
); // Could be tracker series depending on series type
if (seriesEl) {
// For some series types the order of elements do not match the order of
// points in series. In that case we have to reverse them in order for
// AT to read them out in an understandable order
if (seriesEl.lastChild === firstPointEl) {
reverseChildNodes(seriesEl);
}
// Make individual point elements accessible if possible. Note: If
// markers are disabled there might not be any elements there to make
// accessible.
if (
this.points && (
this.points.length < a11yOptions.pointDescriptionThreshold ||
a11yOptions.pointDescriptionThreshold === false
)
) {
this.points.forEach(function (point) {
if (point.graphic) {
point.graphic.element.setAttribute('role', 'img');
point.graphic.element.setAttribute('tabindex', '-1');
point.graphic.element.setAttribute('aria-label', stripTags(
point.series.options.pointDescriptionFormatter &&
point.series.options.pointDescriptionFormatter(point) ||
a11yOptions.pointDescriptionFormatter &&
a11yOptions.pointDescriptionFormatter(point) ||
point.buildPointInfoString()
));
}
});
}
// Make series element accessible
if (this.chart.series.length > 1 || a11yOptions.describeSingleSeries) {
seriesEl.setAttribute(
'role',
this.options.exposeElementToA11y ? 'img' : 'region'
);
seriesEl.setAttribute('tabindex', '-1');
seriesEl.setAttribute(
'aria-label',
stripTags(
a11yOptions.seriesDescriptionFormatter &&
a11yOptions.seriesDescriptionFormatter(this) ||
this.buildSeriesInfoString()
)
);
}
}
};
/**
* Return string with information about series.
*
* @private
* @function Highcharts.Series#buildSeriesInfoString
*
* @return {string}
*/
H.Series.prototype.buildSeriesInfoString = function () {
var chart = this.chart,
desc = this.description || this.options.description,
description = desc && chart.langFormat(
'accessibility.series.description', {
description: desc,
series: this
}
),
xAxisInfo = chart.langFormat(
'accessibility.series.xAxisDescription',
{
name: this.xAxis && this.xAxis.getDescription(),
series: this
}
),
yAxisInfo = chart.langFormat(
'accessibility.series.yAxisDescription',
{
name: this.yAxis && this.yAxis.getDescription(),
series: this
}
),
summaryContext = {
name: this.name || '',
ix: this.index + 1,
numSeries: chart.series.length,
numPoints: this.points.length,
series: this
},
combination = chart.types.length === 1 ? '' : 'Combination',
summary = chart.langFormat(
'accessibility.series.summary.' + this.type + combination,
summaryContext
) || chart.langFormat(
'accessibility.series.summary.default' + combination,
summaryContext
);
return summary + (description ? ' ' + description : '') + (
chart.yAxis.length > 1 && this.yAxis ?
' ' + yAxisInfo : ''
) + (
chart.xAxis.length > 1 && this.xAxis ?
' ' + xAxisInfo : ''
);
};
/**
* Return string with information about point.
*
* @private
* @function Highcharts.Point#buildPointInfoString
*
* @return {string}
*/
H.Point.prototype.buildPointInfoString = function () {
var point = this,
series = point.series,
chart = series.chart,
a11yOptions = chart.options.accessibility,
infoString = '',
dateTimePoint = series.xAxis && series.xAxis.isDatetimeAxis,
timeDesc =
dateTimePoint &&
chart.time.dateFormat(
a11yOptions.pointDateFormatter &&
a11yOptions.pointDateFormatter(point) ||
a11yOptions.pointDateFormat ||
H.Tooltip.prototype.getXDateFormat.call(
{
getDateFormat: H.Tooltip.prototype.getDateFormat,
chart: chart
},
point,
chart.options.tooltip,
series.xAxis
),
point.x
),
hasSpecialKey = H.find(series.specialKeys, function (key) {
return point[key] !== undefined;
});
// If the point has one of the less common properties defined, display all
// that are defined
if (hasSpecialKey) {
if (dateTimePoint) {
infoString = timeDesc;
}
series.commonKeys.concat(series.specialKeys).forEach(function (key) {
if (point[key] !== undefined && !(dateTimePoint && key === 'x')) {
infoString += (infoString ? '. ' : '') +
key + ', ' +
point[key];
}
});
} else {
// Pick and choose properties for a succint label
infoString =
(
this.name ||
timeDesc ||
this.category ||
this.id ||
'x, ' + this.x
) + ', ' +
(this.value !== undefined ? this.value : this.y);
}
return (this.index + 1) + '. ' + infoString + '.' +
(this.description ? ' ' + this.description : '') +
(chart.series.length > 1 && series.name ? ' ' + series.name : '');
};
/**
* Get descriptive label for axis.
*
* @private
* @function Highcharts.Axis#getDescription
*
* @return {string}
*/
H.Axis.prototype.getDescription = function () {
return (
this.userOptions && this.userOptions.description ||
this.axisTitle && this.axisTitle.textStr ||
this.options.id ||
this.categories && 'categories' ||
this.isDatetimeAxis && 'Time' ||
'values'
);
};
// Whenever adding or removing series, keep track of types present in chart
addEvent(H.Series, 'afterInit', function () {
var chart = this.chart;
if (chart.options.accessibility.enabled) {
chart.types = chart.types || [];
// Add type to list if does not exist
if (chart.types.indexOf(this.type) < 0) {
chart.types.push(this.type);
}
}
});
addEvent(H.Series, 'remove', function () {
var chart = this.chart,
removedSeries = this,
hasType = false;
// Check if any of the other series have the same type as this one.
// Otherwise remove it from the list.
chart.series.forEach(function (s) {
if (
s !== removedSeries &&
chart.types.indexOf(removedSeries.type) < 0
) {
hasType = true;
}
});
if (!hasType) {
erase(chart.types, removedSeries.type);
}
});
/**
* Return simplified description of chart type. Some types will not be familiar
* to most screen reader users, but in those cases we try to add a description
* of the type.
*
* @private
* @function Highcharts.Chart#getTypeDescription
*
* @return {string}
*/
H.Chart.prototype.getTypeDescription = function () {
var firstType = this.types && this.types[0],
firstSeries = this.series && this.series[0] || {},
mapTitle = firstSeries.mapTitle,
typeDesc = this.langFormat(
'accessibility.seriesTypeDescriptions.' + firstType,
{ chart: this }
),
formatContext = {
numSeries: this.series.length,
numPoints: firstSeries.points && firstSeries.points.length,
chart: this,
mapTitle: mapTitle
},
multi = this.series && this.series.length === 1 ? 'Single' : 'Multiple';
if (!firstType) {
return this.langFormat(
'accessibility.chartTypes.emptyChart', formatContext
);
}
if (firstType === 'map') {
return mapTitle ?
this.langFormat(
'accessibility.chartTypes.mapTypeDescription',
formatContext
) :
this.langFormat(
'accessibility.chartTypes.unknownMap',
formatContext
);
}
if (this.types.length > 1) {
return this.langFormat(
'accessibility.chartTypes.combinationChart', formatContext
);
}
return (
this.langFormat(
'accessibility.chartTypes.' + firstType + multi,
formatContext
) ||
this.langFormat(
'accessibility.chartTypes.default' + multi,
formatContext
)
) +
(typeDesc ? ' ' + typeDesc : '');
};
/**
* Return object with text description of each of the chart's axes.
*
* @private
* @function Highcharts.Chart#getAxesDescription
*
* @return {*}
*/
H.Chart.prototype.getAxesDescription = function () {
var numXAxes = this.xAxis.length,
numYAxes = this.yAxis.length,
desc = {};
if (numXAxes) {
desc.xAxis = this.langFormat(
'accessibility.axis.xAxisDescription' + (
numXAxes > 1 ? 'Plural' : 'Singular'
),
{
chart: this,
names: this.xAxis.map(function (axis) {
return axis.getDescription();
}),
numAxes: numXAxes
}
);
}
if (numYAxes) {
desc.yAxis = this.langFormat(
'accessibility.axis.yAxisDescription' + (
numYAxes > 1 ? 'Plural' : 'Singular'
),
{
chart: this,
names: this.yAxis.map(function (axis) {
return axis.getDescription();
}),
numAxes: numYAxes
}
);
}
return desc;
};
/**
* Set a11y attribs on exporting menu.
*
* @private
* @function Highcharts.Chart#addAccessibleContextMenuAttribs
*/
H.Chart.prototype.addAccessibleContextMenuAttribs = function () {
var exportList = this.exportDivElements;
if (exportList) {
// Set tabindex on the menu items to allow focusing by script
// Set role to give screen readers a chance to pick up the contents
exportList.forEach(function (item) {
if (item.tagName === 'DIV' &&
!(item.children && item.children.length)) {
item.setAttribute('role', 'menuitem');
item.setAttribute('tabindex', -1);
}
});
// Set accessibility properties on parent div
exportList[0].parentNode.setAttribute('role', 'menu');
exportList[0].parentNode.setAttribute(
'aria-label',
this.langFormat(
'accessibility.exporting.chartMenuLabel', { chart: this }
)
);
}
};
/**
* Add screen reader region to chart. tableId is the HTML id of the table to
* focus when clicking the table anchor in the screen reader region.
*
* @private
* @function Highcharts.Chart#addScreenReaderRegion
*
* @param {string} id
*
* @param {string} tableId
*/
H.Chart.prototype.addScreenReaderRegion = function (id, tableId) {
var chart = this,
hiddenSection = chart.screenReaderRegion = doc.createElement('div'),
tableShortcut = doc.createElement('h4'),
tableShortcutAnchor = doc.createElement('a'),
chartHeading = chart.screenReaderHeading = doc.createElement('h4');
hiddenSection.setAttribute('id', id);
hiddenSection.setAttribute('role', 'region');
hiddenSection.setAttribute(
'aria-label',
chart.langFormat(
'accessibility.screenReaderRegionLabel', { chart: this }
)
);
hiddenSection.innerHTML = chart.options.accessibility
.screenReaderSectionFormatter(chart);
// Add shortcut to data table if export-data is loaded
if (chart.getCSV) {
tableShortcutAnchor.innerHTML = chart.langFormat(
'accessibility.viewAsDataTable', { chart: chart }
);
tableShortcutAnchor.href = '#' + tableId;
// Make this unreachable by user tabbing
tableShortcutAnchor.setAttribute('tabindex', '-1');
tableShortcutAnchor.onclick =
chart.options.accessibility.onTableAnchorClick || function () {
chart.viewData();
doc.getElementById(tableId).focus();
};
tableShortcut.appendChild(tableShortcutAnchor);
hiddenSection.appendChild(tableShortcut);
}
// Note: JAWS seems to refuse to read aria-label on the container, so add an
// h4 element as title for the chart.
chartHeading.innerHTML = chart.langFormat(
'accessibility.chartHeading', { chart: chart }
);
chart.renderTo.insertBefore(chartHeading, chart.renderTo.firstChild);
chart.renderTo.insertBefore(hiddenSection, chart.renderTo.firstChild);
// Hide the section and the chart heading
merge(true, chartHeading.style, hiddenStyle);
merge(true, hiddenSection.style, hiddenStyle);
};
// Add ARIA to legend
addEvent(H.Legend, 'afterRender', function () {
var group = this.group,
items = this.allItems,
chart = this.chart;
if (group && items && items.length) {
group.attr({
role: 'region',
'aria-label': chart.langFormat('accessibility.legendLabel')
});
if (this.box) {
this.box.attr('aria-hidden', 'true');
}
items.forEach(function (item) {
var itemGroup = item.legendGroup,
text = item.legendItem,
visible = item.visible,
label = chart.langFormat(
'accessibility.legendItem',
{
chart: chart,
itemName: stripTags(item.name)
}
);
if (itemGroup && text) {
itemGroup.attr({
role: 'button',
'aria-pressed': visible ? 'false' : 'true'
});
if (label) {
itemGroup.attr('aria-label', label);
}
text.attr('aria-hidden', 'false');
}
});
}
});
// Handle show/hide series/points
addEvent(H.Legend, 'afterColorizeItem', function (e) {
var legendGroup = e.item && e.item.legendGroup,
pressed = e.visible ? 'false' : 'true';
if (legendGroup) {
legendGroup.attr('aria-pressed', pressed);
if (legendGroup.div) {
legendGroup.div.setAttribute('aria-pressed', pressed);
}
}
});
// Make chart container accessible, and wrap table functionality.
H.Chart.prototype.callbacks.push(function (chart) {
var options = chart.options,
a11yOptions = options.accessibility;
if (!a11yOptions.enabled) {
return;
}
var titleElement,
descElement = chart.container.getElementsByTagName('desc')[0],
textElements = chart.container.getElementsByTagName('text'),
titleId = 'highcharts-title-' + chart.index,
tableId = 'highcharts-data-table-' + chart.index,
hiddenSectionId = 'highcharts-information-region-' + chart.index,
chartTitle = options.title.text || chart.langFormat(
'accessibility.defaultChartTitle', { chart: chart }
),
svgContainerTitle = stripTags(chart.langFormat(
'accessibility.svgContainerTitle', {
chartTitle: chartTitle
}
));
// Add SVG title tag if it is set
if (svgContainerTitle.length) {
titleElement = doc.createElementNS(
'http://www.w3.org/2000/svg',
'title'
);
titleElement.textContent = svgContainerTitle;
titleElement.id = titleId;
descElement.parentNode.insertBefore(titleElement, descElement);
}
chart.renderTo.setAttribute('role', 'region');
chart.renderTo.setAttribute(
'aria-label',
chart.langFormat(
'accessibility.chartContainerLabel',
{
title: stripTags(chartTitle),
chart: chart
}
)
);
// Set screen reader properties on export menu
if (
chart.exportSVGElements &&
chart.exportSVGElements[0] &&
chart.exportSVGElements[0].element
) {
// Set event handler on button
var button = chart.exportSVGElements[0].element,
oldExportCallback = button.onclick;
button.onclick = function () {
oldExportCallback.apply(
this,
Array.prototype.slice.call(arguments)
);
chart.addAccessibleContextMenuAttribs();
chart.highlightExportItem(0);
};
// Set props on button
button.setAttribute('role', 'button');
button.setAttribute(
'aria-label',
chart.langFormat(
'accessibility.exporting.menuButtonLabel', { chart: chart }
)
);
// Set props on group
chart.exportingGroup.element.setAttribute('role', 'region');
chart.exportingGroup.element.setAttribute(
'aria-label',
chart.langFormat(
'accessibility.exporting.exportRegionLabel', { chart: chart }
)
);
}
// Set screen reader properties on input boxes for range selector. We need
// to do this regardless of whether or not these are visible, as they are
// by default part of the page's tabindex unless we set them to -1.
if (chart.rangeSelector) {
['minInput', 'maxInput'].forEach(function (key, i) {
if (chart.rangeSelector[key]) {
chart.rangeSelector[key].setAttribute('tabindex', '-1');
chart.rangeSelector[key].setAttribute('role', 'textbox');
chart.rangeSelector[key].setAttribute(
'aria-label',
chart.langFormat(
'accessibility.rangeSelector' +
(i ? 'MaxInput' : 'MinInput'), { chart: chart }
)
);
}
});
}
// Hide text elements from screen readers
[].forEach.call(textElements, function (el) {
if (el.getAttribute('aria-hidden') !== 'false') {
el.setAttribute('aria-hidden', 'true');
}
});
// Add top-secret screen reader region
chart.addScreenReaderRegion(hiddenSectionId, tableId);
// Add ID and summary attr to table HTML
addEvent(chart, 'afterGetTable', function (e) {
e.html = e.html
.replace(
'<table ',
'<table summary="' + chart.langFormat(
'accessibility.tableSummary', { chart: chart }
) + '"'
);
});
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,486 @@
/* *
* Solid angular gauge module
*
* (c) 2010-2019 Torstein Honsi
*
* License: www.highcharts.com/license
*/
/**
* Additional options, depending on the actual symbol drawn.
*
* @interface Highcharts.SymbolOptionsObject
*//**
* Whether to draw rounded edges.
* @name Highcharts.SymbolOptionsObject#rounded
* @type {boolean|undefined}
*/
'use strict';
import H from '../parts/Globals.js';
import '../parts/Utilities.js';
import '../parts/Options.js';
import '../parts-more/GaugeSeries.js';
var pInt = H.pInt,
pick = H.pick,
isNumber = H.isNumber,
wrap = H.wrap,
Renderer = H.Renderer,
colorAxisMethods;
/**
* Symbol definition of an arc with round edges.
*
* @private
* @function Highcharts.Renderer#symbols.arc
*
* @param {number} x
* The X coordinate for the top left position.
*
* @param {number} y
* The Y coordinate for the top left position.
*
* @param {number} w
* The pixel width.
*
* @param {number} h
* The pixel height.
*
* @param {Highcharts.SymbolOptionsObject} [options]
* Additional options, depending on the actual symbol drawn.
*
* @return {Highcharts.SVGPathArray}
* Path of the created arc.
*/
wrap(
Renderer.prototype.symbols,
'arc',
function (proceed, x, y, w, h, options) {
var arc = proceed,
path = arc(x, y, w, h, options);
if (options.rounded) {
var r = options.r || w,
smallR = (r - options.innerR) / 2,
x1 = path[1],
y1 = path[2],
x2 = path[12],
y2 = path[13],
roundStart = ['A', smallR, smallR, 0, 1, 1, x1, y1],
roundEnd = ['A', smallR, smallR, 0, 1, 1, x2, y2];
// Insert rounded edge on end, and remove line.
path.splice.apply(path, [path.length - 1, 0].concat(roundStart));
// Insert rounded edge on end, and remove line.
path.splice.apply(path, [11, 3].concat(roundEnd));
}
return path;
}
);
// These methods are defined in the ColorAxis object, and copied here.
// If we implement an AMD system we should make ColorAxis a dependency.
colorAxisMethods = {
initDataClasses: function (userOptions) {
var chart = this.chart,
dataClasses,
colorCounter = 0,
options = this.options;
this.dataClasses = dataClasses = [];
userOptions.dataClasses.forEach(function (dataClass, i) {
var colors;
dataClass = H.merge(dataClass);
dataClasses.push(dataClass);
if (!dataClass.color) {
if (options.dataClassColor === 'category') {
colors = chart.options.colors;
dataClass.color = colors[colorCounter++];
// loop back to zero
if (colorCounter === colors.length) {
colorCounter = 0;
}
} else {
dataClass.color = H.color(options.minColor).tweenTo(
H.color(options.maxColor),
i / (userOptions.dataClasses.length - 1)
);
}
}
});
},
initStops: function (userOptions) {
this.stops = userOptions.stops || [
[0, this.options.minColor],
[1, this.options.maxColor]
];
this.stops.forEach(function (stop) {
stop.color = H.color(stop[1]);
});
},
// Translate from a value to a color
toColor: function (value, point) {
var pos,
stops = this.stops,
from,
to,
color,
dataClasses = this.dataClasses,
dataClass,
i;
if (dataClasses) {
i = dataClasses.length;
while (i--) {
dataClass = dataClasses[i];
from = dataClass.from;
to = dataClass.to;
if (
(from === undefined || value >= from) &&
(to === undefined || value <= to)
) {
color = dataClass.color;
if (point) {
point.dataClass = i;
}
break;
}
}
} else {
if (this.isLog) {
value = this.val2lin(value);
}
pos = 1 - ((this.max - value) / (this.max - this.min));
i = stops.length;
while (i--) {
if (pos > stops[i][0]) {
break;
}
}
from = stops[i] || stops[i + 1];
to = stops[i + 1] || from;
// The position within the gradient
pos = 1 - (to[0] - pos) / ((to[0] - from[0]) || 1);
color = from.color.tweenTo(
to.color,
pos
);
}
return color;
}
};
/**
* A solid gauge is a circular gauge where the value is indicated by a filled
* arc, and the color of the arc may variate with the value.
*
* @sample highcharts/demo/gauge-solid/
* Solid gauges
*
* @extends plotOptions.gauge
* @excluding dial, pivot, wrap
* @product highcharts
* @optionparent plotOptions.solidgauge
*/
var solidGaugeOptions = {
/**
* Whether the strokes of the solid gauge should be `round` or `square`.
*
* @sample {highcharts} highcharts/demo/gauge-activity/
* Rounded gauge
*
* @type {string}
* @default round
* @since 4.2.2
* @product highcharts
* @validvalue ["square", "round"]
* @apioption plotOptions.solidgauge.linecap
*/
/**
* Allow the gauge to overshoot the end of the perimeter axis by this
* many degrees. Say if the gauge axis goes from 0 to 60, a value of
* 100, or 1000, will show 5 degrees beyond the end of the axis when this
* option is set to 5.
*
* @type {number}
* @default 0
* @since 3.0.10
* @product highcharts
* @apioption plotOptions.solidgauge.overshoot
*/
/**
* Wether to draw rounded edges on the gauge.
*
* @sample {highcharts} highcharts/demo/gauge-activity/
* Activity Gauge
*
* @type {boolean}
* @default false
* @since 5.0.8
* @product highcharts
* @apioption plotOptions.solidgauge.rounded
*/
/**
* The threshold or base level for the gauge.
*
* @sample {highcharts} highcharts/plotoptions/solidgauge-threshold/
* Zero threshold with negative and positive values
*
* @type {number}
* @since 5.0.3
* @product highcharts
* @apioption plotOptions.solidgauge.threshold
*/
/**
* Whether to give each point an individual color.
*/
colorByPoint: true
};
// The solidgauge series type
H.seriesType('solidgauge', 'gauge', solidGaugeOptions, {
// Extend the translate function to extend the Y axis with the necessary
// decoration (#5895).
translate: function () {
var axis = this.yAxis;
H.extend(axis, colorAxisMethods);
// Prepare data classes
if (!axis.dataClasses && axis.options.dataClasses) {
axis.initDataClasses(axis.options);
}
axis.initStops(axis.options);
// Generate points and inherit data label position
H.seriesTypes.gauge.prototype.translate.call(this);
},
// Draw the points where each point is one needle.
drawPoints: function () {
var series = this,
yAxis = series.yAxis,
center = yAxis.center,
options = series.options,
renderer = series.chart.renderer,
overshoot = options.overshoot,
overshootVal = isNumber(overshoot) ? overshoot / 180 * Math.PI : 0,
thresholdAngleRad;
// Handle the threshold option
if (isNumber(options.threshold)) {
thresholdAngleRad = yAxis.startAngleRad + yAxis.translate(
options.threshold,
null,
null,
null,
true
);
}
this.thresholdAngleRad = pick(thresholdAngleRad, yAxis.startAngleRad);
series.points.forEach(function (point) {
var graphic = point.graphic,
rotation = yAxis.startAngleRad +
yAxis.translate(point.y, null, null, null, true),
radius = (
pInt(
pick(point.options.radius, options.radius, 100)
) * center[2]
) / 200,
innerRadius = (
pInt(
pick(point.options.innerRadius, options.innerRadius, 60)
) * center[2]
) / 200,
shapeArgs,
d,
toColor = yAxis.toColor(point.y, point),
axisMinAngle = Math.min(yAxis.startAngleRad, yAxis.endAngleRad),
axisMaxAngle = Math.max(yAxis.startAngleRad, yAxis.endAngleRad),
minAngle,
maxAngle;
if (toColor === 'none') { // #3708
toColor = point.color || series.color || 'none';
}
if (toColor !== 'none') {
point.color = toColor;
}
// Handle overshoot and clipping to axis max/min
rotation = Math.max(
axisMinAngle - overshootVal,
Math.min(axisMaxAngle + overshootVal, rotation)
);
// Handle the wrap option
if (options.wrap === false) {
rotation = Math.max(
axisMinAngle,
Math.min(axisMaxAngle, rotation)
);
}
minAngle = Math.min(rotation, series.thresholdAngleRad);
maxAngle = Math.max(rotation, series.thresholdAngleRad);
if (maxAngle - minAngle > 2 * Math.PI) {
maxAngle = minAngle + 2 * Math.PI;
}
point.shapeArgs = shapeArgs = {
x: center[0],
y: center[1],
r: radius,
innerR: innerRadius,
start: minAngle,
end: maxAngle,
rounded: options.rounded
};
point.startR = radius; // For PieSeries.animate
if (graphic) {
d = shapeArgs.d;
graphic.animate(H.extend({ fill: toColor }, shapeArgs));
if (d) {
shapeArgs.d = d; // animate alters it
}
} else {
point.graphic = graphic = renderer.arc(shapeArgs)
.attr({
fill: toColor,
'sweep-flag': 0
})
.add(series.group);
if (!series.chart.styledMode) {
if (options.linecap !== 'square') {
graphic.attr({
'stroke-linecap': 'round',
'stroke-linejoin': 'round'
});
}
graphic.attr({
stroke: options.borderColor || 'none',
'stroke-width': options.borderWidth || 0
});
}
}
if (graphic) {
graphic.addClass(point.getClassName(), true);
}
});
},
// Extend the pie slice animation by animating from start angle and up.
animate: function (init) {
if (!init) {
this.startAngleRad = this.thresholdAngleRad;
H.seriesTypes.pie.prototype.animate.call(this, init);
}
}
});
/**
* A `solidgauge` series. If the [type](#series.solidgauge.type) option is not
* specified, it is inherited from [chart.type](#chart.type).
*
*
* @extends series,plotOptions.solidgauge
* @excluding animationLimit, boostThreshold, connectEnds, connectNulls,
* cropThreshold, dashStyle, dataParser, dataURL, dial,
* findNearestPointBy, getExtremesFromAll, marker, negativeColor,
* pointPlacement, pivot, shadow, softThreshold, stack, stacking,
* states, step, threshold, turboThreshold, wrap, zoneAxis, zones
* @product highcharts
* @apioption series.solidgauge
*/
/**
* An array of data points for the series. For the `solidgauge` series
* type, points can be given in the following ways:
*
* 1. An array of numerical values. In this case, the numerical values will be
* interpreted as `y` options. Example:
* ```js
* data: [0, 5, 3, 5]
* ```
*
* 2. An array of objects with named values. The following snippet shows only a
* few settings, see the complete options set below. If the total number of
* data points exceeds the series'
* [turboThreshold](#series.solidgauge.turboThreshold), this option is not
* available.
* ```js
* data: [{
* y: 5,
* name: "Point2",
* color: "#00FF00"
* }, {
* y: 7,
* name: "Point1",
* color: "#FF00FF"
* }]
* ```
*
* The typical gauge only contains a single data value.
*
* @sample {highcharts} highcharts/chart/reflow-true/
* Numerical values
* @sample {highcharts} highcharts/series/data-array-of-objects/
* Config objects
*
* @type {Array<number|*>}
* @extends series.gauge.data
* @product highcharts
* @apioption series.solidgauge.data
*/
/**
* The inner radius of an individual point in a solid gauge. Can be given as a
* number (pixels) or percentage string.
*
* @sample {highcharts} highcharts/plotoptions/solidgauge-radius/
* Individual radius and innerRadius
*
* @type {number|string}
* @since 4.1.6
* @product highcharts
* @apioption series.solidgauge.data.innerRadius
*/
/**
* The outer radius of an individual point in a solid gauge. Can be
* given as a number (pixels) or percentage string.
*
* @sample {highcharts} highcharts/plotoptions/solidgauge-radius/
* Individual radius and innerRadius
*
* @type {number|string}
* @since 4.1.6
* @product highcharts
* @apioption series.solidgauge.data.radius
*/
@@ -0,0 +1,184 @@
/* *
*
* (c) 2009-2019 Øystein Moseng
*
* Earcons for the sonification module in Highcharts.
*
* License: www.highcharts.com/license
*
* */
'use strict';
import H from '../../parts/Globals.js';
/**
* Define an Instrument and the options for playing it.
*
* @requires module:modules/sonification
*
* @interface Highcharts.EarconInstrument
*//**
* An instrument instance or the name of the instrument in the
* Highcharts.sonification.instruments map.
* @name Highcharts.EarconInstrument#instrument
* @type {Highcharts.Instrument|String}
*//**
* The options to pass to Instrument.play.
* @name Highcharts.EarconInstrument#playOptions
* @type {object}
*/
/**
* Options for an Earcon.
*
* @requires module:modules/sonification
*
* @interface Highcharts.EarconOptionsObject
*//**
* The instruments and their options defining this earcon.
* @name Highcharts.EarconOptionsObject#instruments
* @type {Array<Highcharts.EarconInstrument>}
*//**
* The unique ID of the Earcon. Generated if not supplied.
* @name Highcharts.EarconOptionsObject#id
* @type {string|undefined}
*//**
* Global panning of all instruments. Overrides all panning on individual
* instruments. Can be a number between -1 and 1.
* @name Highcharts.EarconOptionsObject#pan
* @type {number|undefined}
*//**
* Master volume for all instruments. Volume settings on individual instruments
* can still be used for relative volume between the instruments. This setting
* does not affect volumes set by functions in individual instruments. Can be a
* number between 0 and 1. Defaults to 1.
* @name Highcharts.EarconOptionsObject#volume
* @type {number|undefined}
*//**
* Callback function to call when earcon has finished playing.
* @name Highcharts.EarconOptionsObject#onEnd
* @type {Function|undefined}
*/
/**
* The Earcon class. Earcon objects represent a certain sound consisting of
* one or more instruments playing a predefined sound.
*
* @sample highcharts/sonification/earcon/
* Using earcons directly
*
* @requires module:modules/sonification
*
* @class
* @name Highcharts.Earcon
*
* @param {Highcharts.EarconOptionsObject} options
* Options for the Earcon instance.
*/
function Earcon(options) {
this.init(options || {});
}
Earcon.prototype.init = function (options) {
this.options = options;
if (!this.options.id) {
this.options.id = this.id = H.uniqueKey();
}
this.instrumentsPlaying = {};
};
/**
* Play the earcon, optionally overriding init options.
*
* @sample highcharts/sonification/earcon/
* Using earcons directly
*
* @function Highcharts.Earcon#sonify
*
* @param {Highcharts.EarconOptionsObject} options
* Override existing options.
*/
Earcon.prototype.sonify = function (options) {
var playOptions = H.merge(this.options, options);
// Find master volume/pan settings
var masterVolume = H.pick(playOptions.volume, 1),
masterPan = playOptions.pan,
earcon = this,
playOnEnd = options && options.onEnd,
masterOnEnd = earcon.options.onEnd;
// Go through the instruments and play them
playOptions.instruments.forEach(function (opts) {
var instrument = typeof opts.instrument === 'string' ?
H.sonification.instruments[opts.instrument] : opts.instrument,
instrumentOpts = H.merge(opts.playOptions),
instrOnEnd,
instrumentCopy,
copyId;
if (instrument && instrument.play) {
if (opts.playOptions) {
// Handle master pan/volume
if (typeof opts.playOptions.volume !== 'function') {
instrumentOpts.volume = H.pick(masterVolume, 1) *
H.pick(opts.playOptions.volume, 1);
}
instrumentOpts.pan = H.pick(masterPan, instrumentOpts.pan);
// Handle onEnd
instrOnEnd = instrumentOpts.onEnd;
instrumentOpts.onEnd = function () {
delete earcon.instrumentsPlaying[copyId];
if (instrOnEnd) {
instrOnEnd.apply(this, arguments);
}
if (!Object.keys(earcon.instrumentsPlaying).length) {
if (playOnEnd) {
playOnEnd.apply(this, arguments);
}
if (masterOnEnd) {
masterOnEnd.apply(this, arguments);
}
}
};
// Play the instrument. Use a copy so we can play multiple at
// the same time.
instrumentCopy = instrument.copy();
copyId = instrumentCopy.id;
earcon.instrumentsPlaying[copyId] = instrumentCopy;
instrumentCopy.play(instrumentOpts);
}
} else {
H.error(30);
}
});
};
/**
* Cancel any current sonification of the Earcon. Calls onEnd functions.
*
* @function Highcharts.Earcon#cancelSonify
*
* @param {boolean} [fadeOut=false]
* Whether or not to fade out as we stop. If false, the earcon is
* cancelled synchronously.
*/
Earcon.prototype.cancelSonify = function (fadeOut) {
var playing = this.instrumentsPlaying,
instrIds = playing && Object.keys(playing);
if (instrIds && instrIds.length) {
instrIds.forEach(function (instr) {
playing[instr].stop(!fadeOut, null, 'cancelled');
});
this.instrumentsPlaying = {};
}
};
export default Earcon;
@@ -0,0 +1,582 @@
/* *
*
* (c) 2009-2019 Øystein Moseng
*
* Instrument class for sonification module.
*
* License: www.highcharts.com/license
*
* */
/**
* A set of options for the Instrument class.
*
* @requires module:modules/sonification
*
* @interface Highcharts.InstrumentOptionsObject
*//**
* The type of instrument. Currently only `oscillator` is supported. Defaults
* to `oscillator`.
* @name Highcharts.InstrumentOptionsObject#type
* @type {string|undefined}
*//**
* The unique ID of the instrument. Generated if not supplied.
* @name Highcharts.InstrumentOptionsObject#id
* @type {string|undefined}
*//**
* When using functions to determine frequency or other parameters during
* playback, this options specifies how often to call the callback functions.
* Number given in milliseconds. Defaults to 20.
* @name Highcharts.InstrumentOptionsObject#playCallbackInterval
* @type {number|undefined}
*//**
* A list of allowed frequencies for this instrument. If trying to play a
* frequency not on this list, the closest frequency will be used. Set to `null`
* to allow all frequencies to be used. Defaults to `null`.
* @name Highcharts.InstrumentOptionsObject#allowedFrequencies
* @type {Array<number>|undefined}
*//**
* Options specific to oscillator instruments.
* @name Highcharts.InstrumentOptionsObject#oscillator
* @type {Highcharts.OscillatorOptionsObject|undefined}
*/
/**
* Options for playing an instrument.
*
* @requires module:modules/sonification
*
* @interface Highcharts.InstrumentPlayOptionsObject
*//**
* The frequency of the note to play. Can be a fixed number, or a function. The
* function receives one argument: the relative time of the note playing (0
* being the start, and 1 being the end of the note). It should return the
* frequency number for each point in time. The poll interval of this function
* is specified by the Instrument.playCallbackInterval option.
* @name Highcharts.InstrumentPlayOptionsObject#frequency
* @type {number|Function}
*//**
* The duration of the note in milliseconds.
* @name Highcharts.InstrumentPlayOptionsObject#duration
* @type {number}
*//**
* The minimum frequency to allow. If the instrument has a set of allowed
* frequencies, the closest frequency is used by default. Use this option to
* stop too low frequencies from being used.
* @name Highcharts.InstrumentPlayOptionsObject#minFrequency
* @type {number|undefined}
*//**
* The maximum frequency to allow. If the instrument has a set of allowed
* frequencies, the closest frequency is used by default. Use this option to
* stop too high frequencies from being used.
* @name Highcharts.InstrumentPlayOptionsObject#maxFrequency
* @type {number|undefined}
*//**
* The volume of the instrument. Can be a fixed number between 0 and 1, or a
* function. The function receives one argument: the relative time of the note
* playing (0 being the start, and 1 being the end of the note). It should
* return the volume for each point in time. The poll interval of this function
* is specified by the Instrument.playCallbackInterval option. Defaults to 1.
* @name Highcharts.InstrumentPlayOptionsObject#volume
* @type {number|Function|undefined}
*//**
* The panning of the instrument. Can be a fixed number between -1 and 1, or a
* function. The function receives one argument: the relative time of the note
* playing (0 being the start, and 1 being the end of the note). It should
* return the panning value for each point in time. The poll interval of this
* function is specified by the Instrument.playCallbackInterval option.
* Defaults to 0.
* @name Highcharts.InstrumentPlayOptionsObject#pan
* @type {number|Function|undefined}
*//**
* Callback function to be called when the play is completed.
* @name Highcharts.InstrumentPlayOptionsObject#onEnd
* @type {Function|undefined}
*/
/**
* @requires module:modules/sonification
*
* @interface Highcharts.OscillatorOptionsObject
*//**
* The waveform shape to use for oscillator instruments. Defaults to `sine`.
* @name Highcharts.OscillatorOptionsObject#waveformShape
* @type {string|undefined}
*/
'use strict';
import H from '../../parts/Globals.js';
// Default options for Instrument constructor
var defaultOptions = {
type: 'oscillator',
playCallbackInterval: 20,
oscillator: {
waveformShape: 'sine'
}
};
/**
* The Instrument class. Instrument objects represent an instrument capable of
* playing a certain pitch for a specified duration.
*
* @sample highcharts/sonification/instrument/
* Using Instruments directly
* @sample highcharts/sonification/instrument-advanced/
* Using callbacks for instrument parameters
*
* @requires module:modules/sonification
*
* @class
* @name Highcharts.Instrument
*
* @param {Highcharts.InstrumentOptionsObject} options
* Options for the instrument instance.
*/
function Instrument(options) {
this.init(options);
}
Instrument.prototype.init = function (options) {
if (!this.initAudioContext()) {
H.error(29);
return;
}
this.options = H.merge(defaultOptions, options);
this.id = this.options.id = options && options.id || H.uniqueKey();
// Init the audio nodes
var ctx = H.audioContext;
this.gainNode = ctx.createGain();
this.setGain(0);
this.panNode = ctx.createStereoPanner && ctx.createStereoPanner();
if (this.panNode) {
this.setPan(0);
this.gainNode.connect(this.panNode);
this.panNode.connect(ctx.destination);
} else {
this.gainNode.connect(ctx.destination);
}
// Oscillator initialization
if (this.options.type === 'oscillator') {
this.initOscillator(this.options.oscillator);
}
// Init timer list
this.playCallbackTimers = [];
};
/**
* Return a copy of an instrument. Only one instrument instance can play at a
* time, so use this to get a new copy of the instrument that can play alongside
* it. The new instrument copy will receive a new ID unless one is supplied in
* options.
*
* @function Highcharts.Instrument#copy
*
* @param {Highcharts.InstrumentOptionsObject} [options]
* Options to merge in for the copy.
*
* @return {Highcharts.Instrument}
* A new Instrument instance with the same options.
*/
Instrument.prototype.copy = function (options) {
return new Instrument(H.merge(this.options, { id: null }, options));
};
/**
* Init the audio context, if we do not have one.
* @private
* @return {boolean} True if successful, false if not.
*/
Instrument.prototype.initAudioContext = function () {
var Context = H.win.AudioContext || H.win.webkitAudioContext,
hasOldContext = !!H.audioContext;
if (Context) {
H.audioContext = H.audioContext || new Context();
if (
!hasOldContext &&
H.audioContext &&
H.audioContext.state === 'running'
) {
H.audioContext.suspend(); // Pause until we need it
}
return !!(
H.audioContext &&
H.audioContext.createOscillator &&
H.audioContext.createGain
);
}
return false;
};
/**
* Init an oscillator instrument.
* @private
* @param {object} oscillatorOptions - The oscillator options passed to
* Highcharts.Instrument#init.
*/
Instrument.prototype.initOscillator = function (options) {
var ctx = H.audioContext;
this.oscillator = ctx.createOscillator();
this.oscillator.type = options.waveformShape;
this.oscillator.connect(this.gainNode);
this.oscillatorStarted = false;
};
/**
* Set pan position.
* @private
* @param {number} panValue - The pan position to set for the instrument.
*/
Instrument.prototype.setPan = function (panValue) {
if (this.panNode) {
this.panNode.pan.setValueAtTime(panValue, H.audioContext.currentTime);
}
};
/**
* Set gain level. A maximum of 1.2 is allowed before we emit a warning. The
* actual volume is not set above this level regardless of input.
* @private
* @param {number} gainValue - The gain level to set for the instrument.
* @param {number} [rampTime=0] - Gradually change the gain level, time given in
* milliseconds.
*/
Instrument.prototype.setGain = function (gainValue, rampTime) {
if (this.gainNode) {
if (gainValue > 1.2) {
console.warn( // eslint-disable-line
'Highcharts sonification warning: ' +
'Volume of instrument set too high.'
);
gainValue = 1.2;
}
if (rampTime) {
this.gainNode.gain.setValueAtTime(
this.gainNode.gain.value, H.audioContext.currentTime
);
this.gainNode.gain.linearRampToValueAtTime(
gainValue,
H.audioContext.currentTime + rampTime / 1000
);
} else {
this.gainNode.gain.setValueAtTime(
gainValue, H.audioContext.currentTime
);
}
}
};
/**
* Cancel ongoing gain ramps.
* @private
*/
Instrument.prototype.cancelGainRamp = function () {
if (this.gainNode) {
this.gainNode.gain.cancelScheduledValues(0);
}
};
/**
* Get the closest valid frequency for this instrument.
* @private
* @param {number} frequency - The target frequency.
* @param {number} [min] - Minimum frequency to return.
* @param {number} [max] - Maximum frequency to return.
* @return {number} The closest valid frequency to the input frequency.
*/
Instrument.prototype.getValidFrequency = function (frequency, min, max) {
var validFrequencies = this.options.allowedFrequencies,
maximum = H.pick(max, Infinity),
minimum = H.pick(min, -Infinity);
return !validFrequencies || !validFrequencies.length ?
// No valid frequencies for this instrument, return the target
frequency :
// Use the valid frequencies and return the closest match
validFrequencies.reduce(function (acc, cur) {
// Find the closest allowed value
return Math.abs(cur - frequency) < Math.abs(acc - frequency) &&
cur < maximum && cur > minimum ?
cur : acc;
}, Infinity);
};
/**
* Clear existing play callback timers.
* @private
*/
Instrument.prototype.clearPlayCallbackTimers = function () {
this.playCallbackTimers.forEach(function (timer) {
clearInterval(timer);
});
this.playCallbackTimers = [];
};
/**
* Set the current frequency being played by the instrument. The closest valid
* frequency between the frequency limits is used.
* @param {number} frequency - The frequency to set.
* @param {object} [frequencyLimits] - Object with maxFrequency and minFrequency
*/
Instrument.prototype.setFrequency = function (frequency, frequencyLimits) {
var limits = frequencyLimits || {},
validFrequency = this.getValidFrequency(
frequency, limits.min, limits.max
);
if (this.options.type === 'oscillator') {
this.oscillatorPlay(validFrequency);
}
};
/**
* Play oscillator instrument.
* @private
* @param {number} frequency - The frequency to play.
*/
Instrument.prototype.oscillatorPlay = function (frequency) {
if (!this.oscillatorStarted) {
this.oscillator.start();
this.oscillatorStarted = true;
}
this.oscillator.frequency.setValueAtTime(
frequency, H.audioContext.currentTime
);
};
/**
* Prepare instrument before playing. Resumes the audio context and starts the
* oscillator.
* @private
*/
Instrument.prototype.preparePlay = function () {
this.setGain(0.001);
if (H.audioContext.state === 'suspended') {
H.audioContext.resume();
}
if (this.oscillator && !this.oscillatorStarted) {
this.oscillator.start();
this.oscillatorStarted = true;
}
};
/**
* Play the instrument according to options.
*
* @sample highcharts/sonification/instrument/
* Using Instruments directly
* @sample highcharts/sonification/instrument-advanced/
* Using callbacks for instrument parameters
*
* @function Highcharts.Instrument#play
*
* @param {Highcharts.InstrumentPlayOptionsObject} options
* Options for the playback of the instrument.
*/
Instrument.prototype.play = function (options) {
var instrument = this,
duration = options.duration || 0,
// Set a value, or if it is a function, set it continously as a timer.
// Pass in the value/function to set, the setter function, and any
// additional data to pass through to the setter function.
setOrStartTimer = function (value, setter, setterData) {
var target = options.duration,
currentDurationIx = 0,
callbackInterval = instrument.options.playCallbackInterval;
if (typeof value === 'function') {
var timer = setInterval(function () {
currentDurationIx++;
var curTime = currentDurationIx * callbackInterval / target;
if (curTime >= 1) {
instrument[setter](value(1), setterData);
clearInterval(timer);
} else {
instrument[setter](value(curTime), setterData);
}
}, callbackInterval);
instrument.playCallbackTimers.push(timer);
} else {
instrument[setter](value, setterData);
}
};
if (!instrument.id) {
// No audio support - do nothing
return;
}
// If the AudioContext is suspended we have to resume it before playing
if (
H.audioContext.state === 'suspended' ||
this.oscillator && !this.oscillatorStarted
) {
instrument.preparePlay();
// Try again in 10ms
setTimeout(function () {
instrument.play(options);
}, 10);
return;
}
// Clear any existing play timers
if (instrument.playCallbackTimers.length) {
instrument.clearPlayCallbackTimers();
}
// Clear any gain ramps
instrument.cancelGainRamp();
// Clear stop oscillator timer
if (instrument.stopOscillatorTimeout) {
clearTimeout(instrument.stopOscillatorTimeout);
delete instrument.stopOscillatorTimeout;
}
// If a note is playing right now, clear the stop timeout, and call the
// callback.
if (instrument.stopTimeout) {
clearTimeout(instrument.stopTimeout);
delete instrument.stopTimeout;
if (instrument.stopCallback) {
// We have a callback for the play we are interrupting. We do not
// allow this callback to start a new play, because that leads to
// chaos. We pass in 'cancelled' to indicate that this note did not
// finish, but still stopped.
instrument._play = instrument.play;
instrument.play = function () { };
instrument.stopCallback('cancelled');
instrument.play = instrument._play;
}
}
// Stop the note without fadeOut if the duration is too short to hear the
// note otherwise.
var immediate = duration < H.sonification.fadeOutDuration + 20;
// Stop the instrument after the duration of the note
instrument.stopCallback = options.onEnd;
var onStop = function () {
delete instrument.stopTimeout;
instrument.stop(immediate);
};
if (duration) {
instrument.stopTimeout = setTimeout(
onStop,
immediate ? duration :
duration - H.sonification.fadeOutDuration
);
// Play the note
setOrStartTimer(options.frequency, 'setFrequency', null, {
minFrequency: options.minFrequency,
maxFrequency: options.maxFrequency
});
// Set the volume and panning
setOrStartTimer(H.pick(options.volume, 1), 'setGain', 4); // Slight ramp
setOrStartTimer(H.pick(options.pan, 0), 'setPan');
} else {
// No note duration, so just stop immediately
onStop();
}
};
/**
* Mute an instrument that is playing. If the instrument is not currently
* playing, this function does nothing.
*
* @function Highcharts.Instrument#mute
*/
Instrument.prototype.mute = function () {
this.setGain(0.0001, H.sonification.fadeOutDuration * 0.8);
};
/**
* Stop the instrument playing.
*
* @function Highcharts.Instrument#stop
*
* @param {boolean} immediately
* Whether to do the stop immediately or fade out.
*
* @param {Function} onStopped
* Callback function to be called when the stop is completed.
*
* @param {*} callbackData
* Data to send to the onEnd callback functions.
*/
Instrument.prototype.stop = function (immediately, onStopped, callbackData) {
var instr = this,
reset = function () {
// Remove timeout reference
if (instr.stopOscillatorTimeout) {
delete instr.stopOscillatorTimeout;
}
// The oscillator may have stopped in the meantime here, so allow
// this function to fail if so.
try {
instr.oscillator.stop();
} catch (e) {}
instr.oscillator.disconnect(instr.gainNode);
// We need a new oscillator in order to restart it
instr.initOscillator(instr.options.oscillator);
// Done stopping, call the callback from the stop
if (onStopped) {
onStopped(callbackData);
}
// Call the callback for the play we finished
if (instr.stopCallback) {
instr.stopCallback(callbackData);
}
};
// Clear any existing timers
if (instr.playCallbackTimers.length) {
instr.clearPlayCallbackTimers();
}
if (instr.stopTimeout) {
clearTimeout(instr.stopTimeout);
}
if (immediately) {
instr.setGain(0);
reset();
} else {
instr.mute();
// Stop the oscillator after the mute fade-out has finished
instr.stopOscillatorTimeout =
setTimeout(reset, H.sonification.fadeOutDuration + 100);
}
};
export default Instrument;
@@ -0,0 +1,702 @@
/* *
*
* (c) 2009-2019 Øystein Moseng
*
* TimelineEvent class definition.
*
* License: www.highcharts.com/license
*
* */
/**
* A set of options for the TimelineEvent class.
*
* @requires module:modules/sonification
*
* @private
* @interface Highcharts.TimelineEventOptionsObject
*//**
* The object we want to sonify when playing the TimelineEvent. Can be any
* object that implements the `sonify` and `cancelSonify` functions. If this is
* not supplied, the TimelineEvent is considered a silent event, and the onEnd
* event is immediately called.
* @name Highcharts.TimelineEventOptionsObject#eventObject
* @type {*}
*//**
* Options to pass on to the eventObject when playing it.
* @name Highcharts.TimelineEventOptionsObject#playOptions
* @type {object|undefined}
*//**
* The time at which we want this event to play (in milliseconds offset). This
* is not used for the TimelineEvent.play function, but rather intended as a
* property to decide when to call TimelineEvent.play. Defaults to 0.
* @name Highcharts.TimelineEventOptionsObject#time
* @type {number|undefined}
*//**
* Unique ID for the event. Generated automatically if not supplied.
* @name Highcharts.TimelineEventOptionsObject#id
* @type {string|undefined}
*//**
* Callback called when the play has finished.
* @name Highcharts.TimelineEventOptionsObject#onEnd
* @type {Function|undefined}
*/
'use strict';
import H from '../../parts/Globals.js';
import utilities from 'utilities.js';
/**
* The TimelineEvent class. Represents a sound event on a timeline.
*
* @requires module:modules/sonification
*
* @private
* @class
* @name Highcharts.TimelineEvent
*
* @param {Highcharts.TimelineEventOptionsObject} options
* Options for the TimelineEvent.
*/
function TimelineEvent(options) {
this.init(options || {});
}
TimelineEvent.prototype.init = function (options) {
this.options = options;
this.time = options.time || 0;
this.id = this.options.id = options.id || H.uniqueKey();
};
/**
* Play the event. Does not take the TimelineEvent.time option into account,
* and plays the event immediately.
*
* @function Highcharts.TimelineEvent#play
*
* @param {Highcharts.TimelineEventOptionsObject} [options]
* Options to pass in to the eventObject when playing it.
*/
TimelineEvent.prototype.play = function (options) {
var eventObject = this.options.eventObject,
masterOnEnd = this.options.onEnd,
playOnEnd = options && options.onEnd,
playOptionsOnEnd = this.options.playOptions &&
this.options.playOptions.onEnd,
playOptions = H.merge(this.options.playOptions, options);
if (eventObject && eventObject.sonify) {
// If we have multiple onEnds defined, use all
playOptions.onEnd = masterOnEnd || playOnEnd || playOptionsOnEnd ?
function () {
var args = arguments;
[masterOnEnd, playOnEnd, playOptionsOnEnd].forEach(
function (onEnd) {
if (onEnd) {
onEnd.apply(this, args);
}
}
);
} : undefined;
eventObject.sonify(playOptions);
} else {
if (playOnEnd) {
playOnEnd();
}
if (masterOnEnd) {
masterOnEnd();
}
}
};
/**
* Cancel the sonification of this event. Does nothing if the event is not
* currently sonifying.
*
* @function Highcharts.TimelineEvent#cancel
*
* @param {boolean} [fadeOut=false]
* Whether or not to fade out as we stop. If false, the event is
* cancelled synchronously.
*/
TimelineEvent.prototype.cancel = function (fadeOut) {
this.options.eventObject.cancelSonify(fadeOut);
};
/**
* A set of options for the TimelinePath class.
*
* @requires module:modules/
*
* @private
* @interface Highcharts.TimelinePathOptionsObject
*//**
* List of TimelineEvents to play on this track.
* @name Highcharts.TimelinePathOptionsObject#events
* @type {Array<Highcharts.TimelineEvent>}
*//**
* If this option is supplied, this path ignores all events and just waits for
* the specified number of milliseconds before calling onEnd.
* @name Highcharts.TimelinePathOptionsObject#silentWait
* @type {number|undefined}
*//**
* Unique ID for this timeline path. Automatically generated if not supplied.
* @name Highcharts.TimelinePathOptionsObject#id
* @type {string|undefined}
*//**
* Callback called before the path starts playing.
* @name Highcharts.TimelinePathOptionsObject#onStart
* @type {Function|undefined}
*//**
* Callback function to call before an event plays.
* @name Highcharts.TimelinePathOptionsObject#onEventStart
* @type {Function|undefined}
*//**
* Callback function to call after an event has stopped playing.
* @name Highcharts.TimelinePathOptionsObject#onEventEnd
* @type {Function|undefined}
*//**
* Callback called when the whole path is finished.
* @name Highcharts.TimelinePathOptionsObject#onEnd
* @type {Function|undefined}
*/
/**
* The TimelinePath class. Represents a track on a timeline with a list of
* sound events to play at certain times relative to each other.
*
* @requires module:modules/sonification
*
* @private
* @class
* @name Highcharts.TimelinePath
*
* @param {Highcharts.TimelinePathOptionsObject} options
* Options for the TimelinePath.
*/
function TimelinePath(options) {
this.init(options);
}
TimelinePath.prototype.init = function (options) {
this.options = options;
this.id = this.options.id = options.id || H.uniqueKey();
this.cursor = 0;
this.eventsPlaying = {};
// Handle silent wait, otherwise use events from options
this.events = options.silentWait ?
[
new TimelineEvent({ time: 0 }),
new TimelineEvent({ time: options.silentWait })
] :
this.options.events;
// We need to sort our events by time
this.sortEvents();
// Get map from event ID to index
this.updateEventIdMap();
// Signal events to fire
this.signalHandler = new utilities.SignalHandler(
['playOnEnd', 'masterOnEnd', 'onStart', 'onEventStart', 'onEventEnd']
);
this.signalHandler.registerSignalCallbacks(
H.merge(options, { masterOnEnd: options.onEnd })
);
};
/**
* Sort the internal event list by time.
* @private
*/
TimelinePath.prototype.sortEvents = function () {
this.events = this.events.sort(function (a, b) {
return a.time - b.time;
});
};
/**
* Update the internal eventId to index map.
* @private
*/
TimelinePath.prototype.updateEventIdMap = function () {
this.eventIdMap = this.events.reduce(function (acc, cur, i) {
acc[cur.id] = i;
return acc;
}, {});
};
/**
* Add events to the path. Should not be done while the path is playing.
* The new events are inserted according to their time property.
* @private
* @param {Array<Highcharts.TimelineEvent>} newEvents - The new timeline events
* to add.
*/
TimelinePath.prototype.addTimelineEvents = function (newEvents) {
this.events = this.events.concat(newEvents);
this.sortEvents(); // Sort events by time
this.updateEventIdMap(); // Update the event ID to index map
};
/**
* Get the current TimelineEvent under the cursor.
* @private
* @return {Highcharts.TimelineEvent} The current timeline event.
*/
TimelinePath.prototype.getCursor = function () {
return this.events[this.cursor];
};
/**
* Set the current TimelineEvent under the cursor.
* @private
* @param {string} eventId - The ID of the timeline event to set as current.
* @return {boolean} True if there is an event with this ID in the path. False
* otherwise.
*/
TimelinePath.prototype.setCursor = function (eventId) {
var ix = this.eventIdMap[eventId];
if (ix !== undefined) {
this.cursor = ix;
return true;
}
return false;
};
/**
* Play the timeline from the current cursor.
* @private
* @param {Function} onEnd - Callback to call when play finished. Does not
* override other onEnd callbacks.
*/
TimelinePath.prototype.play = function (onEnd) {
this.pause();
this.signalHandler.emitSignal('onStart');
this.signalHandler.clearSignalCallbacks(['playOnEnd']);
this.signalHandler.registerSignalCallbacks({ playOnEnd: onEnd });
this.playEvents(1);
};
/**
* Play the timeline backwards from the current cursor.
* @private
* @param {Function} onEnd - Callback to call when play finished. Does not
* override other onEnd callbacks.
*/
TimelinePath.prototype.rewind = function (onEnd) {
this.pause();
this.signalHandler.emitSignal('onStart');
this.signalHandler.clearSignalCallbacks(['playOnEnd']);
this.signalHandler.registerSignalCallbacks({ playOnEnd: onEnd });
this.playEvents(-1);
};
/**
* Reset the cursor to the beginning.
* @private
*/
TimelinePath.prototype.resetCursor = function () {
this.cursor = 0;
};
/**
* Reset the cursor to the end.
* @private
*/
TimelinePath.prototype.resetCursorEnd = function () {
this.cursor = this.events.length - 1;
};
/**
* Cancel current playing. Leaves the cursor intact.
* @private
* @param {boolean} [fadeOut=false] - Whether or not to fade out as we stop. If
* false, the path is cancelled synchronously.
*/
TimelinePath.prototype.pause = function (fadeOut) {
var timelinePath = this;
// Cancel next scheduled play
clearTimeout(timelinePath.nextScheduledPlay);
// Cancel currently playing events
Object.keys(timelinePath.eventsPlaying).forEach(function (id) {
if (timelinePath.eventsPlaying[id]) {
timelinePath.eventsPlaying[id].cancel(fadeOut);
}
});
timelinePath.eventsPlaying = {};
};
/**
* Play the events, starting from current cursor, and going in specified
* direction.
* @private
* @param {number} direction - The direction to play, 1 for forwards and -1 for
* backwards.
*/
TimelinePath.prototype.playEvents = function (direction) {
var timelinePath = this,
curEvent = timelinePath.events[this.cursor],
nextEvent = timelinePath.events[this.cursor + direction],
timeDiff,
onEnd = function (signalData) {
timelinePath.signalHandler.emitSignal(
'masterOnEnd', signalData
);
timelinePath.signalHandler.emitSignal(
'playOnEnd', signalData
);
};
// Store reference to path on event
curEvent.timelinePath = timelinePath;
// Emit event, cancel if returns false
if (
timelinePath.signalHandler.emitSignal(
'onEventStart', curEvent
) === false
) {
onEnd({
event: curEvent,
cancelled: true
});
return;
}
// Play the current event
timelinePath.eventsPlaying[curEvent.id] = curEvent;
curEvent.play({
onEnd: function (cancelled) {
var signalData = {
event: curEvent,
cancelled: !!cancelled
};
// Keep track of currently playing events for cancelling
delete timelinePath.eventsPlaying[curEvent.id];
// Handle onEventEnd
timelinePath.signalHandler.emitSignal('onEventEnd', signalData);
// Reached end of path?
if (!nextEvent) {
onEnd(signalData);
}
}
});
// Schedule next
if (nextEvent) {
timeDiff = Math.abs(nextEvent.time - curEvent.time);
if (timeDiff < 1) {
// Play immediately
timelinePath.cursor += direction;
timelinePath.playEvents(direction);
} else {
// Schedule after the difference in ms
this.nextScheduledPlay = setTimeout(function () {
timelinePath.cursor += direction;
timelinePath.playEvents(direction);
}, timeDiff);
}
}
};
/* ************************************************************************** *
* TIMELINE *
* ************************************************************************** */
/**
* A set of options for the Timeline class.
*
* @requires module:modules/sonification
*
* @private
* @interface Highcharts.TimelineOptionsObject
*//**
* List of TimelinePaths to play. Multiple paths can be grouped together and
* played simultaneously by supplying an array of paths in place of a single
* path.
* @name Highcharts.TimelineOptionsObject#paths
* @type {Array<Highcharts.TimelinePath|Array<Highcharts.TimelinePath>>}
*//**
* Callback function to call before a path plays.
* @name Highcharts.TimelineOptionsObject#onPathStart
* @type {Function|undefined}
*//**
* Callback function to call after a path has stopped playing.
* @name Highcharts.TimelineOptionsObject#onPathEnd
* @type {Function|undefined}
*//**
* Callback called when the whole path is finished.
* @name Highcharts.TimelineOptionsObject#onEnd
* @type {Function|undefined}
*/
/**
* The Timeline class. Represents a sonification timeline with a list of
* timeline paths with events to play at certain times relative to each other.
*
* @requires module:modules/sonification
*
* @private
* @class
* @name Highcharts.Timeline
*
* @param {Highcharts.TimelineOptionsObject} options
* Options for the Timeline.
*/
function Timeline(options) {
this.init(options || {});
}
Timeline.prototype.init = function (options) {
this.options = options;
this.cursor = 0;
this.paths = options.paths;
this.pathsPlaying = {};
this.signalHandler = new utilities.SignalHandler(
['playOnEnd', 'masterOnEnd', 'onPathStart', 'onPathEnd']
);
this.signalHandler.registerSignalCallbacks(
H.merge(options, { masterOnEnd: options.onEnd })
);
};
/**
* Play the timeline forwards from cursor.
* @private
* @param {Function} onEnd - Callback to call when play finished. Does not
* override other onEnd callbacks.
*/
Timeline.prototype.play = function (onEnd) {
this.pause();
this.signalHandler.clearSignalCallbacks(['playOnEnd']);
this.signalHandler.registerSignalCallbacks({ playOnEnd: onEnd });
this.playPaths(1);
};
/**
* Play the timeline backwards from cursor.
* @private
* @param {Function} onEnd - Callback to call when play finished. Does not
* override other onEnd callbacks.
*/
Timeline.prototype.rewind = function (onEnd) {
this.pause();
this.signalHandler.clearSignalCallbacks(['playOnEnd']);
this.signalHandler.registerSignalCallbacks({ playOnEnd: onEnd });
this.playPaths(-1);
};
/**
* Play the timeline in the specified direction.
* @private
* @param {number} direction - Direction to play in. 1 for forwards, -1 for
* backwards.
*/
Timeline.prototype.playPaths = function (direction) {
var curPaths = H.splat(this.paths[this.cursor]),
nextPaths = this.paths[this.cursor + direction],
timeline = this,
signalHandler = this.signalHandler,
pathsEnded = 0,
// Play a path
playPath = function (path) {
// Emit signal and set playing state
signalHandler.emitSignal('onPathStart', path);
timeline.pathsPlaying[path.id] = path;
// Do the play
path[direction > 0 ? 'play' : 'rewind'](function (callbackData) {
// Play ended callback
// Data to pass to signal callbacks
var cancelled = callbackData && callbackData.cancelled,
signalData = {
path: path,
cancelled: cancelled
};
// Clear state and send signal
delete timeline.pathsPlaying[path.id];
signalHandler.emitSignal('onPathEnd', signalData);
// Handle next paths
pathsEnded++;
if (pathsEnded >= curPaths.length) {
// We finished all of the current paths for cursor.
if (nextPaths && !cancelled) {
// We have more paths, move cursor along
timeline.cursor += direction;
// Reset upcoming path cursors before playing
H.splat(nextPaths).forEach(function (nextPath) {
nextPath[
direction > 0 ? 'resetCursor' : 'resetCursorEnd'
]();
});
// Play next
timeline.playPaths(direction);
} else {
// If it is the last path in this direction, call onEnd
signalHandler.emitSignal('playOnEnd', signalData);
signalHandler.emitSignal('masterOnEnd', signalData);
}
}
});
};
// Go through the paths under cursor and play them
curPaths.forEach(function (path) {
if (path) {
// Store reference to timeline
path.timeline = timeline;
// Leave a timeout to let notes fade out before next play
setTimeout(function () {
playPath(path);
}, H.sonification.fadeOutTime);
}
});
};
/**
* Stop the playing of the timeline. Cancels all current sounds, but does not
* affect the cursor.
* @private
* @param {boolean} [fadeOut=false] - Whether or not to fade out as we stop. If
* false, the timeline is cancelled synchronously.
*/
Timeline.prototype.pause = function (fadeOut) {
var timeline = this;
// Cancel currently playing events
Object.keys(timeline.pathsPlaying).forEach(function (id) {
if (timeline.pathsPlaying[id]) {
timeline.pathsPlaying[id].pause(fadeOut);
}
});
timeline.pathsPlaying = {};
};
/**
* Reset the cursor to the beginning of the timeline.
* @private
*/
Timeline.prototype.resetCursor = function () {
this.paths.forEach(function (paths) {
H.splat(paths).forEach(function (path) {
path.resetCursor();
});
});
this.cursor = 0;
};
/**
* Reset the cursor to the end of the timeline.
* @private
*/
Timeline.prototype.resetCursorEnd = function () {
this.paths.forEach(function (paths) {
H.splat(paths).forEach(function (path) {
path.resetCursorEnd();
});
});
this.cursor = this.paths.length - 1;
};
/**
* Set the current TimelineEvent under the cursor. If multiple paths are being
* played at the same time, this function only affects a single path (the one
* that contains the eventId that is passed in).
* @private
* @param {string} eventId - The ID of the timeline event to set as current.
* @return {boolean} True if the cursor was set, false if no TimelineEvent was
* found for this ID.
*/
Timeline.prototype.setCursor = function (eventId) {
return this.paths.some(function (paths) {
return H.splat(paths).some(function (path) {
return path.setCursor(eventId);
});
});
};
/**
* Get the current TimelineEvents under the cursors. This function will return
* the event under the cursor for each currently playing path, as an object
* where the path ID is mapped to the TimelineEvent under that path's cursor.
* @private
* @return {object} The TimelineEvents under each path's cursors.
*/
Timeline.prototype.getCursor = function () {
return this.getCurrentPlayingPaths().reduce(function (acc, cur) {
acc[cur.id] = cur.getCursor();
return acc;
}, {});
};
/**
* Check if timeline is reset or at start.
* @private
* @return {boolean} True if timeline is at the beginning.
*/
Timeline.prototype.atStart = function () {
return !this.getCurrentPlayingPaths().some(function (path) {
return path.cursor;
});
};
/**
* Get the current TimelinePaths being played.
* @private
* @return {Array<Highcharts.TimelinePath>} The TimelinePaths currently being
* played.
*/
Timeline.prototype.getCurrentPlayingPaths = function () {
return H.splat(this.paths[this.cursor]);
};
// Export the classes
var timelineClasses = {
TimelineEvent: TimelineEvent,
TimelinePath: TimelinePath,
Timeline: Timeline
};
export default timelineClasses;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,37 @@
/* *
*
* (c) 2009-2019 Øystein Moseng
*
* Instrument definitions for sonification module.
*
* License: www.highcharts.com/license
*
* */
'use strict';
import Instrument from 'Instrument.js';
import utilities from 'utilities.js';
var instruments = {};
['sine', 'square', 'triangle', 'sawtooth'].forEach(function (waveform) {
// Add basic instruments
instruments[waveform] = new Instrument({
oscillator: { waveformShape: waveform }
});
// Add musical instruments
instruments[waveform + 'Musical'] = new Instrument({
allowedFrequencies: utilities.musicalFrequencies,
oscillator: { waveformShape: waveform }
});
// Add scaled instruments
instruments[waveform + 'Major'] = new Instrument({
allowedFrequencies: utilities.getMusicalScale([1, 3, 5, 6, 8, 10, 12]),
oscillator: { waveformShape: waveform }
});
});
export default instruments;
@@ -0,0 +1,113 @@
/* *
*
* (c) 2009-2019 Øystein Moseng
*
* List of musical frequencies from C0 to C8.
*
* License: www.highcharts.com/license
*
* */
'use strict';
var frequencies = [
16.351597831287414, // C0
17.323914436054505,
18.354047994837977,
19.445436482630058,
20.601722307054366,
21.826764464562746,
23.12465141947715,
24.499714748859326,
25.956543598746574,
27.5, // A0
29.13523509488062,
30.86770632850775,
32.70319566257483, // C1
34.64782887210901,
36.70809598967594,
38.890872965260115,
41.20344461410875,
43.653528929125486,
46.2493028389543,
48.999429497718666,
51.91308719749314,
55, // A1
58.27047018976124,
61.7354126570155,
65.40639132514966, // C2
69.29565774421802,
73.41619197935188,
77.78174593052023,
82.4068892282175,
87.30705785825097,
92.4986056779086,
97.99885899543733,
103.82617439498628,
110, // A2
116.54094037952248,
123.47082531403103,
130.8127826502993, // C3
138.59131548843604,
146.8323839587038,
155.56349186104046,
164.81377845643496,
174.61411571650194,
184.9972113558172,
195.99771799087463,
207.65234878997256,
220, // A3
233.08188075904496,
246.94165062806206,
261.6255653005986, // C4
277.1826309768721,
293.6647679174076,
311.1269837220809,
329.6275569128699,
349.2282314330039,
369.9944227116344,
391.99543598174927,
415.3046975799451,
440, // A4
466.1637615180899,
493.8833012561241,
523.2511306011972, // C5
554.3652619537442,
587.3295358348151,
622.2539674441618,
659.2551138257398,
698.4564628660078,
739.9888454232688,
783.9908719634985,
830.6093951598903,
880, // A5
932.3275230361799,
987.7666025122483,
1046.5022612023945, // C6
1108.7305239074883,
1174.6590716696303,
1244.5079348883237,
1318.5102276514797,
1396.9129257320155,
1479.9776908465376,
1567.981743926997,
1661.2187903197805,
1760, // A6
1864.6550460723597,
1975.533205024496,
2093.004522404789, // C7
2217.4610478149766,
2349.31814333926,
2489.0158697766474,
2637.02045530296,
2793.825851464031,
2959.955381693075,
3135.9634878539946,
3322.437580639561,
3520, // A7
3729.3100921447194,
3951.066410048992,
4186.009044809578 // C8
];
export default frequencies;
@@ -0,0 +1,385 @@
/* *
*
* (c) 2009-2019 Øystein Moseng
*
* Code for sonifying single points.
*
* License: www.highcharts.com/license
*
* */
/**
* Define the parameter mapping for an instrument.
*
* @requires module:modules/sonification
*
* @interface Highcharts.PointInstrumentMappingObject
*//**
* Define the volume of the instrument. This can be a string with a data
* property name, e.g. `'y'`, in which case this data property is used to define
* the volume relative to the `y`-values of the other points. A higher `y` value
* would then result in a higher volume. This option can also be a fixed number
* or a function. If it is a function, this function is called in regular
* intervals while the note is playing. It receives three arguments: The point,
* the dataExtremes, and the current relative time - where 0 is the beginning of
* the note and 1 is the end. The function should return the volume of the note
* as a number between 0 and 1.
* @name Highcharts.PointInstrumentMappingObject#volume
* @type {string|number|Function}
*//**
* Define the duration of the notes for this instrument. This can be a string
* with a data property name, e.g. `'y'`, in which case this data property is
* used to define the duration relative to the `y`-values of the other points. A
* higher `y` value would then result in a longer duration. This option can also
* be a fixed number or a function. If it is a function, this function is called
* once before the note starts playing, and should return the duration in
* milliseconds. It receives two arguments: The point, and the dataExtremes.
* @name Highcharts.PointInstrumentMappingObject#duration
* @type {string|number|Function}
*//**
* Define the panning of the instrument. This can be a string with a data
* property name, e.g. `'x'`, in which case this data property is used to define
* the panning relative to the `x`-values of the other points. A higher `x`
* value would then result in a higher panning value (panned further to the
* right). This option can also be a fixed number or a function. If it is a
* function, this function is called in regular intervals while the note is
* playing. It receives three arguments: The point, the dataExtremes, and the
* current relative time - where 0 is the beginning of the note and 1 is the
* end. The function should return the panning of the note as a number between
* -1 and 1.
* @name Highcharts.PointInstrumentMappingObject#pan
* @type {string|number|Function|undefined}
*//**
* Define the frequency of the instrument. This can be a string with a data
* property name, e.g. `'y'`, in which case this data property is used to define
* the frequency relative to the `y`-values of the other points. A higher `y`
* value would then result in a higher frequency. This option can also be a
* fixed number or a function. If it is a function, this function is called in
* regular intervals while the note is playing. It receives three arguments:
* The point, the dataExtremes, and the current relative time - where 0 is the
* beginning of the note and 1 is the end. The function should return the
* frequency of the note as a number (in Hz).
* @name Highcharts.PointInstrumentMappingObject#frequency
* @type {string|number|Function}
*/
/**
* @requires module:modules/sonification
*
* @interface Highcharts.PointInstrumentOptionsObject
*//**
* The minimum duration for a note when using a data property for duration. Can
* be overridden by using either a fixed number or a function for
* instrumentMapping.duration. Defaults to 20.
* @name Highcharts.PointInstrumentOptionsObject#minDuration
* @type {number|undefined}
*//**
* The maximum duration for a note when using a data property for duration. Can
* be overridden by using either a fixed number or a function for
* instrumentMapping.duration. Defaults to 2000.
* @name Highcharts.PointInstrumentOptionsObject#maxDuration
* @type {number|undefined}
*//**
* The minimum pan value for a note when using a data property for panning. Can
* be overridden by using either a fixed number or a function for
* instrumentMapping.pan. Defaults to -1 (fully left).
* @name Highcharts.PointInstrumentOptionsObject#minPan
* @type {number|undefined}
*//**
* The maximum pan value for a note when using a data property for panning. Can
* be overridden by using either a fixed number or a function for
* instrumentMapping.pan. Defaults to 1 (fully right).
* @name Highcharts.PointInstrumentOptionsObject#maxPan
* @type {number|undefined}
*//**
* The minimum volume for a note when using a data property for volume. Can be
* overridden by using either a fixed number or a function for
* instrumentMapping.volume. Defaults to 0.1.
* @name Highcharts.PointInstrumentOptionsObject#minVolume
* @type {number|undefined}
*//**
* The maximum volume for a note when using a data property for volume. Can be
* overridden by using either a fixed number or a function for
* instrumentMapping.volume. Defaults to 1.
* @name Highcharts.PointInstrumentOptionsObject#maxVolume
* @type {number|undefined}
*//**
* The minimum frequency for a note when using a data property for frequency.
* Can be overridden by using either a fixed number or a function for
* instrumentMapping.frequency. Defaults to 220.
* @name Highcharts.PointInstrumentOptionsObject#minFrequency
* @type {number|undefined}
*//**
* The maximum frequency for a note when using a data property for frequency.
* Can be overridden by using either a fixed number or a function for
* instrumentMapping.frequency. Defaults to 2200.
* @name Highcharts.PointInstrumentOptionsObject#maxFrequency
* @type {number|undefined}
*/
/**
* An instrument definition for a point, specifying the instrument to play and
* how to play it.
*
* @interface Highcharts.PointInstrumentObject
*//**
* An Instrument instance or the name of the instrument in the
* Highcharts.sonification.instruments map.
* @name Highcharts.PointInstrumentObject#instrument
* @type {Highcharts.Instrument|string}
*//**
* Mapping of instrument parameters for this instrument.
* @name Highcharts.PointInstrumentObject#instrumentMapping
* @type {Highcharts.PointInstrumentMappingObject}
*//**
* Options for this instrument.
* @name Highcharts.PointInstrumentObject#instrumentOptions
* @type {Highcharts.PointInstrumentOptionsObject|undefined}
*//**
* Callback to call when the instrument has stopped playing.
* @name Highcharts.PointInstrumentObject#onEnd
* @type {Function|undefined}
*/
/**
* Options for sonifying a point.
* @interface Highcharts.PointSonifyOptionsObject
*//**
* The instrument definitions for this point.
* @name Highcharts.PointSonifyOptionsObject#instruments
* @type {Array<Highcharts.PointInstrumentObject>}
*//**
* Optionally provide the minimum/maximum values for the points. If this is not
* supplied, it is calculated from the points in the chart on demand. This
* option is supplied in the following format, as a map of point data properties
* to objects with min/max values:
* ```js
* dataExtremes: {
* y: {
* min: 0,
* max: 100
* },
* z: {
* min: -10,
* max: 10
* }
* // Properties used and not provided are calculated on demand
* }
* ```
* @name Highcharts.PointSonifyOptionsObject#dataExtremes
* @type {object|undefined}
*//**
* Callback called when the sonification has finished.
* @name Highcharts.PointSonifyOptionsObject#onEnd
* @type {Function|undefined}
*/
'use strict';
import H from '../../parts/Globals.js';
import utilities from 'utilities.js';
// Defaults for the instrument options
// NOTE: Also change defaults in Highcharts.PointInstrumentOptionsObject if
// making changes here.
var defaultInstrumentOptions = {
minDuration: 20,
maxDuration: 2000,
minVolume: 0.1,
maxVolume: 1,
minPan: -1,
maxPan: 1,
minFrequency: 220,
maxFrequency: 2200
};
/**
* Sonify a single point.
*
* @sample highcharts/sonification/point-basic/
* Click on points to sonify
* @sample highcharts/sonification/point-advanced/
* Sonify bubbles
*
* @requires module:modules/sonification
*
* @function Highcharts.Point#sonify
*
* @param {Highcharts.PointSonifyOptionsObject} options
* Options for the sonification of the point.
*/
function pointSonify(options) {
var point = this,
chart = point.series.chart,
dataExtremes = options.dataExtremes || {},
// Get the value to pass to instrument.play from the mapping value
// passed in.
getMappingValue = function (
value, makeFunction, allowedExtremes, allowedValues
) {
// Fixed number, just use that
if (typeof value === 'number' || value === undefined) {
return value;
}
// Function. Return new function if we try to use callback,
// otherwise call it now and return result.
if (typeof value === 'function') {
return makeFunction ?
function (time) {
return value(point, dataExtremes, time);
} :
value(point, dataExtremes);
}
// String, this is a data prop.
if (typeof value === 'string') {
// Find data extremes if we don't have them
dataExtremes[value] = dataExtremes[value] ||
utilities.calculateDataExtremes(
point.series.chart, value
);
// Find the value
return utilities.virtualAxisTranslate(
H.pick(point[value], point.options[value]),
dataExtremes[value],
allowedExtremes,
allowedValues
);
}
};
// Register playing point on chart
chart.sonification.currentlyPlayingPoint = point;
// Keep track of instruments playing
point.sonification = point.sonification || {};
point.sonification.instrumentsPlaying =
point.sonification.instrumentsPlaying || {};
// Register signal handler for the point
var signalHandler = point.sonification.signalHandler =
point.sonification.signalHandler ||
new utilities.SignalHandler(['onEnd']);
signalHandler.clearSignalCallbacks();
signalHandler.registerSignalCallbacks({ onEnd: options.onEnd });
// If we have a null point or invisible point, just return
if (point.isNull || !point.visible || !point.series.visible) {
signalHandler.emitSignal('onEnd');
return;
}
// Go through instruments and play them
options.instruments.forEach(function (instrumentDefinition) {
var instrument = typeof instrumentDefinition.instrument === 'string' ?
H.sonification.instruments[instrumentDefinition.instrument] :
instrumentDefinition.instrument,
mapping = instrumentDefinition.instrumentMapping || {},
extremes = H.merge(
defaultInstrumentOptions,
instrumentDefinition.instrumentOptions
),
id = instrument.id,
onEnd = function (cancelled) {
// Instrument on end
if (instrumentDefinition.onEnd) {
instrumentDefinition.onEnd.apply(this, arguments);
}
// Remove currently playing point reference on chart
if (
chart.sonification &&
chart.sonification.currentlyPlayingPoint
) {
delete chart.sonification.currentlyPlayingPoint;
}
// Remove reference from instruments playing
if (
point.sonification && point.sonification.instrumentsPlaying
) {
delete point.sonification.instrumentsPlaying[id];
// This was the last instrument?
if (
!Object.keys(
point.sonification.instrumentsPlaying
).length
) {
signalHandler.emitSignal('onEnd', cancelled);
}
}
};
// Play the note on the instrument
if (instrument && instrument.play) {
point.sonification.instrumentsPlaying[instrument.id] = instrument;
instrument.play({
frequency: getMappingValue(
mapping.frequency,
true,
{ min: extremes.minFrequency, max: extremes.maxFrequency }
),
duration: getMappingValue(
mapping.duration,
false,
{ min: extremes.minDuration, max: extremes.maxDuration }
),
pan: getMappingValue(
mapping.pan,
true,
{ min: extremes.minPan, max: extremes.maxPan }
),
volume: getMappingValue(
mapping.volume,
true,
{ min: extremes.minVolume, max: extremes.maxVolume }
),
onEnd: onEnd,
minFrequency: extremes.minFrequency,
maxFrequency: extremes.maxFrequency
});
} else {
H.error(30);
}
});
}
/**
* Cancel sonification of a point. Calls onEnd functions.
*
* @requires module:modules/sonification
*
* @function Highcharts.Point#cancelSonify
*
* @param {boolean} [fadeOut=false]
* Whether or not to fade out as we stop. If false, the points are
* cancelled synchronously.
*/
function pointCancelSonify(fadeOut) {
var playing = this.sonification && this.sonification.instrumentsPlaying,
instrIds = playing && Object.keys(playing);
if (instrIds && instrIds.length) {
instrIds.forEach(function (instr) {
playing[instr].stop(!fadeOut, null, 'cancelled');
});
this.sonification.instrumentsPlaying = {};
this.sonification.signalHandler.emitSignal('onEnd', 'cancelled');
}
}
var pointSonifyFunctions = {
pointSonify: pointSonify,
pointCancelSonify: pointCancelSonify
};
export default pointSonifyFunctions;
@@ -0,0 +1,107 @@
/* *
*
* (c) 2009-2019 Øystein Moseng
*
* Sonification module for Highcharts
*
* License: www.highcharts.com/license
*
* */
'use strict';
import H from '../../parts/Globals.js';
import Instrument from 'Instrument.js';
import instruments from 'instrumentDefinitions.js';
import Earcon from 'Earcon.js';
import pointSonifyFunctions from 'pointSonify.js';
import chartSonifyFunctions from 'chartSonify.js';
import utilities from 'utilities.js';
import TimelineClasses from 'Timeline.js';
// Expose on the Highcharts object
/**
* Global classes and objects related to sonification.
*
* @requires module:modules/sonification
*
* @name Highcharts.sonification
* @type {Highcharts.SonificationObject}
*/
/**
* Global classes and objects related to sonification.
*
* @requires module:modules/sonification
*
* @interface Highcharts.SonificationObject
*//**
* Note fade-out-time in milliseconds. Most notes are faded out quickly by
* default if there is time. This is to avoid abrupt stops which will cause
* perceived clicks.
* @name Highcharts.SonificationObject#fadeOutDuration
* @type {number}
*//**
* Utility functions.
* @name Highcharts.SonificationObject#utilities
* @private
* @type {object}
*//**
* The Instrument class.
* @name Highcharts.SonificationObject#Instrument
* @type {Function}
*//**
* Predefined instruments, given as an object with a map between the instrument
* name and the Highcharts.Instrument object.
* @name Highcharts.SonificationObject#instruments
* @type {Object}
*//**
* The Earcon class.
* @name Highcharts.SonificationObject#Earcon
* @type {Function}
*//**
* The TimelineEvent class.
* @private
* @name Highcharts.SonificationObject#TimelineEvent
* @type {Function}
*//**
* The TimelinePath class.
* @private
* @name Highcharts.SonificationObject#TimelinePath
* @type {Function}
*//**
* The Timeline class.
* @private
* @name Highcharts.SonificationObject#Timeline
* @type {Function}
*/
H.sonification = {
fadeOutDuration: 20,
// Classes and functions
utilities: utilities,
Instrument: Instrument,
instruments: instruments,
Earcon: Earcon,
TimelineEvent: TimelineClasses.TimelineEvent,
TimelinePath: TimelineClasses.TimelinePath,
Timeline: TimelineClasses.Timeline
};
// Chart specific
H.Point.prototype.sonify = pointSonifyFunctions.pointSonify;
H.Point.prototype.cancelSonify = pointSonifyFunctions.pointCancelSonify;
H.Series.prototype.sonify = chartSonifyFunctions.seriesSonify;
H.extend(H.Chart.prototype, {
sonify: chartSonifyFunctions.chartSonify,
pauseSonify: chartSonifyFunctions.pause,
resumeSonify: chartSonifyFunctions.resume,
rewindSonify: chartSonifyFunctions.rewind,
cancelSonify: chartSonifyFunctions.cancel,
getCurrentSonifyPoints: chartSonifyFunctions.getCurrentPoints,
setSonifyCursor: chartSonifyFunctions.setCursor,
resetSonifyCursor: chartSonifyFunctions.resetCursor,
resetSonifyCursorEnd: chartSonifyFunctions.resetCursorEnd,
sonification: {}
});
@@ -0,0 +1,178 @@
/* *
*
* (c) 2009-2019 Øystein Moseng
*
* Utility functions for sonification.
*
* License: www.highcharts.com/license
*
* */
'use strict';
import musicalFrequencies from 'musicalFrequencies.js';
/**
* The SignalHandler class. Stores signal callbacks (event handlers), and
* provides an interface to register them, and emit signals. The word "event" is
* not used to avoid confusion with TimelineEvents.
*
* @requires module:modules/sonification
*
* @private
* @class
* @name Highcharts.SignalHandler
*
* @param {Array<string>} supportedSignals
* List of supported signal names.
*/
function SignalHandler(supportedSignals) {
this.init(supportedSignals || []);
}
SignalHandler.prototype.init = function (supportedSignals) {
this.supportedSignals = supportedSignals;
this.signals = {};
};
/**
* Register a set of signal callbacks with this SignalHandler.
* Multiple signal callbacks can be registered for the same signal.
* @private
* @param {object} signals - An object that contains a mapping from the signal
* name to the callbacks. Only supported events are considered.
*/
SignalHandler.prototype.registerSignalCallbacks = function (signals) {
var signalHandler = this;
signalHandler.supportedSignals.forEach(function (supportedSignal) {
if (signals[supportedSignal]) {
(
signalHandler.signals[supportedSignal] =
signalHandler.signals[supportedSignal] || []
).push(
signals[supportedSignal]
);
}
});
};
/**
* Clear signal callbacks, optionally by name.
* @private
* @param {Array<string>} [signalNames] - A list of signal names to clear. If
* not supplied, all signal callbacks are removed.
*/
SignalHandler.prototype.clearSignalCallbacks = function (signalNames) {
var signalHandler = this;
if (signalNames) {
signalNames.forEach(function (signalName) {
if (signalHandler.signals[signalName]) {
delete signalHandler.signals[signalName];
}
});
} else {
signalHandler.signals = {};
}
};
/**
* Emit a signal. Does nothing if the signal does not exist, or has no
* registered callbacks.
* @private
* @param {string} signalNames - Name of signal to emit.
* @param {*} data - Data to pass to the callback.
*/
SignalHandler.prototype.emitSignal = function (signalName, data) {
var retval;
if (this.signals[signalName]) {
this.signals[signalName].forEach(function (handler) {
var result = handler(data);
retval = result !== undefined ? result : retval;
});
}
return retval;
};
var utilities = {
// List of musical frequencies from C0 to C8
musicalFrequencies: musicalFrequencies,
// SignalHandler class
SignalHandler: SignalHandler,
/**
* Get a musical scale by specifying the semitones from 1-12 to include.
* 1: C, 2: C#, 3: D, 4: D#, 5: E, 6: F,
* 7: F#, 8: G, 9: G#, 10: A, 11: Bb, 12: B
* @private
* @param {Array<number>} semitones - Array of semitones from 1-12 to
* include in the scale. Duplicate entries are ignored.
* @return {Array<number>} Array of frequencies from C0 to C8 that are
* included in this scale.
*/
getMusicalScale: function (semitones) {
return musicalFrequencies.filter(function (freq, i) {
var interval = i % 12 + 1;
return semitones.some(function (allowedInterval) {
return allowedInterval === interval;
});
});
},
/**
* Calculate the extreme values in a chart for a data prop.
* @private
* @param {Highcharts.Chart} chart - The chart
* @param {string} prop - The data prop to find extremes for
* @return {object} Object with min and max properties
*/
calculateDataExtremes: function (chart, prop) {
return chart.series.reduce(function (extremes, series) {
// We use cropped points rather than series.data here, to allow
// users to zoom in for better fidelity.
series.points.forEach(function (point) {
var val = point[prop] !== undefined ?
point[prop] : point.options[prop];
extremes.min = Math.min(extremes.min, val);
extremes.max = Math.max(extremes.max, val);
});
return extremes;
}, {
min: Infinity,
max: -Infinity
});
},
/**
* Translate a value on a virtual axis. Creates a new, virtual, axis with a
* min and max, and maps the relative value onto this axis.
* @private
* @param {number} value - The relative data value to translate.
* @param {object} dataExtremes - The possible extremes for this value.
* @param {object} limits - Limits for the virtual axis.
* @return {number} The value mapped to the virtual axis.
*/
virtualAxisTranslate: function (value, dataExtremes, limits) {
var lenValueAxis = dataExtremes.max - dataExtremes.min,
lenVirtualAxis = limits.max - limits.min,
virtualAxisValue = limits.min +
lenVirtualAxis * (value - dataExtremes.min) / lenValueAxis;
return lenValueAxis > 0 ?
Math.max(Math.min(virtualAxisValue, limits.max), limits.min) :
limits.min;
}
};
export default utilities;
@@ -0,0 +1,88 @@
/* *
* (c) 2016-2019 Torstein Honsi, Lars Cabrera
*
* License: www.highcharts.com/license
*/
'use strict';
import H from '../parts/Globals.js';
import '../parts/Utilities.js';
var Chart = H.Chart,
pick = H.pick;
/**
* For vertical axes only. Setting the static scale ensures that each tick unit
* is translated into a fixed pixel height. For example, setting the static
* scale to 24 results in each Y axis category taking up 24 pixels, and the
* height of the chart adjusts. Adding or removing items will make the chart
* resize.
*
* @sample gantt/xrange-series/demo/
* X-range series with static scale
*
* @type {number}
* @default 50
* @since 6.2.0
* @product gantt
* @apioption yAxis.staticScale
*/
H.addEvent(H.Axis, 'afterSetOptions', function () {
if (
!this.horiz &&
H.isNumber(this.options.staticScale) &&
!this.chart.options.chart.height
) {
this.staticScale = this.options.staticScale;
}
});
Chart.prototype.adjustHeight = function () {
if (this.redrawTrigger !== 'adjustHeight') {
(this.axes || []).forEach(function (axis) {
var chart = axis.chart,
animate = !!chart.initiatedScale && chart.options.animation,
staticScale = axis.options.staticScale,
height,
diff;
if (axis.staticScale && H.defined(axis.min)) {
height = pick(
axis.unitLength,
axis.max + axis.tickInterval - axis.min
) * staticScale;
// Minimum height is 1 x staticScale.
height = Math.max(height, staticScale);
diff = height - chart.plotHeight;
if (Math.abs(diff) >= 1) {
chart.plotHeight = height;
chart.redrawTrigger = 'adjustHeight';
chart.setSize(undefined, chart.chartHeight + diff, animate);
}
// Make sure clip rects have the right height before initial
// animation.
axis.series.forEach(function (series) {
var clipRect =
series.sharedClipKey && chart[series.sharedClipKey];
if (clipRect) {
clipRect.attr({
height: chart.plotHeight
});
}
});
}
});
this.initiatedScale = true;
}
this.redrawTrigger = null;
};
H.addEvent(Chart, 'render', Chart.prototype.adjustHeight);
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,132 @@
/* *
* Streamgraph module
*
* (c) 2010-2019 Torstein Honsi
*
* License: www.highcharts.com/license
*/
'use strict';
import H from '../parts/Globals.js';
import '../parts/AreaSeries.js';
var seriesType = H.seriesType;
/**
* @private
* @class
* @name Highcharts.seriesTypes.streamgraph
*
* @augments Highcharts.Series
*/
seriesType('streamgraph', 'areaspline'
/**
* A streamgraph is a type of stacked area graph which is displaced around a
* central axis, resulting in a flowing, organic shape.
*
* @sample {highcharts|highstock} highcharts/demo/streamgraph/
* Streamgraph
*
* @extends plotOptions.areaspline
* @since 6.0.0
* @product highcharts highstock
* @optionparent plotOptions.streamgraph
*/
, {
fillOpacity: 1,
lineWidth: 0,
marker: {
enabled: false
},
stacking: 'stream'
// Prototype functions
}, {
negStacks: false,
// Modifier function for stream stacks. It simply moves the point up or
// down in order to center the full stack vertically.
streamStacker: function (pointExtremes, stack, i) {
// Y bottom value
pointExtremes[0] -= stack.total / 2;
// Y value
pointExtremes[1] -= stack.total / 2;
// Record the Y data for use when getting axis extremes
this.stackedYData[i] = pointExtremes;
}
});
/**
* A `streamgraph` series. If the [type](#series.streamgraph.type) option is not
* specified, it is inherited from [chart.type](#chart.type).
*
* @extends series,plotOptions.streamgraph
* @excluding dataParser, dataURL
* @product highcharts highstock
* @apioption series.streamgraph
*/
/**
* An array of data points for the series. For the `streamgraph` series type,
* points can be given in the following ways:
*
* 1. An array of numerical values. In this case, the numerical values
* will be interpreted as `y` options. The `x` values will be automatically
* calculated, either starting at 0 and incremented by 1, or from `pointStart`
* and `pointInterval` given in the series options. If the axis has
* categories, these will be used. Example:
*
* ```js
* data: [0, 5, 3, 5]
* ```
*
* 2. An array of arrays with 2 values. In this case, the values correspond
* to `x,y`. If the first value is a string, it is applied as the name
* of the point, and the `x` value is inferred.
*
* ```js
* data: [
* [0, 9],
* [1, 7],
* [2, 6]
* ]
* ```
*
* 3. An array of objects with named values. The following snippet shows only a
* few settings, see the complete options set below. If the total number of data
* points exceeds the series' [turboThreshold](#series.area.turboThreshold),
* this option is not available.
*
* ```js
* data: [{
* x: 1,
* y: 9,
* name: "Point2",
* color: "#00FF00"
* }, {
* x: 1,
* y: 6,
* name: "Point1",
* color: "#FF00FF"
* }]
* ```
*
* @sample {highcharts} highcharts/chart/reflow-true/
* Numerical values
* @sample {highcharts} highcharts/series/data-array-of-arrays/
* Arrays of numeric x and y
* @sample {highcharts} highcharts/series/data-array-of-arrays-datetime/
* Arrays of datetime x and y
* @sample {highcharts} highcharts/series/data-array-of-name-value/
* Arrays of point.name and y
* @sample {highcharts} highcharts/series/data-array-of-objects/
* Config objects
*
* @type {Array<number|Array<(number|string),number>|*>}
* @extends series.line.data
* @product highcharts highstock
* @apioption series.streamgraph.data
*/
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,769 @@
/* *
* Tilemaps module
*
* (c) 2010-2017 Highsoft AS
* Author: Øystein Moseng
*
* License: www.highcharts.com/license
*/
'use strict';
import H from '../parts/Globals.js';
import '../parts-map/HeatmapSeries.js';
var seriesType = H.seriesType,
pick = H.pick,
// Utility func to get the middle number of 3
between = function (x, a, b) {
return Math.min(Math.max(a, x), b);
},
// Utility func to get padding definition from tile size division
tilePaddingFromTileSize = function (series, xDiv, yDiv) {
var options = series.options;
return {
xPad: (options.colsize || 1) / -xDiv,
yPad: (options.rowsize || 1) / -yDiv
};
};
// Map of shape types.
H.tileShapeTypes = {
// Hexagon shape type.
hexagon: {
alignDataLabel: H.seriesTypes.scatter.prototype.alignDataLabel,
getSeriesPadding: function (series) {
return tilePaddingFromTileSize(series, 3, 2);
},
haloPath: function (size) {
if (!size) {
return [];
}
var hexagon = this.tileEdges;
return [
'M', hexagon.x2 - size, hexagon.y1 + size,
'L', hexagon.x3 + size, hexagon.y1 + size,
hexagon.x4 + size * 1.5, hexagon.y2,
hexagon.x3 + size, hexagon.y3 - size,
hexagon.x2 - size, hexagon.y3 - size,
hexagon.x1 - size * 1.5, hexagon.y2,
'Z'
];
},
translate: function () {
var series = this,
options = series.options,
xAxis = series.xAxis,
yAxis = series.yAxis,
seriesPointPadding = options.pointPadding || 0,
xPad = (options.colsize || 1) / 3,
yPad = (options.rowsize || 1) / 2,
yShift;
series.generatePoints();
series.points.forEach(function (point) {
var x1 = between(
Math.floor(
xAxis.len -
xAxis.translate(point.x - xPad * 2, 0, 1, 0, 1)
), -xAxis.len, 2 * xAxis.len
),
x2 = between(
Math.floor(
xAxis.len -
xAxis.translate(point.x - xPad, 0, 1, 0, 1)
), -xAxis.len, 2 * xAxis.len
),
x3 = between(
Math.floor(
xAxis.len -
xAxis.translate(point.x + xPad, 0, 1, 0, 1)
), -xAxis.len, 2 * xAxis.len
),
x4 = between(
Math.floor(
xAxis.len -
xAxis.translate(point.x + xPad * 2, 0, 1, 0, 1)
), -xAxis.len, 2 * xAxis.len
),
y1 = between(
Math.floor(yAxis.translate(point.y - yPad, 0, 1, 0, 1)),
-yAxis.len,
2 * yAxis.len
),
y2 = between(
Math.floor(yAxis.translate(point.y, 0, 1, 0, 1)),
-yAxis.len,
2 * yAxis.len
),
y3 = between(
Math.floor(yAxis.translate(point.y + yPad, 0, 1, 0, 1)),
-yAxis.len,
2 * yAxis.len
),
pointPadding = pick(point.pointPadding, seriesPointPadding),
// We calculate the point padding of the midpoints to
// preserve the angles of the shape.
midPointPadding = pointPadding *
Math.abs(x2 - x1) / Math.abs(y3 - y2),
xMidPadding = xAxis.reversed ?
-midPointPadding : midPointPadding,
xPointPadding = xAxis.reversed ?
-pointPadding : pointPadding,
yPointPadding = yAxis.reversed ?
-pointPadding : pointPadding;
// Shift y-values for every second grid column
if (point.x % 2) {
yShift = yShift || Math.round(Math.abs(y3 - y1) / 2) *
// We have to reverse the shift for reversed y-axes
(yAxis.reversed ? -1 : 1);
y1 += yShift;
y2 += yShift;
y3 += yShift;
}
// Set plotX and plotY for use in K-D-Tree and more
point.plotX = point.clientX = (x2 + x3) / 2;
point.plotY = y2;
// Apply point padding to translated coordinates
x1 += xMidPadding + xPointPadding;
x2 += xPointPadding;
x3 -= xPointPadding;
x4 -= xMidPadding + xPointPadding;
y1 -= yPointPadding;
y3 += yPointPadding;
// Store points for halo creation
point.tileEdges = {
x1: x1, x2: x2, x3: x3, x4: x4, y1: y1, y2: y2, y3: y3
};
// Finally set the shape for this point
point.shapeType = 'path';
point.shapeArgs = {
d: [
'M', x2, y1,
'L', x3, y1,
x4, y2,
x3, y3,
x2, y3,
x1, y2,
'Z'
]
};
});
series.translateColors();
}
},
// Diamond shape type.
diamond: {
alignDataLabel: H.seriesTypes.scatter.prototype.alignDataLabel,
getSeriesPadding: function (series) {
return tilePaddingFromTileSize(series, 2, 2);
},
haloPath: function (size) {
if (!size) {
return [];
}
var diamond = this.tileEdges;
return [
'M', diamond.x2, diamond.y1 + size,
'L', diamond.x3 + size, diamond.y2,
diamond.x2, diamond.y3 - size,
diamond.x1 - size, diamond.y2,
'Z'
];
},
translate: function () {
var series = this,
options = series.options,
xAxis = series.xAxis,
yAxis = series.yAxis,
seriesPointPadding = options.pointPadding || 0,
xPad = (options.colsize || 1),
yPad = (options.rowsize || 1) / 2,
yShift;
series.generatePoints();
series.points.forEach(function (point) {
var x1 = between(
Math.round(
xAxis.len -
xAxis.translate(point.x - xPad, 0, 1, 0, 0)
), -xAxis.len, 2 * xAxis.len
),
x2 = between(
Math.round(
xAxis.len -
xAxis.translate(point.x, 0, 1, 0, 0)
), -xAxis.len, 2 * xAxis.len
),
x3 = between(
Math.round(
xAxis.len -
xAxis.translate(point.x + xPad, 0, 1, 0, 0)
), -xAxis.len, 2 * xAxis.len
),
y1 = between(
Math.round(yAxis.translate(point.y - yPad, 0, 1, 0, 0)),
-yAxis.len,
2 * yAxis.len
),
y2 = between(
Math.round(yAxis.translate(point.y, 0, 1, 0, 0)),
-yAxis.len,
2 * yAxis.len
),
y3 = between(
Math.round(yAxis.translate(point.y + yPad, 0, 1, 0, 0)),
-yAxis.len,
2 * yAxis.len
),
pointPadding = pick(point.pointPadding, seriesPointPadding),
// We calculate the point padding of the midpoints to
// preserve the angles of the shape.
midPointPadding = pointPadding *
Math.abs(x2 - x1) / Math.abs(y3 - y2),
xPointPadding = xAxis.reversed ?
-midPointPadding : midPointPadding,
yPointPadding = yAxis.reversed ?
-pointPadding : pointPadding;
// Shift y-values for every second grid column
// We have to reverse the shift for reversed y-axes
if (point.x % 2) {
yShift = Math.abs(y3 - y1) / 2 * (yAxis.reversed ? -1 : 1);
y1 += yShift;
y2 += yShift;
y3 += yShift;
}
// Set plotX and plotY for use in K-D-Tree and more
point.plotX = point.clientX = x2;
point.plotY = y2;
// Apply point padding to translated coordinates
x1 += xPointPadding;
x3 -= xPointPadding;
y1 -= yPointPadding;
y3 += yPointPadding;
// Store points for halo creation
point.tileEdges = {
x1: x1, x2: x2, x3: x3, y1: y1, y2: y2, y3: y3
};
// Set this point's shape parameters
point.shapeType = 'path';
point.shapeArgs = {
d: [
'M', x2, y1,
'L', x3, y2,
x2, y3,
x1, y2,
'Z'
]
};
});
series.translateColors();
}
},
// Circle shape type.
circle: {
alignDataLabel: H.seriesTypes.scatter.prototype.alignDataLabel,
getSeriesPadding: function (series) {
return tilePaddingFromTileSize(series, 2, 2);
},
haloPath: function (size) {
return H.seriesTypes.scatter.prototype.pointClass.prototype.haloPath
.call(
this,
size + (size && this.radius)
);
},
translate: function () {
var series = this,
options = series.options,
xAxis = series.xAxis,
yAxis = series.yAxis,
seriesPointPadding = options.pointPadding || 0,
yRadius = (options.rowsize || 1) / 2,
colsize = (options.colsize || 1),
colsizePx,
yRadiusPx,
xRadiusPx,
radius,
forceNextRadiusCompute = false;
series.generatePoints();
series.points.forEach(function (point) {
var x = between(
Math.round(
xAxis.len -
xAxis.translate(point.x, 0, 1, 0, 0)
), -xAxis.len, 2 * xAxis.len
),
y = between(
Math.round(yAxis.translate(point.y, 0, 1, 0, 0)),
-yAxis.len,
2 * yAxis.len
),
pointPadding = seriesPointPadding,
hasPerPointPadding = false;
// If there is point padding defined on a single point, add it
if (point.pointPadding !== undefined) {
pointPadding = point.pointPadding;
hasPerPointPadding = true;
forceNextRadiusCompute = true;
}
// Find radius if not found already.
// Use the smallest one (x vs y) to avoid overlap.
// Note that the radius will be recomputed for each series.
// Ideal (max) x radius is dependent on y radius:
/*
* (circle 2)
* (circle 3)
| yRadiusPx
(circle 1) *-------|
colsizePx
The distance between circle 1 and 3 (and circle 2 and 3) is
2r, which is the hypotenuse of the triangle created by
colsizePx and yRadiusPx. If the distance between circle 2
and circle 1 is less than 2r, we use half of that distance
instead (yRadiusPx).
*/
if (!radius || forceNextRadiusCompute) {
colsizePx = Math.abs(
between(
Math.floor(
xAxis.len -
xAxis.translate(point.x + colsize, 0, 1, 0, 0)
), -xAxis.len, 2 * xAxis.len
) - x
);
yRadiusPx = Math.abs(
between(
Math.floor(
yAxis.translate(point.y + yRadius, 0, 1, 0, 0)
), -yAxis.len, 2 * yAxis.len
) - y
);
xRadiusPx = Math.floor(
Math.sqrt(
(colsizePx * colsizePx + yRadiusPx * yRadiusPx)
) / 2
);
radius = Math.min(
colsizePx, xRadiusPx, yRadiusPx
) - pointPadding;
// If we have per point padding we need to always compute
// the radius for this point and the next. If we used to
// have per point padding but don't anymore, don't force
// compute next radius.
if (forceNextRadiusCompute && !hasPerPointPadding) {
forceNextRadiusCompute = false;
}
}
// Shift y-values for every second grid column.
// Note that we always use the optimal y axis radius for this.
// Also note: We have to reverse the shift for reversed y-axes.
if (point.x % 2) {
y += yRadiusPx * (yAxis.reversed ? -1 : 1);
}
// Set plotX and plotY for use in K-D-Tree and more
point.plotX = point.clientX = x;
point.plotY = y;
// Save radius for halo
point.radius = radius;
// Set this point's shape parameters
point.shapeType = 'circle';
point.shapeArgs = {
x: x,
y: y,
r: radius
};
});
series.translateColors();
}
},
// Square shape type.
square: {
alignDataLabel: H.seriesTypes.heatmap.prototype.alignDataLabel,
translate: H.seriesTypes.heatmap.prototype.translate,
getSeriesPadding: function () {
},
haloPath: H.seriesTypes.heatmap.prototype.pointClass.prototype.haloPath
}
};
// Extension to add pixel padding for series. Uses getSeriesPixelPadding on each
// series and adds the largest padding required. If no series has this function
// defined, we add nothing.
H.addEvent(H.Axis, 'afterSetAxisTranslation', function () {
if (this.recomputingForTilemap) {
return;
}
var axis = this,
// Find which series' padding to use
seriesPadding = axis.series
.map(function (series) {
return series.getSeriesPixelPadding &&
series.getSeriesPixelPadding(axis);
})
.reduce(function (a, b) {
return (a && a.padding) > (b && b.padding) ? a : b;
}, undefined) ||
{
padding: 0,
axisLengthFactor: 1
},
lengthPadding = Math.round(
seriesPadding.padding * seriesPadding.axisLengthFactor
);
// Don't waste time on this if we're not adding extra padding
if (seriesPadding.padding) {
// Recompute translation with new axis length now (minus padding)
axis.len -= lengthPadding;
axis.recomputingForTilemap = true;
axis.setAxisTranslation();
delete axis.recomputingForTilemap;
axis.minPixelPadding += seriesPadding.padding;
axis.len += lengthPadding;
}
});
/**
* @private
* @class
* @name Highcharts.seriesTypes.tilemap
*
* @augments Highcharts.Series
*/
seriesType('tilemap', 'heatmap'
/**
* A tilemap series is a type of heatmap where the tile shapes are configurable.
*
* @sample highcharts/demo/honeycomb-usa/
* Honeycomb tilemap, USA
* @sample maps/plotoptions/honeycomb-brazil/
* Honeycomb tilemap, Brazil
* @sample maps/plotoptions/honeycomb-china/
* Honeycomb tilemap, China
* @sample maps/plotoptions/honeycomb-europe/
* Honeycomb tilemap, Europe
* @sample maps/demo/circlemap-africa/
* Circlemap tilemap, Africa
* @sample maps/demo/diamondmap
* Diamondmap tilemap
*
* @extends plotOptions.heatmap
* @since 6.0.0
* @excluding jitter, joinBy, shadow, allAreas, mapData, data
* @product highcharts highmaps
* @optionparent plotOptions.tilemap
*/
, { // Default options
states: {
hover: {
halo: {
enabled: true,
size: 2,
opacity: 0.5,
attributes: {
zIndex: 3
}
}
}
},
/**
* The padding between points in the tilemap.
*
* @sample maps/plotoptions/tilemap-pointpadding
* Point padding on tiles
*/
pointPadding: 2,
/**
* The column size - how many X axis units each column in the tilemap
* should span. Works as in [Heatmaps](#plotOptions.heatmap.colsize).
*
* @sample {highcharts} maps/demo/heatmap/
* One day
* @sample {highmaps} maps/demo/heatmap/
* One day
*
* @type {number}
* @default 1
* @product highcharts highmaps
* @apioption plotOptions.tilemap.colsize
*/
/**
* The row size - how many Y axis units each tilemap row should span.
* Analogous to [colsize](#plotOptions.tilemap.colsize).
*
* @sample {highcharts} maps/demo/heatmap/
* 1 by default
* @sample {highmaps} maps/demo/heatmap/
* 1 by default
*
* @type {number}
* @default 1
* @product highcharts highmaps
* @apioption plotOptions.tilemap.rowsize
*/
/**
* The shape of the tiles in the tilemap. Possible values are `hexagon`,
* `circle`, `diamond`, and `square`.
*
* @sample maps/demo/circlemap-africa
* Circular tile shapes
* @sample maps/demo/diamondmap
* Diamond tile shapes
*
* @validvalue ["circle", "diamond", "hexagon", "square"]
*/
tileShape: 'hexagon'
}, { // Prototype functions
// Set tile shape object on series
setOptions: function () {
// Call original function
var ret = H.seriesTypes.heatmap.prototype.setOptions.apply(
this,
Array.prototype.slice.call(arguments)
);
this.tileShape = H.tileShapeTypes[ret.tileShape];
return ret;
},
// Use the shape's defined data label alignment function
alignDataLabel: function () {
return this.tileShape.alignDataLabel.apply(
this,
Array.prototype.slice.call(arguments)
);
},
// Get metrics for padding of axis for this series
getSeriesPixelPadding: function (axis) {
var isX = axis.isXAxis,
padding = this.tileShape.getSeriesPadding(this),
coord1,
coord2;
// If the shape type does not require padding, return no-op padding
if (!padding) {
return {
padding: 0,
axisLengthFactor: 1
};
}
// Use translate to compute how far outside the points we
// draw, and use this difference as padding.
coord1 = Math.round(
axis.translate(
isX ?
padding.xPad * 2 :
padding.yPad,
0, 1, 0, 1
)
);
coord2 = Math.round(
axis.translate(
isX ? padding.xPad : 0,
0, 1, 0, 1
)
);
return {
padding: Math.abs(coord1 - coord2) || 0,
// Offset the yAxis length to compensate for shift. Setting the
// length factor to 2 would add the same margin to max as min.
// Now we only add a slight bit of the min margin to max, as we
// don't actually draw outside the max bounds. For the xAxis we
// draw outside on both sides so we add the same margin to min
// and max.
axisLengthFactor: isX ? 2 : 1.1
};
},
// Use translate from tileShape
translate: function () {
return this.tileShape.translate.apply(
this,
Array.prototype.slice.call(arguments)
);
}
}, H.extend({
/**
* @private
* @function Highcharts.Point#haloPath
*
* @return {Highcharts.SVGPathArray}
*/
haloPath: function () {
return this.series.tileShape.haloPath.apply(
this,
Array.prototype.slice.call(arguments)
);
}
}, H.colorPointMixin));
/**
* A `tilemap` series. If the [type](#series.tilemap.type) option is
* not specified, it is inherited from [chart.type](#chart.type).
*
* @extends series,plotOptions.tilemap
* @excluding allAreas, dataParser, dataURL, joinBy, mapData, marker,
* pointRange, shadow, stack
* @product highcharts highmaps
* @apioption series.tilemap
*/
/**
* An array of data points for the series. For the `tilemap` series
* type, points can be given in the following ways:
*
* 1. An array of arrays with 3 or 2 values. In this case, the values correspond
* to `x,y,value`. If the first value is a string, it is applied as the name
* of the point, and the `x` value is inferred. The `x` value can also be
* omitted, in which case the inner arrays should be of length 2\. Then the
* `x` value is automatically calculated, either starting at 0 and
* incremented by 1, or from `pointStart` and `pointInterval` given in the
* series options.
* ```js
* data: [
* [0, 9, 7],
* [1, 10, 4],
* [2, 6, 3]
* ]
* ```
*
* 2. An array of objects with named values. The objects are point configuration
* objects as seen below. If the total number of data points exceeds the
* series' [turboThreshold](#series.tilemap.turboThreshold), this option is
* not available.
* ```js
* data: [{
* x: 1,
* y: 3,
* value: 10,
* name: "Point2",
* color: "#00FF00"
* }, {
* x: 1,
* y: 7,
* value: 10,
* name: "Point1",
* color: "#FF00FF"
* }]
* ```
*
* Note that for some [tileShapes](#plotOptions.tilemap.tileShape) the grid
* coordinates are offset.
*
* @sample maps/series/tilemap-gridoffset
* Offset grid coordinates
* @sample {highcharts} highcharts/series/data-array-of-arrays/
* Arrays of numeric x and y
* @sample {highcharts} highcharts/series/data-array-of-arrays-datetime/
* Arrays of datetime x and y
* @sample {highcharts} highcharts/series/data-array-of-name-value/
* Arrays of point.name and y
* @sample {highcharts} highcharts/series/data-array-of-objects/
* Config objects
*
* @type {Array<Array<(number|string),number>|Array<(number|string),number,number>|*>}
* @extends series.heatmap.data
* @excluding marker
* @product highcharts highmaps
* @apioption series.tilemap.data
*/
/**
* The color of the point. In tilemaps the point color is rarely set
* explicitly, as we use the color to denote the `value`. Options for
* this are set in the [colorAxis](#colorAxis) configuration.
*
* @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
* @product highcharts highmaps
* @apioption series.tilemap.data.color
*/
/**
* The x coordinate of the point.
*
* Note that for some [tileShapes](#plotOptions.tilemap.tileShape) the grid
* coordinates are offset.
*
* @sample maps/series/tilemap-gridoffset
* Offset grid coordinates
*
* @type {number}
* @product highcharts highmaps
* @apioption series.tilemap.data.x
*/
/**
* The y coordinate of the point.
*
* Note that for some [tileShapes](#plotOptions.tilemap.tileShape) the grid
* coordinates are offset.
*
* @sample maps/series/tilemap-gridoffset
* Offset grid coordinates
*
* @type {number}
* @product highcharts highmaps
* @apioption series.tilemap.data.y
*/
@@ -0,0 +1,750 @@
/* *
*
* Experimental Timeline Series.
* Note: This API is in alpha stage and will be changed before final release.
*
* (c) 2010-2019 Highsoft AS
*
* Author: Daniel Studencki
*
* License: www.highcharts.com/license
*
* */
'use strict';
import H from '../parts/Globals.js';
var addEvent = H.addEvent,
extend = H.extend,
defined = H.defined,
LegendSymbolMixin = H.LegendSymbolMixin,
TrackerMixin = H.TrackerMixin,
merge = H.merge,
pick = H.pick,
Point = H.Point,
Series = H.Series,
undocumentedSeriesType = H.seriesType;
/* *
* The timeline series type.
*
* @private
* @class
* @name Highcharts.seriesTypes.timeline
*
* @augments Highcharts.Series
*/
undocumentedSeriesType('timeline', 'line',
/* *
* The timeline series presents given events along a drawn line.
*
* @sample highcharts/series-timeline/alternate-labels Timeline series
*
* @extends plotOptions.line
* @since 7.0.0
* @product highcharts
* @excluding animationLimit, boostThreshold, connectEnds, connectNulls,
* cropThreshold, dashStyle, findNearestPointBy,
* getExtremesFromAll, lineWidth, negativeColor,
* pointInterval, pointIntervalUnit, pointPlacement,
* pointStart, softThreshold, stacking, step, threshold,
* turboThreshold, zoneAxis, zones
* @optionparent plotOptions.timeline
*/
{
colorByPoint: true,
stickyTracking: false,
ignoreHiddenPoint: true,
legendType: 'point',
lineWidth: 0,
tooltip: {
headerFormat: '<span style="color:{point.color}">● </span>' +
'<span style="font-weight: bold;">{point.point.date}</span><br/>',
pointFormat: '{point.description}'
},
states: {
hover: {
lineWidthPlus: 5,
halo: {
size: 0
}
}
},
dataLabels: {
enabled: true,
allowOverlap: true,
/* *
* The width of the line connecting the data label to the point.
*
*
* In styled mode, the connector stroke width is given in the
* `.highcharts-data-label-connector` class.
*
* @type {Number}
* @default 1
* @sample {highcharts} highcharts/series-timeline/connector-styles
* Custom connector width and color
*/
connectorWidth: 1,
/* *
* The color of the line connecting the data label to the point.
*
* In styled mode, the connector stroke is given in the
* `.highcharts-data-label-connector` class.
*
* @type {String}
* @sample {highcharts} highcharts/series-timeline/connector-styles
* Custom connector width and color
*/
connectorColor: '#000000',
backgroundColor: '#ffffff',
/* *
* @type {Highcharts.FormatterCallbackFunction<object>}
* @default function () {
* var format;
*
* if (!this.series.chart.styledMode) {
* format = '<span style="color:' + this.point.color +
* '">● </span><span style="font-weight: bold;" > ' +
* (this.point.date || '') + '</span><br/>' +
* (this.point.label || '');
* } else {
* format = '<span>● </span>' +
* '<span>' + (this.point.date || '') +
* '</span><br/>' + (this.point.label || '');
* }
* return format;
* }
* @apioption plotOptions.timeline.dataLabels.formatter
*/
formatter: function () {
var format;
if (!this.series.chart.styledMode) {
format = '<span style="color:' + this.point.color +
'">● </span><span style="font-weight: bold;" > ' +
(this.point.date || '') + '</span><br/>' +
(this.point.label || '');
} else {
format = '<span>● </span>' +
'<span>' + (this.point.date || '') +
'</span><br/>' + (this.point.label || '');
}
return format;
},
borderWidth: 1,
borderColor: '#666666',
/* *
* A pixel value defining the distance between the data label
* and the point. Negative numbers puts the label on top
* of the point.
*
* @type {Number}
* @default 100
*/
distance: 100,
/* *
* Whether to position data labels alternately. For example, if
* [distance](#plotOptions.timeline.dataLabels.distance) is set
* equal to `100`, then the first data label 's distance will be
* set equal to `100`, the second one equal to `-100`, and so on.
*
* @type {Boolean}
* @default true
* @sample {highcharts} highcharts/series-timeline/alternate-disabled
* Alternate disabled
*/
alternate: true,
verticalAlign: 'middle',
color: '#333333'
},
marker: {
enabledThreshold: 0,
symbol: 'square',
height: 15
}
},
/* *
* @lends Highcharts.Series#
*/
{
requireSorting: false,
trackerGroups: ['markerGroup', 'dataLabelsGroup'],
// Use a simple symbol from LegendSymbolMixin
drawLegendSymbol: LegendSymbolMixin.drawRectangle,
// Use a group of trackers from TrackerMixin
drawTracker: TrackerMixin.drawTrackerPoint,
init: function () {
var series = this;
Series.prototype.init.apply(series, arguments);
// Distribute data labels before rendering them. Distribution is
// based on the 'dataLabels.distance' and 'dataLabels.alternate'
// property.
addEvent(series, 'drawDataLabels', function () {
// Delete the oldTextWidth parameter, in order to force
// adjusting data label wrapper box width. It's needed only when
// useHTML is enabled. This prevents the data label text getting
// out of the box range.
if (series.options.dataLabels.useHTML) {
series.points.forEach(function (p) {
if (p.visible && p.dataLabel) {
delete p.dataLabel.text.oldTextWidth;
}
});
}
// Distribute data labels basing on defined algorithm.
series.distributeDL();
});
addEvent(series, 'afterDrawDataLabels', function () {
var seriesOptions = series.options,
options = seriesOptions.dataLabels,
hasRendered = series.hasRendered || 0,
defer = pick(options.defer, !!seriesOptions.animation),
connectorsGroup = series.connectorsGroup,
dataLabel;
// Create (or redraw) the group for all connectors.
connectorsGroup = series.plotGroup(
'connectorsGroup',
'data-labels-connectors',
defer && !hasRendered ? 'hidden' : 'visible',
options.zIndex || 5
);
// Draw or align connector for each point.
series.points.forEach(function (point) {
dataLabel = point.dataLabel;
if (dataLabel) {
// Within this wrap method is necessary to save the
// current animation params, because the data label
// target position (after animation) is needed to align
// connectors.
dataLabel.animate = function (params) {
if (this.targetPosition) {
this.targetPosition = params;
}
return H.SVGElement.prototype.animate.apply(
this,
arguments
);
};
// Initialize the targetPosition field within data label
// object. It's necessary because there is need to know
// expected position of specific data label, when
// aligning connectors. This field is overrided inside
// of SVGElement.animate() wrapped method.
if (!dataLabel.targetPosition) {
dataLabel.targetPosition = {};
}
return !point.connector ?
point.drawConnector() :
point.alignConnector();
}
});
// Animate connectors group. It's animated in the same way like
// dataLabels, and also depends on dataLabels.defer parameter.
if (defer) {
connectorsGroup.attr({
opacity: +hasRendered
});
if (!hasRendered) {
addEvent(series, 'afterAnimate', function () {
if (series.visible) {
connectorsGroup.show(true);
}
connectorsGroup[
seriesOptions.animation ? 'animate' : 'attr'
]({
opacity: 1
}, {
duration: 200
});
});
}
}
});
},
alignDataLabel: function (point, dataLabel) {
var series = this,
isInverted = series.chart.inverted,
visiblePoints = series.visibilityMap.filter(function (point) {
return point;
}),
visiblePointsCount = series.visiblePointsCount,
pointIndex = visiblePoints.indexOf(point),
isFirstOrLast = !pointIndex ||
pointIndex === visiblePointsCount - 1,
dataLabelsOptions = series.options.dataLabels,
userDLOptions = point.userDLOptions || {},
// Define multiplier which is used to calculate data label
// width. If data labels are alternate, they have two times more
// space to adapt (excepting first and last ones, which has only
// one and half), than in case of placing all data labels side
// by side.
multiplier = dataLabelsOptions.alternate ?
(isFirstOrLast ? 1.5 : 2) :
1,
distance,
availableSpace = Math.floor(
series.xAxis.len / visiblePointsCount
),
pad = dataLabel.padding,
targetDLWidth,
styles;
// Adjust data label width to the currently available space.
if (point.visible) {
distance = Math.abs(
userDLOptions.x || point.options.dataLabels.x
);
if (isInverted) {
targetDLWidth = (
(distance - pad) * 2 - (point.itemHeight / 2)
);
styles = {
width: targetDLWidth,
// Apply ellipsis when data label height is exceeded.
textOverflow: dataLabel.width / targetDLWidth *
dataLabel.height / 2 > availableSpace * multiplier ?
'ellipsis' : 'none'
};
} else {
styles = {
width: userDLOptions.width ||
dataLabelsOptions.width ||
availableSpace * multiplier - (pad * 2)
};
}
dataLabel.css(styles);
if (!series.chart.styledMode) {
dataLabel.shadow({});
}
}
Series.prototype.alignDataLabel.apply(series, arguments);
},
processData: function () {
var series = this,
xMap = [],
base,
visiblePoints = 0,
i;
series.visibilityMap = series.getVisibilityMap();
// Calculate currently visible points.
series.visibilityMap.forEach(function (point) {
if (point) {
visiblePoints++;
}
});
series.visiblePointsCount = visiblePoints;
base = series.xAxis.options.max / visiblePoints;
// Generate xData map.
for (i = 1; i <= visiblePoints; i++) {
xMap.push(
(base * i) - (base / 2)
);
}
// Set all hidden points y values as negatives, in order to move
// them away from plot area. It is necessary to avoid hiding data
// labels, when dataLabels.allowOverlap is set to false.
series.visibilityMap.forEach(function (vis, i) {
if (!vis) {
xMap.splice(i, 0, series.yData[i] === null ? null : -99);
}
});
series.xData = xMap;
series.yData = xMap.map(function (data) {
return defined(data) ? 1 : null;
});
Series.prototype.processData.call(this, arguments);
},
generatePoints: function () {
var series = this;
Series.prototype.generatePoints.apply(series);
series.points.forEach(function (point, i) {
point.applyOptions({
x: series.xData[i]
});
});
},
getVisibilityMap: function () {
var series = this,
map = (series.data.length ?
series.data : series.userOptions.data
).map(function (point) {
return (
point &&
point.visible !== false &&
!point.isNull
) ? point : false;
});
return map;
},
distributeDL: function () {
var series = this,
dataLabelsOptions = series.options.dataLabels,
options,
pointDLOptions,
newOptions = {},
visibilityIndex = 1,
distance = dataLabelsOptions.distance;
series.points.forEach(function (point) {
if (point.visible && !point.isNull) {
options = point.options;
pointDLOptions = point.options.dataLabels;
if (!series.hasRendered) {
point.userDLOptions = merge({}, pointDLOptions);
}
newOptions[series.chart.inverted ? 'x' : 'y'] =
dataLabelsOptions.alternate && visibilityIndex % 2 ?
-distance : distance;
options.dataLabels = merge(newOptions, point.userDLOptions);
visibilityIndex++;
}
});
},
markerAttribs: function (point, state) {
var series = this,
seriesMarkerOptions = series.options.marker,
seriesStateOptions,
pointMarkerOptions = point.marker || {},
symbol = (
pointMarkerOptions.symbol || seriesMarkerOptions.symbol
),
pointStateOptions,
width = pick(
pointMarkerOptions.width,
seriesMarkerOptions.width,
series.xAxis.len / series.visiblePointsCount
),
height = pick(
pointMarkerOptions.height,
seriesMarkerOptions.height
),
radius = 0,
attribs;
// Handle hover and select states
if (state) {
seriesStateOptions = seriesMarkerOptions.states[state] || {};
pointStateOptions = pointMarkerOptions.states &&
pointMarkerOptions.states[state] || {};
radius = pick(
pointStateOptions.radius,
seriesStateOptions.radius,
radius + (
seriesStateOptions.radiusPlus ||
0
)
);
}
point.hasImage = symbol && symbol.indexOf('url') === 0;
attribs = {
x: Math.floor(point.plotX) - (width / 2) - (radius / 2),
y: point.plotY - (height / 2) - (radius / 2),
width: width + radius,
height: height + radius
};
return attribs;
},
bindAxes: function () {
var series = this,
timelineXAxis = {
gridLineWidth: 0,
lineWidth: 0,
min: 0,
dataMin: 0,
minPadding: 0,
max: 100,
dataMax: 100,
maxPadding: 0,
title: null,
tickPositions: []
},
timelineYAxis = {
gridLineWidth: 0,
min: 0.5,
dataMin: 0.5,
minPadding: 0,
max: 1.5,
dataMax: 1.5,
maxPadding: 0,
title: null,
labels: {
enabled: false
}
};
Series.prototype.bindAxes.call(series);
extend(series.xAxis.options, timelineXAxis);
extend(series.yAxis.options, timelineYAxis);
}
},
/* *
* @lends Highcharts.Point#
*/
{
init: function () {
var point = Point.prototype.init.apply(this, arguments);
point.name = pick(point.name, point.date, 'Event');
point.y = 1;
return point;
},
// The setVisible method is taken from Pie series prototype, in order to
// prevent importing whole Pie series.
setVisible: function (vis, redraw) {
var point = this,
series = point.series,
chart = series.chart,
ignoreHiddenPoint = series.options.ignoreHiddenPoint;
redraw = pick(redraw, ignoreHiddenPoint);
if (vis !== point.visible) {
// If called without an argument, toggle visibility
point.visible = point.options.visible = vis =
vis === undefined ? !point.visible : vis;
// update userOptions.data
series.options.data[series.data.indexOf(point)] = point.options;
// Show and hide associated elements. This is performed
// regardless of redraw or not, because chart.redraw only
// handles full series.
['graphic', 'dataLabel', 'connector'].forEach(
function (key) {
if (point[key]) {
point[key][vis ? 'show' : 'hide'](true);
}
}
);
if (point.legendItem) {
chart.legend.colorizeItem(point, vis);
}
// #4170, hide halo after hiding point
if (!vis && point.state === 'hover') {
point.setState('');
}
// Handle ignore hidden slices
if (ignoreHiddenPoint) {
series.isDirty = true;
}
if (redraw) {
chart.redraw();
}
}
},
setState: function () {
var proceed = Series.prototype.pointClass.prototype.setState;
// Prevent triggering the setState method on null points.
if (!this.isNull) {
proceed.apply(this, arguments);
}
},
getConnectorPath: function () {
var point = this,
chart = point.series.chart,
xAxisLen = point.series.xAxis.len,
inverted = chart.inverted,
direction = inverted ? 'x2' : 'y2',
dl = point.dataLabel,
targetDLPos = dl.targetPosition,
coords = {
x1: point.plotX,
y1: point.plotY,
x2: point.plotX,
y2: targetDLPos.y || dl.y
},
negativeDistance = (
coords[direction] < point.series.yAxis.len / 2
),
path;
// Recalculate coords when the chart is inverted.
if (inverted) {
coords = {
x1: point.plotY,
y1: xAxisLen - point.plotX,
x2: targetDLPos.x || dl.x,
y2: xAxisLen - point.plotX
};
}
// Subtract data label width or height from expected coordinate so
// that the connector would start from the appropriate edge.
if (negativeDistance) {
coords[direction] += dl[inverted ? 'width' : 'height'];
}
path = chart.renderer.crispLine([
'M',
coords.x1,
coords.y1,
'L',
coords.x2,
coords.y2
], dl.options.connectorWidth || 1);
return path;
},
drawConnector: function () {
var point = this,
series = point.series,
dlOptions = point.dataLabel.options = merge(
{}, series.options.dataLabels,
point.options.dataLabels
);
point.connector = series.chart.renderer
.path(point.getConnectorPath())
.add(series.connectorsGroup);
if (!series.chart.styledMode) {
point.connector.attr({
stroke: dlOptions.connectorColor,
'stroke-width': dlOptions.connectorWidth,
opacity: point.dataLabel.opacity
});
}
},
alignConnector: function () {
var point = this,
connector = point.connector,
bBox = connector.getBBox(),
isVisible = bBox.y > 0;
connector[isVisible ? 'animate' : 'attr']({
d: point.getConnectorPath()
});
}
});
// Hide/show connector related with a specific data label, after overlapping
// detected.
addEvent(H.Chart, 'afterHideOverlappingLabels', function () {
var series = this.series,
dataLabel,
connector;
series.forEach(function (series) {
if (series.points) {
series.points.forEach(function (point) {
dataLabel = point.dataLabel;
connector = point.connector;
if (
dataLabel &&
dataLabel.targetPosition &&
connector
) {
connector.attr({
opacity: dataLabel.targetPosition.opacity ||
dataLabel.newOpacity
});
}
});
}
});
});
/* *
* The `timeline` series. If the [type](#series.timeline.type) option is
* not specified, it is inherited from [chart.type](#chart.type).
*
* @extends series,plotOptions.timeline
* @excluding animationLimit, boostThreshold, connectEnds, connectNulls,
* cropThreshold, dashStyle, dataParser, dataURL, findNearestPointBy,
* getExtremesFromAll, lineWidth, negativeColor,
* pointInterval, pointIntervalUnit, pointPlacement, pointStart,
* softThreshold, stacking, stack, step, threshold, turboThreshold,
* zoneAxis, zones
* @product highcharts
* @apioption series.timeline
*/
/* *
* An array of data points for the series. For the `timeline` series type,
* points can be given with three general parameters, `date`, `label`,
* and `description`:
*
* Example:
*
* ```js
* series: [{
* type: 'timeline',
* data: [{
* date: 'Jan 2018',
* label: 'Some event label',
* description: 'Description to show in tooltip'
* }]
* }]
* ```
*
* @sample {highcharts} highcharts/series-timeline/alternate-labels
* Alternate labels
*
* @type {Array<number|*>}
* @extends series.line.data
* @excluding marker, x, y
* @product highcharts
* @apioption series.timeline.data
*/
/* *
* The date of event.
*
* @type {string}
* @product highcharts
* @apioption series.timeline.data.date
*/
/* *
* The label of event.
*
* @type {string}
* @product highcharts
* @apioption series.timeline.data.label
*/
/* *
* The description of event. This description will be shown in tooltip.
*
* @type {string}
* @product highcharts
* @apioption series.timeline.data.description
*/
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,459 @@
/* *
* Variable Pie module for Highcharts
*
* (c) 2010-2017 Grzegorz Blachliński
*
* License: www.highcharts.com/license
*/
'use strict';
import H from '../parts/Globals.js';
import '../parts/Utilities.js';
import '../parts/Options.js';
var pick = H.pick,
arrayMin = H.arrayMin,
arrayMax = H.arrayMax,
seriesType = H.seriesType,
pieProto = H.seriesTypes.pie.prototype;
/**
* The variablepie series type.
*
* @private
* @class
* @name Highcharts.seriesTypes.variablepie
*
* @augments Highcharts.Series
*/
seriesType(
'variablepie',
'pie',
/**
* A variable pie series is a two dimensional series type, where each point
* renders an Y and Z value. Each point is drawn as a pie slice where the
* size (arc) of the slice relates to the Y value and the radius of pie
* slice relates to the Z value. Requires `highcharts-more.js`.
*
* @sample {highcharts} highcharts/demo/variable-radius-pie/
* Variable-radius pie chart
*
* @extends plotOptions.pie
* @since 6.0.0
* @product highcharts
* @optionparent plotOptions.variablepie
*/
{
/**
* The minimum size of the points' radius related to chart's `plotArea`.
* If a number is set, it applies in pixels.
*
* @sample {highcharts} highcharts/variable-radius-pie/min-max-point-size/
* Example of minPointSize and maxPointSize
* @sample {highcharts} highcharts/variable-radius-pie/min-point-size-100/
* minPointSize set to 100
*
* @type {number|string}
* @since 6.0.0
*/
minPointSize: '10%',
/**
* The maximum size of the points' radius related to chart's `plotArea`.
* If a number is set, it applies in pixels.
*
* @sample {highcharts} highcharts/variable-radius-pie/min-max-point-size/
* Example of minPointSize and maxPointSize
*
* @type {number|string}
* @since 6.0.0
*/
maxPointSize: '100%',
/**
* The minimum possible z value for the point's radius calculation. If
* the point's Z value is smaller than zMin, the slice will be drawn
* according to the zMin value.
*
* @sample {highcharts} highcharts/variable-radius-pie/zmin-5/
* zMin set to 5, smaller z values are treated as 5
* @sample {highcharts} highcharts/variable-radius-pie/zmin-zmax/
* Series limited by both zMin and zMax
*
* @type {number}
* @since 6.0.0
*/
zMin: undefined,
/**
* The maximum possible z value for the point's radius calculation. If
* the point's Z value is bigger than zMax, the slice will be drawn
* according to the zMax value
*
* @sample {highcharts} highcharts/variable-radius-pie/zmin-zmax/
* Series limited by both zMin and zMax
*
* @type {number}
* @since 6.0.0
*/
zMax: undefined,
/**
* Whether the pie slice's value should be represented by the area or
* the radius of the slice. Can be either `area` or `radius`. The
* default, `area`, corresponds best to the human perception of the size
* of each pie slice.
*
* @sample {highcharts} highcharts/variable-radius-pie/sizeby/
* Difference between area and radius sizeBy
*
* @since 6.0.0
* @validvalue ["area", "radius"]
*/
sizeBy: 'area',
tooltip: {
pointFormat: '<span style="color:{point.color}">\u25CF</span> {series.name}<br/>Value: {point.y}<br/>Size: {point.z}<br/>'
}
}, {
pointArrayMap: ['y', 'z'],
parallelArrays: ['x', 'y', 'z'],
// It is needed to null series.center on chart redraw. Probably good
// idea will be to add this option in directly in pie series.
redraw: function () {
this.center = null;
pieProto.redraw.call(this, arguments);
},
// For arrayMin and arrayMax calculations array shouldn't have
// null/undefined/string values. In this case it is needed to check if
// points Z value is a Number.
zValEval: function (zVal) {
if (typeof zVal === 'number' && !isNaN(zVal)) {
return true;
}
return null;
},
// Before standard translate method for pie chart it is needed to
// calculate min/max radius of each pie slice based on its Z value.
calculateExtremes: function () {
var series = this,
chart = series.chart,
plotWidth = chart.plotWidth,
plotHeight = chart.plotHeight,
seriesOptions = series.options,
slicingRoom = 2 * (seriesOptions.slicedOffset || 0),
zMin,
zMax,
zData = series.zData,
smallestSize = Math.min(plotWidth, plotHeight) - slicingRoom,
extremes = {}, // Min and max size of pie slice.
// In pie charts size of a pie is changed to make space for
// dataLabels, then series.center is changing.
positions = series.center || series.getCenter();
['minPointSize', 'maxPointSize'].forEach(function (prop) {
var length = seriesOptions[prop],
isPercent = /%$/.test(length);
length = parseInt(length, 10);
extremes[prop] = isPercent ?
smallestSize * length / 100 :
length * 2; // Because it should be radius, not diameter.
});
series.minPxSize = positions[3] + extremes.minPointSize;
series.maxPxSize = Math.max(
Math.min(positions[2], extremes.maxPointSize),
positions[3] + extremes.minPointSize
);
if (zData.length) {
zMin = pick(
seriesOptions.zMin,
arrayMin(zData.filter(series.zValEval))
);
zMax = pick(
seriesOptions.zMax,
arrayMax(zData.filter(series.zValEval))
);
this.getRadii(zMin, zMax, series.minPxSize, series.maxPxSize);
}
},
/**
* Finding radius of series points based on their Z value and min/max Z
* value for all series.
*
* @private
* @function Highcharts.Series#getRadii
*
* @param {number} zMin
* Min threshold for Z value. If point's Z value is smaller that
* zMin, point will have the smallest possible radius.
*
* @param {number} zMax
* Max threshold for Z value. If point's Z value is bigger that
* zMax, point will have the biggest possible radius.
*
* @param {number} minSize
* Minimal pixel size possible for radius.
*
* @param {numbner} maxSize
* Minimal pixel size possible for radius.
*/
getRadii: function (zMin, zMax, minSize, maxSize) {
var i = 0,
pos,
zData = this.zData,
len = zData.length,
radii = [],
options = this.options,
sizeByArea = options.sizeBy !== 'radius',
zRange = zMax - zMin,
value,
radius;
// Calculate radius for all pie slice's based on their Z values
for (i; i < len; i++) {
// if zData[i] is null/undefined/string we need to take zMin for
// smallest radius.
value = this.zValEval(zData[i]) ? zData[i] : zMin;
if (value <= zMin) {
radius = minSize / 2;
} else if (value >= zMax) {
radius = maxSize / 2;
} else {
// Relative size, a number between 0 and 1
pos = zRange > 0 ? (value - zMin) / zRange : 0.5;
if (sizeByArea) {
pos = Math.sqrt(pos);
}
radius = Math.ceil(minSize + pos * (maxSize - minSize)) / 2;
}
radii.push(radius);
}
this.radii = radii;
},
// Extend translate by updating radius for each pie slice instead of
// using one global radius.
translate: function (positions) {
this.generatePoints();
var series = this,
cumulative = 0,
precision = 1000, // issue #172
options = series.options,
slicedOffset = options.slicedOffset,
connectorOffset = slicedOffset + (options.borderWidth || 0),
finalConnectorOffset,
start,
end,
angle,
startAngle = options.startAngle || 0,
startAngleRad = Math.PI / 180 * (startAngle - 90),
endAngleRad = Math.PI / 180 * (pick(
options.endAngle,
startAngle + 360
) - 90),
circ = endAngleRad - startAngleRad, // 2 * Math.PI,
points = series.points,
// the x component of the radius vector for a given point
radiusX,
radiusY,
labelDistance = options.dataLabels.distance,
ignoreHiddenPoint = options.ignoreHiddenPoint,
i,
len = points.length,
point,
pointRadii,
pointRadiusX,
pointRadiusY;
series.startAngleRad = startAngleRad;
series.endAngleRad = endAngleRad;
// Use calculateExtremes to get series.radii array.
series.calculateExtremes();
// Get positions - either an integer or a percentage string must be
// given. If positions are passed as a parameter, we're in a
// recursive loop for adjusting space for data labels.
if (!positions) {
series.center = positions = series.getCenter();
}
// Calculate the geometry for each point
for (i = 0; i < len; i++) {
point = points[i];
pointRadii = series.radii[i];
// Used for distance calculation for specific point.
point.labelDistance = pick(
point.options.dataLabels &&
point.options.dataLabels.distance,
labelDistance
);
// Saved for later dataLabels distance calculation.
series.maxLabelDistance = Math.max(
series.maxLabelDistance || 0,
point.labelDistance
);
// set start and end angle
start = startAngleRad + (cumulative * circ);
if (!ignoreHiddenPoint || point.visible) {
cumulative += point.percentage / 100;
}
end = startAngleRad + (cumulative * circ);
// set the shape
point.shapeType = 'arc';
point.shapeArgs = {
x: positions[0],
y: positions[1],
r: pointRadii,
innerR: positions[3] / 2,
start: Math.round(start * precision) / precision,
end: Math.round(end * precision) / precision
};
// The angle must stay within -90 and 270 (#2645)
angle = (end + start) / 2;
if (angle > 1.5 * Math.PI) {
angle -= 2 * Math.PI;
} else if (angle < -Math.PI / 2) {
angle += 2 * Math.PI;
}
// Center for the sliced out slice
point.slicedTranslation = {
translateX: Math.round(Math.cos(angle) * slicedOffset),
translateY: Math.round(Math.sin(angle) * slicedOffset)
};
// set the anchor point for tooltips
radiusX = Math.cos(angle) * positions[2] / 2;
radiusY = Math.sin(angle) * positions[2] / 2;
pointRadiusX = Math.cos(angle) * pointRadii;
pointRadiusY = Math.sin(angle) * pointRadii;
point.tooltipPos = [
positions[0] + radiusX * 0.7,
positions[1] + radiusY * 0.7
];
point.half = angle < -Math.PI / 2 || angle > Math.PI / 2 ?
1 :
0;
point.angle = angle;
// Set the anchor point for data labels. Use point.labelDistance
// instead of labelDistance // #1174
// finalConnectorOffset - not override connectorOffset value.
finalConnectorOffset = Math.min(
connectorOffset,
point.labelDistance / 5
); // #1678
point.labelPosition = {
natural: {
// initial position of the data label - it's utilized
// for finding the final position for the label
x: positions[0] + pointRadiusX +
Math.cos(angle) * point.labelDistance,
y: positions[1] + pointRadiusY +
Math.sin(angle) * point.labelDistance
},
'final': {
// used for generating connector path -
// initialized later in drawDataLabels function
// x: undefined,
// y: undefined
},
// left - pie on the left side of the data label
// right - pie on the right side of the data label
alignment: point.half ? 'right' : 'left',
connectorPosition: {
breakAt: { // used in connectorShapes.fixedOffset
x: positions[0] + pointRadiusX +
Math.cos(angle) * finalConnectorOffset,
y: positions[1] + pointRadiusY +
Math.sin(angle) * finalConnectorOffset
},
touchingSliceAt: { // middle of the arc
x: positions[0] + pointRadiusX,
y: positions[1] + pointRadiusY
}
}
};
}
}
}
);
/**
* A `variablepie` series. If the [type](#series.variablepie.type) option is not
* specified, it is inherited from [chart.type](#chart.type).
*
* @extends series,plotOptions.variablepie
* @excluding dataParser, dataURL, stack, xAxis, yAxis
* @product highcharts
* @apioption series.variablepie
*/
/**
* An array of data points for the series. For the `variablepie` series type,
* points can be given in the following ways:
*
* 1. An array of arrays with 2 values. In this case, the numerical values will
* be interpreted as `y, z` options. Example:
* ```js
* data: [
* [40, 75],
* [50, 50],
* [60, 40]
* ]
* ```
*
* 2. An array of objects with named values. The following snippet shows only a
* few settings, see the complete options set below. If the total number of
* data points exceeds the series'
* [turboThreshold](#series.variablepie.turboThreshold), this option is not
* available.
* ```js
* data: [{
* y: 1,
* z: 4,
* name: "Point2",
* color: "#00FF00"
* }, {
* y: 7,
* z: 10,
* name: "Point1",
* color: "#FF00FF"
* }]
* ```
*
* @sample {highcharts} highcharts/series/data-array-of-arrays/
* Arrays of numeric x and y
* @sample {highcharts} highcharts/series/data-array-of-arrays-datetime/
* Arrays of datetime x and y
* @sample {highcharts} highcharts/series/data-array-of-name-value/
* Arrays of point.name and y
* @sample {highcharts} highcharts/series/data-array-of-objects/
* Config objects
*
* @type {Array<Array<(number|string),number>|*>}
* @extends series.pie.data
* @excluding marker, x
* @product highcharts
* @apioption series.variablepie.data
*/
@@ -0,0 +1,350 @@
/* *
* Highcharts variwide module
*
* (c) 2010-2019 Torstein Honsi
*
* License: www.highcharts.com/license
*/
'use strict';
import H from '../parts/Globals.js';
import '../parts/AreaSeries.js';
var addEvent = H.addEvent,
seriesType = H.seriesType,
seriesTypes = H.seriesTypes,
pick = H.pick;
/**
* @private
* @class
* @name Highcharts.seriesTypes.variwide
*
* @augments Highcharts.Series
*/
seriesType('variwide', 'column'
/**
* A variwide chart (related to marimekko chart) is a column chart with a
* variable width expressing a third dimension.
*
* @sample {highcharts} highcharts/demo/variwide/
* Variwide chart
* @sample {highcharts} highcharts/series-variwide/inverted/
* Inverted variwide chart
* @sample {highcharts} highcharts/series-variwide/datetime/
* Variwide columns on a datetime axis
*
* @extends plotOptions.column
* @since 6.0.0
* @product highcharts
* @excluding boostThreshold, crisp, depth, edgeColor, edgeWidth,
* groupZPadding
* @optionparent plotOptions.variwide
*/
, {
/**
* In a variwide chart, the point padding is 0 in order to express the
* horizontal stacking of items.
*/
pointPadding: 0,
/**
* In a variwide chart, the group padding is 0 in order to express the
* horizontal stacking of items.
*/
groupPadding: 0
}, {
pointArrayMap: ['y', 'z'],
parallelArrays: ['x', 'y', 'z'],
processData: function (force) {
this.totalZ = 0;
this.relZ = [];
seriesTypes.column.prototype.processData.call(this, force);
(this.xAxis.reversed ?
this.zData.slice().reverse() :
this.zData).forEach(
function (z, i) {
this.relZ[i] = this.totalZ;
this.totalZ += z;
},
this
);
if (this.xAxis.categories) {
this.xAxis.variwide = true;
this.xAxis.zData = this.zData; // Used for label rank
}
},
/**
* Translate an x value inside a given category index into the distorted
* axis translation.
*
* @private
* @function Highcharts.Series#postTranslate
*
* @param {number} index
* The category index
*
* @param {number} x
* The X pixel position in undistorted axis pixels
*
* @return {number}
* Distorted X position
*/
postTranslate: function (index, x, point) {
var axis = this.xAxis,
relZ = this.relZ,
i = axis.reversed ? relZ.length - index : index,
goRight = axis.reversed ? -1 : 1,
len = axis.len,
totalZ = this.totalZ,
linearSlotLeft = i / relZ.length * len,
linearSlotRight = (i + goRight) / relZ.length * len,
slotLeft = (pick(relZ[i], totalZ) / totalZ) * len,
slotRight = (pick(relZ[i + goRight], totalZ) / totalZ) * len,
xInsideLinearSlot = x - linearSlotLeft,
ret;
// Set crosshairWidth for every point (#8173)
if (point) {
point.crosshairWidth = slotRight - slotLeft;
}
ret = slotLeft +
xInsideLinearSlot * (slotRight - slotLeft) /
(linearSlotRight - linearSlotLeft);
return ret;
},
// Extend translation by distoring X position based on Z.
translate: function () {
// Temporarily disable crisping when computing original shapeArgs
var crispOption = this.options.crisp,
xAxis = this.xAxis;
this.options.crisp = false;
seriesTypes.column.prototype.translate.call(this);
// Reset option
this.options.crisp = crispOption;
var inverted = this.chart.inverted,
crisp = this.borderWidth % 2 / 2;
// Distort the points to reflect z dimension
this.points.forEach(function (point, i) {
var left, right;
if (xAxis.variwide) {
left = this.postTranslate(
i,
point.shapeArgs.x,
point
);
right = this.postTranslate(
i,
point.shapeArgs.x + point.shapeArgs.width
);
// For linear or datetime axes, the variwide column should
// start with X and extend Z units, without modifying the
// axis.
} else {
left = point.plotX;
right = xAxis.translate(
point.x + point.z,
0,
0,
0,
1
);
}
if (this.options.crisp) {
left = Math.round(left) - crisp;
right = Math.round(right) - crisp;
}
point.shapeArgs.x = left;
point.shapeArgs.width = right - left;
// Crosshair position (#8083)
point.plotX = (left + right) / 2;
// Adjust the tooltip position
if (!inverted) {
point.tooltipPos[0] =
point.shapeArgs.x + point.shapeArgs.width / 2;
} else {
point.tooltipPos[1] =
xAxis.len - point.shapeArgs.x - point.shapeArgs.width / 2;
}
}, this);
}
// Point functions
}, {
isValid: function () {
return H.isNumber(this.y, true) && H.isNumber(this.z, true);
}
});
H.Tick.prototype.postTranslate = function (xy, xOrY, index) {
var axis = this.axis,
pos = xy[xOrY] - axis.pos;
if (!axis.horiz) {
pos = axis.len - pos;
}
pos = axis.series[0].postTranslate(index, pos);
if (!axis.horiz) {
pos = axis.len - pos;
}
xy[xOrY] = axis.pos + pos;
};
// Same width as the category (#8083)
addEvent(H.Axis, 'afterDrawCrosshair', function (e) {
if (this.variwide && this.cross) {
this.cross.attr('stroke-width', e.point && e.point.crosshairWidth);
}
});
// On a vertical axis, apply anti-collision logic to the labels.
addEvent(H.Axis, 'afterRender', function () {
var axis = this;
if (!this.horiz && this.variwide) {
this.chart.labelCollectors.push(function () {
return axis.tickPositions.map(function (pos, i) {
var label = axis.ticks[pos].label;
label.labelrank = axis.zData[i];
return label;
});
});
}
});
addEvent(H.Tick, 'afterGetPosition', function (e) {
var axis = this.axis,
xOrY = axis.horiz ? 'x' : 'y';
if (axis.variwide) {
this[xOrY + 'Orig'] = e.pos[xOrY];
this.postTranslate(e.pos, xOrY, this.pos);
}
});
H.wrap(H.Tick.prototype, 'getLabelPosition', function (
proceed,
x,
y,
label,
horiz,
labelOptions,
tickmarkOffset,
index
) {
var args = Array.prototype.slice.call(arguments, 1),
xy,
xOrY = horiz ? 'x' : 'y';
// Replace the x with the original x
if (this.axis.variwide && typeof this[xOrY + 'Orig'] === 'number') {
args[horiz ? 0 : 1] = this[xOrY + 'Orig'];
}
xy = proceed.apply(this, args);
// Post-translate
if (this.axis.variwide && this.axis.categories) {
this.postTranslate(xy, xOrY, index);
}
return xy;
});
/**
* A `variwide` series. If the [type](#series.variwide.type) option is not
* specified, it is inherited from [chart.type](#chart.type).
*
* @extends series,plotOptions.variwide
* @product highcharts
* @apioption series.variwide
*/
/**
* An array of data points for the series. For the `variwide` series type,
* points can be given in the following ways:
*
* 1. An array of arrays with 3 or 2 values. In this case, the values correspond
* to `x,y,z`. If the first value is a string, it is applied as the name of
* the point, and the `x` value is inferred. The `x` value can also be
* omitted, in which case the inner arrays should be of length 2. Then the
* `x` value is automatically calculated, either starting at 0 and
* incremented by 1, or from `pointStart` and `pointInterval` given in the
* series options.
* ```js
* data: [
* [0, 1, 2],
* [1, 5, 5],
* [2, 0, 2]
* ]
* ```
*
* 2. An array of objects with named values. The following snippet shows only a
* few settings, see the complete options set below. If the total number of
* data points exceeds the series'
* [turboThreshold](#series.variwide.turboThreshold), this option is not
* available.
* ```js
* data: [{
* x: 1,
* y: 1,
* z: 1,
* name: "Point2",
* color: "#00FF00"
* }, {
* x: 1,
* y: 5,
* z: 4,
* name: "Point1",
* color: "#FF00FF"
* }]
* ```
*
* @sample {highcharts} highcharts/series/data-array-of-arrays/
* Arrays of numeric x and y
* @sample {highcharts} highcharts/series/data-array-of-arrays-datetime/
* Arrays of datetime x and y
* @sample {highcharts} highcharts/series/data-array-of-name-value/
* Arrays of point.name and y
* @sample {highcharts} highcharts/series/data-array-of-objects/
* Config objects
*
* @type {Array<Array<(number|string),number>|Array<(number|string),number,number>|*>}
* @extends series.line.data
* @excluding marker
* @product highcharts
* @apioption series.variwide.data
*/
/**
* The relative width for each column. On a category axis, the widths are
* distributed so they sum up to the X axis length. On linear and datetime axes,
* the columns will be laid out from the X value and Z units along the axis.
*
* @type {number}
* @product highcharts
* @apioption series.variwide.data.z
*/
@@ -0,0 +1,354 @@
/* *
* Vector plot series module
*
* (c) 2010-2019 Torstein Honsi
*
* License: www.highcharts.com/license
*/
'use strict';
import H from '../parts/Globals.js';
var seriesType = H.seriesType;
/**
* The vector series class.
*
* @private
* @class
* @name Highcharts.seriesTypes.vector
*
* @augments Highcharts.seriesTypes.scatter
*/
seriesType('vector', 'scatter'
/**
* A vector plot is a type of cartesian chart where each point has an X and Y
* position, a length and a direction. Vectors are drawn as arrows.
*
* @sample {highcharts|highstock} highcharts/demo/vector-plot/
* Vector pot
*
* @since 6.0.0
* @extends plotOptions.scatter
* @excluding boostThreshold, marker, connectEnds, connectNulls,
* cropThreshold, dashStyle, gapSize, gapUnit, dataGrouping,
* linecap, shadow, stacking, step, jitter
* @product highcharts highstock
* @optionparent plotOptions.vector
*/
, {
/**
* The line width for each vector arrow.
*/
lineWidth: 2,
/**
* @ignore
*/
marker: null,
/**
* What part of the vector it should be rotated around. Can be one of
* `start`, `center` and `end`. When `start`, the vectors will start from
* the given [x, y] position, and when `end` the vectors will end in the
* [x, y] position.
*
* @sample highcharts/plotoptions/vector-rotationorigin-start/
* Rotate from start
*
* @validvalue ["start", "center", "end"]
*/
rotationOrigin: 'center',
states: {
hover: {
/**
* Additonal line width for the vector errors when they are hovered.
*/
lineWidthPlus: 1
}
},
tooltip: {
/**
* @default [{point.x}, {point.y}] Length: {point.length} Direction: {point.direction}°
*/
pointFormat: '<b>[{point.x}, {point.y}]</b><br/>Length: <b>{point.length}</b><br/>Direction: <b>{point.direction}\u00B0</b><br/>'
},
/**
* Maximum length of the arrows in the vector plot. The individual arrow
* length is computed between 0 and this value.
*/
vectorLength: 20
}, {
pointArrayMap: ['y', 'length', 'direction'],
parallelArrays: ['x', 'y', 'length', 'direction'],
/**
* Get presentational attributes.
*
* @private
* @function Highcharts.seriesTypes.vector#pointAttribs
*
* @param {Highcharts.Point} point
*
* @param {string} state
*
* @return {*}
*/
pointAttribs: function (point, state) {
var options = this.options,
stroke = point.color || this.color,
strokeWidth = this.options.lineWidth;
if (state) {
stroke = options.states[state].color || stroke;
strokeWidth =
(options.states[state].lineWidth || strokeWidth) +
(options.states[state].lineWidthPlus || 0);
}
return {
'stroke': stroke,
'stroke-width': strokeWidth
};
},
/**
* @ignore
* @deprecated
* @function Highcharts.seriesTypes.vector#markerAttribs
*/
markerAttribs: H.noop,
/**
* @ignore
* @deprecated
* @function Highcharts.seriesTypes.vector#getSymbol
*/
getSymbol: H.noop,
/**
* Create a single arrow. It is later rotated around the zero
* centerpoint.
*
* @private
* @function Highcharts.seriesTypes.vector#arrow
*
* @param {Highcharts.Point} point
*
* @return {Highcharts.SVGPathArray}
*/
arrow: function (point) {
var path,
fraction = point.length / this.lengthMax,
u = fraction * this.options.vectorLength / 20,
o = {
start: 10 * u,
center: 0,
end: -10 * u
}[this.options.rotationOrigin] || 0;
// The stem and the arrow head. Draw the arrow first with rotation
// 0, which is the arrow pointing down (vector from north to south).
path = [
'M', 0, 7 * u + o, // base of arrow
'L', -1.5 * u, 7 * u + o,
0, 10 * u + o,
1.5 * u, 7 * u + o,
0, 7 * u + o,
0, -10 * u + o// top
];
return path;
},
/**
* @private
* @function Highcharts.seriesTypes.vector#translate
*/
translate: function () {
H.Series.prototype.translate.call(this);
this.lengthMax = H.arrayMax(this.lengthData);
},
/**
* @private
* @function Highcharts.seriesTypes.vector#drawPoints
*/
drawPoints: function () {
var chart = this.chart;
this.points.forEach(function (point) {
var plotX = point.plotX,
plotY = point.plotY;
if (chart.isInsidePlot(plotX, plotY, chart.inverted)) {
if (!point.graphic) {
point.graphic = this.chart.renderer
.path()
.add(this.markerGroup);
}
point.graphic
.attr({
d: this.arrow(point),
translateX: plotX,
translateY: plotY,
rotation: point.direction
})
.attr(this.pointAttribs(point));
} else if (point.graphic) {
point.graphic = point.graphic.destroy();
}
}, this);
},
/**
* @ignore
* @deprecated
* @function Highcharts.seriesTypes.vector#drawGraph
*/
drawGraph: H.noop,
/*
drawLegendSymbol: function (legend, item) {
var options = legend.options,
symbolHeight = legend.symbolHeight,
square = options.squareSymbol,
symbolWidth = square ? symbolHeight : legend.symbolWidth,
path = this.arrow.call({
lengthMax: 1,
options: {
vectorLength: symbolWidth
}
}, {
length: 1
});
item.legendLine = this.chart.renderer.path(path)
.addClass('highcharts-point')
.attr({
zIndex: 3,
translateY: symbolWidth / 2,
rotation: 270,
'stroke-width': 1,
'stroke': 'black'
}).add(item.legendGroup);
},
*/
/**
* Fade in the arrows on initializing series.
*
* @private
* @function Highcharts.seriesTypes.vector#animate
*
* @param {boolean} [init]
*/
animate: function (init) {
if (init) {
this.markerGroup.attr({
opacity: 0.01
});
} else {
this.markerGroup.animate({
opacity: 1
}, H.animObject(this.options.animation));
this.animate = null;
}
}
});
/**
* A `vector` series. If the [type](#series.vector.type) option is not
* specified, it is inherited from [chart.type](#chart.type).
*
* @extends series,plotOptions.vector
* @excluding dataParser, dataURL
* @product highcharts highstock
* @apioption series.vector
*/
/**
* An array of data points for the series. For the `vector` series type,
* points can be given in the following ways:
*
* 1. An array of arrays with 4 values. In this case, the values correspond to
* to `x,y,length,direction`. If the first value is a string, it is applied
* as the name of the point, and the `x` value is inferred.
* ```js
* data: [
* [0, 0, 10, 90],
* [0, 1, 5, 180],
* [1, 1, 2, 270]
* ]
* ```
*
* 2. An array of objects with named values. The following snippet shows only a
* few settings, see the complete options set below. If the total number of
* data points exceeds the series'
* [turboThreshold](#series.area.turboThreshold), this option is not
* available.
* ```js
* data: [{
* x: 0,
* y: 0,
* name: "Point2",
* length: 10,
* direction: 90
* }, {
* x: 1,
* y: 1,
* name: "Point1",
* direction: 270
* }]
* ```
*
* @sample {highcharts} highcharts/series/data-array-of-arrays/
* Arrays of numeric x and y
* @sample {highcharts} highcharts/series/data-array-of-arrays-datetime/
* Arrays of datetime x and y
* @sample {highcharts} highcharts/series/data-array-of-name-value/
* Arrays of point.name and y
* @sample {highcharts} highcharts/series/data-array-of-objects/
* Config objects
*
* @type {Array<Array<(number|string),number,number,number>|*>}
* @extends series.line.data
* @product highcharts highstock
* @apioption series.vector.data
*/
/**
* The length of the vector. The rendered length will relate to the
* `vectorLength` setting.
*
* @type {number}
* @product highcharts highstock
* @apioption series.vector.data.length
*/
/**
* The vector direction in degrees, where 0 is north (pointing towards south).
*
* @type {number}
* @product highcharts highstock
* @apioption series.vector.data.direction
*/
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,380 @@
/* *
* Wind barb series module
*
* (c) 2010-2019 Torstein Honsi
*
* License: www.highcharts.com/license
*/
'use strict';
import H from '../parts/Globals.js';
import onSeriesMixin from '../mixins/on-series.js';
var noop = H.noop,
seriesType = H.seriesType;
/**
* @private
* @class
* @name Highcharts.seriesTypes.windbarb
*
* @augments Highcharts.Series
*/
seriesType('windbarb', 'column'
/**
* Wind barbs are a convenient way to represent wind speed and direction in one
* graphical form. Wind direction is given by the stem direction, and wind speed
* by the number and shape of barbs.
*
* @sample {highcharts|highstock} highcharts/demo/windbarb-series/
* Wind barb series
*
* @extends plotOptions.column
* @excluding boostThreshold, marker, connectEnds, connectNulls,
* cropThreshold, dashStyle, gapSize, gapUnit, dataGrouping,
* linecap, shadow, stacking, step
* @since 6.0.0
* @product highcharts highstock
* @optionparent plotOptions.windbarb
*/
, {
/**
* The line width of the wind barb symbols.
*/
lineWidth: 2,
/**
* The id of another series in the chart that the wind barbs are projected
* on. When `null`, the wind symbols are drawn on the X axis, but offset up
* or down by the `yOffset` setting.
*
* @sample {highcharts|highstock} highcharts/plotoptions/windbarb-onseries
* Projected on area series
*
* @type {string|null}
*/
onSeries: null,
states: {
hover: {
lineWidthPlus: 0
}
},
tooltip: {
/**
* The default point format for the wind barb tooltip. Note the
* `point.beaufort` property that refers to the Beaufort wind scale. The
* names can be internationalized by modifying
* `Highcharts.seriesTypes.windbarb.prototype.beaufortNames`.
*/
pointFormat: '<span style="color:{point.color}">\u25CF</span> {series.name}: <b>{point.value}</b> ({point.beaufort})<br/>'
},
/**
* Pixel length of the stems.
*/
vectorLength: 20,
/**
* Vertical offset from the cartesian position, in pixels. The default value
* makes sure the symbols don't overlap the X axis when `onSeries` is
* `null`, and that they don't overlap the linked series when `onSeries` is
* given.
*/
yOffset: -20,
/**
* Horizontal offset from the cartesian position, in pixels. When the chart
* is inverted, this option allows translation like
* [yOffset](#plotOptions.windbarb.yOffset) in non inverted charts.
*
* @since 6.1.0
*/
xOffset: 0
}, {
pointArrayMap: ['value', 'direction'],
parallelArrays: ['x', 'value', 'direction'],
beaufortName: ['Calm', 'Light air', 'Light breeze',
'Gentle breeze', 'Moderate breeze', 'Fresh breeze',
'Strong breeze', 'Near gale', 'Gale', 'Strong gale', 'Storm',
'Violent storm', 'Hurricane'],
beaufortFloor: [0, 0.3, 1.6, 3.4, 5.5, 8.0, 10.8, 13.9, 17.2, 20.8,
24.5, 28.5, 32.7],
trackerGroups: ['markerGroup'],
// Get presentational attributes.
pointAttribs: function (point, state) {
var options = this.options,
stroke = point.color || this.color,
strokeWidth = this.options.lineWidth;
if (state) {
stroke = options.states[state].color || stroke;
strokeWidth =
(options.states[state].lineWidth || strokeWidth) +
(options.states[state].lineWidthPlus || 0);
}
return {
'stroke': stroke,
'stroke-width': strokeWidth
};
},
markerAttribs: function () {
return undefined;
},
getPlotBox: onSeriesMixin.getPlotBox,
// Create a single wind arrow. It is later rotated around the zero
// centerpoint.
windArrow: function (point) {
var knots = point.value * 1.943844,
level = point.beaufortLevel,
path,
barbs,
u = this.options.vectorLength / 20,
pos = -10;
if (point.isNull) {
return [];
}
if (level === 0) {
return this.chart.renderer.symbols.circle(
-10 * u,
-10 * u,
20 * u,
20 * u
);
}
// The stem and the arrow head
path = [
'M', 0, 7 * u, // base of arrow
'L', -1.5 * u, 7 * u,
0, 10 * u,
1.5 * u, 7 * u,
0, 7 * u,
0, -10 * u// top
];
// For each full 50 knots, add a pennant
barbs = (knots - knots % 50) / 50; // pennants
if (barbs > 0) {
while (barbs--) {
path.push(
pos === -10 ? 'L' : 'M',
0,
pos * u,
'L',
5 * u,
pos * u + 2,
'L',
0,
pos * u + 4
);
// Substract from the rest and move position for next
knots -= 50;
pos += 7;
}
}
// For each full 10 knots, add a full barb
barbs = (knots - knots % 10) / 10;
if (barbs > 0) {
while (barbs--) {
path.push(
pos === -10 ? 'L' : 'M',
0,
pos * u,
'L',
7 * u,
pos * u
);
knots -= 10;
pos += 3;
}
}
// For each full 5 knots, add a half barb
barbs = (knots - knots % 5) / 5; // half barbs
if (barbs > 0) {
while (barbs--) {
path.push(
pos === -10 ? 'L' : 'M',
0,
pos * u,
'L',
4 * u,
pos * u
);
knots -= 5;
pos += 3;
}
}
return path;
},
translate: function () {
var beaufortFloor = this.beaufortFloor,
beaufortName = this.beaufortName;
onSeriesMixin.translate.call(this);
this.points.forEach(function (point) {
var level = 0;
// Find the beaufort level (zero based)
for (; level < beaufortFloor.length; level++) {
if (beaufortFloor[level] > point.value) {
break;
}
}
point.beaufortLevel = level - 1;
point.beaufort = beaufortName[level - 1];
});
},
drawPoints: function () {
var chart = this.chart,
yAxis = this.yAxis,
inverted = chart.inverted,
shapeOffset = this.options.vectorLength / 2;
this.points.forEach(function (point) {
var plotX = point.plotX,
plotY = point.plotY;
// Check if it's inside the plot area, but only for the X
// dimension.
if (chart.isInsidePlot(plotX, 0, false)) {
// Create the graphic the first time
if (!point.graphic) {
point.graphic = this.chart.renderer
.path()
.add(this.markerGroup);
}
// Position the graphic
point.graphic
.attr({
d: this.windArrow(point),
translateX: plotX + this.options.xOffset,
translateY: plotY + this.options.yOffset,
rotation: point.direction
})
.attr(this.pointAttribs(point));
} else if (point.graphic) {
point.graphic = point.graphic.destroy();
}
// Set the tooltip anchor position
point.tooltipPos = [
plotX + this.options.xOffset + (inverted && !this.onSeries ?
shapeOffset : 0),
plotY + this.options.yOffset - (inverted ? 0 :
shapeOffset + yAxis.pos - chart.plotTop)
]; // #6327
}, this);
},
// Fade in the arrows on initializing series.
animate: function (init) {
if (init) {
this.markerGroup.attr({
opacity: 0.01
});
} else {
this.markerGroup.animate({
opacity: 1
}, H.animObject(this.options.animation));
this.animate = null;
}
},
// Don't invert the marker group (#4960)
invertGroups: noop
}, {
isValid: function () {
return H.isNumber(this.value) && this.value >= 0;
}
});
/**
* A `windbarb` series. If the [type](#series.windbarb.type) option is not
* specified, it is inherited from [chart.type](#chart.type).
*
* @extends series,plotOptions.windbarb
* @excluding dataParser, dataURL
* @product highcharts highstock
* @apioption series.windbarb
*/
/**
* An array of data points for the series. For the `windbarb` series type,
* points can be given in the following ways:
*
* 1. An array of arrays with 3 values. In this case, the values correspond to
* `x,value,direction`. If the first value is a string, it is applied as the
* name of the point, and the `x` value is inferred.
* ```js
* data: [
* [Date.UTC(2017, 0, 1, 0), 3.3, 90],
* [Date.UTC(2017, 0, 1, 1), 12.1, 180],
* [Date.UTC(2017, 0, 1, 2), 11.1, 270]
* ]
* ```
*
* 2. An array of objects with named values. The following snippet shows only a
* few settings, see the complete options set below. If the total number of
* data points exceeds the series'
* [turboThreshold](#series.area.turboThreshold), this option is not
* available.
* ```js
* data: [{
* x: Date.UTC(2017, 0, 1, 0),
* value: 12.1,
* direction: 90
* }, {
* x: Date.UTC(2017, 0, 1, 1),
* value: 11.1,
* direction: 270
* }]
* ```
*
* @sample {highcharts} highcharts/chart/reflow-true/
* Numerical values
* @sample {highcharts} highcharts/series/data-array-of-arrays/
* Arrays of numeric x and y
* @sample {highcharts} highcharts/series/data-array-of-arrays-datetime/
* Arrays of datetime x and y
* @sample {highcharts} highcharts/series/data-array-of-name-value/
* Arrays of point.name and y
* @sample {highcharts} highcharts/series/data-array-of-objects/
* Config objects
*
* @type {Array<Array<(number|string),number,number>|*>}
* @extends series.line.data
* @product highcharts highstock
* @apioption series.windbarb.data
*/
/**
* The wind speed in meters per second.
*
* @type {number}
* @product highcharts highstock
* @apioption series.windbarb.data.value
*/
/**
* The wind direction in degrees, where 0 is north (pointing towards south).
*
* @type {number}
* @product highcharts highstock
* @apioption series.windbarb.data.direction
*/
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,761 @@
/* *
* X-range series module
*
* (c) 2010-2019 Torstein Honsi, Lars A. V. Cabrera
*
* License: www.highcharts.com/license
*/
'use strict';
import H from '../parts/Globals.js';
var addEvent = H.addEvent,
defined = H.defined,
color = H.Color,
columnType = H.seriesTypes.column,
correctFloat = H.correctFloat,
isNumber = H.isNumber,
isObject = H.isObject,
merge = H.merge,
pick = H.pick,
seriesType = H.seriesType,
seriesTypes = H.seriesTypes,
Axis = H.Axis,
Point = H.Point,
Series = H.Series;
/**
* Return color of a point based on its category.
*
* @private
* @function getColorByCategory
*
* @param {object} series
* The series which the point belongs to.
*
* @param {object} point
* The point to calculate its color for.
*
* @return {object}
* Returns an object containing the properties color and colorIndex.
*/
function getColorByCategory(series, point) {
var colors = series.options.colors || series.chart.options.colors,
colorCount = colors ?
colors.length :
series.chart.options.chart.colorCount,
colorIndex = point.y % colorCount,
color = colors && colors[colorIndex];
return {
colorIndex: colorIndex,
color: color
};
}
/**
* @private
* @class
* @name Highcharts.seriesTypes.xrange
*
* @augments Highcharts.Series
*/
seriesType('xrange', 'column'
/**
* The X-range series displays ranges on the X axis, typically time intervals
* with a start and end date.
*
* @sample {highcharts} highcharts/demo/x-range/
* X-range
* @sample {highcharts} highcharts/css/x-range/
* Styled mode X-range
* @sample {highcharts} highcharts/chart/inverted-xrange/
* Inverted X-range
*
* @extends plotOptions.column
* @since 6.0.0
* @product highcharts highstock gantt
* @excluding boostThreshold, crisp, cropThreshold, depth, edgeColor,
* edgeWidth, findNearestPointBy, getExtremesFromAll,
* negativeColor, pointInterval, pointIntervalUnit,
* pointPlacement, pointRange, pointStart, softThreshold,
* stacking, threshold, data
* @optionparent plotOptions.xrange
*/
, {
/**
* A partial fill for each point, typically used to visualize how much of
* a task is performed. The partial fill object can be set either on series
* or point level.
*
* @sample {highcharts} highcharts/demo/x-range
* X-range with partial fill
*
* @product highcharts highstock gantt
* @apioption plotOptions.xrange.partialFill
*/
/**
* The fill color to be used for partial fills. Defaults to a darker shade
* of the point color.
*
* @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
* @product highcharts highstock gantt
* @apioption plotOptions.xrange.partialFill.fill
*/
/**
* A partial fill for each point, typically used to visualize how much of
* a task is performed. See [completed](series.gantt.data.completed).
*
* @sample gantt/demo/progress-indicator
* Gantt with progress indicator
*
* @product gantt
* @apioption plotOptions.gantt.partialFill
*/
/**
* In an X-range series, this option makes all points of the same Y-axis
* category the same color.
*/
colorByPoint: true,
dataLabels: {
verticalAlign: 'middle',
inside: true,
/**
* The default formatter for X-range data labels displays the percentage
* of the partial fill amount.
*
* @type {Highcharts.FormatterCallbackFunction<Highcharts.SeriesDataLabelsFormatterContextObject>}
* @default function () { return (amount * 100) + '%'; }
*/
formatter: function () {
var point = this.point,
amount = point.partialFill;
if (isObject(amount)) {
amount = amount.amount;
}
if (!defined(amount)) {
amount = 0;
}
return correctFloat(amount * 100) + '%';
}
},
tooltip: {
headerFormat: '<span style="font-size: 10px">{point.x} - {point.x2}</span><br/>',
pointFormat: '<span style="color:{point.color}">\u25CF</span> {series.name}: <b>{point.yCategory}</b><br/>'
},
borderRadius: 3,
pointRange: 0
}, {
type: 'xrange',
parallelArrays: ['x', 'x2', 'y'],
requireSorting: false,
animate: seriesTypes.line.prototype.animate,
cropShoulder: 1,
getExtremesFromAll: true,
autoIncrement: H.noop,
/**
* Borrow the column series metrics, but with swapped axes. This gives free
* access to features like groupPadding, grouping, pointWidth etc.
*
* @private
* @function Higcharts.Series#getColumnMetrics
*
* @return {Highcharts.ColumnMetricsObject}
*/
getColumnMetrics: function () {
var metrics,
chart = this.chart;
function swapAxes() {
chart.series.forEach(function (s) {
var xAxis = s.xAxis;
s.xAxis = s.yAxis;
s.yAxis = xAxis;
});
}
swapAxes();
metrics = columnType.prototype.getColumnMetrics.call(this);
swapAxes();
return metrics;
},
/**
* Override cropData to show a point where x or x2 is outside visible range,
* but one of them is inside.
*
* @private
* @function Highcharts.Series#cropData
*
* @param {Array<number>} xData
*
* @param {Array<number>} yData
*
* @param {number} min
*
* @param {number} max
*
* @param {number} [cropShoulder]
*
* @return {*}
*/
cropData: function (xData, yData, min, max) {
// Replace xData with x2Data to find the appropriate cropStart
var cropData = Series.prototype.cropData,
crop = cropData.call(this, this.x2Data, yData, min, max);
// Re-insert the cropped xData
crop.xData = xData.slice(crop.start, crop.end);
return crop;
},
/**
* @private
* @function Highcharts.Series#translatePoint
*
* @param {Highcharts.Point} point
*/
translatePoint: function (point) {
var series = this,
xAxis = series.xAxis,
yAxis = series.yAxis,
metrics = series.columnMetrics,
options = series.options,
minPointLength = options.minPointLength || 0,
plotX = point.plotX,
posX = pick(point.x2, point.x + (point.len || 0)),
plotX2 = xAxis.translate(posX, 0, 0, 0, 1),
length = Math.abs(plotX2 - plotX),
widthDifference,
shapeArgs,
partialFill,
inverted = this.chart.inverted,
borderWidth = pick(options.borderWidth, 1),
crisper = borderWidth % 2 / 2,
yOffset = metrics.offset,
pointHeight = Math.round(metrics.width),
dlLeft,
dlRight,
dlWidth;
if (minPointLength) {
widthDifference = minPointLength - length;
if (widthDifference < 0) {
widthDifference = 0;
}
plotX -= widthDifference / 2;
plotX2 += widthDifference / 2;
}
plotX = Math.max(plotX, -10);
plotX2 = Math.min(Math.max(plotX2, -10), xAxis.len + 10);
// Handle individual pointWidth
if (defined(point.options.pointWidth)) {
yOffset -= (
(Math.ceil(point.options.pointWidth) - pointHeight) / 2
);
pointHeight = Math.ceil(point.options.pointWidth);
}
// Apply pointPlacement to the Y axis
if (
options.pointPlacement &&
isNumber(point.plotY) &&
yAxis.categories
) {
point.plotY = yAxis
.translate(point.y, 0, 1, 0, 1, options.pointPlacement);
}
point.shapeArgs = {
x: Math.floor(Math.min(plotX, plotX2)) + crisper,
y: Math.floor(point.plotY + yOffset) + crisper,
width: Math.round(Math.abs(plotX2 - plotX)),
height: pointHeight,
r: series.options.borderRadius
};
// Align data labels inside the shape and inside the plot area
dlLeft = point.shapeArgs.x;
dlRight = dlLeft + point.shapeArgs.width;
if (dlLeft < 0 || dlRight > xAxis.len) {
dlLeft = Math.min(xAxis.len, Math.max(0, dlLeft));
dlRight = Math.max(0, Math.min(dlRight, xAxis.len));
dlWidth = dlRight - dlLeft;
point.dlBox = merge(point.shapeArgs, {
x: dlLeft,
width: dlRight - dlLeft,
centerX: dlWidth ? dlWidth / 2 : null
});
} else {
point.dlBox = null;
}
// Tooltip position
if (!inverted) {
point.tooltipPos[0] += length / 2 * (xAxis.reversed ? -1 : 1);
point.tooltipPos[1] -= metrics.width / 2;
// Limit position by the correct axis size (#9727)
point.tooltipPos[0] = Math.max(
Math.min(point.tooltipPos[0], xAxis.len - 1), 0
);
point.tooltipPos[1] = Math.max(
Math.min(point.tooltipPos[1], yAxis.len - 1), 0
);
} else {
point.tooltipPos[1] += length / 2 * (xAxis.reversed ? 1 : -1);
point.tooltipPos[0] += metrics.width / 2;
// Limit position by the correct axis size (#9727)
point.tooltipPos[1] = Math.max(
Math.min(point.tooltipPos[1], xAxis.len - 1), 0
);
point.tooltipPos[0] = Math.max(
Math.min(point.tooltipPos[0], yAxis.len - 1), 0
);
}
// Add a partShapeArgs to the point, based on the shapeArgs property
partialFill = point.partialFill;
if (partialFill) {
// Get the partial fill amount
if (isObject(partialFill)) {
partialFill = partialFill.amount;
}
// If it was not a number, assume 0
if (!isNumber(partialFill)) {
partialFill = 0;
}
shapeArgs = point.shapeArgs;
point.partShapeArgs = {
x: shapeArgs.x,
y: shapeArgs.y,
width: shapeArgs.width,
height: shapeArgs.height,
r: series.options.borderRadius
};
point.clipRectArgs = {
x: shapeArgs.x,
y: shapeArgs.y,
width: Math.max(
Math.round(
length * partialFill +
(point.plotX - plotX)
),
0
),
height: shapeArgs.height
};
}
},
/**
* @private
* @function Highcharts.Series#translate
*/
translate: function () {
columnType.prototype.translate.apply(this, arguments);
this.points.forEach(function (point) {
this.translatePoint(point);
}, this);
},
/**
* Draws a single point in the series. Needed for partial fill.
*
* This override turns point.graphic into a group containing the original
* graphic and an overlay displaying the partial fill.
*
* @private
* @function Highcharts.Series#drawPoint
*
* @param {Highcharts.Point} point
* An instance of Point in the series.
*
* @param {"animate"|"attr"} verb
* 'animate' (animates changes) or 'attr' (sets options)
*/
drawPoint: function (point, verb) {
var series = this,
seriesOpts = series.options,
renderer = series.chart.renderer,
graphic = point.graphic,
type = point.shapeType,
shapeArgs = point.shapeArgs,
partShapeArgs = point.partShapeArgs,
clipRectArgs = point.clipRectArgs,
pfOptions = point.partialFill,
fill,
state = point.selected && 'select',
cutOff = seriesOpts.stacking && !seriesOpts.borderRadius;
if (!point.isNull) {
// Original graphic
if (graphic) { // update
point.graphicOriginal[verb](
merge(shapeArgs)
);
} else {
point.graphic = graphic = renderer.g('point')
.addClass(point.getClassName())
.add(point.group || series.group);
point.graphicOriginal = renderer[type](shapeArgs)
.addClass(point.getClassName())
.addClass('highcharts-partfill-original')
.add(graphic);
}
// Partial fill graphic
if (partShapeArgs) {
if (point.graphicOverlay) {
point.graphicOverlay[verb](
merge(partShapeArgs)
);
point.clipRect.animate(
merge(clipRectArgs)
);
} else {
point.clipRect = renderer.clipRect(
clipRectArgs.x,
clipRectArgs.y,
clipRectArgs.width,
clipRectArgs.height
);
point.graphicOverlay = renderer[type](partShapeArgs)
.addClass('highcharts-partfill-overlay')
.add(graphic)
.clip(point.clipRect);
}
}
// Presentational
if (!series.chart.styledMode) {
point.graphicOriginal
.attr(series.pointAttribs(point, state))
.shadow(seriesOpts.shadow, null, cutOff);
if (partShapeArgs) {
// Ensure pfOptions is an object
if (!isObject(pfOptions)) {
pfOptions = {};
}
if (isObject(seriesOpts.partialFill)) {
pfOptions = merge(
pfOptions, seriesOpts.partialFill
);
}
fill = (
pfOptions.fill ||
color(point.color || series.color).brighten(-0.3).get()
);
point.graphicOverlay
.attr(series.pointAttribs(point, state))
.attr({
'fill': fill
})
.shadow(seriesOpts.shadow, null, cutOff);
}
}
} else if (graphic) {
point.graphic = graphic.destroy(); // #1269
}
},
/**
* @private
* @function Highcharts.Series#drawPoints
*/
drawPoints: function () {
var series = this,
verb = series.getAnimationVerb();
// Draw the columns
series.points.forEach(function (point) {
series.drawPoint(point, verb);
});
},
/**
* Returns "animate", or "attr" if the number of points is above the
* animation limit.
*
* @private
* @function Highcharts.Series#getAnimationVerb
*
* @return {string}
*/
getAnimationVerb: function () {
return (
this.chart.pointCount < (this.options.animationLimit || 250) ?
'animate' :
'attr'
);
}
/*
// Override to remove stroke from points. For partial fill.
pointAttribs: function () {
var series = this,
retVal = columnType.prototype.pointAttribs.apply(series, arguments);
//retVal['stroke-width'] = 0;
return retVal;
}
//*/
}, { // Point class properties
/**
* Extend applyOptions so that `colorByPoint` for x-range means that one
* color is applied per Y axis category.
*
* @private
* @function Highcharts.Point#applyOptions
*
* @return {Highcharts.Series}
*/
applyOptions: function () {
var point = Point.prototype.applyOptions.apply(this, arguments),
series = point.series,
colorByPoint;
if (series.options.colorByPoint && !point.options.color) {
colorByPoint = getColorByCategory(series, point);
if (!series.chart.styledMode) {
point.color = colorByPoint.color;
}
if (!point.options.colorIndex) {
point.colorIndex = colorByPoint.colorIndex;
}
}
return point;
},
/**
* Extend init to have y default to 0.
*
* @private
* @function Highcharts.Point#init
*
* @return {Highcharts.Series}
*/
init: function () {
Point.prototype.init.apply(this, arguments);
if (!this.y) {
this.y = 0;
}
return this;
},
/**
* @private
* @function Highcharts.Point#setState
*/
setState: function () {
Point.prototype.setState.apply(this, arguments);
this.series.drawPoint(this, this.series.getAnimationVerb());
},
/**
* @private
* @function Highcharts.Point#getLabelConfig
*
* @return {Highcharts.PointLabelObject}
*/
// Add x2 and yCategory to the available properties for tooltip formats
getLabelConfig: function () {
var point = this,
cfg = Point.prototype.getLabelConfig.call(point),
yCats = point.series.yAxis.categories;
cfg.x2 = point.x2;
cfg.yCategory = point.yCategory = yCats && yCats[point.y];
return cfg;
},
tooltipDateKeys: ['x', 'x2'],
/**
* @private
* @function Highcharts.Point#isValid
*
* @return {boolean}
*/
isValid: function () {
return typeof this.x === 'number' &&
typeof this.x2 === 'number';
}
});
// Max x2 should be considered in xAxis extremes
addEvent(Axis, 'afterGetSeriesExtremes', function () {
var axis = this,
axisSeries = axis.series,
dataMax,
modMax;
if (axis.isXAxis) {
dataMax = pick(axis.dataMax, -Number.MAX_VALUE);
axisSeries.forEach(function (series) {
if (series.x2Data) {
series.x2Data.forEach(function (val) {
if (val > dataMax) {
dataMax = val;
modMax = true;
}
});
}
});
if (modMax) {
axis.dataMax = dataMax;
}
}
});
/**
* An `xrange` series. If the [type](#series.xrange.type) option is not
* specified, it is inherited from [chart.type](#chart.type).
*
* @extends series,plotOptions.xrange
* @excluding boostThreshold, crisp, cropThreshold, depth, edgeColor, edgeWidth,
* findNearestPointBy, getExtremesFromAll, negativeColor,
* pointInterval, pointIntervalUnit, pointPlacement, pointRange,
* pointStart, softThreshold, stacking, threshold
* @product highcharts highstock gantt
* @apioption series.xrange
*/
/**
* An array of data points for the series. For the `xrange` series type,
* points can be given in the following ways:
*
* 1. An array of objects with named values. The objects are point configuration
* objects as seen below.
* ```js
* data: [{
* x: Date.UTC(2017, 0, 1),
* x2: Date.UTC(2017, 0, 3),
* name: "Test",
* y: 0,
* color: "#00FF00"
* }, {
* x: Date.UTC(2017, 0, 4),
* x2: Date.UTC(2017, 0, 5),
* name: "Deploy",
* y: 1,
* color: "#FF0000"
* }]
* ```
*
* @sample {highcharts} highcharts/series/data-array-of-objects/
* Config objects
*
* @type {Array<*>}
* @extends series.line.data
* @product highcharts highstock gantt
* @apioption series.xrange.data
*/
/**
* The starting X value of the range point.
*
* @sample {highcharts} highcharts/demo/x-range
* X-range
*
* @type {number}
* @product highcharts highstock gantt
* @apioption series.xrange.data.x
*/
/**
* The ending X value of the range point.
*
* @sample {highcharts} highcharts/demo/x-range
* X-range
*
* @type {number}
* @product highcharts highstock gantt
* @apioption series.xrange.data.x2
*/
/**
* The Y value of the range point.
*
* @sample {highcharts} highcharts/demo/x-range
* X-range
*
* @type {number}
* @product highcharts highstock gantt
* @apioption series.xrange.data.y
*/
/**
* A partial fill for each point, typically used to visualize how much of
* a task is performed. The partial fill object can be set either on series
* or point level.
*
* @sample {highcharts} highcharts/demo/x-range
* X-range with partial fill
*
* @product highcharts highstock gantt
* @apioption series.xrange.data.partialFill
*/
/**
* The amount of the X-range point to be filled. Values can be 0-1 and are
* converted to percentages in the default data label formatter.
*
* @type {number}
* @product highcharts highstock gantt
* @apioption series.xrange.data.partialFill.amount
*/
/**
* The fill color to be used for partial fills. Defaults to a darker shade
* of the point color.
*
* @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
* @product highcharts highstock gantt
* @apioption series.xrange.data.partialFill.fill
*/