initial commit
This commit is contained in:
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/\//g, '/');
|
||||
}
|
||||
|
||||
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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/\//g, '/');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
+37
@@ -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
|
||||
*/
|
||||
Reference in New Issue
Block a user