chore: version inicial del codigo fuente del LMS Estafeta

This commit is contained in:
2026-10-02 20:33:12 -06:00
commit c8bb6b97b9
1572 changed files with 457520 additions and 0 deletions
@@ -0,0 +1,251 @@
/* *
*
* Accessibility module - internationalization support
*
* (c) 2010-2020 Highsoft AS
* Author: Øystein Moseng
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import H from '../Core/Globals.js';
import U from '../Core/Utilities.js';
var format = U.format, pick = U.pick;
/* eslint-disable valid-jsdoc */
/**
* 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 (typeof val === 'undefined') {
val = arr[0];
}
}
else {
val = arr[ix];
// Handle positive overshoot
if (typeof val === 'undefined') {
val = arr[arr.length - 1];
}
}
}
return typeof val !== 'undefined' ? val : '';
}
// Standard substitution, delegate to 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.
*
* @requires modules/accessibility
*
* @function Highcharts.i18nFormat
*
* @param {string} formatString
* The string to format.
*
* @param {Highcharts.Dictionary<*>} context
* Context to apply to the format string.
*
* @param {Highcharts.Chart} chart
* A `Chart` instance with a time object and numberFormatter, passed on
* to format().
*
* @return {string}
* The formatted string.
*/
H.i18nFormat = function (formatString, context, chart) {
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 : cursor + 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 format below.
tokens.forEach(function (token) {
if (token.type === 'statement') {
token.value = formatExtendedStatement(token.value, context);
}
});
// Join string back together and pass to format to pick up non-array
// statements.
return format(tokens.reduce(function (acc, cur) {
return acc + cur.value;
}, ''), context, chart);
};
/**
* Apply context to a format string from lang options of the chart.
*
* @requires modules/accessibility
*
* @function Highcharts.Chart#langFormat
*
* @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) {
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, this) : '';
};
@@ -0,0 +1,282 @@
/* *
*
* (c) 2009-2020 Øystein Moseng
*
* Accessibility module for Highcharts
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import ChartUtilities from './Utils/ChartUtilities.js';
import H from '../Core/Globals.js';
import KeyboardNavigationHandler from './KeyboardNavigationHandler.js';
import O from '../Core/Options.js';
var defaultOptions = O.defaultOptions;
import Point from '../Core/Series/Point.js';
import U from '../Core/Utilities.js';
var addEvent = U.addEvent, extend = U.extend, fireEvent = U.fireEvent, merge = U.merge;
var doc = H.win.document;
import AccessibilityComponent from './AccessibilityComponent.js';
import KeyboardNavigation from './KeyboardNavigation.js';
import LegendComponent from './Components/LegendComponent.js';
import MenuComponent from './Components/MenuComponent.js';
import SeriesComponent from './Components/SeriesComponent/SeriesComponent.js';
import ZoomComponent from './Components/ZoomComponent.js';
import RangeSelectorComponent from './Components/RangeSelectorComponent.js';
import InfoRegionsComponent from './Components/InfoRegionsComponent.js';
import ContainerComponent from './Components/ContainerComponent.js';
import whcm from './HighContrastMode.js';
import highContrastTheme from './HighContrastTheme.js';
import defaultOptionsA11Y from './Options/Options.js';
import defaultLangOptions from './Options/LangOptions.js';
import copyDeprecatedOptions from './Options/DeprecatedOptions.js';
import './A11yI18n.js';
import './FocusBorder.js';
// Add default options
merge(true, defaultOptions, defaultOptionsA11Y, {
accessibility: {
highContrastTheme: highContrastTheme
},
lang: defaultLangOptions
});
// Expose functionality on Highcharts namespace
H.A11yChartUtilities = ChartUtilities;
H.KeyboardNavigationHandler = KeyboardNavigationHandler;
H.AccessibilityComponent = AccessibilityComponent;
/* eslint-disable no-invalid-this, valid-jsdoc */
/**
* The Accessibility class
*
* @private
* @requires module:modules/accessibility
*
* @class
* @name Highcharts.Accessibility
*
* @param {Highcharts.Chart} chart
* Chart object
*/
function Accessibility(chart) {
this.init(chart);
}
Accessibility.prototype = {
/**
* Initialize the accessibility class
* @private
* @param {Highcharts.Chart} chart
* Chart object
*/
init: function (chart) {
this.chart = chart;
// Abort on old browsers
if (!doc.addEventListener || !chart.renderer.isSVG) {
chart.renderTo.setAttribute('aria-hidden', true);
return;
}
// Copy over any deprecated options that are used. We could do this on
// every update, but it is probably not needed.
copyDeprecatedOptions(chart);
this.initComponents();
this.keyboardNavigation = new KeyboardNavigation(chart, this.components);
this.update();
},
/**
* @private
*/
initComponents: function () {
var chart = this.chart, a11yOptions = chart.options.accessibility;
this.components = {
container: new ContainerComponent(),
infoRegions: new InfoRegionsComponent(),
legend: new LegendComponent(),
chartMenu: new MenuComponent(),
rangeSelector: new RangeSelectorComponent(),
series: new SeriesComponent(),
zoom: new ZoomComponent()
};
if (a11yOptions.customComponents) {
extend(this.components, a11yOptions.customComponents);
}
var components = this.components;
this.getComponentOrder().forEach(function (componentName) {
components[componentName].initBase(chart);
components[componentName].init();
});
},
/**
* Get order to update components in.
* @private
*/
getComponentOrder: function () {
if (!this.components) {
return []; // For zombie accessibility object on old browsers
}
if (!this.components.series) {
return Object.keys(this.components);
}
var componentsExceptSeries = Object.keys(this.components)
.filter(function (c) { return c !== 'series'; });
// Update series first, so that other components can read accessibility
// info on points.
return ['series'].concat(componentsExceptSeries);
},
/**
* Update all components.
*/
update: function () {
var components = this.components, chart = this.chart, a11yOptions = chart.options.accessibility;
fireEvent(chart, 'beforeA11yUpdate');
// Update the chart type list as this is used by multiple modules
chart.types = this.getChartTypes();
// Update markup
this.getComponentOrder().forEach(function (componentName) {
components[componentName].onChartUpdate();
fireEvent(chart, 'afterA11yComponentUpdate', {
name: componentName,
component: components[componentName]
});
});
// Update keyboard navigation
this.keyboardNavigation.update(a11yOptions.keyboardNavigation.order);
// Handle high contrast mode
if (!chart.highContrastModeActive && // Only do this once
whcm.isHighContrastModeActive()) {
whcm.setHighContrastTheme(chart);
}
fireEvent(chart, 'afterA11yUpdate', {
accessibility: this
});
},
/**
* Destroy all elements.
*/
destroy: function () {
var chart = this.chart || {};
// Destroy components
var components = this.components;
Object.keys(components).forEach(function (componentName) {
components[componentName].destroy();
components[componentName].destroyBase();
});
// Kill keyboard nav
if (this.keyboardNavigation) {
this.keyboardNavigation.destroy();
}
// Hide container from screen readers if it exists
if (chart.renderTo) {
chart.renderTo.setAttribute('aria-hidden', true);
}
// Remove focus border if it exists
if (chart.focusElement) {
chart.focusElement.removeFocusBorder();
}
},
/**
* Return a list of the types of series we have in the chart.
* @private
*/
getChartTypes: function () {
var types = {};
this.chart.series.forEach(function (series) {
types[series.type] = 1;
});
return Object.keys(types);
}
};
/**
* @private
*/
H.Chart.prototype.updateA11yEnabled = function () {
var a11y = this.accessibility, accessibilityOptions = this.options.accessibility;
if (accessibilityOptions && accessibilityOptions.enabled) {
if (a11y) {
a11y.update();
}
else {
this.accessibility = a11y = new Accessibility(this);
}
}
else if (a11y) {
// Destroy if after update we have a11y and it is disabled
if (a11y.destroy) {
a11y.destroy();
}
delete this.accessibility;
}
else {
// Just hide container
this.renderTo.setAttribute('aria-hidden', true);
}
};
// Handle updates to the module and send render updates to components
addEvent(H.Chart, 'render', function (e) {
// Update/destroy
if (this.a11yDirty && this.renderTo) {
delete this.a11yDirty;
this.updateA11yEnabled();
}
var a11y = this.accessibility;
if (a11y) {
a11y.getComponentOrder().forEach(function (componentName) {
a11y.components[componentName].onChartRender();
});
}
});
// Update with chart/series/point updates
addEvent(H.Chart, 'update', function (e) {
// Merge new options
var newOptions = e.options.accessibility;
if (newOptions) {
// Handle custom component updating specifically
if (newOptions.customComponents) {
this.options.accessibility.customComponents =
newOptions.customComponents;
delete newOptions.customComponents;
}
merge(true, this.options.accessibility, newOptions);
// Recreate from scratch
if (this.accessibility && this.accessibility.destroy) {
this.accessibility.destroy();
delete this.accessibility;
}
}
// Mark dirty for update
this.a11yDirty = true;
});
// Mark dirty for update
addEvent(Point, 'update', function () {
if (this.series.chart.accessibility) {
this.series.chart.a11yDirty = true;
}
});
['addSeries', 'init'].forEach(function (event) {
addEvent(H.Chart, event, function () {
this.a11yDirty = true;
});
});
['update', 'updatedData', 'remove'].forEach(function (event) {
addEvent(H.Series, event, function () {
if (this.chart.accessibility) {
this.chart.a11yDirty = true;
}
});
});
// Direct updates (events happen after render)
[
'afterDrilldown', 'drillupall'
].forEach(function (event) {
addEvent(H.Chart, event, function () {
if (this.accessibility) {
this.accessibility.update();
}
});
});
// Destroy with chart
addEvent(H.Chart, 'destroy', function () {
if (this.accessibility) {
this.accessibility.destroy();
}
});
@@ -0,0 +1,380 @@
/* *
*
* (c) 2009-2020 Øystein Moseng
*
* Accessibility component class definition
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import H from '../Core/Globals.js';
var win = H.win, doc = win.document;
import U from '../Core/Utilities.js';
var extend = U.extend, fireEvent = U.fireEvent, merge = U.merge;
import HTMLUtilities from './Utils/HTMLUtilities.js';
var removeElement = HTMLUtilities.removeElement, getFakeMouseEvent = HTMLUtilities.getFakeMouseEvent;
import ChartUtilities from './Utils/ChartUtilities.js';
var unhideChartElementFromAT = ChartUtilities.unhideChartElementFromAT;
import EventProvider from './Utils/EventProvider.js';
import DOMElementProvider from './Utils/DOMElementProvider.js';
/* eslint-disable valid-jsdoc */
/** @lends Highcharts.AccessibilityComponent */
var functionsToOverrideByDerivedClasses = {
/**
* Called on component initialization.
*/
init: function () { },
/**
* Get keyboard navigation handler for this component.
* @return {Highcharts.KeyboardNavigationHandler}
*/
getKeyboardNavigation: function () { },
/**
* Called on updates to the chart, including options changes.
* Note that this is also called on first render of chart.
*/
onChartUpdate: function () { },
/**
* Called on every chart render.
*/
onChartRender: function () { },
/**
* Called when accessibility is disabled or chart is destroyed.
*/
destroy: function () { }
};
/**
* The AccessibilityComponent base class, representing a part of the chart that
* has accessibility logic connected to it. This class can be inherited from to
* create a custom accessibility component for a chart.
*
* Components should take care to destroy added elements and unregister event
* handlers on destroy. This is handled automatically if using this.addEvent and
* this.createElement.
*
* @sample highcharts/accessibility/custom-component
* Custom accessibility component
*
* @requires module:modules/accessibility
* @class
* @name Highcharts.AccessibilityComponent
*/
function AccessibilityComponent() { }
/**
* @lends Highcharts.AccessibilityComponent
*/
AccessibilityComponent.prototype = {
/**
* Initialize the class
* @private
* @param {Highcharts.Chart} chart
* Chart object
*/
initBase: function (chart) {
this.chart = chart;
this.eventProvider = new EventProvider();
this.domElementProvider = new DOMElementProvider();
// Key code enum for common keys
this.keyCodes = {
left: 37,
right: 39,
up: 38,
down: 40,
enter: 13,
space: 32,
esc: 27,
tab: 9
};
},
/**
* Add an event to an element and keep track of it for later removal.
* See EventProvider for details.
* @private
*/
addEvent: function () {
return this.eventProvider.addEvent
.apply(this.eventProvider, arguments);
},
/**
* Create an element and keep track of it for later removal.
* See DOMElementProvider for details.
* @private
*/
createElement: function () {
return this.domElementProvider.createElement.apply(this.domElementProvider, arguments);
},
/**
* Fire an event on an element that is either wrapped by Highcharts,
* or a DOM element
* @private
* @param {Highcharts.HTMLElement|Highcharts.HTMLDOMElement|
* Highcharts.SVGDOMElement|Highcharts.SVGElement} el
* @param {Event} eventObject
*/
fireEventOnWrappedOrUnwrappedElement: function (el, eventObject) {
var type = eventObject.type;
if (doc.createEvent && (el.dispatchEvent || el.fireEvent)) {
if (el.dispatchEvent) {
el.dispatchEvent(eventObject);
}
else {
el.fireEvent(type, eventObject);
}
}
else {
fireEvent(el, type, eventObject);
}
},
/**
* Utility function to attempt to fake a click event on an element.
* @private
* @param {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement} element
*/
fakeClickEvent: function (element) {
if (element) {
var fakeEventObject = getFakeMouseEvent('click');
this.fireEventOnWrappedOrUnwrappedElement(element, fakeEventObject);
}
},
/**
* Add a new proxy group to the proxy container. Creates the proxy container
* if it does not exist.
* @private
* @param {Highcharts.HTMLAttributes} [attrs]
* The attributes to set on the new group div.
* @return {Highcharts.HTMLDOMElement}
* The new proxy group element.
*/
addProxyGroup: function (attrs) {
this.createOrUpdateProxyContainer();
var groupDiv = this.createElement('div');
Object.keys(attrs || {}).forEach(function (prop) {
if (attrs[prop] !== null) {
groupDiv.setAttribute(prop, attrs[prop]);
}
});
this.chart.a11yProxyContainer.appendChild(groupDiv);
return groupDiv;
},
/**
* Creates and updates DOM position of proxy container
* @private
*/
createOrUpdateProxyContainer: function () {
var chart = this.chart, rendererSVGEl = chart.renderer.box;
chart.a11yProxyContainer = chart.a11yProxyContainer ||
this.createProxyContainerElement();
if (rendererSVGEl.nextSibling !== chart.a11yProxyContainer) {
chart.container.insertBefore(chart.a11yProxyContainer, rendererSVGEl.nextSibling);
}
},
/**
* @private
* @return {Highcharts.HTMLDOMElement} element
*/
createProxyContainerElement: function () {
var pc = doc.createElement('div');
pc.className = 'highcharts-a11y-proxy-container';
return pc;
},
/**
* Create an invisible proxy HTML button in the same position as an SVG
* element
* @private
* @param {Highcharts.SVGElement} svgElement
* The wrapped svg el to proxy.
* @param {Highcharts.HTMLDOMElement} parentGroup
* The proxy group element in the proxy container to add this button to.
* @param {Highcharts.SVGAttributes} [attributes]
* Additional attributes to set.
* @param {Highcharts.SVGElement} [posElement]
* Element to use for positioning instead of svgElement.
* @param {Function} [preClickEvent]
* Function to call before click event fires.
*
* @return {Highcharts.HTMLDOMElement} The proxy button.
*/
createProxyButton: function (svgElement, parentGroup, attributes, posElement, preClickEvent) {
var svgEl = svgElement.element, proxy = this.createElement('button'), attrs = merge({
'aria-label': svgEl.getAttribute('aria-label')
}, attributes);
Object.keys(attrs).forEach(function (prop) {
if (attrs[prop] !== null) {
proxy.setAttribute(prop, attrs[prop]);
}
});
proxy.className = 'highcharts-a11y-proxy-button';
if (preClickEvent) {
this.addEvent(proxy, 'click', preClickEvent);
}
this.setProxyButtonStyle(proxy);
this.updateProxyButtonPosition(proxy, posElement || svgElement);
this.proxyMouseEventsForButton(svgEl, proxy);
// Add to chart div and unhide from screen readers
parentGroup.appendChild(proxy);
if (!attrs['aria-hidden']) {
unhideChartElementFromAT(this.chart, proxy);
}
return proxy;
},
/**
* Get the position relative to chart container for a wrapped SVG element.
* @private
* @param {Highcharts.SVGElement} element
* The element to calculate position for.
* @return {Highcharts.BBoxObject}
* Object with x and y props for the position.
*/
getElementPosition: function (element) {
var el = element.element, div = this.chart.renderTo;
if (div && el && el.getBoundingClientRect) {
var rectEl = el.getBoundingClientRect(), rectDiv = div.getBoundingClientRect();
return {
x: rectEl.left - rectDiv.left,
y: rectEl.top - rectDiv.top,
width: rectEl.right - rectEl.left,
height: rectEl.bottom - rectEl.top
};
}
return { x: 0, y: 0, width: 1, height: 1 };
},
/**
* @private
* @param {Highcharts.HTMLElement} button The proxy element.
*/
setProxyButtonStyle: function (button) {
merge(true, button.style, {
'border-width': 0,
'background-color': 'transparent',
cursor: 'pointer',
outline: 'none',
opacity: 0.001,
filter: 'alpha(opacity=1)',
'-ms-filter': 'progid:DXImageTransform.Microsoft.Alpha(Opacity=1)',
zIndex: 999,
overflow: 'hidden',
padding: 0,
margin: 0,
display: 'block',
position: 'absolute'
});
},
/**
* @private
* @param {Highcharts.HTMLElement} proxy The proxy to update position of.
* @param {Highcharts.SVGElement} posElement The element to overlay and take position from.
*/
updateProxyButtonPosition: function (proxy, posElement) {
var bBox = this.getElementPosition(posElement);
merge(true, proxy.style, {
width: (bBox.width || 1) + 'px',
height: (bBox.height || 1) + 'px',
left: (bBox.x || 0) + 'px',
top: (bBox.y || 0) + 'px'
});
},
/**
* @private
* @param {Highcharts.HTMLElement|Highcharts.HTMLDOMElement|
* Highcharts.SVGDOMElement|Highcharts.SVGElement} source
* @param {Highcharts.HTMLElement} button
*/
proxyMouseEventsForButton: function (source, button) {
var component = this;
[
'click', 'touchstart', 'touchend', 'touchcancel', 'touchmove',
'mouseover', 'mouseenter', 'mouseleave', 'mouseout'
].forEach(function (evtType) {
var isTouchEvent = evtType.indexOf('touch') === 0;
component.addEvent(button, evtType, function (e) {
var clonedEvent = isTouchEvent ?
component.cloneTouchEvent(e) :
component.cloneMouseEvent(e);
if (source) {
component.fireEventOnWrappedOrUnwrappedElement(source, clonedEvent);
}
e.stopPropagation();
e.preventDefault();
});
});
},
/**
* Utility function to clone a mouse event for re-dispatching.
* @private
* @param {global.MouseEvent} e The event to clone.
* @return {global.MouseEvent} The cloned event
*/
cloneMouseEvent: function (e) {
if (typeof win.MouseEvent === 'function') {
return new win.MouseEvent(e.type, e);
}
// No MouseEvent support, try using initMouseEvent
if (doc.createEvent) {
var evt = doc.createEvent('MouseEvent');
if (evt.initMouseEvent) {
evt.initMouseEvent(e.type, e.bubbles, // #10561, #12161
e.cancelable, e.view || win, e.detail, e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey, e.altKey, e.shiftKey, e.metaKey, e.button, e.relatedTarget);
return evt;
}
}
return getFakeMouseEvent(e.type);
},
/**
* Utility function to clone a touch event for re-dispatching.
* @private
* @param {global.TouchEvent} e The event to clone.
* @return {global.TouchEvent} The cloned event
*/
cloneTouchEvent: function (e) {
var touchListToTouchArray = function (l) {
var touchArray = [];
for (var i = 0; i < l.length; ++i) {
var item = l.item(i);
if (item) {
touchArray.push(item);
}
}
return touchArray;
};
if (typeof win.TouchEvent === 'function') {
var newEvent = new win.TouchEvent(e.type, {
touches: touchListToTouchArray(e.touches),
targetTouches: touchListToTouchArray(e.targetTouches),
changedTouches: touchListToTouchArray(e.changedTouches),
ctrlKey: e.ctrlKey,
shiftKey: e.shiftKey,
altKey: e.altKey,
metaKey: e.metaKey,
bubbles: e.bubbles,
cancelable: e.cancelable,
composed: e.composed,
detail: e.detail,
view: e.view
});
if (e.defaultPrevented) {
newEvent.preventDefault();
}
return newEvent;
}
// Fallback to mouse event
var fakeEvt = this.cloneMouseEvent(e);
fakeEvt.touches = e.touches;
fakeEvt.changedTouches = e.changedTouches;
fakeEvt.targetTouches = e.targetTouches;
return fakeEvt;
},
/**
* Remove traces of the component.
* @private
*/
destroyBase: function () {
removeElement(this.chart.a11yProxyContainer);
this.domElementProvider.destroyCreatedElements();
this.eventProvider.removeAddedEvents();
}
};
extend(AccessibilityComponent.prototype, functionsToOverrideByDerivedClasses);
export default AccessibilityComponent;
@@ -0,0 +1,134 @@
/* *
*
* (c) 2009-2019 Øystein Moseng
*
* Annotations accessibility code.
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import HTMLUtilities from '../Utils/HTMLUtilities.js';
var escapeStringForHTML = HTMLUtilities.escapeStringForHTML, stripHTMLTagsFromString = HTMLUtilities.stripHTMLTagsFromString;
/**
* Get list of all annotation labels in the chart.
*
* @private
* @param {Highcharts.Chart} chart The chart to get annotation info on.
* @return {Array<object>} The labels, or empty array if none.
*/
function getChartAnnotationLabels(chart) {
var annotations = chart.annotations || [];
return annotations.reduce(function (acc, cur) {
var _a;
if (((_a = cur.options) === null || _a === void 0 ? void 0 : _a.visible) !== false) {
acc = acc.concat(cur.labels);
}
return acc;
}, []);
}
/**
* Get the text of an annotation label.
*
* @private
* @param {object} label The annotation label object
* @return {string} The text in the label.
*/
function getLabelText(label) {
var _a, _b, _c, _d;
var a11yDesc = (_b = (_a = label.options) === null || _a === void 0 ? void 0 : _a.accessibility) === null || _b === void 0 ? void 0 : _b.description;
return a11yDesc ? a11yDesc : ((_d = (_c = label.graphic) === null || _c === void 0 ? void 0 : _c.text) === null || _d === void 0 ? void 0 : _d.textStr) || '';
}
/**
* Describe an annotation label.
*
* @private
* @param {object} label The annotation label object to describe
* @return {string} The description for the label.
*/
function getAnnotationLabelDescription(label) {
var _a, _b;
var a11yDesc = (_b = (_a = label.options) === null || _a === void 0 ? void 0 : _a.accessibility) === null || _b === void 0 ? void 0 : _b.description;
if (a11yDesc) {
return a11yDesc;
}
var chart = label.chart;
var labelText = getLabelText(label);
var points = label.points;
var getAriaLabel = function (point) { var _a, _b; return ((_b = (_a = point === null || point === void 0 ? void 0 : point.graphic) === null || _a === void 0 ? void 0 : _a.element) === null || _b === void 0 ? void 0 : _b.getAttribute('aria-label')) || ''; };
var getValueDesc = function (point) {
var _a;
var valDesc = ((_a = point === null || point === void 0 ? void 0 : point.accessibility) === null || _a === void 0 ? void 0 : _a.valueDescription) || getAriaLabel(point);
var seriesName = (point === null || point === void 0 ? void 0 : point.series.name) || '';
return (seriesName ? seriesName + ', ' : '') + 'data point ' + valDesc;
};
var pointValueDescriptions = points
.filter(function (p) { return !!p.graphic; }) // Filter out mock points
.map(getValueDesc)
.filter(function (desc) { return !!desc; }); // Filter out points we can't describe
var numPoints = pointValueDescriptions.length;
var pointsSelector = numPoints > 1 ? 'MultiplePoints' : numPoints ? 'SinglePoint' : 'NoPoints';
var langFormatStr = 'accessibility.screenReaderSection.annotations.description' + pointsSelector;
var context = {
annotationText: labelText,
numPoints: numPoints,
annotationPoint: pointValueDescriptions[0],
additionalAnnotationPoints: pointValueDescriptions.slice(1)
};
return chart.langFormat(langFormatStr, context);
}
/**
* Return array of HTML strings for each annotation label in the chart.
*
* @private
* @param {Highcharts.Chart} chart The chart to get annotation info on.
* @return {Array<string>} Array of strings with HTML content for each annotation label.
*/
function getAnnotationListItems(chart) {
var labels = getChartAnnotationLabels(chart);
return labels.map(function (label) {
var desc = escapeStringForHTML(stripHTMLTagsFromString(getAnnotationLabelDescription(label)));
return desc ? "<li>" + desc + "</li>" : '';
});
}
/**
* Return the annotation info for a chart as string.
*
* @private
* @param {Highcharts.Chart} chart The chart to get annotation info on.
* @return {string} String with HTML content or empty string if no annotations.
*/
function getAnnotationsInfoHTML(chart) {
var annotations = chart.annotations;
if (!(annotations && annotations.length)) {
return '';
}
var annotationItems = getAnnotationListItems(chart);
return "<ul>" + annotationItems.join(' ') + "</ul>";
}
/**
* Return the texts for the annotation(s) connected to a point, or empty array
* if none.
*
* @private
* @param {Highcharts.Point} point The data point to get the annotation info from.
* @return {Array<string>} Annotation texts
*/
function getPointAnnotationTexts(point) {
var labels = getChartAnnotationLabels(point.series.chart);
var pointLabels = labels
.filter(function (label) { return label.points.indexOf(point) > -1; });
if (!pointLabels.length) {
return [];
}
return pointLabels.map(function (label) { return "" + getLabelText(label); });
}
var AnnotationsA11y = {
getAnnotationsInfoHTML: getAnnotationsInfoHTML,
getAnnotationLabelDescription: getAnnotationLabelDescription,
getAnnotationListItems: getAnnotationListItems,
getPointAnnotationTexts: getPointAnnotationTexts
};
export default AnnotationsA11y;
@@ -0,0 +1,115 @@
/* *
*
* (c) 2009-2020 Øystein Moseng
*
* Accessibility component for chart container.
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import H from '../../Core/Globals.js';
var doc = H.win.document;
import U from '../../Core/Utilities.js';
var extend = U.extend;
import HTMLUtilities from '../Utils/HTMLUtilities.js';
var stripHTMLTags = HTMLUtilities.stripHTMLTagsFromString;
import ChartUtilities from '../Utils/ChartUtilities.js';
var unhideChartElementFromAT = ChartUtilities.unhideChartElementFromAT, getChartTitle = ChartUtilities.getChartTitle;
import AccessibilityComponent from '../AccessibilityComponent.js';
/* eslint-disable valid-jsdoc */
/**
* The ContainerComponent class
*
* @private
* @class
* @name Highcharts.ContainerComponent
*/
var ContainerComponent = function () { };
ContainerComponent.prototype = new AccessibilityComponent();
extend(ContainerComponent.prototype, /** @lends Highcharts.ContainerComponent */ {
/**
* Called on first render/updates to the chart, including options changes.
*/
onChartUpdate: function () {
this.handleSVGTitleElement();
this.setSVGContainerLabel();
this.setGraphicContainerAttrs();
this.setRenderToAttrs();
this.makeCreditsAccessible();
},
/**
* @private
*/
handleSVGTitleElement: function () {
var chart = this.chart, titleId = 'highcharts-title-' + chart.index, titleContents = stripHTMLTags(chart.langFormat('accessibility.svgContainerTitle', {
chartTitle: getChartTitle(chart)
}));
if (titleContents.length) {
var titleElement = this.svgTitleElement =
this.svgTitleElement || doc.createElementNS('http://www.w3.org/2000/svg', 'title');
titleElement.textContent = titleContents;
titleElement.id = titleId;
chart.renderTo.insertBefore(titleElement, chart.renderTo.firstChild);
}
},
/**
* @private
*/
setSVGContainerLabel: function () {
var chart = this.chart, svgContainerLabel = stripHTMLTags(chart.langFormat('accessibility.svgContainerLabel', {
chartTitle: getChartTitle(chart)
}));
if (chart.renderer.box && svgContainerLabel.length) {
chart.renderer.box.setAttribute('aria-label', svgContainerLabel);
}
},
/**
* @private
*/
setGraphicContainerAttrs: function () {
var chart = this.chart, label = chart.langFormat('accessibility.graphicContainerLabel', {
chartTitle: getChartTitle(chart)
});
if (label.length) {
chart.container.setAttribute('aria-label', label);
}
},
/**
* @private
*/
setRenderToAttrs: function () {
var chart = this.chart;
if (chart.options.accessibility.landmarkVerbosity !== 'disabled') {
chart.renderTo.setAttribute('role', 'region');
}
else {
chart.renderTo.removeAttribute('role');
}
chart.renderTo.setAttribute('aria-label', chart.langFormat('accessibility.chartContainerLabel', {
title: getChartTitle(chart),
chart: chart
}));
},
/**
* @private
*/
makeCreditsAccessible: function () {
var chart = this.chart, credits = chart.credits;
if (credits) {
if (credits.textStr) {
credits.element.setAttribute('aria-label', stripHTMLTags(chart.langFormat('accessibility.credits', { creditsStr: credits.textStr })));
}
unhideChartElementFromAT(chart, credits.element);
}
},
/**
* Accessibility disabled/chart destroyed.
*/
destroy: function () {
this.chart.renderTo.setAttribute('aria-hidden', true);
}
});
export default ContainerComponent;
@@ -0,0 +1,563 @@
/* *
*
* (c) 2009-2020 Øystein Moseng
*
* Accessibility component for chart info region and table.
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import H from '../../Core/Globals.js';
var doc = H.doc;
import U from '../../Core/Utilities.js';
var extend = U.extend, format = U.format, pick = U.pick;
import AccessibilityComponent from '../AccessibilityComponent.js';
import Announcer from '../Utils/Announcer.js';
import AnnotationsA11y from './AnnotationsA11y.js';
var getAnnotationsInfoHTML = AnnotationsA11y.getAnnotationsInfoHTML;
import ChartUtilities from '../Utils/ChartUtilities.js';
var unhideChartElementFromAT = ChartUtilities.unhideChartElementFromAT, getChartTitle = ChartUtilities.getChartTitle, getAxisDescription = ChartUtilities.getAxisDescription;
import HTMLUtilities from '../Utils/HTMLUtilities.js';
var addClass = HTMLUtilities.addClass, setElAttrs = HTMLUtilities.setElAttrs, escapeStringForHTML = HTMLUtilities.escapeStringForHTML, stripHTMLTagsFromString = HTMLUtilities.stripHTMLTagsFromString, getElement = HTMLUtilities.getElement, visuallyHideElement = HTMLUtilities.visuallyHideElement;
/* eslint-disable no-invalid-this, valid-jsdoc */
/**
* @private
*/
function getTypeDescForMapChart(chart, formatContext) {
return formatContext.mapTitle ?
chart.langFormat('accessibility.chartTypes.mapTypeDescription', formatContext) :
chart.langFormat('accessibility.chartTypes.unknownMap', formatContext);
}
/**
* @private
*/
function getTypeDescForCombinationChart(chart, formatContext) {
return chart.langFormat('accessibility.chartTypes.combinationChart', formatContext);
}
/**
* @private
*/
function getTypeDescForEmptyChart(chart, formatContext) {
return chart.langFormat('accessibility.chartTypes.emptyChart', formatContext);
}
/**
* @private
*/
function buildTypeDescriptionFromSeries(chart, types, context) {
var firstType = types[0], typeExplaination = chart.langFormat('accessibility.seriesTypeDescriptions.' + firstType, context), multi = chart.series && chart.series.length < 2 ? 'Single' : 'Multiple';
return (chart.langFormat('accessibility.chartTypes.' + firstType + multi, context) ||
chart.langFormat('accessibility.chartTypes.default' + multi, context)) + (typeExplaination ? ' ' + typeExplaination : '');
}
/**
* @private
*/
function getTableSummary(chart) {
return chart.langFormat('accessibility.table.tableSummary', { chart: chart });
}
/**
* @private
*/
function stripEmptyHTMLTags(str) {
return str.replace(/<(\w+)[^>]*?>\s*<\/\1>/g, '');
}
/**
* @private
*/
function enableSimpleHTML(str) {
return str
.replace(/&lt;(h[1-7]|p|div|ul|ol|li)&gt;/g, '<$1>')
.replace(/&lt;&#x2F;(h[1-7]|p|div|ul|ol|li|a|button)&gt;/g, '</$1>')
.replace(/&lt;(div|a|button) id=&quot;([a-zA-Z\-0-9#]*?)&quot;&gt;/g, '<$1 id="$2">');
}
/**
* @private
*/
function stringToSimpleHTML(str) {
return stripEmptyHTMLTags(enableSimpleHTML(escapeStringForHTML(str)));
}
/**
* Return simplified explaination of chart type. Some types will not be familiar
* to most users, but in those cases we try to add an explaination of the type.
*
* @private
* @function Highcharts.Chart#getTypeDescription
* @param {Array<string>} types The series types in this chart.
* @return {string} The text description of the chart type.
*/
H.Chart.prototype.getTypeDescription = function (types) {
var firstType = types[0], firstSeries = this.series && this.series[0] || {}, formatContext = {
numSeries: this.series.length,
numPoints: firstSeries.points && firstSeries.points.length,
chart: this,
mapTitle: firstSeries.mapTitle
};
if (!firstType) {
return getTypeDescForEmptyChart(this, formatContext);
}
if (firstType === 'map') {
return getTypeDescForMapChart(this, formatContext);
}
if (this.types.length > 1) {
return getTypeDescForCombinationChart(this, formatContext);
}
return buildTypeDescriptionFromSeries(this, types, formatContext);
};
/**
* The InfoRegionsComponent class
*
* @private
* @class
* @name Highcharts.InfoRegionsComponent
*/
var InfoRegionsComponent = function () { };
InfoRegionsComponent.prototype = new AccessibilityComponent();
extend(InfoRegionsComponent.prototype, /** @lends Highcharts.InfoRegionsComponent */ {
/**
* Init the component
* @private
*/
init: function () {
var chart = this.chart;
var component = this;
this.initRegionsDefinitions();
this.addEvent(chart, 'afterGetTable', function (e) {
component.onDataTableCreated(e);
});
this.addEvent(chart, 'afterViewData', function (tableDiv) {
component.dataTableDiv = tableDiv;
// Use small delay to give browsers & AT time to register new table
setTimeout(function () {
component.focusDataTable();
}, 300);
});
this.announcer = new Announcer(chart, 'assertive');
},
/**
* @private
*/
initRegionsDefinitions: function () {
var component = this;
this.screenReaderSections = {
before: {
element: null,
buildContent: function (chart) {
var formatter = chart.options.accessibility
.screenReaderSection.beforeChartFormatter;
return formatter ? formatter(chart) :
component.defaultBeforeChartFormatter(chart);
},
insertIntoDOM: function (el, chart) {
chart.renderTo.insertBefore(el, chart.renderTo.firstChild);
},
afterInserted: function () {
if (typeof component.sonifyButtonId !== 'undefined') {
component.initSonifyButton(component.sonifyButtonId);
}
if (typeof component.dataTableButtonId !== 'undefined') {
component.initDataTableButton(component.dataTableButtonId);
}
}
},
after: {
element: null,
buildContent: function (chart) {
var formatter = chart.options.accessibility.screenReaderSection
.afterChartFormatter;
return formatter ? formatter(chart) :
component.defaultAfterChartFormatter();
},
insertIntoDOM: function (el, chart) {
chart.renderTo.insertBefore(el, chart.container.nextSibling);
}
}
};
},
/**
* Called on chart render. Have to update the sections on render, in order
* to get a11y info from series.
*/
onChartRender: function () {
var component = this;
this.linkedDescriptionElement = this.getLinkedDescriptionElement();
this.setLinkedDescriptionAttrs();
Object.keys(this.screenReaderSections).forEach(function (regionKey) {
component.updateScreenReaderSection(regionKey);
});
},
/**
* @private
*/
getLinkedDescriptionElement: function () {
var chartOptions = this.chart.options, linkedDescOption = chartOptions.accessibility.linkedDescription;
if (!linkedDescOption) {
return;
}
if (typeof linkedDescOption !== 'string') {
return linkedDescOption;
}
var query = format(linkedDescOption, this.chart), queryMatch = doc.querySelectorAll(query);
if (queryMatch.length === 1) {
return queryMatch[0];
}
},
/**
* @private
*/
setLinkedDescriptionAttrs: function () {
var el = this.linkedDescriptionElement;
if (el) {
el.setAttribute('aria-hidden', 'true');
addClass(el, 'highcharts-linked-description');
}
},
/**
* @private
* @param {string} regionKey The name/key of the region to update
*/
updateScreenReaderSection: function (regionKey) {
var chart = this.chart, region = this.screenReaderSections[regionKey], content = region.buildContent(chart), sectionDiv = region.element = (region.element || this.createElement('div')), hiddenDiv = (sectionDiv.firstChild || this.createElement('div'));
this.setScreenReaderSectionAttribs(sectionDiv, regionKey);
hiddenDiv.innerHTML = content;
sectionDiv.appendChild(hiddenDiv);
region.insertIntoDOM(sectionDiv, chart);
visuallyHideElement(hiddenDiv);
unhideChartElementFromAT(chart, hiddenDiv);
if (region.afterInserted) {
region.afterInserted();
}
},
/**
* @private
* @param {Highcharts.HTMLDOMElement} sectionDiv The section element
* @param {string} regionKey Name/key of the region we are setting attrs for
*/
setScreenReaderSectionAttribs: function (sectionDiv, regionKey) {
var labelLangKey = ('accessibility.screenReaderSection.' + regionKey + 'RegionLabel'), chart = this.chart, labelText = chart.langFormat(labelLangKey, { chart: chart }), sectionId = 'highcharts-screen-reader-region-' + regionKey + '-' +
chart.index;
setElAttrs(sectionDiv, {
id: sectionId,
'aria-label': labelText
});
// Sections are wrapped to be positioned relatively to chart in case
// elements inside are tabbed to.
sectionDiv.style.position = 'relative';
if (chart.options.accessibility.landmarkVerbosity === 'all' &&
labelText) {
sectionDiv.setAttribute('role', 'region');
}
},
/**
* @private
* @return {string}
*/
defaultBeforeChartFormatter: function () {
var _a;
var chart = this.chart, format = chart.options.accessibility
.screenReaderSection.beforeChartFormat, axesDesc = this.getAxesDescription(), shouldHaveSonifyBtn = chart.sonify && ((_a = chart.options.sonification) === null || _a === void 0 ? void 0 : _a.enabled), sonifyButtonId = 'highcharts-a11y-sonify-data-btn-' +
chart.index, dataTableButtonId = 'hc-linkto-highcharts-data-table-' +
chart.index, annotationsList = getAnnotationsInfoHTML(chart), annotationsTitleStr = chart.langFormat('accessibility.screenReaderSection.annotations.heading', { chart: chart }), context = {
chartTitle: getChartTitle(chart),
typeDescription: this.getTypeDescriptionText(),
chartSubtitle: this.getSubtitleText(),
chartLongdesc: this.getLongdescText(),
xAxisDescription: axesDesc.xAxis,
yAxisDescription: axesDesc.yAxis,
playAsSoundButton: shouldHaveSonifyBtn ?
this.getSonifyButtonText(sonifyButtonId) : '',
viewTableButton: chart.getCSV ?
this.getDataTableButtonText(dataTableButtonId) : '',
annotationsTitle: annotationsList ? annotationsTitleStr : '',
annotationsList: annotationsList
}, formattedString = H.i18nFormat(format, context, chart);
this.dataTableButtonId = dataTableButtonId;
this.sonifyButtonId = sonifyButtonId;
return stringToSimpleHTML(formattedString);
},
/**
* @private
* @return {string}
*/
defaultAfterChartFormatter: function () {
var chart = this.chart, format = chart.options.accessibility
.screenReaderSection.afterChartFormat, context = {
endOfChartMarker: this.getEndOfChartMarkerText()
}, formattedString = H.i18nFormat(format, context, chart);
return stringToSimpleHTML(formattedString);
},
/**
* @private
* @return {string}
*/
getLinkedDescription: function () {
var el = this.linkedDescriptionElement, content = el && el.innerHTML || '';
return stripHTMLTagsFromString(content);
},
/**
* @private
* @return {string}
*/
getLongdescText: function () {
var chartOptions = this.chart.options, captionOptions = chartOptions.caption, captionText = captionOptions && captionOptions.text, linkedDescription = this.getLinkedDescription();
return (chartOptions.accessibility.description ||
linkedDescription ||
captionText ||
'');
},
/**
* @private
* @return {string}
*/
getTypeDescriptionText: function () {
var chart = this.chart;
return chart.types ?
chart.options.accessibility.typeDescription ||
chart.getTypeDescription(chart.types) : '';
},
/**
* @private
* @param {string} buttonId
* @return {string}
*/
getDataTableButtonText: function (buttonId) {
var chart = this.chart, buttonText = chart.langFormat('accessibility.table.viewAsDataTableButtonText', { chart: chart, chartTitle: getChartTitle(chart) });
return '<button id="' + buttonId + '">' + buttonText + '</button>';
},
/**
* @private
* @param {string} buttonId
* @return {string}
*/
getSonifyButtonText: function (buttonId) {
var _a;
var chart = this.chart;
if (((_a = chart.options.sonification) === null || _a === void 0 ? void 0 : _a.enabled) === false) {
return '';
}
var buttonText = chart.langFormat('accessibility.sonification.playAsSoundButtonText', { chart: chart, chartTitle: getChartTitle(chart) });
return '<button id="' + buttonId + '">' + buttonText + '</button>';
},
/**
* @private
* @return {string}
*/
getSubtitleText: function () {
var subtitle = (this.chart.options.subtitle);
return stripHTMLTagsFromString(subtitle && subtitle.text || '');
},
/**
* @private
* @return {string}
*/
getEndOfChartMarkerText: function () {
var chart = this.chart, markerText = chart.langFormat('accessibility.screenReaderSection.endOfChartMarker', { chart: chart }), id = 'highcharts-end-of-chart-marker-' + chart.index;
return '<div id="' + id + '">' + markerText + '</div>';
},
/**
* @private
* @param {Highcharts.Dictionary<string>} e
*/
onDataTableCreated: function (e) {
var chart = this.chart;
if (chart.options.accessibility.enabled) {
if (this.viewDataTableButton) {
this.viewDataTableButton.setAttribute('aria-expanded', 'true');
}
e.html = e.html.replace('<table ', '<table tabindex="-1" summary="' + getTableSummary(chart) + '"');
}
},
/**
* @private
*/
focusDataTable: function () {
var tableDiv = this.dataTableDiv, table = tableDiv && tableDiv.getElementsByTagName('table')[0];
if (table && table.focus) {
table.focus();
}
},
/**
* @private
* @param {string} sonifyButtonId
*/
initSonifyButton: function (sonifyButtonId) {
var _this = this;
var el = this.sonifyButton = getElement(sonifyButtonId);
var chart = this.chart;
var defaultHandler = function (e) {
el === null || el === void 0 ? void 0 : el.setAttribute('aria-hidden', 'true');
el === null || el === void 0 ? void 0 : el.setAttribute('aria-label', '');
e.preventDefault();
e.stopPropagation();
var announceMsg = chart.langFormat('accessibility.sonification.playAsSoundClickAnnouncement', { chart: chart });
_this.announcer.announce(announceMsg);
setTimeout(function () {
el === null || el === void 0 ? void 0 : el.removeAttribute('aria-hidden');
el === null || el === void 0 ? void 0 : el.removeAttribute('aria-label');
if (chart.sonify) {
chart.sonify();
}
}, 1000); // Delay to let screen reader speak the button press
};
if (el && chart) {
setElAttrs(el, {
tabindex: '-1'
});
el.onclick = function (e) {
var _a;
var onPlayAsSoundClick = (_a = chart.options.accessibility) === null || _a === void 0 ? void 0 : _a.screenReaderSection.onPlayAsSoundClick;
(onPlayAsSoundClick || defaultHandler).call(this, e, chart);
};
}
},
/**
* Set attribs and handlers for default viewAsDataTable button if exists.
* @private
* @param {string} tableButtonId
*/
initDataTableButton: function (tableButtonId) {
var el = this.viewDataTableButton = getElement(tableButtonId), chart = this.chart, tableId = tableButtonId.replace('hc-linkto-', '');
if (el) {
setElAttrs(el, {
tabindex: '-1',
'aria-expanded': !!getElement(tableId)
});
el.onclick = chart.options.accessibility
.screenReaderSection.onViewDataTableClick ||
function () {
chart.viewData();
};
}
},
/**
* Return object with text description of each of the chart's axes.
* @private
* @return {Highcharts.Dictionary<string>}
*/
getAxesDescription: function () {
var chart = this.chart, shouldDescribeColl = function (collectionKey, defaultCondition) {
var axes = chart[collectionKey];
return axes.length > 1 || axes[0] &&
pick(axes[0].options.accessibility &&
axes[0].options.accessibility.enabled, defaultCondition);
}, hasNoMap = !!chart.types && chart.types.indexOf('map') < 0, hasCartesian = !!chart.hasCartesianSeries, showXAxes = shouldDescribeColl('xAxis', !chart.angular && hasCartesian && hasNoMap), showYAxes = shouldDescribeColl('yAxis', hasCartesian && hasNoMap), desc = {};
if (showXAxes) {
desc.xAxis = this.getAxisDescriptionText('xAxis');
}
if (showYAxes) {
desc.yAxis = this.getAxisDescriptionText('yAxis');
}
return desc;
},
/**
* @private
* @param {string} collectionKey
* @return {string}
*/
getAxisDescriptionText: function (collectionKey) {
var component = this, chart = this.chart, axes = chart[collectionKey];
return chart.langFormat('accessibility.axis.' + collectionKey + 'Description' + (axes.length > 1 ? 'Plural' : 'Singular'), {
chart: chart,
names: axes.map(function (axis) {
return getAxisDescription(axis);
}),
ranges: axes.map(function (axis) {
return component.getAxisRangeDescription(axis);
}),
numAxes: axes.length
});
},
/**
* Return string with text description of the axis range.
* @private
* @param {Highcharts.Axis} axis The axis to get range desc of.
* @return {string} A string with the range description for the axis.
*/
getAxisRangeDescription: function (axis) {
var axisOptions = axis.options || {};
// Handle overridden range description
if (axisOptions.accessibility &&
typeof axisOptions.accessibility.rangeDescription !== 'undefined') {
return axisOptions.accessibility.rangeDescription;
}
// Handle category axes
if (axis.categories) {
return this.getCategoryAxisRangeDesc(axis);
}
// Use time range, not from-to?
if (axis.dateTime && (axis.min === 0 || axis.dataMin === 0)) {
return this.getAxisTimeLengthDesc(axis);
}
// Just use from and to.
// We have the range and the unit to use, find the desc format
return this.getAxisFromToDescription(axis);
},
/**
* @private
* @param {Highcharts.Axis} axis
* @return {string}
*/
getCategoryAxisRangeDesc: function (axis) {
var chart = this.chart;
if (axis.dataMax && axis.dataMin) {
return chart.langFormat('accessibility.axis.rangeCategories', {
chart: chart,
axis: axis,
numCategories: axis.dataMax - axis.dataMin + 1
});
}
return '';
},
/**
* @private
* @param {Highcharts.Axis} axis
* @return {string}
*/
getAxisTimeLengthDesc: function (axis) {
var chart = this.chart, range = {}, rangeUnit = 'Seconds';
range.Seconds = ((axis.max || 0) - (axis.min || 0)) / 1000;
range.Minutes = range.Seconds / 60;
range.Hours = range.Minutes / 60;
range.Days = range.Hours / 24;
['Minutes', 'Hours', 'Days'].forEach(function (unit) {
if (range[unit] > 2) {
rangeUnit = unit;
}
});
var rangeValue = range[rangeUnit].toFixed(rangeUnit !== 'Seconds' &&
rangeUnit !== 'Minutes' ? 1 : 0 // Use decimals for days/hours
);
// We have the range and the unit to use, find the desc format
return chart.langFormat('accessibility.axis.timeRange' + rangeUnit, {
chart: chart,
axis: axis,
range: rangeValue.replace('.0', '')
});
},
/**
* @private
* @param {Highcharts.Axis} axis
* @return {string}
*/
getAxisFromToDescription: function (axis) {
var chart = this.chart, dateRangeFormat = chart.options.accessibility
.screenReaderSection.axisRangeDateFormat, format = function (axisKey) {
return axis.dateTime ? chart.time.dateFormat(dateRangeFormat, axis[axisKey]) : axis[axisKey];
};
return chart.langFormat('accessibility.axis.rangeFromTo', {
chart: chart,
axis: axis,
rangeFrom: format('min'),
rangeTo: format('max')
});
},
/**
* Remove component traces
*/
destroy: function () {
var _a;
(_a = this.announcer) === null || _a === void 0 ? void 0 : _a.destroy();
}
});
export default InfoRegionsComponent;
@@ -0,0 +1,300 @@
/* *
*
* (c) 2009-2020 Øystein Moseng
*
* Accessibility component for chart legend.
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import H from '../../Core/Globals.js';
import Legend from '../../Core/Legend.js';
import U from '../../Core/Utilities.js';
var addEvent = U.addEvent, extend = U.extend, find = U.find, fireEvent = U.fireEvent;
import AccessibilityComponent from '../AccessibilityComponent.js';
import KeyboardNavigationHandler from '../KeyboardNavigationHandler.js';
import HTMLUtilities from '../Utils/HTMLUtilities.js';
var stripHTMLTags = HTMLUtilities.stripHTMLTagsFromString, removeElement = HTMLUtilities.removeElement;
/* eslint-disable no-invalid-this, valid-jsdoc */
/**
* @private
*/
function scrollLegendToItem(legend, itemIx) {
var itemPage = legend.allItems[itemIx].pageIx, curPage = legend.currentPage;
if (typeof itemPage !== 'undefined' && itemPage + 1 !== curPage) {
legend.scroll(1 + itemPage - curPage);
}
}
/**
* @private
*/
function shouldDoLegendA11y(chart) {
var items = chart.legend && chart.legend.allItems, legendA11yOptions = (chart.options.legend.accessibility || {});
return !!(items && items.length &&
!(chart.colorAxis && chart.colorAxis.length) &&
legendA11yOptions.enabled !== false);
}
/**
* Highlight legend item by index.
*
* @private
* @function Highcharts.Chart#highlightLegendItem
*
* @param {number} ix
*
* @return {boolean}
*/
H.Chart.prototype.highlightLegendItem = function (ix) {
var items = this.legend.allItems, oldIx = this.highlightedLegendItemIx;
if (items[ix]) {
if (items[oldIx]) {
fireEvent(items[oldIx].legendGroup.element, 'mouseout');
}
scrollLegendToItem(this.legend, ix);
this.setFocusToElement(items[ix].legendItem, items[ix].a11yProxyElement);
fireEvent(items[ix].legendGroup.element, 'mouseover');
return true;
}
return false;
};
// Keep track of pressed state for legend items
addEvent(Legend, 'afterColorizeItem', function (e) {
var chart = this.chart, a11yOptions = chart.options.accessibility, legendItem = e.item;
if (a11yOptions.enabled && legendItem && legendItem.a11yProxyElement) {
legendItem.a11yProxyElement.setAttribute('aria-pressed', e.visible ? 'false' : 'true');
}
});
/**
* The LegendComponent class
*
* @private
* @class
* @name Highcharts.LegendComponent
*/
var LegendComponent = function () { };
LegendComponent.prototype = new AccessibilityComponent();
extend(LegendComponent.prototype, /** @lends Highcharts.LegendComponent */ {
/**
* Init the component
* @private
*/
init: function () {
var component = this;
this.proxyElementsList = [];
this.recreateProxies();
// Note: Chart could create legend dynamically, so events can not be
// tied to the component's chart's current legend.
this.addEvent(Legend, 'afterScroll', function () {
if (this.chart === component.chart) {
component.updateProxiesPositions();
component.updateLegendItemProxyVisibility();
this.chart.highlightLegendItem(component.highlightedLegendItemIx);
}
});
this.addEvent(Legend, 'afterPositionItem', function (e) {
if (this.chart === component.chart && this.chart.renderer) {
component.updateProxyPositionForItem(e.item);
}
});
},
/**
* @private
*/
updateLegendItemProxyVisibility: function () {
var legend = this.chart.legend, items = legend.allItems || [], curPage = legend.currentPage || 1, clipHeight = legend.clipHeight || 0;
items.forEach(function (item) {
var itemPage = item.pageIx || 0, y = item._legendItemPos ? item._legendItemPos[1] : 0, h = item.legendItem ? Math.round(item.legendItem.getBBox().height) : 0, hide = y + h - legend.pages[itemPage] > clipHeight || itemPage !== curPage - 1;
if (item.a11yProxyElement) {
item.a11yProxyElement.style.visibility = hide ?
'hidden' : 'visible';
}
});
},
/**
* The legend needs updates on every render, in order to update positioning
* of the proxy overlays.
*/
onChartRender: function () {
if (shouldDoLegendA11y(this.chart)) {
this.updateProxiesPositions();
}
else {
this.removeProxies();
}
},
/**
* @private
*/
updateProxiesPositions: function () {
for (var _i = 0, _a = this.proxyElementsList; _i < _a.length; _i++) {
var _b = _a[_i], element = _b.element, posElement = _b.posElement;
this.updateProxyButtonPosition(element, posElement);
}
},
/**
* @private
*/
updateProxyPositionForItem: function (item) {
var proxyRef = find(this.proxyElementsList, function (ref) { return ref.item === item; });
if (proxyRef) {
this.updateProxyButtonPosition(proxyRef.element, proxyRef.posElement);
}
},
/**
* @private
*/
recreateProxies: function () {
this.removeProxies();
if (shouldDoLegendA11y(this.chart)) {
this.addLegendProxyGroup();
this.proxyLegendItems();
this.updateLegendItemProxyVisibility();
}
},
/**
* @private
*/
removeProxies: function () {
removeElement(this.legendProxyGroup);
this.proxyElementsList = [];
},
/**
* @private
*/
addLegendProxyGroup: function () {
var a11yOptions = this.chart.options.accessibility, groupLabel = this.chart.langFormat('accessibility.legend.legendLabel', {}), groupRole = a11yOptions.landmarkVerbosity === 'all' ?
'region' : null;
this.legendProxyGroup = this.addProxyGroup({
'aria-label': groupLabel,
'role': groupRole
});
},
/**
* @private
*/
proxyLegendItems: function () {
var component = this, items = (this.chart.legend &&
this.chart.legend.allItems || []);
items.forEach(function (item) {
if (item.legendItem && item.legendItem.element) {
component.proxyLegendItem(item);
}
});
},
/**
* @private
* @param {Highcharts.BubbleLegend|Point|Highcharts.Series} item
*/
proxyLegendItem: function (item) {
if (!item.legendItem || !item.legendGroup) {
return;
}
var itemLabel = this.chart.langFormat('accessibility.legend.legendItem', {
chart: this.chart,
itemName: stripHTMLTags(item.name)
}), attribs = {
tabindex: -1,
'aria-pressed': !item.visible,
'aria-label': itemLabel
},
// Considers useHTML
proxyPositioningElement = item.legendGroup.div ?
item.legendItem : item.legendGroup;
item.a11yProxyElement = this.createProxyButton(item.legendItem, this.legendProxyGroup, attribs, proxyPositioningElement);
this.proxyElementsList.push({
item: item,
element: item.a11yProxyElement,
posElement: proxyPositioningElement
});
},
/**
* Get keyboard navigation handler for this component.
* @return {Highcharts.KeyboardNavigationHandler}
*/
getKeyboardNavigation: function () {
var keys = this.keyCodes, component = this, chart = this.chart;
return new KeyboardNavigationHandler(chart, {
keyCodeMap: [
[
[keys.left, keys.right, keys.up, keys.down],
function (keyCode) {
return component.onKbdArrowKey(this, keyCode);
}
],
[
[keys.enter, keys.space],
function () {
return component.onKbdClick(this);
}
]
],
validate: function () {
return component.shouldHaveLegendNavigation();
},
init: function (direction) {
return component.onKbdNavigationInit(direction);
}
});
},
/**
* @private
* @param {Highcharts.KeyboardNavigationHandler} keyboardNavigationHandler
* @param {number} keyCode
* @return {number}
* Response code
*/
onKbdArrowKey: function (keyboardNavigationHandler, keyCode) {
var keys = this.keyCodes, response = keyboardNavigationHandler.response, chart = this.chart, a11yOptions = chart.options.accessibility, numItems = chart.legend.allItems.length, direction = (keyCode === keys.left || keyCode === keys.up) ? -1 : 1;
var res = chart.highlightLegendItem(this.highlightedLegendItemIx + direction);
if (res) {
this.highlightedLegendItemIx += direction;
return response.success;
}
if (numItems > 1 &&
a11yOptions.keyboardNavigation.wrapAround) {
keyboardNavigationHandler.init(direction);
return response.success;
}
// No wrap, move
return response[direction > 0 ? 'next' : 'prev'];
},
/**
* @private
* @param {Highcharts.KeyboardNavigationHandler} keyboardNavigationHandler
* @return {number}
* Response code
*/
onKbdClick: function (keyboardNavigationHandler) {
var legendItem = this.chart.legend.allItems[this.highlightedLegendItemIx];
if (legendItem && legendItem.a11yProxyElement) {
fireEvent(legendItem.a11yProxyElement, 'click');
}
return keyboardNavigationHandler.response.success;
},
/**
* @private
* @return {boolean|undefined}
*/
shouldHaveLegendNavigation: function () {
var chart = this.chart, legendOptions = chart.options.legend || {}, hasLegend = chart.legend && chart.legend.allItems, hasColorAxis = chart.colorAxis && chart.colorAxis.length, legendA11yOptions = (legendOptions.accessibility || {});
return !!(hasLegend &&
chart.legend.display &&
!hasColorAxis &&
legendA11yOptions.enabled &&
legendA11yOptions.keyboardNavigation &&
legendA11yOptions.keyboardNavigation.enabled);
},
/**
* @private
* @param {number} direction
*/
onKbdNavigationInit: function (direction) {
var chart = this.chart, lastIx = chart.legend.allItems.length - 1, ixToHighlight = direction > 0 ? 0 : lastIx;
chart.highlightLegendItem(ixToHighlight);
this.highlightedLegendItemIx = ixToHighlight;
}
});
export default LegendComponent;
@@ -0,0 +1,349 @@
/* *
*
* (c) 2009-2020 Øystein Moseng
*
* Accessibility component for exporting menu.
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import H from '../../Core/Globals.js';
import U from '../../Core/Utilities.js';
var extend = U.extend;
import AccessibilityComponent from '../AccessibilityComponent.js';
import KeyboardNavigationHandler from '../KeyboardNavigationHandler.js';
import ChartUtilities from '../Utils/ChartUtilities.js';
var unhideChartElementFromAT = ChartUtilities.unhideChartElementFromAT;
import HTMLUtilities from '../Utils/HTMLUtilities.js';
var removeElement = HTMLUtilities.removeElement, getFakeMouseEvent = HTMLUtilities.getFakeMouseEvent;
/* eslint-disable no-invalid-this, valid-jsdoc */
/**
* Get the wrapped export button element of a chart.
*
* @private
* @param {Highcharts.Chart} chart
* @returns {Highcharts.SVGElement}
*/
function getExportMenuButtonElement(chart) {
return chart.exportSVGElements && chart.exportSVGElements[0];
}
/**
* Show the export menu and focus the first item (if exists).
*
* @private
* @function Highcharts.Chart#showExportMenu
*/
H.Chart.prototype.showExportMenu = function () {
var exportButton = getExportMenuButtonElement(this);
if (exportButton) {
var el = exportButton.element;
if (el.onclick) {
el.onclick(getFakeMouseEvent('click'));
}
}
};
/**
* @private
* @function Highcharts.Chart#hideExportMenu
*/
H.Chart.prototype.hideExportMenu = function () {
var chart = this, exportList = chart.exportDivElements;
if (exportList && chart.exportContextMenu) {
// Reset hover states etc.
exportList.forEach(function (el) {
if (el.className === 'highcharts-menu-item' && el.onmouseout) {
el.onmouseout(getFakeMouseEvent('mouseout'));
}
});
chart.highlightedExportItemIx = 0;
// Hide the menu div
chart.exportContextMenu.hideMenu();
// Make sure the chart has focus and can capture keyboard events
chart.container.focus();
}
};
/**
* Highlight export menu item by index.
*
* @private
* @function Highcharts.Chart#highlightExportItem
*
* @param {number} ix
*
* @return {boolean}
*/
H.Chart.prototype.highlightExportItem = function (ix) {
var listItem = this.exportDivElements && this.exportDivElements[ix], curHighlighted = this.exportDivElements &&
this.exportDivElements[this.highlightedExportItemIx], hasSVGFocusSupport;
if (listItem &&
listItem.tagName === 'LI' &&
!(listItem.children && listItem.children.length)) {
// Test if we have focus support for SVG elements
hasSVGFocusSupport = !!(this.renderTo.getElementsByTagName('g')[0] || {}).focus;
// Only focus if we can set focus back to the elements after
// destroying the menu (#7422)
if (listItem.focus && hasSVGFocusSupport) {
listItem.focus();
}
if (curHighlighted && curHighlighted.onmouseout) {
curHighlighted.onmouseout(getFakeMouseEvent('mouseout'));
}
if (listItem.onmouseover) {
listItem.onmouseover(getFakeMouseEvent('mouseover'));
}
this.highlightedExportItemIx = ix;
return true;
}
return false;
};
/**
* Try to highlight the last valid export menu item.
*
* @private
* @function Highcharts.Chart#highlightLastExportItem
* @return {boolean}
*/
H.Chart.prototype.highlightLastExportItem = function () {
var chart = this, i;
if (chart.exportDivElements) {
i = chart.exportDivElements.length;
while (i--) {
if (chart.highlightExportItem(i)) {
return true;
}
}
}
return false;
};
/**
* @private
* @param {Highcharts.Chart} chart
*/
function exportingShouldHaveA11y(chart) {
var exportingOpts = chart.options.exporting, exportButton = getExportMenuButtonElement(chart);
return !!(exportingOpts &&
exportingOpts.enabled !== false &&
exportingOpts.accessibility &&
exportingOpts.accessibility.enabled &&
exportButton &&
exportButton.element);
}
/**
* The MenuComponent class
*
* @private
* @class
* @name Highcharts.MenuComponent
*/
var MenuComponent = function () { };
MenuComponent.prototype = new AccessibilityComponent();
extend(MenuComponent.prototype, /** @lends Highcharts.MenuComponent */ {
/**
* Init the component
*/
init: function () {
var chart = this.chart, component = this;
this.addEvent(chart, 'exportMenuShown', function () {
component.onMenuShown();
});
this.addEvent(chart, 'exportMenuHidden', function () {
component.onMenuHidden();
});
},
/**
* @private
*/
onMenuHidden: function () {
var menu = this.chart.exportContextMenu;
if (menu) {
menu.setAttribute('aria-hidden', 'true');
}
this.isExportMenuShown = false;
this.setExportButtonExpandedState('false');
},
/**
* @private
*/
onMenuShown: function () {
var chart = this.chart, menu = chart.exportContextMenu;
if (menu) {
this.addAccessibleContextMenuAttribs();
unhideChartElementFromAT(chart, menu);
}
this.isExportMenuShown = true;
this.setExportButtonExpandedState('true');
},
/**
* @private
* @param {string} stateStr
*/
setExportButtonExpandedState: function (stateStr) {
var button = this.exportButtonProxy;
if (button) {
button.setAttribute('aria-expanded', stateStr);
}
},
/**
* Called on each render of the chart. We need to update positioning of the
* proxy overlay.
*/
onChartRender: function () {
var chart = this.chart, a11yOptions = chart.options.accessibility;
// Always start with a clean slate
removeElement(this.exportProxyGroup);
// Set screen reader properties on export menu
if (exportingShouldHaveA11y(chart)) {
// Proxy button and group
this.exportProxyGroup = this.addProxyGroup(
// Wrap in a region div if verbosity is high
a11yOptions.landmarkVerbosity === 'all' ? {
'aria-label': chart.langFormat('accessibility.exporting.exportRegionLabel', { chart: chart }),
'role': 'region'
} : {});
var button = getExportMenuButtonElement(this.chart);
this.exportButtonProxy = this.createProxyButton(button, this.exportProxyGroup, {
'aria-label': chart.langFormat('accessibility.exporting.menuButtonLabel', { chart: chart }),
'aria-expanded': 'false'
});
}
},
/**
* @private
*/
addAccessibleContextMenuAttribs: function () {
var chart = this.chart, exportList = chart.exportDivElements;
if (exportList && exportList.length) {
// 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 === 'LI' &&
!(item.children && item.children.length)) {
item.setAttribute('tabindex', -1);
}
else {
item.setAttribute('aria-hidden', 'true');
}
});
// Set accessibility properties on parent div
var parentDiv = exportList[0].parentNode;
parentDiv.removeAttribute('aria-hidden');
parentDiv.setAttribute('aria-label', chart.langFormat('accessibility.exporting.chartMenuLabel', { chart: chart }));
}
},
/**
* Get keyboard navigation handler for this component.
* @return {Highcharts.KeyboardNavigationHandler}
*/
getKeyboardNavigation: function () {
var keys = this.keyCodes, chart = this.chart, component = this;
return new KeyboardNavigationHandler(chart, {
keyCodeMap: [
// Arrow prev handler
[
[keys.left, keys.up],
function () {
return component.onKbdPrevious(this);
}
],
// Arrow next handler
[
[keys.right, keys.down],
function () {
return component.onKbdNext(this);
}
],
// Click handler
[
[keys.enter, keys.space],
function () {
return component.onKbdClick(this);
}
]
],
// Only run exporting navigation if exporting support exists and is
// enabled on chart
validate: function () {
return chart.exportChart &&
chart.options.exporting.enabled !== false &&
chart.options.exporting.accessibility.enabled !==
false;
},
// Focus export menu button
init: function () {
var exportBtn = component.exportButtonProxy, exportGroup = chart.exportingGroup;
if (exportGroup && exportBtn) {
chart.setFocusToElement(exportGroup, exportBtn);
}
},
// Hide the menu
terminate: function () {
chart.hideExportMenu();
}
});
},
/**
* @private
* @param {Highcharts.KeyboardNavigationHandler} keyboardNavigationHandler
* @return {number}
* Response code
*/
onKbdPrevious: function (keyboardNavigationHandler) {
var chart = this.chart, a11yOptions = chart.options.accessibility, response = keyboardNavigationHandler.response, i = chart.highlightedExportItemIx || 0;
// Try to highlight prev item in list. Highlighting e.g.
// separators will fail.
while (i--) {
if (chart.highlightExportItem(i)) {
return response.success;
}
}
// We failed, so wrap around or move to prev module
if (a11yOptions.keyboardNavigation.wrapAround) {
chart.highlightLastExportItem();
return response.success;
}
return response.prev;
},
/**
* @private
* @param {Highcharts.KeyboardNavigationHandler} keyboardNavigationHandler
* @return {number}
* Response code
*/
onKbdNext: function (keyboardNavigationHandler) {
var chart = this.chart, a11yOptions = chart.options.accessibility, response = keyboardNavigationHandler.response, i = (chart.highlightedExportItemIx || 0) + 1;
// Try to highlight next item in list. Highlighting e.g.
// separators will fail.
for (; i < chart.exportDivElements.length; ++i) {
if (chart.highlightExportItem(i)) {
return response.success;
}
}
// We failed, so wrap around or move to next module
if (a11yOptions.keyboardNavigation.wrapAround) {
chart.highlightExportItem(0);
return response.success;
}
return response.next;
},
/**
* @private
* @param {Highcharts.KeyboardNavigationHandler} keyboardNavigationHandler
* @return {number}
* Response code
*/
onKbdClick: function (keyboardNavigationHandler) {
var chart = this.chart, curHighlightedItem = chart.exportDivElements[chart.highlightedExportItemIx], exportButtonElement = getExportMenuButtonElement(chart).element;
if (this.isExportMenuShown) {
this.fakeClickEvent(curHighlightedItem);
}
else {
this.fakeClickEvent(exportButtonElement);
chart.highlightExportItem(0);
}
return keyboardNavigationHandler.response.success;
}
});
export default MenuComponent;
@@ -0,0 +1,266 @@
/* *
*
* (c) 2009-2020 Øystein Moseng
*
* Accessibility component for the range selector.
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import H from '../../Core/Globals.js';
import U from '../../Core/Utilities.js';
var extend = U.extend;
import AccessibilityComponent from '../AccessibilityComponent.js';
import KeyboardNavigationHandler from '../KeyboardNavigationHandler.js';
import ChartUtilities from '../Utils/ChartUtilities.js';
var unhideChartElementFromAT = ChartUtilities.unhideChartElementFromAT;
import HTMLUtilities from '../Utils/HTMLUtilities.js';
var setElAttrs = HTMLUtilities.setElAttrs;
/* eslint-disable no-invalid-this, valid-jsdoc */
/**
* @private
*/
function shouldRunInputNavigation(chart) {
var inputVisible = (chart.rangeSelector &&
chart.rangeSelector.inputGroup &&
chart.rangeSelector.inputGroup.element
.getAttribute('visibility') !== 'hidden');
return (inputVisible &&
chart.options.rangeSelector.inputEnabled !== false &&
chart.rangeSelector.minInput &&
chart.rangeSelector.maxInput);
}
/**
* Highlight range selector button by index.
*
* @private
* @function Highcharts.Chart#highlightRangeSelectorButton
*
* @param {number} ix
*
* @return {boolean}
*/
H.Chart.prototype.highlightRangeSelectorButton = function (ix) {
var buttons = this.rangeSelector.buttons, curSelectedIx = this.highlightedRangeSelectorItemIx;
// Deselect old
if (typeof curSelectedIx !== 'undefined' && buttons[curSelectedIx]) {
buttons[curSelectedIx].setState(this.oldRangeSelectorItemState || 0);
}
// Select new
this.highlightedRangeSelectorItemIx = ix;
if (buttons[ix]) {
this.setFocusToElement(buttons[ix].box, buttons[ix].element);
this.oldRangeSelectorItemState = buttons[ix].state;
buttons[ix].setState(2);
return true;
}
return false;
};
/**
* The RangeSelectorComponent class
*
* @private
* @class
* @name Highcharts.RangeSelectorComponent
*/
var RangeSelectorComponent = function () { };
RangeSelectorComponent.prototype = new AccessibilityComponent();
extend(RangeSelectorComponent.prototype, /** @lends Highcharts.RangeSelectorComponent */ {
/**
* Called on first render/updates to the chart, including options changes.
*/
onChartUpdate: function () {
var chart = this.chart, component = this, rangeSelector = chart.rangeSelector;
if (!rangeSelector) {
return;
}
if (rangeSelector.buttons && rangeSelector.buttons.length) {
rangeSelector.buttons.forEach(function (button) {
unhideChartElementFromAT(chart, button.element);
component.setRangeButtonAttrs(button);
});
}
// Make sure input boxes are accessible and focusable
if (rangeSelector.maxInput && rangeSelector.minInput) {
['minInput', 'maxInput'].forEach(function (key, i) {
var input = rangeSelector[key];
if (input) {
unhideChartElementFromAT(chart, input);
component.setRangeInputAttrs(input, 'accessibility.rangeSelector.' + (i ? 'max' : 'min') +
'InputLabel');
}
});
}
},
/**
* @private
* @param {Highcharts.SVGElement} button
*/
setRangeButtonAttrs: function (button) {
var chart = this.chart, label = chart.langFormat('accessibility.rangeSelector.buttonText', {
chart: chart,
buttonText: button.text && button.text.textStr
});
setElAttrs(button.element, {
tabindex: -1,
role: 'button',
'aria-label': label
});
},
/**
* @private
*/
setRangeInputAttrs: function (input, langKey) {
var chart = this.chart;
setElAttrs(input, {
tabindex: -1,
role: 'textbox',
'aria-label': chart.langFormat(langKey, { chart: chart })
});
},
/**
* Get navigation for the range selector buttons.
* @private
* @return {Highcharts.KeyboardNavigationHandler} The module object.
*/
getRangeSelectorButtonNavigation: function () {
var chart = this.chart, keys = this.keyCodes, component = this;
return new KeyboardNavigationHandler(chart, {
keyCodeMap: [
[
[keys.left, keys.right, keys.up, keys.down],
function (keyCode) {
return component.onButtonNavKbdArrowKey(this, keyCode);
}
],
[
[keys.enter, keys.space],
function () {
return component.onButtonNavKbdClick(this);
}
]
],
validate: function () {
var hasRangeSelector = chart.rangeSelector &&
chart.rangeSelector.buttons &&
chart.rangeSelector.buttons.length;
return hasRangeSelector;
},
init: function (direction) {
var lastButtonIx = (chart.rangeSelector.buttons.length - 1);
chart.highlightRangeSelectorButton(direction > 0 ? 0 : lastButtonIx);
}
});
},
/**
* @private
* @param {Highcharts.KeyboardNavigationHandler} keyboardNavigationHandler
* @param {number} keyCode
* @return {number} Response code
*/
onButtonNavKbdArrowKey: function (keyboardNavigationHandler, keyCode) {
var response = keyboardNavigationHandler.response, keys = this.keyCodes, chart = this.chart, wrapAround = chart.options.accessibility
.keyboardNavigation.wrapAround, direction = (keyCode === keys.left || keyCode === keys.up) ? -1 : 1, didHighlight = chart.highlightRangeSelectorButton(chart.highlightedRangeSelectorItemIx + direction);
if (!didHighlight) {
if (wrapAround) {
keyboardNavigationHandler.init(direction);
return response.success;
}
return response[direction > 0 ? 'next' : 'prev'];
}
return response.success;
},
/**
* @private
*/
onButtonNavKbdClick: function (keyboardNavigationHandler) {
var response = keyboardNavigationHandler.response, chart = this.chart, wasDisabled = chart.oldRangeSelectorItemState === 3;
if (!wasDisabled) {
this.fakeClickEvent(chart.rangeSelector.buttons[chart.highlightedRangeSelectorItemIx].element);
}
return response.success;
},
/**
* Get navigation for the range selector input boxes.
* @private
* @return {Highcharts.KeyboardNavigationHandler}
* The module object.
*/
getRangeSelectorInputNavigation: function () {
var chart = this.chart, keys = this.keyCodes, component = this;
return new KeyboardNavigationHandler(chart, {
keyCodeMap: [
[
[
keys.tab, keys.up, keys.down
],
function (keyCode, e) {
var direction = (keyCode === keys.tab && e.shiftKey ||
keyCode === keys.up) ? -1 : 1;
return component.onInputKbdMove(this, direction);
}
]
],
validate: function () {
return shouldRunInputNavigation(chart);
},
init: function (direction) {
component.onInputNavInit(direction);
},
terminate: function () {
component.onInputNavTerminate();
}
});
},
/**
* @private
* @param {Highcharts.KeyboardNavigationHandler} keyboardNavigationHandler
* @param {number} direction
* @return {number} Response code
*/
onInputKbdMove: function (keyboardNavigationHandler, direction) {
var chart = this.chart, response = keyboardNavigationHandler.response, newIx = chart.highlightedInputRangeIx =
chart.highlightedInputRangeIx + direction, newIxOutOfRange = newIx > 1 || newIx < 0;
if (newIxOutOfRange) {
return response[direction > 0 ? 'next' : 'prev'];
}
chart.rangeSelector[newIx ? 'maxInput' : 'minInput'].focus();
return response.success;
},
/**
* @private
* @param {number} direction
*/
onInputNavInit: function (direction) {
var chart = this.chart, buttonIxToHighlight = direction > 0 ? 0 : 1;
chart.highlightedInputRangeIx = buttonIxToHighlight;
chart.rangeSelector[buttonIxToHighlight ? 'maxInput' : 'minInput'].focus();
},
/**
* @private
*/
onInputNavTerminate: function () {
var rangeSel = (this.chart.rangeSelector || {});
if (rangeSel.maxInput) {
rangeSel.hideInput('max');
}
if (rangeSel.minInput) {
rangeSel.hideInput('min');
}
},
/**
* Get keyboard navigation handlers for this component.
* @return {Array<Highcharts.KeyboardNavigationHandler>}
* List of module objects.
*/
getKeyboardNavigation: function () {
return [
this.getRangeSelectorButtonNavigation(),
this.getRangeSelectorInputNavigation()
];
}
});
export default RangeSelectorComponent;
@@ -0,0 +1,171 @@
/* *
*
* (c) 2009-2020 Øystein Moseng
*
* Handle forcing series markers.
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import H from '../../../Core/Globals.js';
import U from '../../../Core/Utilities.js';
var addEvent = U.addEvent, merge = U.merge;
/* eslint-disable no-invalid-this, valid-jsdoc */
/**
* @private
*/
function isWithinDescriptionThreshold(series) {
var a11yOptions = series.chart.options.accessibility;
return series.points.length <
a11yOptions.series.pointDescriptionEnabledThreshold ||
a11yOptions.series.pointDescriptionEnabledThreshold === false;
}
/**
* @private
*/
function shouldForceMarkers(series) {
var chart = series.chart, chartA11yEnabled = chart.options.accessibility.enabled, seriesA11yEnabled = (series.options.accessibility &&
series.options.accessibility.enabled) !== false;
return chartA11yEnabled && seriesA11yEnabled && isWithinDescriptionThreshold(series);
}
/**
* @private
*/
function hasIndividualPointMarkerOptions(series) {
return !!(series._hasPointMarkers && series.points && series.points.length);
}
/**
* @private
*/
function unforceSeriesMarkerOptions(series) {
var resetMarkerOptions = series.resetA11yMarkerOptions;
if (resetMarkerOptions) {
merge(true, series.options, {
marker: {
enabled: resetMarkerOptions.enabled,
states: {
normal: {
opacity: resetMarkerOptions.states &&
resetMarkerOptions.states.normal &&
resetMarkerOptions.states.normal.opacity
}
}
}
});
}
}
/**
* @private
*/
function forceZeroOpacityMarkerOptions(options) {
merge(true, options, {
marker: {
enabled: true,
states: {
normal: {
opacity: 0
}
}
}
});
}
/**
* @private
*/
function getPointMarkerOpacity(pointOptions) {
return pointOptions.marker.states &&
pointOptions.marker.states.normal &&
pointOptions.marker.states.normal.opacity || 1;
}
/**
* @private
*/
function unforcePointMarkerOptions(pointOptions) {
merge(true, pointOptions.marker, {
states: {
normal: {
opacity: getPointMarkerOpacity(pointOptions)
}
}
});
}
/**
* @private
*/
function handleForcePointMarkers(series) {
var i = series.points.length;
while (i--) {
var point = series.points[i];
var pointOptions = point.options;
delete point.hasForcedA11yMarker;
if (pointOptions.marker) {
if (pointOptions.marker.enabled) {
unforcePointMarkerOptions(pointOptions);
point.hasForcedA11yMarker = false;
}
else {
forceZeroOpacityMarkerOptions(pointOptions);
point.hasForcedA11yMarker = true;
}
}
}
}
/**
* @private
*/
function addForceMarkersEvents() {
/**
* Keep track of forcing markers.
* @private
*/
addEvent(H.Series, 'render', function () {
var series = this, options = series.options;
if (shouldForceMarkers(series)) {
if (options.marker && options.marker.enabled === false) {
series.a11yMarkersForced = true;
forceZeroOpacityMarkerOptions(series.options);
}
if (hasIndividualPointMarkerOptions(series)) {
handleForcePointMarkers(series);
}
}
else if (series.a11yMarkersForced) {
delete series.a11yMarkersForced;
unforceSeriesMarkerOptions(series);
}
});
/**
* Keep track of options to reset markers to if no longer forced.
* @private
*/
addEvent(H.Series, 'afterSetOptions', function (e) {
this.resetA11yMarkerOptions = merge(e.options.marker || {}, this.userOptions.marker || {});
});
/**
* Process marker graphics after render
* @private
*/
addEvent(H.Series, 'afterRender', function () {
var series = this;
// For styled mode the rendered graphic does not reflect the style
// options, and we need to add/remove classes to achieve the same.
if (series.chart.styledMode) {
if (series.markerGroup) {
series.markerGroup[series.a11yMarkersForced ? 'addClass' : 'removeClass']('highcharts-a11y-markers-hidden');
}
// Do we need to handle individual points?
if (hasIndividualPointMarkerOptions(series)) {
series.points.forEach(function (point) {
if (point.graphic) {
point.graphic[point.hasForcedA11yMarker ? 'addClass' : 'removeClass']('highcharts-a11y-marker-hidden');
point.graphic[point.hasForcedA11yMarker === false ? 'addClass' : 'removeClass']('highcharts-a11y-marker-visible');
}
});
}
}
});
}
export default addForceMarkersEvents;
@@ -0,0 +1,254 @@
/* *
*
* (c) 2009-2020 Øystein Moseng
*
* Handle announcing new data for a chart.
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import H from '../../../Core/Globals.js';
import U from '../../../Core/Utilities.js';
var extend = U.extend, defined = U.defined;
import ChartUtilities from '../../Utils/ChartUtilities.js';
var getChartTitle = ChartUtilities.getChartTitle;
import SeriesDescriber from './SeriesDescriber.js';
var defaultPointDescriptionFormatter = SeriesDescriber
.defaultPointDescriptionFormatter, defaultSeriesDescriptionFormatter = SeriesDescriber
.defaultSeriesDescriptionFormatter;
import Announcer from '../../Utils/Announcer.js';
import EventProvider from '../../Utils/EventProvider.js';
/* eslint-disable no-invalid-this, valid-jsdoc */
/**
* @private
*/
function chartHasAnnounceEnabled(chart) {
return !!chart.options.accessibility.announceNewData.enabled;
}
/**
* @private
*/
function findPointInDataArray(point) {
var candidates = point.series.data.filter(function (candidate) {
return point.x === candidate.x && point.y === candidate.y;
});
return candidates.length === 1 ? candidates[0] : point;
}
/**
* Get array of unique series from two arrays
* @private
*/
function getUniqueSeries(arrayA, arrayB) {
var uniqueSeries = (arrayA || []).concat(arrayB || [])
.reduce(function (acc, cur) {
acc[cur.name + cur.index] = cur;
return acc;
}, {});
return Object.keys(uniqueSeries).map(function (ix) {
return uniqueSeries[ix];
});
}
/**
* @private
* @class
*/
var NewDataAnnouncer = function (chart) {
this.chart = chart;
};
extend(NewDataAnnouncer.prototype, {
/**
* Initialize the new data announcer.
* @private
*/
init: function () {
var chart = this.chart;
var announceOptions = chart.options.accessibility.announceNewData;
var announceType = announceOptions.interruptUser ? 'assertive' : 'polite';
this.lastAnnouncementTime = 0;
this.dirty = {
allSeries: {}
};
this.eventProvider = new EventProvider();
this.announcer = new Announcer(chart, announceType);
this.addEventListeners();
},
/**
* Remove traces of announcer.
* @private
*/
destroy: function () {
this.eventProvider.removeAddedEvents();
this.announcer.destroy();
},
/**
* Add event listeners for the announcer
* @private
*/
addEventListeners: function () {
var announcer = this, chart = this.chart, e = this.eventProvider;
e.addEvent(chart, 'afterDrilldown', function () {
announcer.lastAnnouncementTime = 0;
});
e.addEvent(H.Series, 'updatedData', function () {
announcer.onSeriesUpdatedData(this);
});
e.addEvent(chart, 'afterAddSeries', function (e) {
announcer.onSeriesAdded(e.series);
});
e.addEvent(H.Series, 'addPoint', function (e) {
announcer.onPointAdded(e.point);
});
e.addEvent(chart, 'redraw', function () {
announcer.announceDirtyData();
});
},
/**
* On new data in the series, make sure we add it to the dirty list.
* @private
* @param {Highcharts.Series} series
*/
onSeriesUpdatedData: function (series) {
var chart = this.chart;
if (series.chart === chart && chartHasAnnounceEnabled(chart)) {
this.dirty.hasDirty = true;
this.dirty.allSeries[series.name + series.index] = series;
}
},
/**
* On new data series added, update dirty list.
* @private
* @param {Highcharts.Series} series
*/
onSeriesAdded: function (series) {
if (chartHasAnnounceEnabled(this.chart)) {
this.dirty.hasDirty = true;
this.dirty.allSeries[series.name + series.index] = series;
// Add it to newSeries storage unless we already have one
this.dirty.newSeries = defined(this.dirty.newSeries) ?
void 0 : series;
}
},
/**
* On new point added, update dirty list.
* @private
* @param {Highcharts.Point} point
*/
onPointAdded: function (point) {
var chart = point.series.chart;
if (this.chart === chart && chartHasAnnounceEnabled(chart)) {
// Add it to newPoint storage unless we already have one
this.dirty.newPoint = defined(this.dirty.newPoint) ?
void 0 : point;
}
},
/**
* Gather what we know and announce the data to user.
* @private
*/
announceDirtyData: function () {
var chart = this.chart, announcer = this;
if (chart.options.accessibility.announceNewData &&
this.dirty.hasDirty) {
var newPoint = this.dirty.newPoint;
// If we have a single new point, see if we can find it in the
// data array. Otherwise we can only pass through options to
// the description builder, and it is a bit sparse in info.
if (newPoint) {
newPoint = findPointInDataArray(newPoint);
}
this.queueAnnouncement(Object.keys(this.dirty.allSeries).map(function (ix) {
return announcer.dirty.allSeries[ix];
}), this.dirty.newSeries, newPoint);
// Reset
this.dirty = {
allSeries: {}
};
}
},
/**
* Announce to user that there is new data.
* @private
* @param {Array<Highcharts.Series>} dirtySeries
* Array of series with new data.
* @param {Highcharts.Series} [newSeries]
* If a single new series was added, a reference to this series.
* @param {Highcharts.Point} [newPoint]
* If a single point was added, a reference to this point.
*/
queueAnnouncement: function (dirtySeries, newSeries, newPoint) {
var _this = this;
var chart = this.chart;
var annOptions = chart.options.accessibility.announceNewData;
if (annOptions.enabled) {
var now = +new Date();
var dTime = now - this.lastAnnouncementTime;
var time = Math.max(0, annOptions.minAnnounceInterval - dTime);
// Add series from previously queued announcement.
var allSeries = getUniqueSeries(this.queuedAnnouncement && this.queuedAnnouncement.series, dirtySeries);
// Build message and announce
var message = this.buildAnnouncementMessage(allSeries, newSeries, newPoint);
if (message) {
// Is there already one queued?
if (this.queuedAnnouncement) {
clearTimeout(this.queuedAnnouncementTimer);
}
// Build the announcement
this.queuedAnnouncement = {
time: now,
message: message,
series: allSeries
};
// Queue the announcement
this.queuedAnnouncementTimer = setTimeout(function () {
if (_this && _this.announcer) {
_this.lastAnnouncementTime = +new Date();
_this.announcer.announce(_this.queuedAnnouncement.message);
delete _this.queuedAnnouncement;
delete _this.queuedAnnouncementTimer;
}
}, time);
}
}
},
/**
* Get announcement message for new data.
* @private
* @param {Array<Highcharts.Series>} dirtySeries
* Array of series with new data.
* @param {Highcharts.Series} [newSeries]
* If a single new series was added, a reference to this series.
* @param {Highcharts.Point} [newPoint]
* If a single point was added, a reference to this point.
*
* @return {string|null}
* The announcement message to give to user.
*/
buildAnnouncementMessage: function (dirtySeries, newSeries, newPoint) {
var chart = this.chart, annOptions = chart.options.accessibility.announceNewData;
// User supplied formatter?
if (annOptions.announcementFormatter) {
var formatterRes = annOptions.announcementFormatter(dirtySeries, newSeries, newPoint);
if (formatterRes !== false) {
return formatterRes.length ? formatterRes : null;
}
}
// Default formatter - use lang options
var multiple = H.charts && H.charts.length > 1 ? 'Multiple' : 'Single', langKey = newSeries ? 'newSeriesAnnounce' + multiple :
newPoint ? 'newPointAnnounce' + multiple : 'newDataAnnounce', chartTitle = getChartTitle(chart);
return chart.langFormat('accessibility.announceNewData.' + langKey, {
chartTitle: chartTitle,
seriesDesc: newSeries ?
defaultSeriesDescriptionFormatter(newSeries) :
null,
pointDesc: newPoint ?
defaultPointDescriptionFormatter(newPoint) :
null,
point: newPoint,
series: newSeries
});
}
});
export default NewDataAnnouncer;
@@ -0,0 +1,109 @@
/* *
*
* (c) 2009-2020 Øystein Moseng
*
* Accessibility component for series and points.
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import H from '../../../Core/Globals.js';
import U from '../../../Core/Utilities.js';
var extend = U.extend;
import AccessibilityComponent from '../../AccessibilityComponent.js';
import SeriesKeyboardNavigation from './SeriesKeyboardNavigation.js';
import NewDataAnnouncer from './NewDataAnnouncer.js';
import addForceMarkersEvents from './ForcedMarkers.js';
import ChartUtilities from '../../Utils/ChartUtilities.js';
var hideSeriesFromAT = ChartUtilities.hideSeriesFromAT;
import SeriesDescriber from './SeriesDescriber.js';
var describeSeries = SeriesDescriber.describeSeries;
import Tooltip from '../../../Core/Tooltip.js';
// Expose functionality to users
H.SeriesAccessibilityDescriber = SeriesDescriber;
// Handle forcing markers
addForceMarkersEvents();
/* eslint-disable no-invalid-this, valid-jsdoc */
/**
* The SeriesComponent class
*
* @private
* @class
* @name Highcharts.SeriesComponent
*/
var SeriesComponent = function () { };
SeriesComponent.prototype = new AccessibilityComponent();
extend(SeriesComponent.prototype, /** @lends Highcharts.SeriesComponent */ {
/**
* Init the component.
*/
init: function () {
this.newDataAnnouncer = new NewDataAnnouncer(this.chart);
this.newDataAnnouncer.init();
this.keyboardNavigation = new SeriesKeyboardNavigation(this.chart, this.keyCodes);
this.keyboardNavigation.init();
this.hideTooltipFromATWhenShown();
this.hideSeriesLabelsFromATWhenShown();
},
/**
* @private
*/
hideTooltipFromATWhenShown: function () {
var component = this;
this.addEvent(Tooltip, 'refresh', function () {
if (this.chart === component.chart &&
this.label &&
this.label.element) {
this.label.element.setAttribute('aria-hidden', true);
}
});
},
/**
* @private
*/
hideSeriesLabelsFromATWhenShown: function () {
this.addEvent(this.chart, 'afterDrawSeriesLabels', function () {
this.series.forEach(function (series) {
if (series.labelBySeries) {
series.labelBySeries.attr('aria-hidden', true);
}
});
});
},
/**
* Called on chart render. It is necessary to do this for render in case
* markers change on zoom/pixel density.
*/
onChartRender: function () {
var chart = this.chart;
chart.series.forEach(function (series) {
var shouldDescribeSeries = (series.options.accessibility &&
series.options.accessibility.enabled) !== false &&
series.visible;
if (shouldDescribeSeries) {
describeSeries(series);
}
else {
hideSeriesFromAT(series);
}
});
},
/**
* Get keyboard navigation handler for this component.
* @return {Highcharts.KeyboardNavigationHandler}
*/
getKeyboardNavigation: function () {
return this.keyboardNavigation.getKeyboardNavigationHandler();
},
/**
* Remove traces
*/
destroy: function () {
this.newDataAnnouncer.destroy();
this.keyboardNavigation.destroy();
}
});
export default SeriesComponent;
@@ -0,0 +1,397 @@
/* *
*
* (c) 2009-2020 Øystein Moseng
*
* Place desriptions on a series and its points.
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import U from '../../../Core/Utilities.js';
var find = U.find, format = U.format, isNumber = U.isNumber, numberFormat = U.numberFormat, pick = U.pick, defined = U.defined;
import AnnotationsA11y from '../AnnotationsA11y.js';
var getPointAnnotationTexts = AnnotationsA11y.getPointAnnotationTexts;
import HTMLUtilities from '../../Utils/HTMLUtilities.js';
var escapeStringForHTML = HTMLUtilities.escapeStringForHTML, reverseChildNodes = HTMLUtilities.reverseChildNodes, stripHTMLTags = HTMLUtilities.stripHTMLTagsFromString;
import ChartUtilities from '../../Utils/ChartUtilities.js';
var getAxisDescription = ChartUtilities.getAxisDescription, getSeriesFirstPointElement = ChartUtilities.getSeriesFirstPointElement, getSeriesA11yElement = ChartUtilities.getSeriesA11yElement, unhideChartElementFromAT = ChartUtilities.unhideChartElementFromAT;
import Tooltip from '../../../Core/Tooltip.js';
/* eslint-disable valid-jsdoc */
/**
* @private
*/
function findFirstPointWithGraphic(point) {
var sourcePointIndex = point.index;
if (!point.series || !point.series.data || !defined(sourcePointIndex)) {
return null;
}
return find(point.series.data, function (p) {
return !!(p &&
typeof p.index !== 'undefined' &&
p.index > sourcePointIndex &&
p.graphic &&
p.graphic.element);
}) || null;
}
/**
* @private
*/
function shouldAddDummyPoint(point) {
// Note: Sunburst series use isNull for hidden points on drilldown.
// Ignore these.
var isSunburst = point.series && point.series.is('sunburst'), isNull = point.isNull;
return isNull && !isSunburst;
}
/**
* @private
*/
function makeDummyElement(point, pos) {
var renderer = point.series.chart.renderer, dummy = renderer.rect(pos.x, pos.y, 1, 1);
dummy.attr({
'class': 'highcharts-a11y-dummy-point',
fill: 'none',
opacity: 0,
'fill-opacity': 0,
'stroke-opacity': 0
});
return dummy;
}
/**
* @private
* @param {Highcharts.Point} point
* @return {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement|undefined}
*/
function addDummyPointElement(point) {
var series = point.series, firstPointWithGraphic = findFirstPointWithGraphic(point), firstGraphic = firstPointWithGraphic && firstPointWithGraphic.graphic, parentGroup = firstGraphic ?
firstGraphic.parentGroup :
series.graph || series.group, dummyPos = firstPointWithGraphic ? {
x: pick(point.plotX, firstPointWithGraphic.plotX, 0),
y: pick(point.plotY, firstPointWithGraphic.plotY, 0)
} : {
x: pick(point.plotX, 0),
y: pick(point.plotY, 0)
}, dummyElement = makeDummyElement(point, dummyPos);
if (parentGroup && parentGroup.element) {
point.graphic = dummyElement;
point.hasDummyGraphic = true;
dummyElement.add(parentGroup);
// Move to correct pos in DOM
parentGroup.element.insertBefore(dummyElement.element, firstGraphic ? firstGraphic.element : null);
return dummyElement.element;
}
}
/**
* @private
* @param {Highcharts.Series} series
* @return {boolean}
*/
function hasMorePointsThanDescriptionThreshold(series) {
var chartA11yOptions = series.chart.options.accessibility, threshold = (chartA11yOptions.series.pointDescriptionEnabledThreshold);
return !!(threshold !== false &&
series.points &&
series.points.length >= threshold);
}
/**
* @private
* @param {Highcharts.Series} series
* @return {boolean}
*/
function shouldSetScreenReaderPropsOnPoints(series) {
var seriesA11yOptions = series.options.accessibility || {};
return !hasMorePointsThanDescriptionThreshold(series) &&
!seriesA11yOptions.exposeAsGroupOnly;
}
/**
* @private
* @param {Highcharts.Series} series
* @return {boolean}
*/
function shouldSetKeyboardNavPropsOnPoints(series) {
var chartA11yOptions = series.chart.options.accessibility, seriesNavOptions = chartA11yOptions.keyboardNavigation.seriesNavigation;
return !!(series.points && (series.points.length <
seriesNavOptions.pointNavigationEnabledThreshold ||
seriesNavOptions.pointNavigationEnabledThreshold === false));
}
/**
* @private
* @param {Highcharts.Series} series
* @return {boolean}
*/
function shouldDescribeSeriesElement(series) {
var chart = series.chart, chartOptions = chart.options.chart || {}, chartHas3d = chartOptions.options3d && chartOptions.options3d.enabled, hasMultipleSeries = chart.series.length > 1, describeSingleSeriesOption = chart.options.accessibility.series.describeSingleSeries, exposeAsGroupOnlyOption = (series.options.accessibility || {}).exposeAsGroupOnly, noDescribe3D = chartHas3d && hasMultipleSeries;
return !noDescribe3D && (hasMultipleSeries || describeSingleSeriesOption ||
exposeAsGroupOnlyOption || hasMorePointsThanDescriptionThreshold(series));
}
/**
* @private
* @param {Highcharts.Point} point
* @param {number} value
* @return {string}
*/
function pointNumberToString(point, value) {
var chart = point.series.chart, a11yPointOptions = chart.options.accessibility.point || {}, tooltipOptions = point.series.tooltipOptions || {}, lang = chart.options.lang;
if (isNumber(value)) {
return numberFormat(value, a11yPointOptions.valueDecimals ||
tooltipOptions.valueDecimals ||
-1, lang.decimalPoint, lang.accessibility.thousandsSep || lang.thousandsSep);
}
return value;
}
/**
* @private
* @param {Highcharts.Series} series
* @return {string}
*/
function getSeriesDescriptionText(series) {
var seriesA11yOptions = series.options.accessibility || {}, descOpt = seriesA11yOptions.description;
return descOpt && series.chart.langFormat('accessibility.series.description', {
description: descOpt,
series: series
}) || '';
}
/**
* @private
* @param {Highcharts.series} series
* @param {string} axisCollection
* @return {string}
*/
function getSeriesAxisDescriptionText(series, axisCollection) {
var axis = series[axisCollection];
return series.chart.langFormat('accessibility.series.' + axisCollection + 'Description', {
name: getAxisDescription(axis),
series: series
});
}
/**
* Get accessible time description for a point on a datetime axis.
*
* @private
* @function Highcharts.Point#getTimeDescription
* @param {Highcharts.Point} point
* @return {string|undefined}
* The description as string.
*/
function getPointA11yTimeDescription(point) {
var series = point.series, chart = series.chart, a11yOptions = chart.options.accessibility.point || {}, hasDateXAxis = series.xAxis && series.xAxis.dateTime;
if (hasDateXAxis) {
var tooltipDateFormat = Tooltip.prototype.getXDateFormat.call({
getDateFormat: Tooltip.prototype.getDateFormat,
chart: chart
}, point, chart.options.tooltip, series.xAxis), dateFormat = a11yOptions.dateFormatter &&
a11yOptions.dateFormatter(point) ||
a11yOptions.dateFormat ||
tooltipDateFormat;
return chart.time.dateFormat(dateFormat, point.x, void 0);
}
}
/**
* @private
* @param {Highcharts.Point} point
* @return {string}
*/
function getPointXDescription(point) {
var timeDesc = getPointA11yTimeDescription(point), xAxis = point.series.xAxis || {}, pointCategory = xAxis.categories && defined(point.category) &&
('' + point.category).replace('<br/>', ' '), canUseId = point.id && point.id.indexOf('highcharts-') < 0, fallback = 'x, ' + point.x;
return point.name || timeDesc || pointCategory ||
(canUseId ? point.id : fallback);
}
/**
* @private
* @param {Highcharts.Point} point
* @param {string} prefix
* @param {string} suffix
* @return {string}
*/
function getPointArrayMapValueDescription(point, prefix, suffix) {
var pre = prefix || '', suf = suffix || '', keyToValStr = function (key) {
var num = pointNumberToString(point, pick(point[key], point.options[key]));
return key + ': ' + pre + num + suf;
}, pointArrayMap = point.series.pointArrayMap;
return pointArrayMap.reduce(function (desc, key) {
return desc + (desc.length ? ', ' : '') + keyToValStr(key);
}, '');
}
/**
* @private
* @param {Highcharts.Point} point
* @return {string}
*/
function getPointValue(point) {
var series = point.series, a11yPointOpts = series.chart.options.accessibility.point || {}, tooltipOptions = series.tooltipOptions || {}, valuePrefix = a11yPointOpts.valuePrefix ||
tooltipOptions.valuePrefix || '', valueSuffix = a11yPointOpts.valueSuffix ||
tooltipOptions.valueSuffix || '', fallbackKey = (typeof point.value !==
'undefined' ?
'value' : 'y'), fallbackDesc = pointNumberToString(point, point[fallbackKey]);
if (point.isNull) {
return series.chart.langFormat('accessibility.series.nullPointValue', {
point: point
});
}
if (series.pointArrayMap) {
return getPointArrayMapValueDescription(point, valuePrefix, valueSuffix);
}
return valuePrefix + fallbackDesc + valueSuffix;
}
/**
* Return the description for the annotation(s) connected to a point, or empty
* string if none.
*
* @private
* @param {Highcharts.Point} point The data point to get the annotation info from.
* @return {string} Annotation description
*/
function getPointAnnotationDescription(point) {
var chart = point.series.chart;
var langKey = 'accessibility.series.pointAnnotationsDescription';
var annotations = getPointAnnotationTexts(point);
var context = { point: point, annotations: annotations };
return annotations.length ? chart.langFormat(langKey, context) : '';
}
/**
* Return string with information about point.
* @private
* @return {string}
*/
function getPointValueDescription(point) {
var series = point.series, chart = series.chart, pointValueDescriptionFormat = chart.options.accessibility
.point.valueDescriptionFormat, showXDescription = pick(series.xAxis &&
series.xAxis.options.accessibility &&
series.xAxis.options.accessibility.enabled, !chart.angular), xDesc = showXDescription ? getPointXDescription(point) : '', context = {
point: point,
index: defined(point.index) ? (point.index + 1) : '',
xDescription: xDesc,
value: getPointValue(point),
separator: showXDescription ? ', ' : ''
};
return format(pointValueDescriptionFormat, context, chart);
}
/**
* Return string with information about point.
* @private
* @return {string}
*/
function defaultPointDescriptionFormatter(point) {
var series = point.series, chart = series.chart, valText = getPointValueDescription(point), description = point.options && point.options.accessibility &&
point.options.accessibility.description, userDescText = description ? ' ' + description : '', seriesNameText = chart.series.length > 1 && series.name ?
' ' + series.name + '.' : '', annotationsDesc = getPointAnnotationDescription(point), pointAnnotationsText = annotationsDesc ? ' ' + annotationsDesc : '';
point.accessibility = point.accessibility || {};
point.accessibility.valueDescription = valText;
return valText + userDescText + seriesNameText + pointAnnotationsText;
}
/**
* Set a11y props on a point element
* @private
* @param {Highcharts.Point} point
* @param {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement} pointElement
*/
function setPointScreenReaderAttribs(point, pointElement) {
var series = point.series, a11yPointOptions = series.chart.options.accessibility.point || {}, seriesA11yOptions = series.options.accessibility || {}, label = escapeStringForHTML(stripHTMLTags(seriesA11yOptions.pointDescriptionFormatter &&
seriesA11yOptions.pointDescriptionFormatter(point) ||
a11yPointOptions.descriptionFormatter &&
a11yPointOptions.descriptionFormatter(point) ||
defaultPointDescriptionFormatter(point)));
pointElement.setAttribute('role', 'img');
pointElement.setAttribute('aria-label', label);
}
/**
* Add accessible info to individual point elements of a series
* @private
* @param {Highcharts.Series} series
*/
function describePointsInSeries(series) {
var setScreenReaderProps = shouldSetScreenReaderPropsOnPoints(series), setKeyboardProps = shouldSetKeyboardNavPropsOnPoints(series);
if (setScreenReaderProps || setKeyboardProps) {
series.points.forEach(function (point) {
var pointEl = point.graphic && point.graphic.element ||
shouldAddDummyPoint(point) && addDummyPointElement(point);
if (pointEl) {
// We always set tabindex, as long as we are setting props.
// When setting tabindex, also remove default outline to
// avoid ugly border on click.
pointEl.setAttribute('tabindex', '-1');
pointEl.style.outline = '0';
if (setScreenReaderProps) {
setPointScreenReaderAttribs(point, pointEl);
}
else {
pointEl.setAttribute('aria-hidden', true);
}
}
});
}
}
/**
* Return string with information about series.
* @private
* @return {string}
*/
function defaultSeriesDescriptionFormatter(series) {
var chart = series.chart, chartTypes = chart.types || [], description = getSeriesDescriptionText(series), shouldDescribeAxis = function (coll) {
return chart[coll] && chart[coll].length > 1 && series[coll];
}, xAxisInfo = getSeriesAxisDescriptionText(series, 'xAxis'), yAxisInfo = getSeriesAxisDescriptionText(series, 'yAxis'), summaryContext = {
name: series.name || '',
ix: series.index + 1,
numSeries: chart.series && chart.series.length,
numPoints: series.points && series.points.length,
series: series
}, combinationSuffix = chartTypes.length > 1 ? 'Combination' : '', summary = chart.langFormat('accessibility.series.summary.' + series.type + combinationSuffix, summaryContext) || chart.langFormat('accessibility.series.summary.default' + combinationSuffix, summaryContext);
return summary + (description ? ' ' + description : '') + (shouldDescribeAxis('yAxis') ? ' ' + yAxisInfo : '') + (shouldDescribeAxis('xAxis') ? ' ' + xAxisInfo : '');
}
/**
* Set a11y props on a series element
* @private
* @param {Highcharts.Series} series
* @param {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement} seriesElement
*/
function describeSeriesElement(series, seriesElement) {
var seriesA11yOptions = series.options.accessibility || {}, a11yOptions = series.chart.options.accessibility, landmarkVerbosity = a11yOptions.landmarkVerbosity;
// Handle role attribute
if (seriesA11yOptions.exposeAsGroupOnly) {
seriesElement.setAttribute('role', 'img');
}
else if (landmarkVerbosity === 'all') {
seriesElement.setAttribute('role', 'region');
} /* else do not add role */
seriesElement.setAttribute('tabindex', '-1');
seriesElement.style.outline = '0'; // Don't show browser outline on click, despite tabindex
seriesElement.setAttribute('aria-label', escapeStringForHTML(stripHTMLTags(a11yOptions.series.descriptionFormatter &&
a11yOptions.series.descriptionFormatter(series) ||
defaultSeriesDescriptionFormatter(series))));
}
/**
* Put accessible info on series and points of a series.
* @param {Highcharts.Series} series The series to add info on.
*/
function describeSeries(series) {
var chart = series.chart, firstPointEl = getSeriesFirstPointElement(series), seriesEl = getSeriesA11yElement(series), is3d = chart.is3d && chart.is3d();
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.
// Due to z-index issues we can not do this for 3D charts.
if (seriesEl.lastChild === firstPointEl && !is3d) {
reverseChildNodes(seriesEl);
}
describePointsInSeries(series);
unhideChartElementFromAT(chart, seriesEl);
if (shouldDescribeSeriesElement(series)) {
describeSeriesElement(series, seriesEl);
}
else {
seriesEl.setAttribute('aria-label', '');
}
}
}
var SeriesDescriber = {
describeSeries: describeSeries,
defaultPointDescriptionFormatter: defaultPointDescriptionFormatter,
defaultSeriesDescriptionFormatter: defaultSeriesDescriptionFormatter,
getPointA11yTimeDescription: getPointA11yTimeDescription,
getPointXDescription: getPointXDescription,
getPointValue: getPointValue,
getPointValueDescription: getPointValueDescription
};
export default SeriesDescriber;
@@ -0,0 +1,569 @@
/* *
*
* (c) 2009-2020 Øystein Moseng
*
* Handle keyboard navigation for series.
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import Chart from '../../../Core/Chart/Chart.js';
import H from '../../../Core/Globals.js';
import Point from '../../../Core/Series/Point.js';
import U from '../../../Core/Utilities.js';
var defined = U.defined, extend = U.extend;
import KeyboardNavigationHandler from '../../KeyboardNavigationHandler.js';
import EventProvider from '../../Utils/EventProvider.js';
import ChartUtilities from '../../Utils/ChartUtilities.js';
var getPointFromXY = ChartUtilities.getPointFromXY, getSeriesFromName = ChartUtilities.getSeriesFromName, scrollToPoint = ChartUtilities.scrollToPoint;
import '../../../Series/ColumnSeries.js';
import '../../../Series/PieSeries.js';
/* eslint-disable no-invalid-this, valid-jsdoc */
/*
* Set for which series types it makes sense to move to the closest point with
* up/down arrows, and which series types should just move to next series.
*/
H.Series.prototype.keyboardMoveVertical = true;
['column', 'pie'].forEach(function (type) {
if (H.seriesTypes[type]) {
H.seriesTypes[type].prototype.keyboardMoveVertical = false;
}
});
/**
* Get the index of a point in a series. This is needed when using e.g. data
* grouping.
*
* @private
* @function getPointIndex
*
* @param {Highcharts.AccessibilityPoint} point
* The point to find index of.
*
* @return {number|undefined}
* The index in the series.points array of the point.
*/
function getPointIndex(point) {
var index = point.index, points = point.series.points, i = points.length;
if (points[index] !== point) {
while (i--) {
if (points[i] === point) {
return i;
}
}
}
else {
return index;
}
}
/**
* Determine if series navigation should be skipped
*
* @private
* @function isSkipSeries
*
* @param {Highcharts.Series} series
*
* @return {boolean|number|undefined}
*/
function isSkipSeries(series) {
var a11yOptions = series.chart.options.accessibility, seriesNavOptions = a11yOptions.keyboardNavigation.seriesNavigation, seriesA11yOptions = series.options.accessibility || {}, seriesKbdNavOptions = seriesA11yOptions.keyboardNavigation;
return seriesKbdNavOptions && seriesKbdNavOptions.enabled === false ||
seriesA11yOptions.enabled === false ||
series.options.enableMouseTracking === false || // #8440
!series.visible ||
// Skip all points in a series where pointNavigationEnabledThreshold is
// reached
(seriesNavOptions.pointNavigationEnabledThreshold &&
seriesNavOptions.pointNavigationEnabledThreshold <=
series.points.length);
}
/**
* Determine if navigation for a point should be skipped
*
* @private
* @function isSkipPoint
*
* @param {Highcharts.Point} point
*
* @return {boolean|number|undefined}
*/
function isSkipPoint(point) {
var a11yOptions = point.series.chart.options.accessibility;
return point.isNull &&
a11yOptions.keyboardNavigation.seriesNavigation.skipNullPoints ||
point.visible === false ||
isSkipSeries(point.series);
}
/**
* Get the point in a series that is closest (in pixel distance) to a reference
* point. Optionally supply weight factors for x and y directions.
*
* @private
* @function getClosestPoint
*
* @param {Highcharts.Point} point
* @param {Highcharts.Series} series
* @param {number} [xWeight]
* @param {number} [yWeight]
*
* @return {Highcharts.Point|undefined}
*/
function getClosestPoint(point, series, xWeight, yWeight) {
var minDistance = Infinity, dPoint, minIx, distance, i = series.points.length, hasUndefinedPosition = function (point) {
return !(defined(point.plotX) && defined(point.plotY));
};
if (hasUndefinedPosition(point)) {
return;
}
while (i--) {
dPoint = series.points[i];
if (hasUndefinedPosition(dPoint)) {
continue;
}
distance = (point.plotX - dPoint.plotX) *
(point.plotX - dPoint.plotX) *
(xWeight || 1) +
(point.plotY - dPoint.plotY) *
(point.plotY - dPoint.plotY) *
(yWeight || 1);
if (distance < minDistance) {
minDistance = distance;
minIx = i;
}
}
return defined(minIx) ? series.points[minIx] : void 0;
}
/**
* Highlights a point (show tooltip and display hover state).
*
* @private
* @function Highcharts.Point#highlight
*
* @return {Highcharts.Point}
* This highlighted point.
*/
Point.prototype.highlight = function () {
var chart = this.series.chart;
if (!this.isNull) {
this.onMouseOver(); // Show the hover marker and tooltip
}
else {
if (chart.tooltip) {
chart.tooltip.hide(0);
}
// Don't call blur on the element, as it messes up the chart div's focus
}
scrollToPoint(this);
// We focus only after calling onMouseOver because the state change can
// change z-index and mess up the element.
if (this.graphic) {
chart.setFocusToElement(this.graphic);
}
chart.highlightedPoint = this;
return this;
};
/**
* Function to highlight next/previous point in chart.
*
* @private
* @function Highcharts.Chart#highlightAdjacentPoint
*
* @param {boolean} next
* Flag for the direction.
*
* @return {Highcharts.Point|boolean}
* Returns highlighted point on success, false on failure (no adjacent
* point to highlight in chosen direction).
*/
Chart.prototype.highlightAdjacentPoint = function (next) {
var chart = this, series = chart.series, curPoint = chart.highlightedPoint, curPointIndex = curPoint && getPointIndex(curPoint) || 0, curPoints = (curPoint && curPoint.series.points), lastSeries = chart.series && chart.series[chart.series.length - 1], lastPoint = lastSeries && lastSeries.points &&
lastSeries.points[lastSeries.points.length - 1], newSeries, newPoint;
// If no points, return false
if (!series[0] || !series[0].points) {
return false;
}
if (!curPoint) {
// No point is highlighted yet. Try first/last point depending on move
// direction
newPoint = next ? series[0].points[0] : lastPoint;
}
else {
// We have a highlighted point.
// Grab next/prev point & series
newSeries = series[curPoint.series.index + (next ? 1 : -1)];
newPoint = curPoints[curPointIndex + (next ? 1 : -1)];
if (!newPoint && newSeries) {
// Done with this series, try next one
newPoint = newSeries.points[next ? 0 : newSeries.points.length - 1];
}
// If there is no adjacent point, we return false
if (!newPoint) {
return false;
}
}
// Recursively skip points
if (isSkipPoint(newPoint)) {
// If we skip this whole series, move to the end of the series before we
// recurse, just to optimize
newSeries = newPoint.series;
if (isSkipSeries(newSeries)) {
chart.highlightedPoint = next ?
newSeries.points[newSeries.points.length - 1] :
newSeries.points[0];
}
else {
// Otherwise, just move one point
chart.highlightedPoint = newPoint;
}
// Retry
return chart.highlightAdjacentPoint(next);
}
// There is an adjacent point, highlight it
return newPoint.highlight();
};
/**
* Highlight first valid point in a series. Returns the point if successfully
* highlighted, otherwise false. If there is a highlighted point in the series,
* use that as starting point.
*
* @private
* @function Highcharts.Series#highlightFirstValidPoint
*
* @return {boolean|Highcharts.Point}
*/
H.Series.prototype.highlightFirstValidPoint = function () {
var curPoint = this.chart.highlightedPoint, start = (curPoint && curPoint.series) === this ?
getPointIndex(curPoint) :
0, points = this.points, len = points.length;
if (points && len) {
for (var i = start; i < len; ++i) {
if (!isSkipPoint(points[i])) {
return points[i].highlight();
}
}
for (var j = start; j >= 0; --j) {
if (!isSkipPoint(points[j])) {
return points[j].highlight();
}
}
}
return false;
};
/**
* Highlight next/previous series in chart. Returns false if no adjacent series
* in the direction, otherwise returns new highlighted point.
*
* @private
* @function Highcharts.Chart#highlightAdjacentSeries
*
* @param {boolean} down
*
* @return {Highcharts.Point|boolean}
*/
Chart.prototype.highlightAdjacentSeries = function (down) {
var chart = this, newSeries, newPoint, adjacentNewPoint, curPoint = chart.highlightedPoint, lastSeries = chart.series && chart.series[chart.series.length - 1], lastPoint = lastSeries && lastSeries.points &&
lastSeries.points[lastSeries.points.length - 1];
// If no point is highlighted, highlight the first/last point
if (!chart.highlightedPoint) {
newSeries = down ? (chart.series && chart.series[0]) : lastSeries;
newPoint = down ?
(newSeries && newSeries.points && newSeries.points[0]) : lastPoint;
return newPoint ? newPoint.highlight() : false;
}
newSeries = chart.series[curPoint.series.index + (down ? -1 : 1)];
if (!newSeries) {
return false;
}
// We have a new series in this direction, find the right point
// Weigh xDistance as counting much higher than Y distance
newPoint = getClosestPoint(curPoint, newSeries, 4);
if (!newPoint) {
return false;
}
// New series and point exists, but we might want to skip it
if (isSkipSeries(newSeries)) {
// Skip the series
newPoint.highlight();
adjacentNewPoint = chart.highlightAdjacentSeries(down); // Try recurse
if (!adjacentNewPoint) {
// Recurse failed
curPoint.highlight();
return false;
}
// Recurse succeeded
return adjacentNewPoint;
}
// Highlight the new point or any first valid point back or forwards from it
newPoint.highlight();
return newPoint.series.highlightFirstValidPoint();
};
/**
* Highlight the closest point vertically.
*
* @private
* @function Highcharts.Chart#highlightAdjacentPointVertical
*
* @param {boolean} down
*
* @return {Highcharts.Point|boolean}
*/
Chart.prototype.highlightAdjacentPointVertical = function (down) {
var curPoint = this.highlightedPoint, minDistance = Infinity, bestPoint;
if (!defined(curPoint.plotX) || !defined(curPoint.plotY)) {
return false;
}
this.series.forEach(function (series) {
if (isSkipSeries(series)) {
return;
}
series.points.forEach(function (point) {
if (!defined(point.plotY) || !defined(point.plotX) ||
point === curPoint) {
return;
}
var yDistance = point.plotY - curPoint.plotY, width = Math.abs(point.plotX - curPoint.plotX), distance = Math.abs(yDistance) * Math.abs(yDistance) +
width * width * 4; // Weigh horizontal distance highly
// Reverse distance number if axis is reversed
if (series.yAxis && series.yAxis.reversed) {
yDistance *= -1;
}
if (yDistance <= 0 && down || yDistance >= 0 && !down || // Chk dir
distance < 5 || // Points in same spot => infinite loop
isSkipPoint(point)) {
return;
}
if (distance < minDistance) {
minDistance = distance;
bestPoint = point;
}
});
});
return bestPoint ? bestPoint.highlight() : false;
};
/**
* @private
* @param {Highcharts.Chart} chart
* @return {Highcharts.Point|boolean}
*/
function highlightFirstValidPointInChart(chart) {
var res = false;
delete chart.highlightedPoint;
res = chart.series.reduce(function (acc, cur) {
return acc || cur.highlightFirstValidPoint();
}, false);
return res;
}
/**
* @private
* @param {Highcharts.Chart} chart
* @return {Highcharts.Point|boolean}
*/
function highlightLastValidPointInChart(chart) {
var numSeries = chart.series.length, i = numSeries, res = false;
while (i--) {
chart.highlightedPoint = chart.series[i].points[chart.series[i].points.length - 1];
// Highlight first valid point in the series will also
// look backwards. It always starts from currently
// highlighted point.
res = chart.series[i].highlightFirstValidPoint();
if (res) {
break;
}
}
return res;
}
/**
* @private
* @param {Highcharts.Chart} chart
*/
function updateChartFocusAfterDrilling(chart) {
highlightFirstValidPointInChart(chart);
if (chart.focusElement) {
chart.focusElement.removeFocusBorder();
}
}
/**
* @private
* @class
* @name Highcharts.SeriesKeyboardNavigation
*/
function SeriesKeyboardNavigation(chart, keyCodes) {
this.keyCodes = keyCodes;
this.chart = chart;
}
extend(SeriesKeyboardNavigation.prototype, /** @lends Highcharts.SeriesKeyboardNavigation */ {
/**
* Init the keyboard navigation
*/
init: function () {
var keyboardNavigation = this, chart = this.chart, e = this.eventProvider = new EventProvider();
e.addEvent(H.Series, 'destroy', function () {
return keyboardNavigation.onSeriesDestroy(this);
});
e.addEvent(chart, 'afterDrilldown', function () {
updateChartFocusAfterDrilling(this);
});
e.addEvent(chart, 'drilldown', function (e) {
var point = e.point, series = point.series;
keyboardNavigation.lastDrilledDownPoint = {
x: point.x,
y: point.y,
seriesName: series ? series.name : ''
};
});
e.addEvent(chart, 'drillupall', function () {
setTimeout(function () {
keyboardNavigation.onDrillupAll();
}, 10);
});
},
onDrillupAll: function () {
// After drillup we want to find the point that was drilled down to and
// highlight it.
var last = this.lastDrilledDownPoint, chart = this.chart, series = last && getSeriesFromName(chart, last.seriesName), point;
if (last && series && defined(last.x) && defined(last.y)) {
point = getPointFromXY(series, last.x, last.y);
}
// Container focus can be lost on drillup due to deleted elements.
if (chart.container) {
chart.container.focus();
}
if (point && point.highlight) {
point.highlight();
}
if (chart.focusElement) {
chart.focusElement.removeFocusBorder();
}
},
/**
* @return {Highcharts.KeyboardNavigationHandler}
*/
getKeyboardNavigationHandler: function () {
var keyboardNavigation = this, keys = this.keyCodes, chart = this.chart, inverted = chart.inverted;
return new KeyboardNavigationHandler(chart, {
keyCodeMap: [
[inverted ? [keys.up, keys.down] : [keys.left, keys.right], function (keyCode) {
return keyboardNavigation.onKbdSideways(this, keyCode);
}],
[inverted ? [keys.left, keys.right] : [keys.up, keys.down], function (keyCode) {
return keyboardNavigation.onKbdVertical(this, keyCode);
}],
[[keys.enter, keys.space], function () {
if (chart.highlightedPoint) {
chart.highlightedPoint.firePointEvent('click');
}
return this.response.success;
}]
],
init: function (dir) {
return keyboardNavigation.onHandlerInit(this, dir);
},
terminate: function () {
return keyboardNavigation.onHandlerTerminate();
}
});
},
/**
* @private
* @param {Highcharts.KeyboardNavigationHandler} handler
* @param {number} keyCode
* @return {number}
* response
*/
onKbdSideways: function (handler, keyCode) {
var keys = this.keyCodes, isNext = keyCode === keys.right || keyCode === keys.down;
return this.attemptHighlightAdjacentPoint(handler, isNext);
},
/**
* @private
* @param {Highcharts.KeyboardNavigationHandler} handler
* @param {number} keyCode
* @return {number}
* response
*/
onKbdVertical: function (handler, keyCode) {
var chart = this.chart, keys = this.keyCodes, isNext = keyCode === keys.down || keyCode === keys.right, navOptions = chart.options.accessibility.keyboardNavigation
.seriesNavigation;
// Handle serialized mode, act like left/right
if (navOptions.mode && navOptions.mode === 'serialize') {
return this.attemptHighlightAdjacentPoint(handler, isNext);
}
// Normal mode, move between series
var highlightMethod = (chart.highlightedPoint &&
chart.highlightedPoint.series.keyboardMoveVertical) ?
'highlightAdjacentPointVertical' :
'highlightAdjacentSeries';
chart[highlightMethod](isNext);
return handler.response.success;
},
/**
* @private
* @param {Highcharts.KeyboardNavigationHandler} handler
* @param {number} initDirection
* @return {number}
* response
*/
onHandlerInit: function (handler, initDirection) {
var chart = this.chart;
if (initDirection > 0) {
highlightFirstValidPointInChart(chart);
}
else {
highlightLastValidPointInChart(chart);
}
return handler.response.success;
},
/**
* @private
*/
onHandlerTerminate: function () {
var _a, _b;
var chart = this.chart;
var curPoint = chart.highlightedPoint;
(_a = chart.tooltip) === null || _a === void 0 ? void 0 : _a.hide(0);
(_b = curPoint === null || curPoint === void 0 ? void 0 : curPoint.onMouseOut) === null || _b === void 0 ? void 0 : _b.call(curPoint);
delete chart.highlightedPoint;
},
/**
* Function that attempts to highlight next/prev point. Handles wrap around.
* @private
* @param {Highcharts.KeyboardNavigationHandler} handler
* @param {boolean} directionIsNext
* @return {number}
* response
*/
attemptHighlightAdjacentPoint: function (handler, directionIsNext) {
var chart = this.chart, wrapAround = chart.options.accessibility.keyboardNavigation
.wrapAround, highlightSuccessful = chart.highlightAdjacentPoint(directionIsNext);
if (!highlightSuccessful) {
if (wrapAround) {
return handler.init(directionIsNext ? 1 : -1);
}
return handler.response[directionIsNext ? 'next' : 'prev'];
}
return handler.response.success;
},
/**
* @private
*/
onSeriesDestroy: function (series) {
var chart = this.chart, currentHighlightedPointDestroyed = chart.highlightedPoint &&
chart.highlightedPoint.series === series;
if (currentHighlightedPointDestroyed) {
delete chart.highlightedPoint;
if (chart.focusElement) {
chart.focusElement.removeFocusBorder();
}
}
},
/**
* @private
*/
destroy: function () {
this.eventProvider.removeAddedEvents();
}
});
export default SeriesKeyboardNavigation;
@@ -0,0 +1,288 @@
/* *
*
* (c) 2009-2020 Øystein Moseng
*
* Accessibility component for chart zoom.
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import H from '../../Core/Globals.js';
import U from '../../Core/Utilities.js';
var extend = U.extend, pick = U.pick;
import AccessibilityComponent from '../AccessibilityComponent.js';
import KeyboardNavigationHandler from '../KeyboardNavigationHandler.js';
import ChartUtilities from '../Utils/ChartUtilities.js';
var unhideChartElementFromAT = ChartUtilities.unhideChartElementFromAT;
import HTMLUtilities from '../Utils/HTMLUtilities.js';
var setElAttrs = HTMLUtilities.setElAttrs, removeElement = HTMLUtilities.removeElement;
/* eslint-disable no-invalid-this, valid-jsdoc */
/**
* @private
*/
function chartHasMapZoom(chart) {
return !!(chart.mapZoom &&
chart.mapNavButtons &&
chart.mapNavButtons.length);
}
/**
* Pan along axis in a direction (1 or -1), optionally with a defined
* granularity (number of steps it takes to walk across current view)
*
* @private
* @function Highcharts.Axis#panStep
*
* @param {number} direction
* @param {number} [granularity]
*/
H.Axis.prototype.panStep = function (direction, granularity) {
var gran = granularity || 3, extremes = this.getExtremes(), step = (extremes.max - extremes.min) / gran * direction, newMax = extremes.max + step, newMin = extremes.min + step, size = newMax - newMin;
if (direction < 0 && newMin < extremes.dataMin) {
newMin = extremes.dataMin;
newMax = newMin + size;
}
else if (direction > 0 && newMax > extremes.dataMax) {
newMax = extremes.dataMax;
newMin = newMax - size;
}
this.setExtremes(newMin, newMax);
};
/**
* The ZoomComponent class
*
* @private
* @class
* @name Highcharts.ZoomComponent
*/
var ZoomComponent = function () { };
ZoomComponent.prototype = new AccessibilityComponent();
extend(ZoomComponent.prototype, /** @lends Highcharts.ZoomComponent */ {
/**
* Initialize the component
*/
init: function () {
var component = this, chart = this.chart;
[
'afterShowResetZoom', 'afterDrilldown', 'drillupall'
].forEach(function (eventType) {
component.addEvent(chart, eventType, function () {
component.updateProxyOverlays();
});
});
},
/**
* Called when chart is updated
*/
onChartUpdate: function () {
var chart = this.chart, component = this;
// Make map zoom buttons accessible
if (chart.mapNavButtons) {
chart.mapNavButtons.forEach(function (button, i) {
unhideChartElementFromAT(chart, button.element);
component.setMapNavButtonAttrs(button.element, 'accessibility.zoom.mapZoom' + (i ? 'Out' : 'In'));
});
}
},
/**
* @private
* @param {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement} button
* @param {string} labelFormatKey
*/
setMapNavButtonAttrs: function (button, labelFormatKey) {
var chart = this.chart, label = chart.langFormat(labelFormatKey, { chart: chart });
setElAttrs(button, {
tabindex: -1,
role: 'button',
'aria-label': label
});
},
/**
* Update the proxy overlays on every new render to ensure positions are
* correct.
*/
onChartRender: function () {
this.updateProxyOverlays();
},
/**
* Update proxy overlays, recreating the buttons.
*/
updateProxyOverlays: function () {
var chart = this.chart;
// Always start with a clean slate
removeElement(this.drillUpProxyGroup);
removeElement(this.resetZoomProxyGroup);
if (chart.resetZoomButton) {
this.recreateProxyButtonAndGroup(chart.resetZoomButton, 'resetZoomProxyButton', 'resetZoomProxyGroup', chart.langFormat('accessibility.zoom.resetZoomButton', { chart: chart }));
}
if (chart.drillUpButton) {
this.recreateProxyButtonAndGroup(chart.drillUpButton, 'drillUpProxyButton', 'drillUpProxyGroup', chart.langFormat('accessibility.drillUpButton', {
chart: chart,
buttonText: chart.getDrilldownBackText()
}));
}
},
/**
* @private
* @param {Highcharts.SVGElement} buttonEl
* @param {string} buttonProp
* @param {string} groupProp
* @param {string} label
*/
recreateProxyButtonAndGroup: function (buttonEl, buttonProp, groupProp, label) {
removeElement(this[groupProp]);
this[groupProp] = this.addProxyGroup();
this[buttonProp] = this.createProxyButton(buttonEl, this[groupProp], { 'aria-label': label, tabindex: -1 });
},
/**
* Get keyboard navigation handler for map zoom.
* @private
* @return {Highcharts.KeyboardNavigationHandler} The module object
*/
getMapZoomNavigation: function () {
var keys = this.keyCodes, chart = this.chart, component = this;
return new KeyboardNavigationHandler(chart, {
keyCodeMap: [
[
[keys.up, keys.down, keys.left, keys.right],
function (keyCode) {
return component.onMapKbdArrow(this, keyCode);
}
],
[
[keys.tab],
function (_keyCode, e) {
return component.onMapKbdTab(this, e);
}
],
[
[keys.space, keys.enter],
function () {
return component.onMapKbdClick(this);
}
]
],
validate: function () {
return chartHasMapZoom(chart);
},
init: function (direction) {
return component.onMapNavInit(direction);
}
});
},
/**
* @private
* @param {Highcharts.KeyboardNavigationHandler} keyboardNavigationHandler
* @param {number} keyCode
* @return {number} Response code
*/
onMapKbdArrow: function (keyboardNavigationHandler, keyCode) {
var keys = this.keyCodes, panAxis = (keyCode === keys.up || keyCode === keys.down) ?
'yAxis' : 'xAxis', stepDirection = (keyCode === keys.left || keyCode === keys.up) ?
-1 : 1;
this.chart[panAxis][0].panStep(stepDirection);
return keyboardNavigationHandler.response.success;
},
/**
* @private
* @param {Highcharts.KeyboardNavigationHandler} keyboardNavigationHandler
* @param {global.KeyboardEvent} event
* @return {number} Response code
*/
onMapKbdTab: function (keyboardNavigationHandler, event) {
var button, chart = this.chart, response = keyboardNavigationHandler.response, isBackwards = event.shiftKey, isMoveOutOfRange = isBackwards && !this.focusedMapNavButtonIx ||
!isBackwards && this.focusedMapNavButtonIx;
// Deselect old
chart.mapNavButtons[this.focusedMapNavButtonIx].setState(0);
if (isMoveOutOfRange) {
chart.mapZoom(); // Reset zoom
return response[isBackwards ? 'prev' : 'next'];
}
// Select other button
this.focusedMapNavButtonIx += isBackwards ? -1 : 1;
button = chart.mapNavButtons[this.focusedMapNavButtonIx];
chart.setFocusToElement(button.box, button.element);
button.setState(2);
return response.success;
},
/**
* @private
* @param {Highcharts.KeyboardNavigationHandler} keyboardNavigationHandler
* @return {number} Response code
*/
onMapKbdClick: function (keyboardNavigationHandler) {
this.fakeClickEvent(this.chart.mapNavButtons[this.focusedMapNavButtonIx]
.element);
return keyboardNavigationHandler.response.success;
},
/**
* @private
* @param {number} direction
*/
onMapNavInit: function (direction) {
var chart = this.chart, zoomIn = chart.mapNavButtons[0], zoomOut = chart.mapNavButtons[1], initialButton = direction > 0 ? zoomIn : zoomOut;
chart.setFocusToElement(initialButton.box, initialButton.element);
initialButton.setState(2);
this.focusedMapNavButtonIx = direction > 0 ? 0 : 1;
},
/**
* Get keyboard navigation handler for a simple chart button. Provide the
* button reference for the chart, and a function to call on click.
*
* @private
* @param {string} buttonProp The property on chart referencing the button.
* @return {Highcharts.KeyboardNavigationHandler} The module object
*/
simpleButtonNavigation: function (buttonProp, proxyProp, onClick) {
var keys = this.keyCodes, component = this, chart = this.chart;
return new KeyboardNavigationHandler(chart, {
keyCodeMap: [
[
[keys.tab, keys.up, keys.down, keys.left, keys.right],
function (keyCode, e) {
var isBackwards = keyCode === keys.tab && e.shiftKey ||
keyCode === keys.left || keyCode === keys.up;
// Arrow/tab => just move
return this.response[isBackwards ? 'prev' : 'next'];
}
],
[
[keys.space, keys.enter],
function () {
var res = onClick(this, chart);
return pick(res, this.response.success);
}
]
],
validate: function () {
var hasButton = (chart[buttonProp] &&
chart[buttonProp].box &&
component[proxyProp]);
return hasButton;
},
init: function () {
chart.setFocusToElement(chart[buttonProp].box, component[proxyProp]);
}
});
},
/**
* Get keyboard navigation handlers for this component.
* @return {Array<Highcharts.KeyboardNavigationHandler>}
* List of module objects
*/
getKeyboardNavigation: function () {
return [
this.simpleButtonNavigation('resetZoomButton', 'resetZoomProxyButton', function (_handler, chart) {
chart.zoomOut();
}),
this.simpleButtonNavigation('drillUpButton', 'drillUpProxyButton', function (handler, chart) {
chart.drillUp();
return handler.response.prev;
}),
this.getMapZoomNavigation()
];
}
});
export default ZoomComponent;
@@ -0,0 +1,251 @@
/* *
*
* (c) 2009-2020 Øystein Moseng
*
* Extend SVG and Chart classes with focus border capabilities.
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import H from '../Core/Globals.js';
import SVGElement from '../Core/Renderer/SVG/SVGElement.js';
import SVGLabel from '../Core/Renderer/SVG/SVGLabel.js';
import U from '../Core/Utilities.js';
var addEvent = U.addEvent, extend = U.extend, pick = U.pick;
/* eslint-disable no-invalid-this, valid-jsdoc */
// Attributes that trigger a focus border update
var svgElementBorderUpdateTriggers = [
'x', 'y', 'transform', 'width', 'height', 'r', 'd', 'stroke-width'
];
/**
* Add hook to destroy focus border if SVG element is destroyed, unless
* hook already exists.
* @private
* @param el Element to add destroy hook to
*/
function addDestroyFocusBorderHook(el) {
if (el.focusBorderDestroyHook) {
return;
}
var origDestroy = el.destroy;
el.destroy = function () {
var _a, _b;
(_b = (_a = el.focusBorder) === null || _a === void 0 ? void 0 : _a.destroy) === null || _b === void 0 ? void 0 : _b.call(_a);
return origDestroy.apply(el, arguments);
};
el.focusBorderDestroyHook = origDestroy;
}
/**
* Remove hook from SVG element added by addDestroyFocusBorderHook, if
* existing.
* @private
* @param el Element to remove destroy hook from
*/
function removeDestroyFocusBorderHook(el) {
if (!el.focusBorderDestroyHook) {
return;
}
el.destroy = el.focusBorderDestroyHook;
delete el.focusBorderDestroyHook;
}
/**
* Add hooks to update the focus border of an element when the element
* size/position is updated, unless already added.
* @private
* @param el Element to add update hooks to
* @param updateParams Parameters to pass through to addFocusBorder when updating.
*/
function addUpdateFocusBorderHooks(el) {
var updateParams = [];
for (var _i = 1; _i < arguments.length; _i++) {
updateParams[_i - 1] = arguments[_i];
}
if (el.focusBorderUpdateHooks) {
return;
}
el.focusBorderUpdateHooks = {};
svgElementBorderUpdateTriggers.forEach(function (trigger) {
var setterKey = trigger + 'Setter';
var origSetter = el[setterKey] || el._defaultSetter;
el.focusBorderUpdateHooks[setterKey] = origSetter;
el[setterKey] = function () {
var ret = origSetter.apply(el, arguments);
el.addFocusBorder.apply(el, updateParams);
return ret;
};
});
}
/**
* Remove hooks from SVG element added by addUpdateFocusBorderHooks, if
* existing.
* @private
* @param el Element to remove update hooks from
*/
function removeUpdateFocusBorderHooks(el) {
if (!el.focusBorderUpdateHooks) {
return;
}
Object.keys(el.focusBorderUpdateHooks).forEach(function (setterKey) {
var origSetter = el.focusBorderUpdateHooks[setterKey];
if (origSetter === el._defaultSetter) {
delete el[setterKey];
}
else {
el[setterKey] = origSetter;
}
});
delete el.focusBorderUpdateHooks;
}
/*
* Add focus border functionality to SVGElements. Draws a new rect on top of
* element around its bounding box. This is used by multiple components.
*/
extend(SVGElement.prototype, {
/**
* @private
* @function Highcharts.SVGElement#addFocusBorder
*
* @param {number} margin
*
* @param {Highcharts.CSSObject} style
*/
addFocusBorder: function (margin, style) {
// Allow updating by just adding new border
if (this.focusBorder) {
this.removeFocusBorder();
}
// Add the border rect
var bb = this.getBBox(), pad = pick(margin, 3);
bb.x += this.translateX ? this.translateX : 0;
bb.y += this.translateY ? this.translateY : 0;
var borderPosX = bb.x - pad, borderPosY = bb.y - pad, borderWidth = bb.width + 2 * pad, borderHeight = bb.height + 2 * pad;
// For text elements, apply x and y offset, #11397.
/**
* @private
* @function
*
* @param {Highcharts.SVGElement} text
*
* @return {TextAnchorCorrectionObject}
*/
function getTextAnchorCorrection(text) {
var posXCorrection = 0, posYCorrection = 0;
if (text.attr('text-anchor') === 'middle') {
posXCorrection = H.isFirefox && text.rotation ? 0.25 : 0.5;
posYCorrection = H.isFirefox && !text.rotation ? 0.75 : 0.5;
}
else if (!text.rotation) {
posYCorrection = 0.75;
}
else {
posXCorrection = 0.25;
}
return {
x: posXCorrection,
y: posYCorrection
};
}
var isLabel = this instanceof SVGLabel;
if (this.element.nodeName === 'text' || isLabel) {
var isRotated = !!this.rotation, correction = !isLabel ? getTextAnchorCorrection(this) :
{
x: isRotated ? 1 : 0,
y: 0
};
borderPosX = +this.attr('x') - (bb.width * correction.x) - pad;
borderPosY = +this.attr('y') - (bb.height * correction.y) - pad;
if (isLabel && isRotated) {
var temp = borderWidth;
borderWidth = borderHeight;
borderHeight = temp;
borderPosX = +this.attr('x') - (bb.height * correction.x) - pad;
borderPosY = +this.attr('y') - (bb.width * correction.y) - pad;
}
}
this.focusBorder = this.renderer.rect(borderPosX, borderPosY, borderWidth, borderHeight, parseInt((style && style.borderRadius || 0).toString(), 10))
.addClass('highcharts-focus-border')
.attr({
zIndex: 99
})
.add(this.parentGroup);
if (!this.renderer.styledMode) {
this.focusBorder.attr({
stroke: style && style.stroke,
'stroke-width': style && style.strokeWidth
});
}
addUpdateFocusBorderHooks(this, margin, style);
addDestroyFocusBorderHook(this);
},
/**
* @private
* @function Highcharts.SVGElement#removeFocusBorder
*/
removeFocusBorder: function () {
removeUpdateFocusBorderHooks(this);
removeDestroyFocusBorderHook(this);
if (this.focusBorder) {
this.focusBorder.destroy();
delete this.focusBorder;
}
}
});
/**
* Redraws the focus border on the currently focused element.
*
* @private
* @function Highcharts.Chart#renderFocusBorder
*/
H.Chart.prototype.renderFocusBorder = function () {
var focusElement = this.focusElement, focusBorderOptions = this.options.accessibility.keyboardNavigation.focusBorder;
if (focusElement) {
focusElement.removeFocusBorder();
if (focusBorderOptions.enabled) {
focusElement.addFocusBorder(focusBorderOptions.margin, {
stroke: focusBorderOptions.style.color,
strokeWidth: focusBorderOptions.style.lineWidth,
borderRadius: focusBorderOptions.style.borderRadius
});
}
}
};
/**
* Set chart's focus to an SVGElement. Calls focus() on it, and draws the focus
* border. This is used by multiple components.
*
* @private
* @function Highcharts.Chart#setFocusToElement
*
* @param {Highcharts.SVGElement} svgElement
* Element to draw the border around.
*
* @param {SVGDOMElement|HTMLDOMElement} [focusElement]
* If supplied, it draws the border around svgElement and sets the focus
* to focusElement.
*/
H.Chart.prototype.setFocusToElement = function (svgElement, focusElement) {
var focusBorderOptions = this.options.accessibility.keyboardNavigation.focusBorder, browserFocusElement = focusElement || svgElement.element;
// Set browser focus if possible
if (browserFocusElement &&
browserFocusElement.focus) {
// If there is no focusin-listener, add one to work around Edge issue
// where Narrator is not reading out points despite calling focus().
if (!(browserFocusElement.hcEvents &&
browserFocusElement.hcEvents.focusin)) {
addEvent(browserFocusElement, 'focusin', function () { });
}
browserFocusElement.focus();
// Hide default focus ring
if (focusBorderOptions.hideBrowserFocusOutline) {
browserFocusElement.style.outline = 'none';
}
}
if (this.focusElement) {
this.focusElement.removeFocusBorder();
}
this.focusElement = svgElement;
this.renderFocusBorder();
};
@@ -0,0 +1,83 @@
/* *
*
* (c) 2009-2020 Øystein Moseng
*
* Handling for Windows High Contrast Mode.
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import H from '../Core/Globals.js';
var isMS = H.isMS, win = H.win, doc = win.document;
var whcm = {
/**
* Detect WHCM in the browser.
*
* @function Highcharts#isHighContrastModeActive
* @private
* @return {boolean} Returns true if the browser is in High Contrast mode.
*/
isHighContrastModeActive: function () {
// Use media query on Edge, but not on IE
var isEdge = /(Edg)/.test(win.navigator.userAgent);
if (win.matchMedia && isEdge) {
return win.matchMedia('(-ms-high-contrast: active)').matches;
}
// Test BG image for IE
if (isMS && win.getComputedStyle) {
var testDiv = doc.createElement('div');
var imageSrc = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
testDiv.style.backgroundImage = "url(" + imageSrc + ")"; // #13071
doc.body.appendChild(testDiv);
var bi = (testDiv.currentStyle ||
win.getComputedStyle(testDiv)).backgroundImage;
doc.body.removeChild(testDiv);
return bi === 'none';
}
// Not used for other browsers
return false;
},
/**
* Force high contrast theme for the chart. The default theme is defined in
* a separate file.
*
* @function Highcharts#setHighContrastTheme
* @private
* @param {Highcharts.AccessibilityChart} chart The chart to set the theme of.
* @return {void}
*/
setHighContrastTheme: function (chart) {
// We might want to add additional functionality here in the future for
// storing the old state so that we can reset the theme if HC mode is
// disabled. For now, the user will have to reload the page.
chart.highContrastModeActive = true;
// Apply theme to chart
var theme = (chart.options.accessibility.highContrastTheme);
chart.update(theme, false);
// Force series colors (plotOptions is not enough)
chart.series.forEach(function (s) {
var plotOpts = theme.plotOptions[s.type] || {};
s.update({
color: plotOpts.color || 'windowText',
colors: [plotOpts.color || 'windowText'],
borderColor: plotOpts.borderColor || 'window'
});
// Force point colors if existing
s.points.forEach(function (p) {
if (p.options && p.options.color) {
p.update({
color: plotOpts.color || 'windowText',
borderColor: plotOpts.borderColor || 'window'
}, false);
}
});
});
// The redraw for each series and after is required for 3D pie
// (workaround)
chart.redraw();
}
};
export default whcm;
@@ -0,0 +1,211 @@
/* *
*
* (c) 2009-2020 Øystein Moseng
*
* Default theme for Windows High Contrast Mode.
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
var theme = {
chart: {
backgroundColor: 'window'
},
title: {
style: {
color: 'windowText'
}
},
subtitle: {
style: {
color: 'windowText'
}
},
colorAxis: {
minColor: 'windowText',
maxColor: 'windowText',
stops: []
},
colors: ['windowText'],
xAxis: {
gridLineColor: 'windowText',
labels: {
style: {
color: 'windowText'
}
},
lineColor: 'windowText',
minorGridLineColor: 'windowText',
tickColor: 'windowText',
title: {
style: {
color: 'windowText'
}
}
},
yAxis: {
gridLineColor: 'windowText',
labels: {
style: {
color: 'windowText'
}
},
lineColor: 'windowText',
minorGridLineColor: 'windowText',
tickColor: 'windowText',
title: {
style: {
color: 'windowText'
}
}
},
tooltip: {
backgroundColor: 'window',
borderColor: 'windowText',
style: {
color: 'windowText'
}
},
plotOptions: {
series: {
lineColor: 'windowText',
fillColor: 'window',
borderColor: 'windowText',
edgeColor: 'windowText',
borderWidth: 1,
dataLabels: {
connectorColor: 'windowText',
color: 'windowText',
style: {
color: 'windowText',
textOutline: 'none'
}
},
marker: {
lineColor: 'windowText',
fillColor: 'windowText'
}
},
pie: {
color: 'window',
colors: ['window'],
borderColor: 'windowText',
borderWidth: 1
},
boxplot: {
fillColor: 'window'
},
candlestick: {
lineColor: 'windowText',
fillColor: 'window'
},
errorbar: {
fillColor: 'window'
}
},
legend: {
backgroundColor: 'window',
itemStyle: {
color: 'windowText'
},
itemHoverStyle: {
color: 'windowText'
},
itemHiddenStyle: {
color: '#555'
},
title: {
style: {
color: 'windowText'
}
}
},
credits: {
style: {
color: 'windowText'
}
},
labels: {
style: {
color: 'windowText'
}
},
drilldown: {
activeAxisLabelStyle: {
color: 'windowText'
},
activeDataLabelStyle: {
color: 'windowText'
}
},
navigation: {
buttonOptions: {
symbolStroke: 'windowText',
theme: {
fill: 'window'
}
}
},
rangeSelector: {
buttonTheme: {
fill: 'window',
stroke: 'windowText',
style: {
color: 'windowText'
},
states: {
hover: {
fill: 'window',
stroke: 'windowText',
style: {
color: 'windowText'
}
},
select: {
fill: '#444',
stroke: 'windowText',
style: {
color: 'windowText'
}
}
}
},
inputBoxBorderColor: 'windowText',
inputStyle: {
backgroundColor: 'window',
color: 'windowText'
},
labelStyle: {
color: 'windowText'
}
},
navigator: {
handles: {
backgroundColor: 'window',
borderColor: 'windowText'
},
outlineColor: 'windowText',
maskFill: 'transparent',
series: {
color: 'windowText',
lineColor: 'windowText'
},
xAxis: {
gridLineColor: 'windowText'
}
},
scrollbar: {
barBackgroundColor: '#444',
barBorderColor: 'windowText',
buttonArrowColor: 'windowText',
buttonBackgroundColor: 'window',
buttonBorderColor: 'windowText',
rifleColor: 'windowText',
trackBackgroundColor: 'window',
trackBorderColor: 'windowText'
}
};
export default theme;
@@ -0,0 +1,359 @@
/* *
*
* (c) 2009-2020 Øystein Moseng
*
* Main keyboard navigation handling.
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import H from '../Core/Globals.js';
var doc = H.doc, win = H.win;
import U from '../Core/Utilities.js';
var addEvent = U.addEvent, fireEvent = U.fireEvent;
import HTMLUtilities from './Utils/HTMLUtilities.js';
var getElement = HTMLUtilities.getElement;
import EventProvider from './Utils/EventProvider.js';
/* eslint-disable valid-jsdoc */
// Add event listener to document to detect ESC key press and dismiss
// hover/popup content.
addEvent(doc, 'keydown', function (e) {
var keycode = e.which || e.keyCode;
var esc = 27;
if (keycode === esc && H.charts) {
H.charts.forEach(function (chart) {
if (chart && chart.dismissPopupContent) {
chart.dismissPopupContent();
}
});
}
});
/**
* Dismiss popup content in chart, including export menu and tooltip.
*/
H.Chart.prototype.dismissPopupContent = function () {
var chart = this;
fireEvent(this, 'dismissPopupContent', {}, function () {
if (chart.tooltip) {
chart.tooltip.hide(0);
}
chart.hideExportMenu();
});
};
/**
* The KeyboardNavigation class, containing the overall keyboard navigation
* logic for the chart.
*
* @requires module:modules/accessibility
*
* @private
* @class
* @param {Highcharts.Chart} chart
* Chart object
* @param {object} components
* Map of component names to AccessibilityComponent objects.
* @name Highcharts.KeyboardNavigation
*/
function KeyboardNavigation(chart, components) {
this.init(chart, components);
}
KeyboardNavigation.prototype = {
/**
* Initialize the class
* @private
* @param {Highcharts.Chart} chart
* Chart object
* @param {object} components
* Map of component names to AccessibilityComponent objects.
*/
init: function (chart, components) {
var _this = this;
var ep = this.eventProvider = new EventProvider();
this.chart = chart;
this.components = components;
this.modules = [];
this.currentModuleIx = 0;
// Run an update to get all modules
this.update();
ep.addEvent(this.tabindexContainer, 'keydown', function (e) { return _this.onKeydown(e); });
ep.addEvent(this.tabindexContainer, 'focus', function (e) { return _this.onFocus(e); });
ep.addEvent(doc, 'mouseup', function () { return _this.onMouseUp(); });
ep.addEvent(chart.renderTo, 'mousedown', function () {
_this.isClickingChart = true;
});
ep.addEvent(chart.renderTo, 'mouseover', function () {
_this.pointerIsOverChart = true;
});
ep.addEvent(chart.renderTo, 'mouseout', function () {
_this.pointerIsOverChart = false;
});
// Init first module
if (this.modules.length) {
this.modules[0].init(1);
}
},
/**
* Update the modules for the keyboard navigation.
* @param {Array<string>} [order]
* Array specifying the tab order of the components.
*/
update: function (order) {
var a11yOptions = this.chart.options.accessibility, keyboardOptions = a11yOptions && a11yOptions.keyboardNavigation, components = this.components;
this.updateContainerTabindex();
if (keyboardOptions &&
keyboardOptions.enabled &&
order &&
order.length) {
// We (still) have keyboard navigation. Update module list
this.modules = order.reduce(function (modules, componentName) {
var navModules = components[componentName].getKeyboardNavigation();
return modules.concat(navModules);
}, []);
this.updateExitAnchor();
}
else {
this.modules = [];
this.currentModuleIx = 0;
this.removeExitAnchor();
}
},
/**
* Function to run on container focus
* @private
* @param {global.FocusEvent} e Browser focus event.
*/
onFocus: function (e) {
var _a;
var chart = this.chart;
var focusComesFromChart = (e.relatedTarget &&
chart.container.contains(e.relatedTarget));
// Init keyboard nav if tabbing into chart
if (!this.isClickingChart && !focusComesFromChart) {
(_a = this.modules[0]) === null || _a === void 0 ? void 0 : _a.init(1);
}
},
/**
* Reset chart navigation state if we click outside the chart and it's
* not already reset.
* @private
*/
onMouseUp: function () {
delete this.isClickingChart;
if (!this.keyboardReset && !this.pointerIsOverChart) {
var chart = this.chart, curMod = this.modules &&
this.modules[this.currentModuleIx || 0];
if (curMod && curMod.terminate) {
curMod.terminate();
}
if (chart.focusElement) {
chart.focusElement.removeFocusBorder();
}
this.currentModuleIx = 0;
this.keyboardReset = true;
}
},
/**
* Function to run on keydown
* @private
* @param {global.KeyboardEvent} ev Browser keydown event.
*/
onKeydown: function (ev) {
var e = ev || win.event, preventDefault, curNavModule = this.modules && this.modules.length &&
this.modules[this.currentModuleIx];
// Used for resetting nav state when clicking outside chart
this.keyboardReset = false;
// If there is a nav module for the current index, run it.
// Otherwise, we are outside of the chart in some direction.
if (curNavModule) {
var response = curNavModule.run(e);
if (response === curNavModule.response.success) {
preventDefault = true;
}
else if (response === curNavModule.response.prev) {
preventDefault = this.prev();
}
else if (response === curNavModule.response.next) {
preventDefault = this.next();
}
if (preventDefault) {
e.preventDefault();
e.stopPropagation();
}
}
},
/**
* Go to previous module.
* @private
*/
prev: function () {
return this.move(-1);
},
/**
* Go to next module.
* @private
*/
next: function () {
return this.move(1);
},
/**
* Move to prev/next module.
* @private
* @param {number} direction
* Direction to move. +1 for next, -1 for prev.
* @return {boolean}
* True if there was a valid module in direction.
*/
move: function (direction) {
var curModule = this.modules && this.modules[this.currentModuleIx];
if (curModule && curModule.terminate) {
curModule.terminate(direction);
}
// Remove existing focus border if any
if (this.chart.focusElement) {
this.chart.focusElement.removeFocusBorder();
}
this.currentModuleIx += direction;
var newModule = this.modules && this.modules[this.currentModuleIx];
if (newModule) {
if (newModule.validate && !newModule.validate()) {
return this.move(direction); // Invalid module, recurse
}
if (newModule.init) {
newModule.init(direction); // Valid module, init it
return true;
}
}
// No module
this.currentModuleIx = 0; // Reset counter
// Set focus to chart or exit anchor depending on direction
if (direction > 0) {
this.exiting = true;
this.exitAnchor.focus();
}
else {
this.tabindexContainer.focus();
}
return false;
},
/**
* We use an exit anchor to move focus out of chart whenever we want, by
* setting focus to this div and not preventing the default tab action. We
* also use this when users come back into the chart by tabbing back, in
* order to navigate from the end of the chart.
* @private
*/
updateExitAnchor: function () {
var endMarkerId = 'highcharts-end-of-chart-marker-' + this.chart.index, endMarker = getElement(endMarkerId);
this.removeExitAnchor();
if (endMarker) {
this.makeElementAnExitAnchor(endMarker);
this.exitAnchor = endMarker;
}
else {
this.createExitAnchor();
}
},
/**
* Chart container should have tabindex if navigation is enabled.
* @private
*/
updateContainerTabindex: function () {
var a11yOptions = this.chart.options.accessibility, keyboardOptions = a11yOptions && a11yOptions.keyboardNavigation, shouldHaveTabindex = !(keyboardOptions && keyboardOptions.enabled === false), chart = this.chart, container = chart.container;
var tabindexContainer;
if (chart.renderTo.hasAttribute('tabindex')) {
container.removeAttribute('tabindex');
tabindexContainer = chart.renderTo;
}
else {
tabindexContainer = container;
}
this.tabindexContainer = tabindexContainer;
var curTabindex = tabindexContainer.getAttribute('tabindex');
if (shouldHaveTabindex && !curTabindex) {
tabindexContainer.setAttribute('tabindex', '0');
}
else if (!shouldHaveTabindex) {
chart.container.removeAttribute('tabindex');
}
},
/**
* @private
*/
makeElementAnExitAnchor: function (el) {
var chartTabindex = this.tabindexContainer.getAttribute('tabindex') || 0;
el.setAttribute('class', 'highcharts-exit-anchor');
el.setAttribute('tabindex', chartTabindex);
el.setAttribute('aria-hidden', false);
// Handle focus
this.addExitAnchorEventsToEl(el);
},
/**
* Add new exit anchor to the chart.
*
* @private
*/
createExitAnchor: function () {
var chart = this.chart, exitAnchor = this.exitAnchor = doc.createElement('div');
chart.renderTo.appendChild(exitAnchor);
this.makeElementAnExitAnchor(exitAnchor);
},
/**
* @private
*/
removeExitAnchor: function () {
if (this.exitAnchor && this.exitAnchor.parentNode) {
this.exitAnchor.parentNode
.removeChild(this.exitAnchor);
delete this.exitAnchor;
}
},
/**
* @private
*/
addExitAnchorEventsToEl: function (element) {
var chart = this.chart, keyboardNavigation = this;
this.eventProvider.addEvent(element, 'focus', function (ev) {
var e = ev || win.event, curModule, focusComesFromChart = (e.relatedTarget &&
chart.container.contains(e.relatedTarget)), comingInBackwards = !(focusComesFromChart || keyboardNavigation.exiting);
if (comingInBackwards) {
keyboardNavigation.tabindexContainer.focus();
e.preventDefault();
// Move to last valid keyboard nav module
// Note the we don't run it, just set the index
if (keyboardNavigation.modules &&
keyboardNavigation.modules.length) {
keyboardNavigation.currentModuleIx =
keyboardNavigation.modules.length - 1;
curModule = keyboardNavigation.modules[keyboardNavigation.currentModuleIx];
// Validate the module
if (curModule &&
curModule.validate && !curModule.validate()) {
// Invalid. Try moving backwards to find next valid.
keyboardNavigation.prev();
}
else if (curModule) {
// We have a valid module, init it
curModule.init(-1);
}
}
}
else {
// Don't skip the next focus, we only skip once.
keyboardNavigation.exiting = false;
}
});
},
/**
* Remove all traces of keyboard navigation.
* @private
*/
destroy: function () {
this.removeExitAnchor();
this.eventProvider.removeAddedEvents();
this.chart.container.removeAttribute('tabindex');
}
};
export default KeyboardNavigation;
@@ -0,0 +1,99 @@
/* *
*
* (c) 2009-2020 Øystein Moseng
*
* Keyboard navigation handler base class definition
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import U from '../Core/Utilities.js';
var find = U.find;
/**
* Options for the keyboard navigation handler.
*
* @interface Highcharts.KeyboardNavigationHandlerOptionsObject
*/ /**
* An array containing pairs of an array of keycodes, mapped to a handler
* function. When the keycode is received, the handler is called with the
* keycode as parameter.
* @name Highcharts.KeyboardNavigationHandlerOptionsObject#keyCodeMap
* @type {Array<Array<Array<number>, Function>>}
*/ /**
* Function to run on initialization of module.
* @name Highcharts.KeyboardNavigationHandlerOptionsObject#init
* @type {Function}
*/ /**
* Function to run before moving to next/prev module. Receives moving direction
* as parameter: +1 for next, -1 for previous.
* @name Highcharts.KeyboardNavigationHandlerOptionsObject#terminate
* @type {Function|undefined}
*/ /**
* Function to run to validate module. Should return false if module should not
* run, true otherwise. Receives chart as parameter.
* @name Highcharts.KeyboardNavigationHandlerOptionsObject#validate
* @type {Function|undefined}
*/
/* eslint-disable no-invalid-this, valid-jsdoc */
/**
* Define a keyboard navigation handler for use with a
* Highcharts.AccessibilityComponent instance. This functions as an abstraction
* layer for keyboard navigation, and defines a map of keyCodes to handler
* functions.
*
* @requires module:modules/accessibility
*
* @sample highcharts/accessibility/custom-component
* Custom accessibility component
*
* @class
* @name Highcharts.KeyboardNavigationHandler
*
* @param {Highcharts.Chart} chart
* The chart this module should act on.
*
* @param {Highcharts.KeyboardNavigationHandlerOptionsObject} options
* Options for the keyboard navigation handler.
*/
function KeyboardNavigationHandler(chart, options) {
this.chart = chart;
this.keyCodeMap = options.keyCodeMap || [];
this.validate = options.validate;
this.init = options.init;
this.terminate = options.terminate;
// Response enum
this.response = {
success: 1,
prev: 2,
next: 3,
noHandler: 4,
fail: 5 // Handler failed
};
}
KeyboardNavigationHandler.prototype = {
/**
* Find handler function(s) for key code in the keyCodeMap and run it.
*
* @function KeyboardNavigationHandler#run
* @param {global.KeyboardEvent} e
* @return {number} Returns a response code indicating whether the run was
* a success/fail/unhandled, or if we should move to next/prev module.
*/
run: function (e) {
var keyCode = e.which || e.keyCode, response = this.response.noHandler, handlerCodeSet = find(this.keyCodeMap, function (codeSet) {
return codeSet[0].indexOf(keyCode) > -1;
});
if (handlerCodeSet) {
response = handlerCodeSet[1].call(this, keyCode, e);
}
else if (keyCode === 9) {
// Default tab handler, move to next/prev module
response = this.response[e.shiftKey ? 'prev' : 'next'];
}
return response;
}
};
export default KeyboardNavigationHandler;
@@ -0,0 +1,233 @@
/* *
*
* (c) 2009-2020 Øystein Moseng
*
* Default options for accessibility.
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
/* eslint-disable max-len */
/*
* List of deprecated options:
*
* chart.description -> accessibility.description
* chart.typeDescription -> accessibility.typeDescription
* series.description -> series.accessibility.description
* series.exposeElementToA11y -> series.accessibility.exposeAsGroupOnly
* series.pointDescriptionFormatter ->
* series.accessibility.pointDescriptionFormatter
* series.skipKeyboardNavigation ->
* series.accessibility.keyboardNavigation.enabled
* point.description -> point.accessibility.description !!!! WARNING: No longer deprecated and handled, removed for HC8.
* axis.description -> axis.accessibility.description
*
* accessibility.pointDateFormat -> accessibility.point.dateFormat
* accessibility.addTableShortcut -> Handled by screenReaderSection.beforeChartFormat
* accessibility.pointDateFormatter -> accessibility.point.dateFormatter
* accessibility.pointDescriptionFormatter -> accessibility.point.descriptionFormatter
* accessibility.pointDescriptionThreshold -> accessibility.series.pointDescriptionEnabledThreshold
* accessibility.pointNavigationThreshold -> accessibility.keyboardNavigation.seriesNavigation.pointNavigationEnabledThreshold
* accessibility.pointValueDecimals -> accessibility.point.valueDecimals
* accessibility.pointValuePrefix -> accessibility.point.valuePrefix
* accessibility.pointValueSuffix -> accessibility.point.valueSuffix
* accessibility.screenReaderSectionFormatter -> accessibility.screenReaderSection.beforeChartFormatter
* accessibility.describeSingleSeries -> accessibility.series.describeSingleSeries
* accessibility.seriesDescriptionFormatter -> accessibility.series.descriptionFormatter
* accessibility.onTableAnchorClick -> accessibility.screenReaderSection.onViewDataTableClick
* accessibility.axisRangeDateFormat -> accessibility.screenReaderSection.axisRangeDateFormat
* accessibility.keyboardNavigation.skipNullPoints -> accessibility.keyboardNavigation.seriesNavigation.skipNullPoints
* accessibility.keyboardNavigation.mode -> accessibility.keyboardNavigation.seriesNavigation.mode
*
* lang.accessibility.chartHeading -> no longer used, remove
* lang.accessibility.legendItem -> lang.accessibility.legend.legendItem
* lang.accessibility.legendLabel -> lang.accessibility.legend.legendLabel
* lang.accessibility.mapZoomIn -> lang.accessibility.zoom.mapZoomIn
* lang.accessibility.mapZoomOut -> lang.accessibility.zoom.mapZoomOut
* lang.accessibility.resetZoomButton -> lang.accessibility.zoom.resetZoomButton
* lang.accessibility.screenReaderRegionLabel -> lang.accessibility.screenReaderSection.beforeRegionLabel
* lang.accessibility.rangeSelectorButton -> lang.accessibility.rangeSelector.buttonText
* lang.accessibility.rangeSelectorMaxInput -> lang.accessibility.rangeSelector.maxInputLabel
* lang.accessibility.rangeSelectorMinInput -> lang.accessibility.rangeSelector.minInputLabel
* lang.accessibility.svgContainerEnd -> lang.accessibility.screenReaderSection.endOfChartMarker
* lang.accessibility.viewAsDataTable -> lang.accessibility.table.viewAsDataTableButtonText
* lang.accessibility.tableSummary -> lang.accessibility.table.tableSummary
*
*/
/* eslint-enable max-len */
'use strict';
import U from '../../Core/Utilities.js';
var error = U.error, pick = U.pick;
/* eslint-disable valid-jsdoc */
/**
* Set a new option on a root prop, where the option is defined as an array of
* suboptions.
* @private
* @param root
* @param {Array<string>} optionAsArray
* @param {*} val
* @return {void}
*/
function traverseSetOption(root, optionAsArray, val) {
var opt = root, prop, i = 0;
for (; i < optionAsArray.length - 1; ++i) {
prop = optionAsArray[i];
opt = opt[prop] = pick(opt[prop], {});
}
opt[optionAsArray[optionAsArray.length - 1]] = val;
}
/**
* If we have a clear root option node for old and new options and a mapping
* between, we can use this generic function for the copy and warn logic.
*/
function deprecateFromOptionsMap(chart, rootOldAsArray, rootNewAsArray, mapToNewOptions) {
/**
* @private
*/
function getChildProp(root, propAsArray) {
return propAsArray.reduce(function (acc, cur) {
return acc[cur];
}, root);
}
var rootOld = getChildProp(chart.options, rootOldAsArray), rootNew = getChildProp(chart.options, rootNewAsArray);
Object.keys(mapToNewOptions).forEach(function (oldOptionKey) {
var _a;
var val = rootOld[oldOptionKey];
if (typeof val !== 'undefined') {
traverseSetOption(rootNew, mapToNewOptions[oldOptionKey], val);
error(32, false, chart, (_a = {},
_a[rootOldAsArray.join('.') + "." + oldOptionKey] = rootNewAsArray.join('.') + "." + mapToNewOptions[oldOptionKey].join('.'),
_a));
}
});
}
/**
* @private
*/
function copyDeprecatedChartOptions(chart) {
var chartOptions = chart.options.chart || {}, a11yOptions = chart.options.accessibility || {};
['description', 'typeDescription'].forEach(function (prop) {
var _a;
if (chartOptions[prop]) {
a11yOptions[prop] = chartOptions[prop];
error(32, false, chart, (_a = {}, _a["chart." + prop] = "use accessibility." + prop, _a));
}
});
}
/**
* @private
*/
function copyDeprecatedAxisOptions(chart) {
chart.axes.forEach(function (axis) {
var opts = axis.options;
if (opts && opts.description) {
opts.accessibility = opts.accessibility || {};
opts.accessibility.description = opts.description;
error(32, false, chart, { 'axis.description': 'use axis.accessibility.description' });
}
});
}
/**
* @private
*/
function copyDeprecatedSeriesOptions(chart) {
// Map of deprecated series options. New options are defined as
// arrays of paths under series.options.
var oldToNewSeriesOptions = {
description: ['accessibility', 'description'],
exposeElementToA11y: ['accessibility', 'exposeAsGroupOnly'],
pointDescriptionFormatter: [
'accessibility', 'pointDescriptionFormatter'
],
skipKeyboardNavigation: [
'accessibility', 'keyboardNavigation', 'enabled'
]
};
chart.series.forEach(function (series) {
// Handle series wide options
Object.keys(oldToNewSeriesOptions).forEach(function (oldOption) {
var _a;
var optionVal = series.options[oldOption];
if (typeof optionVal !== 'undefined') {
// Set the new option
traverseSetOption(series.options, oldToNewSeriesOptions[oldOption],
// Note that skipKeyboardNavigation has inverted option
// value, since we set enabled rather than disabled
oldOption === 'skipKeyboardNavigation' ?
!optionVal : optionVal);
error(32, false, chart, (_a = {}, _a["series." + oldOption] = "series." + oldToNewSeriesOptions[oldOption].join('.'), _a));
}
});
});
}
/**
* @private
*/
function copyDeprecatedTopLevelAccessibilityOptions(chart) {
deprecateFromOptionsMap(chart, ['accessibility'], ['accessibility'], {
pointDateFormat: ['point', 'dateFormat'],
pointDateFormatter: ['point', 'dateFormatter'],
pointDescriptionFormatter: ['point', 'descriptionFormatter'],
pointDescriptionThreshold: ['series',
'pointDescriptionEnabledThreshold'],
pointNavigationThreshold: ['keyboardNavigation', 'seriesNavigation',
'pointNavigationEnabledThreshold'],
pointValueDecimals: ['point', 'valueDecimals'],
pointValuePrefix: ['point', 'valuePrefix'],
pointValueSuffix: ['point', 'valueSuffix'],
screenReaderSectionFormatter: ['screenReaderSection',
'beforeChartFormatter'],
describeSingleSeries: ['series', 'describeSingleSeries'],
seriesDescriptionFormatter: ['series', 'descriptionFormatter'],
onTableAnchorClick: ['screenReaderSection', 'onViewDataTableClick'],
axisRangeDateFormat: ['screenReaderSection', 'axisRangeDateFormat']
});
}
/**
* @private
*/
function copyDeprecatedKeyboardNavigationOptions(chart) {
deprecateFromOptionsMap(chart, ['accessibility', 'keyboardNavigation'], ['accessibility', 'keyboardNavigation', 'seriesNavigation'], {
skipNullPoints: ['skipNullPoints'],
mode: ['mode']
});
}
/**
* @private
*/
function copyDeprecatedLangOptions(chart) {
deprecateFromOptionsMap(chart, ['lang', 'accessibility'], ['lang', 'accessibility'], {
legendItem: ['legend', 'legendItem'],
legendLabel: ['legend', 'legendLabel'],
mapZoomIn: ['zoom', 'mapZoomIn'],
mapZoomOut: ['zoom', 'mapZoomOut'],
resetZoomButton: ['zoom', 'resetZoomButton'],
screenReaderRegionLabel: ['screenReaderSection',
'beforeRegionLabel'],
rangeSelectorButton: ['rangeSelector', 'buttonText'],
rangeSelectorMaxInput: ['rangeSelector', 'maxInputLabel'],
rangeSelectorMinInput: ['rangeSelector', 'minInputLabel'],
svgContainerEnd: ['screenReaderSection', 'endOfChartMarker'],
viewAsDataTable: ['table', 'viewAsDataTableButtonText'],
tableSummary: ['table', 'tableSummary']
});
}
/**
* Copy options that are deprecated over to new options. Logs warnings to
* console if deprecated options are used.
*
* @private
*/
function copyDeprecatedOptions(chart) {
copyDeprecatedChartOptions(chart);
copyDeprecatedAxisOptions(chart);
if (chart.series) {
copyDeprecatedSeriesOptions(chart);
}
copyDeprecatedTopLevelAccessibilityOptions(chart);
copyDeprecatedKeyboardNavigationOptions(chart);
copyDeprecatedLangOptions(chart);
}
export default copyDeprecatedOptions;
@@ -0,0 +1,338 @@
/* *
*
* (c) 2009-2020 Øystein Moseng
*
* Default lang/i18n options for accessibility.
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
var langOptions = {
/**
* Configure the accessibility strings in 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](https://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
*/
accessibility: {
defaultChartTitle: 'Chart',
chartContainerLabel: '{title}. Highcharts interactive chart.',
svgContainerLabel: 'Interactive chart',
drillUpButton: '{buttonText}',
credits: 'Chart credits: {creditsStr}',
/**
* Thousands separator to use when formatting numbers for screen
* readers. Note that many screen readers will not handle space as a
* thousands separator, and will consider "11 700" as two numbers.
*
* Set to `null` to use the separator defined in
* [lang.thousandsSep](lang.thousandsSep).
*
* @since 7.1.0
*/
thousandsSep: ',',
/**
* 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: '',
/**
* Set a label on the container wrapping the SVG.
*
* @see [chartContainerLabel](#lang.accessibility.chartContainerLabel)
*
* @since 8.0.0
*/
graphicContainerLabel: '',
/**
* Language options for the screen reader information sections added
* before and after the charts.
*
* @since 8.0.0
*/
screenReaderSection: {
beforeRegionLabel: 'Chart screen reader information.',
afterRegionLabel: '',
/**
* Language options for annotation descriptions.
*
* @since 8.0.1
*/
annotations: {
heading: 'Chart annotations summary',
descriptionSinglePoint: '{annotationText}. Related to {annotationPoint}',
descriptionMultiplePoints: '{annotationText}. Related to {annotationPoint}' +
'{ Also related to, #each(additionalAnnotationPoints)}',
descriptionNoPoints: '{annotationText}'
},
/**
* Label for the end of the chart. Announced by screen readers.
*
* @since 8.0.0
*/
endOfChartMarker: 'End of interactive chart.'
},
/**
* Language options for sonification.
*
* @since 8.0.1
*/
sonification: {
playAsSoundButtonText: 'Play as sound, {chartTitle}',
playAsSoundClickAnnouncement: 'Play'
},
/**
* Language options for accessibility of the legend.
*
* @since 8.0.0
*/
legend: {
legendLabel: 'Toggle series visibility',
legendItem: 'Hide {itemName}'
},
/**
* Chart and map zoom accessibility language options.
*
* @since 8.0.0
*/
zoom: {
mapZoomIn: 'Zoom chart',
mapZoomOut: 'Zoom out chart',
resetZoomButton: 'Reset zoom'
},
/**
* Range selector language options for accessibility.
*
* @since 8.0.0
*/
rangeSelector: {
minInputLabel: 'Select start date.',
maxInputLabel: 'Select end date.',
buttonText: 'Select range {buttonText}'
},
/**
* Accessibility language options for the data table.
*
* @since 8.0.0
*/
table: {
viewAsDataTableButtonText: 'View as data table, {chartTitle}',
tableSummary: 'Table representation of chart.'
},
/**
* Default announcement for new data in charts. If addPoint or
* addSeries is used, and only one series/point is added, the
* `newPointAnnounce` and `newSeriesAnnounce` strings are used.
* The `...Single` versions will be used if there is only one chart
* on the page, and the `...Multiple` versions will be used if there
* are multiple charts on the page. For all other new data events,
* the `newDataAnnounce` string will be used.
*
* @since 7.1.0
*/
announceNewData: {
newDataAnnounce: 'Updated data for chart {chartTitle}',
newSeriesAnnounceSingle: 'New data series: {seriesDesc}',
newPointAnnounceSingle: 'New data point: {pointDesc}',
newSeriesAnnounceMultiple: 'New data series in chart {chartTitle}: {seriesDesc}',
newPointAnnounceMultiple: 'New data point in chart {chartTitle}: {pointDesc}'
},
/**
* 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.'
},
/**
* Axis description format strings.
*
* @since 6.0.6
*/
axis: {
/* eslint-disable max-len */
xAxisDescriptionSingular: 'The chart has 1 X axis displaying {names[0]}. {ranges[0]}',
xAxisDescriptionPlural: 'The chart has {numAxes} X axes displaying {#each(names, -1) }and {names[-1]}.',
yAxisDescriptionSingular: 'The chart has 1 Y axis displaying {names[0]}. {ranges[0]}',
yAxisDescriptionPlural: 'The chart has {numAxes} Y axes displaying {#each(names, -1) }and {names[-1]}.',
timeRangeDays: 'Range: {range} days.',
timeRangeHours: 'Range: {range} hours.',
timeRangeMinutes: 'Range: {range} minutes.',
timeRangeSeconds: 'Range: {range} seconds.',
rangeFromTo: 'Range: {rangeFrom} to {rangeTo}.',
rangeCategories: 'Range: {numCategories} categories.'
},
/**
* Exporting menu format strings for accessibility module.
*
* @since 6.0.6
*/
exporting: {
chartMenuLabel: 'Chart menu',
menuButtonLabel: 'View chart menu',
exportRegionLabel: 'Chart 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)}.'
},
/**
* User supplied description text. This is added in the point
* comment description by default 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}',
/**
* Description for the value of null points.
*
* @since 8.0.0
*/
nullPointValue: 'No value',
/**
* Description for annotations on a point, as it is made available
* to assistive technology.
*
* @since 8.0.1
*/
pointAnnotationsDescription: '{Annotation: #each(annotations). }'
}
}
};
export default langOptions;
@@ -0,0 +1,793 @@
/* *
*
* (c) 2009-2020 Øystein Moseng
*
* Default options for accessibility.
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
/**
* Formatter callback for the accessibility announcement.
*
* @callback Highcharts.AccessibilityAnnouncementFormatter
*
* @param {Array<Highcharts.Series>} updatedSeries
* Array of all series that received updates. If an announcement is already
* queued, the series that received updates for that announcement are also
* included in this array.
*
* @param {Highcharts.Series} [addedSeries]
* This is provided if {@link Highcharts.Chart#addSeries} was called, and there
* is a new series. In that case, this argument is a reference to the new
* series.
*
* @param {Highcharts.Point} [addedPoint]
* This is provided if {@link Highcharts.Series#addPoint} was called, and there
* is a new point. In that case, this argument is a reference to the new point.
*
* @return {false|string}
* The function should return a string with the text to announce to the user.
* Return empty string to not announce anything. Return `false` to use the
* default announcement format.
*/
/**
* @interface Highcharts.PointAccessibilityOptionsObject
*/ /**
* Provide a description of the data point, announced to screen readers.
* @name Highcharts.PointAccessibilityOptionsObject#description
* @type {string|undefined}
* @requires modules/accessibility
* @since 7.1.0
*/
/* *
* @interface Highcharts.PointOptionsObject in parts/Point.ts
*/ /**
* @name Highcharts.PointOptionsObject#accessibility
* @type {Highcharts.PointAccessibilityOptionsObject|undefined}
* @requires modules/accessibility
* @since 7.1.0
*/
/**
* @callback Highcharts.ScreenReaderClickCallbackFunction
*
* @param {global.MouseEvent} evt
* Mouse click event
*
* @return {void}
*/
/**
* Creates a formatted string for the screen reader module.
*
* @callback Highcharts.ScreenReaderFormatterCallbackFunction<T>
*
* @param {T} context
* Context to format
*
* @return {string}
* Formatted string for the screen reader module.
*/
var options = {
/**
* 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](https://www.highcharts.com/docs/chart-concepts/accessibility).
*
* @since 5.0.0
* @requires modules/accessibility
* @optionparent accessibility
*/
accessibility: {
/**
* Enable accessibility functionality for the chart.
*
* @since 5.0.0
*/
enabled: true,
/**
* Accessibility options for the screen reader information sections
* added before and after the chart.
*
* @since 8.0.0
*/
screenReaderSection: {
/**
* 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 {Highcharts.ScreenReaderClickCallbackFunction}
* @since 8.0.0
* @apioption accessibility.screenReaderSection.onViewDataTableClick
*/
/**
* Function to run upon clicking the "Play as sound" button in
* the screen reader region.
*
* By default Highcharts will call the `chart.sonify` function.
*
* @type {Highcharts.ScreenReaderClickCallbackFunction}
* @since 8.0.1
* @apioption accessibility.screenReaderSection.onPlayAsSoundClick
*/
/**
* A formatter function to create the HTML contents of the hidden
* screen reader information region before the chart. Receives one
* argument, `chart`, referring to the chart object. Should return a
* string with the HTML content of the region. By default this
* returns an automatic description of the chart based on
* [beforeChartFormat](#accessibility.screenReaderSection.beforeChartFormat).
*
* @type {Highcharts.ScreenReaderFormatterCallbackFunction<Highcharts.Chart>}
* @since 8.0.0
* @apioption accessibility.screenReaderSection.beforeChartFormatter
*/
/**
* Format for the screen reader information region before the chart.
* Supported HTML tags are `<h1-7>`, `<p>`, `<div>`, `<a>`, `<ul>`,
* `<ol>`, `<li>`, and `<button>`. Attributes are not supported,
* except for id on `<div>`, `<a>`, and `<button>`. Id is required
* on `<a>` and `<button>` in the format `<tag id="abcd">`. Numbers,
* lower- and uppercase letters, "-" and "#" are valid characters in
* IDs.
*
* @since 8.0.0
*/
beforeChartFormat: '<h5>{chartTitle}</h5>' +
'<div>{typeDescription}</div>' +
'<div>{chartSubtitle}</div>' +
'<div>{chartLongdesc}</div>' +
'<div>{playAsSoundButton}</div>' +
'<div>{viewTableButton}</div>' +
'<div>{xAxisDescription}</div>' +
'<div>{yAxisDescription}</div>' +
'<div>{annotationsTitle}{annotationsList}</div>',
/**
* A formatter function to create the HTML contents of the hidden
* screen reader information region after the chart. Analogous to
* [beforeChartFormatter](#accessibility.screenReaderSection.beforeChartFormatter).
*
* @type {Highcharts.ScreenReaderFormatterCallbackFunction<Highcharts.Chart>}
* @since 8.0.0
* @apioption accessibility.screenReaderSection.afterChartFormatter
*/
/**
* Format for the screen reader information region after the chart.
* Analogous to [beforeChartFormat](#accessibility.screenReaderSection.beforeChartFormat).
*
* @since 8.0.0
*/
afterChartFormat: '{endOfChartMarker}',
/**
* Date format to use to describe range of datetime axes.
*
* For an overview of the replacement codes, see
* [dateFormat](/class-reference/Highcharts#dateFormat).
*
* @see [point.dateFormat](#accessibility.point.dateFormat)
*
* @since 8.0.0
*/
axisRangeDateFormat: '%Y-%m-%d %H:%M:%S'
},
/**
* Accessibility options global to all data series. Individual series
* can also have specific [accessibility options](#plotOptions.series.accessibility)
* set.
*
* @since 8.0.0
*/
series: {
/**
* 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. If `false` is returned,
* the default formatter will be used for that series.
*
* @see [series.description](#plotOptions.series.description)
*
* @type {Highcharts.ScreenReaderFormatterCallbackFunction<Highcharts.Series>}
* @since 8.0.0
* @apioption accessibility.series.descriptionFormatter
*/
/**
* Whether or not to add series descriptions to charts with a single
* series.
*
* @since 8.0.0
*/
describeSingleSeries: false,
/**
* 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 {boolean|number}
* @since 8.0.0
*/
pointDescriptionEnabledThreshold: 200
},
/**
* Options for descriptions of individual data points.
*
* @since 8.0.0
*/
point: {
/**
* 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 [dateFormatter](#accessibility.point.dateFormatter)
*
* @type {string}
* @since 8.0.0
* @apioption accessibility.point.dateFormat
*/
/**
* 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 [dateFormat](#accessibility.point.dateFormat)
*
* @type {Highcharts.ScreenReaderFormatterCallbackFunction<Highcharts.Point>}
* @since 8.0.0
* @apioption accessibility.point.dateFormatter
*/
/**
* Prefix to add to the values in the point descriptions. Uses
* [tooltip.valuePrefix](#tooltip.valuePrefix) if not defined.
*
* @type {string}
* @since 8.0.0
* @apioption accessibility.point.valuePrefix
*/
/**
* Suffix to add to the values in the point descriptions. Uses
* [tooltip.valueSuffix](#tooltip.valueSuffix) if not defined.
*
* @type {string}
* @since 8.0.0
* @apioption accessibility.point.valueSuffix
*/
/**
* Decimals to use for the values in the point descriptions. Uses
* [tooltip.valueDecimals](#tooltip.valueDecimals) if not defined.
*
* @type {number}
* @since 8.0.0
* @apioption accessibility.point.valueDecimals
*/
/**
* 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. If `false` is returned, the
* default formatter will be used for that point.
*
* Note: Prefer using [accessibility.point.valueDescriptionFormat](#accessibility.point.valueDescriptionFormat)
* instead if possible, as default functionality such as describing
* annotations will be preserved.
*
* @see [accessibility.point.valueDescriptionFormat](#accessibility.point.valueDescriptionFormat)
* @see [point.accessibility.description](#series.line.data.accessibility.description)
*
* @type {Highcharts.ScreenReaderFormatterCallbackFunction<Highcharts.Point>}
* @since 8.0.0
* @apioption accessibility.point.descriptionFormatter
*/
/**
* Format to use for describing the values of data points
* to assistive technology - including screen readers.
* The point context is available as `{point}`.
*
* Additionally, the series name, annotation info, and
* description added in `point.accessibility.description`
* is added by default if relevant. To override this, use the
* [accessibility.point.descriptionFormatter](#accessibility.point.descriptionFormatter)
* option.
*
* @see [point.accessibility.description](#series.line.data.accessibility.description)
* @see [accessibility.point.descriptionFormatter](#accessibility.point.descriptionFormatter)
*
* @type {string}
* @since 8.0.1
*/
valueDescriptionFormat: '{index}. {xDescription}{separator}{value}.'
},
/**
* Amount of landmarks/regions to create for screen reader users. More
* landmarks can make navigation with screen readers easier, but can
* be distracting if there are lots of charts on the page. Three modes
* are available:
* - `all`: Adds regions for all series, legend, menu, information
* region.
* - `one`: Adds a single landmark per chart.
* - `disabled`: No landmarks are added.
*
* @since 7.1.0
* @validvalue ["all", "one", "disabled"]
*/
landmarkVerbosity: 'all',
/**
* Link the chart to an HTML element describing the contents of the
* chart.
*
* It is always recommended to describe charts using visible text, to
* improve SEO as well as accessibility for users with disabilities.
* This option lets an HTML element with a description be linked to the
* chart, so that screen reader users can connect the two.
*
* By setting this option to a string, Highcharts runs the string as an
* HTML selector query on the entire document. If there is only a single
* match, this element is linked to the chart. The content of the linked
* element will be included in the chart description for screen reader
* users.
*
* By default, the chart looks for an adjacent sibling element with the
* `highcharts-description` class.
*
* The feature can be disabled by setting the option to an empty string,
* or overridden by providing the
* [accessibility.description](#accessibility.description) option.
* Alternatively, the HTML element to link can be passed in directly as
* an HTML node.
*
* If you need the description to be part of the exported image,
* consider using the [caption](#caption) feature.
*
* If you need the description to be hidden visually, use the
* [accessibility.description](#accessibility.description) option.
*
* @see [caption](#caption)
* @see [description](#accessibility.description)
* @see [typeDescription](#accessibility.typeDescription)
*
* @sample highcharts/accessibility/accessible-line
* Accessible line chart
*
* @type {string|Highcharts.HTMLDOMElement}
* @since 8.0.0
*/
linkedDescription: '*[data-highcharts-chart="{index}"] + .highcharts-description',
/**
* A hook for adding custom components to the accessibility module.
* Should be an object mapping component names to instances of classes
* inheriting from the Highcharts.AccessibilityComponent base class.
* Remember to add the component to the
* [keyboardNavigation.order](#accessibility.keyboardNavigation.order)
* for the keyboard navigation to be usable.
*
* @sample highcharts/accessibility/custom-component
* Custom accessibility component
*
* @type {*}
* @since 7.1.0
* @apioption accessibility.customComponents
*/
/**
* Theme to apply to the chart when Windows High Contrast Mode is
* detected. By default, a high contrast theme matching the high
* contrast system system colors is used.
*
* @type {*}
* @since 7.1.3
* @apioption accessibility.highContrastTheme
*/
/**
* A text description of the chart.
*
* **Note: Prefer using [linkedDescription](#accessibility.linkedDescription)
* or [caption](#caption.text) instead.**
*
* If the Accessibility module is loaded, this option is included by
* default as a long description of the chart in the hidden screen
* reader information region.
*
* Note: Since Highcharts now supports captions and linked descriptions,
* it is preferred to define the description using those methods, as a
* visible caption/description benefits all users. If the
* `accessibility.description` option is defined, the linked description
* is ignored, and the caption is hidden from screen reader users.
*
* @see [linkedDescription](#accessibility.linkedDescription)
* @see [caption](#caption)
* @see [typeDescription](#accessibility.typeDescription)
*
* @type {string}
* @since 5.0.0
* @apioption accessibility.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 accessibility.typeDescription
*/
/**
* Options for keyboard navigation.
*
* @declare Highcharts.KeyboardNavigationOptionsObject
* @since 5.0.0
*/
keyboardNavigation: {
/**
* Enable keyboard navigation for the chart.
*
* @since 5.0.0
*/
enabled: true,
/**
* Options for the focus border drawn around elements while
* navigating through them.
*
* @sample highcharts/accessibility/custom-focus
* Custom focus ring
*
* @declare Highcharts.KeyboardNavigationFocusBorderOptionsObject
* @since 6.0.3
*/
focusBorder: {
/**
* Enable/disable focus border for chart.
*
* @since 6.0.3
*/
enabled: true,
/**
* Hide the browser's default focus indicator.
*
* @since 6.0.4
*/
hideBrowserFocusOutline: true,
/**
* Style options for the focus border drawn around elements
* while navigating through them. Note that some browsers in
* addition draw their own borders for focused elements. These
* automatic borders can not be styled by Highcharts.
*
* In styled mode, the border is given the
* `.highcharts-focus-border` class.
*
* @type {Highcharts.CSSObject}
* @since 6.0.3
*/
style: {
/** @internal */
color: '#335cad',
/** @internal */
lineWidth: 2,
/** @internal */
borderRadius: 3
},
/**
* Focus border margin around the elements.
*
* @since 6.0.3
*/
margin: 2
},
/**
* Order of tab navigation in the chart. Determines which elements
* are tabbed to first. Available elements are: `series`, `zoom`,
* `rangeSelector`, `chartMenu`, `legend`. In addition, any custom
* components can be added here.
*
* @type {Array<string>}
* @since 7.1.0
*/
order: ['series', 'zoom', 'rangeSelector', 'legend', 'chartMenu'],
/**
* Whether or not to wrap around when reaching the end of arrow-key
* navigation for an element in the chart.
* @since 7.1.0
*/
wrapAround: true,
/**
* Options for the keyboard navigation of data points and series.
*
* @declare Highcharts.KeyboardNavigationSeriesNavigationOptionsObject
* @since 8.0.0
*/
seriesNavigation: {
/**
* Set the keyboard navigation mode for the chart. Can be
* "normal" or "serialize". In normal mode, left/right arrow
* keys move between points in a series, while up/down arrow
* keys move between series. Up/down navigation acts
* intelligently to figure out which series makes sense to move
* to from any given point.
*
* In "serialize" mode, points are instead navigated as a single
* list. Left/right behaves as in "normal" mode. Up/down arrow
* keys will behave like left/right. This can be useful for
* unifying navigation behavior with/without screen readers
* enabled.
*
* @type {string}
* @default normal
* @since 8.0.0
* @validvalue ["normal", "serialize"]
* @apioption accessibility.keyboardNavigation.seriesNavigation.mode
*/
/**
* Skip null points when navigating through points with the
* keyboard.
*
* @since 8.0.0
*/
skipNullPoints: true,
/**
* When a series contains more points than this, we no longer
* allow keyboard navigation for it.
*
* Set to `false` to disable.
*
* @type {boolean|number}
* @since 8.0.0
*/
pointNavigationEnabledThreshold: false
}
},
/**
* Options for announcing new data to screen reader users. Useful
* for dynamic data applications and drilldown.
*
* Keep in mind that frequent announcements will not be useful to
* users, as they won't have time to explore the new data. For these
* applications, consider making snapshots of the data accessible, and
* do the announcements in batches.
*
* @declare Highcharts.AccessibilityAnnounceNewDataOptionsObject
* @since 7.1.0
*/
announceNewData: {
/**
* Optional formatter callback for the announcement. Receives
* up to three arguments. The first argument is always an array
* of all series that received updates. If an announcement is
* already queued, the series that received updates for that
* announcement are also included in this array. The second
* argument is provided if `chart.addSeries` was called, and
* there is a new series. In that case, this argument is a
* reference to the new series. The third argument, similarly,
* is provided if `series.addPoint` was called, and there is a
* new point. In that case, this argument is a reference to the
* new point.
*
* The function should return a string with the text to announce
* to the user. Return empty string to not announce anything.
* Return `false` to use the default announcement format.
*
* @sample highcharts/accessibility/custom-dynamic
* High priority live alerts
*
* @type {Highcharts.AccessibilityAnnouncementFormatter}
* @apioption accessibility.announceNewData.announcementFormatter
*/
/**
* Enable announcing new data to screen reader users
* @sample highcharts/accessibility/accessible-dynamic
* Dynamic data accessible
*/
enabled: false,
/**
* Minimum interval between announcements in milliseconds. If
* new data arrives before this amount of time has passed, it is
* queued for announcement. If another new data event happens
* while an announcement is queued, the queued announcement is
* dropped, and the latest announcement is queued instead. Set
* to 0 to allow all announcements, but be warned that frequent
* announcements are disturbing to users.
*/
minAnnounceInterval: 5000,
/**
* Choose whether or not the announcements should interrupt the
* screen reader. If not enabled, the user will be notified once
* idle. It is recommended not to enable this setting unless
* there is a specific reason to do so.
*/
interruptUser: false
}
},
/**
* Accessibility options for a data point.
*
* @declare Highcharts.PointAccessibilityOptionsObject
* @since 7.1.0
* @apioption series.line.data.accessibility
*/
/**
* Provide a description of the data point, announced to screen readers.
*
* @type {string}
* @since 7.1.0
* @apioption series.line.data.accessibility.description
*/
/**
* Accessibility options for a series.
*
* @declare Highcharts.SeriesAccessibilityOptionsObject
* @since 7.1.0
* @requires modules/accessibility
* @apioption plotOptions.series.accessibility
*/
/**
* Enable/disable accessibility functionality for a specific series.
*
* @type {boolean}
* @since 7.1.0
* @apioption plotOptions.series.accessibility.enabled
*/
/**
* Provide a description of the series, announced to screen readers.
*
* @type {string}
* @since 7.1.0
* @apioption plotOptions.series.accessibility.description
*/
/**
* Formatter function to use instead of the default for point
* descriptions. Same as `accessibility.point.descriptionFormatter`, but for
* a single series.
*
* @see [accessibility.point.descriptionFormatter](#accessibility.point.descriptionFormatter)
*
* @type {Highcharts.ScreenReaderFormatterCallbackFunction<Highcharts.Point>}
* @since 7.1.0
* @apioption plotOptions.series.accessibility.pointDescriptionFormatter
*/
/**
* Expose only the series element to screen readers, not its points.
*
* @type {boolean}
* @since 7.1.0
* @apioption plotOptions.series.accessibility.exposeAsGroupOnly
*/
/**
* Keyboard navigation for a series
*
* @declare Highcharts.SeriesAccessibilityKeyboardNavigationOptionsObject
* @since 7.1.0
* @apioption plotOptions.series.accessibility.keyboardNavigation
*/
/**
* Enable/disable keyboard navigation support for a specific series.
*
* @type {boolean}
* @since 7.1.0
* @apioption plotOptions.series.accessibility.keyboardNavigation.enabled
*/
/**
* Accessibility options for an annotation label.
*
* @declare Highcharts.AnnotationLabelAccessibilityOptionsObject
* @since 8.0.1
* @requires modules/accessibility
* @apioption annotations.labelOptions.accessibility
*/
/**
* Description of an annotation label for screen readers and other assistive
* technology.
*
* @type {string}
* @since 8.0.1
* @apioption annotations.labelOptions.accessibility.description
*/
/**
* Accessibility options for an axis. Requires the accessibility module.
*
* @declare Highcharts.AxisAccessibilityOptionsObject
* @since 7.1.0
* @requires modules/accessibility
* @apioption xAxis.accessibility
*/
/**
* Enable axis accessibility features, including axis information in the
* screen reader information region. If this is disabled on the xAxis, the
* x values are not exposed to screen readers for the individual data points
* by default.
*
* @type {boolean}
* @since 7.1.0
* @apioption xAxis.accessibility.enabled
*/
/**
* Description for an axis to expose to screen reader users.
*
* @type {string}
* @since 7.1.0
* @apioption xAxis.accessibility.description
*/
/**
* Range description for an axis. Overrides the default range description.
* Set to empty to disable range description for this axis.
*
* @type {string}
* @since 7.1.0
* @apioption xAxis.accessibility.rangeDescription
*/
/**
* @optionparent legend
*/
legend: {
/**
* Accessibility options for the legend. Requires the Accessibility
* module.
*
* @since 7.1.0
* @requires modules/accessibility
*/
accessibility: {
/**
* Enable accessibility support for the legend.
*
* @since 7.1.0
*/
enabled: true,
/**
* Options for keyboard navigation for the legend.
*
* @since 7.1.0
* @requires modules/accessibility
*/
keyboardNavigation: {
/**
* Enable keyboard navigation for the legend.
*
* @see [accessibility.keyboardNavigation](#accessibility.keyboardNavigation.enabled)
*
* @since 7.1.0
*/
enabled: true
}
}
},
/**
* @optionparent exporting
*/
exporting: {
/**
* Accessibility options for the exporting menu. Requires the
* Accessibility module.
*
* @since 7.1.0
* @requires modules/accessibility
*/
accessibility: {
/**
* Enable accessibility support for the export menu.
*
* @since 7.1.0
*/
enabled: true
}
}
};
export default options;
@@ -0,0 +1,51 @@
/* *
*
* (c) 2009-2020 Øystein Moseng
*
* Create announcer to speak messages to screen readers and other AT.
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import H from '../../Core/Globals.js';
import DOMElementProvider from './DOMElementProvider.js';
import HTMLUtilities from './HTMLUtilities.js';
var visuallyHideElement = HTMLUtilities.visuallyHideElement;
var Announcer = /** @class */ (function () {
function Announcer(chart, type) {
this.chart = chart;
this.domElementProvider = new DOMElementProvider();
this.announceRegion = this.addAnnounceRegion(type);
}
Announcer.prototype.destroy = function () {
this.domElementProvider.destroyCreatedElements();
};
Announcer.prototype.announce = function (message) {
var _this = this;
this.announceRegion.innerHTML = message;
// Delete contents after a little while to avoid user finding the live
// region in the DOM.
if (this.clearAnnouncementRegionTimer) {
clearTimeout(this.clearAnnouncementRegionTimer);
}
this.clearAnnouncementRegionTimer = setTimeout(function () {
_this.announceRegion.innerHTML = '';
delete _this.clearAnnouncementRegionTimer;
}, 1000);
};
Announcer.prototype.addAnnounceRegion = function (type) {
var chartContainer = this.chart.renderTo;
var div = this.domElementProvider.createElement('div');
div.setAttribute('aria-hidden', false);
div.setAttribute('aria-live', type);
visuallyHideElement(div);
chartContainer.insertBefore(div, chartContainer.firstChild);
return div;
};
return Announcer;
}());
H.Announcer = Announcer;
export default Announcer;
@@ -0,0 +1,187 @@
/* *
*
* (c) 2009-2020 Øystein Moseng
*
* Utils for dealing with charts.
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import HTMLUtilities from './HTMLUtilities.js';
var stripHTMLTags = HTMLUtilities.stripHTMLTagsFromString;
import U from '../../Core/Utilities.js';
var defined = U.defined, find = U.find, fireEvent = U.fireEvent;
/* eslint-disable valid-jsdoc */
/**
* @return {string}
*/
function getChartTitle(chart) {
return stripHTMLTags(chart.options.title.text ||
chart.langFormat('accessibility.defaultChartTitle', { chart: chart }));
}
/**
* @param {Highcharts.Axis} axis
* @return {string}
*/
function getAxisDescription(axis) {
return stripHTMLTags(axis && (axis.userOptions && axis.userOptions.accessibility &&
axis.userOptions.accessibility.description ||
axis.axisTitle && axis.axisTitle.textStr ||
axis.options.id ||
axis.categories && 'categories' ||
axis.dateTime && 'Time' ||
'values'));
}
/**
* Get the DOM element for the first point in the series.
* @private
* @param {Highcharts.Series} series
* The series to get element for.
* @return {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement|undefined}
* The DOM element for the point.
*/
function getSeriesFirstPointElement(series) {
if (series.points &&
series.points.length &&
series.points[0].graphic) {
return series.points[0].graphic.element;
}
}
/**
* Get the DOM element for the series that we put accessibility info on.
* @private
* @param {Highcharts.Series} series
* The series to get element for.
* @return {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement|undefined}
* The DOM element for the series
*/
function getSeriesA11yElement(series) {
var firstPointEl = getSeriesFirstPointElement(series);
return (firstPointEl &&
firstPointEl.parentNode || series.graph &&
series.graph.element || series.group &&
series.group.element); // Could be tracker series depending on series type
}
/**
* Remove aria-hidden from element. Also unhides parents of the element, and
* hides siblings that are not explicitly unhidden.
* @private
* @param {Highcharts.Chart} chart
* @param {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement} element
*/
function unhideChartElementFromAT(chart, element) {
element.setAttribute('aria-hidden', false);
if (element === chart.renderTo || !element.parentNode) {
return;
}
// Hide siblings unless their hidden state is already explicitly set
Array.prototype.forEach.call(element.parentNode.childNodes, function (node) {
if (!node.hasAttribute('aria-hidden')) {
node.setAttribute('aria-hidden', true);
}
});
// Repeat for parent
unhideChartElementFromAT(chart, element.parentNode);
}
/**
* Hide series from screen readers.
* @private
* @param {Highcharts.Series} series
* The series to hide
* @return {void}
*/
function hideSeriesFromAT(series) {
var seriesEl = getSeriesA11yElement(series);
if (seriesEl) {
seriesEl.setAttribute('aria-hidden', true);
}
}
/**
* Get series objects by series name.
* @private
* @param {Highcharts.Chart} chart
* @param {string} name
* @return {Array<Highcharts.Series>}
*/
function getSeriesFromName(chart, name) {
if (!name) {
return chart.series;
}
return (chart.series || []).filter(function (s) {
return s.name === name;
});
}
/**
* Get point in a series from x/y values.
* @private
* @param {Array<Highcharts.Series>} series
* @param {number} x
* @param {number} y
* @return {Highcharts.Point|undefined}
*/
function getPointFromXY(series, x, y) {
var i = series.length, res;
while (i--) {
res = find(series[i].points || [], function (p) {
return p.x === x && p.y === y;
});
if (res) {
return res;
}
}
}
/**
* Get relative position of point on an x/y axis from 0 to 1.
* @private
* @param {Highcharts.Axis} axis
* @param {Highcharts.Point} point
* @return {number}
*/
function getRelativePointAxisPosition(axis, point) {
if (!defined(axis.dataMin) || !defined(axis.dataMax)) {
return 0;
}
var axisStart = axis.toPixels(axis.dataMin);
var axisEnd = axis.toPixels(axis.dataMax);
// We have to use pixel position because of axis breaks, log axis etc.
var positionProp = axis.coll === 'xAxis' ? 'x' : 'y';
var pointPos = axis.toPixels(point[positionProp] || 0);
return (pointPos - axisStart) / (axisEnd - axisStart);
}
/**
* Get relative position of point on an x/y axis from 0 to 1.
* @private
* @param {Highcharts.Point} point
*/
function scrollToPoint(point) {
var xAxis = point.series.xAxis;
var yAxis = point.series.yAxis;
var axis = (xAxis === null || xAxis === void 0 ? void 0 : xAxis.scrollbar) ? xAxis : yAxis;
var scrollbar = axis === null || axis === void 0 ? void 0 : axis.scrollbar;
if (scrollbar && defined(scrollbar.to) && defined(scrollbar.from)) {
var range = scrollbar.to - scrollbar.from;
var pos = getRelativePointAxisPosition(axis, point);
scrollbar.updatePosition(pos - range / 2, pos + range / 2);
fireEvent(scrollbar, 'changed', {
from: scrollbar.from,
to: scrollbar.to,
trigger: 'scrollbar',
DOMEvent: null
});
}
}
var ChartUtilities = {
getChartTitle: getChartTitle,
getAxisDescription: getAxisDescription,
getPointFromXY: getPointFromXY,
getSeriesFirstPointElement: getSeriesFirstPointElement,
getSeriesFromName: getSeriesFromName,
getSeriesA11yElement: getSeriesA11yElement,
unhideChartElementFromAT: unhideChartElementFromAT,
hideSeriesFromAT: hideSeriesFromAT,
scrollToPoint: scrollToPoint
};
export default ChartUtilities;
@@ -0,0 +1,50 @@
/* *
*
* (c) 2009-2020 Øystein Moseng
*
* Class that can keep track of elements added to DOM and clean them up on
* destroy.
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import H from '../../Core/Globals.js';
var doc = H.win.document;
import U from '../../Core/Utilities.js';
var extend = U.extend;
import HTMLUtilities from './HTMLUtilities.js';
var removeElement = HTMLUtilities.removeElement;
/* eslint-disable no-invalid-this, valid-jsdoc */
/**
* @private
* @class
*/
var DOMElementProvider = function () {
this.elements = [];
};
extend(DOMElementProvider.prototype, {
/**
* Create an element and keep track of it for later removal.
* Same args as document.createElement
* @private
*/
createElement: function () {
var el = doc.createElement.apply(doc, arguments);
this.elements.push(el);
return el;
},
/**
* Destroy all created elements, removing them from the DOM.
* @private
*/
destroyCreatedElements: function () {
this.elements.forEach(function (element) {
removeElement(element);
});
this.elements = [];
}
});
export default DOMElementProvider;
@@ -0,0 +1,48 @@
/* *
*
* (c) 2009-2020 Øystein Moseng
*
* Class that can keep track of events added, and clean them up on destroy.
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import H from '../../Core/Globals.js';
import U from '../../Core/Utilities.js';
var addEvent = U.addEvent, extend = U.extend;
/* eslint-disable no-invalid-this, valid-jsdoc */
/**
* @private
* @class
*/
var EventProvider = function () {
this.eventRemovers = [];
};
extend(EventProvider.prototype, {
/**
* Add an event to an element and keep track of it for later removal.
* Same args as Highcharts.addEvent.
* @private
* @return {Function}
*/
addEvent: function () {
var remover = addEvent.apply(H, arguments);
this.eventRemovers.push(remover);
return remover;
},
/**
* Remove all added events.
* @private
* @return {void}
*/
removeAddedEvents: function () {
this.eventRemovers.forEach(function (remover) {
remover();
});
this.eventRemovers = [];
}
});
export default EventProvider;
@@ -0,0 +1,173 @@
/* *
*
* (c) 2009-2020 Øystein Moseng
*
* Utility functions for accessibility module.
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import U from '../../Core/Utilities.js';
var merge = U.merge;
import H from '../../Core/Globals.js';
var win = H.win, doc = win.document;
/* eslint-disable valid-jsdoc */
/**
* @private
* @param {Highcharts.HTMLDOMElement} el
* @param {string} className
* @return {void}
*/
function addClass(el, className) {
if (el.classList) {
el.classList.add(className);
}
else if (el.className.indexOf(className) < 0) {
// Note: Dumb check for class name exists, should be fine for practical
// use cases, but will return false positives if the element has a class
// that contains the className.
el.className += className;
}
}
/**
* @private
* @param {string} str
* @return {string}
*/
function escapeStringForHTML(str) {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;')
.replace(/\//g, '&#x2F;');
}
/**
* Get an element by ID
* @param {string} id
* @private
* @return {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement|null}
*/
function getElement(id) {
return doc.getElementById(id);
}
/**
* Get a fake mouse event of a given type
* @param {string} type
* @private
* @return {global.MouseEvent}
*/
function getFakeMouseEvent(type) {
if (typeof win.MouseEvent === 'function') {
return new win.MouseEvent(type);
}
// No MouseEvent support, try using initMouseEvent
if (doc.createEvent) {
var evt = doc.createEvent('MouseEvent');
if (evt.initMouseEvent) {
evt.initMouseEvent(type, true, // Bubble
true, // Cancel
win, // View
type === 'click' ? 1 : 0, // Detail
// Coords
0, 0, 0, 0,
// Pressed keys
false, false, false, false, 0, // button
null // related target
);
return evt;
}
}
return { type: type };
}
/**
* Remove an element from the DOM.
* @private
* @param {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement} [element]
* @return {void}
*/
function removeElement(element) {
if (element && element.parentNode) {
element.parentNode.removeChild(element);
}
}
/**
* Utility function. Reverses child nodes of a DOM element.
* @private
* @param {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement} node
* @return {void}
*/
function reverseChildNodes(node) {
var i = node.childNodes.length;
while (i--) {
node.appendChild(node.childNodes[i]);
}
}
/**
* Set attributes on element. Set to null to remove attribute.
* @private
* @param {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement} el
* @param {Highcharts.HTMLAttributes|Highcharts.SVGAttributes} attrs
* @return {void}
*/
function setElAttrs(el, attrs) {
Object.keys(attrs).forEach(function (attr) {
var val = attrs[attr];
if (val === null) {
el.removeAttribute(attr);
}
else {
var cleanedVal = escapeStringForHTML('' + val);
el.setAttribute(attr, cleanedVal);
}
});
}
/**
* Used for aria-label attributes, painting on a canvas will fail if the
* text contains tags.
* @private
* @param {string} str
* @return {string}
*/
function stripHTMLTagsFromString(str) {
return typeof str === 'string' ?
str.replace(/<\/?[^>]+(>|$)/g, '') : str;
}
/**
* Utility function for hiding an element visually, but still keeping it
* available to screen reader users.
* @private
* @param {Highcharts.HTMLDOMElement} element
* @return {void}
*/
function visuallyHideElement(element) {
var hiddenStyle = {
position: 'absolute',
width: '1px',
height: '1px',
overflow: 'hidden',
whiteSpace: 'nowrap',
clip: 'rect(1px, 1px, 1px, 1px)',
marginTop: '-3px',
'-ms-filter': 'progid:DXImageTransform.Microsoft.Alpha(Opacity=1)',
filter: 'alpha(opacity=1)',
opacity: '0.01'
};
merge(true, element.style, hiddenStyle);
}
var HTMLUtilities = {
addClass: addClass,
escapeStringForHTML: escapeStringForHTML,
getElement: getElement,
getFakeMouseEvent: getFakeMouseEvent,
removeElement: removeElement,
reverseChildNodes: reverseChildNodes,
setElAttrs: setElAttrs,
stripHTMLTagsFromString: stripHTMLTagsFromString,
visuallyHideElement: visuallyHideElement
};
export default HTMLUtilities;
File diff suppressed because it is too large Load Diff
+611
View File
@@ -0,0 +1,611 @@
/* *
*
* (c) 2010-2020 Torstein Honsi
*
* Extenstion for 3d axes
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import H from '../Globals.js';
import Math3D from '../../Extensions/Math3D.js';
var perspective = Math3D.perspective, perspective3D = Math3D.perspective3D, shapeArea = Math3D.shapeArea;
import Tick from './Tick.js';
import Tick3D from './Tick3D.js';
import U from '../Utilities.js';
var addEvent = U.addEvent, merge = U.merge, pick = U.pick, wrap = U.wrap;
var deg2rad = H.deg2rad;
/* eslint-disable valid-jsdoc */
/**
* Adds 3D support to axes.
* @private
* @class
*/
var Axis3DAdditions = /** @class */ (function () {
/* *
*
* Constructors
*
* */
/**
* @private
*/
function Axis3DAdditions(axis) {
this.axis = axis;
}
/* *
*
* Functions
*
* */
/**
* @private
* @param {Highcharts.Axis} axis
* Related axis.
* @param {Highcharts.Position3dObject} pos
* Position to fix.
* @param {boolean} [isTitle]
* Whether this is a title position.
* @return {Highcharts.Position3dObject}
* Fixed position.
*/
Axis3DAdditions.prototype.fix3dPosition = function (pos, isTitle) {
var axis3D = this;
var axis = axis3D.axis;
var chart = axis.chart;
// Do not do this if the chart is not 3D
if (axis.coll === 'colorAxis' ||
!chart.chart3d ||
!chart.is3d()) {
return pos;
}
var alpha = deg2rad * chart.options.chart.options3d.alpha, beta = deg2rad * chart.options.chart.options3d.beta, positionMode = pick(isTitle && axis.options.title.position3d, axis.options.labels.position3d), skew = pick(isTitle && axis.options.title.skew3d, axis.options.labels.skew3d), frame = chart.chart3d.frame3d, plotLeft = chart.plotLeft, plotRight = chart.plotWidth + plotLeft, plotTop = chart.plotTop, plotBottom = chart.plotHeight + plotTop,
// Indicates that we are labelling an X or Z axis on the "back" of
// the chart
reverseFlap = false, offsetX = 0, offsetY = 0, vecX, vecY = { x: 0, y: 1, z: 0 };
pos = axis.axis3D.swapZ({ x: pos.x, y: pos.y, z: 0 });
if (axis.isZAxis) { // Z Axis
if (axis.opposite) {
if (frame.axes.z.top === null) {
return {};
}
offsetY = pos.y - plotTop;
pos.x = frame.axes.z.top.x;
pos.y = frame.axes.z.top.y;
vecX = frame.axes.z.top.xDir;
reverseFlap = !frame.top.frontFacing;
}
else {
if (frame.axes.z.bottom === null) {
return {};
}
offsetY = pos.y - plotBottom;
pos.x = frame.axes.z.bottom.x;
pos.y = frame.axes.z.bottom.y;
vecX = frame.axes.z.bottom.xDir;
reverseFlap = !frame.bottom.frontFacing;
}
}
else if (axis.horiz) { // X Axis
if (axis.opposite) {
if (frame.axes.x.top === null) {
return {};
}
offsetY = pos.y - plotTop;
pos.y = frame.axes.x.top.y;
pos.z = frame.axes.x.top.z;
vecX = frame.axes.x.top.xDir;
reverseFlap = !frame.top.frontFacing;
}
else {
if (frame.axes.x.bottom === null) {
return {};
}
offsetY = pos.y - plotBottom;
pos.y = frame.axes.x.bottom.y;
pos.z = frame.axes.x.bottom.z;
vecX = frame.axes.x.bottom.xDir;
reverseFlap = !frame.bottom.frontFacing;
}
}
else { // Y Axis
if (axis.opposite) {
if (frame.axes.y.right === null) {
return {};
}
offsetX = pos.x - plotRight;
pos.x = frame.axes.y.right.x;
pos.z = frame.axes.y.right.z;
vecX = frame.axes.y.right.xDir;
// Rotate 90º on opposite edge
vecX = { x: vecX.z, y: vecX.y, z: -vecX.x };
}
else {
if (frame.axes.y.left === null) {
return {};
}
offsetX = pos.x - plotLeft;
pos.x = frame.axes.y.left.x;
pos.z = frame.axes.y.left.z;
vecX = frame.axes.y.left.xDir;
}
}
if (positionMode === 'chart') {
// Labels preserve their direction relative to the chart
// nothing to do
}
else if (positionMode === 'flap') {
// Labels are rotated around the axis direction to face the screen
if (!axis.horiz) { // Y Axis
vecX = { x: Math.cos(beta), y: 0, z: Math.sin(beta) };
}
else { // X and Z Axis
var sin = Math.sin(alpha);
var cos = Math.cos(alpha);
if (axis.opposite) {
sin = -sin;
}
if (reverseFlap) {
sin = -sin;
}
vecY = { x: vecX.z * sin, y: cos, z: -vecX.x * sin };
}
}
else if (positionMode === 'ortho') {
// Labels will be rotated to be ortogonal to the axis
if (!axis.horiz) { // Y Axis
vecX = { x: Math.cos(beta), y: 0, z: Math.sin(beta) };
}
else { // X and Z Axis
var sina = Math.sin(alpha);
var cosa = Math.cos(alpha);
var sinb = Math.sin(beta);
var cosb = Math.cos(beta);
var vecZ = { x: sinb * cosa, y: -sina, z: -cosa * cosb };
vecY = {
x: vecX.y * vecZ.z - vecX.z * vecZ.y,
y: vecX.z * vecZ.x - vecX.x * vecZ.z,
z: vecX.x * vecZ.y - vecX.y * vecZ.x
};
var scale = 1 / Math.sqrt(vecY.x * vecY.x + vecY.y * vecY.y + vecY.z * vecY.z);
if (reverseFlap) {
scale = -scale;
}
vecY = { x: scale * vecY.x, y: scale * vecY.y, z: scale * vecY.z };
}
}
else { // positionMode == 'offset'
// Labels will be skewd to maintain vertical / horizontal offsets
// from axis
if (!axis.horiz) { // Y Axis
vecX = { x: Math.cos(beta), y: 0, z: Math.sin(beta) };
}
else { // X and Z Axis
vecY = {
x: Math.sin(beta) * Math.sin(alpha),
y: Math.cos(alpha),
z: -Math.cos(beta) * Math.sin(alpha)
};
}
}
pos.x += offsetX * vecX.x + offsetY * vecY.x;
pos.y += offsetX * vecX.y + offsetY * vecY.y;
pos.z += offsetX * vecX.z + offsetY * vecY.z;
var projected = perspective([pos], axis.chart)[0];
if (skew) {
// Check if the label text would be mirrored
var isMirrored = shapeArea(perspective([
pos,
{ x: pos.x + vecX.x, y: pos.y + vecX.y, z: pos.z + vecX.z },
{ x: pos.x + vecY.x, y: pos.y + vecY.y, z: pos.z + vecY.z }
], axis.chart)) < 0;
if (isMirrored) {
vecX = { x: -vecX.x, y: -vecX.y, z: -vecX.z };
}
var pointsProjected = perspective([
{ x: pos.x, y: pos.y, z: pos.z },
{ x: pos.x + vecX.x, y: pos.y + vecX.y, z: pos.z + vecX.z },
{ x: pos.x + vecY.x, y: pos.y + vecY.y, z: pos.z + vecY.z }
], axis.chart);
projected.matrix = [
pointsProjected[1].x - pointsProjected[0].x,
pointsProjected[1].y - pointsProjected[0].y,
pointsProjected[2].x - pointsProjected[0].x,
pointsProjected[2].y - pointsProjected[0].y,
projected.x,
projected.y
];
projected.matrix[4] -= projected.x * projected.matrix[0] +
projected.y * projected.matrix[2];
projected.matrix[5] -= projected.x * projected.matrix[1] +
projected.y * projected.matrix[3];
}
return projected;
};
/**
* @private
*/
Axis3DAdditions.prototype.swapZ = function (p, insidePlotArea) {
var axis = this.axis;
if (axis.isZAxis) {
var plotLeft = insidePlotArea ? 0 : axis.chart.plotLeft;
return {
x: plotLeft + p.z,
y: p.y,
z: p.x - plotLeft
};
}
return p;
};
return Axis3DAdditions;
}());
/**
* Axis with 3D support.
* @private
* @class
*/
var Axis3D = /** @class */ (function () {
function Axis3D() {
}
/* *
*
* Static Functions
*
* */
/**
* Extends axis class with 3D support.
* @private
*/
Axis3D.compose = function (AxisClass) {
merge(true, AxisClass.defaultOptions, Axis3D.defaultOptions);
AxisClass.keepProps.push('axis3D');
addEvent(AxisClass, 'init', Axis3D.onInit);
addEvent(AxisClass, 'afterSetOptions', Axis3D.onAfterSetOptions);
addEvent(AxisClass, 'drawCrosshair', Axis3D.onDrawCrosshair);
addEvent(AxisClass, 'destroy', Axis3D.onDestroy);
var axisProto = AxisClass.prototype;
wrap(axisProto, 'getLinePath', Axis3D.wrapGetLinePath);
wrap(axisProto, 'getPlotBandPath', Axis3D.wrapGetPlotBandPath);
wrap(axisProto, 'getPlotLinePath', Axis3D.wrapGetPlotLinePath);
wrap(axisProto, 'getSlotWidth', Axis3D.wrapGetSlotWidth);
wrap(axisProto, 'getTitlePosition', Axis3D.wrapGetTitlePosition);
Tick3D.compose(Tick);
};
/**
* @private
*/
Axis3D.onAfterSetOptions = function () {
var axis = this;
var chart = axis.chart;
var options = axis.options;
if (chart.is3d && chart.is3d() && axis.coll !== 'colorAxis') {
options.tickWidth = pick(options.tickWidth, 0);
options.gridLineWidth = pick(options.gridLineWidth, 1);
}
};
/**
* @private
*/
Axis3D.onDestroy = function () {
['backFrame', 'bottomFrame', 'sideFrame'].forEach(function (prop) {
if (this[prop]) {
this[prop] = this[prop].destroy();
}
}, this);
};
/**
* @private
*/
Axis3D.onDrawCrosshair = function (e) {
var axis = this;
if (axis.chart.is3d() &&
axis.coll !== 'colorAxis') {
if (e.point) {
e.point.crosshairPos = axis.isXAxis ?
e.point.axisXpos :
axis.len - e.point.axisYpos;
}
}
};
/**
* @private
*/
Axis3D.onInit = function () {
var axis = this;
if (!axis.axis3D) {
axis.axis3D = new Axis3DAdditions(axis);
}
};
/**
* Do not draw axislines in 3D.
* @private
*/
Axis3D.wrapGetLinePath = function (proceed) {
var axis = this;
// Do not do this if the chart is not 3D
if (!axis.chart.is3d() || axis.coll === 'colorAxis') {
return proceed.apply(axis, [].slice.call(arguments, 1));
}
return [];
};
/**
* @private
*/
Axis3D.wrapGetPlotBandPath = function (proceed) {
// Do not do this if the chart is not 3D
if (!this.chart.is3d() || this.coll === 'colorAxis') {
return proceed.apply(this, [].slice.call(arguments, 1));
}
var args = arguments, from = args[1], to = args[2], path = [], fromPath = this.getPlotLinePath({ value: from }), toPath = this.getPlotLinePath({ value: to });
if (fromPath && toPath) {
for (var i = 0; i < fromPath.length; i += 2) {
var fromStartSeg = fromPath[i], fromEndSeg = fromPath[i + 1], toStartSeg = toPath[i], toEndSeg = toPath[i + 1];
if (fromStartSeg[0] === 'M' &&
fromEndSeg[0] === 'L' &&
toStartSeg[0] === 'M' &&
toEndSeg[0] === 'L') {
path.push(fromStartSeg, fromEndSeg, toEndSeg,
// lineTo instead of moveTo
['L', toStartSeg[1], toStartSeg[2]], ['Z']);
}
}
}
return path;
};
/**
* @private
*/
Axis3D.wrapGetPlotLinePath = function (proceed) {
var axis = this;
var axis3D = axis.axis3D;
var chart = axis.chart;
var path = proceed.apply(axis, [].slice.call(arguments, 1));
// Do not do this if the chart is not 3D
if (axis.coll === 'colorAxis' ||
!chart.chart3d ||
!chart.is3d()) {
return path;
}
if (path === null) {
return path;
}
var options3d = chart.options.chart.options3d, d = axis.isZAxis ? chart.plotWidth : options3d.depth, frame = chart.chart3d.frame3d, startSegment = path[0], endSegment = path[1], pArr, pathSegments = [];
if (startSegment[0] === 'M' && endSegment[0] === 'L') {
pArr = [
axis3D.swapZ({ x: startSegment[1], y: startSegment[2], z: 0 }),
axis3D.swapZ({ x: startSegment[1], y: startSegment[2], z: d }),
axis3D.swapZ({ x: endSegment[1], y: endSegment[2], z: 0 }),
axis3D.swapZ({ x: endSegment[1], y: endSegment[2], z: d })
];
if (!this.horiz) { // Y-Axis
if (frame.front.visible) {
pathSegments.push(pArr[0], pArr[2]);
}
if (frame.back.visible) {
pathSegments.push(pArr[1], pArr[3]);
}
if (frame.left.visible) {
pathSegments.push(pArr[0], pArr[1]);
}
if (frame.right.visible) {
pathSegments.push(pArr[2], pArr[3]);
}
}
else if (this.isZAxis) { // Z-Axis
if (frame.left.visible) {
pathSegments.push(pArr[0], pArr[2]);
}
if (frame.right.visible) {
pathSegments.push(pArr[1], pArr[3]);
}
if (frame.top.visible) {
pathSegments.push(pArr[0], pArr[1]);
}
if (frame.bottom.visible) {
pathSegments.push(pArr[2], pArr[3]);
}
}
else { // X-Axis
if (frame.front.visible) {
pathSegments.push(pArr[0], pArr[2]);
}
if (frame.back.visible) {
pathSegments.push(pArr[1], pArr[3]);
}
if (frame.top.visible) {
pathSegments.push(pArr[0], pArr[1]);
}
if (frame.bottom.visible) {
pathSegments.push(pArr[2], pArr[3]);
}
}
pathSegments = perspective(pathSegments, this.chart, false);
}
return chart.renderer.toLineSegments(pathSegments);
};
/**
* Wrap getSlotWidth function to calculate individual width value for each
* slot (#8042).
* @private
*/
Axis3D.wrapGetSlotWidth = function (proceed, tick) {
var axis = this;
var chart = axis.chart;
var ticks = axis.ticks;
var gridGroup = axis.gridGroup;
if (axis.categories &&
chart.frameShapes &&
chart.is3d() &&
gridGroup &&
tick &&
tick.label) {
var firstGridLine = gridGroup.element.childNodes[0].getBBox(), frame3DLeft = chart.frameShapes.left.getBBox(), options3d = chart.options.chart.options3d, origin = {
x: chart.plotWidth / 2,
y: chart.plotHeight / 2,
z: options3d.depth / 2,
vd: pick(options3d.depth, 1) * pick(options3d.viewDistance, 0)
}, labelPos, prevLabelPos, nextLabelPos, slotWidth, tickId = tick.pos, prevTick = ticks[tickId - 1], nextTick = ticks[tickId + 1];
// Check whether the tick is not the first one and previous tick
// exists, then calculate position of previous label.
if (tickId !== 0 && prevTick && prevTick.label.xy) {
prevLabelPos = perspective3D({
x: prevTick.label.xy.x,
y: prevTick.label.xy.y,
z: null
}, origin, origin.vd);
}
// If next label position is defined, then recalculate its position
// basing on the perspective.
if (nextTick && nextTick.label.xy) {
nextLabelPos = perspective3D({
x: nextTick.label.xy.x,
y: nextTick.label.xy.y,
z: null
}, origin, origin.vd);
}
labelPos = {
x: tick.label.xy.x,
y: tick.label.xy.y,
z: null
};
labelPos = perspective3D(labelPos, origin, origin.vd);
// If tick is first one, check whether next label position is
// already calculated, then return difference between the first and
// the second label. If there is no next label position calculated,
// return the difference between the first grid line and left 3d
// frame.
slotWidth = Math.abs(prevLabelPos ?
labelPos.x - prevLabelPos.x : nextLabelPos ?
nextLabelPos.x - labelPos.x :
firstGridLine.x - frame3DLeft.x);
return slotWidth;
}
return proceed.apply(axis, [].slice.call(arguments, 1));
};
/**
* @private
*/
Axis3D.wrapGetTitlePosition = function (proceed) {
var pos = proceed.apply(this, [].slice.call(arguments, 1));
return this.axis3D ?
this.axis3D.fix3dPosition(pos, true) :
pos;
};
/* *
*
* Static Properties
*
* */
/**
* @optionparent xAxis
*/
Axis3D.defaultOptions = {
labels: {
/**
* Defines how the labels are be repositioned according to the 3D
* chart orientation.
*
* - `'offset'`: Maintain a fixed horizontal/vertical distance from
* the tick marks, despite the chart orientation. This is the
* backwards compatible behavior, and causes skewing of X and Z
* axes.
*
* - `'chart'`: Preserve 3D position relative to the chart. This
* looks nice, but hard to read if the text isn't forward-facing.
*
* - `'flap'`: Rotated text along the axis to compensate for the
* chart orientation. This tries to maintain text as legible as
* possible on all orientations.
*
* - `'ortho'`: Rotated text along the axis direction so that the
* labels are orthogonal to the axis. This is very similar to
* `'flap'`, but prevents skewing the labels (X and Y scaling are
* still present).
*
* @sample highcharts/3d/skewed-labels/
* Skewed labels
*
* @since 5.0.15
* @validvalue ['offset', 'chart', 'flap', 'ortho']
* @product highcharts
* @requires highcharts-3d
*/
position3d: 'offset',
/**
* If enabled, the axis labels will skewed to follow the
* perspective.
*
* This will fix overlapping labels and titles, but texts become
* less legible due to the distortion.
*
* The final appearance depends heavily on `labels.position3d`.
*
* @sample highcharts/3d/skewed-labels/
* Skewed labels
*
* @since 5.0.15
* @product highcharts
* @requires highcharts-3d
*/
skew3d: false
},
title: {
/**
* Defines how the title is repositioned according to the 3D chart
* orientation.
*
* - `'offset'`: Maintain a fixed horizontal/vertical distance from
* the tick marks, despite the chart orientation. This is the
* backwards compatible behavior, and causes skewing of X and Z
* axes.
*
* - `'chart'`: Preserve 3D position relative to the chart. This
* looks nice, but hard to read if the text isn't forward-facing.
*
* - `'flap'`: Rotated text along the axis to compensate for the
* chart orientation. This tries to maintain text as legible as
* possible on all orientations.
*
* - `'ortho'`: Rotated text along the axis direction so that the
* labels are orthogonal to the axis. This is very similar to
* `'flap'`, but prevents skewing the labels (X and Y scaling are
* still present).
*
* - `undefined`: Will use the config from `labels.position3d`
*
* @sample highcharts/3d/skewed-labels/
* Skewed labels
*
* @type {"offset"|"chart"|"flap"|"ortho"|null}
* @since 5.0.15
* @product highcharts
* @requires highcharts-3d
*/
position3d: null,
/**
* If enabled, the axis title will skewed to follow the perspective.
*
* This will fix overlapping labels and titles, but texts become
* less legible due to the distortion.
*
* The final appearance depends heavily on `title.position3d`.
*
* A `null` value will use the config from `labels.skew3d`.
*
* @sample highcharts/3d/skewed-labels/
* Skewed labels
*
* @type {boolean|null}
* @since 5.0.15
* @product highcharts
* @requires highcharts-3d
*/
skew3d: null
}
};
return Axis3D;
}());
export default Axis3D;
@@ -0,0 +1,534 @@
/* *
*
* (c) 2009-2020 Torstein Honsi
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import Axis from './Axis.js';
import H from '../Globals.js';
import U from '../Utilities.js';
var addEvent = U.addEvent, find = U.find, fireEvent = U.fireEvent, isArray = U.isArray, isNumber = U.isNumber, pick = U.pick;
import '../Series/Series.js';
import StackItem from '../../Extensions/Stacking.js';
var Series = H.Series;
/* eslint-disable valid-jsdoc */
/**
* Provides support for broken axes.
* @private
* @class
*/
var BrokenAxisAdditions = /** @class */ (function () {
/* *
*
* Constructors
*
* */
function BrokenAxisAdditions(axis) {
this.hasBreaks = false;
this.axis = axis;
}
/* *
*
* Static Functions
*
* */
/**
* @private
*/
BrokenAxisAdditions.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;
};
/**
* @private
*/
BrokenAxisAdditions.lin2Val = function (val) {
var axis = this;
var brokenAxis = axis.brokenAxis;
var breakArray = brokenAxis && brokenAxis.breakArray;
if (!breakArray) {
return val;
}
var nval = val, brk, i;
for (i = 0; i < breakArray.length; i++) {
brk = breakArray[i];
if (brk.from >= nval) {
break;
}
else if (brk.to < nval) {
nval += brk.len;
}
else if (BrokenAxisAdditions.isInBreak(brk, nval)) {
nval += brk.len;
}
}
return nval;
};
/**
* @private
*/
BrokenAxisAdditions.val2Lin = function (val) {
var axis = this;
var brokenAxis = axis.brokenAxis;
var breakArray = brokenAxis && brokenAxis.breakArray;
if (!breakArray) {
return val;
}
var nval = val, brk, i;
for (i = 0; i < breakArray.length; i++) {
brk = breakArray[i];
if (brk.to <= val) {
nval -= brk.len;
}
else if (brk.from >= val) {
break;
}
else if (BrokenAxisAdditions.isInBreak(brk, val)) {
nval -= (val - brk.from);
break;
}
}
return nval;
};
/* *
*
* Functions
*
* */
/**
* Returns the first break found where the x is larger then break.from and
* smaller then break.to.
*
* @param {number} x
* The number which should be within a break.
*
* @param {Array<Highcharts.XAxisBreaksOptions>} breaks
* The array of breaks to search within.
*
* @return {Highcharts.XAxisBreaksOptions|undefined}
* Returns the first break found that matches, returns false if no break is
* found.
*/
BrokenAxisAdditions.prototype.findBreakAt = function (x, breaks) {
return find(breaks, function (b) {
return b.from < x && x < b.to;
});
};
/**
* @private
*/
BrokenAxisAdditions.prototype.isInAnyBreak = function (val, testKeep) {
var brokenAxis = this;
var axis = brokenAxis.axis;
var breaks = axis.options.breaks, i = breaks && breaks.length, inbrk, keep, ret;
if (i) {
while (i--) {
if (BrokenAxisAdditions.isInBreak(breaks[i], val)) {
inbrk = true;
if (!keep) {
keep = pick(breaks[i].showPoints, !axis.isXAxis);
}
}
}
if (inbrk && testKeep) {
ret = inbrk && !keep;
}
else {
ret = inbrk;
}
}
return ret;
};
/**
* 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<Highcharts.XAxisBreaksOptions>} [breaks]
* The breaks to add. When `undefined` it removes existing breaks.
*
* @param {boolean} [redraw=true]
* Whether to redraw the chart immediately.
*
* @return {void}
*/
BrokenAxisAdditions.prototype.setBreaks = function (breaks, redraw) {
var brokenAxis = this;
var axis = brokenAxis.axis;
var hasBreaks = (isArray(breaks) && !!breaks.length);
axis.isDirty = brokenAxis.hasBreaks !== hasBreaks;
brokenAxis.hasBreaks = hasBreaks;
axis.options.breaks = axis.userOptions.breaks = breaks;
axis.forceRedraw = true; // Force recalculation in setScale
// Recalculate series related to the axis.
axis.series.forEach(function (series) {
series.isDirty = true;
});
if (!hasBreaks && axis.val2lin === BrokenAxisAdditions.val2Lin) {
// Revert to prototype functions
delete axis.val2lin;
delete axis.lin2val;
}
if (hasBreaks) {
axis.userOptions.ordinal = false;
axis.lin2val = BrokenAxisAdditions.lin2Val;
axis.val2lin = BrokenAxisAdditions.val2Lin;
axis.setExtremes = function (newMin, newMax, redraw, animation, eventArguments) {
// If trying to set extremes inside a break, extend min to
// after, and max to before the break ( #3857 )
if (brokenAxis.hasBreaks) {
var axisBreak, breaks = this.options.breaks;
while ((axisBreak = brokenAxis.findBreakAt(newMin, breaks))) {
newMin = axisBreak.to;
}
while ((axisBreak = brokenAxis.findBreakAt(newMax, breaks))) {
newMax = axisBreak.from;
}
// If both min and max is within the same break.
if (newMax < newMin) {
newMax = newMin;
}
}
Axis.prototype.setExtremes.call(this, newMin, newMax, redraw, animation, eventArguments);
};
axis.setAxisTranslation = function (saveOld) {
Axis.prototype.setAxisTranslation.call(this, saveOld);
brokenAxis.unitLength = null;
if (brokenAxis.hasBreaks) {
var breaks = axis.options.breaks || [],
// Temporary one:
breakArrayT = [], 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 (BrokenAxisAdditions.isInBreak(brk, min)) {
min +=
(brk.to % repeat) -
(min % repeat);
}
if (BrokenAxisAdditions.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);
}
});
/**
* HC <= 8 backwards compatibility, used by demo samples.
* @deprecated
* @private
* @requires modules/broken-axis
*/
axis.breakArray = brokenAxis.breakArray = breakArray;
// Used with staticScale, and below the actual axis length,
// when breaks are substracted.
brokenAxis.unitLength = max - min - length + pointRangePadding;
fireEvent(axis, 'afterBreaks');
if (axis.staticScale) {
axis.transA = axis.staticScale;
}
else if (brokenAxis.unitLength) {
axis.transA *=
(max - axis.min + pointRangePadding) /
brokenAxis.unitLength;
}
if (pointRangePadding) {
axis.minPixelPadding =
axis.transA * axis.minPointOffset;
}
axis.min = min;
axis.max = max;
}
};
}
if (pick(redraw, true)) {
axis.chart.redraw();
}
};
return BrokenAxisAdditions;
}());
/**
* Axis with support of broken data rows.
* @private
* @class
*/
var BrokenAxis = /** @class */ (function () {
function BrokenAxis() {
}
/**
* Adds support for broken axes.
* @private
*/
BrokenAxis.compose = function (AxisClass, SeriesClass) {
AxisClass.keepProps.push('brokenAxis');
var seriesProto = Series.prototype;
/**
* @private
*/
seriesProto.drawBreaks = function (axis, keys) {
var series = this, points = series.points, breaks, threshold, eventName, y;
if (axis && // #5950
axis.brokenAxis &&
axis.brokenAxis.hasBreaks) {
var brokenAxis_1 = axis.brokenAxis;
keys.forEach(function (key) {
breaks = brokenAxis_1 && brokenAxis_1.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) {
if (isNumber(threshold) && isNumber(y)) {
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
*
* @return {Highcharts.SVGPathArray}
* Gapped path
*/
seriesProto.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, 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
* @requires modules/broken-axis
* @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
* 5 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"]
* @requires modules/broken-axis
* @apioption plotOptions.series.gapUnit
*/
if (gapSize && i > 0) { // #5008
// Gap unit is relative
if (this.options.gapUnit !== 'value') {
gapSize *= this.basePointRange;
}
// Setting a new gapSize in case dataGrouping is enabled (#7686)
if (groupingSize &&
groupingSize > gapSize &&
// Except when DG is forced (e.g. from other series)
// and has lower granularity than actual points (#11351)
groupingSize >= this.basePointRange) {
gapSize = groupingSize;
}
// extension for ordinal breaks
var current = void 0, next = void 0;
while (i--) {
// Reassign next if it is not visible
if (!(next && next.visible !== false)) {
next = points[i + 1];
}
current = points[i];
// Skip iteration if one of the points is not visible
if (next.visible === false || current.visible === false) {
continue;
}
if (next.x - current.x > gapSize) {
var xRange = (current.x + next.x) / 2;
points.splice(// insert after this one
i + 1, 0, {
isNull: true,
x: xRange
});
// For stacked chart generate empty stack items, #6546
if (yAxis.stacking && this.options.stacking) {
stack = yAxis.stacking.stacks[this.stackKey][xRange] =
new StackItem(yAxis, yAxis.options
.stackLabels, false, xRange, this.stack);
stack.total = 0;
}
}
// Assign current to next for the upcoming iteration
next = current;
}
}
// Call base method
return this.getGraphPath(points);
};
/* eslint-disable no-invalid-this */
addEvent(AxisClass, 'init', function () {
var axis = this;
if (!axis.brokenAxis) {
axis.brokenAxis = new BrokenAxisAdditions(axis);
}
});
addEvent(AxisClass, 'afterInit', function () {
if (typeof this.brokenAxis !== 'undefined') {
this.brokenAxis.setBreaks(this.options.breaks, false);
}
});
addEvent(AxisClass, 'afterSetTickPositions', function () {
var axis = this;
var brokenAxis = axis.brokenAxis;
if (brokenAxis &&
brokenAxis.hasBreaks) {
var tickPositions = this.tickPositions, info = this.tickPositions.info, newPositions = [], i;
for (i = 0; i < tickPositions.length; i++) {
if (!brokenAxis.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(AxisClass, 'afterSetOptions', function () {
if (this.brokenAxis && this.brokenAxis.hasBreaks) {
this.options.ordinal = false;
}
});
addEvent(SeriesClass, 'afterGeneratePoints', function () {
var _a = this, isDirty = _a.isDirty, connectNulls = _a.options.connectNulls, points = _a.points, xAxis = _a.xAxis, yAxis = _a.yAxis;
// Set, or reset visibility of the points. Axis.setBreaks marks the
// series as isDirty
if (isDirty) {
var i = points.length;
while (i--) {
var point = points[i];
// Respect nulls inside the break (#4275)
var nullGap = point.y === null && connectNulls === false;
var isPointInBreak = (!nullGap && ((xAxis &&
xAxis.brokenAxis &&
xAxis.brokenAxis.isInAnyBreak(point.x, true)) || (yAxis &&
yAxis.brokenAxis &&
yAxis.brokenAxis.isInAnyBreak(point.y, true))));
// Set point.visible if in any break.
// If not in break, reset visible to original value.
point.visible = isPointInBreak ?
false :
point.options.visible !== false;
}
}
});
addEvent(SeriesClass, 'afterRender', function drawPointsWrapped() {
this.drawBreaks(this.xAxis, ['x']);
this.drawBreaks(this.yAxis, pick(this.pointArrayMap, ['y']));
});
};
return BrokenAxis;
}());
BrokenAxis.compose(Axis, Series); // @todo remove automatism
export default BrokenAxis;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,177 @@
/* *
*
* (c) 2010-2020 Torstein Honsi
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import Axis from './Axis.js';
import U from '../Utilities.js';
var addEvent = U.addEvent, getMagnitude = U.getMagnitude, normalizeTickInterval = U.normalizeTickInterval, timeUnits = U.timeUnits;
/* eslint-disable valid-jsdoc */
var DateTimeAxisAdditions = /** @class */ (function () {
/* *
*
* Constructors
*
* */
function DateTimeAxisAdditions(axis) {
this.axis = axis;
}
/* *
*
* Functions
*
* */
/**
* Get a normalized tick interval for dates. Returns a configuration object
* with unit range (interval), count and name. Used to prepare data for
* `getTimeTicks`. Previously this logic was part of getTimeTicks, but as
* `getTimeTicks` now runs of segments in stock charts, the normalizing
* logic was extracted in order to prevent it for running over again for
* each segment having the same interval. #662, #697.
* @private
*/
/**
* Get a normalized tick interval for dates. Returns a configuration object
* with unit range (interval), count and name. Used to prepare data for
* `getTimeTicks`. Previously this logic was part of getTimeTicks, but as
* `getTimeTicks` now runs of segments in stock charts, the normalizing
* logic was extracted in order to prevent it for running over again for
* each segment having the same interval. #662, #697.
* @private
*/
DateTimeAxisAdditions.prototype.normalizeTimeTickInterval = function (tickInterval, unitsOption) {
var units = unitsOption || [[
'millisecond',
[1, 2, 5, 10, 20, 25, 50, 100, 200, 500] // allowed multiples
], [
'second',
[1, 2, 5, 10, 15, 30]
], [
'minute',
[1, 2, 5, 10, 15, 30]
], [
'hour',
[1, 2, 3, 4, 6, 8, 12]
], [
'day',
[1, 2]
], [
'week',
[1, 2]
], [
'month',
[1, 2, 3, 4, 6]
], [
'year',
null
]], unit = units[units.length - 1], // default unit is years
interval = timeUnits[unit[0]], multiples = unit[1], count, i;
// loop through the units to find the one that best fits the
// tickInterval
for (i = 0; i < units.length; i++) {
unit = units[i];
interval = timeUnits[unit[0]];
multiples = unit[1];
if (units[i + 1]) {
// lessThan is in the middle between the highest multiple and
// the next unit.
var lessThan = (interval *
multiples[multiples.length - 1] +
timeUnits[units[i + 1][0]]) / 2;
// break and keep the current unit
if (tickInterval <= lessThan) {
break;
}
}
}
// prevent 2.5 years intervals, though 25, 250 etc. are allowed
if (interval === timeUnits.year && tickInterval < 5 * interval) {
multiples = [1, 2, 5];
}
// get the count
count = normalizeTickInterval(tickInterval / interval, multiples, unit[0] === 'year' ? // #1913, #2360
Math.max(getMagnitude(tickInterval / interval), 1) :
1);
return {
unitRange: interval,
count: count,
unitName: unit[0]
};
};
return DateTimeAxisAdditions;
}());
/**
* Date and time support for axes.
*
* @private
* @class
*/
var DateTimeAxis = /** @class */ (function () {
function DateTimeAxis() {
}
/* *
*
* Static Functions
*
* */
/**
* Extends axis class with date and time support.
* @private
*/
DateTimeAxis.compose = function (AxisClass) {
AxisClass.keepProps.push('dateTime');
var axisProto = AxisClass.prototype;
/**
* Set the tick positions to a time unit that makes sense, for example
* on the first of each month or on every Monday. Return an array with
* the time positions. Used in datetime axes as well as for grouping
* data on a datetime axis.
*
* @private
* @function Highcharts.Axis#getTimeTicks
*
* @param {Highcharts.TimeNormalizeObject} normalizedInterval
* The interval in axis values (ms) and thecount.
*
* @param {number} min
* The minimum in axis values.
*
* @param {number} max
* The maximum in axis values.
*
* @param {number} startOfWeek
*
* @return {Highcharts.AxisTickPositionsArray}
*/
axisProto.getTimeTicks = function () {
return this.chart.time.getTimeTicks.apply(this.chart.time, arguments);
};
/* eslint-disable no-invalid-this */
addEvent(AxisClass, 'init', function (e) {
var axis = this;
var options = e.userOptions;
if (options.type !== 'datetime') {
axis.dateTime = void 0;
return;
}
if (!axis.dateTime) {
axis.dateTime = new DateTimeAxisAdditions(axis);
}
});
/* eslint-enable no-invalid-this */
};
/* *
*
* Static Properties
*
* */
DateTimeAxis.AdditionsClass = DateTimeAxisAdditions;
return DateTimeAxis;
}());
DateTimeAxis.compose(Axis);
export default DateTimeAxis;
+884
View File
@@ -0,0 +1,884 @@
/* *
*
* (c) 2016 Highsoft AS
* Authors: Lars A. V. Cabrera
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import Axis from './Axis.js';
import H from '../Globals.js';
import O from '../Options.js';
var dateFormat = O.dateFormat;
import Tick from './Tick.js';
import U from '../Utilities.js';
var addEvent = U.addEvent, defined = U.defined, erase = U.erase, find = U.find, isArray = U.isArray, isNumber = U.isNumber, merge = U.merge, pick = U.pick, timeUnits = U.timeUnits, wrap = U.wrap;
var argsToArray = function (args) {
return Array.prototype.slice.call(args, 1);
}, isObject = function (x) {
// Always use strict mode
return U.isObject(x, true);
}, Chart = H.Chart;
var applyGridOptions = function applyGridOptions(axis) {
var options = axis.options;
// Center-align by default
if (!options.labels) {
options.labels = {};
}
options.labels.align = pick(options.labels.align, 'center');
// @todo: Check against tickLabelPlacement between/on etc
/* Prevents adding the last tick label if the axis is not a category
axis.
Since numeric labels are normally placed at starts and ends of a
range of value, and this module makes the label point at the value,
an "extra" label would appear. */
if (!axis.categories) {
options.showLastLabel = false;
}
// Prevents rotation of labels when squished, as rotating them would not
// help.
axis.labelRotation = 0;
options.labels.rotation = 0;
};
/**
* For a datetime axis, the scale will automatically adjust to the
* appropriate unit. This member gives the default string
* representations used for each unit. For intermediate values,
* different units may be used, for example the `day` unit can be used
* on midnight and `hour` unit be used for intermediate values on the
* same axis.
* For grid axes (like in Gantt charts),
* it is possible to declare as a list to provide different
* formats depending on available space.
* For an overview of the replacement codes, see
* [dateFormat](/class-reference/Highcharts#dateFormat).
*
* Defaults to:
* ```js
* {
hour: {
list: ['%H:%M', '%H']
},
day: {
list: ['%A, %e. %B', '%a, %e. %b', '%E']
},
week: {
list: ['Week %W', 'W%W']
},
month: {
list: ['%B', '%b', '%o']
}
},
* ```
*
* @sample {gantt} gantt/demo/left-axis-table
* Gantt Chart with custom axis date format.
*
* @product gantt
* @apioption xAxis.dateTimeLabelFormats
*/
/**
* Set grid options for the axis labels. Requires Highcharts Gantt.
*
* @since 6.2.0
* @product gantt
* @apioption xAxis.grid
*/
/**
* Enable grid on the axis labels. Defaults to true for Gantt charts.
*
* @type {boolean}
* @default true
* @since 6.2.0
* @product gantt
* @apioption xAxis.grid.enabled
*/
/**
* Set specific options for each column (or row for horizontal axes) in the
* grid. Each extra column/row is its own axis, and the axis options can be set
* here.
*
* @sample gantt/demo/left-axis-table
* Left axis as a table
*
* @type {Array<Highcharts.XAxisOptions>}
* @apioption xAxis.grid.columns
*/
/**
* Set border color for the label grid lines.
*
* @type {Highcharts.ColorString}
* @apioption xAxis.grid.borderColor
*/
/**
* Set border width of the label grid lines.
*
* @type {number}
* @default 1
* @apioption xAxis.grid.borderWidth
*/
/**
* Set cell height for grid axis labels. By default this is calculated from font
* size. This option only applies to horizontal axes.
*
* @sample gantt/grid-axis/cellheight
* Gant chart with custom cell height
* @type {number}
* @apioption xAxis.grid.cellHeight
*/
''; // detach doclets above
/**
* Get the largest label width and height.
*
* @private
* @function Highcharts.Axis#getMaxLabelDimensions
*
* @param {Highcharts.Dictionary<Highcharts.Tick>} ticks
* All the ticks on one axis.
*
* @param {Array<number|string>} tickPositions
* All the tick positions on one axis.
*
* @return {Highcharts.SizeObject}
* Object containing the properties height and width.
*
* @todo Move this to the generic axis implementation, as it is used there.
*/
Axis.prototype.getMaxLabelDimensions = function (ticks, tickPositions) {
var dimensions = {
width: 0,
height: 0
};
tickPositions.forEach(function (pos) {
var tick = ticks[pos], tickHeight = 0, tickWidth = 0, label;
if (isObject(tick)) {
label = isObject(tick.label) ? tick.label : {};
// Find width and height of tick
tickHeight = label.getBBox ? label.getBBox().height : 0;
if (label.textStr) {
// Set the tickWidth same as the label width after ellipsis
// applied #10281
tickWidth = Math.round(label.getBBox().width);
}
// Update the result if width and/or height are larger
dimensions.height = Math.max(tickHeight, dimensions.height);
dimensions.width = Math.max(tickWidth, dimensions.width);
}
});
return dimensions;
};
// Adds week date format
H.dateFormats.W = function (timestamp) {
var d = new this.Date(timestamp);
var firstDay = (this.get('Day', d) + 6) % 7;
var thursday = new this.Date(d.valueOf());
this.set('Date', thursday, this.get('Date', d) - firstDay + 3);
var firstThursday = new this.Date(this.get('FullYear', thursday), 0, 1);
if (this.get('Day', firstThursday) !== 4) {
this.set('Month', d, 0);
this.set('Date', d, 1 + (11 - this.get('Day', firstThursday)) % 7);
}
return (1 +
Math.floor((thursday.valueOf() - firstThursday.valueOf()) / 604800000)).toString();
};
// First letter of the day of the week, e.g. 'M' for 'Monday'.
H.dateFormats.E = function (timestamp) {
return dateFormat('%a', timestamp, true).charAt(0);
};
/* eslint-disable no-invalid-this */
addEvent(Chart, 'afterSetChartSize', function () {
this.axes.forEach(function (axis) {
(axis.grid && axis.grid.columns || []).forEach(function (column) {
column.setAxisSize();
column.setAxisTranslation();
});
});
});
// Center tick labels in cells.
addEvent(Tick, 'afterGetLabelPosition', function (e) {
var tick = this, label = tick.label, axis = tick.axis, reversed = axis.reversed, chart = axis.chart, options = axis.options, gridOptions = options.grid || {}, labelOpts = axis.options.labels, align = labelOpts.align,
// verticalAlign is currently not supported for axis.labels.
verticalAlign = 'middle', // labelOpts.verticalAlign,
side = GridAxis.Side[axis.side], tickmarkOffset = e.tickmarkOffset, tickPositions = axis.tickPositions, tickPos = tick.pos - tickmarkOffset, nextTickPos = (isNumber(tickPositions[e.index + 1]) ?
tickPositions[e.index + 1] - tickmarkOffset :
axis.max + tickmarkOffset), tickSize = axis.tickSize('tick'), tickWidth = tickSize ? tickSize[0] : 0, crispCorr = tickSize ? tickSize[1] / 2 : 0, labelHeight, lblMetrics, lines, bottom, top, left, right;
// Only center tick labels in grid axes
if (gridOptions.enabled === true) {
// Calculate top and bottom positions of the cell.
if (side === 'top') {
bottom = axis.top + axis.offset;
top = bottom - tickWidth;
}
else if (side === 'bottom') {
top = chart.chartHeight - axis.bottom + axis.offset;
bottom = top + tickWidth;
}
else {
bottom = axis.top + axis.len - axis.translate(reversed ? nextTickPos : tickPos);
top = axis.top + axis.len - axis.translate(reversed ? tickPos : nextTickPos);
}
// Calculate left and right positions of the cell.
if (side === 'right') {
left = chart.chartWidth - axis.right + axis.offset;
right = left + tickWidth;
}
else if (side === 'left') {
right = axis.left + axis.offset;
left = right - tickWidth;
}
else {
left = Math.round(axis.left + axis.translate(reversed ? nextTickPos : tickPos)) - crispCorr;
right = Math.round(axis.left + axis.translate(reversed ? tickPos : nextTickPos)) - crispCorr;
}
tick.slotWidth = right - left;
// Calculate the positioning of the label based on
// alignment.
e.pos.x = (align === 'left' ?
left :
align === 'right' ?
right :
left + ((right - left) / 2) // default to center
);
e.pos.y = (verticalAlign === 'top' ?
top :
verticalAlign === 'bottom' ?
bottom :
top + ((bottom - top) / 2) // default to middle
);
lblMetrics = chart.renderer.fontMetrics(labelOpts.style.fontSize, label.element);
labelHeight = label.getBBox().height;
// Adjustment to y position to align the label correctly.
// Would be better to have a setter or similar for this.
if (!labelOpts.useHTML) {
lines = Math.round(labelHeight / lblMetrics.h);
e.pos.y += (
// Center the label
// TODO: why does this actually center the label?
((lblMetrics.b - (lblMetrics.h - lblMetrics.f)) / 2) +
// Adjust for height of additional lines.
-(((lines - 1) * lblMetrics.h) / 2));
}
else {
e.pos.y += (
// Readjust yCorr in htmlUpdateTransform
lblMetrics.b +
// Adjust for height of html label
-(labelHeight / 2));
}
e.pos.x += (axis.horiz && labelOpts.x || 0);
}
});
/* eslint-enable no-invalid-this */
/**
* Additions for grid axes.
* @private
* @class
*/
var GridAxisAdditions = /** @class */ (function () {
/* *
*
* Constructors
*
* */
function GridAxisAdditions(axis) {
this.axis = axis;
}
/* *
*
* Functions
*
* */
/**
* Checks if an axis is the outer axis in its dimension. Since
* axes are placed outwards in order, the axis with the highest
* index is the outermost axis.
*
* Example: If there are multiple x-axes at the top of the chart,
* this function returns true if the axis supplied is the last
* of the x-axes.
*
* @private
*
* @return {boolean}
* True if the axis is the outermost axis in its dimension; false if
* not.
*/
GridAxisAdditions.prototype.isOuterAxis = function () {
var axis = this.axis;
var chart = axis.chart;
var columnIndex = axis.grid.columnIndex;
var columns = (axis.linkedParent && axis.linkedParent.grid.columns ||
axis.grid.columns);
var parentAxis = columnIndex ? axis.linkedParent : axis;
var thisIndex = -1, lastIndex = 0;
chart[axis.coll].forEach(function (otherAxis, index) {
if (otherAxis.side === axis.side && !otherAxis.options.isInternal) {
lastIndex = index;
if (otherAxis === parentAxis) {
// Get the index of the axis in question
thisIndex = index;
}
}
});
return (lastIndex === thisIndex &&
(isNumber(columnIndex) ? columns.length === columnIndex : true));
};
return GridAxisAdditions;
}());
/**
* Axis with grid support.
* @private
* @class
*/
var GridAxis = /** @class */ (function () {
function GridAxis() {
}
/* *
*
* Static Functions
*
* */
/* eslint-disable valid-jsdoc */
/**
* Extends axis class with grid support.
* @private
*/
GridAxis.compose = function (AxisClass) {
Axis.keepProps.push('grid');
wrap(AxisClass.prototype, 'unsquish', GridAxis.wrapUnsquish);
// Add event handlers
addEvent(AxisClass, 'init', GridAxis.onInit);
addEvent(AxisClass, 'afterGetOffset', GridAxis.onAfterGetOffset);
addEvent(AxisClass, 'afterGetTitlePosition', GridAxis.onAfterGetTitlePosition);
addEvent(AxisClass, 'afterInit', GridAxis.onAfterInit);
addEvent(AxisClass, 'afterRender', GridAxis.onAfterRender);
addEvent(AxisClass, 'afterSetAxisTranslation', GridAxis.onAfterSetAxisTranslation);
addEvent(AxisClass, 'afterSetOptions', GridAxis.onAfterSetOptions);
addEvent(AxisClass, 'afterSetOptions', GridAxis.onAfterSetOptions2);
addEvent(AxisClass, 'afterSetScale', GridAxis.onAfterSetScale);
addEvent(AxisClass, 'afterTickSize', GridAxis.onAfterTickSize);
addEvent(AxisClass, 'trimTicks', GridAxis.onTrimTicks);
addEvent(AxisClass, 'destroy', GridAxis.onDestroy);
};
/**
* Handle columns and getOffset.
* @private
*/
GridAxis.onAfterGetOffset = function () {
var grid = this.grid;
(grid && grid.columns || []).forEach(function (column) {
column.getOffset();
});
};
/**
* @private
*/
GridAxis.onAfterGetTitlePosition = function (e) {
var axis = this;
var options = axis.options;
var gridOptions = options.grid || {};
if (gridOptions.enabled === true) {
// compute anchor points for each of the title align options
var title = axis.axisTitle, axisHeight = axis.height, horiz = axis.horiz, axisLeft = axis.left, offset = axis.offset, opposite = axis.opposite, _a = axis.options.title, axisTitleOptions = _a === void 0 ? {} : _a, axisTop = axis.top, axisWidth = axis.width;
var tickSize = axis.tickSize();
var titleWidth = title && title.getBBox().width;
var xOption = axisTitleOptions.x || 0;
var yOption = axisTitleOptions.y || 0;
var titleMargin = pick(axisTitleOptions.margin, horiz ? 5 : 10);
var titleFontSize = axis.chart.renderer.fontMetrics(axisTitleOptions.style &&
axisTitleOptions.style.fontSize, title).f;
var crispCorr = tickSize ? tickSize[0] / 2 : 0;
// TODO account for alignment
// the position in the perpendicular direction of the axis
var offAxis = ((horiz ? axisTop + axisHeight : axisLeft) +
(horiz ? 1 : -1) * // horizontal axis reverses the margin
(opposite ? -1 : 1) * // so does opposite axes
crispCorr +
(axis.side === GridAxis.Side.bottom ? titleFontSize : 0));
e.titlePosition.x = horiz ?
axisLeft - titleWidth / 2 - titleMargin + xOption :
offAxis + (opposite ? axisWidth : 0) + offset + xOption;
e.titlePosition.y = horiz ?
(offAxis -
(opposite ? axisHeight : 0) +
(opposite ? titleFontSize : -titleFontSize) / 2 +
offset +
yOption) :
axisTop - titleMargin + yOption;
}
};
/**
* @private
*/
GridAxis.onAfterInit = function () {
var axis = this;
var chart = axis.chart, _a = axis.options.grid, gridOptions = _a === void 0 ? {} : _a, userOptions = axis.userOptions;
if (gridOptions.enabled) {
applyGridOptions(axis);
/* eslint-disable no-invalid-this */
// TODO: wrap the axis instead
wrap(axis, 'labelFormatter', function (proceed) {
var _a = this, axis = _a.axis, value = _a.value;
var tickPos = axis.tickPositions;
var series = (axis.isLinked ?
axis.linkedParent :
axis).series[0];
var isFirst = value === tickPos[0];
var isLast = value === tickPos[tickPos.length - 1];
var point = series && find(series.options.data, function (p) {
return p[axis.isXAxis ? 'x' : 'y'] === value;
});
// Make additional properties available for the
// formatter
this.isFirst = isFirst;
this.isLast = isLast;
this.point = point;
// Call original labelFormatter
return proceed.call(this);
});
/* eslint-enable no-invalid-this */
}
if (gridOptions.columns) {
var columns = axis.grid.columns = [], columnIndex = axis.grid.columnIndex = 0;
// Handle columns, each column is a grid axis
while (++columnIndex < gridOptions.columns.length) {
var columnOptions = merge(userOptions, gridOptions.columns[gridOptions.columns.length - columnIndex - 1], {
linkedTo: 0,
// Force to behave like category axis
type: 'category',
// Disable by default the scrollbar on the grid axis
scrollbar: {
enabled: false
}
});
delete columnOptions.grid.columns; // Prevent recursion
var column = new Axis(axis.chart, columnOptions);
column.grid.isColumn = true;
column.grid.columnIndex = columnIndex;
// Remove column axis from chart axes array, and place it
// in the columns array.
erase(chart.axes, column);
erase(chart[axis.coll], column);
columns.push(column);
}
}
};
/**
* Draw an extra line on the far side of the outermost axis,
* creating floor/roof/wall of a grid. And some padding.
* ```
* Make this:
* (axis.min) __________________________ (axis.max)
* | | | | |
* Into this:
* (axis.min) __________________________ (axis.max)
* ___|____|____|____|____|__
* ```
* @private
*/
GridAxis.onAfterRender = function () {
var axis = this;
var grid = axis.grid;
var options = axis.options;
var renderer = axis.chart.renderer;
var gridOptions = options.grid || {};
var yStartIndex, yEndIndex, xStartIndex, xEndIndex;
if (gridOptions.enabled === true) {
// @todo acutual label padding (top, bottom, left, right)
axis.maxLabelDimensions = axis.getMaxLabelDimensions(axis.ticks, axis.tickPositions);
// Remove right wall before rendering if updating
if (axis.rightWall) {
axis.rightWall.destroy();
}
/*
Draw an extra axis line on outer axes
>
Make this: |______|______|______|___
> _________________________
Into this: |______|______|______|__|
*/
if (axis.grid && axis.grid.isOuterAxis() && axis.axisLine) {
var lineWidth = options.lineWidth;
if (lineWidth) {
var linePath = axis.getLinePath(lineWidth);
var startPoint = linePath[0];
var endPoint = linePath[1];
// Negate distance if top or left axis
// Subtract 1px to draw the line at the end of the tick
var tickLength = (axis.tickSize('tick') || [1])[0];
var distance = (tickLength - 1) * ((axis.side === GridAxis.Side.top ||
axis.side === GridAxis.Side.left) ? -1 : 1);
// If axis is horizontal, reposition line path vertically
if (startPoint[0] === 'M' && endPoint[0] === 'L') {
if (axis.horiz) {
startPoint[2] += distance;
endPoint[2] += distance;
}
else {
// If axis is vertical, reposition line path
// horizontally
startPoint[1] += distance;
endPoint[1] += distance;
}
}
if (!axis.grid.axisLineExtra) {
axis.grid.axisLineExtra = renderer
.path(linePath)
.attr({
zIndex: 7
})
.addClass('highcharts-axis-line')
.add(axis.axisGroup);
if (!renderer.styledMode) {
axis.grid.axisLineExtra.attr({
stroke: options.lineColor,
'stroke-width': lineWidth
});
}
}
else {
axis.grid.axisLineExtra.animate({
d: linePath
});
}
// show or hide the line depending on
// options.showEmpty
axis.axisLine[axis.showAxis ? 'show' : 'hide'](true);
}
}
(grid && grid.columns || []).forEach(function (column) {
column.render();
});
}
};
/**
* @private
*/
GridAxis.onAfterSetAxisTranslation = function () {
var axis = this;
var tickInfo = axis.tickPositions && axis.tickPositions.info;
var options = axis.options;
var gridOptions = options.grid || {};
var userLabels = axis.userOptions.labels || {};
if (axis.horiz) {
if (gridOptions.enabled === true) {
axis.series.forEach(function (series) {
series.options.pointRange = 0;
});
}
// Lower level time ticks, like hours or minutes, represent
// points in time and not ranges. These should be aligned
// left in the grid cell by default. The same applies to
// years of higher order.
if (tickInfo &&
options.dateTimeLabelFormats &&
options.labels &&
!defined(userLabels.align) &&
(options.dateTimeLabelFormats[tickInfo.unitName].range === false ||
tickInfo.count > 1 // years
)) {
options.labels.align = 'left';
if (!defined(userLabels.x)) {
options.labels.x = 3;
}
}
}
};
/**
* Creates a left and right wall on horizontal axes:
* - Places leftmost tick at the start of the axis, to create a left
* wall
* - Ensures that the rightmost tick is at the end of the axis, to
* create a right wall.
* @private
*/
GridAxis.onAfterSetOptions = function (e) {
var options = this.options, userOptions = e.userOptions, gridAxisOptions, gridOptions = ((options && isObject(options.grid)) ? options.grid : {});
if (gridOptions.enabled === true) {
// Merge the user options into default grid axis options so
// that when a user option is set, it takes presedence.
gridAxisOptions = merge(true, {
className: ('highcharts-grid-axis ' + (userOptions.className || '')),
dateTimeLabelFormats: {
hour: {
list: ['%H:%M', '%H']
},
day: {
list: ['%A, %e. %B', '%a, %e. %b', '%E']
},
week: {
list: ['Week %W', 'W%W']
},
month: {
list: ['%B', '%b', '%o']
}
},
grid: {
borderWidth: 1
},
labels: {
padding: 2,
style: {
fontSize: '13px'
}
},
margin: 0,
title: {
text: null,
reserveSpace: false,
rotation: 0
},
// In a grid axis, only allow one unit of certain types,
// for example we shouln't have one grid cell spanning
// two days.
units: [[
'millisecond',
[1, 10, 100]
], [
'second',
[1, 10]
], [
'minute',
[1, 5, 15]
], [
'hour',
[1, 6]
], [
'day',
[1]
], [
'week',
[1]
], [
'month',
[1]
], [
'year',
null
]]
}, userOptions);
// X-axis specific options
if (this.coll === 'xAxis') {
// For linked axes, tickPixelInterval is used only if
// the tickPositioner below doesn't run or returns
// undefined (like multiple years)
if (defined(userOptions.linkedTo) &&
!defined(userOptions.tickPixelInterval)) {
gridAxisOptions.tickPixelInterval = 350;
}
// For the secondary grid axis, use the primary axis'
// tick intervals and return ticks one level higher.
if (
// Check for tick pixel interval in options
!defined(userOptions.tickPixelInterval) &&
// Only for linked axes
defined(userOptions.linkedTo) &&
!defined(userOptions.tickPositioner) &&
!defined(userOptions.tickInterval)) {
gridAxisOptions.tickPositioner = function (min, max) {
var parentInfo = (this.linkedParent &&
this.linkedParent.tickPositions &&
this.linkedParent.tickPositions.info);
if (parentInfo) {
var unitIdx, count, unitName, i, units = gridAxisOptions.units, unitRange;
for (i = 0; i < units.length; i++) {
if (units[i][0] ===
parentInfo.unitName) {
unitIdx = i;
break;
}
}
// Get the first allowed count on the next
// unit.
if (units[unitIdx + 1]) {
unitName = units[unitIdx + 1][0];
count =
(units[unitIdx + 1][1] || [1])[0];
// In case the base X axis shows years, make
// the secondary axis show ten times the
// years (#11427)
}
else if (parentInfo.unitName === 'year') {
unitName = 'year';
count = parentInfo.count * 10;
}
unitRange = timeUnits[unitName];
this.tickInterval = unitRange * count;
return this.getTimeTicks({
unitRange: unitRange,
count: count,
unitName: unitName
}, min, max, this.options.startOfWeek);
}
};
}
}
// Now merge the combined options into the axis options
merge(true, this.options, gridAxisOptions);
if (this.horiz) {
/* _________________________
Make this: ___|_____|_____|_____|__|
^ ^
_________________________
Into this: |_____|_____|_____|_____|
^ ^ */
options.minPadding = pick(userOptions.minPadding, 0);
options.maxPadding = pick(userOptions.maxPadding, 0);
}
// If borderWidth is set, then use its value for tick and
// line width.
if (isNumber(options.grid.borderWidth)) {
options.tickWidth = options.lineWidth = gridOptions.borderWidth;
}
}
};
/**
* @private
*/
GridAxis.onAfterSetOptions2 = function (e) {
var axis = this;
var userOptions = e.userOptions;
var gridOptions = userOptions && userOptions.grid || {};
var columns = gridOptions.columns;
// Add column options to the parent axis. Children has their column
// options set on init in onGridAxisAfterInit.
if (gridOptions.enabled && columns) {
merge(true, axis.options, columns[columns.length - 1]);
}
};
/**
* Handle columns and setScale.
* @private
*/
GridAxis.onAfterSetScale = function () {
var axis = this;
(axis.grid.columns || []).forEach(function (column) {
column.setScale();
});
};
/**
* Draw vertical axis ticks extra long to create cell floors and roofs.
* Overrides the tickLength for vertical axes.
* @private
*/
GridAxis.onAfterTickSize = function (e) {
var defaultLeftAxisOptions = Axis.defaultLeftAxisOptions;
var _a = this, horiz = _a.horiz, maxLabelDimensions = _a.maxLabelDimensions, _b = _a.options.grid, gridOptions = _b === void 0 ? {} : _b;
if (gridOptions.enabled && maxLabelDimensions) {
var labelPadding = (Math.abs(defaultLeftAxisOptions.labels.x) * 2);
var distance = horiz ?
gridOptions.cellHeight || labelPadding + maxLabelDimensions.height :
labelPadding + maxLabelDimensions.width;
if (isArray(e.tickSize)) {
e.tickSize[0] = distance;
}
else {
e.tickSize = [distance, 0];
}
}
};
/**
* @private
*/
GridAxis.onDestroy = function (e) {
var grid = this.grid;
(grid.columns || []).forEach(function (column) {
column.destroy(e.keepEvents);
});
grid.columns = void 0;
};
/**
* Wraps axis init to draw cell walls on vertical axes.
* @private
*/
GridAxis.onInit = function (e) {
var axis = this;
var userOptions = e.userOptions || {};
var gridOptions = userOptions.grid || {};
if (gridOptions.enabled && defined(gridOptions.borderColor)) {
userOptions.tickColor = userOptions.lineColor = gridOptions.borderColor;
}
if (!axis.grid) {
axis.grid = new GridAxisAdditions(axis);
}
};
/**
* Makes tick labels which are usually ignored in a linked axis
* displayed if they are within range of linkedParent.min.
* ```
* _____________________________
* | | | | |
* Make this: | | 2 | 3 | 4 |
* |___|_______|_______|_______|
* ^
* _____________________________
* | | | | |
* Into this: | 1 | 2 | 3 | 4 |
* |___|_______|_______|_______|
* ^
* ```
* @private
* @todo Does this function do what the drawing says? Seems to affect
* ticks and not the labels directly?
*/
GridAxis.onTrimTicks = function () {
var axis = this;
var options = axis.options;
var gridOptions = options.grid || {};
var categoryAxis = axis.categories;
var tickPositions = axis.tickPositions;
var firstPos = tickPositions[0];
var lastPos = tickPositions[tickPositions.length - 1];
var linkedMin = axis.linkedParent && axis.linkedParent.min;
var linkedMax = axis.linkedParent && axis.linkedParent.max;
var min = linkedMin || axis.min;
var max = linkedMax || axis.max;
var tickInterval = axis.tickInterval;
var endMoreThanMin = (firstPos < min &&
firstPos + tickInterval > min);
var startLessThanMax = (lastPos > max &&
lastPos - tickInterval < max);
if (gridOptions.enabled === true &&
!categoryAxis &&
(axis.horiz || axis.isLinked)) {
if (endMoreThanMin && !options.startOnTick) {
tickPositions[0] = min;
}
if (startLessThanMax && !options.endOnTick) {
tickPositions[tickPositions.length - 1] = max;
}
}
};
/**
* Avoid altering tickInterval when reserving space.
* @private
*/
GridAxis.wrapUnsquish = function (proceed) {
var axis = this;
var _a = axis.options.grid, gridOptions = _a === void 0 ? {} : _a;
if (gridOptions.enabled === true && axis.categories) {
return axis.tickInterval;
}
return proceed.apply(axis, argsToArray(arguments));
};
return GridAxis;
}());
(function (GridAxis) {
/**
* Enum for which side the axis is on. Maps to axis.side.
* @private
*/
var Side;
(function (Side) {
Side[Side["top"] = 0] = "top";
Side[Side["right"] = 1] = "right";
Side[Side["bottom"] = 2] = "bottom";
Side[Side["left"] = 3] = "left";
})(Side = GridAxis.Side || (GridAxis.Side = {}));
})(GridAxis || (GridAxis = {}));
GridAxis.compose(Axis);
export default GridAxis;
@@ -0,0 +1,46 @@
/* *
*
* (c) 2010-2020 Torstein Honsi
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
/**
* @private
* @class
*/
var HiddenAxis = /** @class */ (function () {
function HiddenAxis() {
}
/**
* Augments methods for the x axis in order to hide it completely. Used for
* the X axis in gauges
*
* @private
*
* @param {Highcharts.Axis} axis
* Radial axis to augment.
*/
HiddenAxis.init = function (axis) {
axis.getOffset = function () { };
axis.redraw = function () {
this.isDirty = false; // prevent setting Y axis dirty
};
axis.render = function () {
this.isDirty = false; // prevent setting Y axis dirty
};
axis.createLabelCollector = function () {
return function () {
return;
};
};
axis.setScale = function () { };
axis.setCategories = function () { };
axis.setTitle = function () { };
axis.isHidden = true;
};
return HiddenAxis;
}());
export default HiddenAxis;
@@ -0,0 +1,176 @@
/* *
*
* (c) 2010-2020 Torstein Honsi
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import Axis from './Axis.js';
import U from '../Utilities.js';
var addEvent = U.addEvent, getMagnitude = U.getMagnitude, normalizeTickInterval = U.normalizeTickInterval, pick = U.pick;
/* eslint-disable valid-jsdoc */
/**
* Provides logarithmic support for axes.
*
* @private
* @class
*/
var LogarithmicAxisAdditions = /** @class */ (function () {
/* *
*
* Constructors
*
* */
function LogarithmicAxisAdditions(axis) {
this.axis = axis;
}
/* *
*
* Functions
*
* */
/**
* Set the tick positions of a logarithmic axis.
*/
LogarithmicAxisAdditions.prototype.getLogTickPositions = function (interval, min, max, minor) {
var log = this;
var axis = log.axis;
var axisLength = axis.len;
var options = axis.options;
// Since we use this method for both major and minor ticks,
// use a local variable and return the result
var positions = [];
// Reset
if (!minor) {
log.minorAutoInterval = void 0;
}
// First case: All ticks fall on whole logarithms: 1, 10, 100 etc.
if (interval >= 0.5) {
interval = Math.round(interval);
positions = axis.getLinearTickPositions(interval, min, max);
// Second case: We need intermediary ticks. For example
// 1, 2, 4, 6, 8, 10, 20, 40 etc.
}
else if (interval >= 0.08) {
var roundedMin = Math.floor(min), intermediate, i, j, len, pos, lastPos, break2;
if (interval > 0.3) {
intermediate = [1, 2, 4];
// 0.2 equals five minor ticks per 1, 10, 100 etc
}
else if (interval > 0.15) {
intermediate = [1, 2, 4, 6, 8];
}
else { // 0.1 equals ten minor ticks per 1, 10, 100 etc
intermediate = [1, 2, 3, 4, 5, 6, 7, 8, 9];
}
for (i = roundedMin; i < max + 1 && !break2; i++) {
len = intermediate.length;
for (j = 0; j < len && !break2; j++) {
pos = log.log2lin(log.lin2log(i) * intermediate[j]);
// #1670, lastPos is #3113
if (pos > min &&
(!minor || lastPos <= max) &&
typeof lastPos !== 'undefined') {
positions.push(lastPos);
}
if (lastPos > max) {
break2 = true;
}
lastPos = pos;
}
}
// Third case: We are so deep in between whole logarithmic values that
// we might as well handle the tick positions like a linear axis. For
// example 1.01, 1.02, 1.03, 1.04.
}
else {
var realMin = log.lin2log(min), realMax = log.lin2log(max), tickIntervalOption = minor ?
axis.getMinorTickInterval() :
options.tickInterval, filteredTickIntervalOption = tickIntervalOption === 'auto' ?
null :
tickIntervalOption, tickPixelIntervalOption = options.tickPixelInterval / (minor ? 5 : 1), totalPixelLength = minor ?
axisLength / axis.tickPositions.length :
axisLength;
interval = pick(filteredTickIntervalOption, log.minorAutoInterval, (realMax - realMin) *
tickPixelIntervalOption / (totalPixelLength || 1));
interval = normalizeTickInterval(interval, void 0, getMagnitude(interval));
positions = axis.getLinearTickPositions(interval, realMin, realMax).map(log.log2lin);
if (!minor) {
log.minorAutoInterval = interval / 5;
}
}
// Set the axis-level tickInterval variable
if (!minor) {
axis.tickInterval = interval;
}
return positions;
};
LogarithmicAxisAdditions.prototype.lin2log = function (num) {
return Math.pow(10, num);
};
LogarithmicAxisAdditions.prototype.log2lin = function (num) {
return Math.log(num) / Math.LN10;
};
return LogarithmicAxisAdditions;
}());
var LogarithmicAxis = /** @class */ (function () {
function LogarithmicAxis() {
}
/**
* Provides logarithmic support for axes.
*
* @private
*/
LogarithmicAxis.compose = function (AxisClass) {
AxisClass.keepProps.push('logarithmic');
// HC <= 8 backwards compatibility, allow wrapping
// Axis.prototype.lin2log and log2lin
// @todo Remove this in next major
var axisProto = AxisClass.prototype;
var logAxisProto = LogarithmicAxisAdditions.prototype;
axisProto.log2lin = logAxisProto.log2lin;
axisProto.lin2log = logAxisProto.lin2log;
/* eslint-disable no-invalid-this */
addEvent(AxisClass, 'init', function (e) {
var axis = this;
var options = e.userOptions;
var logarithmic = axis.logarithmic;
if (options.type !== 'logarithmic') {
axis.logarithmic = void 0;
}
else {
if (!logarithmic) {
logarithmic = axis.logarithmic = new LogarithmicAxisAdditions(axis);
}
// HC <= 8 backwards compatibility, allow wrapping
// Axis.prototype.lin2log and log2lin
// @todo Remove this in next major
if (axis.log2lin !== logarithmic.log2lin) {
logarithmic.log2lin = axis.log2lin.bind(axis);
}
if (axis.lin2log !== logarithmic.lin2log) {
logarithmic.lin2log = axis.lin2log.bind(axis);
}
}
});
addEvent(AxisClass, 'afterInit', function () {
var axis = this;
var log = axis.logarithmic;
// extend logarithmic axis
if (log) {
axis.lin2val = function (num) {
return log.lin2log(num);
};
axis.val2lin = function (num) {
return log.log2lin(num);
};
}
});
};
return LogarithmicAxis;
}());
LogarithmicAxis.compose(Axis); // @todo move to factory functions
export default LogarithmicAxis;
+157
View File
@@ -0,0 +1,157 @@
/* *
*
* (c) 2010-2020 Torstein Honsi
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import Axis from './Axis.js';
import U from '../Utilities.js';
var addEvent = U.addEvent, pick = U.pick;
/**
* Map support for axes.
* @private
* @class
*/
var MapAxisAdditions = /** @class */ (function () {
/* *
*
* Constructors
*
* */
function MapAxisAdditions(axis) {
this.axis = axis;
}
return MapAxisAdditions;
}());
/**
* Axis with map support.
* @private
* @class
*/
var MapAxis = /** @class */ (function () {
function MapAxis() {
}
/**
* Extends axes with map support.
* @private
*
* @param {Highcharts.Axis} AxisClass
* Axis class to extend.
*/
MapAxis.compose = function (AxisClass) {
AxisClass.keepProps.push('mapAxis');
/* eslint-disable no-invalid-this */
addEvent(AxisClass, 'init', function () {
var axis = this;
if (!axis.mapAxis) {
axis.mapAxis = new MapAxisAdditions(axis);
}
});
// Override to use the extreme coordinates from the SVG shape, not the
// data values
addEvent(AxisClass, 'getSeriesExtremes', function () {
if (!this.mapAxis) {
return;
}
var axis = this;
var xData = [];
// Remove the xData array and cache it locally so that the proceed
// method doesn't use it
if (axis.isXAxis) {
axis.series.forEach(function (series, i) {
if (series.useMapGeometry) {
xData[i] = series.xData;
series.xData = [];
}
});
axis.mapAxis.seriesXData = xData;
}
});
addEvent(AxisClass, 'afterGetSeriesExtremes', function () {
if (!this.mapAxis) {
return;
}
var axis = this;
var xData = axis.mapAxis.seriesXData || [];
var dataMin, dataMax, useMapGeometry;
// Run extremes logic for map and mapline
if (axis.isXAxis) {
dataMin = pick(axis.dataMin, Number.MAX_VALUE);
dataMax = pick(axis.dataMax, -Number.MAX_VALUE);
axis.series.forEach(function (series, i) {
if (series.useMapGeometry) {
dataMin = Math.min(dataMin, pick(series.minX, dataMin));
dataMax = Math.max(dataMax, pick(series.maxX, dataMax));
series.xData = xData[i]; // Reset xData array
useMapGeometry = true;
}
});
if (useMapGeometry) {
axis.dataMin = dataMin;
axis.dataMax = dataMax;
}
axis.mapAxis.seriesXData = void 0;
}
});
// Override axis translation to make sure the aspect ratio is always
// kept
addEvent(AxisClass, 'afterSetAxisTranslation', function () {
if (!this.mapAxis) {
return;
}
var axis = this;
var chart = axis.chart;
var plotRatio = chart.plotWidth / chart.plotHeight;
var xAxis = chart.xAxis[0];
var mapRatio, adjustedAxisLength, padAxis, fixTo, fixDiff, preserveAspectRatio;
// Check for map-like series
if (axis.coll === 'yAxis' && typeof xAxis.transA !== 'undefined') {
axis.series.forEach(function (series) {
if (series.preserveAspectRatio) {
preserveAspectRatio = true;
}
});
}
// On Y axis, handle both
if (preserveAspectRatio) {
// Use the same translation for both axes
axis.transA = xAxis.transA = Math.min(axis.transA, xAxis.transA);
mapRatio = plotRatio / ((xAxis.max - xAxis.min) /
(axis.max - axis.min));
// What axis to pad to put the map in the middle
padAxis = mapRatio < 1 ? axis : xAxis;
// Pad it
adjustedAxisLength =
(padAxis.max - padAxis.min) * padAxis.transA;
padAxis.mapAxis.pixelPadding = padAxis.len - adjustedAxisLength;
padAxis.minPixelPadding = padAxis.mapAxis.pixelPadding / 2;
fixTo = padAxis.mapAxis.fixTo;
if (fixTo) {
fixDiff = fixTo[1] - padAxis.toValue(fixTo[0], true);
fixDiff *= padAxis.transA;
if (Math.abs(fixDiff) > padAxis.minPixelPadding ||
(padAxis.min === padAxis.dataMin &&
padAxis.max === padAxis.dataMax)) { // zooming out again, keep within restricted area
fixDiff = 0;
}
padAxis.minPixelPadding -= fixDiff;
}
}
});
// Override Axis.render in order to delete the fixTo prop
addEvent(AxisClass, 'render', function () {
var axis = this;
if (axis.mapAxis) {
axis.mapAxis.fixTo = void 0;
}
});
/* eslint-enable no-invalid-this */
};
return MapAxis;
}());
MapAxis.compose(Axis); // @todo move to factory functions
export default MapAxis;
@@ -0,0 +1,164 @@
/* *
*
* (c) 2010-2020 Torstein Honsi
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import H from '../Globals.js';
var isTouchDevice = H.isTouchDevice;
import U from '../Utilities.js';
var addEvent = U.addEvent, correctFloat = U.correctFloat, defined = U.defined, isNumber = U.isNumber, pick = U.pick;
/* eslint-disable valid-jsdoc */
/**
* @private
* @class
*/
var NavigatorAxisAdditions = /** @class */ (function () {
/* *
*
* Constructors
*
* */
function NavigatorAxisAdditions(axis) {
this.axis = axis;
}
/* *
*
* Functions
*
* */
/**
* @private
*/
NavigatorAxisAdditions.prototype.destroy = function () {
this.axis = void 0;
};
/**
* Add logic to normalize the zoomed range in order to preserve the pressed
* state of range selector buttons
*
* @private
* @function Highcharts.Axis#toFixedRange
* @param {number} [pxMin]
* @param {number} [pxMax]
* @param {number} [fixedMin]
* @param {number} [fixedMax]
* @return {*}
*/
NavigatorAxisAdditions.prototype.toFixedRange = function (pxMin, pxMax, fixedMin, fixedMax) {
var navigator = this;
var axis = navigator.axis;
var chart = axis.chart;
var fixedRange = chart && chart.fixedRange, halfPointRange = (axis.pointRange || 0) / 2, newMin = pick(fixedMin, axis.translate(pxMin, true, !axis.horiz)), newMax = pick(fixedMax, axis.translate(pxMax, true, !axis.horiz)), changeRatio = fixedRange && (newMax - newMin) / fixedRange;
// Add/remove half point range to/from the extremes (#1172)
if (!defined(fixedMin)) {
newMin = correctFloat(newMin + halfPointRange);
}
if (!defined(fixedMax)) {
newMax = correctFloat(newMax - halfPointRange);
}
// If the difference between the fixed range and the actual requested
// range is too great, the user is dragging across an ordinal gap, and
// we need to release the range selector button.
if (changeRatio > 0.7 && changeRatio < 1.3) {
if (fixedMax) {
newMin = newMax - fixedRange;
}
else {
newMax = newMin + fixedRange;
}
}
if (!isNumber(newMin) || !isNumber(newMax)) { // #1195, #7411
newMin = newMax = void 0;
}
return {
min: newMin,
max: newMax
};
};
return NavigatorAxisAdditions;
}());
/**
* @private
* @class
*/
var NavigatorAxis = /** @class */ (function () {
function NavigatorAxis() {
}
/* *
*
* Static Functions
*
* */
/**
* @private
*/
NavigatorAxis.compose = function (AxisClass) {
AxisClass.keepProps.push('navigatorAxis');
/* eslint-disable no-invalid-this */
addEvent(AxisClass, 'init', function () {
var axis = this;
if (!axis.navigatorAxis) {
axis.navigatorAxis = new NavigatorAxisAdditions(axis);
}
});
// For Stock charts, override selection zooming with some special
// features because X axis zooming is already allowed by the Navigator
// and Range selector.
addEvent(AxisClass, 'zoom', function (e) {
var axis = this;
var chart = axis.chart;
var chartOptions = chart.options;
var navigator = chartOptions.navigator;
var navigatorAxis = axis.navigatorAxis;
var pinchType = chartOptions.chart.pinchType;
var rangeSelector = chartOptions.rangeSelector;
var zoomType = chartOptions.chart.zoomType;
var previousZoom;
if (axis.isXAxis && ((navigator && navigator.enabled) ||
(rangeSelector && rangeSelector.enabled))) {
// For y only zooming, ignore the X axis completely
if (zoomType === 'y') {
e.zoomed = false;
// For xy zooming, record the state of the zoom before zoom
// selection, then when the reset button is pressed, revert to
// this state. This should apply only if the chart is
// initialized with a range (#6612), otherwise zoom all the way
// out.
}
else if (((!isTouchDevice && zoomType === 'xy') ||
(isTouchDevice && pinchType === 'xy')) &&
axis.options.range) {
previousZoom = navigatorAxis.previousZoom;
if (defined(e.newMin)) {
navigatorAxis.previousZoom = [axis.min, axis.max];
}
else if (previousZoom) {
e.newMin = previousZoom[0];
e.newMax = previousZoom[1];
navigatorAxis.previousZoom = void 0;
}
}
}
if (typeof e.zoomed !== 'undefined') {
e.preventDefault();
}
});
/* eslint-enable no-invalid-this */
};
/* *
*
* Static Properties
*
* */
/**
* @private
*/
NavigatorAxis.AdditionsClass = NavigatorAxisAdditions;
return NavigatorAxis;
}());
export default NavigatorAxis;
@@ -0,0 +1,777 @@
/* *
*
* (c) 2010-2020 Torstein Honsi
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import Axis from './Axis.js';
import H from '../Globals.js';
import U from '../Utilities.js';
var addEvent = U.addEvent, css = U.css, defined = U.defined, pick = U.pick, timeUnits = U.timeUnits;
import '../Chart/Chart.js';
// Has a dependency on Navigator due to the use of Axis.toFixedRange
import '../Navigator.js';
import '../Series/Series.js';
var Chart = H.Chart, Series = H.Series;
/**
* Extends the axis with ordinal support.
* @private
*/
var OrdinalAxis;
(function (OrdinalAxis) {
/* *
*
* Classes
*
* */
/**
* @private
*/
var Composition = /** @class */ (function () {
/* *
*
* Constructors
*
* */
/**
* @private
*/
function Composition(axis) {
this.index = {};
this.axis = axis;
}
/* *
*
* Functions
*
* */
/**
* Calculate the ordinal positions before tick positions are calculated.
*
* @private
*/
Composition.prototype.beforeSetTickPositions = function () {
var axis = this.axis, ordinal = axis.ordinal, len, ordinalPositions = [], uniqueOrdinalPositions, useOrdinal = false, dist, extremes = axis.getExtremes(), min = extremes.min, max = extremes.max, minIndex, maxIndex, slope, hasBreaks = axis.isXAxis && !!axis.options.breaks, isOrdinal = axis.options.ordinal, overscrollPointsRange = Number.MAX_VALUE, ignoreHiddenSeries = axis.chart.options.chart.ignoreHiddenSeries, i, hasBoostedSeries;
// Apply the ordinal logic
if (isOrdinal || hasBreaks) { // #4167 YAxis is never ordinal ?
axis.series.forEach(function (series, i) {
uniqueOrdinalPositions = [];
if ((!ignoreHiddenSeries || series.visible !== false) &&
(series.takeOrdinalPosition !== false || hasBreaks)) {
// concatenate the processed X data into the existing
// positions, or the empty array
ordinalPositions = ordinalPositions.concat(series.processedXData);
len = ordinalPositions.length;
// remove duplicates (#1588)
ordinalPositions.sort(function (a, b) {
// without a custom function it is sorted as strings
return a - b;
});
overscrollPointsRange = Math.min(overscrollPointsRange, pick(
// Check for a single-point series:
series.closestPointRange, overscrollPointsRange));
if (len) {
i = 0;
while (i < len - 1) {
if (ordinalPositions[i] !== ordinalPositions[i + 1]) {
uniqueOrdinalPositions.push(ordinalPositions[i + 1]);
}
i++;
}
// Check first item:
if (uniqueOrdinalPositions[0] !== ordinalPositions[0]) {
uniqueOrdinalPositions.unshift(ordinalPositions[0]);
}
ordinalPositions = uniqueOrdinalPositions;
}
}
if (series.isSeriesBoosting) {
hasBoostedSeries = true;
}
});
if (hasBoostedSeries) {
ordinalPositions.length = 0;
}
// cache the length
len = ordinalPositions.length;
// Check if we really need the overhead of mapping axis data
// against the ordinal positions. If the series consist of
// evenly spaced data any way, we don't need any ordinal logic.
if (len > 2) { // two points have equal distance by default
dist = ordinalPositions[1] - ordinalPositions[0];
i = len - 1;
while (i-- && !useOrdinal) {
if (ordinalPositions[i + 1] - ordinalPositions[i] !== dist) {
useOrdinal = true;
}
}
// When zooming in on a week, prevent axis padding for
// weekends even though the data within the week is evenly
// spaced.
if (!axis.options.keepOrdinalPadding &&
(ordinalPositions[0] - min > dist ||
max - ordinalPositions[ordinalPositions.length - 1] >
dist)) {
useOrdinal = true;
}
}
else if (axis.options.overscroll) {
if (len === 2) {
// Exactly two points, distance for overscroll is fixed:
overscrollPointsRange =
ordinalPositions[1] - ordinalPositions[0];
}
else if (len === 1) {
// We have just one point, closest distance is unknown.
// Assume then it is last point and overscrolled range:
overscrollPointsRange = axis.options.overscroll;
ordinalPositions = [
ordinalPositions[0],
ordinalPositions[0] + overscrollPointsRange
];
}
else {
// In case of zooming in on overscrolled range, stick to
// the old range:
overscrollPointsRange = ordinal.overscrollPointsRange;
}
}
// Record the slope and offset to compute the linear values from
// the array index. Since the ordinal positions may exceed the
// current range, get the start and end positions within it
// (#719, #665b)
if (useOrdinal) {
if (axis.options.overscroll) {
ordinal.overscrollPointsRange = overscrollPointsRange;
ordinalPositions = ordinalPositions.concat(ordinal.getOverscrollPositions());
}
// Register
ordinal.positions = ordinalPositions;
// This relies on the ordinalPositions being set. Use
// Math.max and Math.min to prevent padding on either sides
// of the data.
minIndex = axis.ordinal2lin(// #5979
Math.max(min, ordinalPositions[0]), true);
maxIndex = Math.max(axis.ordinal2lin(Math.min(max, ordinalPositions[ordinalPositions.length - 1]), true), 1); // #3339
// Set the slope and offset of the values compared to the
// indices in the ordinal positions
ordinal.slope = slope = (max - min) / (maxIndex - minIndex);
ordinal.offset = min - (minIndex * slope);
}
else {
ordinal.overscrollPointsRange = pick(axis.closestPointRange, ordinal.overscrollPointsRange);
ordinal.positions = axis.ordinal.slope = ordinal.offset =
void 0;
}
}
axis.isOrdinal = isOrdinal && useOrdinal; // #3818, #4196, #4926
ordinal.groupIntervalFactor = null; // reset for next run
};
/**
* Get the ordinal positions for the entire data set. This is necessary
* in chart panning because we need to find out what points or data
* groups are available outside the visible range. When a panning
* operation starts, if an index for the given grouping does not exists,
* it is created and cached. This index is deleted on updated data, so
* it will be regenerated the next time a panning operation starts.
*
* @private
*/
Composition.prototype.getExtendedPositions = function () {
var ordinal = this, axis = ordinal.axis, axisProto = axis.constructor.prototype, chart = axis.chart, grouping = axis.series[0].currentDataGrouping, ordinalIndex = ordinal.index, key = grouping ?
grouping.count + grouping.unitName :
'raw', overscroll = axis.options.overscroll, extremes = axis.getExtremes(), fakeAxis, fakeSeries;
// If this is the first time, or the ordinal index is deleted by
// updatedData,
// create it.
if (!ordinalIndex) {
ordinalIndex = ordinal.index = {};
}
if (!ordinalIndex[key]) {
// Create a fake axis object where the extended ordinal
// positions are emulated
fakeAxis = {
series: [],
chart: chart,
getExtremes: function () {
return {
min: extremes.dataMin,
max: extremes.dataMax + overscroll
};
},
options: {
ordinal: true
},
ordinal: {},
ordinal2lin: axisProto.ordinal2lin,
val2lin: axisProto.val2lin // #2590
};
fakeAxis.ordinal.axis = fakeAxis;
// Add the fake series to hold the full data, then apply
// processData to it
axis.series.forEach(function (series) {
fakeSeries = {
xAxis: fakeAxis,
xData: series.xData.slice(),
chart: chart,
destroyGroupedData: H.noop,
getProcessedData: H.Series.prototype.getProcessedData
};
fakeSeries.xData = fakeSeries.xData.concat(ordinal.getOverscrollPositions());
fakeSeries.options = {
dataGrouping: grouping ? {
enabled: true,
forced: true,
// doesn't matter which, use the fastest
approximation: 'open',
units: [[
grouping.unitName,
[grouping.count]
]]
} : {
enabled: false
}
};
series.processData.apply(fakeSeries);
fakeAxis.series.push(fakeSeries);
});
// Run beforeSetTickPositions to compute the ordinalPositions
axis.ordinal.beforeSetTickPositions.apply({ axis: fakeAxis });
// Cache it
ordinalIndex[key] = fakeAxis.ordinal.positions;
}
return ordinalIndex[key];
};
/**
* Find the factor to estimate how wide the plot area would have been if
* ordinal gaps were included. This value is used to compute an imagined
* plot width in order to establish the data grouping interval.
*
* A real world case is the intraday-candlestick example. Without this
* logic, it would show the correct data grouping when viewing a range
* within each day, but once moving the range to include the gap between
* two days, the interval would include the cut-away night hours and the
* data grouping would be wrong. So the below method tries to compensate
* by identifying the most common point interval, in this case days.
*
* An opposite case is presented in issue #718. We have a long array of
* daily data, then one point is appended one hour after the last point.
* We expect the data grouping not to change.
*
* In the future, if we find cases where this estimation doesn't work
* optimally, we might need to add a second pass to the data grouping
* logic, where we do another run with a greater interval if the number
* of data groups is more than a certain fraction of the desired group
* count.
*
* @private
*/
Composition.prototype.getGroupIntervalFactor = function (xMin, xMax, series) {
var ordinal = this, axis = ordinal.axis, i, processedXData = series.processedXData, len = processedXData.length, distances = [], median, groupIntervalFactor = ordinal.groupIntervalFactor;
// Only do this computation for the first series, let the other
// inherit it (#2416)
if (!groupIntervalFactor) {
// Register all the distances in an array
for (i = 0; i < len - 1; i++) {
distances[i] =
processedXData[i + 1] - processedXData[i];
}
// Sort them and find the median
distances.sort(function (a, b) {
return a - b;
});
median = distances[Math.floor(len / 2)];
// Compensate for series that don't extend through the entire
// axis extent. #1675.
xMin = Math.max(xMin, processedXData[0]);
xMax = Math.min(xMax, processedXData[len - 1]);
ordinal.groupIntervalFactor = groupIntervalFactor =
(len * median) / (xMax - xMin);
}
// Return the factor needed for data grouping
return groupIntervalFactor;
};
/**
* Get ticks for an ordinal axis within a range where points don't
* exist. It is required when overscroll is enabled. We can't base on
* points, because we may not have any, so we use approximated
* pointRange and generate these ticks between Axis.dataMax,
* Axis.dataMax + Axis.overscroll evenly spaced. Used in panning and
* navigator scrolling.
*
* @private
*/
Composition.prototype.getOverscrollPositions = function () {
var ordinal = this, axis = ordinal.axis, extraRange = axis.options.overscroll, distance = ordinal.overscrollPointsRange, positions = [], max = axis.dataMax;
if (defined(distance)) {
// Max + pointRange because we need to scroll to the last
positions.push(max);
while (max <= axis.dataMax + extraRange) {
max += distance;
positions.push(max);
}
}
return positions;
};
/**
* Make the tick intervals closer because the ordinal gaps make the
* ticks spread out or cluster.
*
* @private
*/
Composition.prototype.postProcessTickInterval = function (tickInterval) {
// Problem: https://jsfiddle.net/highcharts/FQm4E/1/
// This is a case where this algorithm doesn't work optimally. In
// this case, the tick labels are spread out per week, but all the
// gaps reside within weeks. So we have a situation where the labels
// are courser than the ordinal gaps, and thus the tick interval
// should not be altered.
var ordinal = this, axis = ordinal.axis, ordinalSlope = ordinal.slope, ret;
if (ordinalSlope) {
if (!axis.options.breaks) {
ret = tickInterval / (ordinalSlope / axis.closestPointRange);
}
else {
ret = axis.closestPointRange || tickInterval; // #7275
}
}
else {
ret = tickInterval;
}
return ret;
};
return Composition;
}());
OrdinalAxis.Composition = Composition;
/* *
*
* Functions
*
* */
/**
* Extends the axis with ordinal support.
*
* @private
*
* @param AxisClass
* Axis class to extend.
*
* @param ChartClass
* Chart class to use.
*
* @param SeriesClass
* Series class to use.
*/
function compose(AxisClass, ChartClass, SeriesClass) {
AxisClass.keepProps.push('ordinal');
var axisProto = AxisClass.prototype;
/**
* In an ordinal axis, there might be areas with dense consentrations of
* points, then large gaps between some. Creating equally distributed
* ticks over this entire range may lead to a huge number of ticks that
* will later be removed. So instead, break the positions up in
* segments, find the tick positions for each segment then concatenize
* them. This method is used from both data grouping logic and X axis
* tick position logic.
*
* @private
*/
AxisClass.prototype.getTimeTicks = function (normalizedInterval, min, max, startOfWeek, positions, closestDistance, findHigherRanks) {
if (positions === void 0) { positions = []; }
if (closestDistance === void 0) { closestDistance = 0; }
var start = 0, end, segmentPositions, higherRanks = {}, hasCrossedHigherRank, info, posLength, outsideMax, groupPositions = [], lastGroupPosition = -Number.MAX_VALUE, tickPixelIntervalOption = this.options.tickPixelInterval, time = this.chart.time,
// Record all the start positions of a segment, to use when
// deciding what's a gap in the data.
segmentStarts = [];
// The positions are not always defined, for example for ordinal
// positions when data has regular interval (#1557, #2090)
if ((!this.options.ordinal && !this.options.breaks) ||
!positions ||
positions.length < 3 ||
typeof min === 'undefined') {
return time.getTimeTicks.apply(time, arguments);
}
// Analyze the positions array to split it into segments on gaps
// larger than 5 times the closest distance. The closest distance is
// already found at this point, so we reuse that instead of
// computing it again.
posLength = positions.length;
for (end = 0; end < posLength; end++) {
outsideMax = end && positions[end - 1] > max;
if (positions[end] < min) { // Set the last position before min
start = end;
}
if (end === posLength - 1 ||
positions[end + 1] - positions[end] > closestDistance * 5 ||
outsideMax) {
// For each segment, calculate the tick positions from the
// getTimeTicks utility function. The interval will be the
// same regardless of how long the segment is.
if (positions[end] > lastGroupPosition) { // #1475
segmentPositions = time.getTimeTicks(normalizedInterval, positions[start], positions[end], startOfWeek);
// Prevent duplicate groups, for example for multiple
// segments within one larger time frame (#1475)
while (segmentPositions.length &&
segmentPositions[0] <= lastGroupPosition) {
segmentPositions.shift();
}
if (segmentPositions.length) {
lastGroupPosition =
segmentPositions[segmentPositions.length - 1];
}
segmentStarts.push(groupPositions.length);
groupPositions = groupPositions.concat(segmentPositions);
}
// Set start of next segment
start = end + 1;
}
if (outsideMax) {
break;
}
}
// Get the grouping info from the last of the segments. The info is
// the same for all segments.
info = segmentPositions.info;
// Optionally identify ticks with higher rank, for example when the
// ticks have crossed midnight.
if (findHigherRanks && info.unitRange <= timeUnits.hour) {
end = groupPositions.length - 1;
// Compare points two by two
for (start = 1; start < end; start++) {
if (time.dateFormat('%d', groupPositions[start]) !==
time.dateFormat('%d', groupPositions[start - 1])) {
higherRanks[groupPositions[start]] = 'day';
hasCrossedHigherRank = true;
}
}
// If the complete array has crossed midnight, we want to mark
// the first positions also as higher rank
if (hasCrossedHigherRank) {
higherRanks[groupPositions[0]] = 'day';
}
info.higherRanks = higherRanks;
}
// Save the info
info.segmentStarts = segmentStarts;
groupPositions.info = info;
// Don't show ticks within a gap in the ordinal axis, where the
// space between two points is greater than a portion of the tick
// pixel interval
if (findHigherRanks && defined(tickPixelIntervalOption)) {
var length = groupPositions.length, i = length, itemToRemove, translated, translatedArr = [], lastTranslated, medianDistance, distance, distances = [];
// Find median pixel distance in order to keep a reasonably even
// distance between ticks (#748)
while (i--) {
translated = this.translate(groupPositions[i]);
if (lastTranslated) {
distances[i] = lastTranslated - translated;
}
translatedArr[i] = lastTranslated = translated;
}
distances.sort();
medianDistance = distances[Math.floor(distances.length / 2)];
if (medianDistance < tickPixelIntervalOption * 0.6) {
medianDistance = null;
}
// Now loop over again and remove ticks where needed
i = groupPositions[length - 1] > max ? length - 1 : length; // #817
lastTranslated = void 0;
while (i--) {
translated = translatedArr[i];
distance = Math.abs(lastTranslated - translated);
// #4175 - when axis is reversed, the distance, is negative
// but tickPixelIntervalOption positive, so we need to
// compare the same values
// Remove ticks that are closer than 0.6 times the pixel
// interval from the one to the right, but not if it is
// close to the median distance (#748).
if (lastTranslated &&
distance < tickPixelIntervalOption * 0.8 &&
(medianDistance === null || distance < medianDistance * 0.8)) {
// Is this a higher ranked position with a normal
// position to the right?
if (higherRanks[groupPositions[i]] &&
!higherRanks[groupPositions[i + 1]]) {
// Yes: remove the lower ranked neighbour to the
// right
itemToRemove = i + 1;
lastTranslated = translated; // #709
}
else {
// No: remove this one
itemToRemove = i;
}
groupPositions.splice(itemToRemove, 1);
}
else {
lastTranslated = translated;
}
}
}
return groupPositions;
};
/**
* Translate from linear (internal) to axis value.
*
* @private
* @function Highcharts.Axis#lin2val
*
* @param {number} val
* The linear abstracted value.
*
* @param {boolean} [fromIndex]
* Translate from an index in the ordinal positions rather than a
* value.
*
* @return {number}
*/
axisProto.lin2val = function (val, fromIndex) {
var axis = this, ordinal = axis.ordinal, ordinalPositions = ordinal.positions, ret;
// the visible range contains only equally spaced values
if (!ordinalPositions) {
ret = val;
}
else {
var ordinalSlope = ordinal.slope, ordinalOffset = ordinal.offset, i = ordinalPositions.length - 1, linearEquivalentLeft, linearEquivalentRight, distance;
// Handle the case where we translate from the index directly,
// used only when panning an ordinal axis
if (fromIndex) {
if (val < 0) { // out of range, in effect panning to the left
val = ordinalPositions[0];
}
else if (val > i) { // out of range, panning to the right
val = ordinalPositions[i];
}
else { // split it up
i = Math.floor(val);
distance = val - i; // the decimal
}
// Loop down along the ordinal positions. When the linear
// equivalent of i matches an ordinal position, interpolate
// between the left and right values.
}
else {
while (i--) {
linearEquivalentLeft =
(ordinalSlope * i) + ordinalOffset;
if (val >= linearEquivalentLeft) {
linearEquivalentRight =
(ordinalSlope *
(i + 1)) +
ordinalOffset;
// something between 0 and 1
distance = (val - linearEquivalentLeft) /
(linearEquivalentRight - linearEquivalentLeft);
break;
}
}
}
// If the index is within the range of the ordinal positions,
// return the associated or interpolated value. If not, just
// return the value.
return (typeof distance !== 'undefined' &&
typeof ordinalPositions[i] !== 'undefined' ?
ordinalPositions[i] + (distance ?
distance *
(ordinalPositions[i + 1] - ordinalPositions[i]) :
0) :
val);
}
return ret;
};
/**
* Translate from a linear axis value to the corresponding ordinal axis
* position. If there are no gaps in the ordinal axis this will be the
* same. The translated value is the value that the point would have if
* the axis were linear, using the same min and max.
*
* @private
* @function Highcharts.Axis#val2lin
*
* @param {number} val
* The axis value.
*
* @param {boolean} [toIndex]
* Whether to return the index in the ordinalPositions or the new value.
*
* @return {number}
*/
axisProto.val2lin = function (val, toIndex) {
var axis = this, ordinal = axis.ordinal, ordinalPositions = ordinal.positions, ret;
if (!ordinalPositions) {
ret = val;
}
else {
var ordinalLength = ordinalPositions.length, i, distance, ordinalIndex;
// first look for an exact match in the ordinalpositions array
i = ordinalLength;
while (i--) {
if (ordinalPositions[i] === val) {
ordinalIndex = i;
break;
}
}
// if that failed, find the intermediate position between the
// two nearest values
i = ordinalLength - 1;
while (i--) {
if (val > ordinalPositions[i] || i === 0) { // interpolate
// something between 0 and 1
distance = (val - ordinalPositions[i]) /
(ordinalPositions[i + 1] - ordinalPositions[i]);
ordinalIndex = i + distance;
break;
}
}
ret = toIndex ?
ordinalIndex :
ordinal.slope *
(ordinalIndex || 0) +
ordinal.offset;
}
return ret;
};
// Record this to prevent overwriting by broken-axis module (#5979)
axisProto.ordinal2lin = axisProto.val2lin;
/* eslint-disable no-invalid-this */
addEvent(AxisClass, 'afterInit', function () {
var axis = this;
if (!axis.ordinal) {
axis.ordinal = new OrdinalAxis.Composition(axis);
}
});
addEvent(AxisClass, 'foundExtremes', function () {
var axis = this;
if (axis.isXAxis &&
defined(axis.options.overscroll) &&
axis.max === axis.dataMax &&
(
// Panning is an execption. We don't want to apply
// overscroll when panning over the dataMax
!axis.chart.mouseIsDown ||
axis.isInternal) && (
// Scrollbar buttons are the other execption:
!axis.eventArgs ||
axis.eventArgs && axis.eventArgs.trigger !== 'navigator')) {
axis.max += axis.options.overscroll;
// Live data and buttons require translation for the min:
if (!axis.isInternal && defined(axis.userMin)) {
axis.min += axis.options.overscroll;
}
}
});
// For ordinal axis, that loads data async, redraw axis after data is
// loaded. If we don't do that, axis will have the same extremes as
// previously, but ordinal positions won't be calculated. See #10290
addEvent(AxisClass, 'afterSetScale', function () {
var axis = this;
if (axis.horiz && !axis.isDirty) {
axis.isDirty = axis.isOrdinal &&
axis.chart.navigator &&
!axis.chart.navigator.adaptToUpdatedData;
}
});
addEvent(AxisClass, 'initialAxisTranslation', function () {
var axis = this;
if (axis.ordinal) {
axis.ordinal.beforeSetTickPositions();
axis.tickInterval = axis.ordinal.postProcessTickInterval(axis.tickInterval);
}
});
// Extending the Chart.pan method for ordinal axes
addEvent(ChartClass, 'pan', function (e) {
var chart = this, xAxis = chart.xAxis[0], overscroll = xAxis.options.overscroll, chartX = e.originalEvent.chartX, panning = chart.options.chart &&
chart.options.chart.panning, runBase = false;
if (panning &&
panning.type !== 'y' &&
xAxis.options.ordinal &&
xAxis.series.length) {
var mouseDownX = chart.mouseDownX, extremes = xAxis.getExtremes(), dataMax = extremes.dataMax, min = extremes.min, max = extremes.max, trimmedRange, hoverPoints = chart.hoverPoints, closestPointRange = (xAxis.closestPointRange ||
(xAxis.ordinal && xAxis.ordinal.overscrollPointsRange)), pointPixelWidth = (xAxis.translationSlope *
(xAxis.ordinal.slope || closestPointRange)),
// how many ordinal units did we move?
movedUnits = (mouseDownX - chartX) / pointPixelWidth,
// get index of all the chart's points
extendedAxis = { ordinal: { positions: xAxis.ordinal.getExtendedPositions() } }, ordinalPositions, searchAxisLeft, lin2val = xAxis.lin2val, val2lin = xAxis.val2lin, searchAxisRight;
// we have an ordinal axis, but the data is equally spaced
if (!extendedAxis.ordinal.positions) {
runBase = true;
}
else if (Math.abs(movedUnits) > 1) {
// Remove active points for shared tooltip
if (hoverPoints) {
hoverPoints.forEach(function (point) {
point.setState();
});
}
if (movedUnits < 0) {
searchAxisLeft = extendedAxis;
searchAxisRight = xAxis.ordinal.positions ? xAxis : extendedAxis;
}
else {
searchAxisLeft = xAxis.ordinal.positions ? xAxis : extendedAxis;
searchAxisRight = extendedAxis;
}
// In grouped data series, the last ordinal position
// represents the grouped data, which is to the left of the
// real data max. If we don't compensate for this, we will
// be allowed to pan grouped data series passed the right of
// the plot area.
ordinalPositions = searchAxisRight.ordinal.positions;
if (dataMax >
ordinalPositions[ordinalPositions.length - 1]) {
ordinalPositions.push(dataMax);
}
// Get the new min and max values by getting the ordinal
// index for the current extreme, then add the moved units
// and translate back to values. This happens on the
// extended ordinal positions if the new position is out of
// range, else it happens on the current x axis which is
// smaller and faster.
chart.fixedRange = max - min;
trimmedRange = xAxis.navigatorAxis.toFixedRange(null, null, lin2val.apply(searchAxisLeft, [
val2lin.apply(searchAxisLeft, [min, true]) + movedUnits,
true // translate from index
]), lin2val.apply(searchAxisRight, [
val2lin.apply(searchAxisRight, [max, true]) + movedUnits,
true // translate from index
]));
// Apply it if it is within the available data range
if (trimmedRange.min >= Math.min(extremes.dataMin, min) &&
trimmedRange.max <= Math.max(dataMax, max) + overscroll) {
xAxis.setExtremes(trimmedRange.min, trimmedRange.max, true, false, { trigger: 'pan' });
}
chart.mouseDownX = chartX; // set new reference for next run
css(chart.container, { cursor: 'move' });
}
}
else {
runBase = true;
}
// revert to the linear chart.pan version
if (runBase || (panning && /y/.test(panning.type))) {
if (overscroll) {
xAxis.max = xAxis.dataMax + overscroll;
}
}
else {
e.preventDefault();
}
});
addEvent(SeriesClass, 'updatedData', function () {
var xAxis = this.xAxis;
// Destroy the extended ordinal index on updated data
if (xAxis && xAxis.options.ordinal) {
delete xAxis.ordinal.index;
}
});
/* eslint-enable no-invalid-this */
}
OrdinalAxis.compose = compose;
})(OrdinalAxis || (OrdinalAxis = {}));
OrdinalAxis.compose(Axis, Chart, Series); // @todo move to StockChart, remove from master
export default OrdinalAxis;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,863 @@
/* *
*
* (c) 2010-2020 Torstein Honsi
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import Axis from './Axis.js';
import Tick from './Tick.js';
import HiddenAxis from './HiddenAxis.js';
import U from '../Utilities.js';
var addEvent = U.addEvent, correctFloat = U.correctFloat, defined = U.defined, extend = U.extend, fireEvent = U.fireEvent, isNumber = U.isNumber, merge = U.merge, pick = U.pick, pInt = U.pInt, relativeLength = U.relativeLength, wrap = U.wrap;
/**
* @private
* @class
*/
var RadialAxis = /** @class */ (function () {
function RadialAxis() {
}
/* *
*
* Static Functions
*
* */
RadialAxis.init = function (axis) {
var axisProto = Axis.prototype;
// Merge and set options.
axis.setOptions = function (userOptions) {
var options = this.options = merge(axis.constructor.defaultOptions, this.defaultPolarOptions, userOptions);
// Make sure the plotBands array is instanciated for each Axis
// (#2649)
if (!options.plotBands) {
options.plotBands = [];
}
fireEvent(this, 'afterSetOptions');
};
// Wrap the getOffset method to return zero offset for title or labels
// in a radial axis.
axis.getOffset = function () {
// Call the Axis prototype method (the method we're in now is on the
// instance)
axisProto.getOffset.call(this);
// Title or label offsets are not counted
this.chart.axisOffset[this.side] = 0;
};
/**
* Get the path for the axis line. This method is also referenced in the
* getPlotLinePath method.
*
* @private
*
* @param {number} _lineWidth
* Line width is not used.
*
* @param {number} [radius]
* Radius of radial path.
*
* @param {number} [innerRadius]
* Inner radius of radial path.
*
* @return {RadialAxisPath}
*/
axis.getLinePath = function (_lineWidth, radius, innerRadius) {
var center = this.pane.center, end, chart = this.chart, r = pick(radius, center[2] / 2 - this.offset), path;
if (typeof innerRadius === 'undefined') {
innerRadius = this.horiz ? 0 : this.center && -this.center[3] / 2;
}
// In case when innerSize of pane is set, it must be included
if (innerRadius) {
r += innerRadius;
}
if (this.isCircular || typeof radius !== 'undefined') {
path = this.chart.renderer.symbols.arc(this.left + center[0], this.top + center[1], r, r, {
start: this.startAngleRad,
end: this.endAngleRad,
open: true,
innerR: 0
});
// Bounds used to position the plotLine label next to the line
// (#7117)
path.xBounds = [this.left + center[0]];
path.yBounds = [this.top + center[1] - r];
}
else {
end = this.postTranslate(this.angleRad, r);
path = [
['M', this.center[0] + chart.plotLeft, this.center[1] + chart.plotTop],
['L', end.x, end.y]
];
}
return path;
};
/**
* Override setAxisTranslation by setting the translation to the
* difference in rotation. This allows the translate method to return
* angle for any given value.
*
* @private
*/
axis.setAxisTranslation = function () {
// Call uber method
axisProto.setAxisTranslation.call(this);
// Set transA and minPixelPadding
if (this.center) { // it's not defined the first time
if (this.isCircular) {
this.transA = (this.endAngleRad - this.startAngleRad) /
((this.max - this.min) || 1);
}
else {
// The transA here is the length of the axis, so in case
// of inner radius, the length must be decreased by it
this.transA = ((this.center[2] - this.center[3]) / 2) /
((this.max - this.min) || 1);
}
if (this.isXAxis) {
this.minPixelPadding = this.transA * this.minPointOffset;
}
else {
// This is a workaround for regression #2593, but categories
// still don't position correctly.
this.minPixelPadding = 0;
}
}
};
/**
* In case of auto connect, add one closestPointRange to the max value
* right before tickPositions are computed, so that ticks will extend
* passed the real max.
* @private
*/
axis.beforeSetTickPositions = function () {
// If autoConnect is true, polygonal grid lines are connected, and
// one closestPointRange is added to the X axis to prevent the last
// point from overlapping the first.
this.autoConnect = (this.isCircular &&
typeof pick(this.userMax, this.options.max) === 'undefined' &&
correctFloat(this.endAngleRad - this.startAngleRad) ===
correctFloat(2 * Math.PI));
// This will lead to add an extra tick to xAxis in order to display
// a correct range on inverted polar
if (!this.isCircular && this.chart.inverted) {
this.max++;
}
if (this.autoConnect) {
this.max += ((this.categories && 1) ||
this.pointRange ||
this.closestPointRange ||
0); // #1197, #2260
}
};
/**
* Override the setAxisSize method to use the arc's circumference as
* length. This allows tickPixelInterval to apply to pixel lengths along
* the perimeter.
* @private
*/
axis.setAxisSize = function () {
var center, start;
axisProto.setAxisSize.call(this);
if (this.isRadial) {
// Set the center array
this.pane.updateCenter(this);
// In case when the innerSize is set in a polar chart, the axis'
// center cannot be a reference to pane's center
center = this.center = extend([], this.pane.center);
// The sector is used in Axis.translate to compute the
// translation of reversed axis points (#2570)
if (this.isCircular) {
this.sector = this.endAngleRad - this.startAngleRad;
}
else {
// When the pane's startAngle or the axis' angle is set then
// new x and y values for vertical axis' center must be
// calulated
start = this.postTranslate(this.angleRad, center[3] / 2);
center[0] = start.x - this.chart.plotLeft;
center[1] = start.y - this.chart.plotTop;
}
// Axis len is used to lay out the ticks
this.len = this.width = this.height =
(center[2] - center[3]) * pick(this.sector, 1) / 2;
}
};
/**
* Returns the x, y coordinate of a point given by a value and a pixel
* distance from center.
*
* @private
*
* @param {number} value
* Point value.
*
* @param {number} [length]
* Distance from center.
*
* @return {Highcharts.PositionObject}
*/
axis.getPosition = function (value, length) {
var translatedVal = this.translate(value);
return this.postTranslate(this.isCircular ? translatedVal : this.angleRad, // #2848
// In case when translatedVal is negative, the 0 value must be
// used instead, in order to deal with lines and labels that
// fall out of the visible range near the center of a pane
pick(this.isCircular ?
length :
(translatedVal < 0 ? 0 : translatedVal), this.center[2] / 2) - this.offset);
};
/**
* Translate from intermediate plotX (angle), plotY (axis.len - radius)
* to final chart coordinates.
*
* @private
*
* @param {number} angle
* Translation angle.
*
* @param {number} radius
* Translation radius.
*
* @return {Highcharts.PositionObject}
*/
axis.postTranslate = function (angle, radius) {
var chart = this.chart, center = this.center;
angle = this.startAngleRad + angle;
return {
x: chart.plotLeft + center[0] + Math.cos(angle) * radius,
y: chart.plotTop + center[1] + Math.sin(angle) * radius
};
};
/**
* Find the path for plot bands along the radial axis.
*
* @private
*
* @param {number} from
* From value.
*
* @param {number} to
* To value.
*
* @param {Highcharts.AxisPlotBandsOptions} options
* Band options.
*
* @return {RadialAxisPath}
*/
axis.getPlotBandPath = function (from, to, options) {
var radiusToPixels = function (radius) {
if (typeof radius === 'string') {
var r = parseInt(radius, 10);
if (percentRegex.test(radius)) {
r = (r * fullRadius) / 100;
}
return r;
}
return radius;
};
var center = this.center, startAngleRad = this.startAngleRad, fullRadius = center[2] / 2, offset = Math.min(this.offset, 0), percentRegex = /%$/, start, end, angle, xOnPerimeter, open, isCircular = this.isCircular, // X axis in a polar chart
path, outerRadius = pick(radiusToPixels(options.outerRadius), fullRadius), innerRadius = radiusToPixels(options.innerRadius), thickness = pick(radiusToPixels(options.thickness), 10);
// Polygonal plot bands
if (this.options.gridLineInterpolation === 'polygon') {
path = this.getPlotLinePath({ value: from }).concat(this.getPlotLinePath({ value: to, reverse: true }));
// Circular grid bands
}
else {
// Keep within bounds
from = Math.max(from, this.min);
to = Math.min(to, this.max);
var transFrom = this.translate(from);
var transTo = this.translate(to);
// Plot bands on Y axis (radial axis) - inner and outer
// radius depend on to and from
if (!isCircular) {
outerRadius = transFrom || 0;
innerRadius = transTo || 0;
}
// Handle full circle
if (options.shape === 'circle' || !isCircular) {
start = -Math.PI / 2;
end = Math.PI * 1.5;
open = true;
}
else {
start = startAngleRad + (transFrom || 0);
end = startAngleRad + (transTo || 0);
}
outerRadius -= offset; // #5283
thickness -= offset; // #5283
path = this.chart.renderer.symbols.arc(this.left + center[0], this.top + center[1], outerRadius, outerRadius, {
// Math is for reversed yAxis (#3606)
start: Math.min(start, end),
end: Math.max(start, end),
innerR: pick(innerRadius, outerRadius - thickness),
open: open
});
// Provide positioning boxes for the label (#6406)
if (isCircular) {
angle = (end + start) / 2;
xOnPerimeter = (this.left +
center[0] +
(center[2] / 2) * Math.cos(angle));
path.xBounds = angle > -Math.PI / 2 && angle < Math.PI / 2 ?
// Right hemisphere
[xOnPerimeter, this.chart.plotWidth] :
// Left hemisphere
[0, xOnPerimeter];
path.yBounds = [
this.top + center[1] + (center[2] / 2) * Math.sin(angle)
];
// Shift up or down to get the label clear of the perimeter
path.yBounds[0] += ((angle > -Math.PI && angle < 0) ||
(angle > Math.PI)) ? -10 : 10;
}
}
return path;
};
// Find the correct end values of crosshair in polar.
axis.getCrosshairPosition = function (options, x1, y1) {
var axis = this, value = options.value, center = axis.pane.center, shapeArgs, end, x2, y2;
if (axis.isCircular) {
if (!defined(value)) {
// When the snap is set to false
x2 = options.chartX || 0;
y2 = options.chartY || 0;
value = axis.translate(Math.atan2(y2 - y1, x2 - x1) - axis.startAngleRad, true);
}
else if (options.point) {
// When the snap is set to true
shapeArgs = options.point.shapeArgs || {};
if (shapeArgs.start) {
// Find a true value of the point based on the
// angle
value = axis.chart.inverted ?
axis.translate(options.point.rectPlotY, true) :
options.point.x;
}
}
end = axis.getPosition(value);
x2 = end.x;
y2 = end.y;
}
else {
if (!defined(value)) {
x2 = options.chartX;
y2 = options.chartY;
}
if (defined(x2) && defined(y2)) {
// Calculate radius of non-circular axis' crosshair
y1 = center[1] + axis.chart.plotTop;
value = axis.translate(Math.min(Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)), center[2] / 2) - center[3] / 2, true);
}
}
return [value, x2 || 0, y2 || 0];
};
// Find the path for plot lines perpendicular to the radial axis.
axis.getPlotLinePath = function (options) {
var axis = this, center = axis.pane.center, chart = axis.chart, inverted = chart.inverted, value = options.value, reverse = options.reverse, end = axis.getPosition(value), background = axis.pane.options.background ?
(axis.pane.options.background[0] ||
axis.pane.options.background) :
{}, innerRadius = background.innerRadius || '0%', outerRadius = background.outerRadius || '100%', x1 = center[0] + chart.plotLeft, y1 = center[1] + chart.plotTop, x2 = end.x, y2 = end.y, height = axis.height, isCrosshair = options.isCrosshair, paneInnerR = center[3] / 2, innerRatio, distance, a, b, otherAxis, xy, tickPositions, crossPos, path;
// Crosshair logic
if (isCrosshair) {
// Find crosshair's position and perform destructuring
// assignment
crossPos = this.getCrosshairPosition(options, x1, y1);
value = crossPos[0];
x2 = crossPos[1];
y2 = crossPos[2];
}
// Spokes
if (axis.isCircular) {
distance =
Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
a = (typeof innerRadius === 'string') ?
relativeLength(innerRadius, 1) : (innerRadius / distance);
b = (typeof outerRadius === 'string') ?
relativeLength(outerRadius, 1) : (outerRadius / distance);
// To ensure that gridlines won't be displayed in area
// defined by innerSize in case of custom radiuses of pane's
// background
if (center && paneInnerR) {
innerRatio = paneInnerR / distance;
if (a < innerRatio) {
a = innerRatio;
}
if (b < innerRatio) {
b = innerRatio;
}
}
path = [
['M', x1 + a * (x2 - x1), y1 - a * (y1 - y2)],
['L', x2 - (1 - b) * (x2 - x1), y2 + (1 - b) * (y1 - y2)]
];
// Concentric circles
}
else {
// Pick the right values depending if it is grid line or
// crosshair
value = axis.translate(value);
// This is required in case when xAxis is non-circular to
// prevent grid lines (or crosshairs, if enabled) from
// rendering above the center after they supposed to be
// displayed below the center point
if (value) {
if (value < 0 || value > height) {
value = 0;
}
}
if (axis.options.gridLineInterpolation === 'circle') {
// A value of 0 is in the center, so it won't be
// visible, but draw it anyway for update and animation
// (#2366)
path = axis.getLinePath(0, value, paneInnerR);
// Concentric polygons
}
else {
path = [];
// Find the other axis (a circular one) in the same pane
chart[inverted ? 'yAxis' : 'xAxis'].forEach(function (a) {
if (a.pane === axis.pane) {
otherAxis = a;
}
});
if (otherAxis) {
tickPositions = otherAxis.tickPositions;
if (otherAxis.autoConnect) {
tickPositions =
tickPositions.concat([tickPositions[0]]);
}
// Reverse the positions for concatenation of polygonal
// plot bands
if (reverse) {
tickPositions = tickPositions.slice().reverse();
}
if (value) {
value += paneInnerR;
}
for (var i = 0; i < tickPositions.length; i++) {
xy = otherAxis.getPosition(tickPositions[i], value);
path.push(i ? ['L', xy.x, xy.y] : ['M', xy.x, xy.y]);
}
}
}
}
return path;
};
// Find the position for the axis title, by default inside the gauge.
axis.getTitlePosition = function () {
var center = this.center, chart = this.chart, titleOptions = this.options.title;
return {
x: chart.plotLeft + center[0] + (titleOptions.x || 0),
y: (chart.plotTop +
center[1] -
({
high: 0.5,
middle: 0.25,
low: 0
}[titleOptions.align] *
center[2]) +
(titleOptions.y || 0))
};
};
/**
* Attach and return collecting function for labels in radial axis for
* anti-collision.
*
* @private
*
* @return {Highcharts.ChartLabelCollectorFunction}
*/
axis.createLabelCollector = function () {
var axis = this;
return function () {
if (axis.isRadial &&
axis.tickPositions &&
// undocumented option for now, but working
axis.options.labels.allowOverlap !== true) {
return axis.tickPositions
.map(function (pos) {
return axis.ticks[pos] && axis.ticks[pos].label;
})
.filter(function (label) {
return Boolean(label);
});
}
};
};
};
/**
* Augments methods for the value axis.
*
* @private
*
* @param {Highcharts.Axis} AxisClass
* Axis class to extend.
*
* @param {Highcharts.Tick} TickClass
* Tick class to use.
*/
RadialAxis.compose = function (AxisClass, TickClass) {
/* eslint-disable no-invalid-this */
// Actions before axis init.
addEvent(AxisClass, 'init', function (e) {
var axis = this;
var chart = axis.chart;
var inverted = chart.inverted, angular = chart.angular, polar = chart.polar, isX = axis.isXAxis, coll = axis.coll, isHidden = angular && isX, isCircular, chartOptions = chart.options, paneIndex = e.userOptions.pane || 0, pane = this.pane =
chart.pane && chart.pane[paneIndex];
// Prevent changes for colorAxis
if (coll === 'colorAxis') {
this.isRadial = false;
return;
}
// Before prototype.init
if (angular) {
if (isHidden) {
HiddenAxis.init(axis);
}
else {
RadialAxis.init(axis);
}
isCircular = !isX;
if (isCircular) {
axis.defaultPolarOptions = RadialAxis.defaultRadialGaugeOptions;
}
}
else if (polar) {
RadialAxis.init(axis);
// Check which axis is circular
isCircular = axis.horiz;
axis.defaultPolarOptions = isCircular ?
RadialAxis.defaultCircularOptions :
merge(coll === 'xAxis' ?
AxisClass.defaultOptions :
AxisClass.defaultYAxisOptions, RadialAxis.defaultRadialOptions);
// Apply the stack labels for yAxis in case of inverted chart
if (inverted && coll === 'yAxis') {
axis.defaultPolarOptions.stackLabels = AxisClass.defaultYAxisOptions.stackLabels;
}
}
// Disable certain features on angular and polar axes
if (angular || polar) {
axis.isRadial = true;
chartOptions.chart.zoomType = null;
if (!axis.labelCollector) {
axis.labelCollector = axis.createLabelCollector();
}
if (axis.labelCollector) {
// Prevent overlapping axis labels (#9761)
chart.labelCollectors.push(axis.labelCollector);
}
}
else {
this.isRadial = false;
}
// A pointer back to this axis to borrow geometry
if (pane && isCircular) {
pane.axis = axis;
}
axis.isCircular = isCircular;
});
addEvent(AxisClass, 'afterInit', function () {
var axis = this;
var chart = axis.chart, options = axis.options, isHidden = chart.angular && axis.isXAxis, pane = axis.pane, paneOptions = pane && pane.options;
if (!isHidden && pane && (chart.angular || chart.polar)) {
// Start and end angle options are given in degrees relative to
// top, while internal computations are in radians relative to
// right (like SVG).
// Y axis in polar charts
axis.angleRad = (options.angle || 0) * Math.PI / 180;
// Gauges
axis.startAngleRad =
(paneOptions.startAngle - 90) * Math.PI / 180;
axis.endAngleRad = (pick(paneOptions.endAngle, paneOptions.startAngle + 360) - 90) * Math.PI / 180; // Gauges
axis.offset = options.offset || 0;
}
});
// Wrap auto label align to avoid setting axis-wide rotation on radial
// axes. (#4920)
addEvent(AxisClass, 'autoLabelAlign', function (e) {
if (this.isRadial) {
e.align = void 0;
e.preventDefault();
}
});
// Remove label collector function on axis remove/update
addEvent(AxisClass, 'destroy', function () {
var axis = this;
if (axis.chart &&
axis.chart.labelCollectors) {
var index = (axis.labelCollector ?
axis.chart.labelCollectors.indexOf(axis.labelCollector) :
-1);
if (index >= 0) {
axis.chart.labelCollectors.splice(index, 1);
}
}
});
addEvent(AxisClass, 'initialAxisTranslation', function () {
var axis = this;
if (axis.isRadial) {
axis.beforeSetTickPositions();
}
});
// Add special cases within the Tick class' methods for radial axes.
addEvent(TickClass, 'afterGetPosition', function (e) {
var tick = this;
if (tick.axis.getPosition) {
extend(e.pos, tick.axis.getPosition(this.pos));
}
});
// Find the center position of the label based on the distance option.
addEvent(TickClass, 'afterGetLabelPosition', function (e) {
var tick = this;
var axis = tick.axis;
var label = tick.label;
if (!label) {
return;
}
var labelBBox = label.getBBox(), labelOptions = axis.options.labels, optionsY = labelOptions.y, ret, centerSlot = 20, // 20 degrees to each side at the top and bottom
align = labelOptions.align, angle = ((axis.translate(this.pos) + axis.startAngleRad +
Math.PI / 2) / Math.PI * 180) % 360, correctAngle = Math.round(angle), labelDir = 'end', // Direction of the label 'start' or 'end'
reducedAngle1 = correctAngle < 0 ?
correctAngle + 360 : correctAngle, reducedAngle2 = reducedAngle1, translateY = 0, translateX = 0, labelYPosCorrection = labelOptions.y === null ? -labelBBox.height * 0.3 : 0;
if (axis.isRadial) { // Both X and Y axes in a polar chart
ret = axis.getPosition(this.pos, (axis.center[2] / 2) +
relativeLength(pick(labelOptions.distance, -25), axis.center[2] / 2, -axis.center[2] / 2));
// Automatically rotated
if (labelOptions.rotation === 'auto') {
label.attr({
rotation: angle
});
// Vertically centered
}
else if (optionsY === null) {
optionsY = (axis.chart.renderer
.fontMetrics(label.styles && label.styles.fontSize).b -
labelBBox.height / 2);
}
// Automatic alignment
if (align === null) {
if (axis.isCircular) { // Y axis
if (labelBBox.width >
axis.len * axis.tickInterval / (axis.max - axis.min)) { // #3506
centerSlot = 0;
}
if (angle > centerSlot && angle < 180 - centerSlot) {
align = 'left'; // right hemisphere
}
else if (angle > 180 + centerSlot &&
angle < 360 - centerSlot) {
align = 'right'; // left hemisphere
}
else {
align = 'center'; // top or bottom
}
}
else {
align = 'center';
}
label.attr({
align: align
});
}
// Auto alignment for solid-gauges with two labels (#10635)
if (align === 'auto' &&
axis.tickPositions.length === 2 &&
axis.isCircular) {
// Angles reduced to 0 - 90 or 180 - 270
if (reducedAngle1 > 90 && reducedAngle1 < 180) {
reducedAngle1 = 180 - reducedAngle1;
}
else if (reducedAngle1 > 270 && reducedAngle1 <= 360) {
reducedAngle1 = 540 - reducedAngle1;
}
// Angles reduced to 0 - 180
if (reducedAngle2 > 180 && reducedAngle2 <= 360) {
reducedAngle2 = 360 - reducedAngle2;
}
if ((axis.pane.options.startAngle === correctAngle) ||
(axis.pane.options.startAngle === correctAngle + 360) ||
(axis.pane.options.startAngle === correctAngle - 360)) {
labelDir = 'start';
}
if ((correctAngle >= -90 && correctAngle <= 90) ||
(correctAngle >= -360 && correctAngle <= -270) ||
(correctAngle >= 270 && correctAngle <= 360)) {
align = (labelDir === 'start') ? 'right' : 'left';
}
else {
align = (labelDir === 'start') ? 'left' : 'right';
}
// For angles beetwen (90 + n * 180) +- 20
if (reducedAngle2 > 70 && reducedAngle2 < 110) {
align = 'center';
}
// auto Y translation
if (reducedAngle1 < 15 ||
(reducedAngle1 >= 180 && reducedAngle1 < 195)) {
translateY = labelBBox.height * 0.3;
}
else if (reducedAngle1 >= 15 && reducedAngle1 <= 35) {
translateY = labelDir === 'start' ?
0 : labelBBox.height * 0.75;
}
else if (reducedAngle1 >= 195 && reducedAngle1 <= 215) {
translateY = labelDir === 'start' ?
labelBBox.height * 0.75 : 0;
}
else if (reducedAngle1 > 35 && reducedAngle1 <= 90) {
translateY = labelDir === 'start' ?
-labelBBox.height * 0.25 : labelBBox.height;
}
else if (reducedAngle1 > 215 && reducedAngle1 <= 270) {
translateY = labelDir === 'start' ?
labelBBox.height : -labelBBox.height * 0.25;
}
// auto X translation
if (reducedAngle2 < 15) {
translateX = labelDir === 'start' ?
-labelBBox.height * 0.15 : labelBBox.height * 0.15;
}
else if (reducedAngle2 > 165 && reducedAngle2 <= 180) {
translateX = labelDir === 'start' ?
labelBBox.height * 0.15 : -labelBBox.height * 0.15;
}
label.attr({ align: align });
label.translate(translateX, translateY + labelYPosCorrection);
}
e.pos.x = ret.x + labelOptions.x;
e.pos.y = ret.y + optionsY;
}
});
// Wrap the getMarkPath function to return the path of the radial marker
wrap(TickClass.prototype, 'getMarkPath', function (proceed, x, y, tickLength, tickWidth, horiz, renderer) {
var tick = this;
var axis = tick.axis;
var endPoint, ret;
if (axis.isRadial) {
endPoint = axis.getPosition(this.pos, axis.center[2] / 2 + tickLength);
ret = [
'M',
x,
y,
'L',
endPoint.x,
endPoint.y
];
}
else {
ret = proceed.call(this, x, y, tickLength, tickWidth, horiz, renderer);
}
return ret;
});
};
/* *
*
* Static Properties
*
* */
/**
* Circular axis around the perimeter of a polar chart.
* @private
*/
RadialAxis.defaultCircularOptions = {
gridLineWidth: 1,
labels: {
align: null,
distance: 15,
x: 0,
y: null,
style: {
textOverflow: 'none' // wrap lines by default (#7248)
}
},
maxPadding: 0,
minPadding: 0,
showLastLabel: false,
tickLength: 0
};
/**
* The default options extend defaultYAxisOptions.
* @private
*/
RadialAxis.defaultRadialGaugeOptions = {
labels: {
align: 'center',
x: 0,
y: null // auto
},
minorGridLineWidth: 0,
minorTickInterval: 'auto',
minorTickLength: 10,
minorTickPosition: 'inside',
minorTickWidth: 1,
tickLength: 10,
tickPosition: 'inside',
tickWidth: 2,
title: {
rotation: 0
},
zIndex: 2 // behind dials, points in the series group
};
/**
* Radial axis, like a spoke in a polar chart.
* @private
*/
RadialAxis.defaultRadialOptions = {
/**
* In a polar chart, this is the angle of the Y axis in degrees, where
* 0 is up and 90 is right. The angle determines the position of the
* axis line and the labels, though the coordinate system is unaffected.
* Since v8.0.0 this option is also applicable for X axis (inverted
* polar).
*
* @sample {highcharts} highcharts/xaxis/angle/
* Custom X axis' angle on inverted polar chart
* @sample {highcharts} highcharts/yaxis/angle/
* Dual axis polar chart
*
* @type {number}
* @default 0
* @since 4.2.7
* @product highcharts
* @apioption xAxis.angle
*/
/**
* Polar charts only. Whether the grid lines should draw as a polygon
* with straight lines between categories, or as circles. Can be either
* `circle` or `polygon`. Since v8.0.0 this option is also applicable
* for X axis (inverted polar).
*
* @sample {highcharts} highcharts/demo/polar-spider/
* Polygon grid lines
* @sample {highcharts} highcharts/xaxis/gridlineinterpolation/
* Circle and polygon on inverted polar
* @sample {highcharts} highcharts/yaxis/gridlineinterpolation/
* Circle and polygon
*
* @type {string}
* @product highcharts
* @validvalue ["circle", "polygon"]
* @apioption xAxis.gridLineInterpolation
*/
gridLineInterpolation: 'circle',
gridLineWidth: 1,
labels: {
align: 'right',
x: -3,
y: -2
},
showLastLabel: false,
title: {
x: 4,
text: null,
rotation: 90
}
};
return RadialAxis;
}());
RadialAxis.compose(Axis, Tick); // @todo move outside
export default RadialAxis;
@@ -0,0 +1,151 @@
/* *
*
* (c) 2010-2020 Torstein Honsi
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
import H from '../Globals.js';
import U from '../Utilities.js';
var addEvent = U.addEvent, defined = U.defined, pick = U.pick;
/* eslint-disable no-invalid-this, valid-jsdoc */
/**
* Creates scrollbars if enabled.
*
* @private
*/
var ScrollbarAxis = /** @class */ (function () {
function ScrollbarAxis() {
}
/**
* Attaches to axis events to create scrollbars if enabled.
*
* @private
*
* @param AxisClass
* Axis class to extend.
*
* @param ScrollbarClass
* Scrollbar class to use.
*/
ScrollbarAxis.compose = function (AxisClass, ScrollbarClass) {
// Wrap axis initialization and create scrollbar if enabled:
addEvent(AxisClass, 'afterInit', function () {
var axis = this;
if (axis.options &&
axis.options.scrollbar &&
axis.options.scrollbar.enabled) {
// Predefined options:
axis.options.scrollbar.vertical = !axis.horiz;
axis.options.startOnTick = axis.options.endOnTick = false;
axis.scrollbar = new ScrollbarClass(axis.chart.renderer, axis.options.scrollbar, axis.chart);
addEvent(axis.scrollbar, 'changed', function (e) {
var axisMin = pick(axis.options && axis.options.min, axis.min), axisMax = pick(axis.options && axis.options.max, axis.max), unitedMin = defined(axis.dataMin) ?
Math.min(axisMin, axis.min, axis.dataMin) : axisMin, unitedMax = defined(axis.dataMax) ?
Math.max(axisMax, axis.max, axis.dataMax) : axisMax, range = unitedMax - unitedMin, to, from;
// #12834, scroll when show/hide series, wrong extremes
if (!defined(axisMin) || !defined(axisMax)) {
return;
}
if ((axis.horiz && !axis.reversed) ||
(!axis.horiz && axis.reversed)) {
to = unitedMin + range * this.to;
from = unitedMin + range * this.from;
}
else {
// y-values in browser are reversed, but this also
// applies for reversed horizontal axis:
to = unitedMin + range * (1 - this.from);
from = unitedMin + range * (1 - this.to);
}
if (pick(this.options.liveRedraw, H.svg && !H.isTouchDevice && !this.chart.isBoosting) ||
// Mouseup always should change extremes
e.DOMType === 'mouseup' ||
// Internal events
!defined(e.DOMType)) {
axis.setExtremes(from, to, true, e.DOMType !== 'mousemove', e);
}
else {
// When live redraw is disabled, don't change extremes
// Only change the position of the scollbar thumb
this.setRange(this.from, this.to);
}
});
}
});
// Wrap rendering axis, and update scrollbar if one is created:
addEvent(AxisClass, 'afterRender', function () {
var axis = this, scrollMin = Math.min(pick(axis.options.min, axis.min), axis.min, pick(axis.dataMin, axis.min) // #6930
), scrollMax = Math.max(pick(axis.options.max, axis.max), axis.max, pick(axis.dataMax, axis.max) // #6930
), scrollbar = axis.scrollbar, offset = axis.axisTitleMargin + (axis.titleOffset || 0), scrollbarsOffsets = axis.chart.scrollbarsOffsets, axisMargin = axis.options.margin || 0, offsetsIndex, from, to;
if (scrollbar) {
if (axis.horiz) {
// Reserve space for labels/title
if (!axis.opposite) {
scrollbarsOffsets[1] += offset;
}
scrollbar.position(axis.left, axis.top + axis.height + 2 + scrollbarsOffsets[1] -
(axis.opposite ? axisMargin : 0), axis.width, axis.height);
// Next scrollbar should reserve space for margin (if set)
if (!axis.opposite) {
scrollbarsOffsets[1] += axisMargin;
}
offsetsIndex = 1;
}
else {
// Reserve space for labels/title
if (axis.opposite) {
scrollbarsOffsets[0] += offset;
}
scrollbar.position(axis.left + axis.width + 2 + scrollbarsOffsets[0] -
(axis.opposite ? 0 : axisMargin), axis.top, axis.width, axis.height);
// Next scrollbar should reserve space for margin (if set)
if (axis.opposite) {
scrollbarsOffsets[0] += axisMargin;
}
offsetsIndex = 0;
}
scrollbarsOffsets[offsetsIndex] += scrollbar.size +
scrollbar.options.margin;
if (isNaN(scrollMin) ||
isNaN(scrollMax) ||
!defined(axis.min) ||
!defined(axis.max) ||
axis.min === axis.max // #10733
) {
// default action: when extremes are the same or there is
// not extremes on the axis, but scrollbar exists, make it
// full size
scrollbar.setRange(0, 1);
}
else {
from =
(axis.min - scrollMin) / (scrollMax - scrollMin);
to =
(axis.max - scrollMin) / (scrollMax - scrollMin);
if ((axis.horiz && !axis.reversed) ||
(!axis.horiz && axis.reversed)) {
scrollbar.setRange(from, to);
}
else {
// inverse vertical axis
scrollbar.setRange(1 - to, 1 - from);
}
}
}
});
// Make space for a scrollbar:
addEvent(AxisClass, 'afterGetOffset', function () {
var axis = this, index = axis.horiz ? 2 : 1, scrollbar = axis.scrollbar;
if (scrollbar) {
axis.chart.scrollbarsOffsets = [0, 0]; // reset scrollbars offsets
axis.chart.axisOffset[index] +=
scrollbar.size + scrollbar.options.margin;
}
});
};
return ScrollbarAxis;
}());
export default ScrollbarAxis;
@@ -0,0 +1,193 @@
/* *
*
* (c) 2010-2020 Torstein Honsi
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
import U from '../Utilities.js';
var addEvent = U.addEvent, destroyObjectProperties = U.destroyObjectProperties, fireEvent = U.fireEvent, getDeferredAnimation = U.getDeferredAnimation, objectEach = U.objectEach, pick = U.pick;
/* eslint-disable valid-jsdoc */
/**
* Adds stacking support to axes.
* @private
* @class
*/
var StackingAxisAdditions = /** @class */ (function () {
/* *
*
* Constructors
*
* */
function StackingAxisAdditions(axis) {
this.oldStacks = {};
this.stacks = {};
this.stacksTouched = 0;
this.axis = axis;
}
/* *
*
* Functions
*
* */
/**
* Build the stacks from top down
* @private
*/
StackingAxisAdditions.prototype.buildStacks = function () {
var stacking = this;
var axis = stacking.axis;
var axisSeries = axis.series;
var reversedStacks = pick(axis.options.reversedStacks, true);
var len = axisSeries.length;
var actualSeries, i;
if (!axis.isXAxis) {
stacking.usePercentage = false;
i = len;
while (i--) {
actualSeries = axisSeries[reversedStacks ? i : len - i - 1];
actualSeries.setStackedPoints();
actualSeries.setGroupedPoints();
}
// Loop up again to compute percent and stream stack
for (i = 0; i < len; i++) {
axisSeries[i].modifyStacks();
}
fireEvent(axis, 'afterBuildStacks');
}
};
/**
* @private
*/
StackingAxisAdditions.prototype.cleanStacks = function () {
var stacking = this;
var axis = stacking.axis;
var stacks;
if (!axis.isXAxis) {
if (stacking.oldStacks) {
stacks = stacking.stacks = stacking.oldStacks;
}
// reset stacks
objectEach(stacks, function (type) {
objectEach(type, function (stack) {
stack.cumulative = stack.total;
});
});
}
};
/**
* Set all the stacks to initial states and destroy unused ones.
* @private
*/
StackingAxisAdditions.prototype.resetStacks = function () {
var stacking = this;
var axis = stacking.axis;
var stacks = stacking.stacks;
if (!axis.isXAxis) {
objectEach(stacks, function (type) {
objectEach(type, function (stack, key) {
// Clean up memory after point deletion (#1044, #4320)
if (stack.touched < stacking.stacksTouched) {
stack.destroy();
delete type[key];
// Reset stacks
}
else {
stack.total = null;
stack.cumulative = null;
}
});
});
}
};
/**
* @private
*/
StackingAxisAdditions.prototype.renderStackTotals = function () {
var stacking = this;
var axis = stacking.axis;
var chart = axis.chart;
var renderer = chart.renderer;
var stacks = stacking.stacks;
var stackLabelsAnim = axis.options.stackLabels.animation;
var animationConfig = getDeferredAnimation(chart, stackLabelsAnim);
var stackTotalGroup = stacking.stackTotalGroup = (stacking.stackTotalGroup ||
renderer
.g('stack-labels')
.attr({
visibility: 'visible',
zIndex: 6,
opacity: 0
})
.add());
// plotLeft/Top will change when y axis gets wider so we need to
// translate the stackTotalGroup at every render call. See bug #506
// and #516
stackTotalGroup.translate(chart.plotLeft, chart.plotTop);
// Render each stack total
objectEach(stacks, function (type) {
objectEach(type, function (stack) {
stack.render(stackTotalGroup);
});
});
stackTotalGroup.animate({
opacity: 1
}, animationConfig);
};
return StackingAxisAdditions;
}());
/**
* Axis with stacking support.
* @private
* @class
*/
var StackingAxis = /** @class */ (function () {
function StackingAxis() {
}
/* *
*
* Static Functions
*
* */
/**
* Extends axis with stacking support.
* @private
*/
StackingAxis.compose = function (AxisClass) {
var axisProto = AxisClass.prototype;
addEvent(AxisClass, 'init', StackingAxis.onInit);
addEvent(AxisClass, 'destroy', StackingAxis.onDestroy);
};
/**
* @private
*/
StackingAxis.onDestroy = function () {
var stacking = this.stacking;
if (!stacking) {
return;
}
var stacks = stacking.stacks;
// Destroy each stack total
objectEach(stacks, function (stack, stackKey) {
destroyObjectProperties(stack);
stacks[stackKey] = null;
});
if (stacking &&
stacking.stackTotalGroup) {
stacking.stackTotalGroup.destroy();
}
};
/**
* @private
*/
StackingAxis.onInit = function () {
var axis = this;
if (!axis.stacking) {
axis.stacking = new StackingAxisAdditions(axis);
}
};
return StackingAxis;
}());
export default StackingAxis;
+667
View File
@@ -0,0 +1,667 @@
/* *
*
* (c) 2010-2020 Torstein Honsi
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import H from '../Globals.js';
/**
* Optional parameters for the tick.
* @private
* @interface Highcharts.TickParametersObject
*/ /**
* Set category for the tick.
* @name Highcharts.TickParametersObject#category
* @type {string|undefined}
*/ /**
* @name Highcharts.TickParametersObject#options
* @type {Highcharts.Dictionary<any>|undefined}
*/ /**
* Set tickmarkOffset for the tick.
* @name Highcharts.TickParametersObject#tickmarkOffset
* @type {number|undefined}
*/
import U from '../Utilities.js';
var clamp = U.clamp, correctFloat = U.correctFloat, defined = U.defined, destroyObjectProperties = U.destroyObjectProperties, extend = U.extend, fireEvent = U.fireEvent, isNumber = U.isNumber, merge = U.merge, objectEach = U.objectEach, pick = U.pick;
var deg2rad = H.deg2rad;
/* eslint-disable no-invalid-this, valid-jsdoc */
/**
* The Tick class.
*
* @class
* @name Highcharts.Tick
*
* @param {Highcharts.Axis} axis
* The axis of the tick.
*
* @param {number} pos
* The position of the tick on the axis in terms of axis values.
*
* @param {string} [type]
* The type of tick, either 'minor' or an empty string
*
* @param {boolean} [noLabel=false]
* Whether to disable the label or not. Defaults to false.
*
* @param {object} [parameters]
* Optional parameters for the tick.
*/
var Tick = /** @class */ (function () {
/* *
*
* Constructors
*
* */
function Tick(axis, pos, type, noLabel, parameters) {
this.isNew = true;
this.isNewLabel = true;
/**
* The related axis of the tick.
* @name Highcharts.Tick#axis
* @type {Highcharts.Axis}
*/
this.axis = axis;
/**
* The logical position of the tick on the axis in terms of axis values.
* @name Highcharts.Tick#pos
* @type {number}
*/
this.pos = pos;
/**
* The tick type, which can be `"minor"`, or an empty string.
* @name Highcharts.Tick#type
* @type {string}
*/
this.type = type || '';
this.parameters = parameters || {};
/**
* The mark offset of the tick on the axis. Usually `undefined`, numeric
* for grid axes.
* @name Highcharts.Tick#tickmarkOffset
* @type {number|undefined}
*/
this.tickmarkOffset = this.parameters.tickmarkOffset;
this.options = this.parameters.options;
fireEvent(this, 'init');
if (!type && !noLabel) {
this.addLabel();
}
}
/* *
*
* Functions
*
* */
/**
* Write the tick label.
*
* @private
* @function Highcharts.Tick#addLabel
* @return {void}
*/
Tick.prototype.addLabel = function () {
var tick = this, axis = tick.axis, options = axis.options, chart = axis.chart, categories = axis.categories, log = axis.logarithmic, names = axis.names, pos = tick.pos, labelOptions = pick(tick.options && tick.options.labels, options.labels), str, tickPositions = axis.tickPositions, isFirst = pos === tickPositions[0], isLast = pos === tickPositions[tickPositions.length - 1], value = this.parameters.category || (categories ?
pick(categories[pos], names[pos], pos) :
pos), label = tick.label, animateLabels = (!labelOptions.step || labelOptions.step === 1) &&
axis.tickInterval === 1, tickPositionInfo = tickPositions.info, dateTimeLabelFormat, dateTimeLabelFormats, i, list;
// Set the datetime label format. If a higher rank is set for this
// position, use that. If not, use the general format.
if (axis.dateTime && tickPositionInfo) {
dateTimeLabelFormats = chart.time.resolveDTLFormat(options.dateTimeLabelFormats[(!options.grid &&
tickPositionInfo.higherRanks[pos]) ||
tickPositionInfo.unitName]);
dateTimeLabelFormat = dateTimeLabelFormats.main;
}
// set properties for access in render method
/**
* True if the tick is the first one on the axis.
* @name Highcharts.Tick#isFirst
* @readonly
* @type {boolean|undefined}
*/
tick.isFirst = isFirst;
/**
* True if the tick is the last one on the axis.
* @name Highcharts.Tick#isLast
* @readonly
* @type {boolean|undefined}
*/
tick.isLast = isLast;
// Get the string
tick.formatCtx = {
axis: axis,
chart: chart,
isFirst: isFirst,
isLast: isLast,
dateTimeLabelFormat: dateTimeLabelFormat,
tickPositionInfo: tickPositionInfo,
value: log ? correctFloat(log.lin2log(value)) : value,
pos: pos
};
str = axis.labelFormatter.call(tick.formatCtx, this.formatCtx);
// Set up conditional formatting based on the format list if existing.
list = dateTimeLabelFormats && dateTimeLabelFormats.list;
if (list) {
tick.shortenLabel = function () {
for (i = 0; i < list.length; i++) {
label.attr({
text: axis.labelFormatter.call(extend(tick.formatCtx, { dateTimeLabelFormat: list[i] }))
});
if (label.getBBox().width <
axis.getSlotWidth(tick) - 2 *
pick(labelOptions.padding, 5)) {
return;
}
}
label.attr({
text: ''
});
};
}
// Call only after first render
if (animateLabels && axis._addedPlotLB) {
tick.moveLabel(str, labelOptions);
}
// First call
if (!defined(label) && !tick.movedLabel) {
/**
* The rendered text label of the tick.
* @name Highcharts.Tick#label
* @type {Highcharts.SVGElement|undefined}
*/
tick.label = label = tick.createLabel({ x: 0, y: 0 }, str, labelOptions);
// Base value to detect change for new calls to getBBox
tick.rotation = 0;
// update
}
else if (label && label.textStr !== str && !animateLabels) {
// When resetting text, also reset the width if dynamically set
// (#8809)
if (label.textWidth &&
!(labelOptions.style && labelOptions.style.width) &&
!label.styles.width) {
label.css({ width: null });
}
label.attr({ text: str });
label.textPxLength = label.getBBox().width;
}
};
/**
* Render and return the label of the tick.
*
* @private
* @function Highcharts.Tick#createLabel
* @param {Highcharts.PositionObject} xy
* @param {string} str
* @param {Highcharts.XAxisLabelsOptions} labelOptions
* @return {Highcharts.SVGElement|undefined}
*/
Tick.prototype.createLabel = function (xy, str, labelOptions) {
var axis = this.axis, chart = axis.chart, label = defined(str) && labelOptions.enabled ?
chart.renderer
.text(str, xy.x, xy.y, labelOptions.useHTML)
.add(axis.labelGroup) :
null;
// Un-rotated length
if (label) {
// Without position absolute, IE export sometimes is wrong
if (!chart.styledMode) {
label.css(merge(labelOptions.style));
}
label.textPxLength = label.getBBox().width;
}
return label;
};
/**
* Destructor for the tick prototype
*
* @private
* @function Highcharts.Tick#destroy
* @return {void}
*/
Tick.prototype.destroy = function () {
destroyObjectProperties(this, this.axis);
};
/**
* Gets the x and y positions for ticks in terms of pixels.
*
* @private
* @function Highcharts.Tick#getPosition
*
* @param {boolean} horiz
* Whether the tick is on an horizontal axis or not.
*
* @param {number} tickPos
* Position of the tick.
*
* @param {number} tickmarkOffset
* Tickmark offset for all ticks.
*
* @param {boolean} [old]
* Whether the axis has changed or not.
*
* @return {Highcharts.PositionObject}
* The tick position.
*
* @fires Highcharts.Tick#event:afterGetPosition
*/
Tick.prototype.getPosition = function (horiz, tickPos, tickmarkOffset, old) {
var axis = this.axis, chart = axis.chart, cHeight = (old && chart.oldChartHeight) || chart.chartHeight, pos;
pos = {
x: horiz ?
correctFloat(axis.translate(tickPos + tickmarkOffset, null, null, old) +
axis.transB) :
(axis.left +
axis.offset +
(axis.opposite ?
(((old && chart.oldChartWidth) ||
chart.chartWidth) -
axis.right -
axis.left) :
0)),
y: horiz ?
(cHeight -
axis.bottom +
axis.offset -
(axis.opposite ? axis.height : 0)) :
correctFloat(cHeight -
axis.translate(tickPos + tickmarkOffset, null, null, old) -
axis.transB)
};
// Chrome workaround for #10516
pos.y = clamp(pos.y, -1e5, 1e5);
fireEvent(this, 'afterGetPosition', { pos: pos });
return pos;
};
/**
* Get the x, y position of the tick label
*
* @private
* @return {Highcharts.PositionObject}
*/
Tick.prototype.getLabelPosition = function (x, y, label, horiz, labelOptions, tickmarkOffset, index, step) {
var axis = this.axis, transA = axis.transA, reversed = ( // #7911
axis.isLinked && axis.linkedParent ?
axis.linkedParent.reversed :
axis.reversed), staggerLines = axis.staggerLines, rotCorr = axis.tickRotCorr || { x: 0, y: 0 }, yOffset = labelOptions.y,
// Adjust for label alignment if we use reserveSpace: true (#5286)
labelOffsetCorrection = (!horiz && !axis.reserveSpaceDefault ?
-axis.labelOffset * (axis.labelAlign === 'center' ? 0.5 : 1) :
0), line, pos = {};
if (!defined(yOffset)) {
if (axis.side === 0) {
yOffset = label.rotation ? -8 : -label.getBBox().height;
}
else if (axis.side === 2) {
yOffset = rotCorr.y + 8;
}
else {
// #3140, #3140
yOffset = Math.cos(label.rotation * deg2rad) *
(rotCorr.y - label.getBBox(false, 0).height / 2);
}
}
x = x +
labelOptions.x +
labelOffsetCorrection +
rotCorr.x -
(tickmarkOffset && horiz ?
tickmarkOffset * transA * (reversed ? -1 : 1) :
0);
y = y + yOffset - (tickmarkOffset && !horiz ?
tickmarkOffset * transA * (reversed ? 1 : -1) : 0);
// Correct for staggered labels
if (staggerLines) {
line = (index / (step || 1) % staggerLines);
if (axis.opposite) {
line = staggerLines - line - 1;
}
y += line * (axis.labelOffset / staggerLines);
}
pos.x = x;
pos.y = Math.round(y);
fireEvent(this, 'afterGetLabelPosition', { pos: pos, tickmarkOffset: tickmarkOffset, index: index });
return pos;
};
/**
* Get the offset height or width of the label
*
* @private
* @function Highcharts.Tick#getLabelSize
* @return {number}
*/
Tick.prototype.getLabelSize = function () {
return this.label ?
this.label.getBBox()[this.axis.horiz ? 'height' : 'width'] :
0;
};
/**
* Extendible method to return the path of the marker
*
* @private
*
*/
Tick.prototype.getMarkPath = function (x, y, tickLength, tickWidth, horiz, renderer) {
return renderer.crispLine([[
'M',
x,
y
], [
'L',
x + (horiz ? 0 : -tickLength),
y + (horiz ? tickLength : 0)
]], tickWidth);
};
/**
* Handle the label overflow by adjusting the labels to the left and right
* edge, or hide them if they collide into the neighbour label.
*
* @private
* @function Highcharts.Tick#handleOverflow
* @param {Highcharts.PositionObject} xy
* @return {void}
*/
Tick.prototype.handleOverflow = function (xy) {
var tick = this, axis = this.axis, labelOptions = axis.options.labels, pxPos = xy.x, chartWidth = axis.chart.chartWidth, spacing = axis.chart.spacing, leftBound = pick(axis.labelLeft, Math.min(axis.pos, spacing[3])), rightBound = pick(axis.labelRight, Math.max(!axis.isRadial ? axis.pos + axis.len : 0, chartWidth - spacing[1])), label = this.label, rotation = this.rotation, factor = {
left: 0,
center: 0.5,
right: 1
}[axis.labelAlign || label.attr('align')], labelWidth = label.getBBox().width, slotWidth = axis.getSlotWidth(tick), modifiedSlotWidth = slotWidth, xCorrection = factor, goRight = 1, leftPos, rightPos, textWidth, css = {};
// Check if the label overshoots the chart spacing box. If it does, move
// it. If it now overshoots the slotWidth, add ellipsis.
if (!rotation &&
pick(labelOptions.overflow, 'justify') === 'justify') {
leftPos = pxPos - factor * labelWidth;
rightPos = pxPos + (1 - factor) * labelWidth;
if (leftPos < leftBound) {
modifiedSlotWidth =
xy.x + modifiedSlotWidth * (1 - factor) - leftBound;
}
else if (rightPos > rightBound) {
modifiedSlotWidth =
rightBound - xy.x + modifiedSlotWidth * factor;
goRight = -1;
}
modifiedSlotWidth = Math.min(slotWidth, modifiedSlotWidth); // #4177
if (modifiedSlotWidth < slotWidth && axis.labelAlign === 'center') {
xy.x += (goRight *
(slotWidth -
modifiedSlotWidth -
xCorrection * (slotWidth - Math.min(labelWidth, modifiedSlotWidth))));
}
// If the label width exceeds the available space, set a text width
// to be picked up below. Also, if a width has been set before, we
// need to set a new one because the reported labelWidth will be
// limited by the box (#3938).
if (labelWidth > modifiedSlotWidth ||
(axis.autoRotation && (label.styles || {}).width)) {
textWidth = modifiedSlotWidth;
}
// Add ellipsis to prevent rotated labels to be clipped against the edge
// of the chart
}
else if (rotation < 0 &&
pxPos - factor * labelWidth < leftBound) {
textWidth = Math.round(pxPos / Math.cos(rotation * deg2rad) - leftBound);
}
else if (rotation > 0 &&
pxPos + factor * labelWidth > rightBound) {
textWidth = Math.round((chartWidth - pxPos) /
Math.cos(rotation * deg2rad));
}
if (textWidth) {
if (tick.shortenLabel) {
tick.shortenLabel();
}
else {
css.width = Math.floor(textWidth) + 'px';
if (!(labelOptions.style || {}).textOverflow) {
css.textOverflow = 'ellipsis';
}
label.css(css);
}
}
};
/**
* Try to replace the label if the same one already exists.
*
* @private
* @function Highcharts.Tick#moveLabel
* @param {string} str
* @param {Highcharts.XAxisLabelsOptions} labelOptions
*
* @return {void}
*/
Tick.prototype.moveLabel = function (str, labelOptions) {
var tick = this, label = tick.label, moved = false, axis = tick.axis, labelPos, reversed = axis.reversed, xPos, yPos;
if (label && label.textStr === str) {
tick.movedLabel = label;
moved = true;
delete tick.label;
}
else { // Find a label with the same string
objectEach(axis.ticks, function (currentTick) {
if (!moved &&
!currentTick.isNew &&
currentTick !== tick &&
currentTick.label &&
currentTick.label.textStr === str) {
tick.movedLabel = currentTick.label;
moved = true;
currentTick.labelPos = tick.movedLabel.xy;
delete currentTick.label;
}
});
}
// Create new label if the actual one is moved
if (!moved && (tick.labelPos || label)) {
labelPos = tick.labelPos || label.xy;
xPos = axis.horiz ?
(reversed ? 0 : axis.width + axis.left) : labelPos.x;
yPos = axis.horiz ?
labelPos.y : (reversed ? (axis.width + axis.left) : 0);
tick.movedLabel = tick.createLabel({ x: xPos, y: yPos }, str, labelOptions);
if (tick.movedLabel) {
tick.movedLabel.attr({ opacity: 0 });
}
}
};
/**
* Put everything in place
*
* @private
* @param {number} index
* @param {boolean} [old]
* Use old coordinates to prepare an animation into new position
* @param {number} [opacity]
* @return {voids}
*/
Tick.prototype.render = function (index, old, opacity) {
var tick = this, axis = tick.axis, horiz = axis.horiz, pos = tick.pos, tickmarkOffset = pick(tick.tickmarkOffset, axis.tickmarkOffset), xy = tick.getPosition(horiz, pos, tickmarkOffset, old), x = xy.x, y = xy.y, reverseCrisp = ((horiz && x === axis.pos + axis.len) ||
(!horiz && y === axis.pos)) ? -1 : 1; // #1480, #1687
opacity = pick(opacity, 1);
this.isActive = true;
// Create the grid line
this.renderGridLine(old, opacity, reverseCrisp);
// create the tick mark
this.renderMark(xy, opacity, reverseCrisp);
// the label is created on init - now move it into place
this.renderLabel(xy, old, opacity, index);
tick.isNew = false;
fireEvent(this, 'afterRender');
};
/**
* Renders the gridLine.
*
* @private
* @param {boolean} old Whether or not the tick is old
* @param {number} opacity The opacity of the grid line
* @param {number} reverseCrisp Modifier for avoiding overlapping 1 or -1
* @return {void}
*/
Tick.prototype.renderGridLine = function (old, opacity, reverseCrisp) {
var tick = this, axis = tick.axis, options = axis.options, gridLine = tick.gridLine, gridLinePath, attribs = {}, pos = tick.pos, type = tick.type, tickmarkOffset = pick(tick.tickmarkOffset, axis.tickmarkOffset), renderer = axis.chart.renderer, gridPrefix = type ? type + 'Grid' : 'grid', gridLineWidth = options[gridPrefix + 'LineWidth'], gridLineColor = options[gridPrefix + 'LineColor'], dashStyle = options[gridPrefix + 'LineDashStyle'];
if (!gridLine) {
if (!axis.chart.styledMode) {
attribs.stroke = gridLineColor;
attribs['stroke-width'] = gridLineWidth;
if (dashStyle) {
attribs.dashstyle = dashStyle;
}
}
if (!type) {
attribs.zIndex = 1;
}
if (old) {
opacity = 0;
}
/**
* The rendered grid line of the tick.
* @name Highcharts.Tick#gridLine
* @type {Highcharts.SVGElement|undefined}
*/
tick.gridLine = gridLine = renderer.path()
.attr(attribs)
.addClass('highcharts-' + (type ? type + '-' : '') + 'grid-line')
.add(axis.gridGroup);
}
if (gridLine) {
gridLinePath = axis.getPlotLinePath({
value: pos + tickmarkOffset,
lineWidth: gridLine.strokeWidth() * reverseCrisp,
force: 'pass',
old: old
});
// If the parameter 'old' is set, the current call will be followed
// by another call, therefore do not do any animations this time
if (gridLinePath) {
gridLine[old || tick.isNew ? 'attr' : 'animate']({
d: gridLinePath,
opacity: opacity
});
}
}
};
/**
* Renders the tick mark.
*
* @private
* @param {Highcharts.PositionObject} xy The position vector of the mark
* @param {number} opacity The opacity of the mark
* @param {number} reverseCrisp Modifier for avoiding overlapping 1 or -1
* @return {void}
*/
Tick.prototype.renderMark = function (xy, opacity, reverseCrisp) {
var tick = this, axis = tick.axis, options = axis.options, renderer = axis.chart.renderer, type = tick.type, tickPrefix = type ? type + 'Tick' : 'tick', tickSize = axis.tickSize(tickPrefix), mark = tick.mark, isNewMark = !mark, x = xy.x, y = xy.y, tickWidth = pick(options[tickPrefix + 'Width'], !type && axis.isXAxis ? 1 : 0), // X axis defaults to 1
tickColor = options[tickPrefix + 'Color'];
if (tickSize) {
// negate the length
if (axis.opposite) {
tickSize[0] = -tickSize[0];
}
// First time, create it
if (isNewMark) {
/**
* The rendered mark of the tick.
* @name Highcharts.Tick#mark
* @type {Highcharts.SVGElement|undefined}
*/
tick.mark = mark = renderer.path()
.addClass('highcharts-' + (type ? type + '-' : '') + 'tick')
.add(axis.axisGroup);
if (!axis.chart.styledMode) {
mark.attr({
stroke: tickColor,
'stroke-width': tickWidth
});
}
}
mark[isNewMark ? 'attr' : 'animate']({
d: tick.getMarkPath(x, y, tickSize[0], mark.strokeWidth() * reverseCrisp, axis.horiz, renderer),
opacity: opacity
});
}
};
/**
* Renders the tick label.
* Note: The label should already be created in init(), so it should only
* have to be moved into place.
*
* @private
* @param {Highcharts.PositionObject} xy The position vector of the label
* @param {boolean} old Whether or not the tick is old
* @param {number} opacity The opacity of the label
* @param {number} index The index of the tick
* @return {void}
*/
Tick.prototype.renderLabel = function (xy, old, opacity, index) {
var tick = this, axis = tick.axis, horiz = axis.horiz, options = axis.options, label = tick.label, labelOptions = options.labels, step = labelOptions.step, tickmarkOffset = pick(tick.tickmarkOffset, axis.tickmarkOffset), show = true, x = xy.x, y = xy.y;
if (label && isNumber(x)) {
label.xy = xy = tick.getLabelPosition(x, y, label, horiz, labelOptions, tickmarkOffset, index, step);
// Apply show first and show last. If the tick is both first and
// last, it is a single centered tick, in which case we show the
// label anyway (#2100).
if ((tick.isFirst &&
!tick.isLast &&
!pick(options.showFirstLabel, 1)) ||
(tick.isLast &&
!tick.isFirst &&
!pick(options.showLastLabel, 1))) {
show = false;
// Handle label overflow and show or hide accordingly
}
else if (horiz &&
!labelOptions.step &&
!labelOptions.rotation &&
!old &&
opacity !== 0) {
tick.handleOverflow(xy);
}
// apply step
if (step && index % step) {
// show those indices dividable by step
show = false;
}
// Set the new position, and show or hide
if (show && isNumber(xy.y)) {
xy.opacity = opacity;
label[tick.isNewLabel ? 'attr' : 'animate'](xy);
tick.isNewLabel = false;
}
else {
label.attr('y', -9999); // #1338
tick.isNewLabel = true;
}
}
};
/**
* Replace labels with the moved ones to perform animation. Additionally
* destroy unused labels.
*
* @private
* @function Highcharts.Tick#replaceMovedLabel
* @return {void}
*/
Tick.prototype.replaceMovedLabel = function () {
var tick = this, label = tick.label, axis = tick.axis, reversed = axis.reversed, x, y;
// Animate and destroy
if (label && !tick.isNew) {
x = axis.horiz ? (reversed ? axis.left : axis.width + axis.left) : label.xy.x;
y = axis.horiz ?
label.xy.y :
(reversed ? axis.width + axis.top : axis.top);
label.animate({ x: x, y: y, opacity: 0 }, void 0, label.destroy);
delete tick.label;
}
axis.isDirty = true;
tick.label = tick.movedLabel;
delete tick.movedLabel;
};
return Tick;
}());
H.Tick = Tick;
export default H.Tick;
+68
View File
@@ -0,0 +1,68 @@
/* *
*
* (c) 2010-2020 Torstein Honsi
*
* Extenstion for 3d axes
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import U from '../Utilities.js';
var addEvent = U.addEvent, extend = U.extend, wrap = U.wrap;
/* eslint-disable valid-jsdoc */
/**
* Tick with 3D support
* @private
* @class
*/
var Tick3D = /** @class */ (function () {
function Tick3D() {
}
/* *
*
* Static Functions
*
* */
/**
* @private
*/
Tick3D.compose = function (TickClass) {
addEvent(TickClass, 'afterGetLabelPosition', Tick3D.onAfterGetLabelPosition);
var tickProto = TickClass.prototype;
wrap(tickProto, 'getMarkPath', Tick3D.wrapGetMarkPath);
};
/**
* @private
*/
Tick3D.onAfterGetLabelPosition = function (e) {
var axis3D = this.axis.axis3D;
if (axis3D) {
extend(e.pos, axis3D.fix3dPosition(e.pos));
}
};
/**
* @private
*/
Tick3D.wrapGetMarkPath = function (proceed) {
var chart = this.axis.chart;
var axis3D = this.axis.axis3D;
var path = proceed.apply(this, [].slice.call(arguments, 1));
if (axis3D) {
var start = path[0];
var end = path[1];
if (start[0] === 'M' && end[0] === 'L') {
var pArr = [
axis3D.fix3dPosition({ x: start[1], y: start[2], z: 0 }),
axis3D.fix3dPosition({ x: end[1], y: end[2], z: 0 })
];
return this.axis.chart.renderer.toLineSegments(pArr);
}
}
return path;
};
return Tick3D;
}());
export default Tick3D;
@@ -0,0 +1,677 @@
/* *
*
* (c) 2016 Highsoft AS
* Authors: Jon Arild Nygard
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import Axis from './Axis.js';
import Tick from './Tick.js';
import Tree from '../../Gantt/Tree.js';
import TreeGridTick from './TreeGridTick.js';
import mixinTreeSeries from '../../Mixins/TreeSeries.js';
var getLevelOptions = mixinTreeSeries.getLevelOptions;
import U from '../Utilities.js';
var addEvent = U.addEvent, find = U.find, fireEvent = U.fireEvent, isNumber = U.isNumber, isObject = U.isObject, isString = U.isString, merge = U.merge, pick = U.pick, wrap = U.wrap;
import './GridAxis.js';
import './BrokenAxis.js';
/**
* @private
*/
var TreeGridAxis;
(function (TreeGridAxis) {
/* *
*
* Interfaces
*
* */
/* *
*
* Variables
*
* */
var applied = false;
/* *
*
* Functions
*
* */
/**
* @private
*/
function compose(AxisClass) {
if (!applied) {
wrap(AxisClass.prototype, 'generateTick', wrapGenerateTick);
wrap(AxisClass.prototype, 'getMaxLabelDimensions', wrapGetMaxLabelDimensions);
wrap(AxisClass.prototype, 'init', wrapInit);
wrap(AxisClass.prototype, 'setTickInterval', wrapSetTickInterval);
TreeGridTick.compose(Tick);
applied = true;
}
}
TreeGridAxis.compose = compose;
/**
* @private
*/
function getBreakFromNode(node, max) {
var from = node.collapseStart || 0, to = node.collapseEnd || 0;
// In broken-axis, the axis.max is minimized until it is not within a
// break. Therefore, if break.to is larger than axis.max, the axis.to
// should not add the 0.5 axis.tickMarkOffset, to avoid adding a break
// larger than axis.max.
// TODO consider simplifying broken-axis and this might solve itself
if (to >= max) {
from -= 0.5;
}
return {
from: from,
to: to,
showPoints: false
};
}
/**
* Creates a tree structure of the data, and the treegrid. Calculates
* categories, and y-values of points based on the tree.
*
* @private
* @function getTreeGridFromData
*
* @param {Array<Highcharts.GanttPointOptions>} data
* All the data points to display in the axis.
*
* @param {boolean} uniqueNames
* Wether or not the data node with the same name should share grid cell. If
* true they do share cell. False by default.
*
* @param {number} numberOfSeries
*
* @return {object}
* Returns an object containing categories, mapOfIdToNode,
* mapOfPosToGridNode, and tree.
*
* @todo There should be only one point per line.
* @todo It should be optional to have one category per point, or merge
* cells
* @todo Add unit-tests.
*/
function getTreeGridFromData(data, uniqueNames, numberOfSeries) {
var categories = [], collapsedNodes = [], mapOfIdToNode = {}, mapOfPosToGridNode = {}, posIterator = -1, uniqueNamesEnabled = typeof uniqueNames === 'boolean' ? uniqueNames : false, tree;
// Build the tree from the series data.
var treeParams = {
// After the children has been created.
after: function (node) {
var gridNode = mapOfPosToGridNode[node.pos], height = 0, descendants = 0;
gridNode.children.forEach(function (child) {
descendants += (child.descendants || 0) + 1;
height = Math.max((child.height || 0) + 1, height);
});
gridNode.descendants = descendants;
gridNode.height = height;
if (gridNode.collapsed) {
collapsedNodes.push(gridNode);
}
},
// Before the children has been created.
before: function (node) {
var data = isObject(node.data, true) ? node.data : {}, name = isString(data.name) ? data.name : '', parentNode = mapOfIdToNode[node.parent], parentGridNode = (isObject(parentNode, true) ?
mapOfPosToGridNode[parentNode.pos] :
null), hasSameName = function (x) {
return x.name === name;
}, gridNode, pos;
// If not unique names, look for sibling node with the same name
if (uniqueNamesEnabled &&
isObject(parentGridNode, true) &&
!!(gridNode = find(parentGridNode.children, hasSameName))) {
// If there is a gridNode with the same name, reuse position
pos = gridNode.pos;
// Add data node to list of nodes in the grid node.
gridNode.nodes.push(node);
}
else {
// If it is a new grid node, increment position.
pos = posIterator++;
}
// Add new grid node to map.
if (!mapOfPosToGridNode[pos]) {
mapOfPosToGridNode[pos] = gridNode = {
depth: parentGridNode ? parentGridNode.depth + 1 : 0,
name: name,
nodes: [node],
children: [],
pos: pos
};
// If not root, then add name to categories.
if (pos !== -1) {
categories.push(name);
}
// Add name to list of children.
if (isObject(parentGridNode, true)) {
parentGridNode.children.push(gridNode);
}
}
// Add data node to map
if (isString(node.id)) {
mapOfIdToNode[node.id] = node;
}
// If one of the points are collapsed, then start the grid node
// in collapsed state.
if (gridNode &&
data.collapsed === true) {
gridNode.collapsed = true;
}
// Assign pos to data node
node.pos = pos;
}
};
var updateYValuesAndTickPos = function (map, numberOfSeries) {
var setValues = function (gridNode, start, result) {
var nodes = gridNode.nodes, end = start + (start === -1 ? 0 : numberOfSeries - 1), diff = (end - start) / 2, padding = 0.5, pos = start + diff;
nodes.forEach(function (node) {
var data = node.data;
if (isObject(data, true)) {
// Update point
data.y = start + (data.seriesIndex || 0);
// Remove the property once used
delete data.seriesIndex;
}
node.pos = pos;
});
result[pos] = gridNode;
gridNode.pos = pos;
gridNode.tickmarkOffset = diff + padding;
gridNode.collapseStart = end + padding;
gridNode.children.forEach(function (child) {
setValues(child, end + 1, result);
end = (child.collapseEnd || 0) - padding;
});
// Set collapseEnd to the end of the last child node.
gridNode.collapseEnd = end + padding;
return result;
};
return setValues(map['-1'], -1, {});
};
// Create tree from data
tree = Tree.getTree(data, treeParams);
// Update y values of data, and set calculate tick positions.
mapOfPosToGridNode = updateYValuesAndTickPos(mapOfPosToGridNode, numberOfSeries);
// Return the resulting data.
return {
categories: categories,
mapOfIdToNode: mapOfIdToNode,
mapOfPosToGridNode: mapOfPosToGridNode,
collapsedNodes: collapsedNodes,
tree: tree
};
}
/**
* Builds the tree of categories and calculates its positions.
* @private
* @param {object} e Event object
* @param {object} e.target The chart instance which the event was fired on.
* @param {object[]} e.target.axes The axes of the chart.
*/
function onBeforeRender(e) {
var chart = e.target, axes = chart.axes;
axes.filter(function (axis) {
return axis.options.type === 'treegrid';
}).forEach(function (axis) {
var options = axis.options || {}, labelOptions = options.labels, uniqueNames = options.uniqueNames, numberOfSeries = 0, isDirty, data, treeGrid, max = options.max;
// Check whether any of series is rendering for the first time,
// visibility has changed, or its data is dirty,
// and only then update. #10570, #10580
// Also check if mapOfPosToGridNode exists. #10887
isDirty = (!axis.treeGrid.mapOfPosToGridNode ||
axis.series.some(function (series) {
return !series.hasRendered ||
series.isDirtyData ||
series.isDirty;
}));
if (isDirty) {
// Concatenate data from all series assigned to this axis.
data = axis.series.reduce(function (arr, s) {
if (s.visible) {
// Push all data to array
(s.options.data || []).forEach(function (data) {
if (isObject(data, true)) {
// Set series index on data. Removed again
// after use.
data.seriesIndex = numberOfSeries;
arr.push(data);
}
});
// Increment series index
if (uniqueNames === true) {
numberOfSeries++;
}
}
return arr;
}, []);
// If max is higher than set data - add a
// dummy data to render categories #10779
if (max && data.length < max) {
for (var i = data.length; i <= max; i++) {
data.push({
// Use the zero-width character
// to avoid conflict with uniqueNames
name: i + '\u200B'
});
}
}
// setScale is fired after all the series is initialized,
// which is an ideal time to update the axis.categories.
treeGrid = getTreeGridFromData(data, uniqueNames || false, (uniqueNames === true) ? numberOfSeries : 1);
// Assign values to the axis.
axis.categories = treeGrid.categories;
axis.treeGrid.mapOfPosToGridNode = treeGrid.mapOfPosToGridNode;
axis.hasNames = true;
axis.treeGrid.tree = treeGrid.tree;
// Update yData now that we have calculated the y values
axis.series.forEach(function (series) {
var data = (series.options.data || []).map(function (d) {
return isObject(d, true) ? merge(d) : d;
});
// Avoid destroying points when series is not visible
if (series.visible) {
series.setData(data, false);
}
});
// Calculate the label options for each level in the tree.
axis.treeGrid.mapOptionsToLevel =
getLevelOptions({
defaults: labelOptions,
from: 1,
levels: labelOptions && labelOptions.levels,
to: axis.treeGrid.tree && axis.treeGrid.tree.height
});
// Setting initial collapsed nodes
if (e.type === 'beforeRender') {
axis.treeGrid.collapsedNodes = treeGrid.collapsedNodes;
}
}
});
}
/**
* Generates a tick for initial positioning.
*
* @private
* @function Highcharts.GridAxis#generateTick
*
* @param {Function} proceed
* The original generateTick function.
*
* @param {number} pos
* The tick position in axis values.
*/
function wrapGenerateTick(proceed, pos) {
var axis = this, mapOptionsToLevel = axis.treeGrid.mapOptionsToLevel || {}, isTreeGrid = axis.options.type === 'treegrid', ticks = axis.ticks;
var tick = ticks[pos], levelOptions, options, gridNode;
if (isTreeGrid &&
axis.treeGrid.mapOfPosToGridNode) {
gridNode = axis.treeGrid.mapOfPosToGridNode[pos];
levelOptions = mapOptionsToLevel[gridNode.depth];
if (levelOptions) {
options = {
labels: levelOptions
};
}
if (!tick) {
ticks[pos] = tick =
new Tick(axis, pos, void 0, void 0, {
category: gridNode.name,
tickmarkOffset: gridNode.tickmarkOffset,
options: options
});
}
else {
// update labels depending on tick interval
tick.parameters.category = gridNode.name;
tick.options = options;
tick.addLabel();
}
}
else {
proceed.apply(axis, Array.prototype.slice.call(arguments, 1));
}
}
/**
* Override to add indentation to axis.maxLabelDimensions.
*
* @private
* @function Highcharts.GridAxis#getMaxLabelDimensions
*
* @param {Function} proceed
* The original function
*/
function wrapGetMaxLabelDimensions(proceed) {
var axis = this, options = axis.options, labelOptions = options && options.labels, indentation = (labelOptions && isNumber(labelOptions.indentation) ?
labelOptions.indentation :
0), retVal = proceed.apply(axis, Array.prototype.slice.call(arguments, 1)), isTreeGrid = axis.options.type === 'treegrid';
var treeDepth;
if (isTreeGrid && axis.treeGrid.mapOfPosToGridNode) {
treeDepth = axis.treeGrid.mapOfPosToGridNode[-1].height || 0;
retVal.width += indentation * (treeDepth - 1);
}
return retVal;
}
/**
* @private
*/
function wrapInit(proceed, chart, userOptions) {
var axis = this, isTreeGrid = userOptions.type === 'treegrid';
if (!axis.treeGrid) {
axis.treeGrid = new Additions(axis);
}
// Set default and forced options for TreeGrid
if (isTreeGrid) {
// Add event for updating the categories of a treegrid.
// NOTE Preferably these events should be set on the axis.
addEvent(chart, 'beforeRender', onBeforeRender);
addEvent(chart, 'beforeRedraw', onBeforeRender);
// Add new collapsed nodes on addseries
addEvent(chart, 'addSeries', function (e) {
if (e.options.data) {
var treeGrid = getTreeGridFromData(e.options.data, userOptions.uniqueNames || false, 1);
axis.treeGrid.collapsedNodes = (axis.treeGrid.collapsedNodes || []).concat(treeGrid.collapsedNodes);
}
});
// Collapse all nodes in axis.treegrid.collapsednodes
// where collapsed equals true.
addEvent(axis, 'foundExtremes', function () {
if (axis.treeGrid.collapsedNodes) {
axis.treeGrid.collapsedNodes.forEach(function (node) {
var breaks = axis.treeGrid.collapse(node);
if (axis.brokenAxis) {
axis.brokenAxis.setBreaks(breaks, false);
// remove the node from the axis collapsedNodes
if (axis.treeGrid.collapsedNodes) {
axis.treeGrid.collapsedNodes = axis.treeGrid.collapsedNodes.filter(function (n) {
return node.collapseStart !== n.collapseStart ||
node.collapseEnd !== n.collapseEnd;
});
}
}
});
}
});
// If staticScale is not defined on the yAxis
// and chart height is set, set axis.isDirty
// to ensure collapsing works (#12012)
addEvent(axis, 'afterBreaks', function () {
var _a;
if (axis.coll === 'yAxis' && !axis.staticScale && ((_a = axis.chart.options.chart) === null || _a === void 0 ? void 0 : _a.height)) {
axis.isDirty = true;
}
});
userOptions = merge({
// Default options
grid: {
enabled: true
},
// TODO: add support for align in treegrid.
labels: {
align: 'left',
/**
* Set options on specific levels in a tree grid axis. Takes
* precedence over labels options.
*
* @sample {gantt} gantt/treegrid-axis/labels-levels
* Levels on TreeGrid Labels
*
* @type {Array<*>}
* @product gantt
* @apioption yAxis.labels.levels
*
* @private
*/
levels: [{
/**
* Specify the level which the options within this object
* applies to.
*
* @type {number}
* @product gantt
* @apioption yAxis.labels.levels.level
*
* @private
*/
level: void 0
}, {
level: 1,
/**
* @type {Highcharts.CSSObject}
* @product gantt
* @apioption yAxis.labels.levels.style
*
* @private
*/
style: {
/** @ignore-option */
fontWeight: 'bold'
}
}],
/**
* The symbol for the collapse and expand icon in a
* treegrid.
*
* @product gantt
* @optionparent yAxis.labels.symbol
*
* @private
*/
symbol: {
/**
* The symbol type. Points to a definition function in
* the `Highcharts.Renderer.symbols` collection.
*
* @type {Highcharts.SymbolKeyValue}
*
* @private
*/
type: 'triangle',
x: -5,
y: -5,
height: 10,
width: 10,
padding: 5
}
},
uniqueNames: false
}, userOptions, {
// Forced options
reversed: true,
// grid.columns is not supported in treegrid
grid: {
columns: void 0
}
});
}
// Now apply the original function with the original arguments,
// which are sliced off this function's arguments
proceed.apply(axis, [chart, userOptions]);
if (isTreeGrid) {
axis.hasNames = true;
axis.options.showLastLabel = true;
}
}
/**
* Set the tick positions, tickInterval, axis min and max.
*
* @private
* @function Highcharts.GridAxis#setTickInterval
*
* @param {Function} proceed
* The original setTickInterval function.
*/
function wrapSetTickInterval(proceed) {
var axis = this, options = axis.options, isTreeGrid = options.type === 'treegrid';
if (isTreeGrid) {
axis.min = pick(axis.userMin, options.min, axis.dataMin);
axis.max = pick(axis.userMax, options.max, axis.dataMax);
fireEvent(axis, 'foundExtremes');
// setAxisTranslation modifies the min and max according to
// axis breaks.
axis.setAxisTranslation(true);
axis.tickmarkOffset = 0.5;
axis.tickInterval = 1;
axis.tickPositions = axis.treeGrid.mapOfPosToGridNode ?
axis.treeGrid.getTickPositions() :
[];
}
else {
proceed.apply(axis, Array.prototype.slice.call(arguments, 1));
}
}
/* *
*
* Classes
*
* */
/**
* @private
* @class
*/
var Additions = /** @class */ (function () {
/* *
*
* Constructors
*
* */
/**
* @private
*/
function Additions(axis) {
this.axis = axis;
}
/* *
*
* Functions
*
* */
/**
* Calculates the new axis breaks to collapse a node.
*
* @private
*
* @param {Highcharts.Axis} axis
* The axis to check against.
*
* @param {Highcharts.GridNode} node
* The node to collapse.
*
* @param {number} pos
* The tick position to collapse.
*
* @return {Array<object>}
* Returns an array of the new breaks for the axis.
*/
Additions.prototype.collapse = function (node) {
var axis = this.axis, breaks = (axis.options.breaks || []), obj = getBreakFromNode(node, axis.max);
breaks.push(obj);
return breaks;
};
/**
* Calculates the new axis breaks to expand a node.
*
* @private
*
* @param {Highcharts.Axis} axis
* The axis to check against.
*
* @param {Highcharts.GridNode} node
* The node to expand.
*
* @param {number} pos
* The tick position to expand.
*
* @return {Array<object>}
* Returns an array of the new breaks for the axis.
*/
Additions.prototype.expand = function (node) {
var axis = this.axis, breaks = (axis.options.breaks || []), obj = getBreakFromNode(node, axis.max);
// Remove the break from the axis breaks array.
return breaks.reduce(function (arr, b) {
if (b.to !== obj.to || b.from !== obj.from) {
arr.push(b);
}
return arr;
}, []);
};
/**
* Creates a list of positions for the ticks on the axis. Filters out
* positions that are outside min and max, or is inside an axis break.
*
* @private
*
* @return {Array<number>}
* List of positions.
*/
Additions.prototype.getTickPositions = function () {
var axis = this.axis;
return Object.keys(axis.treeGrid.mapOfPosToGridNode || {}).reduce(function (arr, key) {
var pos = +key;
if (axis.min <= pos &&
axis.max >= pos &&
!(axis.brokenAxis && axis.brokenAxis.isInAnyBreak(pos))) {
arr.push(pos);
}
return arr;
}, []);
};
/**
* Check if a node is collapsed.
*
* @private
*
* @param {Highcharts.Axis} axis
* The axis to check against.
*
* @param {object} node
* The node to check if is collapsed.
*
* @param {number} pos
* The tick position to collapse.
*
* @return {boolean}
* Returns true if collapsed, false if expanded.
*/
Additions.prototype.isCollapsed = function (node) {
var axis = this.axis, breaks = (axis.options.breaks || []), obj = getBreakFromNode(node, axis.max);
return breaks.some(function (b) {
return b.from === obj.from && b.to === obj.to;
});
};
/**
* Calculates the new axis breaks after toggling the collapse/expand
* state of a node. If it is collapsed it will be expanded, and if it is
* exapended it will be collapsed.
*
* @private
*
* @param {Highcharts.Axis} axis
* The axis to check against.
*
* @param {Highcharts.GridNode} node
* The node to toggle.
*
* @return {Array<object>}
* Returns an array of the new breaks for the axis.
*/
Additions.prototype.toggleCollapse = function (node) {
return (this.isCollapsed(node) ?
this.expand(node) :
this.collapse(node));
};
return Additions;
}());
TreeGridAxis.Additions = Additions;
})(TreeGridAxis || (TreeGridAxis = {}));
// Make utility functions available for testing.
Axis.prototype.utils = {
getNode: Tree.getNode
};
TreeGridAxis.compose(Axis);
export default TreeGridAxis;
@@ -0,0 +1,305 @@
/* *
*
* (c) 2016 Highsoft AS
* Authors: Jon Arild Nygard
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import U from '../Utilities.js';
var addEvent = U.addEvent, defined = U.defined, isObject = U.isObject, isNumber = U.isNumber, pick = U.pick, wrap = U.wrap;
/**
* @private
*/
var TreeGridTick;
(function (TreeGridTick) {
/* *
*
* Interfaces
*
* */
/* *
*
* Variables
*
* */
var applied = false;
/* *
*
* Functions
*
* */
/**
* @private
*/
function compose(TickClass) {
if (!applied) {
addEvent(TickClass, 'init', onInit);
wrap(TickClass.prototype, 'getLabelPosition', wrapGetLabelPosition);
wrap(TickClass.prototype, 'renderLabel', wrapRenderLabel);
// backwards compatibility
TickClass.prototype.collapse = function (redraw) {
this.treeGrid.collapse(redraw);
};
TickClass.prototype.expand = function (redraw) {
this.treeGrid.expand(redraw);
};
TickClass.prototype.toggleCollapse = function (redraw) {
this.treeGrid.toggleCollapse(redraw);
};
applied = true;
}
}
TreeGridTick.compose = compose;
/**
* @private
*/
function onInit() {
var tick = this;
if (!tick.treeGrid) {
tick.treeGrid = new Additions(tick);
}
}
/**
* @private
*/
function onTickHover(label) {
label.addClass('highcharts-treegrid-node-active');
if (!label.renderer.styledMode) {
label.css({
textDecoration: 'underline'
});
}
}
/**
* @private
*/
function onTickHoverExit(label, options) {
var css = defined(options.style) ? options.style : {};
label.removeClass('highcharts-treegrid-node-active');
if (!label.renderer.styledMode) {
label.css({ textDecoration: css.textDecoration });
}
}
/**
* @private
*/
function renderLabelIcon(tick, params) {
var treeGrid = tick.treeGrid, isNew = !treeGrid.labelIcon, renderer = params.renderer, labelBox = params.xy, options = params.options, width = options.width, height = options.height, iconCenter = {
x: labelBox.x - (width / 2) - options.padding,
y: labelBox.y - (height / 2)
}, rotation = params.collapsed ? 90 : 180, shouldRender = params.show && isNumber(iconCenter.y);
var icon = treeGrid.labelIcon;
if (!icon) {
treeGrid.labelIcon = icon = renderer
.path(renderer.symbols[options.type](options.x, options.y, width, height))
.addClass('highcharts-label-icon')
.add(params.group);
}
// Set the new position, and show or hide
if (!shouldRender) {
icon.attr({ y: -9999 }); // #1338
}
// Presentational attributes
if (!renderer.styledMode) {
icon
.attr({
'stroke-width': 1,
'fill': pick(params.color, '#666666')
})
.css({
cursor: 'pointer',
stroke: options.lineColor,
strokeWidth: options.lineWidth
});
}
// Update the icon positions
icon[isNew ? 'attr' : 'animate']({
translateX: iconCenter.x,
translateY: iconCenter.y,
rotation: rotation
});
}
/**
* @private
*/
function wrapGetLabelPosition(proceed, x, y, label, horiz, labelOptions, tickmarkOffset, index, step) {
var tick = this, lbOptions = pick(tick.options && tick.options.labels, labelOptions), pos = tick.pos, axis = tick.axis, options = axis.options, isTreeGrid = options.type === 'treegrid', result = proceed.apply(tick, [x, y, label, horiz, lbOptions, tickmarkOffset, index, step]);
var symbolOptions, indentation, mapOfPosToGridNode, node, level;
if (isTreeGrid) {
symbolOptions = (lbOptions && isObject(lbOptions.symbol, true) ?
lbOptions.symbol :
{});
indentation = (lbOptions && isNumber(lbOptions.indentation) ?
lbOptions.indentation :
0);
mapOfPosToGridNode = axis.treeGrid.mapOfPosToGridNode;
node = mapOfPosToGridNode && mapOfPosToGridNode[pos];
level = (node && node.depth) || 1;
result.x += (
// Add space for symbols
((symbolOptions.width) + (symbolOptions.padding * 2)) +
// Apply indentation
((level - 1) * indentation));
}
return result;
}
/**
* @private
*/
function wrapRenderLabel(proceed) {
var tick = this, pos = tick.pos, axis = tick.axis, label = tick.label, mapOfPosToGridNode = axis.treeGrid.mapOfPosToGridNode, options = axis.options, labelOptions = pick(tick.options && tick.options.labels, options && options.labels), symbolOptions = (labelOptions && isObject(labelOptions.symbol, true) ?
labelOptions.symbol :
{}), node = mapOfPosToGridNode && mapOfPosToGridNode[pos], level = node && node.depth, isTreeGrid = options.type === 'treegrid', shouldRender = axis.tickPositions.indexOf(pos) > -1, prefixClassName = 'highcharts-treegrid-node-', styledMode = axis.chart.styledMode;
var collapsed, addClassName, removeClassName;
if (isTreeGrid && node) {
// Add class name for hierarchical styling.
if (label &&
label.element) {
label.addClass(prefixClassName + 'level-' + level);
}
}
proceed.apply(tick, Array.prototype.slice.call(arguments, 1));
if (isTreeGrid &&
label &&
label.element &&
node &&
node.descendants &&
node.descendants > 0) {
collapsed = axis.treeGrid.isCollapsed(node);
renderLabelIcon(tick, {
color: !styledMode && label.styles && label.styles.color || '',
collapsed: collapsed,
group: label.parentGroup,
options: symbolOptions,
renderer: label.renderer,
show: shouldRender,
xy: label.xy
});
// Add class name for the node.
addClassName = prefixClassName +
(collapsed ? 'collapsed' : 'expanded');
removeClassName = prefixClassName +
(collapsed ? 'expanded' : 'collapsed');
label
.addClass(addClassName)
.removeClass(removeClassName);
if (!styledMode) {
label.css({
cursor: 'pointer'
});
}
// Add events to both label text and icon
[label, tick.treeGrid.labelIcon].forEach(function (object) {
if (object && !object.attachedTreeGridEvents) {
// On hover
addEvent(object.element, 'mouseover', function () {
onTickHover(label);
});
// On hover out
addEvent(object.element, 'mouseout', function () {
onTickHoverExit(label, labelOptions);
});
addEvent(object.element, 'click', function () {
tick.treeGrid.toggleCollapse();
});
object.attachedTreeGridEvents = true;
}
});
}
}
/* *
*
* Classes
*
* */
/**
* @private
* @class
*/
var Additions = /** @class */ (function () {
/* *
*
* Constructors
*
* */
/**
* @private
*/
function Additions(tick) {
this.tick = tick;
}
/* *
*
* Functions
*
* */
/**
* Collapse the grid cell. Used when axis is of type treegrid.
*
* @see gantt/treegrid-axis/collapsed-dynamically/demo.js
*
* @private
* @function Highcharts.Tick#collapse
*
* @param {boolean} [redraw=true]
* Whether to redraw the chart or wait for an explicit call to
* {@link Highcharts.Chart#redraw}
*/
Additions.prototype.collapse = function (redraw) {
var tick = this.tick, axis = tick.axis, brokenAxis = axis.brokenAxis;
if (brokenAxis &&
axis.treeGrid.mapOfPosToGridNode) {
var pos = tick.pos, node = axis.treeGrid.mapOfPosToGridNode[pos], breaks = axis.treeGrid.collapse(node);
brokenAxis.setBreaks(breaks, pick(redraw, true));
}
};
/**
* Expand the grid cell. Used when axis is of type treegrid.
*
* @see gantt/treegrid-axis/collapsed-dynamically/demo.js
*
* @private
* @function Highcharts.Tick#expand
*
* @param {boolean} [redraw=true]
* Whether to redraw the chart or wait for an explicit call to
* {@link Highcharts.Chart#redraw}
*/
Additions.prototype.expand = function (redraw) {
var tick = this.tick, axis = tick.axis, brokenAxis = axis.brokenAxis;
if (brokenAxis &&
axis.treeGrid.mapOfPosToGridNode) {
var pos = tick.pos, node = axis.treeGrid.mapOfPosToGridNode[pos], breaks = axis.treeGrid.expand(node);
brokenAxis.setBreaks(breaks, pick(redraw, true));
}
};
/**
* Toggle the collapse/expand state of the grid cell. Used when axis is
* of type treegrid.
*
* @see gantt/treegrid-axis/collapsed-dynamically/demo.js
*
* @private
* @function Highcharts.Tick#toggleCollapse
*
* @param {boolean} [redraw=true]
* Whether to redraw the chart or wait for an explicit call to
* {@link Highcharts.Chart#redraw}
*/
Additions.prototype.toggleCollapse = function (redraw) {
var tick = this.tick, axis = tick.axis, brokenAxis = axis.brokenAxis;
if (brokenAxis &&
axis.treeGrid.mapOfPosToGridNode) {
var pos = tick.pos, node = axis.treeGrid.mapOfPosToGridNode[pos], breaks = axis.treeGrid.toggleCollapse(node);
brokenAxis.setBreaks(breaks, pick(redraw, true));
}
};
return Additions;
}());
TreeGridTick.Additions = Additions;
})(TreeGridTick || (TreeGridTick = {}));
export default TreeGridTick;
@@ -0,0 +1,76 @@
/* *
*
* (c) 2010-2020 Torstein Honsi
*
* Extension to the VML Renderer
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
import U from '../Utilities.js';
var addEvent = U.addEvent;
/* eslint-disable valid-jsdoc */
var VMLAxis3DAdditions = /** @class */ (function () {
/* *
*
* Constructors
*
* */
function VMLAxis3DAdditions(axis) {
this.axis = axis;
}
return VMLAxis3DAdditions;
}());
var VMLAxis3D = /** @class */ (function () {
function VMLAxis3D() {
}
/* *
*
* Static Properties
*
* */
VMLAxis3D.compose = function (AxisClass) {
AxisClass.keepProps.push('vml');
addEvent(AxisClass, 'init', VMLAxis3D.onInit);
addEvent(AxisClass, 'render', VMLAxis3D.onRender);
};
/**
* @private
*/
VMLAxis3D.onInit = function () {
var axis = this;
if (!axis.vml) {
axis.vml = new VMLAxis3DAdditions(axis);
}
};
/**
* @private
*/
VMLAxis3D.onRender = function () {
var axis = this;
var vml = axis.vml;
// VML doesn't support a negative z-index
if (vml.sideFrame) {
vml.sideFrame.css({ zIndex: 0 });
vml.sideFrame.front.attr({
fill: vml.sideFrame.color
});
}
if (vml.bottomFrame) {
vml.bottomFrame.css({ zIndex: 1 });
vml.bottomFrame.front.attr({
fill: vml.bottomFrame.color
});
}
if (vml.backFrame) {
vml.backFrame.css({ zIndex: 0 });
vml.backFrame.front.attr({
fill: vml.backFrame.color
});
}
};
return VMLAxis3D;
}());
export default VMLAxis3D;
+155
View File
@@ -0,0 +1,155 @@
/* *
*
* (c) 2010-2020 Torstein Honsi
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
import Axis from './Axis.js';
import U from '../Utilities.js';
var addEvent = U.addEvent, merge = U.merge, pick = U.pick, splat = U.splat;
/* eslint-disable valid-jsdoc */
/**
* 3D chart with support of z coordinates.
* @private
* @class
*/
var ZChart = /** @class */ (function () {
function ZChart() {
}
/* *
*
* Static Functions
*
* */
ZChart.compose = function (ChartClass) {
addEvent(ChartClass, 'afterGetAxes', ZChart.onAfterGetAxes);
var chartProto = ChartClass.prototype;
chartProto.addZAxis = ZChart.wrapAddZAxis;
chartProto.collectionsWithInit.zAxis = [chartProto.addZAxis];
chartProto.collectionsWithUpdate.push('zAxis');
};
/**
* Get the Z axis in addition to the default X and Y.
* @private
*/
ZChart.onAfterGetAxes = function () {
var chart = this;
var options = this.options;
var zAxisOptions = options.zAxis = splat(options.zAxis || {});
if (!chart.is3d()) {
return;
}
chart.zAxis = [];
zAxisOptions.forEach(function (axisOptions, i) {
axisOptions.index = i;
// Z-Axis is shown horizontally, so it's kind of a X-Axis
axisOptions.isX = true;
chart
.addZAxis(axisOptions)
.setScale();
});
};
/**
* @private
*/
ZChart.wrapAddZAxis = function (options) {
return new ZAxis(this, options);
};
return ZChart;
}());
/**
* 3D axis for z coordinates.
*/
var ZAxis = /** @class */ (function (_super) {
__extends(ZAxis, _super);
/* *
*
* Constructors
*
* */
function ZAxis(chart, userOptions) {
var _this = _super.call(this, chart, userOptions) || this;
_this.isZAxis = true;
return _this;
}
/* *
*
* Functions
*
* */
ZAxis.prototype.getSeriesExtremes = function () {
var axis = this;
var chart = axis.chart;
axis.hasVisibleSeries = false;
// Reset properties in case we're redrawing (#3353)
axis.dataMin = axis.dataMax = axis.ignoreMinPadding = axis.ignoreMaxPadding = void 0;
if (axis.stacking) {
axis.stacking.buildStacks();
}
// loop through this axis' series
axis.series.forEach(function (series) {
if (series.visible ||
!(chart.options.chart &&
chart.options.chart.ignoreHiddenSeries)) {
var seriesOptions = series.options, zData, threshold = seriesOptions.threshold;
axis.hasVisibleSeries = true;
// Validate threshold in logarithmic axes
if (axis.positiveValuesOnly && threshold <= 0) {
threshold = void 0;
}
zData = series.zData;
if (zData.length) {
axis.dataMin = Math.min(pick(axis.dataMin, zData[0]), Math.min.apply(null, zData));
axis.dataMax = Math.max(pick(axis.dataMax, zData[0]), Math.max.apply(null, zData));
}
}
});
};
/**
* @private
*/
ZAxis.prototype.setAxisSize = function () {
var axis = this;
var chart = axis.chart;
_super.prototype.setAxisSize.call(this);
axis.width = axis.len = (chart.options.chart &&
chart.options.chart.options3d &&
chart.options.chart.options3d.depth) || 0;
axis.right = chart.chartWidth - axis.width - axis.left;
};
/**
* @private
*/
ZAxis.prototype.setOptions = function (userOptions) {
userOptions = merge({
offset: 0,
lineWidth: 0
}, userOptions);
_super.prototype.setOptions.call(this, userOptions);
this.coll = 'zAxis';
};
/* *
*
* Static Properties
*
* */
ZAxis.ZChartComposition = ZChart;
return ZAxis;
}(Axis));
export default ZAxis;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,118 @@
/* *
*
* (c) 2016-2020 Highsoft AS
*
* Author: Lars A. V. Cabrera
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import Chart from './Chart.js';
import H from '../Globals.js';
import U from '../Utilities.js';
var getOptions = U.getOptions, isArray = U.isArray, merge = U.merge, splat = U.splat;
import '../../Series/GanttSeries.js';
/**
* Factory function for Gantt charts.
*
* @example
* // Render a chart in to div#container
* var chart = Highcharts.ganttChart('container', {
* title: {
* text: 'My chart'
* },
* series: [{
* data: ...
* }]
* });
*
* @function Highcharts.ganttChart
*
* @param {string|Highcharts.HTMLDOMElement} renderTo
* The DOM element to render to, or its id.
*
* @param {Highcharts.Options} options
* The chart options structure.
*
* @param {Highcharts.ChartCallbackFunction} [callback]
* Function to run when the chart has loaded and and all external images
* are loaded. Defining a
* [chart.events.load](https://api.highcharts.com/highcharts/chart.events.load)
* handler is equivalent.
*
* @return {Highcharts.Chart}
* Returns the Chart object.
*/
H.ganttChart = function (renderTo, options, callback) {
var hasRenderToArg = typeof renderTo === 'string' || renderTo.nodeName, seriesOptions = options.series, defaultOptions = getOptions(), defaultLinkedTo, userOptions = options;
options = arguments[hasRenderToArg ? 1 : 0];
// If user hasn't defined axes as array, make it into an array and add a
// second axis by default.
if (!isArray(options.xAxis)) {
options.xAxis = [options.xAxis || {}, {}];
}
// apply X axis options to both single and multi x axes
options.xAxis = options.xAxis.map(function (xAxisOptions, i) {
if (i === 1) { // Second xAxis
defaultLinkedTo = 0;
}
return merge(defaultOptions.xAxis, {
grid: {
enabled: true
},
opposite: true,
linkedTo: defaultLinkedTo
}, xAxisOptions, // user options
{
type: 'datetime'
});
});
// apply Y axis options to both single and multi y axes
options.yAxis = (splat(options.yAxis || {})).map(function (yAxisOptions) {
return merge(defaultOptions.yAxis, // #3802
{
grid: {
enabled: true
},
staticScale: 50,
reversed: true,
// Set default type treegrid, but only if 'categories' is
// undefined
type: yAxisOptions.categories ? yAxisOptions.type : 'treegrid'
}, yAxisOptions // user options
);
});
options.series = null;
options = merge(true, {
chart: {
type: 'gantt'
},
title: {
text: null
},
legend: {
enabled: false
},
navigator: {
series: { type: 'gantt' }
}
}, options, // user's options
// forced options
{
isGantt: true
});
options.series = userOptions.series = seriesOptions;
(options.series || []).forEach(function (series) {
if (series.data) {
series.data.forEach(function (point) {
H.seriesTypes.gantt.prototype.setGanttPointAliases(point);
});
}
});
return hasRenderToArg ?
new Chart(renderTo, options, callback) :
new Chart(options, options); // @todo does not look correct
};
@@ -0,0 +1,760 @@
/* *
*
* (c) 2010-2020 Torstein Honsi
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import Axis from '../Axis/Axis.js';
import Chart from '../Chart/Chart.js';
import H from '../Globals.js';
import Point from '../Series/Point.js';
import SVGRenderer from '../Renderer/SVG/SVGRenderer.js';
import U from '../Utilities.js';
var addEvent = U.addEvent, arrayMax = U.arrayMax, arrayMin = U.arrayMin, clamp = U.clamp, defined = U.defined, extend = U.extend, find = U.find, format = U.format, getOptions = U.getOptions, isNumber = U.isNumber, isString = U.isString, merge = U.merge, pick = U.pick, splat = U.splat;
import '../Pointer.js';
import '../Series/Series.js';
// Has a dependency on Navigator due to the use of
// defaultOptions.navigator
import '../Navigator.js';
// Has a dependency on Scrollbar due to the use of
// defaultOptions.scrollbar
import '../Scrollbar.js';
// Has a dependency on RangeSelector due to the use of
// defaultOptions.rangeSelector
import '../../Extensions/RangeSelector.js';
var Series = H.Series, seriesProto = Series.prototype, seriesInit = seriesProto.init, seriesProcessData = seriesProto.processData, pointTooltipFormatter = Point.prototype.tooltipFormatter;
/**
* Compare the values of the series against the first non-null, non-
* zero value in the visible range. The y axis will show percentage
* or absolute change depending on whether `compare` is set to `"percent"`
* or `"value"`. When this is applied to multiple series, it allows
* comparing the development of the series against each other. Adds
* a `change` field to every point object.
*
* @see [compareBase](#plotOptions.series.compareBase)
* @see [Axis.setCompare()](/class-reference/Highcharts.Axis#setCompare)
*
* @sample {highstock} stock/plotoptions/series-compare-percent/
* Percent
* @sample {highstock} stock/plotoptions/series-compare-value/
* Value
*
* @type {string}
* @since 1.0.1
* @product highstock
* @apioption plotOptions.series.compare
*/
/**
* Defines if comparison should start from the first point within the visible
* range or should start from the first point **before** the range.
*
* In other words, this flag determines if first point within the visible range
* will have 0% (`compareStart=true`) or should have been already calculated
* according to the previous point (`compareStart=false`).
*
* @sample {highstock} stock/plotoptions/series-comparestart/
* Calculate compare within visible range
*
* @type {boolean}
* @default false
* @since 6.0.0
* @product highstock
* @apioption plotOptions.series.compareStart
*/
/**
* When [compare](#plotOptions.series.compare) is `percent`, this option
* dictates whether to use 0 or 100 as the base of comparison.
*
* @sample {highstock} stock/plotoptions/series-comparebase/
* Compare base is 100
*
* @type {number}
* @default 0
* @since 5.0.6
* @product highstock
* @validvalue [0, 100]
* @apioption plotOptions.series.compareBase
*/
/* eslint-disable no-invalid-this, valid-jsdoc */
/**
* Factory function for creating new stock charts. Creates a new
* {@link Highcharts.Chart|Chart} object with different default options than the
* basic Chart.
*
* @example
* var chart = Highcharts.stockChart('container', {
* series: [{
* data: [1, 2, 3, 4, 5, 6, 7, 8, 9],
* pointInterval: 24 * 60 * 60 * 1000
* }]
* });
*
* @function Highcharts.stockChart
*
* @param {string|Highcharts.HTMLDOMElement} [renderTo]
* The DOM element to render to, or its id.
*
* @param {Highcharts.Options} options
* The chart options structure as described in the
* [options reference](https://api.highcharts.com/highstock).
*
* @param {Highcharts.ChartCallbackFunction} [callback]
* A function to execute when the chart object is finished loading and
* rendering. In most cases the chart is built in one thread, but in
* Internet Explorer version 8 or less the chart is sometimes
* initialized before the document is ready, and in these cases the
* chart object will not be finished synchronously. As a consequence,
* code that relies on the newly built Chart object should always run in
* the callback. Defining a
* [chart.events.load](https://api.highcharts.com/highstock/chart.events.load)
* handler is equivalent.
*
* @return {Highcharts.Chart}
* The chart object.
*/
H.StockChart = H.stockChart = function (a, b, c) {
var hasRenderToArg = isString(a) || a.nodeName, options = arguments[hasRenderToArg ? 1 : 0], userOptions = options,
// to increase performance, don't merge the data
seriesOptions = options.series, defaultOptions = getOptions(), opposite,
// Always disable startOnTick:true on the main axis when the navigator
// is enabled (#1090)
navigatorEnabled = pick(options.navigator && options.navigator.enabled, defaultOptions.navigator.enabled, true);
// apply X axis options to both single and multi y axes
options.xAxis = splat(options.xAxis || {}).map(function (xAxisOptions, i) {
return merge({
minPadding: 0,
maxPadding: 0,
overscroll: 0,
ordinal: true,
title: {
text: null
},
labels: {
overflow: 'justify'
},
showLastLabel: true
}, defaultOptions.xAxis, // #3802
defaultOptions.xAxis && defaultOptions.xAxis[i], // #7690
xAxisOptions, // user options
{
type: 'datetime',
categories: null
}, (navigatorEnabled ? {
startOnTick: false,
endOnTick: false
} : null));
});
// apply Y axis options to both single and multi y axes
options.yAxis = splat(options.yAxis || {}).map(function (yAxisOptions, i) {
opposite = pick(yAxisOptions.opposite, true);
return merge({
labels: {
y: -2
},
opposite: opposite,
/**
* @default {highcharts} true
* @default {highstock} false
* @apioption yAxis.showLastLabel
*
* @private
*/
showLastLabel: !!(
// #6104, show last label by default for category axes
yAxisOptions.categories ||
yAxisOptions.type === 'category'),
title: {
text: null
}
}, defaultOptions.yAxis, // #3802
defaultOptions.yAxis && defaultOptions.yAxis[i], // #7690
yAxisOptions // user options
);
});
options.series = null;
options = merge({
chart: {
panning: {
enabled: true,
type: 'x'
},
pinchType: 'x'
},
navigator: {
enabled: navigatorEnabled
},
scrollbar: {
// #4988 - check if setOptions was called
enabled: pick(defaultOptions.scrollbar.enabled, true)
},
rangeSelector: {
// #4988 - check if setOptions was called
enabled: pick(defaultOptions.rangeSelector.enabled, true)
},
title: {
text: null
},
tooltip: {
split: pick(defaultOptions.tooltip.split, true),
crosshairs: true
},
legend: {
enabled: false
}
}, options, // user's options
{
isStock: true // internal flag
});
options.series = userOptions.series = seriesOptions;
return hasRenderToArg ?
new Chart(a, options, c) :
new Chart(options, b);
};
// Handle som Stock-specific series defaults, override the plotOptions before
// series options are handled.
addEvent(Series, 'setOptions', function (e) {
var overrides;
if (this.chart.options.isStock) {
if (this.is('column') || this.is('columnrange')) {
overrides = {
borderWidth: 0,
shadow: false
};
}
else if (!this.is('scatter') && !this.is('sma')) {
overrides = {
marker: {
enabled: false,
radius: 2
}
};
}
if (overrides) {
e.plotOptions[this.type] = merge(e.plotOptions[this.type], overrides);
}
}
});
// Override the automatic label alignment so that the first Y axis' labels
// are drawn on top of the grid line, and subsequent axes are drawn outside
addEvent(Axis, 'autoLabelAlign', function (e) {
var chart = this.chart, options = this.options, panes = chart._labelPanes = chart._labelPanes || {}, key, labelOptions = this.options.labels;
if (this.chart.options.isStock && this.coll === 'yAxis') {
key = options.top + ',' + options.height;
// do it only for the first Y axis of each pane
if (!panes[key] && labelOptions.enabled) {
if (labelOptions.x === 15) { // default
labelOptions.x = 0;
}
if (typeof labelOptions.align === 'undefined') {
labelOptions.align = 'right';
}
panes[key] = this;
e.align = 'right';
e.preventDefault();
}
}
});
// Clear axis from label panes (#6071)
addEvent(Axis, 'destroy', function () {
var chart = this.chart, key = this.options && (this.options.top + ',' + this.options.height);
if (key && chart._labelPanes && chart._labelPanes[key] === this) {
delete chart._labelPanes[key];
}
});
// Override getPlotLinePath to allow for multipane charts
addEvent(Axis, 'getPlotLinePath', function (e) {
var axis = this, series = (this.isLinked && !this.series ?
this.linkedParent.series :
this.series), chart = axis.chart, renderer = chart.renderer, axisLeft = axis.left, axisTop = axis.top, x1, y1, x2, y2, result = [], axes = [], // #3416 need a default array
axes2, uniqueAxes, translatedValue = e.translatedValue, value = e.value, force = e.force, transVal;
/**
* Return the other axis based on either the axis option or on related
* series.
* @private
*/
function getAxis(coll) {
var otherColl = coll === 'xAxis' ? 'yAxis' : 'xAxis', opt = axis.options[otherColl];
// Other axis indexed by number
if (isNumber(opt)) {
return [chart[otherColl][opt]];
}
// Other axis indexed by id (like navigator)
if (isString(opt)) {
return [chart.get(opt)];
}
// Auto detect based on existing series
return series.map(function (s) {
return s[otherColl];
});
}
if ( // For stock chart, by default render paths across the panes
// except the case when `acrossPanes` is disabled by user (#6644)
(chart.options.isStock && e.acrossPanes !== false) &&
// Ignore in case of colorAxis or zAxis. #3360, #3524, #6720
axis.coll === 'xAxis' || axis.coll === 'yAxis') {
e.preventDefault();
// Get the related axes based on series
axes = getAxis(axis.coll);
// Get the related axes based options.*Axis setting #2810
axes2 = (axis.isXAxis ? chart.yAxis : chart.xAxis);
axes2.forEach(function (A) {
if (defined(A.options.id) ?
A.options.id.indexOf('navigator') === -1 :
true) {
var a = (A.isXAxis ? 'yAxis' : 'xAxis'), rax = (defined(A.options[a]) ?
chart[a][A.options[a]] :
chart[a][0]);
if (axis === rax) {
axes.push(A);
}
}
});
// Remove duplicates in the axes array. If there are no axes in the axes
// array, we are adding an axis without data, so we need to populate
// this with grid lines (#2796).
uniqueAxes = axes.length ?
[] :
[axis.isXAxis ? chart.yAxis[0] : chart.xAxis[0]]; // #3742
axes.forEach(function (axis2) {
if (uniqueAxes.indexOf(axis2) === -1 &&
// Do not draw on axis which overlap completely. #5424
!find(uniqueAxes, function (unique) {
return unique.pos === axis2.pos && unique.len === axis2.len;
})) {
uniqueAxes.push(axis2);
}
});
transVal = pick(translatedValue, axis.translate(value, null, null, e.old));
if (isNumber(transVal)) {
if (axis.horiz) {
uniqueAxes.forEach(function (axis2) {
var skip;
y1 = axis2.pos;
y2 = y1 + axis2.len;
x1 = x2 = Math.round(transVal + axis.transB);
// outside plot area
if (force !== 'pass' &&
(x1 < axisLeft || x1 > axisLeft + axis.width)) {
if (force) {
x1 = x2 = clamp(x1, axisLeft, axisLeft + axis.width);
}
else {
skip = true;
}
}
if (!skip) {
result.push(['M', x1, y1], ['L', x2, y2]);
}
});
}
else {
uniqueAxes.forEach(function (axis2) {
var skip;
x1 = axis2.pos;
x2 = x1 + axis2.len;
y1 = y2 = Math.round(axisTop + axis.height - transVal);
// outside plot area
if (force !== 'pass' &&
(y1 < axisTop || y1 > axisTop + axis.height)) {
if (force) {
y1 = y2 = clamp(y1, axisTop, axisTop + axis.height);
}
else {
skip = true;
}
}
if (!skip) {
result.push(['M', x1, y1], ['L', x2, y2]);
}
});
}
}
e.path = result.length > 0 ?
renderer.crispPolyLine(result, e.lineWidth || 1) :
// #3557 getPlotLinePath in regular Highcharts also returns null
null;
}
});
/**
* Function to crisp a line with multiple segments
*
* @private
* @function Highcharts.SVGRenderer#crispPolyLine
* @param {Highcharts.SVGPathArray} points
* @param {number} width
* @return {Highcharts.SVGPathArray}
*/
SVGRenderer.prototype.crispPolyLine = function (points, width) {
// points format: [['M', 0, 0], ['L', 100, 0]]
// normalize to a crisp line
for (var i = 0; i < points.length; i = i + 2) {
var start = points[i], end = points[i + 1];
if (start[1] === end[1]) {
// Substract due to #1129. Now bottom and left axis gridlines behave
// the same.
start[1] = end[1] =
Math.round(start[1]) - (width % 2 / 2);
}
if (start[2] === end[2]) {
start[2] = end[2] =
Math.round(start[2]) + (width % 2 / 2);
}
}
return points;
};
// Wrapper to hide the label
addEvent(Axis, 'afterHideCrosshair', function () {
if (this.crossLabel) {
this.crossLabel = this.crossLabel.hide();
}
});
// Extend crosshairs to also draw the label
addEvent(Axis, 'afterDrawCrosshair', function (event) {
// Check if the label has to be drawn
if (!defined(this.crosshair.label) ||
!this.crosshair.label.enabled ||
!this.cross) {
return;
}
var chart = this.chart, log = this.logarithmic, options = this.options.crosshair.label, // the label's options
horiz = this.horiz, // axis orientation
opposite = this.opposite, // axis position
left = this.left, // left position
top = this.top, // top position
crossLabel = this.crossLabel, // the svgElement
posx, posy, crossBox, formatOption = options.format, formatFormat = '', limit, align, tickInside = this.options.tickPosition === 'inside', snap = this.crosshair.snap !== false, value, offset = 0,
// Use last available event (#5287)
e = event.e || (this.cross && this.cross.e), point = event.point, min = this.min, max = this.max;
if (log) {
min = log.lin2log(min);
max = log.lin2log(max);
}
align = (horiz ? 'center' : opposite ?
(this.labelAlign === 'right' ? 'right' : 'left') :
(this.labelAlign === 'left' ? 'left' : 'center'));
// If the label does not exist yet, create it.
if (!crossLabel) {
crossLabel = this.crossLabel = chart.renderer
.label(null, null, null, options.shape || 'callout')
.addClass('highcharts-crosshair-label' + (this.series[0] &&
' highcharts-color-' + this.series[0].colorIndex))
.attr({
align: options.align || align,
padding: pick(options.padding, 8),
r: pick(options.borderRadius, 3),
zIndex: 2
})
.add(this.labelGroup);
// Presentational
if (!chart.styledMode) {
crossLabel
.attr({
fill: options.backgroundColor ||
(this.series[0] && this.series[0].color) ||
'#666666',
stroke: options.borderColor || '',
'stroke-width': options.borderWidth || 0
})
.css(extend({
color: '#ffffff',
fontWeight: 'normal',
fontSize: '11px',
textAlign: 'center'
}, options.style));
}
}
if (horiz) {
posx = snap ? point.plotX + left : e.chartX;
posy = top + (opposite ? 0 : this.height);
}
else {
posx = opposite ? this.width + left : 0;
posy = snap ? point.plotY + top : e.chartY;
}
if (!formatOption && !options.formatter) {
if (this.dateTime) {
formatFormat = '%b %d, %Y';
}
formatOption =
'{value' + (formatFormat ? ':' + formatFormat : '') + '}';
}
// Show the label
value = snap ?
point[this.isXAxis ? 'x' : 'y'] :
this.toValue(horiz ? e.chartX : e.chartY);
crossLabel.attr({
text: formatOption ?
format(formatOption, { value: value }, chart) :
options.formatter.call(this, value),
x: posx,
y: posy,
// Crosshair should be rendered within Axis range (#7219)
visibility: value < min || value > max ?
'hidden' :
'visible'
});
crossBox = crossLabel.getBBox();
// now it is placed we can correct its position
if (isNumber(crossLabel.y)) {
if (horiz) {
if ((tickInside && !opposite) || (!tickInside && opposite)) {
posy = crossLabel.y - crossBox.height;
}
}
else {
posy = crossLabel.y - (crossBox.height / 2);
}
}
// check the edges
if (horiz) {
limit = {
left: left - crossBox.x,
right: left + this.width - crossBox.x
};
}
else {
limit = {
left: this.labelAlign === 'left' ? left : 0,
right: this.labelAlign === 'right' ?
left + this.width :
chart.chartWidth
};
}
// left edge
if (crossLabel.translateX < limit.left) {
offset = limit.left - crossLabel.translateX;
}
// right edge
if (crossLabel.translateX + crossBox.width >= limit.right) {
offset = -(crossLabel.translateX + crossBox.width - limit.right);
}
// show the crosslabel
crossLabel.attr({
x: posx + offset,
y: posy,
// First set x and y, then anchorX and anchorY, when box is actually
// calculated, #5702
anchorX: horiz ?
posx :
(this.opposite ? 0 : chart.chartWidth),
anchorY: horiz ?
(this.opposite ? chart.chartHeight : 0) :
posy + crossBox.height / 2
});
});
/* ************************************************************************** *
* Start value compare logic *
* ************************************************************************** */
/**
* Extend series.init by adding a method to modify the y value used for plotting
* on the y axis. This method is called both from the axis when finding dataMin
* and dataMax, and from the series.translate method.
*
* @ignore
* @function Highcharts.Series#init
*/
seriesProto.init = function () {
// Call base method
seriesInit.apply(this, arguments);
// Set comparison mode
this.setCompare(this.options.compare);
};
/**
* Highstock only. Set the
* [compare](https://api.highcharts.com/highstock/plotOptions.series.compare)
* mode of the series after render time. In most cases it is more useful running
* {@link Axis#setCompare} on the X axis to update all its series.
*
* @function Highcharts.Series#setCompare
*
* @param {string} [compare]
* Can be one of `null` (default), `"percent"` or `"value"`.
*/
seriesProto.setCompare = function (compare) {
// Set or unset the modifyValue method
this.modifyValue = (compare === 'value' || compare === 'percent') ?
function (value, point) {
var compareValue = this.compareValue;
if (typeof value !== 'undefined' &&
typeof compareValue !== 'undefined') { // #2601, #5814
// Get the modified value
if (compare === 'value') {
value -= compareValue;
// Compare percent
}
else {
value = 100 * (value / compareValue) -
(this.options.compareBase === 100 ? 0 : 100);
}
// record for tooltip etc.
if (point) {
point.change = value;
}
return value;
}
return 0;
} :
null;
// Survive to export, #5485
this.userOptions.compare = compare;
// Mark dirty
if (this.chart.hasRendered) {
this.isDirty = true;
}
};
/**
* Extend series.processData by finding the first y value in the plot area,
* used for comparing the following values
*
* @ignore
* @function Highcharts.Series#processData
*/
seriesProto.processData = function (force) {
var series = this, i, keyIndex = -1, processedXData, processedYData, compareStart = series.options.compareStart === true ? 0 : 1, length, compareValue;
// call base method
seriesProcessData.apply(this, arguments);
if (series.xAxis && series.processedYData) { // not pies
// local variables
processedXData = series.processedXData;
processedYData = series.processedYData;
length = processedYData.length;
// For series with more than one value (range, OHLC etc), compare
// against close or the pointValKey (#4922, #3112, #9854)
if (series.pointArrayMap) {
keyIndex = series.pointArrayMap.indexOf(series.options.pointValKey || series.pointValKey || 'y');
}
// find the first value for comparison
for (i = 0; i < length - compareStart; i++) {
compareValue = processedYData[i] && keyIndex > -1 ?
processedYData[i][keyIndex] :
processedYData[i];
if (isNumber(compareValue) &&
processedXData[i + compareStart] >=
series.xAxis.min &&
compareValue !== 0) {
series.compareValue = compareValue;
break;
}
}
}
return;
};
// Modify series extremes
addEvent(Series, 'afterGetExtremes', function (e) {
var dataExtremes = e.dataExtremes;
if (this.modifyValue && dataExtremes) {
var extremes = [
this.modifyValue(dataExtremes.dataMin),
this.modifyValue(dataExtremes.dataMax)
];
dataExtremes.dataMin = arrayMin(extremes);
dataExtremes.dataMax = arrayMax(extremes);
}
});
/**
* Highstock only. Set the compare mode on all series belonging to an Y axis
* after render time.
*
* @see [series.plotOptions.compare](https://api.highcharts.com/highstock/series.plotOptions.compare)
*
* @sample stock/members/axis-setcompare/
* Set compoare
*
* @function Highcharts.Axis#setCompare
*
* @param {string} [compare]
* The compare mode. Can be one of `null` (default), `"value"` or
* `"percent"`.
*
* @param {boolean} [redraw=true]
* Whether to redraw the chart or to wait for a later call to
* {@link Chart#redraw}.
*/
Axis.prototype.setCompare = function (compare, redraw) {
if (!this.isXAxis) {
this.series.forEach(function (series) {
series.setCompare(compare);
});
if (pick(redraw, true)) {
this.chart.redraw();
}
}
};
/**
* Extend the tooltip formatter by adding support for the point.change variable
* as well as the changeDecimals option.
*
* @ignore
* @function Highcharts.Point#tooltipFormatter
*
* @param {string} pointFormat
*/
Point.prototype.tooltipFormatter = function (pointFormat) {
var point = this;
var numberFormatter = point.series.chart.numberFormatter;
pointFormat = pointFormat.replace('{point.change}', (point.change > 0 ? '+' : '') + numberFormatter(point.change, pick(point.series.tooltipOptions.changeDecimals, 2)));
return pointTooltipFormatter.apply(this, [pointFormat]);
};
/* ************************************************************************** *
* End value compare logic *
* ************************************************************************** */
// Extend the Series prototype to create a separate series clip box. This is
// related to using multiple panes, and a future pane logic should incorporate
// this feature (#2754).
addEvent(Series, 'render', function () {
var chart = this.chart, clipHeight;
// Only do this on not 3d (#2939, #5904) nor polar (#6057) charts, and only
// if the series type handles clipping in the animate method (#2975).
if (!(chart.is3d && chart.is3d()) &&
!chart.polar &&
this.xAxis &&
!this.xAxis.isRadial // Gauge, #6192
) {
clipHeight = this.yAxis.len;
// Include xAxis line width (#8031) but only if the Y axis ends on the
// edge of the X axis (#11005).
if (this.xAxis.axisLine) {
var dist = chart.plotTop + chart.plotHeight -
this.yAxis.pos - this.yAxis.len, lineHeightCorrection = Math.floor(this.xAxis.axisLine.strokeWidth() / 2);
if (dist >= 0) {
clipHeight -= Math.max(lineHeightCorrection - dist, 0);
}
}
// First render, initial clip box
if (!this.clipBox && this.animate) {
this.clipBox = merge(chart.clipBox);
this.clipBox.width = this.xAxis.len;
this.clipBox.height = clipHeight;
// On redrawing, resizing etc, update the clip rectangle
}
else if (chart[this.sharedClipKey]) {
// animate in case resize is done during initial animation
chart[this.sharedClipKey].animate({
width: this.xAxis.len,
height: clipHeight
});
// also change markers clip animation for consistency
// (marker clip rects should exist only on chart init)
if (chart[this.sharedClipKey + 'm']) {
chart[this.sharedClipKey + 'm'].animate({
width: this.xAxis.len
});
}
}
}
});
addEvent(Chart, 'update', function (e) {
var options = e.options;
// Use case: enabling scrollbar from a disabled state.
// Scrollbar needs to be initialized from a controller, Navigator in this
// case (#6615)
if ('scrollbar' in options && this.navigator) {
merge(true, this.options.scrollbar, options.scrollbar);
this.navigator.update({}, false);
delete options.scrollbar;
}
});
+405
View File
@@ -0,0 +1,405 @@
/* *
*
* (c) 2010-2020 Torstein Honsi
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import H from './Globals.js';
/**
* A valid color to be parsed and handled by Highcharts. Highcharts internally
* supports hex colors like `#ffffff`, rgb colors like `rgb(255,255,255)` and
* rgba colors like `rgba(255,255,255,1)`. Other colors may be supported by the
* browsers and displayed correctly, but Highcharts is not able to process them
* and apply concepts like opacity and brightening.
*
* @typedef {string} Highcharts.ColorString
*/
/**
* A valid color type than can be parsed and handled by Highcharts. It can be a
* color string, a gradient object, or a pattern object.
*
* @typedef {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject} Highcharts.ColorType
*/
/**
* Gradient options instead of a solid color.
*
* @example
* // Linear gradient used as a color option
* color: {
* linearGradient: { x1: 0, x2: 0, y1: 0, y2: 1 },
* stops: [
* [0, '#003399'], // start
* [0.5, '#ffffff'], // middle
* [1, '#3366AA'] // end
* ]
* }
*
* @interface Highcharts.GradientColorObject
*/ /**
* Holds an object that defines the start position and the end position relative
* to the shape.
* @name Highcharts.GradientColorObject#linearGradient
* @type {Highcharts.LinearGradientColorObject|undefined}
*/ /**
* Holds an object that defines the center position and the radius.
* @name Highcharts.GradientColorObject#radialGradient
* @type {Highcharts.RadialGradientColorObject|undefined}
*/ /**
* The first item in each tuple is the position in the gradient, where 0 is the
* start of the gradient and 1 is the end of the gradient. Multiple stops can be
* applied. The second item is the color for each stop. This color can also be
* given in the rgba format.
* @name Highcharts.GradientColorObject#stops
* @type {Array<Highcharts.GradientColorStopObject>}
*/
/**
* Color stop tuple.
*
* @see Highcharts.GradientColorObject
*
* @interface Highcharts.GradientColorStopObject
*/ /**
* @name Highcharts.GradientColorStopObject#0
* @type {number}
*/ /**
* @name Highcharts.GradientColorStopObject#1
* @type {Highcharts.ColorString}
*/ /**
* @name Highcharts.GradientColorStopObject#color
* @type {Highcharts.Color|undefined}
*/
/**
* Defines the start position and the end position for a gradient relative
* to the shape. Start position (x1, y1) and end position (x2, y2) are relative
* to the shape, where 0 means top/left and 1 is bottom/right.
*
* @interface Highcharts.LinearGradientColorObject
*/ /**
* Start horizontal position of the gradient. Float ranges 0-1.
* @name Highcharts.LinearGradientColorObject#x1
* @type {number}
*/ /**
* End horizontal position of the gradient. Float ranges 0-1.
* @name Highcharts.LinearGradientColorObject#x2
* @type {number}
*/ /**
* Start vertical position of the gradient. Float ranges 0-1.
* @name Highcharts.LinearGradientColorObject#y1
* @type {number}
*/ /**
* End vertical position of the gradient. Float ranges 0-1.
* @name Highcharts.LinearGradientColorObject#y2
* @type {number}
*/
/**
* Defines the center position and the radius for a gradient.
*
* @interface Highcharts.RadialGradientColorObject
*/ /**
* Center horizontal position relative to the shape. Float ranges 0-1.
* @name Highcharts.RadialGradientColorObject#cx
* @type {number}
*/ /**
* Center vertical position relative to the shape. Float ranges 0-1.
* @name Highcharts.RadialGradientColorObject#cy
* @type {number}
*/ /**
* Radius relative to the shape. Float ranges 0-1.
* @name Highcharts.RadialGradientColorObject#r
* @type {number}
*/
import U from './Utilities.js';
var isNumber = U.isNumber, merge = U.merge, pInt = U.pInt;
/* eslint-disable no-invalid-this, valid-jsdoc */
/**
* Handle color operations. Some object methods are chainable.
*
* @class
* @name Highcharts.Color
*
* @param {Highcharts.ColorType} input
* The input color in either rbga or hex format
*/
var Color = /** @class */ (function () {
/* *
*
* Constructors
*
* */
function Color(input) {
// Collection of parsers. This can be extended from the outside by pushing
// parsers to Highcharts.Color.prototype.parsers.
this.parsers = [{
// RGBA color
// eslint-disable-next-line max-len
regex: /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]?(?:\.[0-9]+)?)\s*\)/,
parse: function (result) {
return [
pInt(result[1]),
pInt(result[2]),
pInt(result[3]),
parseFloat(result[4], 10)
];
}
}, {
// RGB color
regex: /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/,
parse: function (result) {
return [pInt(result[1]), pInt(result[2]), pInt(result[3]), 1];
}
}];
this.rgba = [];
// Backwards compatibility, allow instanciation without new (#13053)
if (!(this instanceof Color)) {
return new Color(input);
}
this.init(input);
}
/* *
*
* Static Functions
*
* */
/**
* Creates a color instance out of a color string or object.
*
* @function Highcharts.Color.parse
*
* @param {Highcharts.ColorType} input
* The input color in either rbga or hex format.
*
* @return {Highcharts.Color}
* Color instance.
*/
Color.parse = function (input) {
return new Color(input);
};
/* *
*
* Functions
*
* */
/**
* Parse the input color to rgba array
*
* @private
* @function Highcharts.Color#init
*
* @param {Highcharts.ColorType} input
* The input color in either rbga or hex format
*
* @return {void}
*/
Color.prototype.init = function (input) {
var result, rgba, i, parser, len;
this.input = input = Color.names[input && input.toLowerCase ?
input.toLowerCase() :
''] || input;
// Gradients
if (input && input.stops) {
this.stops = input.stops.map(function (stop) {
return new Color(stop[1]);
});
// Solid colors
}
else {
// Bitmasking as input[0] is not working for legacy IE.
if (input &&
input.charAt &&
input.charAt() === '#') {
len = input.length;
input = parseInt(input.substr(1), 16);
// Handle long-form, e.g. #AABBCC
if (len === 7) {
rgba = [
(input & 0xFF0000) >> 16,
(input & 0xFF00) >> 8,
(input & 0xFF),
1
];
// Handle short-form, e.g. #ABC
// In short form, the value is assumed to be the same
// for both nibbles for each component. e.g. #ABC = #AABBCC
}
else if (len === 4) {
rgba = [
(((input & 0xF00) >> 4) |
(input & 0xF00) >> 8),
(((input & 0xF0) >> 4) |
(input & 0xF0)),
((input & 0xF) << 4) | (input & 0xF),
1
];
}
}
// Otherwise, check regex parsers
if (!rgba) {
i = this.parsers.length;
while (i-- && !rgba) {
parser = this.parsers[i];
result = parser.regex.exec(input);
if (result) {
rgba = parser.parse(result);
}
}
}
}
this.rgba = rgba || [];
};
/**
* Return the color or gradient stops in the specified format
*
* @function Highcharts.Color#get
*
* @param {string} [format]
* Possible values are 'a', 'rgb', 'rgba' (default).
*
* @return {Highcharts.ColorType}
* This color as a string or gradient stops.
*/
Color.prototype.get = function (format) {
var input = this.input, rgba = this.rgba, ret;
if (typeof this.stops !== 'undefined') {
ret = merge(input);
ret.stops = [].concat(ret.stops);
this.stops.forEach(function (stop, i) {
ret.stops[i] = [
ret.stops[i][0],
stop.get(format)
];
});
// it's NaN if gradient colors on a column chart
}
else if (rgba && isNumber(rgba[0])) {
if (format === 'rgb' || (!format && rgba[3] === 1)) {
ret = 'rgb(' + rgba[0] + ',' + rgba[1] + ',' + rgba[2] + ')';
}
else if (format === 'a') {
ret = rgba[3];
}
else {
ret = 'rgba(' + rgba.join(',') + ')';
}
}
else {
ret = input;
}
return ret;
};
/**
* Brighten the color instance.
*
* @function Highcharts.Color#brighten
*
* @param {number} alpha
* The alpha value.
*
* @return {Highcharts.Color}
* This color with modifications.
*/
Color.prototype.brighten = function (alpha) {
var i, rgba = this.rgba;
if (this.stops) {
this.stops.forEach(function (stop) {
stop.brighten(alpha);
});
}
else if (isNumber(alpha) && alpha !== 0) {
for (i = 0; i < 3; i++) {
rgba[i] += pInt(alpha * 255);
if (rgba[i] < 0) {
rgba[i] = 0;
}
if (rgba[i] > 255) {
rgba[i] = 255;
}
}
}
return this;
};
/**
* Set the color's opacity to a given alpha value.
*
* @function Highcharts.Color#setOpacity
*
* @param {number} alpha
* Opacity between 0 and 1.
*
* @return {Highcharts.Color}
* Color with modifications.
*/
Color.prototype.setOpacity = function (alpha) {
this.rgba[3] = alpha;
return this;
};
/**
* Return an intermediate color between two colors.
*
* @function Highcharts.Color#tweenTo
*
* @param {Highcharts.Color} to
* The color object to tween to.
*
* @param {number} pos
* The intermediate position, where 0 is the from color (current
* color item), and 1 is the `to` color.
*
* @return {Highcharts.ColorString}
* The intermediate color in rgba notation.
*/
Color.prototype.tweenTo = function (to, pos) {
// Check for has alpha, because rgba colors perform worse due to lack of
// support in WebKit.
var fromRgba = this.rgba, toRgba = to.rgba, hasAlpha, ret;
// Unsupported color, return to-color (#3920, #7034)
if (!toRgba.length || !fromRgba || !fromRgba.length) {
ret = to.input || 'none';
// Interpolate
}
else {
hasAlpha = (toRgba[3] !== 1 || fromRgba[3] !== 1);
ret = (hasAlpha ? 'rgba(' : 'rgb(') +
Math.round(toRgba[0] + (fromRgba[0] - toRgba[0]) * (1 - pos)) +
',' +
Math.round(toRgba[1] + (fromRgba[1] - toRgba[1]) * (1 - pos)) +
',' +
Math.round(toRgba[2] + (fromRgba[2] - toRgba[2]) * (1 - pos)) +
(hasAlpha ?
(',' +
(toRgba[3] + (fromRgba[3] - toRgba[3]) * (1 - pos))) :
'') +
')';
}
return ret;
};
/* *
*
* Static Properties
*
* */
// Collection of named colors. Can be extended from the outside by adding
// colors to Highcharts.Color.names.
Color.names = {
white: '#ffffff',
black: '#000000'
};
return Color;
}());
H.Color = Color;
/**
* Creates a color instance out of a color string.
*
* @function Highcharts.color
*
* @param {Highcharts.ColorType} input
* The input color in either rbga or hex format
*
* @return {Highcharts.Color}
* Color instance
*/
H.color = Color.parse;
export default H.Color;
File diff suppressed because it is too large Load Diff
+85
View File
@@ -0,0 +1,85 @@
/* *
*
* (c) 2010-2020 Torstein Honsi
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
/* globals Image, window */
/**
* Reference to the global SVGElement class as a workaround for a name conflict
* in the Highcharts namespace.
*
* @global
* @typedef {global.SVGElement} GlobalSVGElement
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/SVGElement
*/
// glob is a temporary fix to allow our es-modules to work.
var glob = ( // @todo UMD variable named `window`, and glob named `win`
typeof win !== 'undefined' ?
win :
typeof window !== 'undefined' ?
window :
{}), doc = glob.document, SVG_NS = 'http://www.w3.org/2000/svg', userAgent = (glob.navigator && glob.navigator.userAgent) || '', svg = (doc &&
doc.createElementNS &&
!!doc.createElementNS(SVG_NS, 'svg').createSVGRect), isMS = /(edge|msie|trident)/i.test(userAgent) && !glob.opera, isFirefox = userAgent.indexOf('Firefox') !== -1, isChrome = userAgent.indexOf('Chrome') !== -1, hasBidiBug = (isFirefox &&
parseInt(userAgent.split('Firefox/')[1], 10) < 4 // issue #38
);
var H = {
product: 'Highcharts',
version: '8.2.0',
deg2rad: Math.PI * 2 / 360,
doc: doc,
hasBidiBug: hasBidiBug,
hasTouch: !!glob.TouchEvent,
isMS: isMS,
isWebKit: userAgent.indexOf('AppleWebKit') !== -1,
isFirefox: isFirefox,
isChrome: isChrome,
isSafari: !isChrome && userAgent.indexOf('Safari') !== -1,
isTouchDevice: /(Mobile|Android|Windows Phone)/.test(userAgent),
SVG_NS: SVG_NS,
chartCount: 0,
seriesTypes: {},
symbolSizes: {},
svg: svg,
win: glob,
marginNames: ['plotTop', 'marginRight', 'marginBottom', 'plotLeft'],
noop: function () { },
/**
* Theme options that should get applied to the chart. In module mode it
* might not be possible to change this property because of read-only
* restrictions, instead use {@link Highcharts.setOptions}.
*
* @name Highcharts.theme
* @type {Highcharts.Options}
*/
/**
* An array containing the current chart objects in the page. A chart's
* position in the array is preserved throughout the page's lifetime. When
* a chart is destroyed, the array item becomes `undefined`.
*
* @name Highcharts.charts
* @type {Array<Highcharts.Chart|undefined>}
*/
charts: [],
/**
* A hook for defining additional date format specifiers. New
* specifiers are defined as key-value pairs by using the
* specifier as key, and a function which takes the timestamp as
* value. This function returns the formatted portion of the
* date.
*
* @sample highcharts/global/dateformats/
* Adding support for week number
*
* @name Highcharts.dateFormats
* @type {Highcharts.Dictionary<Highcharts.TimeFormatCallbackFunction>}
*/
dateFormats: {}
};
export default H;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+160
View File
@@ -0,0 +1,160 @@
/* *
*
* (c) 2010-2020 Torstein Honsi
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
import H from './Globals.js';
var charts = H.charts, doc = H.doc, noop = H.noop, win = H.win;
import Pointer from './Pointer.js';
import U from './Utilities.js';
var addEvent = U.addEvent, css = U.css, objectEach = U.objectEach, removeEvent = U.removeEvent;
/* globals MSPointerEvent, PointerEvent */
// The touches object keeps track of the points being touched at all times
var touches = {};
var hasPointerEvent = !!win.PointerEvent;
/* eslint-disable valid-jsdoc */
/** @private */
function getWebkitTouches() {
var fake = [];
fake.item = function (i) {
return this[i];
};
objectEach(touches, function (touch) {
fake.push({
pageX: touch.pageX,
pageY: touch.pageY,
target: touch.target
});
});
return fake;
}
/** @private */
function translateMSPointer(e, method, wktype, func) {
var p;
if ((e.pointerType === 'touch' ||
e.pointerType === e.MSPOINTER_TYPE_TOUCH) && charts[H.hoverChartIndex]) {
func(e);
p = charts[H.hoverChartIndex].pointer;
p[method]({
type: wktype,
target: e.currentTarget,
preventDefault: noop,
touches: getWebkitTouches()
});
}
}
/** @private */
var MSPointer = /** @class */ (function (_super) {
__extends(MSPointer, _super);
function MSPointer() {
return _super !== null && _super.apply(this, arguments) || this;
}
/* *
*
* Functions
*
* */
/**
* Add or remove the MS Pointer specific events
*
* @private
* @function Highcharts.Pointer#batchMSEvents
*
* @param {Function} fn
*
* @return {void}
*/
MSPointer.prototype.batchMSEvents = function (fn) {
fn(this.chart.container, hasPointerEvent ? 'pointerdown' : 'MSPointerDown', this.onContainerPointerDown);
fn(this.chart.container, hasPointerEvent ? 'pointermove' : 'MSPointerMove', this.onContainerPointerMove);
fn(doc, hasPointerEvent ? 'pointerup' : 'MSPointerUp', this.onDocumentPointerUp);
};
// Destroy MS events also
MSPointer.prototype.destroy = function () {
this.batchMSEvents(removeEvent);
_super.prototype.destroy.call(this);
};
// Disable default IE actions for pinch and such on chart element
MSPointer.prototype.init = function (chart, options) {
_super.prototype.init.call(this, chart, options);
if (this.hasZoom) { // #4014
css(chart.container, {
'-ms-touch-action': 'none',
'touch-action': 'none'
});
}
};
/**
* @private
* @function Highcharts.Pointer#onContainerPointerDown
*
* @param {Highcharts.PointerEventObject} e
*
* @return {void}
*/
MSPointer.prototype.onContainerPointerDown = function (e) {
translateMSPointer(e, 'onContainerTouchStart', 'touchstart', function (e) {
touches[e.pointerId] = {
pageX: e.pageX,
pageY: e.pageY,
target: e.currentTarget
};
});
};
/**
* @private
* @function Highcharts.Pointer#onContainerPointerMove
*
* @param {Highcharts.PointerEventObject} e
*
* @return {void}
*/
MSPointer.prototype.onContainerPointerMove = function (e) {
translateMSPointer(e, 'onContainerTouchMove', 'touchmove', function (e) {
touches[e.pointerId] = ({ pageX: e.pageX, pageY: e.pageY });
if (!touches[e.pointerId].target) {
touches[e.pointerId].target = e.currentTarget;
}
});
};
/**
* @private
* @function Highcharts.Pointer#onDocumentPointerUp
*
* @param {Highcharts.PointerEventObject} e
*
* @return {void}
*/
MSPointer.prototype.onDocumentPointerUp = function (e) {
translateMSPointer(e, 'onDocumentTouchEnd', 'touchend', function (e) {
delete touches[e.pointerId];
});
};
// Add IE specific touch events to chart
MSPointer.prototype.setDOMEvents = function () {
_super.prototype.setDOMEvents.call(this);
if (this.hasZoom || this.followTouchMove) {
this.batchMSEvents(addEvent);
}
};
return MSPointer;
}(Pointer));
export default MSPointer;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,427 @@
/* *
*
* (c) 2010-2020 Torstein Honsi
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import H from '../../Globals.js';
import SVGElement from '../SVG/SVGElement.js';
import SVGRenderer from '../SVG/SVGRenderer.js';
import U from '../../Utilities.js';
var attr = U.attr, createElement = U.createElement, css = U.css, defined = U.defined, extend = U.extend, pick = U.pick, pInt = U.pInt;
var isFirefox = H.isFirefox, isMS = H.isMS, isWebKit = H.isWebKit, win = H.win;
/* eslint-disable valid-jsdoc */
// Extend SvgElement for useHTML option.
extend(SVGElement.prototype, /** @lends SVGElement.prototype */ {
/**
* Apply CSS to HTML elements. This is used in text within SVG rendering and
* by the VML renderer
*
* @private
* @function Highcharts.SVGElement#htmlCss
*
* @param {Highcharts.CSSObject} styles
*
* @return {Highcharts.SVGElement}
*/
htmlCss: function (styles) {
var wrapper = this, element = wrapper.element,
// When setting or unsetting the width style, we need to update
// transform (#8809)
isSettingWidth = (element.tagName === 'SPAN' &&
styles &&
'width' in styles), textWidth = pick(isSettingWidth && styles.width, void 0), doTransform;
if (isSettingWidth) {
delete styles.width;
wrapper.textWidth = textWidth;
doTransform = true;
}
if (styles && styles.textOverflow === 'ellipsis') {
styles.whiteSpace = 'nowrap';
styles.overflow = 'hidden';
}
wrapper.styles = extend(wrapper.styles, styles);
css(wrapper.element, styles);
// Now that all styles are applied, to the transform
if (doTransform) {
wrapper.htmlUpdateTransform();
}
return wrapper;
},
/**
* VML and useHTML method for calculating the bounding box based on offsets.
*
* @private
* @function Highcharts.SVGElement#htmlGetBBox
*
* @param {boolean} refresh
* Whether to force a fresh value from the DOM or to use the cached
* value.
*
* @return {Highcharts.BBoxObject}
* A hash containing values for x, y, width and height.
*/
htmlGetBBox: function () {
var wrapper = this, element = wrapper.element;
return {
x: element.offsetLeft,
y: element.offsetTop,
width: element.offsetWidth,
height: element.offsetHeight
};
},
/**
* VML override private method to update elements based on internal
* properties based on SVG transform.
*
* @private
* @function Highcharts.SVGElement#htmlUpdateTransform
* @return {void}
*/
htmlUpdateTransform: function () {
// aligning non added elements is expensive
if (!this.added) {
this.alignOnAdd = true;
return;
}
var wrapper = this, renderer = wrapper.renderer, elem = wrapper.element, translateX = wrapper.translateX || 0, translateY = wrapper.translateY || 0, x = wrapper.x || 0, y = wrapper.y || 0, align = wrapper.textAlign || 'left', alignCorrection = {
left: 0, center: 0.5, right: 1
}[align], styles = wrapper.styles, whiteSpace = styles && styles.whiteSpace;
/**
* @private
* @return {number}
*/
function getTextPxLength() {
// Reset multiline/ellipsis in order to read width (#4928,
// #5417)
css(elem, {
width: '',
whiteSpace: whiteSpace || 'nowrap'
});
return elem.offsetWidth;
}
// apply translate
css(elem, {
marginLeft: translateX,
marginTop: translateY
});
if (!renderer.styledMode && wrapper.shadows) { // used in labels/tooltip
wrapper.shadows.forEach(function (shadow) {
css(shadow, {
marginLeft: translateX + 1,
marginTop: translateY + 1
});
});
}
// apply inversion
if (wrapper.inverted) { // wrapper is a group
[].forEach.call(elem.childNodes, function (child) {
renderer.invertChild(child, elem);
});
}
if (elem.tagName === 'SPAN') {
var rotation = wrapper.rotation, baseline, textWidth = wrapper.textWidth && pInt(wrapper.textWidth), currentTextTransform = [
rotation,
align,
elem.innerHTML,
wrapper.textWidth,
wrapper.textAlign
].join(',');
// Update textWidth. Use the memoized textPxLength if possible, to
// avoid the getTextPxLength function using elem.offsetWidth.
// Calling offsetWidth affects rendering time as it forces layout
// (#7656).
if (textWidth !== wrapper.oldTextWidth &&
((textWidth > wrapper.oldTextWidth) ||
(wrapper.textPxLength || getTextPxLength()) > textWidth) && (
// Only set the width if the text is able to word-wrap, or
// text-overflow is ellipsis (#9537)
/[ \-]/.test(elem.textContent || elem.innerText) ||
elem.style.textOverflow === 'ellipsis')) { // #983, #1254
css(elem, {
width: textWidth + 'px',
display: 'block',
whiteSpace: whiteSpace || 'normal' // #3331
});
wrapper.oldTextWidth = textWidth;
wrapper.hasBoxWidthChanged = true; // #8159
}
else {
wrapper.hasBoxWidthChanged = false; // #8159
}
// Do the calculations and DOM access only if properties changed
if (currentTextTransform !== wrapper.cTT) {
baseline = renderer.fontMetrics(elem.style.fontSize, elem).b;
// Renderer specific handling of span rotation, but only if we
// have something to update.
if (defined(rotation) &&
((rotation !== (wrapper.oldRotation || 0)) ||
(align !== wrapper.oldAlign))) {
wrapper.setSpanRotation(rotation, alignCorrection, baseline);
}
wrapper.getSpanCorrection(
// Avoid elem.offsetWidth if we can, it affects rendering
// time heavily (#7656)
((!defined(rotation) && wrapper.textPxLength) || // #7920
elem.offsetWidth), baseline, alignCorrection, rotation, align);
}
// apply position with correction
css(elem, {
left: (x + (wrapper.xCorr || 0)) + 'px',
top: (y + (wrapper.yCorr || 0)) + 'px'
});
// record current text transform
wrapper.cTT = currentTextTransform;
wrapper.oldRotation = rotation;
wrapper.oldAlign = align;
}
},
/**
* Set the rotation of an individual HTML span.
*
* @private
* @function Highcharts.SVGElement#setSpanRotation
* @param {number} rotation
* @param {number} alignCorrection
* @param {number} baseline
* @return {void}
*/
setSpanRotation: function (rotation, alignCorrection, baseline) {
var rotationStyle = {}, cssTransformKey = this.renderer.getTransformKey();
rotationStyle[cssTransformKey] = rotationStyle.transform =
'rotate(' + rotation + 'deg)';
rotationStyle[cssTransformKey + (isFirefox ? 'Origin' : '-origin')] =
rotationStyle.transformOrigin =
(alignCorrection * 100) + '% ' + baseline + 'px';
css(this.element, rotationStyle);
},
/**
* Get the correction in X and Y positioning as the element is rotated.
*
* @private
* @function Highcharts.SVGElement#getSpanCorrection
* @param {number} width
* @param {number} baseline
* @param {number} alignCorrection
* @return {void}
*/
getSpanCorrection: function (width, baseline, alignCorrection) {
this.xCorr = -width * alignCorrection;
this.yCorr = -baseline;
}
});
// Extend SvgRenderer for useHTML option.
extend(SVGRenderer.prototype, /** @lends SVGRenderer.prototype */ {
/**
* @private
* @function Highcharts.SVGRenderer#getTransformKey
*
* @return {string}
*/
getTransformKey: function () {
return isMS && !/Edge/.test(win.navigator.userAgent) ?
'-ms-transform' :
isWebKit ?
'-webkit-transform' :
isFirefox ?
'MozTransform' :
win.opera ?
'-o-transform' :
'';
},
/**
* Create HTML text node. This is used by the VML renderer as well as the
* SVG renderer through the useHTML option.
*
* @private
* @function Highcharts.SVGRenderer#html
*
* @param {string} str
* The text of (subset) HTML to draw.
*
* @param {number} x
* The x position of the text's lower left corner.
*
* @param {number} y
* The y position of the text's lower left corner.
*
* @return {Highcharts.HTMLDOMElement}
*/
html: function (str, x, y) {
var wrapper = this.createElement('span'), element = wrapper.element, renderer = wrapper.renderer, isSVG = renderer.isSVG, addSetters = function (gWrapper, style) {
// These properties are set as attributes on the SVG group, and
// as identical CSS properties on the div. (#3542)
['opacity', 'visibility'].forEach(function (prop) {
gWrapper[prop + 'Setter'] = function (value, key, elem) {
var styleObject = gWrapper.div ?
gWrapper.div.style :
style;
SVGElement.prototype[prop + 'Setter']
.call(this, value, key, elem);
if (styleObject) {
styleObject[key] = value;
}
};
});
gWrapper.addedSetters = true;
};
// Text setter
wrapper.textSetter = function (value) {
if (value !== element.innerHTML) {
delete this.bBox;
delete this.oldTextWidth;
}
this.textStr = value;
element.innerHTML = pick(value, '');
wrapper.doTransform = true;
};
// Add setters for the element itself (#4938)
if (isSVG) { // #4938, only for HTML within SVG
addSetters(wrapper, wrapper.element.style);
}
// Various setters which rely on update transform
wrapper.xSetter =
wrapper.ySetter =
wrapper.alignSetter =
wrapper.rotationSetter =
function (value, key) {
if (key === 'align') {
// Do not overwrite the SVGElement.align method. Same as VML.
wrapper.alignValue = wrapper.textAlign = value;
}
else {
wrapper[key] = value;
}
wrapper.doTransform = true;
};
// Runs at the end of .attr()
wrapper.afterSetters = function () {
// Update transform. Do this outside the loop to prevent redundant
// updating for batch setting of attributes.
if (this.doTransform) {
this.htmlUpdateTransform();
this.doTransform = false;
}
};
// Set the default attributes
wrapper
.attr({
text: str,
x: Math.round(x),
y: Math.round(y)
})
.css({
position: 'absolute'
});
if (!renderer.styledMode) {
wrapper.css({
fontFamily: this.style.fontFamily,
fontSize: this.style.fontSize
});
}
// Keep the whiteSpace style outside the wrapper.styles collection
element.style.whiteSpace = 'nowrap';
// Use the HTML specific .css method
wrapper.css = wrapper.htmlCss;
// This is specific for HTML within SVG
if (isSVG) {
wrapper.add = function (svgGroupWrapper) {
var htmlGroup, container = renderer.box.parentNode, parentGroup, parents = [];
this.parentGroup = svgGroupWrapper;
// Create a mock group to hold the HTML elements
if (svgGroupWrapper) {
htmlGroup = svgGroupWrapper.div;
if (!htmlGroup) {
// Read the parent chain into an array and read from top
// down
parentGroup = svgGroupWrapper;
while (parentGroup) {
parents.push(parentGroup);
// Move up to the next parent group
parentGroup = parentGroup.parentGroup;
}
// Ensure dynamically updating position when any parent
// is translated
parents.reverse().forEach(function (parentGroup) {
var htmlGroupStyle, cls = attr(parentGroup.element, 'class');
/**
* Common translate setter for X and Y on the HTML
* group. Reverted the fix for #6957 du to
* positioning problems and offline export (#7254,
* #7280, #7529)
* @private
* @param {*} value
* @param {string} key
* @return {void}
*/
function translateSetter(value, key) {
parentGroup[key] = value;
if (key === 'translateX') {
htmlGroupStyle.left = value + 'px';
}
else {
htmlGroupStyle.top = value + 'px';
}
parentGroup.doTransform = true;
}
// Create a HTML div and append it to the parent div
// to emulate the SVG group structure
htmlGroup =
parentGroup.div =
parentGroup.div || createElement('div', cls ? { className: cls } : void 0, {
position: 'absolute',
left: (parentGroup.translateX || 0) + 'px',
top: (parentGroup.translateY || 0) + 'px',
display: parentGroup.display,
opacity: parentGroup.opacity,
pointerEvents: (parentGroup.styles &&
parentGroup.styles.pointerEvents) // #5595
// the top group is appended to container
}, htmlGroup || container);
// Shortcut
htmlGroupStyle = htmlGroup.style;
// Set listeners to update the HTML div's position
// whenever the SVG group position is changed.
extend(parentGroup, {
// (#7287) Pass htmlGroup to use
// the related group
classSetter: (function (htmlGroup) {
return function (value) {
this.element.setAttribute('class', value);
htmlGroup.className = value;
};
}(htmlGroup)),
on: function () {
if (parents[0].div) { // #6418
wrapper.on.apply({ element: parents[0].div }, arguments);
}
return parentGroup;
},
translateXSetter: translateSetter,
translateYSetter: translateSetter
});
if (!parentGroup.addedSetters) {
addSetters(parentGroup);
}
});
}
}
else {
htmlGroup = container;
}
htmlGroup.appendChild(element);
// Shared with VML:
wrapper.added = true;
if (wrapper.alignOnAdd) {
wrapper.htmlUpdateTransform();
}
return wrapper;
};
}
return wrapper;
}
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,411 @@
/* *
*
* (c) 2010-2020 Torstein Honsi
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
import SVGElement from './SVGElement.js';
import U from '../../Utilities.js';
var defined = U.defined, extend = U.extend, isNumber = U.isNumber, merge = U.merge, removeEvent = U.removeEvent;
/**
* SVG label to render text.
* @private
* @class
* @name Highcharts.SVGLabel
* @augments Highcharts.SVGElement
*/
var SVGLabel = /** @class */ (function (_super) {
__extends(SVGLabel, _super);
/* *
*
* Constructors
*
* */
function SVGLabel(renderer, str, x, y, shape, anchorX, anchorY, useHTML, baseline, className) {
var _this = _super.call(this) || this;
_this.init(renderer, 'g');
_this.textStr = str;
_this.x = x;
_this.y = y;
_this.anchorX = anchorX;
_this.anchorY = anchorY;
_this.baseline = baseline;
_this.className = className;
if (className !== 'button') {
_this.addClass('highcharts-label');
}
if (className) {
_this.addClass('highcharts-' + className);
}
_this.text = renderer.text('', 0, 0, useHTML)
.attr({
zIndex: 1
});
// Validate the shape argument
var hasBGImage;
if (typeof shape === 'string') {
hasBGImage = /^url\((.*?)\)$/.test(shape);
if (_this.renderer.symbols[shape] || hasBGImage) {
_this.symbolKey = shape;
}
}
_this.bBox = SVGLabel.emptyBBox;
_this.padding = 3;
_this.paddingLeft = 0;
_this.baselineOffset = 0;
_this.needsBox = renderer.styledMode || hasBGImage;
_this.deferredAttr = {};
_this.alignFactor = 0;
return _this;
}
/* *
*
* Functions
*
* */
SVGLabel.prototype.alignSetter = function (value) {
var alignFactor = {
left: 0,
center: 0.5,
right: 1
}[value];
if (alignFactor !== this.alignFactor) {
this.alignFactor = alignFactor;
// Bounding box exists, means we're dynamically changing
if (this.bBox && isNumber(this.xSetting)) {
this.attr({ x: this.xSetting }); // #5134
}
}
};
SVGLabel.prototype.anchorXSetter = function (value, key) {
this.anchorX = value;
this.boxAttr(key, Math.round(value) - this.getCrispAdjust() - this.xSetting);
};
SVGLabel.prototype.anchorYSetter = function (value, key) {
this.anchorY = value;
this.boxAttr(key, value - this.ySetting);
};
/*
* Set a box attribute, or defer it if the box is not yet created
*/
SVGLabel.prototype.boxAttr = function (key, value) {
if (this.box) {
this.box.attr(key, value);
}
else {
this.deferredAttr[key] = value;
}
};
/*
* Pick up some properties and apply them to the text instead of the
* wrapper.
*/
SVGLabel.prototype.css = function (styles) {
if (styles) {
var textStyles = {}, isWidth, isFontStyle;
// Create a copy to avoid altering the original object
// (#537)
styles = merge(styles);
SVGLabel.textProps.forEach(function (prop) {
if (typeof styles[prop] !== 'undefined') {
textStyles[prop] = styles[prop];
delete styles[prop];
}
});
this.text.css(textStyles);
isWidth = 'width' in textStyles;
isFontStyle = 'fontSize' in textStyles ||
'fontWeight' in textStyles;
// Update existing text, box (#9400, #12163)
if (isWidth || isFontStyle) {
this.updateBoxSize();
// Keep updated (#9400, #12163)
if (isFontStyle) {
this.updateTextPadding();
}
}
}
return SVGElement.prototype.css.call(this, styles);
};
/*
* Destroy and release memory.
*/
SVGLabel.prototype.destroy = function () {
// Added by button implementation
removeEvent(this.element, 'mouseenter');
removeEvent(this.element, 'mouseleave');
if (this.text) {
this.text.destroy();
}
if (this.box) {
this.box = this.box.destroy();
}
// Call base implementation to destroy the rest
SVGElement.prototype.destroy.call(this);
return void 0;
};
SVGLabel.prototype.fillSetter = function (value, key) {
if (value) {
this.needsBox = true;
}
// for animation getter (#6776)
this.fill = value;
this.boxAttr(key, value);
};
/*
* Return the bounding box of the box, not the group.
*/
SVGLabel.prototype.getBBox = function () {
var bBox = this.bBox;
var padding = this.padding;
return {
width: bBox.width + 2 * padding,
height: bBox.height + 2 * padding,
x: bBox.x - padding,
y: bBox.y - padding
};
};
SVGLabel.prototype.getCrispAdjust = function () {
return this.renderer.styledMode && this.box ?
this.box.strokeWidth() % 2 / 2 :
(this['stroke-width'] ? parseInt(this['stroke-width'], 10) : 0) % 2 / 2;
};
SVGLabel.prototype.heightSetter = function (value) {
this.heightSetting = value;
};
// Event handling. In case of useHTML, we need to make sure that events
// are captured on the span as well, and that mouseenter/mouseleave
// between the SVG group and the HTML span are not treated as real
// enter/leave events. #13310.
SVGLabel.prototype.on = function (eventType, handler) {
var label = this;
var text = label.text;
var span = text && text.element.tagName === 'SPAN' ? text : void 0;
var selectiveHandler;
if (span) {
selectiveHandler = function (e) {
if ((eventType === 'mouseenter' ||
eventType === 'mouseleave') &&
e.relatedTarget instanceof Element &&
(label.element.contains(e.relatedTarget) ||
span.element.contains(e.relatedTarget))) {
return;
}
handler.call(label.element, e);
};
span.on(eventType, selectiveHandler);
}
SVGElement.prototype.on.call(label, eventType, selectiveHandler || handler);
return label;
};
/*
* After the text element is added, get the desired size of the border
* box and add it before the text in the DOM.
*/
SVGLabel.prototype.onAdd = function () {
var str = this.textStr;
this.text.add(this);
this.attr({
// Alignment is available now (#3295, 0 not rendered if given
// as a value)
text: (defined(str) ? str : ''),
x: this.x,
y: this.y
});
if (this.box && defined(this.anchorX)) {
this.attr({
anchorX: this.anchorX,
anchorY: this.anchorY
});
}
};
SVGLabel.prototype.paddingSetter = function (value) {
if (defined(value) && value !== this.padding) {
this.padding = value;
this.updateTextPadding();
}
};
SVGLabel.prototype.paddingLeftSetter = function (value) {
if (defined(value) && value !== this.paddingLeft) {
this.paddingLeft = value;
this.updateTextPadding();
}
};
SVGLabel.prototype.rSetter = function (value, key) {
this.boxAttr(key, value);
};
SVGLabel.prototype.shadow = function (b) {
if (b && !this.renderer.styledMode) {
this.updateBoxSize();
if (this.box) {
this.box.shadow(b);
}
}
return this;
};
SVGLabel.prototype.strokeSetter = function (value, key) {
// for animation getter (#6776)
this.stroke = value;
this.boxAttr(key, value);
};
SVGLabel.prototype['stroke-widthSetter'] = function (value, key) {
if (value) {
this.needsBox = true;
}
this['stroke-width'] = value;
this.boxAttr(key, value);
};
SVGLabel.prototype['text-alignSetter'] = function (value) {
this.textAlign = value;
};
SVGLabel.prototype.textSetter = function (text) {
if (typeof text !== 'undefined') {
// Must use .attr to ensure transforms are done (#10009)
this.text.attr({ text: text });
}
this.updateBoxSize();
this.updateTextPadding();
};
/*
* This function runs after the label is added to the DOM (when the bounding
* box is available), and after the text of the label is updated to detect
* the new bounding box and reflect it in the border box.
*/
SVGLabel.prototype.updateBoxSize = function () {
var style = this.text.element.style, crispAdjust, attribs = {};
var padding = this.padding;
var paddingLeft = this.paddingLeft;
// #12165 error when width is null (auto)
// #12163 when fontweight: bold, recalculate bBox withot cache
// #3295 && 3514 box failure when string equals 0
var bBox = ((!isNumber(this.widthSetting) || !isNumber(this.heightSetting) || this.textAlign) &&
defined(this.text.textStr)) ?
this.text.getBBox() : SVGLabel.emptyBBox;
this.width = ((this.widthSetting || bBox.width || 0) +
2 * padding +
paddingLeft);
this.height = (this.heightSetting || bBox.height || 0) + 2 * padding;
// Update the label-scoped y offset. Math.min because of inline
// style (#9400)
this.baselineOffset = padding + Math.min(this.renderer.fontMetrics(style && style.fontSize, this.text).b,
// When the height is 0, there is no bBox, so go with the font
// metrics. Highmaps CSS demos.
bBox.height || Infinity);
if (this.needsBox) {
// Create the border box if it is not already present
if (!this.box) {
// Symbol definition exists (#5324)
var box = this.box = this.symbolKey ?
this.renderer.symbol(this.symbolKey) :
this.renderer.rect();
box.addClass(// Don't use label className for buttons
(this.className === 'button' ? '' : 'highcharts-label-box') +
(this.className ? ' highcharts-' + this.className + '-box' : ''));
box.add(this);
crispAdjust = this.getCrispAdjust();
attribs.x = crispAdjust;
attribs.y = (this.baseline ? -this.baselineOffset : 0) + crispAdjust;
}
// Apply the box attributes
attribs.width = Math.round(this.width);
attribs.height = Math.round(this.height);
this.box.attr(extend(attribs, this.deferredAttr));
this.deferredAttr = {};
}
this.bBox = bBox;
};
/*
* This function runs after setting text or padding, but only if padding
* is changed.
*/
SVGLabel.prototype.updateTextPadding = function () {
var text = this.text;
// Determine y based on the baseline
var textY = this.baseline ? 0 : this.baselineOffset;
var textX = this.paddingLeft + this.padding;
// compensate for alignment
if (defined(this.widthSetting) &&
this.bBox &&
(this.textAlign === 'center' || this.textAlign === 'right')) {
textX += { center: 0.5, right: 1 }[this.textAlign] *
(this.widthSetting - this.bBox.width);
}
// update if anything changed
if (textX !== text.x || textY !== text.y) {
text.attr('x', textX);
// #8159 - prevent misplaced data labels in treemap
// (useHTML: true)
if (text.hasBoxWidthChanged) {
this.bBox = text.getBBox(true);
this.updateBoxSize();
}
if (typeof textY !== 'undefined') {
text.attr('y', textY);
}
}
// record current values
text.x = textX;
text.y = textY;
};
SVGLabel.prototype.widthSetter = function (value) {
// width:auto => null
this.widthSetting = isNumber(value) ? value : void 0;
};
SVGLabel.prototype.xSetter = function (value) {
this.x = value; // for animation getter
if (this.alignFactor) {
value -= this.alignFactor * ((this.widthSetting || this.bBox.width) +
2 * this.padding);
// Force animation even when setting to the same value (#7898)
this['forceAnimate:x'] = true;
}
this.xSetting = Math.round(value);
this.attr('translateX', this.xSetting);
};
SVGLabel.prototype.ySetter = function (value) {
this.ySetting = this.y = Math.round(value);
this.attr('translateY', this.ySetting);
};
/* *
*
* Static Properties
*
* */
SVGLabel.emptyBBox = { width: 0, height: 0, x: 0, y: 0 };
/* *
*
* Properties
*
* */
/**
* For labels, these CSS properties are applied to the `text` node directly.
*
* @private
* @name Highcharts.SVGLabel#textProps
* @type {Array<string>}
*/
SVGLabel.textProps = [
'color', 'cursor', 'direction', 'fontFamily', 'fontSize', 'fontStyle',
'fontWeight', 'lineHeight', 'textAlign', 'textDecoration',
'textOutline', 'textOverflow', 'width'
];
return SVGLabel;
}(SVGElement));
export default SVGLabel;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,882 @@
/* *
*
* (c) 2010-2020 Torstein Honsi
*
* Extensions to the SVGRenderer class to enable 3D shapes
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import Color from '../../Color.js';
var color = Color.parse;
import H from '../../Globals.js';
import Math3D from '../../../Extensions/Math3D.js';
var perspective = Math3D.perspective, shapeArea = Math3D.shapeArea;
import SVGElement from './SVGElement.js';
import SVGRenderer from './SVGRenderer.js';
import U from '../../Utilities.js';
var animObject = U.animObject, defined = U.defined, extend = U.extend, merge = U.merge, objectEach = U.objectEach, pick = U.pick;
var cos = Math.cos, PI = Math.PI, sin = Math.sin;
var charts = H.charts, deg2rad = H.deg2rad,
// internal:
dFactor, element3dMethods, cuboidMethods;
/*
EXTENSION TO THE SVG-RENDERER TO ENABLE 3D SHAPES
*/
// HELPER METHODS
dFactor = (4 * (Math.sqrt(2) - 1) / 3) / (PI / 2);
/* eslint-disable no-invalid-this, valid-jsdoc */
/**
* Method to construct a curved path. Can 'wrap' around more then 180 degrees.
* @private
*/
function curveTo(cx, cy, rx, ry, start, end, dx, dy) {
var result = [], arcAngle = end - start;
if ((end > start) && (end - start > Math.PI / 2 + 0.0001)) {
result = result.concat(curveTo(cx, cy, rx, ry, start, start + (Math.PI / 2), dx, dy));
result = result.concat(curveTo(cx, cy, rx, ry, start + (Math.PI / 2), end, dx, dy));
return result;
}
if ((end < start) && (start - end > Math.PI / 2 + 0.0001)) {
result = result.concat(curveTo(cx, cy, rx, ry, start, start - (Math.PI / 2), dx, dy));
result = result.concat(curveTo(cx, cy, rx, ry, start - (Math.PI / 2), end, dx, dy));
return result;
}
return [[
'C',
cx + (rx * Math.cos(start)) -
((rx * dFactor * arcAngle) * Math.sin(start)) + dx,
cy + (ry * Math.sin(start)) +
((ry * dFactor * arcAngle) * Math.cos(start)) + dy,
cx + (rx * Math.cos(end)) +
((rx * dFactor * arcAngle) * Math.sin(end)) + dx,
cy + (ry * Math.sin(end)) -
((ry * dFactor * arcAngle) * Math.cos(end)) + dy,
cx + (rx * Math.cos(end)) + dx,
cy + (ry * Math.sin(end)) + dy
]];
}
SVGRenderer.prototype.toLinePath = function (points, closed) {
var result = [];
// Put "L x y" for each point
points.forEach(function (point) {
result.push(['L', point.x, point.y]);
});
if (points.length) {
// Set the first element to M
result[0][0] = 'M';
// If it is a closed line, add Z
if (closed) {
result.push(['Z']);
}
}
return result;
};
SVGRenderer.prototype.toLineSegments = function (points) {
var result = [], m = true;
points.forEach(function (point) {
result.push(m ? ['M', point.x, point.y] : ['L', point.x, point.y]);
m = !m;
});
return result;
};
// A 3-D Face is defined by it's 3D vertexes, and is only visible if it's
// vertexes are counter-clockwise (Back-face culling). It is used as a
// polyhedron Element
SVGRenderer.prototype.face3d = function (args) {
var renderer = this, ret = this.createElement('path');
ret.vertexes = [];
ret.insidePlotArea = false;
ret.enabled = true;
ret.attr = function (hash) {
if (typeof hash === 'object' &&
(defined(hash.enabled) ||
defined(hash.vertexes) ||
defined(hash.insidePlotArea))) {
this.enabled = pick(hash.enabled, this.enabled);
this.vertexes = pick(hash.vertexes, this.vertexes);
this.insidePlotArea = pick(hash.insidePlotArea, this.insidePlotArea);
delete hash.enabled;
delete hash.vertexes;
delete hash.insidePlotArea;
var chart = charts[renderer.chartIndex], vertexes2d = perspective(this.vertexes, chart, this.insidePlotArea), path = renderer.toLinePath(vertexes2d, true), area = shapeArea(vertexes2d), visibility = (this.enabled && area > 0) ? 'visible' : 'hidden';
hash.d = path;
hash.visibility = visibility;
}
return SVGElement.prototype.attr.apply(this, arguments);
};
ret.animate = function (params) {
if (typeof params === 'object' &&
(defined(params.enabled) ||
defined(params.vertexes) ||
defined(params.insidePlotArea))) {
this.enabled = pick(params.enabled, this.enabled);
this.vertexes = pick(params.vertexes, this.vertexes);
this.insidePlotArea = pick(params.insidePlotArea, this.insidePlotArea);
delete params.enabled;
delete params.vertexes;
delete params.insidePlotArea;
var chart = charts[renderer.chartIndex], vertexes2d = perspective(this.vertexes, chart, this.insidePlotArea), path = renderer.toLinePath(vertexes2d, true), area = shapeArea(vertexes2d), visibility = (this.enabled && area > 0) ? 'visible' : 'hidden';
params.d = path;
this.attr('visibility', visibility);
}
return SVGElement.prototype.animate.apply(this, arguments);
};
return ret.attr(args);
};
// A Polyhedron is a handy way of defining a group of 3-D faces. It's only
// attribute is `faces`, an array of attributes of each one of it's Face3D
// instances.
SVGRenderer.prototype.polyhedron = function (args) {
var renderer = this, result = this.g(), destroy = result.destroy;
if (!this.styledMode) {
result.attr({
'stroke-linejoin': 'round'
});
}
result.faces = [];
// destroy all children
result.destroy = function () {
for (var i = 0; i < result.faces.length; i++) {
result.faces[i].destroy();
}
return destroy.call(this);
};
result.attr = function (hash, val, complete, continueAnimation) {
if (typeof hash === 'object' && defined(hash.faces)) {
while (result.faces.length > hash.faces.length) {
result.faces.pop().destroy();
}
while (result.faces.length < hash.faces.length) {
result.faces.push(renderer.face3d().add(result));
}
for (var i = 0; i < hash.faces.length; i++) {
if (renderer.styledMode) {
delete hash.faces[i].fill;
}
result.faces[i].attr(hash.faces[i], null, complete, continueAnimation);
}
delete hash.faces;
}
return SVGElement.prototype.attr.apply(this, arguments);
};
result.animate = function (params, duration, complete) {
if (params && params.faces) {
while (result.faces.length > params.faces.length) {
result.faces.pop().destroy();
}
while (result.faces.length < params.faces.length) {
result.faces.push(renderer.face3d().add(result));
}
for (var i = 0; i < params.faces.length; i++) {
result.faces[i].animate(params.faces[i], duration, complete);
}
delete params.faces;
}
return SVGElement.prototype.animate.apply(this, arguments);
};
return result.attr(args);
};
// Base, abstract prototype member for 3D elements
element3dMethods = {
/**
* The init is used by base - renderer.Element
* @private
*/
initArgs: function (args) {
var elem3d = this, renderer = elem3d.renderer, paths = renderer[elem3d.pathType + 'Path'](args), zIndexes = paths.zIndexes;
// build parts
elem3d.parts.forEach(function (part) {
elem3d[part] = renderer.path(paths[part]).attr({
'class': 'highcharts-3d-' + part,
zIndex: zIndexes[part] || 0
}).add(elem3d);
});
elem3d.attr({
'stroke-linejoin': 'round',
zIndex: zIndexes.group
});
// store original destroy
elem3d.originalDestroy = elem3d.destroy;
elem3d.destroy = elem3d.destroyParts;
// Store information if any side of element was rendered by force.
elem3d.forcedSides = paths.forcedSides;
},
/**
* Single property setter that applies options to each part
* @private
*/
singleSetterForParts: function (prop, val, values, verb, duration, complete) {
var elem3d = this, newAttr = {}, optionsToApply = [null, null, (verb || 'attr'), duration, complete], hasZIndexes = values && values.zIndexes;
if (!values) {
newAttr[prop] = val;
optionsToApply[0] = newAttr;
}
else {
// It is needed to deal with the whole group zIndexing
// in case of graph rotation
if (hasZIndexes && hasZIndexes.group) {
this.attr({
zIndex: hasZIndexes.group
});
}
objectEach(values, function (partVal, part) {
newAttr[part] = {};
newAttr[part][prop] = partVal;
// include zIndexes if provided
if (hasZIndexes) {
newAttr[part].zIndex = values.zIndexes[part] || 0;
}
});
optionsToApply[1] = newAttr;
}
return elem3d.processParts.apply(elem3d, optionsToApply);
},
/**
* Calls function for each part. Used for attr, animate and destroy.
* @private
*/
processParts: function (props, partsProps, verb, duration, complete) {
var elem3d = this;
elem3d.parts.forEach(function (part) {
// if different props for different parts
if (partsProps) {
props = pick(partsProps[part], false);
}
// only if something to set, but allow undefined
if (props !== false) {
elem3d[part][verb](props, duration, complete);
}
});
return elem3d;
},
/**
* Destroy all parts
* @private
*/
destroyParts: function () {
this.processParts(null, null, 'destroy');
return this.originalDestroy();
}
};
// CUBOID
cuboidMethods = merge(element3dMethods, {
parts: ['front', 'top', 'side'],
pathType: 'cuboid',
attr: function (args, val, complete, continueAnimation) {
// Resolve setting attributes by string name
if (typeof args === 'string' && typeof val !== 'undefined') {
var key = args;
args = {};
args[key] = val;
}
if (args.shapeArgs || defined(args.x)) {
return this.singleSetterForParts('d', null, this.renderer[this.pathType + 'Path'](args.shapeArgs || args));
}
return SVGElement.prototype.attr.call(this, args, void 0, complete, continueAnimation);
},
animate: function (args, duration, complete) {
if (defined(args.x) && defined(args.y)) {
var paths = this.renderer[this.pathType + 'Path'](args), forcedSides = paths.forcedSides;
this.singleSetterForParts('d', null, paths, 'animate', duration, complete);
this.attr({
zIndex: paths.zIndexes.group
});
// If sides that are forced to render changed, recalculate colors.
if (forcedSides !== this.forcedSides) {
this.forcedSides = forcedSides;
cuboidMethods.fillSetter.call(this, this.fill);
}
}
else {
SVGElement.prototype.animate.call(this, args, duration, complete);
}
return this;
},
fillSetter: function (fill) {
var elem3d = this;
elem3d.forcedSides = elem3d.forcedSides || [];
elem3d.singleSetterForParts('fill', null, {
front: fill,
// Do not change color if side was forced to render.
top: color(fill).brighten(elem3d.forcedSides.indexOf('top') >= 0 ? 0 : 0.1).get(),
side: color(fill).brighten(elem3d.forcedSides.indexOf('side') >= 0 ? 0 : -0.1).get()
});
// fill for animation getter (#6776)
elem3d.color = elem3d.fill = fill;
return elem3d;
}
});
// set them up
SVGRenderer.prototype.elements3d = {
base: element3dMethods,
cuboid: cuboidMethods
};
/**
* return result, generalization
* @private
* @requires highcharts-3d
*/
SVGRenderer.prototype.element3d = function (type, shapeArgs) {
// base
var ret = this.g();
// extend
extend(ret, this.elements3d[type]);
// init
ret.initArgs(shapeArgs);
// return
return ret;
};
// generelized, so now use simply
SVGRenderer.prototype.cuboid = function (shapeArgs) {
return this.element3d('cuboid', shapeArgs);
};
// Generates a cuboid path and zIndexes
SVGRenderer.prototype.cuboidPath = function (shapeArgs) {
var x = shapeArgs.x, y = shapeArgs.y, z = shapeArgs.z || 0,
// For side calculation (right/left)
// there is a need for height (and other shapeArgs arguments)
// to be at least 1px
h = shapeArgs.height, w = shapeArgs.width, d = shapeArgs.depth, chart = charts[this.chartIndex], front, back, top, bottom, left, right, shape, path1, path2, path3, isFront, isTop, isRight, options3d = chart.options.chart.options3d, alpha = options3d.alpha,
// Priority for x axis is the biggest,
// because of x direction has biggest influence on zIndex
incrementX = 1000000,
// y axis has the smallest priority in case of our charts
// (needs to be set because of stacking)
incrementY = 10, incrementZ = 100, zIndex = 0,
// The 8 corners of the cube
pArr = [{
x: x,
y: y,
z: z
}, {
x: x + w,
y: y,
z: z
}, {
x: x + w,
y: y + h,
z: z
}, {
x: x,
y: y + h,
z: z
}, {
x: x,
y: y + h,
z: z + d
}, {
x: x + w,
y: y + h,
z: z + d
}, {
x: x + w,
y: y,
z: z + d
}, {
x: x,
y: y,
z: z + d
}], forcedSides = [], pickShape;
// apply perspective
pArr = perspective(pArr, chart, shapeArgs.insidePlotArea);
/**
* helper method to decide which side is visible
* @private
*/
function mapSidePath(i) {
// Added support for 0 value in columns, where height is 0
// but the shape is rendered.
// Height is used from 1st to 6th element of pArr
if (h === 0 && i > 1 && i < 6) { // [2, 3, 4, 5]
return {
x: pArr[i].x,
// when height is 0 instead of cuboid we render plane
// so it is needed to add fake 10 height to imitate cuboid
// for side calculation
y: pArr[i].y + 10,
z: pArr[i].z
};
}
// It is needed to calculate dummy sides (front/back) for breaking
// points in case of x and depth values. If column has side,
// it means that x values of front and back side are different.
if (pArr[0].x === pArr[7].x && i >= 4) { // [4, 5, 6, 7]
return {
x: pArr[i].x + 10,
// when height is 0 instead of cuboid we render plane
// so it is needed to add fake 10 height to imitate cuboid
// for side calculation
y: pArr[i].y,
z: pArr[i].z
};
}
// Added dummy depth
if (d === 0 && i < 2 || i > 5) { // [0, 1, 6, 7]
return {
x: pArr[i].x,
// when height is 0 instead of cuboid we render plane
// so it is needed to add fake 10 height to imitate cuboid
// for side calculation
y: pArr[i].y,
z: pArr[i].z + 10
};
}
return pArr[i];
}
/**
* method creating the final side
* @private
*/
function mapPath(i) {
return pArr[i];
}
/**
* First value - path with specific face
* Second value - added information about side for later calculations.
* Possible second values are 0 for path1, 1 for path2 and -1 for no path
* chosen.
* Third value - string containing information about current side
* of cuboid for forcing side rendering.
* @private
*/
pickShape = function (verticesIndex1, verticesIndex2, side) {
var ret = [[], -1],
// An array of vertices for cuboid face
face1 = verticesIndex1.map(mapPath), face2 = verticesIndex2.map(mapPath),
// dummy face is calculated the same way as standard face,
// but if cuboid height is 0 additional height is added so it is
// possible to use this vertices array for visible face calculation
dummyFace1 = verticesIndex1.map(mapSidePath), dummyFace2 = verticesIndex2.map(mapSidePath);
if (shapeArea(face1) < 0) {
ret = [face1, 0];
}
else if (shapeArea(face2) < 0) {
ret = [face2, 1];
}
else if (side) {
forcedSides.push(side);
if (shapeArea(dummyFace1) < 0) {
ret = [face1, 0];
}
else if (shapeArea(dummyFace2) < 0) {
ret = [face2, 1];
}
else {
ret = [face1, 0]; // force side calculation.
}
}
return ret;
};
// front or back
front = [3, 2, 1, 0];
back = [7, 6, 5, 4];
shape = pickShape(front, back, 'front');
path1 = shape[0];
isFront = shape[1];
// top or bottom
top = [1, 6, 7, 0];
bottom = [4, 5, 2, 3];
shape = pickShape(top, bottom, 'top');
path2 = shape[0];
isTop = shape[1];
// side
right = [1, 2, 5, 6];
left = [0, 7, 4, 3];
shape = pickShape(right, left, 'side');
path3 = shape[0];
isRight = shape[1];
/* New block used for calculating zIndex. It is basing on X, Y and Z
position of specific columns. All zIndexes (for X, Y and Z values) are
added to the final zIndex, where every value has different priority. The
biggest priority is in X and Z directions, the lowest index is for
stacked columns (Y direction and the same X and Z positions). Big
differences between priorities is made because we need to ensure that
even for big changes in Y and Z parameters all columns will be drawn
correctly. */
if (isRight === 1) {
// It is needed to connect value with current chart width
// for big chart size.
zIndex += incrementX * (chart.plotWidth - x);
}
else if (!isRight) {
zIndex += incrementX * x;
}
zIndex += incrementY * (!isTop ||
// Numbers checked empirically
(alpha >= 0 && alpha <= 180 || alpha < 360 && alpha > 357.5) ?
chart.plotHeight - y : 10 + y);
if (isFront === 1) {
zIndex += incrementZ * (z);
}
else if (!isFront) {
zIndex += incrementZ * (1000 - z);
}
return {
front: this.toLinePath(path1, true),
top: this.toLinePath(path2, true),
side: this.toLinePath(path3, true),
zIndexes: {
group: Math.round(zIndex)
},
forcedSides: forcedSides,
// additional info about zIndexes
isFront: isFront,
isTop: isTop
}; // #4774
};
// SECTORS //
SVGRenderer.prototype.arc3d = function (attribs) {
var wrapper = this.g(), renderer = wrapper.renderer, customAttribs = ['x', 'y', 'r', 'innerR', 'start', 'end', 'depth'];
/**
* Get custom attributes. Don't mutate the original object and return an
* object with only custom attr.
* @private
*/
function suckOutCustom(params) {
var hasCA = false, ca = {}, key;
params = merge(params); // Don't mutate the original object
for (key in params) {
if (customAttribs.indexOf(key) !== -1) {
ca[key] = params[key];
delete params[key];
hasCA = true;
}
}
return hasCA ? [ca, params] : false;
}
attribs = merge(attribs);
attribs.alpha = (attribs.alpha || 0) * deg2rad;
attribs.beta = (attribs.beta || 0) * deg2rad;
// Create the different sub sections of the shape
wrapper.top = renderer.path();
wrapper.side1 = renderer.path();
wrapper.side2 = renderer.path();
wrapper.inn = renderer.path();
wrapper.out = renderer.path();
// Add all faces
wrapper.onAdd = function () {
var parent = wrapper.parentGroup, className = wrapper.attr('class');
wrapper.top.add(wrapper);
// These faces are added outside the wrapper group because the z index
// relates to neighbour elements as well
['out', 'inn', 'side1', 'side2'].forEach(function (face) {
wrapper[face]
.attr({
'class': className + ' highcharts-3d-side'
})
.add(parent);
});
};
// Cascade to faces
['addClass', 'removeClass'].forEach(function (fn) {
wrapper[fn] = function () {
var args = arguments;
['top', 'out', 'inn', 'side1', 'side2'].forEach(function (face) {
wrapper[face][fn].apply(wrapper[face], args);
});
};
});
/**
* Compute the transformed paths and set them to the composite shapes
* @private
*/
wrapper.setPaths = function (attribs) {
var paths = wrapper.renderer.arc3dPath(attribs), zIndex = paths.zTop * 100;
wrapper.attribs = attribs;
wrapper.top.attr({ d: paths.top, zIndex: paths.zTop });
wrapper.inn.attr({ d: paths.inn, zIndex: paths.zInn });
wrapper.out.attr({ d: paths.out, zIndex: paths.zOut });
wrapper.side1.attr({ d: paths.side1, zIndex: paths.zSide1 });
wrapper.side2.attr({ d: paths.side2, zIndex: paths.zSide2 });
// show all children
wrapper.zIndex = zIndex;
wrapper.attr({ zIndex: zIndex });
// Set the radial gradient center the first time
if (attribs.center) {
wrapper.top.setRadialReference(attribs.center);
delete attribs.center;
}
};
wrapper.setPaths(attribs);
/**
* Apply the fill to the top and a darker shade to the sides
* @private
*/
wrapper.fillSetter = function (value) {
var darker = color(value).brighten(-0.1).get();
this.fill = value;
this.side1.attr({ fill: darker });
this.side2.attr({ fill: darker });
this.inn.attr({ fill: darker });
this.out.attr({ fill: darker });
this.top.attr({ fill: value });
return this;
};
// Apply the same value to all. These properties cascade down to the
// children when set to the composite arc3d.
['opacity', 'translateX', 'translateY', 'visibility'].forEach(function (setter) {
wrapper[setter + 'Setter'] = function (value, key) {
wrapper[key] = value;
['out', 'inn', 'side1', 'side2', 'top'].forEach(function (el) {
wrapper[el].attr(key, value);
});
};
});
// Override attr to remove shape attributes and use those to set child paths
wrapper.attr = function (params) {
var ca, paramArr;
if (typeof params === 'object') {
paramArr = suckOutCustom(params);
if (paramArr) {
ca = paramArr[0];
arguments[0] = paramArr[1];
extend(wrapper.attribs, ca);
wrapper.setPaths(wrapper.attribs);
}
}
return SVGElement.prototype.attr.apply(wrapper, arguments);
};
// Override the animate function by sucking out custom parameters related to
// the shapes directly, and update the shapes from the animation step.
wrapper.animate = function (params, animation, complete) {
var paramArr, from = this.attribs, to, anim, randomProp = 'data-' + Math.random().toString(26).substring(2, 9);
// Attribute-line properties connected to 3D. These shouldn't have been
// in the attribs collection in the first place.
delete params.center;
delete params.z;
delete params.alpha;
delete params.beta;
anim = animObject(pick(animation, this.renderer.globalAnimation));
if (anim.duration) {
paramArr = suckOutCustom(params);
// Params need to have a property in order for the step to run
// (#5765, #7097, #7437)
wrapper[randomProp] = 0;
params[randomProp] = 1;
wrapper[randomProp + 'Setter'] = H.noop;
if (paramArr) {
to = paramArr[0]; // custom attr
anim.step = function (a, fx) {
/**
* @private
*/
function interpolate(key) {
return from[key] + (pick(to[key], from[key]) -
from[key]) * fx.pos;
}
if (fx.prop === randomProp) {
fx.elem.setPaths(merge(from, {
x: interpolate('x'),
y: interpolate('y'),
r: interpolate('r'),
innerR: interpolate('innerR'),
start: interpolate('start'),
end: interpolate('end'),
depth: interpolate('depth')
}));
}
};
}
animation = anim; // Only when duration (#5572)
}
return SVGElement.prototype.animate.call(this, params, animation, complete);
};
// destroy all children
wrapper.destroy = function () {
this.top.destroy();
this.out.destroy();
this.inn.destroy();
this.side1.destroy();
this.side2.destroy();
return SVGElement.prototype.destroy.call(this);
};
// hide all children
wrapper.hide = function () {
this.top.hide();
this.out.hide();
this.inn.hide();
this.side1.hide();
this.side2.hide();
};
wrapper.show = function (inherit) {
this.top.show(inherit);
this.out.show(inherit);
this.inn.show(inherit);
this.side1.show(inherit);
this.side2.show(inherit);
};
return wrapper;
};
// Generate the paths required to draw a 3D arc
SVGRenderer.prototype.arc3dPath = function (shapeArgs) {
var cx = shapeArgs.x, // x coordinate of the center
cy = shapeArgs.y, // y coordinate of the center
start = shapeArgs.start, // start angle
end = shapeArgs.end - 0.00001, // end angle
r = shapeArgs.r, // radius
ir = shapeArgs.innerR || 0, // inner radius
d = shapeArgs.depth || 0, // depth
alpha = shapeArgs.alpha, // alpha rotation of the chart
beta = shapeArgs.beta; // beta rotation of the chart
// Derived Variables
var cs = Math.cos(start), // cosinus of the start angle
ss = Math.sin(start), // sinus of the start angle
ce = Math.cos(end), // cosinus of the end angle
se = Math.sin(end), // sinus of the end angle
rx = r * Math.cos(beta), // x-radius
ry = r * Math.cos(alpha), // y-radius
irx = ir * Math.cos(beta), // x-radius (inner)
iry = ir * Math.cos(alpha), // y-radius (inner)
dx = d * Math.sin(beta), // distance between top and bottom in x
dy = d * Math.sin(alpha); // distance between top and bottom in y
// TOP
var top = [
['M', cx + (rx * cs), cy + (ry * ss)]
];
top = top.concat(curveTo(cx, cy, rx, ry, start, end, 0, 0));
top.push([
'L', cx + (irx * ce), cy + (iry * se)
]);
top = top.concat(curveTo(cx, cy, irx, iry, end, start, 0, 0));
top.push(['Z']);
// OUTSIDE
var b = (beta > 0 ? Math.PI / 2 : 0), a = (alpha > 0 ? 0 : Math.PI / 2);
var start2 = start > -b ? start : (end > -b ? -b : start), end2 = end < PI - a ? end : (start < PI - a ? PI - a : end), midEnd = 2 * PI - a;
// When slice goes over bottom middle, need to add both, left and right
// outer side. Additionally, when we cross right hand edge, create sharp
// edge. Outer shape/wall:
//
// -------
// / ^ \
// 4) / / \ \ 1)
// / / \ \
// / / \ \
// (c)=> ==== ==== <=(d)
// \ \ / /
// \ \<=(a)/ /
// \ \ / / <=(b)
// 3) \ v / 2)
// -------
//
// (a) - inner side
// (b) - outer side
// (c) - left edge (sharp)
// (d) - right edge (sharp)
// 1..n - rendering order for startAngle = 0, when set to e.g 90, order
// changes clockwise (1->2, 2->3, n->1) and counterclockwise for negative
// startAngle
var out = [
['M', cx + (rx * cos(start2)), cy + (ry * sin(start2))]
];
out = out.concat(curveTo(cx, cy, rx, ry, start2, end2, 0, 0));
// When shape is wide, it can cross both, (c) and (d) edges, when using
// startAngle
if (end > midEnd && start < midEnd) {
// Go to outer side
out.push([
'L', cx + (rx * cos(end2)) + dx, cy + (ry * sin(end2)) + dy
]);
// Curve to the right edge of the slice (d)
out = out.concat(curveTo(cx, cy, rx, ry, end2, midEnd, dx, dy));
// Go to the inner side
out.push([
'L', cx + (rx * cos(midEnd)), cy + (ry * sin(midEnd))
]);
// Curve to the true end of the slice
out = out.concat(curveTo(cx, cy, rx, ry, midEnd, end, 0, 0));
// Go to the outer side
out.push([
'L', cx + (rx * cos(end)) + dx, cy + (ry * sin(end)) + dy
]);
// Go back to middle (d)
out = out.concat(curveTo(cx, cy, rx, ry, end, midEnd, dx, dy));
out.push([
'L', cx + (rx * cos(midEnd)), cy + (ry * sin(midEnd))
]);
// Go back to the left edge
out = out.concat(curveTo(cx, cy, rx, ry, midEnd, end2, 0, 0));
// But shape can cross also only (c) edge:
}
else if (end > PI - a && start < PI - a) {
// Go to outer side
out.push([
'L',
cx + (rx * Math.cos(end2)) + dx,
cy + (ry * Math.sin(end2)) + dy
]);
// Curve to the true end of the slice
out = out.concat(curveTo(cx, cy, rx, ry, end2, end, dx, dy));
// Go to the inner side
out.push([
'L', cx + (rx * Math.cos(end)), cy + (ry * Math.sin(end))
]);
// Go back to the artifical end2
out = out.concat(curveTo(cx, cy, rx, ry, end, end2, 0, 0));
}
out.push([
'L', cx + (rx * Math.cos(end2)) + dx, cy + (ry * Math.sin(end2)) + dy
]);
out = out.concat(curveTo(cx, cy, rx, ry, end2, start2, dx, dy));
out.push(['Z']);
// INSIDE
var inn = [
['M', cx + (irx * cs), cy + (iry * ss)]
];
inn = inn.concat(curveTo(cx, cy, irx, iry, start, end, 0, 0));
inn.push([
'L', cx + (irx * Math.cos(end)) + dx, cy + (iry * Math.sin(end)) + dy
]);
inn = inn.concat(curveTo(cx, cy, irx, iry, end, start, dx, dy));
inn.push(['Z']);
// SIDES
var side1 = [
['M', cx + (rx * cs), cy + (ry * ss)],
['L', cx + (rx * cs) + dx, cy + (ry * ss) + dy],
['L', cx + (irx * cs) + dx, cy + (iry * ss) + dy],
['L', cx + (irx * cs), cy + (iry * ss)],
['Z']
];
var side2 = [
['M', cx + (rx * ce), cy + (ry * se)],
['L', cx + (rx * ce) + dx, cy + (ry * se) + dy],
['L', cx + (irx * ce) + dx, cy + (iry * se) + dy],
['L', cx + (irx * ce), cy + (iry * se)],
['Z']
];
// correction for changed position of vanishing point caused by alpha and
// beta rotations
var angleCorr = Math.atan2(dy, -dx), angleEnd = Math.abs(end + angleCorr), angleStart = Math.abs(start + angleCorr), angleMid = Math.abs((start + end) / 2 + angleCorr);
/**
* set to 0-PI range
* @private
*/
function toZeroPIRange(angle) {
angle = angle % (2 * Math.PI);
if (angle > Math.PI) {
angle = 2 * Math.PI - angle;
}
return angle;
}
angleEnd = toZeroPIRange(angleEnd);
angleStart = toZeroPIRange(angleStart);
angleMid = toZeroPIRange(angleMid);
// *1e5 is to compensate pInt in zIndexSetter
var incPrecision = 1e5, a1 = angleMid * incPrecision, a2 = angleStart * incPrecision, a3 = angleEnd * incPrecision;
return {
top: top,
// max angle is PI, so this is always higher
zTop: Math.PI * incPrecision + 1,
out: out,
zOut: Math.max(a1, a2, a3),
inn: inn,
zInn: Math.max(a1, a2, a3),
side1: side1,
zSide1: a3 * 0.99,
side2: side2,
zSide2: a2 * 0.99
};
};
@@ -0,0 +1,37 @@
/* *
*
* (c) 2010-2020 Torstein Honsi
*
* Extension to the VML Renderer
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import Axis from '../../Axis/Axis.js';
import H from '../../Globals.js';
import SVGRenderer from '../../Renderer/SVG/SVGRenderer.js';
import U from '../../Utilities.js';
var setOptions = U.setOptions;
import VMLAxis3D from '../../Axis/VMLAxis3D.js';
var VMLRenderer = H.VMLRenderer;
if (VMLRenderer) {
setOptions({ animate: false });
VMLRenderer.prototype.face3d = SVGRenderer.prototype.face3d;
VMLRenderer.prototype.polyhedron = SVGRenderer.prototype.polyhedron;
VMLRenderer.prototype.elements3d = SVGRenderer.prototype.elements3d;
VMLRenderer.prototype.element3d = SVGRenderer.prototype.element3d;
VMLRenderer.prototype.cuboid = SVGRenderer.prototype.cuboid;
VMLRenderer.prototype.cuboidPath = SVGRenderer.prototype.cuboidPath;
VMLRenderer.prototype.toLinePath = SVGRenderer.prototype.toLinePath;
VMLRenderer.prototype.toLineSegments = SVGRenderer.prototype.toLineSegments;
VMLRenderer.prototype.arc3d = function (shapeArgs) {
var result = SVGRenderer.prototype.arc3d.call(this, shapeArgs);
result.css({ zIndex: result.zIndex });
return result;
};
H.VMLRenderer.prototype.arc3dPath = SVGRenderer.prototype.arc3dPath;
VMLAxis3D.compose(Axis);
}
+268
View File
@@ -0,0 +1,268 @@
/* *
*
* (c) 2010-2020 Torstein Honsi
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import Chart from './Chart/Chart.js';
import U from './Utilities.js';
var find = U.find, isArray = U.isArray, isObject = U.isObject, merge = U.merge, objectEach = U.objectEach, pick = U.pick, splat = U.splat, uniqueKey = U.uniqueKey;
/**
* A callback function to gain complete control on when the responsive rule
* applies.
*
* @callback Highcharts.ResponsiveCallbackFunction
*
* @param {Highcharts.Chart} this
* Chart context.
*
* @return {boolean}
* Return `true` if it applies.
*/
/**
* Allows setting a set of rules to apply for different screen or chart
* sizes. Each rule specifies additional chart options.
*
* @sample {highstock} stock/demo/responsive/
* Stock chart
* @sample highcharts/responsive/axis/
* Axis
* @sample highcharts/responsive/legend/
* Legend
* @sample highcharts/responsive/classname/
* Class name
*
* @since 5.0.0
* @apioption responsive
*/
/**
* A set of rules for responsive settings. The rules are executed from
* the top down.
*
* @sample {highcharts} highcharts/responsive/axis/
* Axis changes
* @sample {highstock} highcharts/responsive/axis/
* Axis changes
* @sample {highmaps} highcharts/responsive/axis/
* Axis changes
*
* @type {Array<*>}
* @since 5.0.0
* @apioption responsive.rules
*/
/**
* A full set of chart options to apply as overrides to the general
* chart options. The chart options are applied when the given rule
* is active.
*
* A special case is configuration objects that take arrays, for example
* [xAxis](#xAxis), [yAxis](#yAxis) or [series](#series). For these
* collections, an `id` option is used to map the new option set to
* an existing object. If an existing object of the same id is not found,
* the item of the same indexupdated. So for example, setting `chartOptions`
* with two series items without an `id`, will cause the existing chart's
* two series to be updated with respective options.
*
* @sample {highstock} stock/demo/responsive/
* Stock chart
* @sample highcharts/responsive/axis/
* Axis
* @sample highcharts/responsive/legend/
* Legend
* @sample highcharts/responsive/classname/
* Class name
*
* @type {Highcharts.Options}
* @since 5.0.0
* @apioption responsive.rules.chartOptions
*/
/**
* Under which conditions the rule applies.
*
* @since 5.0.0
* @apioption responsive.rules.condition
*/
/**
* A callback function to gain complete control on when the responsive
* rule applies. Return `true` if it applies. This opens for checking
* against other metrics than the chart size, for example the document
* size or other elements.
*
* @type {Highcharts.ResponsiveCallbackFunction}
* @since 5.0.0
* @context Highcharts.Chart
* @apioption responsive.rules.condition.callback
*/
/**
* The responsive rule applies if the chart height is less than this.
*
* @type {number}
* @since 5.0.0
* @apioption responsive.rules.condition.maxHeight
*/
/**
* The responsive rule applies if the chart width is less than this.
*
* @sample highcharts/responsive/axis/
* Max width is 500
*
* @type {number}
* @since 5.0.0
* @apioption responsive.rules.condition.maxWidth
*/
/**
* The responsive rule applies if the chart height is greater than this.
*
* @type {number}
* @default 0
* @since 5.0.0
* @apioption responsive.rules.condition.minHeight
*/
/**
* The responsive rule applies if the chart width is greater than this.
*
* @type {number}
* @default 0
* @since 5.0.0
* @apioption responsive.rules.condition.minWidth
*/
/* eslint-disable no-invalid-this, valid-jsdoc */
/**
* Update the chart based on the current chart/document size and options for
* responsiveness.
*
* @private
* @function Highcharts.Chart#setResponsive
* @param {boolean} [redraw=true]
* @param {boolean} [reset=false]
* Reset by un-applying all rules. Chart.update resets all rules before applying
* updated options.
*/
Chart.prototype.setResponsive = function (redraw, reset) {
var options = this.options.responsive, ruleIds = [], currentResponsive = this.currentResponsive, currentRuleIds, undoOptions;
if (!reset && options && options.rules) {
options.rules.forEach(function (rule) {
if (typeof rule._id === 'undefined') {
rule._id = uniqueKey();
}
this.matchResponsiveRule(rule, ruleIds /* , redraw */);
}, this);
}
// Merge matching rules
var mergedOptions = merge.apply(0, ruleIds.map(function (ruleId) {
return find(options.rules, function (rule) {
return rule._id === ruleId;
}).chartOptions;
}));
mergedOptions.isResponsiveOptions = true;
// Stringified key for the rules that currently apply.
ruleIds = (ruleIds.toString() || void 0);
currentRuleIds = currentResponsive && currentResponsive.ruleIds;
// Changes in what rules apply
if (ruleIds !== currentRuleIds) {
// Undo previous rules. Before we apply a new set of rules, we need to
// roll back completely to base options (#6291).
if (currentResponsive) {
this.update(currentResponsive.undoOptions, redraw, true);
}
if (ruleIds) {
// Get undo-options for matching rules
undoOptions = this.currentOptions(mergedOptions);
undoOptions.isResponsiveOptions = true;
this.currentResponsive = {
ruleIds: ruleIds,
mergedOptions: mergedOptions,
undoOptions: undoOptions
};
this.update(mergedOptions, redraw, true);
}
else {
this.currentResponsive = void 0;
}
}
};
/**
* Handle a single responsiveness rule.
*
* @private
* @function Highcharts.Chart#matchResponsiveRule
* @param {Highcharts.ResponsiveRulesOptions} rule
* @param {Array<string>} matches
*/
Chart.prototype.matchResponsiveRule = function (rule, matches) {
var condition = rule.condition, fn = condition.callback || function () {
return (this.chartWidth <= pick(condition.maxWidth, Number.MAX_VALUE) &&
this.chartHeight <=
pick(condition.maxHeight, Number.MAX_VALUE) &&
this.chartWidth >= pick(condition.minWidth, 0) &&
this.chartHeight >= pick(condition.minHeight, 0));
};
if (fn.call(this)) {
matches.push(rule._id);
}
};
/**
* Get the current values for a given set of options. Used before we update
* the chart with a new responsiveness rule.
*
* @todo Restore axis options (by id?). The matching of items in collections
* bears resemblance to the oneToOne matching in Chart.update. Probably we can
* refactor out that matching and reuse it in both functions.
*
* @private
* @function Highcharts.Chart#currentOptions
* @param {Highcharts.Options} options
* @return {Highcharts.Options}
*/
Chart.prototype.currentOptions = function (options) {
var chart = this, ret = {};
/**
* Recurse over a set of options and its current values,
* and store the current values in the ret object.
*/
function getCurrent(options, curr, ret, depth) {
var i;
objectEach(options, function (val, key) {
if (!depth &&
chart.collectionsWithUpdate.indexOf(key) > -1) {
val = splat(val);
ret[key] = [];
// Iterate over collections like series, xAxis or yAxis and map
// the items by index.
for (i = 0; i < Math.max(val.length, curr[key].length); i++) {
// Item exists in current data (#6347)
if (curr[key][i]) {
// If the item is missing from the new data, we need to
// save the whole config structure. Like when
// responsively updating from a dual axis layout to a
// single axis and back (#13544).
if (val[i] === void 0) {
ret[key][i] = curr[key][i];
// Otherwise, proceed
}
else {
ret[key][i] = {};
getCurrent(val[i], curr[key][i], ret[key][i], depth + 1);
}
}
}
}
else if (isObject(val)) {
ret[key] = isArray(val) ? [] : {};
getCurrent(val, curr[key] || {}, ret[key], depth + 1);
}
else if (typeof curr[key] === 'undefined') { // #10286
ret[key] = null;
}
else {
ret[key] = curr[key];
}
});
}
getCurrent(options, this.options, ret, 0);
return ret;
};
+813
View File
@@ -0,0 +1,813 @@
/* *
*
* (c) 2010-2020 Torstein Honsi
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import Axis from './Axis/Axis.js';
import H from './Globals.js';
import ScrollbarAxis from './Axis/ScrollbarAxis.js';
import U from './Utilities.js';
var addEvent = U.addEvent, correctFloat = U.correctFloat, defined = U.defined, destroyObjectProperties = U.destroyObjectProperties, fireEvent = U.fireEvent, merge = U.merge, pick = U.pick, removeEvent = U.removeEvent;
import O from './Options.js';
var defaultOptions = O.defaultOptions;
var hasTouch = H.hasTouch, isTouchDevice = H.isTouchDevice;
/**
* When we have vertical scrollbar, rifles and arrow in buttons should be
* rotated. The same method is used in Navigator's handles, to rotate them.
*
* @function Highcharts.swapXY
*
* @param {Highcharts.SVGPathArray} path
* Path to be rotated.
*
* @param {boolean} [vertical]
* If vertical scrollbar, swap x-y values.
*
* @return {Highcharts.SVGPathArray}
* Rotated path.
*
* @requires modules/stock
*/
var swapXY = H.swapXY = function (path, vertical) {
if (vertical) {
path.forEach(function (seg) {
var len = seg.length;
var temp;
for (var i = 0; i < len; i += 2) {
temp = seg[i + 1];
if (typeof temp === 'number') {
seg[i + 1] = seg[i + 2];
seg[i + 2] = temp;
}
}
});
}
return path;
};
/* eslint-disable no-invalid-this, valid-jsdoc */
/**
* A reusable scrollbar, internally used in Highstock's navigator and optionally
* on individual axes.
*
* @private
* @class
* @name Highcharts.Scrollbar
* @param {Highcharts.SVGRenderer} renderer
* @param {Highcharts.ScrollbarOptions} options
* @param {Highcharts.Chart} chart
*/
var Scrollbar = /** @class */ (function () {
/* *
*
* Constructors
*
* */
function Scrollbar(renderer, options, chart) {
/* *
*
* Properties
*
* */
this._events = [];
this.chartX = 0;
this.chartY = 0;
this.from = 0;
this.group = void 0;
this.scrollbar = void 0;
this.scrollbarButtons = [];
this.scrollbarGroup = void 0;
this.scrollbarLeft = 0;
this.scrollbarRifles = void 0;
this.scrollbarStrokeWidth = 1;
this.scrollbarTop = 0;
this.size = 0;
this.to = 0;
this.track = void 0;
this.trackBorderWidth = 1;
this.userOptions = {};
this.x = 0;
this.y = 0;
this.chart = chart;
this.options = options;
this.renderer = chart.renderer;
this.init(renderer, options, chart);
}
/* *
*
* Functions
*
* */
/**
* Set up the mouse and touch events for the Scrollbar
*
* @private
* @function Highcharts.Scrollbar#addEvents
* @return {void}
*/
Scrollbar.prototype.addEvents = function () {
var buttonsOrder = this.options.inverted ? [1, 0] : [0, 1], buttons = this.scrollbarButtons, bar = this.scrollbarGroup.element, track = this.track.element, mouseDownHandler = this.mouseDownHandler.bind(this), mouseMoveHandler = this.mouseMoveHandler.bind(this), mouseUpHandler = this.mouseUpHandler.bind(this), _events;
// Mouse events
_events = [
[buttons[buttonsOrder[0]].element, 'click', this.buttonToMinClick.bind(this)],
[buttons[buttonsOrder[1]].element, 'click', this.buttonToMaxClick.bind(this)],
[track, 'click', this.trackClick.bind(this)],
[bar, 'mousedown', mouseDownHandler],
[bar.ownerDocument, 'mousemove', mouseMoveHandler],
[bar.ownerDocument, 'mouseup', mouseUpHandler]
];
// Touch events
if (hasTouch) {
_events.push([bar, 'touchstart', mouseDownHandler], [bar.ownerDocument, 'touchmove', mouseMoveHandler], [bar.ownerDocument, 'touchend', mouseUpHandler]);
}
// Add them all
_events.forEach(function (args) {
addEvent.apply(null, args);
});
this._events = _events;
};
Scrollbar.prototype.buttonToMaxClick = function (e) {
var scroller = this;
var range = (scroller.to - scroller.from) * pick(scroller.options.step, 0.2);
scroller.updatePosition(scroller.from + range, scroller.to + range);
fireEvent(scroller, 'changed', {
from: scroller.from,
to: scroller.to,
trigger: 'scrollbar',
DOMEvent: e
});
};
Scrollbar.prototype.buttonToMinClick = function (e) {
var scroller = this;
var range = correctFloat(scroller.to - scroller.from) *
pick(scroller.options.step, 0.2);
scroller.updatePosition(correctFloat(scroller.from - range), correctFloat(scroller.to - range));
fireEvent(scroller, 'changed', {
from: scroller.from,
to: scroller.to,
trigger: 'scrollbar',
DOMEvent: e
});
};
/**
* Get normalized (0-1) cursor position over the scrollbar
*
* @private
* @function Highcharts.Scrollbar#cursorToScrollbarPosition
*
* @param {*} normalizedEvent
* normalized event, with chartX and chartY values
*
* @return {Highcharts.Dictionary<number>}
* Local position {chartX, chartY}
*/
Scrollbar.prototype.cursorToScrollbarPosition = function (normalizedEvent) {
var scroller = this, options = scroller.options, minWidthDifference = options.minWidth > scroller.calculatedWidth ?
options.minWidth :
0; // minWidth distorts translation
return {
chartX: (normalizedEvent.chartX - scroller.x -
scroller.xOffset) /
(scroller.barWidth - minWidthDifference),
chartY: (normalizedEvent.chartY - scroller.y -
scroller.yOffset) /
(scroller.barWidth - minWidthDifference)
};
};
/**
* Destroys allocated elements.
*
* @private
* @function Highcharts.Scrollbar#destroy
* @return {void}
*/
Scrollbar.prototype.destroy = function () {
var scroller = this.chart.scroller;
// Disconnect events added in addEvents
this.removeEvents();
// Destroy properties
[
'track',
'scrollbarRifles',
'scrollbar',
'scrollbarGroup',
'group'
].forEach(function (prop) {
if (this[prop] && this[prop].destroy) {
this[prop] = this[prop].destroy();
}
}, this);
// #6421, chart may have more scrollbars
if (scroller && this === scroller.scrollbar) {
scroller.scrollbar = null;
// Destroy elements in collection
destroyObjectProperties(scroller.scrollbarButtons);
}
};
/**
* Draw the scrollbar buttons with arrows
*
* @private
* @function Highcharts.Scrollbar#drawScrollbarButton
* @param {number} index
* 0 is left, 1 is right
* @return {void}
*/
Scrollbar.prototype.drawScrollbarButton = function (index) {
var scroller = this, renderer = scroller.renderer, scrollbarButtons = scroller.scrollbarButtons, options = scroller.options, size = scroller.size, group, tempElem;
group = renderer.g().add(scroller.group);
scrollbarButtons.push(group);
// Create a rectangle for the scrollbar button
tempElem = renderer.rect()
.addClass('highcharts-scrollbar-button')
.add(group);
// Presentational attributes
if (!this.chart.styledMode) {
tempElem.attr({
stroke: options.buttonBorderColor,
'stroke-width': options.buttonBorderWidth,
fill: options.buttonBackgroundColor
});
}
// Place the rectangle based on the rendered stroke width
tempElem.attr(tempElem.crisp({
x: -0.5,
y: -0.5,
width: size + 1,
height: size + 1,
r: options.buttonBorderRadius
}, tempElem.strokeWidth()));
// Button arrow
tempElem = renderer
.path(swapXY([[
'M',
size / 2 + (index ? -1 : 1),
size / 2 - 3
], [
'L',
size / 2 + (index ? -1 : 1),
size / 2 + 3
], [
'L',
size / 2 + (index ? 2 : -2),
size / 2
]], options.vertical))
.addClass('highcharts-scrollbar-arrow')
.add(scrollbarButtons[index]);
if (!this.chart.styledMode) {
tempElem.attr({
fill: options.buttonArrowColor
});
}
};
/**
* @private
* @function Highcharts.Scrollbar#init
* @param {Highcharts.SVGRenderer} renderer
* @param {Highcharts.ScrollbarOptions} options
* @param {Highcharts.Chart} chart
*/
Scrollbar.prototype.init = function (renderer, options, chart) {
this.scrollbarButtons = [];
this.renderer = renderer;
this.userOptions = options;
this.options = merge(Scrollbar.defaultOptions, options);
this.chart = chart;
// backward compatibility
this.size = pick(this.options.size, this.options.height);
// Init
if (options.enabled) {
this.render();
this.addEvents();
}
};
Scrollbar.prototype.mouseDownHandler = function (e) {
var scroller = this;
var normalizedEvent = scroller.chart.pointer.normalize(e), mousePosition = scroller.cursorToScrollbarPosition(normalizedEvent);
scroller.chartX = mousePosition.chartX;
scroller.chartY = mousePosition.chartY;
scroller.initPositions = [scroller.from, scroller.to];
scroller.grabbedCenter = true;
};
/**
* Event handler for the mouse move event.
* @private
*/
Scrollbar.prototype.mouseMoveHandler = function (e) {
var scroller = this;
var normalizedEvent = scroller.chart.pointer.normalize(e), options = scroller.options, direction = options.vertical ? 'chartY' : 'chartX', initPositions = scroller.initPositions || [], scrollPosition, chartPosition, change;
// 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.
if (scroller.grabbedCenter &&
// #4696, scrollbar failed on Android
(!e.touches || e.touches[0][direction] !== 0)) {
chartPosition = scroller.cursorToScrollbarPosition(normalizedEvent)[direction];
scrollPosition = scroller[direction];
change = chartPosition - scrollPosition;
scroller.hasDragged = true;
scroller.updatePosition(initPositions[0] + change, initPositions[1] + change);
if (scroller.hasDragged) {
fireEvent(scroller, 'changed', {
from: scroller.from,
to: scroller.to,
trigger: 'scrollbar',
DOMType: e.type,
DOMEvent: e
});
}
}
};
/**
* Event handler for the mouse up event.
* @private
*/
Scrollbar.prototype.mouseUpHandler = function (e) {
var scroller = this;
if (scroller.hasDragged) {
fireEvent(scroller, 'changed', {
from: scroller.from,
to: scroller.to,
trigger: 'scrollbar',
DOMType: e.type,
DOMEvent: e
});
}
scroller.grabbedCenter =
scroller.hasDragged =
scroller.chartX =
scroller.chartY = null;
};
/**
* Position the scrollbar, method called from a parent with defined
* dimensions.
*
* @private
* @function Highcharts.Scrollbar#position
* @param {number} x
* x-position on the chart
* @param {number} y
* y-position on the chart
* @param {number} width
* width of the scrollbar
* @param {number} height
* height of the scorllbar
* @return {void}
*/
Scrollbar.prototype.position = function (x, y, width, height) {
var scroller = this, options = scroller.options, vertical = options.vertical, xOffset = height, yOffset = 0, method = scroller.rendered ? 'animate' : 'attr';
scroller.x = x;
scroller.y = y + this.trackBorderWidth;
scroller.width = width; // width with buttons
scroller.height = height;
scroller.xOffset = xOffset;
scroller.yOffset = yOffset;
// If Scrollbar is a vertical type, swap options:
if (vertical) {
scroller.width = scroller.yOffset = width = yOffset = scroller.size;
scroller.xOffset = xOffset = 0;
scroller.barWidth = height - width * 2; // width without buttons
scroller.x = x = x + scroller.options.margin;
}
else {
scroller.height = scroller.xOffset = height = xOffset =
scroller.size;
scroller.barWidth = width - height * 2; // width without buttons
scroller.y = scroller.y + scroller.options.margin;
}
// Set general position for a group:
scroller.group[method]({
translateX: x,
translateY: scroller.y
});
// Resize background/track:
scroller.track[method]({
width: width,
height: height
});
// Move right/bottom button ot it's place:
scroller.scrollbarButtons[1][method]({
translateX: vertical ? 0 : width - xOffset,
translateY: vertical ? height - yOffset : 0
});
};
/**
* Removes the event handlers attached previously with addEvents.
*
* @private
* @function Highcharts.Scrollbar#removeEvents
* @return {void}
*/
Scrollbar.prototype.removeEvents = function () {
this._events.forEach(function (args) {
removeEvent.apply(null, args);
});
this._events.length = 0;
};
/**
* Render scrollbar with all required items.
*
* @private
* @function Highcharts.Scrollbar#render
*/
Scrollbar.prototype.render = function () {
var scroller = this, renderer = scroller.renderer, options = scroller.options, size = scroller.size, styledMode = this.chart.styledMode, group;
// Draw the scrollbar group
scroller.group = group = renderer.g('scrollbar').attr({
zIndex: options.zIndex,
translateY: -99999
}).add();
// Draw the scrollbar track:
scroller.track = renderer.rect()
.addClass('highcharts-scrollbar-track')
.attr({
x: 0,
r: options.trackBorderRadius || 0,
height: size,
width: size
}).add(group);
if (!styledMode) {
scroller.track.attr({
fill: options.trackBackgroundColor,
stroke: options.trackBorderColor,
'stroke-width': options.trackBorderWidth
});
}
this.trackBorderWidth = scroller.track.strokeWidth();
scroller.track.attr({
y: -this.trackBorderWidth % 2 / 2
});
// Draw the scrollbar itself
scroller.scrollbarGroup = renderer.g().add(group);
scroller.scrollbar = renderer.rect()
.addClass('highcharts-scrollbar-thumb')
.attr({
height: size,
width: size,
r: options.barBorderRadius || 0
}).add(scroller.scrollbarGroup);
scroller.scrollbarRifles = renderer
.path(swapXY([
['M', -3, size / 4],
['L', -3, 2 * size / 3],
['M', 0, size / 4],
['L', 0, 2 * size / 3],
['M', 3, size / 4],
['L', 3, 2 * size / 3]
], options.vertical))
.addClass('highcharts-scrollbar-rifles')
.add(scroller.scrollbarGroup);
if (!styledMode) {
scroller.scrollbar.attr({
fill: options.barBackgroundColor,
stroke: options.barBorderColor,
'stroke-width': options.barBorderWidth
});
scroller.scrollbarRifles.attr({
stroke: options.rifleColor,
'stroke-width': 1
});
}
scroller.scrollbarStrokeWidth = scroller.scrollbar.strokeWidth();
scroller.scrollbarGroup.translate(-scroller.scrollbarStrokeWidth % 2 / 2, -scroller.scrollbarStrokeWidth % 2 / 2);
// Draw the buttons:
scroller.drawScrollbarButton(0);
scroller.drawScrollbarButton(1);
};
/**
* Set scrollbar size, with a given scale.
*
* @private
* @function Highcharts.Scrollbar#setRange
* @param {number} from
* scale (0-1) where bar should start
* @param {number} to
* scale (0-1) where bar should end
* @return {void}
*/
Scrollbar.prototype.setRange = function (from, to) {
var scroller = this, options = scroller.options, vertical = options.vertical, minWidth = options.minWidth, fullWidth = scroller.barWidth, fromPX, toPX, newPos, newSize, newRiflesPos, method = (this.rendered &&
!this.hasDragged &&
!(this.chart.navigator && this.chart.navigator.hasDragged)) ? 'animate' : 'attr';
if (!defined(fullWidth)) {
return;
}
from = Math.max(from, 0);
fromPX = Math.ceil(fullWidth * from);
toPX = fullWidth * Math.min(to, 1);
scroller.calculatedWidth = newSize = correctFloat(toPX - fromPX);
// We need to recalculate position, if minWidth is used
if (newSize < minWidth) {
fromPX = (fullWidth - minWidth + newSize) * from;
newSize = minWidth;
}
newPos = Math.floor(fromPX + scroller.xOffset + scroller.yOffset);
newRiflesPos = newSize / 2 - 0.5; // -0.5 -> rifle line width / 2
// Store current position:
scroller.from = from;
scroller.to = to;
if (!vertical) {
scroller.scrollbarGroup[method]({
translateX: newPos
});
scroller.scrollbar[method]({
width: newSize
});
scroller.scrollbarRifles[method]({
translateX: newRiflesPos
});
scroller.scrollbarLeft = newPos;
scroller.scrollbarTop = 0;
}
else {
scroller.scrollbarGroup[method]({
translateY: newPos
});
scroller.scrollbar[method]({
height: newSize
});
scroller.scrollbarRifles[method]({
translateY: newRiflesPos
});
scroller.scrollbarTop = newPos;
scroller.scrollbarLeft = 0;
}
if (newSize <= 12) {
scroller.scrollbarRifles.hide();
}
else {
scroller.scrollbarRifles.show(true);
}
// Show or hide the scrollbar based on the showFull setting
if (options.showFull === false) {
if (from <= 0 && to >= 1) {
scroller.group.hide();
}
else {
scroller.group.show();
}
}
scroller.rendered = true;
};
Scrollbar.prototype.trackClick = function (e) {
var scroller = this;
var normalizedEvent = scroller.chart.pointer.normalize(e), range = scroller.to - scroller.from, top = scroller.y + scroller.scrollbarTop, left = scroller.x + scroller.scrollbarLeft;
if ((scroller.options.vertical && normalizedEvent.chartY > top) ||
(!scroller.options.vertical && normalizedEvent.chartX > left)) {
// On the top or on the left side of the track:
scroller.updatePosition(scroller.from + range, scroller.to + range);
}
else {
// On the bottom or the right side of the track:
scroller.updatePosition(scroller.from - range, scroller.to - range);
}
fireEvent(scroller, 'changed', {
from: scroller.from,
to: scroller.to,
trigger: 'scrollbar',
DOMEvent: e
});
};
/**
* Update the scrollbar with new options
*
* @private
* @function Highcharts.Scrollbar#update
* @param {Highcharts.ScrollbarOptions} options
* @return {void}
*/
Scrollbar.prototype.update = function (options) {
this.destroy();
this.init(this.chart.renderer, merge(true, this.options, options), this.chart);
};
/**
* Update position option in the Scrollbar, with normalized 0-1 scale
*
* @private
* @function Highcharts.Scrollbar#updatePosition
* @param {number} from
* @param {number} to
* @return {void}
*/
Scrollbar.prototype.updatePosition = function (from, to) {
if (to > 1) {
from = correctFloat(1 - correctFloat(to - from));
to = 1;
}
if (from < 0) {
to = correctFloat(to - from);
from = 0;
}
this.from = from;
this.to = to;
};
/* *
*
* Static Properties
*
* */
/**
*
* The scrollbar is a means of panning over the X axis of a stock chart.
* Scrollbars can also be applied to other types of axes.
*
* Another approach to scrollable charts is the [chart.scrollablePlotArea](
* https://api.highcharts.com/highcharts/chart.scrollablePlotArea) option that
* is especially suitable for simpler cartesian charts on mobile.
*
* In styled mode, all the presentational options for the
* scrollbar are replaced by the classes `.highcharts-scrollbar-thumb`,
* `.highcharts-scrollbar-arrow`, `.highcharts-scrollbar-button`,
* `.highcharts-scrollbar-rifles` and `.highcharts-scrollbar-track`.
*
* @sample stock/yaxis/inverted-bar-scrollbar/
* A scrollbar on a simple bar chart
*
* @product highstock gantt
* @optionparent scrollbar
*
* @private
*/
Scrollbar.defaultOptions = {
/**
* The height of the scrollbar. The height also applies to the width
* of the scroll arrows so that they are always squares. Defaults to
* 20 for touch devices and 14 for mouse devices.
*
* @sample stock/scrollbar/height/
* A 30px scrollbar
*
* @type {number}
* @default 20/14
*/
height: isTouchDevice ? 20 : 14,
/**
* The border rounding radius of the bar.
*
* @sample stock/scrollbar/style/
* Scrollbar styling
*/
barBorderRadius: 0,
/**
* The corner radius of the scrollbar buttons.
*
* @sample stock/scrollbar/style/
* Scrollbar styling
*/
buttonBorderRadius: 0,
/**
* Enable or disable the scrollbar.
*
* @sample stock/scrollbar/enabled/
* Disable the scrollbar, only use navigator
*
* @type {boolean}
* @default true
* @apioption scrollbar.enabled
*/
/**
* Whether to redraw the main chart as the scrollbar or the navigator
* zoomed window is moved. Defaults to `true` for modern browsers and
* `false` for legacy IE browsers as well as mobile devices.
*
* @sample stock/scrollbar/liveredraw
* Setting live redraw to false
*
* @type {boolean}
* @since 1.3
*/
liveRedraw: void 0,
/**
* The margin between the scrollbar and its axis when the scrollbar is
* applied directly to an axis.
*/
margin: 10,
/**
* The minimum width of the scrollbar.
*
* @since 1.2.5
*/
minWidth: 6,
/**
* Whether to show or hide the scrollbar when the scrolled content is
* zoomed out to it full extent.
*
* @type {boolean}
* @default true
* @apioption scrollbar.showFull
*/
step: 0.2,
/**
* The z index of the scrollbar group.
*/
zIndex: 3,
/**
* The background color of the scrollbar itself.
*
* @sample stock/scrollbar/style/
* Scrollbar styling
*
* @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
*/
barBackgroundColor: '#cccccc',
/**
* The width of the bar's border.
*
* @sample stock/scrollbar/style/
* Scrollbar styling
*/
barBorderWidth: 1,
/**
* The color of the scrollbar's border.
*
* @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
*/
barBorderColor: '#cccccc',
/**
* The color of the small arrow inside the scrollbar buttons.
*
* @sample stock/scrollbar/style/
* Scrollbar styling
*
* @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
*/
buttonArrowColor: '#333333',
/**
* The color of scrollbar buttons.
*
* @sample stock/scrollbar/style/
* Scrollbar styling
*
* @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
*/
buttonBackgroundColor: '#e6e6e6',
/**
* The color of the border of the scrollbar buttons.
*
* @sample stock/scrollbar/style/
* Scrollbar styling
*
* @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
*/
buttonBorderColor: '#cccccc',
/**
* The border width of the scrollbar buttons.
*
* @sample stock/scrollbar/style/
* Scrollbar styling
*/
buttonBorderWidth: 1,
/**
* The color of the small rifles in the middle of the scrollbar.
*
* @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
*/
rifleColor: '#333333',
/**
* The color of the track background.
*
* @sample stock/scrollbar/style/
* Scrollbar styling
*
* @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
*/
trackBackgroundColor: '#f2f2f2',
/**
* The color of the border of the scrollbar track.
*
* @sample stock/scrollbar/style/
* Scrollbar styling
*
* @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
*/
trackBorderColor: '#f2f2f2',
/**
* The corner radius of the border of the scrollbar track.
*
* @sample stock/scrollbar/style/
* Scrollbar styling
*
* @type {number}
* @default 0
* @apioption scrollbar.trackBorderRadius
*/
/**
* The width of the border of the scrollbar track.
*
* @sample stock/scrollbar/style/
* Scrollbar styling
*/
trackBorderWidth: 1
};
return Scrollbar;
}());
if (!H.Scrollbar) {
defaultOptions.scrollbar = merge(true, Scrollbar.defaultOptions, defaultOptions.scrollbar);
H.Scrollbar = Scrollbar;
ScrollbarAxis.compose(Axis, Scrollbar);
}
export default H.Scrollbar;
File diff suppressed because it is too large Load Diff
+802
View File
@@ -0,0 +1,802 @@
/* *
*
* (c) 2010-2020 Torstein Honsi
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import H from '../Globals.js';
import U from '../Utilities.js';
var animObject = U.animObject, defined = U.defined, erase = U.erase, extend = U.extend, fireEvent = U.fireEvent, format = U.format, getNestedProperty = U.getNestedProperty, isArray = U.isArray, isNumber = U.isNumber, isObject = U.isObject, syncTimeout = U.syncTimeout, pick = U.pick, removeEvent = U.removeEvent, uniqueKey = U.uniqueKey;
/**
* Function callback when a series point is clicked. Return false to cancel the
* action.
*
* @callback Highcharts.PointClickCallbackFunction
*
* @param {Highcharts.Point} this
* The point where the event occured.
*
* @param {Highcharts.PointClickEventObject} event
* Event arguments.
*/
/**
* Common information for a click event on a series point.
*
* @interface Highcharts.PointClickEventObject
* @extends Highcharts.PointerEventObject
*/ /**
* Clicked point.
* @name Highcharts.PointClickEventObject#point
* @type {Highcharts.Point}
*/
/**
* Configuration hash for the data label and tooltip formatters.
*
* @interface Highcharts.PointLabelObject
*/ /**
* The point's current color.
* @name Highcharts.PointLabelObject#color
* @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject|undefined}
*/ /**
* The point's current color index, used in styled mode instead of `color`. The
* color index is inserted in class names used for styling.
* @name Highcharts.PointLabelObject#colorIndex
* @type {number}
*/ /**
* The name of the related point.
* @name Highcharts.PointLabelObject#key
* @type {string|undefined}
*/ /**
* The percentage for related points in a stacked series or pies.
* @name Highcharts.PointLabelObject#percentage
* @type {number}
*/ /**
* The related point. The point name, if defined, is available through
* `this.point.name`.
* @name Highcharts.PointLabelObject#point
* @type {Highcharts.Point}
*/ /**
* The related series. The series name is available through `this.series.name`.
* @name Highcharts.PointLabelObject#series
* @type {Highcharts.Series}
*/ /**
* The total of values in either a stack for stacked series, or a pie in a pie
* series.
* @name Highcharts.PointLabelObject#total
* @type {number|undefined}
*/ /**
* For categorized axes this property holds the category name for the point. For
* other axes it holds the X value.
* @name Highcharts.PointLabelObject#x
* @type {number|string|undefined}
*/ /**
* The y value of the point.
* @name Highcharts.PointLabelObject#y
* @type {number|undefined}
*/
/**
* Gets fired when the mouse leaves the area close to the point.
*
* @callback Highcharts.PointMouseOutCallbackFunction
*
* @param {Highcharts.Point} this
* Point where the event occured.
*
* @param {global.PointerEvent} event
* Event that occured.
*/
/**
* Gets fired when the mouse enters the area close to the point.
*
* @callback Highcharts.PointMouseOverCallbackFunction
*
* @param {Highcharts.Point} this
* Point where the event occured.
*
* @param {global.Event} event
* Event that occured.
*/
/**
* The generic point options for all series.
*
* In TypeScript you have to extend `PointOptionsObject` with an additional
* declaration to allow custom data options:
*
* ```
* declare interface PointOptionsObject {
* customProperty: string;
* }
* ```
*
* @interface Highcharts.PointOptionsObject
*/
/**
* Possible option types for a data point.
*
* @typedef {number|string|Array<(number|string)>|Highcharts.PointOptionsObject|null} Highcharts.PointOptionsType
*/
/**
* Gets fired when the point is removed using the `.remove()` method.
*
* @callback Highcharts.PointRemoveCallbackFunction
*
* @param {Highcharts.Point} this
* Point where the event occured.
*
* @param {global.Event} event
* Event that occured.
*/
/**
* Possible key values for the point state options.
*
* @typedef {"hover"|"inactive"|"normal"|"select"} Highcharts.PointStateValue
*/
/**
* Gets fired when the point is updated programmatically through the `.update()`
* method.
*
* @callback Highcharts.PointUpdateCallbackFunction
*
* @param {Highcharts.Point} this
* Point where the event occured.
*
* @param {Highcharts.PointUpdateEventObject} event
* Event that occured.
*/
/**
* Information about the update event.
*
* @interface Highcharts.PointUpdateEventObject
* @extends global.Event
*/ /**
* Options data of the update event.
* @name Highcharts.PointUpdateEventObject#options
* @type {Highcharts.PointOptionsType}
*/
''; // detach doclet above
/* eslint-disable no-invalid-this, valid-jsdoc */
/**
* The Point object. The point objects are generated from the `series.data`
* configuration objects or raw numbers. They can be accessed from the
* `Series.points` array. Other ways to instantiate points are through {@link
* Highcharts.Series#addPoint} or {@link Highcharts.Series#setData}.
*
* @class
* @name Highcharts.Point
*/
var Point = /** @class */ (function () {
function Point() {
/* *
*
* Properties
*
* */
/**
* For categorized axes this property holds the category name for the
* point. For other axes it holds the X value.
*
* @name Highcharts.Point#category
* @type {string}
*/
this.category = void 0;
/**
* The point's current color index, used in styled mode instead of
* `color`. The color index is inserted in class names used for styling.
*
* @name Highcharts.Point#colorIndex
* @type {number}
*/
this.colorIndex = void 0;
this.formatPrefix = 'point';
this.id = void 0;
this.isNull = false;
/**
* The name of the point. The name can be given as the first position of the
* point configuration array, or as a `name` property in the configuration:
*
* @example
* // Array config
* data: [
* ['John', 1],
* ['Jane', 2]
* ]
*
* // Object config
* data: [{
* name: 'John',
* y: 1
* }, {
* name: 'Jane',
* y: 2
* }]
*
* @name Highcharts.Point#name
* @type {string}
*/
this.name = void 0;
/**
* The point's options as applied in the initial configuration, or
* extended through `Point.update`.
*
* In TypeScript you have to extend `PointOptionsObject` via an
* additional interface to allow custom data options:
*
* ```
* declare interface PointOptionsObject {
* customProperty: string;
* }
* ```
*
* @name Highcharts.Point#options
* @type {Highcharts.PointOptionsObject}
*/
this.options = void 0;
/**
* The percentage for points in a stacked series or pies.
*
* @name Highcharts.Point#percentage
* @type {number|undefined}
*/
this.percentage = void 0;
this.selected = false;
/**
* The series object associated with the point.
*
* @name Highcharts.Point#series
* @type {Highcharts.Series}
*/
this.series = void 0;
/**
* The total of values in either a stack for stacked series, or a pie in a
* pie series.
*
* @name Highcharts.Point#total
* @type {number|undefined}
*/
this.total = void 0;
/**
* For certain series types, like pie charts, where individual points can
* be shown or hidden.
*
* @name Highcharts.Point#visible
* @type {boolean}
* @default true
*/
this.visible = true;
this.x = void 0;
}
/* *
*
* Functions
*
* */
/**
* Animate SVG elements associated with the point.
*
* @private
* @function Highcharts.Point#animateBeforeDestroy
*/
Point.prototype.animateBeforeDestroy = function () {
var point = this, animateParams = { x: point.startXPos, opacity: 0 }, isDataLabel, graphicalProps = point.getGraphicalProps();
graphicalProps.singular.forEach(function (prop) {
isDataLabel = prop === 'dataLabel';
point[prop] = point[prop].animate(isDataLabel ? {
x: point[prop].startXPos,
y: point[prop].startYPos,
opacity: 0
} : animateParams);
});
graphicalProps.plural.forEach(function (plural) {
point[plural].forEach(function (item) {
if (item.element) {
item.animate(extend({ x: point.startXPos }, (item.startYPos ? {
x: item.startXPos,
y: item.startYPos
} : {})));
}
});
});
};
/**
* Apply the options containing the x and y data and possible some extra
* properties. Called on point init or from point.update.
*
* @private
* @function Highcharts.Point#applyOptions
*
* @param {Highcharts.PointOptionsType} options
* The point options as defined in series.data.
*
* @param {number} [x]
* Optionally, the x value.
*
* @return {Highcharts.Point}
* The Point instance.
*/
Point.prototype.applyOptions = function (options, x) {
var point = this, series = point.series, pointValKey = series.options.pointValKey || series.pointValKey;
options = Point.prototype.optionsToObject.call(this, options);
// copy options directly to point
extend(point, options);
point.options = point.options ? extend(point.options, options) : options;
// Since options are copied into the Point instance, some accidental
// options must be shielded (#5681)
if (options.group) {
delete point.group;
}
if (options.dataLabels) {
delete point.dataLabels;
}
/**
* The y value of the point.
* @name Highcharts.Point#y
* @type {number|undefined}
*/
// For higher dimension series types. For instance, for ranges, point.y
// is mapped to point.low.
if (pointValKey) {
point.y = Point.prototype.getNestedProperty.call(point, pointValKey);
}
point.isNull = pick(point.isValid && !point.isValid(), point.x === null || !isNumber(point.y)); // #3571, check for NaN
point.formatPrefix = point.isNull ? 'null' : 'point'; // #9233, #10874
// The point is initially selected by options (#5777)
if (point.selected) {
point.state = 'select';
}
/**
* The x value of the point.
* @name Highcharts.Point#x
* @type {number}
*/
// If no x is set by now, get auto incremented value. All points must
// have an x value, however the y value can be null to create a gap in
// the series
if ('name' in point &&
typeof x === 'undefined' &&
series.xAxis &&
series.xAxis.hasNames) {
point.x = series.xAxis.nameToX(point);
}
if (typeof point.x === 'undefined' && series) {
if (typeof x === 'undefined') {
point.x = series.autoIncrement(point);
}
else {
point.x = x;
}
}
return point;
};
/**
* Destroy a point to clear memory. Its reference still stays in
* `series.data`.
*
* @private
* @function Highcharts.Point#destroy
*/
Point.prototype.destroy = function () {
var point = this, series = point.series, chart = series.chart, dataSorting = series.options.dataSorting, hoverPoints = chart.hoverPoints, globalAnimation = point.series.chart.renderer.globalAnimation, animation = animObject(globalAnimation), prop;
/**
* Allow to call after animation.
* @private
*/
function destroyPoint() {
// Remove all events and elements
if (point.graphic || point.dataLabel || point.dataLabels) {
removeEvent(point);
point.destroyElements();
}
for (prop in point) { // eslint-disable-line guard-for-in
point[prop] = null;
}
}
if (point.legendItem) { // pies have legend items
chart.legend.destroyItem(point);
}
if (hoverPoints) {
point.setState();
erase(hoverPoints, point);
if (!hoverPoints.length) {
chart.hoverPoints = null;
}
}
if (point === chart.hoverPoint) {
point.onMouseOut();
}
// Remove properties after animation
if (!dataSorting || !dataSorting.enabled) {
destroyPoint();
}
else {
this.animateBeforeDestroy();
syncTimeout(destroyPoint, animation.duration);
}
chart.pointCount--;
};
/**
* Destroy SVG elements associated with the point.
*
* @private
* @function Highcharts.Point#destroyElements
* @param {Highcharts.Dictionary<number>} [kinds]
*/
Point.prototype.destroyElements = function (kinds) {
var point = this, props = point.getGraphicalProps(kinds);
props.singular.forEach(function (prop) {
point[prop] = point[prop].destroy();
});
props.plural.forEach(function (plural) {
point[plural].forEach(function (item) {
if (item.element) {
item.destroy();
}
});
delete point[plural];
});
};
/**
* Fire an event on the Point object.
*
* @private
* @function Highcharts.Point#firePointEvent
*
* @param {string} eventType
* Type of the event.
*
* @param {Highcharts.Dictionary<any>|Event} [eventArgs]
* Additional event arguments.
*
* @param {Highcharts.EventCallbackFunction<Highcharts.Point>|Function} [defaultFunction]
* Default event handler.
*
* @fires Highcharts.Point#event:*
*/
Point.prototype.firePointEvent = function (eventType, eventArgs, defaultFunction) {
var point = this, series = this.series, seriesOptions = series.options;
// load event handlers on demand to save time on mouseover/out
if (seriesOptions.point.events[eventType] ||
(point.options &&
point.options.events &&
point.options.events[eventType])) {
point.importEvents();
}
// add default handler if in selection mode
if (eventType === 'click' && seriesOptions.allowPointSelect) {
defaultFunction = function (event) {
// Control key is for Windows, meta (= Cmd key) for Mac, Shift
// for Opera.
if (point.select) { // #2911
point.select(null, event.ctrlKey || event.metaKey || event.shiftKey);
}
};
}
fireEvent(point, eventType, eventArgs, defaultFunction);
};
/**
* Get the CSS class names for individual points. Used internally where the
* returned value is set on every point.
*
* @function Highcharts.Point#getClassName
*
* @return {string}
* The class names.
*/
Point.prototype.getClassName = function () {
var point = this;
return 'highcharts-point' +
(point.selected ? ' highcharts-point-select' : '') +
(point.negative ? ' highcharts-negative' : '') +
(point.isNull ? ' highcharts-null-point' : '') +
(typeof point.colorIndex !== 'undefined' ?
' highcharts-color-' + point.colorIndex : '') +
(point.options.className ? ' ' + point.options.className : '') +
(point.zone && point.zone.className ? ' ' +
point.zone.className.replace('highcharts-negative', '') : '');
};
/**
* Get props of all existing graphical point elements.
*
* @private
* @function Highcharts.Point#getGraphicalProps
* @param {Highcharts.Dictionary<number>} [kinds]
* @return {Highcharts.PointGraphicalProps}
*/
Point.prototype.getGraphicalProps = function (kinds) {
var point = this, props = [], prop, i, graphicalProps = { singular: [], plural: [] };
kinds = kinds || { graphic: 1, dataLabel: 1 };
if (kinds.graphic) {
props.push('graphic', 'shadowGroup');
}
if (kinds.dataLabel) {
props.push('dataLabel', 'dataLabelUpper', 'connector');
}
i = props.length;
while (i--) {
prop = props[i];
if (point[prop]) {
graphicalProps.singular.push(prop);
}
}
['dataLabel', 'connector'].forEach(function (prop) {
var plural = prop + 's';
if (kinds[prop] && point[plural]) {
graphicalProps.plural.push(plural);
}
});
return graphicalProps;
};
/**
* Return the configuration hash needed for the data label and tooltip
* formatters.
*
* @function Highcharts.Point#getLabelConfig
*
* @return {Highcharts.PointLabelObject}
* Abstract object used in formatters and formats.
*/
Point.prototype.getLabelConfig = function () {
return {
x: this.category,
y: this.y,
color: this.color,
colorIndex: this.colorIndex,
key: this.name || this.category,
series: this.series,
point: this,
percentage: this.percentage,
total: this.total || this.stackTotal
};
};
/**
* Returns the value of the point property for a given value.
* @private
*/
Point.prototype.getNestedProperty = function (key) {
if (!key) {
return;
}
if (key.indexOf('custom.') === 0) {
return getNestedProperty(key, this.options);
}
return this[key];
};
/**
* In a series with `zones`, return the zone that the point belongs to.
*
* @function Highcharts.Point#getZone
*
* @return {Highcharts.SeriesZonesOptionsObject}
* The zone item.
*/
Point.prototype.getZone = function () {
var series = this.series, zones = series.zones, zoneAxis = series.zoneAxis || 'y', i = 0, zone;
zone = zones[i];
while (this[zoneAxis] >= zone.value) {
zone = zones[++i];
}
// For resetting or reusing the point (#8100)
if (!this.nonZonedColor) {
this.nonZonedColor = this.color;
}
if (zone && zone.color && !this.options.color) {
this.color = zone.color;
}
else {
this.color = this.nonZonedColor;
}
return zone;
};
/**
* Utility to check if point has new shape type. Used in column series and
* all others that are based on column series.
*
* @return boolean|undefined
*/
Point.prototype.hasNewShapeType = function () {
var point = this;
var oldShapeType = point.graphic &&
(point.graphic.symbolName || point.graphic.element.nodeName);
return oldShapeType !== this.shapeType;
};
/**
* Initialize the point. Called internally based on the `series.data`
* option.
*
* @function Highcharts.Point#init
*
* @param {Highcharts.Series} series
* The series object containing this point.
*
* @param {Highcharts.PointOptionsType} options
* The data in either number, array or object format.
*
* @param {number} [x]
* Optionally, the X value of the point.
*
* @return {Highcharts.Point}
* The Point instance.
*
* @fires Highcharts.Point#event:afterInit
*/
Point.prototype.init = function (series, options, x) {
this.series = series;
this.applyOptions(options, x);
// Add a unique ID to the point if none is assigned
this.id = defined(this.id) ? this.id : uniqueKey();
this.resolveColor();
series.chart.pointCount++;
fireEvent(this, 'afterInit');
return this;
};
/**
* Transform number or array configs into objects. Also called for object
* configs. Used internally to unify the different configuration formats for
* points. For example, a simple number `10` in a line series will be
* transformed to `{ y: 10 }`, and an array config like `[1, 10]` in a
* scatter series will be transformed to `{ x: 1, y: 10 }`.
*
* @function Highcharts.Point#optionsToObject
*
* @param {Highcharts.PointOptionsType} options
* The input option.
*
* @return {Highcharts.Dictionary<*>}
* Transformed options.
*/
Point.prototype.optionsToObject = function (options) {
var ret = {}, series = this.series, keys = series.options.keys, pointArrayMap = keys || series.pointArrayMap || ['y'], valueCount = pointArrayMap.length, firstItemType, i = 0, j = 0;
if (isNumber(options) || options === null) {
ret[pointArrayMap[0]] = options;
}
else if (isArray(options)) {
// with leading x value
if (!keys && options.length > valueCount) {
firstItemType = typeof options[0];
if (firstItemType === 'string') {
ret.name = options[0];
}
else if (firstItemType === 'number') {
ret.x = options[0];
}
i++;
}
while (j < valueCount) {
// Skip undefined positions for keys
if (!keys || typeof options[i] !== 'undefined') {
if (pointArrayMap[j].indexOf('.') > 0) {
// Handle nested keys, e.g. ['color.pattern.image']
// Avoid function call unless necessary.
Point.prototype.setNestedProperty(ret, options[i], pointArrayMap[j]);
}
else {
ret[pointArrayMap[j]] = options[i];
}
}
i++;
j++;
}
}
else if (typeof options === 'object') {
ret = options;
// This is the fastest way to detect if there are individual point
// dataLabels that need to be considered in drawDataLabels. These
// can only occur in object configs.
if (options.dataLabels) {
series._hasPointLabels = true;
}
// Same approach as above for markers
if (options.marker) {
series._hasPointMarkers = true;
}
}
return ret;
};
/**
* @private
* @function Highcharts.Point#resolveColor
* @return {void}
*/
Point.prototype.resolveColor = function () {
var series = this.series, colors, optionsChart = series.chart.options.chart, colorCount = optionsChart.colorCount, styledMode = series.chart.styledMode, colorIndex;
// remove points nonZonedColor for later recalculation
delete this.nonZonedColor;
/**
* The point's current color.
*
* @name Highcharts.Point#color
* @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject|undefined}
*/
if (!styledMode && !this.options.color) {
this.color = series.color; // #3445
}
if (series.options.colorByPoint) {
if (!styledMode) {
colors = series.options.colors || series.chart.options.colors;
this.color = this.color || colors[series.colorCounter];
colorCount = colors.length;
}
colorIndex = series.colorCounter;
series.colorCounter++;
// loop back to zero
if (series.colorCounter === colorCount) {
series.colorCounter = 0;
}
}
else {
colorIndex = series.colorIndex;
}
this.colorIndex = pick(this.colorIndex, colorIndex);
};
/**
* Set a value in an object, on the property defined by key. The key
* supports nested properties using dot notation. The function modifies the
* input object and does not make a copy.
*
* @function Highcharts.Point#setNestedProperty<T>
*
* @param {T} object
* The object to set the value on.
*
* @param {*} value
* The value to set.
*
* @param {string} key
* Key to the property to set.
*
* @return {T}
* The modified object.
*/
Point.prototype.setNestedProperty = function (object, value, key) {
var nestedKeys = key.split('.');
nestedKeys.reduce(function (result, key, i, arr) {
var isLastKey = arr.length - 1 === i;
result[key] = (isLastKey ?
value :
isObject(result[key], true) ?
result[key] :
{});
return result[key];
}, object);
return object;
};
/**
* Extendable method for formatting each point's tooltip line.
*
* @function Highcharts.Point#tooltipFormatter
*
* @param {string} pointFormat
* The point format.
*
* @return {string}
* A string to be concatenated in to the common tooltip text.
*/
Point.prototype.tooltipFormatter = function (pointFormat) {
// Insert options for valueDecimals, valuePrefix, and valueSuffix
var series = this.series, seriesTooltipOptions = series.tooltipOptions, valueDecimals = pick(seriesTooltipOptions.valueDecimals, ''), valuePrefix = seriesTooltipOptions.valuePrefix || '', valueSuffix = seriesTooltipOptions.valueSuffix || '';
// Replace default point style with class name
if (series.chart.styledMode) {
pointFormat =
series.chart.tooltip.styledModeFormat(pointFormat);
}
// Loop over the point array map and replace unformatted values with
// sprintf formatting markup
(series.pointArrayMap || ['y']).forEach(function (key) {
key = '{point.' + key; // without the closing bracket
if (valuePrefix || valueSuffix) {
pointFormat = pointFormat.replace(RegExp(key + '}', 'g'), valuePrefix + key + '}' + valueSuffix);
}
pointFormat = pointFormat.replace(RegExp(key + '}', 'g'), key + ':,.' + valueDecimals + 'f}');
});
return format(pointFormat, {
point: this,
series: this.series
}, series.chart);
};
return Point;
}());
H.Point = Point;
export default Point;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,61 @@
/* *
*
* (c) 2010-2020 Torstein Honsi
*
* Extension to the Series object in 3D charts.
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import H from '../Globals.js';
import Math3D from '../../Extensions/Math3D.js';
var perspective = Math3D.perspective;
import U from '../Utilities.js';
var addEvent = U.addEvent, pick = U.pick;
import './Series.js';
/* eslint-disable no-invalid-this */
// Wrap the translate method to post-translate points into 3D perspective
addEvent(H.Series, 'afterTranslate', function () {
if (this.chart.is3d()) {
this.translate3dPoints();
}
});
// Translate the plotX, plotY properties and add plotZ.
H.Series.prototype.translate3dPoints = function () {
var series = this, chart = series.chart, zAxis = pick(series.zAxis, chart.options.zAxis[0]), rawPoints = [], rawPoint, projectedPoints, projectedPoint, zValue, i;
for (i = 0; i < series.data.length; i++) {
rawPoint = series.data[i];
if (zAxis && zAxis.translate) {
zValue = zAxis.logarithmic && zAxis.val2lin ?
zAxis.val2lin(rawPoint.z) :
rawPoint.z; // #4562
rawPoint.plotZ = zAxis.translate(zValue);
rawPoint.isInside = rawPoint.isInside ?
(zValue >= zAxis.min &&
zValue <= zAxis.max) :
false;
}
else {
rawPoint.plotZ = 0;
}
rawPoint.axisXpos = rawPoint.plotX;
rawPoint.axisYpos = rawPoint.plotY;
rawPoint.axisZpos = rawPoint.plotZ;
rawPoints.push({
x: rawPoint.plotX,
y: rawPoint.plotY,
z: rawPoint.plotZ
});
}
projectedPoints = perspective(rawPoints, chart, true);
for (i = 0; i < series.data.length; i++) {
rawPoint = series.data[i];
projectedPoint = projectedPoints[i];
rawPoint.plotX = projectedPoint.x;
rawPoint.plotY = projectedPoint.y;
rawPoint.plotZ = projectedPoint.z;
}
};
+656
View File
@@ -0,0 +1,656 @@
/* *
*
* (c) 2010-2020 Torstein Honsi
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import Highcharts from './Globals.js';
/**
* Normalized interval.
*
* @interface Highcharts.TimeNormalizedObject
*/ /**
* The count.
*
* @name Highcharts.TimeNormalizedObject#count
* @type {number}
*/ /**
* The interval in axis values (ms).
*
* @name Highcharts.TimeNormalizedObject#unitRange
* @type {number}
*/
/**
* Function of an additional date format specifier.
*
* @callback Highcharts.TimeFormatCallbackFunction
*
* @param {number} timestamp
* The time to format.
*
* @return {string}
* The formatted portion of the date.
*/
/**
* Additonal time tick information.
*
* @interface Highcharts.TimeTicksInfoObject
* @extends Highcharts.TimeNormalizedObject
*/ /**
* @name Highcharts.TimeTicksInfoObject#higherRanks
* @type {Array<string>}
*/ /**
* @name Highcharts.TimeTicksInfoObject#totalRange
* @type {number}
*/
/**
* Time ticks.
*
* @interface Highcharts.AxisTickPositionsArray
* @extends global.Array<number>
*/ /**
* @name Highcharts.AxisTickPositionsArray#info
* @type {Highcharts.TimeTicksInfoObject|undefined}
*/
/**
* A callback to return the time zone offset for a given datetime. It
* takes the timestamp in terms of milliseconds since January 1 1970,
* and returns the timezone offset in minutes. This provides a hook
* for drawing time based charts in specific time zones using their
* local DST crossover dates, with the help of external libraries.
*
* @callback Highcharts.TimezoneOffsetCallbackFunction
*
* @param {number} timestamp
* Timestamp in terms of milliseconds since January 1 1970.
*
* @return {number}
* Timezone offset in minutes.
*/
/**
* Allows to manually load the `moment.js` library from Highcharts options
* instead of the `window`.
* In case of loading the library from a `script` tag,
* this option is not needed, it will be loaded from there by default.
*
* @type {function}
* @since 8.2.0
* @apioption time.moment
*/
import U from './Utilities.js';
var defined = U.defined, error = U.error, extend = U.extend, isObject = U.isObject, merge = U.merge, objectEach = U.objectEach, pad = U.pad, pick = U.pick, splat = U.splat, timeUnits = U.timeUnits;
var H = Highcharts, win = H.win;
/* eslint-disable no-invalid-this, valid-jsdoc */
/**
* The Time class. Time settings are applied in general for each page using
* `Highcharts.setOptions`, or individually for each Chart item through the
* [time](https://api.highcharts.com/highcharts/time) options set.
*
* The Time object is available from {@link Highcharts.Chart#time},
* which refers to `Highcharts.time` if no individual time settings are
* applied.
*
* @example
* // Apply time settings globally
* Highcharts.setOptions({
* time: {
* timezone: 'Europe/London'
* }
* });
*
* // Apply time settings by instance
* var chart = Highcharts.chart('container', {
* time: {
* timezone: 'America/New_York'
* },
* series: [{
* data: [1, 4, 3, 5]
* }]
* });
*
* // Use the Time object
* console.log(
* 'Current time in New York',
* chart.time.dateFormat('%Y-%m-%d %H:%M:%S', Date.now())
* );
*
* @since 6.0.5
*
* @class
* @name Highcharts.Time
*
* @param {Highcharts.TimeOptions} options
* Time options as defined in [chart.options.time](/highcharts/time).
*/
var Time = /** @class */ (function () {
/* *
*
* Constructors
*
* */
function Time(options) {
/* *
*
* Properties
*
* */
this.options = {};
this.useUTC = false;
this.variableTimezone = false;
this.Date = win.Date;
/**
* Get the time zone offset based on the current timezone information as
* set in the global options.
*
* @function Highcharts.Time#getTimezoneOffset
*
* @param {number} timestamp
* The JavaScript timestamp to inspect.
*
* @return {number}
* The timezone offset in minutes compared to UTC.
*/
this.getTimezoneOffset = this.timezoneOffsetFunction();
this.update(options);
}
/* *
*
* Functions
*
* */
/**
* Time units used in `Time.get` and `Time.set`
*
* @typedef {"Date"|"Day"|"FullYear"|"Hours"|"Milliseconds"|"Minutes"|"Month"|"Seconds"} Highcharts.TimeUnitValue
*/
/**
* Get the value of a date object in given units, and subject to the Time
* object's current timezone settings. This function corresponds directly to
* JavaScripts `Date.getXXX / Date.getUTCXXX`, so instead of calling
* `date.getHours()` or `date.getUTCHours()` we will call
* `time.get('Hours')`.
*
* @function Highcharts.Time#get
*
* @param {Highcharts.TimeUnitValue} unit
* @param {Date} date
*
* @return {number}
* The given time unit
*/
Time.prototype.get = function (unit, date) {
if (this.variableTimezone || this.timezoneOffset) {
var realMs = date.getTime();
var ms = realMs - this.getTimezoneOffset(date);
date.setTime(ms); // Temporary adjust to timezone
var ret = date['getUTC' + unit]();
date.setTime(realMs); // Reset
return ret;
}
// UTC time with no timezone handling
if (this.useUTC) {
return date['getUTC' + unit]();
}
// Else, local time
return date['get' + unit]();
};
/**
* Set the value of a date object in given units, and subject to the Time
* object's current timezone settings. This function corresponds directly to
* JavaScripts `Date.setXXX / Date.setUTCXXX`, so instead of calling
* `date.setHours(0)` or `date.setUTCHours(0)` we will call
* `time.set('Hours', 0)`.
*
* @function Highcharts.Time#set
*
* @param {Highcharts.TimeUnitValue} unit
* @param {Date} date
* @param {number} value
*
* @return {number}
* The epoch milliseconds of the updated date
*/
Time.prototype.set = function (unit, date, value) {
// UTC time with timezone handling
if (this.variableTimezone || this.timezoneOffset) {
// For lower order time units, just set it directly using UTC
// time
if (unit === 'Milliseconds' ||
unit === 'Seconds' ||
unit === 'Minutes') {
return date['setUTC' + unit](value);
}
// Higher order time units need to take the time zone into
// account
// Adjust by timezone
var offset = this.getTimezoneOffset(date);
var ms = date.getTime() - offset;
date.setTime(ms);
date['setUTC' + unit](value);
var newOffset = this.getTimezoneOffset(date);
ms = date.getTime() + newOffset;
return date.setTime(ms);
}
// UTC time with no timezone handling
if (this.useUTC) {
return date['setUTC' + unit](value);
}
// Else, local time
return date['set' + unit](value);
};
/**
* Update the Time object with current options. It is called internally on
* initializing Highcharts, after running `Highcharts.setOptions` and on
* `Chart.update`.
*
* @private
* @function Highcharts.Time#update
*
* @param {Highcharts.TimeOptions} options
*
* @return {void}
*/
Time.prototype.update = function (options) {
var useUTC = pick(options && options.useUTC, true), time = this;
this.options = options = merge(true, this.options || {}, options);
// Allow using a different Date class
this.Date = options.Date || win.Date || Date;
this.useUTC = useUTC;
this.timezoneOffset = (useUTC && options.timezoneOffset);
this.getTimezoneOffset = this.timezoneOffsetFunction();
/*
* The time object has options allowing for variable time zones, meaning
* the axis ticks or series data needs to consider this.
*/
this.variableTimezone = !!(!useUTC ||
options.getTimezoneOffset ||
options.timezone);
};
/**
* Make a time and returns milliseconds. Interprets the inputs as UTC time,
* local time or a specific timezone time depending on the current time
* settings.
*
* @function Highcharts.Time#makeTime
*
* @param {number} year
* The year
*
* @param {number} month
* The month. Zero-based, so January is 0.
*
* @param {number} [date=1]
* The day of the month
*
* @param {number} [hours=0]
* The hour of the day, 0-23.
*
* @param {number} [minutes=0]
* The minutes
*
* @param {number} [seconds=0]
* The seconds
*
* @return {number}
* The time in milliseconds since January 1st 1970.
*/
Time.prototype.makeTime = function (year, month, date, hours, minutes, seconds) {
var d, offset, newOffset;
if (this.useUTC) {
d = this.Date.UTC.apply(0, arguments);
offset = this.getTimezoneOffset(d);
d += offset;
newOffset = this.getTimezoneOffset(d);
if (offset !== newOffset) {
d += newOffset - offset;
// A special case for transitioning from summer time to winter time.
// When the clock is set back, the same time is repeated twice, i.e.
// 02:30 am is repeated since the clock is set back from 3 am to
// 2 am. We need to make the same time as local Date does.
}
else if (offset - 36e5 === this.getTimezoneOffset(d - 36e5) &&
!H.isSafari) {
d -= 36e5;
}
}
else {
d = new this.Date(year, month, pick(date, 1), pick(hours, 0), pick(minutes, 0), pick(seconds, 0)).getTime();
}
return d;
};
/**
* Sets the getTimezoneOffset function. If the `timezone` option is set, a
* default getTimezoneOffset function with that timezone is returned. If
* a `getTimezoneOffset` option is defined, it is returned. If neither are
* specified, the function using the `timezoneOffset` option or 0 offset is
* returned.
*
* @private
* @function Highcharts.Time#timezoneOffsetFunction
*
* @return {Function}
* A getTimezoneOffset function
*/
Time.prototype.timezoneOffsetFunction = function () {
var time = this, options = this.options, moment = options.moment || win.moment;
if (!this.useUTC) {
return function (timestamp) {
return new Date(timestamp.toString()).getTimezoneOffset() * 60000;
};
}
if (options.timezone) {
if (!moment) {
// getTimezoneOffset-function stays undefined because it depends
// on Moment.js
error(25);
}
else {
return function (timestamp) {
return -moment.tz(timestamp, options.timezone).utcOffset() * 60000;
};
}
}
// If not timezone is set, look for the getTimezoneOffset callback
if (this.useUTC && options.getTimezoneOffset) {
return function (timestamp) {
return options.getTimezoneOffset(timestamp.valueOf()) * 60000;
};
}
// Last, use the `timezoneOffset` option if set
return function () {
return (time.timezoneOffset || 0) * 60000;
};
};
/**
* Formats a JavaScript date timestamp (milliseconds since Jan 1st 1970)
* into a human readable date string. The available format keys are listed
* below. Additional formats can be given in the
* {@link Highcharts.dateFormats} hook.
*
* Supported format keys:
* - `%a`: Short weekday, like 'Mon'
* - `%A`: Long weekday, like 'Monday'
* - `%d`: Two digit day of the month, 01 to 31
* - `%e`: Day of the month, 1 through 31
* - `%w`: Day of the week, 0 through 6
* - `%b`: Short month, like 'Jan'
* - `%B`: Long month, like 'January'
* - `%m`: Two digit month number, 01 through 12
* - `%y`: Two digits year, like 09 for 2009
* - `%Y`: Four digits year, like 2009
* - `%H`: Two digits hours in 24h format, 00 through 23
* - `%k`: Hours in 24h format, 0 through 23
* - `%I`: Two digits hours in 12h format, 00 through 11
* - `%l`: Hours in 12h format, 1 through 12
* - `%M`: Two digits minutes, 00 through 59
* - `%p`: Upper case AM or PM
* - `%P`: Lower case AM or PM
* - `%S`: Two digits seconds, 00 through 59
* - `%L`: Milliseconds (naming from Ruby)
*
* @example
* const time = new Highcharts.Time();
* const s = time.dateFormat('%Y-%m-%d %H:%M:%S', Date.UTC(2020, 0, 1));
* console.log(s); // => 2020-01-01 00:00:00
*
* @function Highcharts.Time#dateFormat
*
* @param {string} format
* The desired format where various time representations are
* prefixed with %.
*
* @param {number} timestamp
* The JavaScript timestamp.
*
* @param {boolean} [capitalize=false]
* Upper case first letter in the return.
*
* @return {string}
* The formatted date.
*/
Time.prototype.dateFormat = function (format, timestamp, capitalize) {
var _a;
if (!defined(timestamp) || isNaN(timestamp)) {
return ((_a = H.defaultOptions.lang) === null || _a === void 0 ? void 0 : _a.invalidDate) || '';
}
format = pick(format, '%Y-%m-%d %H:%M:%S');
var time = this, date = new this.Date(timestamp),
// get the basic time values
hours = this.get('Hours', date), day = this.get('Day', date), dayOfMonth = this.get('Date', date), month = this.get('Month', date), fullYear = this.get('FullYear', date), lang = H.defaultOptions.lang, langWeekdays = lang === null || lang === void 0 ? void 0 : lang.weekdays, shortWeekdays = lang === null || lang === void 0 ? void 0 : lang.shortWeekdays,
// List all format keys. Custom formats can be added from the
// outside.
replacements = extend({
// Day
// Short weekday, like 'Mon'
a: shortWeekdays ?
shortWeekdays[day] :
langWeekdays[day].substr(0, 3),
// Long weekday, like 'Monday'
A: langWeekdays[day],
// Two digit day of the month, 01 to 31
d: pad(dayOfMonth),
// Day of the month, 1 through 31
e: pad(dayOfMonth, 2, ' '),
// Day of the week, 0 through 6
w: day,
// Week (none implemented)
// 'W': weekNumber(),
// Month
// Short month, like 'Jan'
b: lang.shortMonths[month],
// Long month, like 'January'
B: lang.months[month],
// Two digit month number, 01 through 12
m: pad(month + 1),
// Month number, 1 through 12 (#8150)
o: month + 1,
// Year
// Two digits year, like 09 for 2009
y: fullYear.toString().substr(2, 2),
// Four digits year, like 2009
Y: fullYear,
// Time
// Two digits hours in 24h format, 00 through 23
H: pad(hours),
// Hours in 24h format, 0 through 23
k: hours,
// Two digits hours in 12h format, 00 through 11
I: pad((hours % 12) || 12),
// Hours in 12h format, 1 through 12
l: (hours % 12) || 12,
// Two digits minutes, 00 through 59
M: pad(this.get('Minutes', date)),
// Upper case AM or PM
p: hours < 12 ? 'AM' : 'PM',
// Lower case AM or PM
P: hours < 12 ? 'am' : 'pm',
// Two digits seconds, 00 through 59
S: pad(date.getSeconds()),
// Milliseconds (naming from Ruby)
L: pad(Math.floor(timestamp % 1000), 3)
}, H.dateFormats);
// Do the replaces
objectEach(replacements, function (val, key) {
// Regex would do it in one line, but this is faster
while (format.indexOf('%' + key) !== -1) {
format = format.replace('%' + key, typeof val === 'function' ? val.call(time, timestamp) : val);
}
});
// Optionally capitalize the string and return
return capitalize ?
(format.substr(0, 1).toUpperCase() +
format.substr(1)) :
format;
};
/**
* Resolve legacy formats of dateTimeLabelFormats (strings and arrays) into
* an object.
* @private
* @param {string|Array<T>|Highcharts.Dictionary<T>} f - General format description
* @return {Highcharts.Dictionary<T>} - The object definition
*/
Time.prototype.resolveDTLFormat = function (f) {
if (!isObject(f, true)) { // check for string or array
f = splat(f);
return {
main: f[0],
from: f[1],
to: f[2]
};
}
return f;
};
/**
* Return an array with time positions distributed on round time values
* right and right after min and max. Used in datetime axes as well as for
* grouping data on a datetime axis.
*
* @function Highcharts.Time#getTimeTicks
*
* @param {Highcharts.TimeNormalizedObject} normalizedInterval
* The interval in axis values (ms) and the count
*
* @param {number} [min]
* The minimum in axis values
*
* @param {number} [max]
* The maximum in axis values
*
* @param {number} [startOfWeek=1]
*
* @return {Highcharts.AxisTickPositionsArray}
*/
Time.prototype.getTimeTicks = function (normalizedInterval, min, max, startOfWeek) {
var time = this, Date = time.Date, tickPositions = [], i, higherRanks = {}, minYear, // used in months and years as a basis for Date.UTC()
// When crossing DST, use the max. Resolves #6278.
minDate = new Date(min), interval = normalizedInterval.unitRange, count = normalizedInterval.count || 1, variableDayLength, minDay;
startOfWeek = pick(startOfWeek, 1);
if (defined(min)) { // #1300
time.set('Milliseconds', minDate, interval >= timeUnits.second ?
0 : // #3935
count * Math.floor(time.get('Milliseconds', minDate) / count)); // #3652, #3654
if (interval >= timeUnits.second) { // second
time.set('Seconds', minDate, interval >= timeUnits.minute ?
0 : // #3935
count * Math.floor(time.get('Seconds', minDate) / count));
}
if (interval >= timeUnits.minute) { // minute
time.set('Minutes', minDate, interval >= timeUnits.hour ?
0 :
count * Math.floor(time.get('Minutes', minDate) / count));
}
if (interval >= timeUnits.hour) { // hour
time.set('Hours', minDate, interval >= timeUnits.day ?
0 :
count * Math.floor(time.get('Hours', minDate) / count));
}
if (interval >= timeUnits.day) { // day
time.set('Date', minDate, interval >= timeUnits.month ?
1 :
Math.max(1, count * Math.floor(time.get('Date', minDate) / count)));
}
if (interval >= timeUnits.month) { // month
time.set('Month', minDate, interval >= timeUnits.year ? 0 :
count * Math.floor(time.get('Month', minDate) / count));
minYear = time.get('FullYear', minDate);
}
if (interval >= timeUnits.year) { // year
minYear -= minYear % count;
time.set('FullYear', minDate, minYear);
}
// week is a special case that runs outside the hierarchy
if (interval === timeUnits.week) {
// get start of current week, independent of count
minDay = time.get('Day', minDate);
time.set('Date', minDate, (time.get('Date', minDate) -
minDay + startOfWeek +
// We don't want to skip days that are before
// startOfWeek (#7051)
(minDay < startOfWeek ? -7 : 0)));
}
// Get basics for variable time spans
minYear = time.get('FullYear', minDate);
var minMonth = time.get('Month', minDate), minDateDate = time.get('Date', minDate), minHours = time.get('Hours', minDate);
// Redefine min to the floored/rounded minimum time (#7432)
min = minDate.getTime();
// Handle local timezone offset
if (time.variableTimezone) {
// Detect whether we need to take the DST crossover into
// consideration. If we're crossing over DST, the day length may
// be 23h or 25h and we need to compute the exact clock time for
// each tick instead of just adding hours. This comes at a cost,
// so first we find out if it is needed (#4951).
variableDayLength = (
// Long range, assume we're crossing over.
max - min > 4 * timeUnits.month ||
// Short range, check if min and max are in different time
// zones.
time.getTimezoneOffset(min) !==
time.getTimezoneOffset(max));
}
// Iterate and add tick positions at appropriate values
var t = minDate.getTime();
i = 1;
while (t < max) {
tickPositions.push(t);
// if the interval is years, use Date.UTC to increase years
if (interval === timeUnits.year) {
t = time.makeTime(minYear + i * count, 0);
// if the interval is months, use Date.UTC to increase months
}
else if (interval === timeUnits.month) {
t = time.makeTime(minYear, minMonth + i * count);
// if we're using global time, the interval is not fixed as it
// jumps one hour at the DST crossover
}
else if (variableDayLength &&
(interval === timeUnits.day || interval === timeUnits.week)) {
t = time.makeTime(minYear, minMonth, minDateDate +
i * count * (interval === timeUnits.day ? 1 : 7));
}
else if (variableDayLength &&
interval === timeUnits.hour &&
count > 1) {
// make sure higher ranks are preserved across DST (#6797,
// #7621)
t = time.makeTime(minYear, minMonth, minDateDate, minHours + i * count);
// else, the interval is fixed and we use simple addition
}
else {
t += interval * count;
}
i++;
}
// push the last time
tickPositions.push(t);
// Handle higher ranks. Mark new days if the time is on midnight
// (#950, #1649, #1760, #3349). Use a reasonable dropout threshold
// to prevent looping over dense data grouping (#6156).
if (interval <= timeUnits.hour && tickPositions.length < 10000) {
tickPositions.forEach(function (t) {
if (
// Speed optimization, no need to run dateFormat unless
// we're on a full or half hour
t % 1800000 === 0 &&
// Check for local or global midnight
time.dateFormat('%H%M%S%L', t) === '000000000') {
higherRanks[t] = 'day';
}
});
}
}
// record information on the chosen unit - for dynamic label formatter
tickPositions.info = extend(normalizedInterval, {
higherRanks: higherRanks,
totalRange: interval * count
});
return tickPositions;
};
return Time;
}());
H.Time = Time;
export default H.Time;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+150
View File
@@ -0,0 +1,150 @@
/* *
*
* (c) 2010-2017 Christer Vasseng, Torstein Honsi
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import H from '../Core/Globals.js';
import U from '../Core/Utilities.js';
var merge = U.merge, objectEach = U.objectEach;
/**
* @interface Highcharts.AjaxSettingsObject
*/ /**
* The payload to send.
*
* @name Highcharts.AjaxSettingsObject#data
* @type {string|Highcharts.Dictionary<any>}
*/ /**
* The data type expected.
* @name Highcharts.AjaxSettingsObject#dataType
* @type {"json"|"xml"|"text"|"octet"}
*/ /**
* Function to call on error.
* @name Highcharts.AjaxSettingsObject#error
* @type {Function}
*/ /**
* The headers; keyed on header name.
* @name Highcharts.AjaxSettingsObject#headers
* @type {Highcharts.Dictionary<string>}
*/ /**
* Function to call on success.
* @name Highcharts.AjaxSettingsObject#success
* @type {Function}
*/ /**
* The HTTP method to use. For example GET or POST.
* @name Highcharts.AjaxSettingsObject#type
* @type {string}
*/ /**
* The URL to call.
* @name Highcharts.AjaxSettingsObject#url
* @type {string}
*/
/**
* Perform an Ajax call.
*
* @function Highcharts.ajax
*
* @param {Partial<Highcharts.AjaxSettingsObject>} attr
* The Ajax settings to use.
*
* @return {false|undefined}
* Returns false, if error occured.
*/
H.ajax = function (attr) {
var options = merge(true, {
url: false,
type: 'get',
dataType: 'json',
success: false,
error: false,
data: false,
headers: {}
}, attr), headers = {
json: 'application/json',
xml: 'application/xml',
text: 'text/plain',
octet: 'application/octet-stream'
}, r = new XMLHttpRequest();
/**
* @private
* @param {XMLHttpRequest} xhr - Internal request object.
* @param {string|Error} err - Occured error.
* @return {void}
*/
function handleError(xhr, err) {
if (options.error) {
options.error(xhr, err);
}
else {
// @todo Maybe emit a highcharts error event here
}
}
if (!options.url) {
return false;
}
r.open(options.type.toUpperCase(), options.url, true);
if (!options.headers['Content-Type']) {
r.setRequestHeader('Content-Type', headers[options.dataType] || headers.text);
}
objectEach(options.headers, function (val, key) {
r.setRequestHeader(key, val);
});
// @todo lacking timeout handling
r.onreadystatechange = function () {
var res;
if (r.readyState === 4) {
if (r.status === 200) {
res = r.responseText;
if (options.dataType === 'json') {
try {
res = JSON.parse(res);
}
catch (e) {
return handleError(r, e);
}
}
return options.success && options.success(res);
}
handleError(r, r.responseText);
}
};
try {
options.data = JSON.stringify(options.data);
}
catch (e) {
// empty
}
r.send(options.data || true);
};
/**
* Get a JSON resource over XHR, also supporting CORS without preflight.
*
* @function Highcharts.getJSON
* @param {string} url
* The URL to load.
* @param {Function} success
* The success callback. For error handling, use the `Highcharts.ajax`
* function instead.
* @return {void}
*/
H.getJSON = function (url, success) {
H.ajax({
url: url,
success: success,
dataType: 'json',
headers: {
// Override the Content-Type to avoid preflight problems with CORS
// in the Highcharts demos
'Content-Type': 'text/plain'
}
});
};
var exports = {
ajax: H.ajax,
getJSON: H.getJSON
};
export default exports;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,143 @@
/* *
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
/**
* Callback to modify annotation's possitioner controls.
*
* @callback Highcharts.AnnotationControlPointPositionerFunction
* @param {Highcharts.AnnotationControlPoint} this
* @param {Highcharts.AnnotationControllable} target
* @return {Highcharts.PositionObject}
*/
import U from '../../Core/Utilities.js';
var extend = U.extend, merge = U.merge, pick = U.pick;
import eventEmitterMixin from './Mixins/EventEmitterMixin.js';
/* eslint-disable no-invalid-this, valid-jsdoc */
/**
* A control point class which is a connection between controllable
* transform methods and a user actions.
*
* @requires modules/annotations
*
* @class
* @name Highcharts.AnnotationControlPoint
*
* @hideconstructor
*
* @param {Highcharts.Chart} chart
* A chart instance.
*
* @param {Highcharts.AnnotationControllable} target
* A controllable instance which is a target for a control point.
*
* @param {Highcharts.AnnotationControlPointOptionsObject} options
* An options object.
*
* @param {number} [index]
* Point index.
*/
var ControlPoint = /** @class */ (function () {
function ControlPoint(chart, target, options, index) {
/**
*
* Properties
*
*/
this.addEvents = eventEmitterMixin.addEvents;
this.graphic = void 0;
this.mouseMoveToRadians = eventEmitterMixin.mouseMoveToRadians;
this.mouseMoveToScale = eventEmitterMixin.mouseMoveToScale;
this.mouseMoveToTranslation = eventEmitterMixin.mouseMoveToTranslation;
this.onDrag = eventEmitterMixin.onDrag;
this.onMouseDown = eventEmitterMixin.onMouseDown;
this.onMouseUp = eventEmitterMixin.onMouseUp;
this.removeDocEvents = eventEmitterMixin.removeDocEvents;
/**
*
* Functions
*
*/
/**
* List of events for `anntation.options.events` that should not be
* added to `annotation.graphic` but to the `annotation`.
* @private
* @name Highcharts.AnnotationControlPoint#nonDOMEvents
* @type {Array<string>}
*/
this.nonDOMEvents = ['drag'];
this.chart = chart;
this.target = target;
this.options = options;
this.index = pick(options.index, index);
}
/**
* Set the visibility of the control point.
*
* @function Highcharts.AnnotationControlPoint#setVisibility
*
* @param {boolean} visible
* Visibility of the control point.
*
* @return {void}
*/
ControlPoint.prototype.setVisibility = function (visible) {
this.graphic.attr('visibility', visible ? 'visible' : 'hidden');
this.options.visible = visible;
};
/**
* Render the control point.
* @private
*/
ControlPoint.prototype.render = function () {
var chart = this.chart, options = this.options;
this.graphic = chart.renderer
.symbol(options.symbol, 0, 0, options.width, options.height)
.add(chart.controlPointsGroup)
.css(options.style);
this.setVisibility(options.visible);
// npm test -- --tests "highcharts/annotations-advanced/*"
this.addEvents();
};
/**
* Redraw the control point.
* @private
* @param {boolean} [animation]
*/
ControlPoint.prototype.redraw = function (animation) {
this.graphic[animation ? 'animate' : 'attr'](this.options.positioner.call(this, this.target));
};
/**
* Destroy the control point.
* @private
*/
ControlPoint.prototype.destroy = function () {
eventEmitterMixin.destroy.call(this);
if (this.graphic) {
this.graphic = this.graphic.destroy();
}
this.chart = null;
this.target = null;
this.options = null;
};
/**
* Update the control point.
*
* @function Highcharts.AnnotationControlPoint#update
*
* @param {Partial<Highcharts.AnnotationControlPointOptionsObject>} userOptions
* New options for the control point.
*
* @return {void}
*/
ControlPoint.prototype.update = function (userOptions) {
var chart = this.chart, target = this.target, index = this.index, options = merge(true, this.options, userOptions);
this.destroy();
this.constructor(chart, target, options, index);
this.render(chart.controlPointsGroup);
this.redraw();
};
return ControlPoint;
}());
export default ControlPoint;
@@ -0,0 +1,117 @@
/* *
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import ControllableMixin from '../Mixins/ControllableMixin.js';
import ControllablePath from './ControllablePath.js';
import U from '../../../Core/Utilities.js';
var merge = U.merge;
/* eslint-disable no-invalid-this, valid-jsdoc */
/**
* A controllable circle class.
*
* @requires modules/annotations
*
* @private
* @class
* @name Highcharts.AnnotationControllableCircle
*
* @param {Highcharts.Annotation} annotation an annotation instance
* @param {Highcharts.AnnotationsShapeOptions} options a shape's options
* @param {number} index of the circle
*/
var ControllableCircle = /** @class */ (function () {
/* *
*
* Constructors
*
* */
function ControllableCircle(annotation, options, index) {
/* *
*
* Properties
*
* */
this.addControlPoints = ControllableMixin.addControlPoints;
this.anchor = ControllableMixin.anchor;
this.attr = ControllableMixin.attr;
this.attrsFromOptions = ControllableMixin.attrsFromOptions;
this.destroy = ControllableMixin.destroy;
this.getPointsOptions = ControllableMixin.getPointsOptions;
this.init = ControllableMixin.init;
this.linkPoints = ControllableMixin.linkPoints;
this.point = ControllableMixin.point;
this.rotate = ControllableMixin.rotate;
this.scale = ControllableMixin.scale;
this.setControlPointsVisibility = ControllableMixin.setControlPointsVisibility;
this.shouldBeDrawn = ControllableMixin.shouldBeDrawn;
this.transform = ControllableMixin.transform;
this.transformPoint = ControllableMixin.transformPoint;
this.translatePoint = ControllableMixin.translatePoint;
this.translateShape = ControllableMixin.translateShape;
this.update = ControllableMixin.update;
/**
* @type 'circle'
*/
this.type = 'circle';
this.translate = ControllableMixin.translateShape;
this.init(annotation, options, index);
this.collection = 'shapes';
}
/* *
*
* Functions
*
* */
ControllableCircle.prototype.render = function (parent) {
var attrs = this.attrsFromOptions(this.options);
this.graphic = this.annotation.chart.renderer
.circle(0, -9e9, 0)
.attr(attrs)
.add(parent);
ControllableMixin.render.call(this);
};
ControllableCircle.prototype.redraw = function (animation) {
var position = this.anchor(this.points[0]).absolutePosition;
if (position) {
this.graphic[animation ? 'animate' : 'attr']({
x: position.x,
y: position.y,
r: this.options.r
});
}
else {
this.graphic.attr({
x: 0,
y: -9e9
});
}
this.graphic.placed = Boolean(position);
ControllableMixin.redraw.call(this, animation);
};
/**
* Set the radius.
*
* @param {number} r a radius to be set
*/
ControllableCircle.prototype.setRadius = function (r) {
this.options.r = r;
};
/* *
*
* Static Properties
*
* */
/**
* A map object which allows to map options attributes to element
* attributes.
*
* @name Highcharts.AnnotationControllableCircle.attrsMap
* @type {Highcharts.Dictionary<string>}
*/
ControllableCircle.attrsMap = merge(ControllablePath.attrsMap, { r: 'r' });
return ControllableCircle;
}());
export default ControllableCircle;
@@ -0,0 +1,111 @@
/* *
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import ControllableLabel from './ControllableLabel.js';
import ControllableMixin from '../Mixins/ControllableMixin.js';
/* eslint-disable no-invalid-this, valid-jsdoc */
/**
* A controllable image class.
*
* @requires modules/annotations
*
* @private
* @class
* @name Highcharts.AnnotationControllableImage
*
* @param {Highcharts.Annotation} annotation
* An annotation instance.
*
* @param {Highcharts.AnnotationsShapeOptions} options
* A controllable's options.
*
* @param {number} index
* Index of the image.
*/
var ControllableImage = /** @class */ (function () {
/* *
*
* Constructors
*
* */
function ControllableImage(annotation, options, index) {
/* *
*
* Properties
*
* */
this.addControlPoints = ControllableMixin.addControlPoints;
this.anchor = ControllableMixin.anchor;
this.attr = ControllableMixin.attr;
this.attrsFromOptions = ControllableMixin.attrsFromOptions;
this.destroy = ControllableMixin.destroy;
this.getPointsOptions = ControllableMixin.getPointsOptions;
this.init = ControllableMixin.init;
this.linkPoints = ControllableMixin.linkPoints;
this.point = ControllableMixin.point;
this.rotate = ControllableMixin.rotate;
this.scale = ControllableMixin.scale;
this.setControlPointsVisibility = ControllableMixin.setControlPointsVisibility;
this.shouldBeDrawn = ControllableMixin.shouldBeDrawn;
this.transform = ControllableMixin.transform;
this.transformPoint = ControllableMixin.transformPoint;
this.translatePoint = ControllableMixin.translatePoint;
this.translateShape = ControllableMixin.translateShape;
this.update = ControllableMixin.update;
/**
* @type 'image'
*/
this.type = 'image';
this.translate = ControllableMixin.translateShape;
this.init(annotation, options, index);
this.collection = 'shapes';
}
ControllableImage.prototype.render = function (parent) {
var attrs = this.attrsFromOptions(this.options), options = this.options;
this.graphic = this.annotation.chart.renderer
.image(options.src, 0, -9e9, options.width, options.height)
.attr(attrs)
.add(parent);
this.graphic.width = options.width;
this.graphic.height = options.height;
ControllableMixin.render.call(this);
};
ControllableImage.prototype.redraw = function (animation) {
var anchor = this.anchor(this.points[0]), position = ControllableLabel.prototype.position.call(this, anchor);
if (position) {
this.graphic[animation ? 'animate' : 'attr']({
x: position.x,
y: position.y
});
}
else {
this.graphic.attr({
x: 0,
y: -9e9
});
}
this.graphic.placed = Boolean(position);
ControllableMixin.redraw.call(this, animation);
};
/* *
*
* Static Properties
*
* */
/**
* A map object which allows to map options attributes to element attributes
*
* @name Highcharts.AnnotationControllableImage.attrsMap
* @type {Highcharts.Dictionary<string>}
*/
ControllableImage.attrsMap = {
width: 'width',
height: 'height',
zIndex: 'zIndex'
};
return ControllableImage;
}());
export default ControllableImage;
@@ -0,0 +1,378 @@
/* *
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import ControllableMixin from '../Mixins/ControllableMixin.js';
import MockPoint from '../MockPoint.js';
import SVGRenderer from '../../../Core/Renderer/SVG/SVGRenderer.js';
import Tooltip from '../../../Core/Tooltip.js';
import U from '../../../Core/Utilities.js';
var extend = U.extend, format = U.format, isNumber = U.isNumber, pick = U.pick;
import '../../../Core/Renderer/SVG/SVGRenderer.js';
/* eslint-disable no-invalid-this, valid-jsdoc */
/**
* A controllable label class.
*
* @requires modules/annotations
*
* @private
* @class
* @name Highcharts.AnnotationControllableLabel
*
* @param {Highcharts.Annotation} annotation
* An annotation instance.
* @param {Highcharts.AnnotationsLabelOptions} options
* A label's options.
* @param {number} index
* Index of the label.
*/
var ControllableLabel = /** @class */ (function () {
/* *
*
* Constructors
*
* */
function ControllableLabel(annotation, options, index) {
/* *
*
* Properties
*
* */
this.addControlPoints = ControllableMixin.addControlPoints;
this.attr = ControllableMixin.attr;
this.attrsFromOptions = ControllableMixin.attrsFromOptions;
this.destroy = ControllableMixin.destroy;
this.getPointsOptions = ControllableMixin.getPointsOptions;
this.init = ControllableMixin.init;
this.linkPoints = ControllableMixin.linkPoints;
this.point = ControllableMixin.point;
this.rotate = ControllableMixin.rotate;
this.scale = ControllableMixin.scale;
this.setControlPointsVisibility = ControllableMixin.setControlPointsVisibility;
this.shouldBeDrawn = ControllableMixin.shouldBeDrawn;
this.transform = ControllableMixin.transform;
this.transformPoint = ControllableMixin.transformPoint;
this.translateShape = ControllableMixin.translateShape;
this.update = ControllableMixin.update;
this.init(annotation, options, index);
this.collection = 'labels';
}
/* *
*
* Static Functions
*
* */
/**
* Returns new aligned position based alignment options and box to align to.
* It is almost a one-to-one copy from SVGElement.prototype.align
* except it does not use and mutate an element
*
* @param {Highcharts.AnnotationAlignObject} alignOptions
*
* @param {Highcharts.BBoxObject} box
*
* @return {Highcharts.PositionObject}
* Aligned position.
*/
ControllableLabel.alignedPosition = function (alignOptions, box) {
var align = alignOptions.align, vAlign = alignOptions.verticalAlign, x = (box.x || 0) + (alignOptions.x || 0), y = (box.y || 0) + (alignOptions.y || 0), alignFactor, vAlignFactor;
if (align === 'right') {
alignFactor = 1;
}
else if (align === 'center') {
alignFactor = 2;
}
if (alignFactor) {
x += (box.width - (alignOptions.width || 0)) / alignFactor;
}
if (vAlign === 'bottom') {
vAlignFactor = 1;
}
else if (vAlign === 'middle') {
vAlignFactor = 2;
}
if (vAlignFactor) {
y += (box.height - (alignOptions.height || 0)) / vAlignFactor;
}
return {
x: Math.round(x),
y: Math.round(y)
};
};
/**
* Returns new alignment options for a label if the label is outside the
* plot area. It is almost a one-to-one copy from
* Series.prototype.justifyDataLabel except it does not mutate the label and
* it works with absolute instead of relative position.
*/
ControllableLabel.justifiedOptions = function (chart, label, alignOptions, alignAttr) {
var align = alignOptions.align, verticalAlign = alignOptions.verticalAlign, padding = label.box ? 0 : (label.padding || 0), bBox = label.getBBox(), off,
//
options = {
align: align,
verticalAlign: verticalAlign,
x: alignOptions.x,
y: alignOptions.y,
width: label.width,
height: label.height
},
//
x = alignAttr.x - chart.plotLeft, y = alignAttr.y - chart.plotTop;
// Off left
off = x + padding;
if (off < 0) {
if (align === 'right') {
options.align = 'left';
}
else {
options.x = -off;
}
}
// Off right
off = x + bBox.width - padding;
if (off > chart.plotWidth) {
if (align === 'left') {
options.align = 'right';
}
else {
options.x = chart.plotWidth - off;
}
}
// Off top
off = y + padding;
if (off < 0) {
if (verticalAlign === 'bottom') {
options.verticalAlign = 'top';
}
else {
options.y = -off;
}
}
// Off bottom
off = y + bBox.height - padding;
if (off > chart.plotHeight) {
if (verticalAlign === 'top') {
options.verticalAlign = 'bottom';
}
else {
options.y = chart.plotHeight - off;
}
}
return options;
};
/* *
*
* Functions
*
* */
/**
* Translate the point of the label by deltaX and deltaY translations.
* The point is the label's anchor.
*
* @param {number} dx translation for x coordinate
* @param {number} dy translation for y coordinate
*/
ControllableLabel.prototype.translatePoint = function (dx, dy) {
ControllableMixin.translatePoint.call(this, dx, dy, 0);
};
/**
* Translate x and y position relative to the label's anchor.
*
* @param {number} dx translation for x coordinate
* @param {number} dy translation for y coordinate
*/
ControllableLabel.prototype.translate = function (dx, dy) {
var chart = this.annotation.chart,
// Annotation.options
labelOptions = this.annotation.userOptions,
// Chart.options.annotations
annotationIndex = chart.annotations.indexOf(this.annotation), chartAnnotations = chart.options.annotations, chartOptions = chartAnnotations[annotationIndex], temp;
if (chart.inverted) {
temp = dx;
dx = dy;
dy = temp;
}
// Local options:
this.options.x += dx;
this.options.y += dy;
// Options stored in chart:
chartOptions[this.collection][this.index].x = this.options.x;
chartOptions[this.collection][this.index].y = this.options.y;
labelOptions[this.collection][this.index].x = this.options.x;
labelOptions[this.collection][this.index].y = this.options.y;
};
ControllableLabel.prototype.render = function (parent) {
var options = this.options, attrs = this.attrsFromOptions(options), style = options.style;
this.graphic = this.annotation.chart.renderer
.label('', 0, -9999, // #10055
options.shape, null, null, options.useHTML, null, 'annotation-label')
.attr(attrs)
.add(parent);
if (!this.annotation.chart.styledMode) {
if (style.color === 'contrast') {
style.color = this.annotation.chart.renderer.getContrast(ControllableLabel.shapesWithoutBackground.indexOf(options.shape) > -1 ? '#FFFFFF' : options.backgroundColor);
}
this.graphic
.css(options.style)
.shadow(options.shadow);
}
if (options.className) {
this.graphic.addClass(options.className);
}
this.graphic.labelrank = options.labelrank;
ControllableMixin.render.call(this);
};
ControllableLabel.prototype.redraw = function (animation) {
var options = this.options, text = this.text || options.format || options.text, label = this.graphic, point = this.points[0], anchor, attrs;
label.attr({
text: text ?
format(text, point.getLabelConfig(), this.annotation.chart) :
options.formatter.call(point, this)
});
anchor = this.anchor(point);
attrs = this.position(anchor);
if (attrs) {
label.alignAttr = attrs;
attrs.anchorX = anchor.absolutePosition.x;
attrs.anchorY = anchor.absolutePosition.y;
label[animation ? 'animate' : 'attr'](attrs);
}
else {
label.attr({
x: 0,
y: -9999 // #10055
});
}
label.placed = !!attrs;
ControllableMixin.redraw.call(this, animation);
};
/**
* All basic shapes don't support alignTo() method except label.
* For a controllable label, we need to subtract translation from
* options.
*/
ControllableLabel.prototype.anchor = function (_point) {
var anchor = ControllableMixin.anchor.apply(this, arguments), x = this.options.x || 0, y = this.options.y || 0;
anchor.absolutePosition.x -= x;
anchor.absolutePosition.y -= y;
anchor.relativePosition.x -= x;
anchor.relativePosition.y -= y;
return anchor;
};
/**
* Returns the label position relative to its anchor.
*
* @param {Highcharts.AnnotationAnchorObject} anchor
*
* @return {Highcharts.PositionObject|null}
*/
ControllableLabel.prototype.position = function (anchor) {
var item = this.graphic, chart = this.annotation.chart, point = this.points[0], itemOptions = this.options, anchorAbsolutePosition = anchor.absolutePosition, anchorRelativePosition = anchor.relativePosition, itemPosition, alignTo, itemPosRelativeX, itemPosRelativeY, showItem = point.series.visible &&
MockPoint.prototype.isInsidePlot.call(point);
if (showItem) {
if (itemOptions.distance) {
itemPosition = Tooltip.prototype.getPosition.call({
chart: chart,
distance: pick(itemOptions.distance, 16)
}, item.width, item.height, {
plotX: anchorRelativePosition.x,
plotY: anchorRelativePosition.y,
negative: point.negative,
ttBelow: point.ttBelow,
h: (anchorRelativePosition.height || anchorRelativePosition.width)
});
}
else if (itemOptions.positioner) {
itemPosition = itemOptions.positioner.call(this);
}
else {
alignTo = {
x: anchorAbsolutePosition.x,
y: anchorAbsolutePosition.y,
width: 0,
height: 0
};
itemPosition = ControllableLabel.alignedPosition(extend(itemOptions, {
width: item.width,
height: item.height
}), alignTo);
if (this.options.overflow === 'justify') {
itemPosition = ControllableLabel.alignedPosition(ControllableLabel.justifiedOptions(chart, item, itemOptions, itemPosition), alignTo);
}
}
if (itemOptions.crop) {
itemPosRelativeX = itemPosition.x - chart.plotLeft;
itemPosRelativeY = itemPosition.y - chart.plotTop;
showItem =
chart.isInsidePlot(itemPosRelativeX, itemPosRelativeY) &&
chart.isInsidePlot(itemPosRelativeX + item.width, itemPosRelativeY + item.height);
}
}
return showItem ? itemPosition : null;
};
/* *
*
* Static Properties
*
* */
/**
* A map object which allows to map options attributes to element attributes
*
* @type {Highcharts.Dictionary<string>}
*/
ControllableLabel.attrsMap = {
backgroundColor: 'fill',
borderColor: 'stroke',
borderWidth: 'stroke-width',
zIndex: 'zIndex',
borderRadius: 'r',
padding: 'padding'
};
/**
* Shapes which do not have background - the object is used for proper
* setting of the contrast color.
*
* @type {Array<string>}
*/
ControllableLabel.shapesWithoutBackground = ['connector'];
return ControllableLabel;
}());
export default ControllableLabel;
/* ********************************************************************** */
/**
* General symbol definition for labels with connector
* @private
*/
SVGRenderer.prototype.symbols.connector = function (x, y, w, h, options) {
var anchorX = options && options.anchorX, anchorY = options && options.anchorY, path, yOffset, lateral = w / 2;
if (isNumber(anchorX) && isNumber(anchorY)) {
path = [['M', anchorX, anchorY]];
// Prefer 45 deg connectors
yOffset = y - anchorY;
if (yOffset < 0) {
yOffset = -h - yOffset;
}
if (yOffset < w) {
lateral = anchorX < x + (w / 2) ? yOffset : w - yOffset;
}
// Anchor below label
if (anchorY > y + h) {
path.push(['L', x + lateral, y + h]);
// Anchor above label
}
else if (anchorY < y) {
path.push(['L', x + lateral, y]);
// Anchor left of label
}
else if (anchorX < x) {
path.push(['L', x, y + h / 2]);
// Anchor right of label
}
else if (anchorX > x + w) {
path.push(['L', x + w, y + h / 2]);
}
}
return path || [];
};
@@ -0,0 +1,180 @@
/* *
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import ControllableMixin from '../Mixins/ControllableMixin.js';
import H from '../../../Core/Globals.js';
import MarkerMixin from '../Mixins/MarkerMixin.js';
import U from '../../../Core/Utilities.js';
var extend = U.extend;
// See TRACKER_FILL in highcharts.src.js
var TRACKER_FILL = 'rgba(192,192,192,' + (H.svg ? 0.0001 : 0.002) + ')';
/* eslint-disable no-invalid-this, valid-jsdoc */
/**
* A controllable path class.
*
* @requires modules/annotations
*
* @private
* @class
* @name Highcharts.AnnotationControllablePath
*
* @param {Highcharts.Annotation}
* Related annotation.
*
* @param {Highcharts.AnnotationsShapeOptions} options
* A path's options object.
*
* @param {number} index
* Index of the path.
*/
var ControllablePath = /** @class */ (function () {
/* *
*
* Constructors
*
* */
function ControllablePath(annotation, options, index) {
/* *
*
* Properties
*
* */
this.addControlPoints = ControllableMixin.addControlPoints;
this.anchor = ControllableMixin.anchor;
this.attr = ControllableMixin.attr;
this.attrsFromOptions = ControllableMixin.attrsFromOptions;
this.destroy = ControllableMixin.destroy;
this.getPointsOptions = ControllableMixin.getPointsOptions;
this.init = ControllableMixin.init;
this.linkPoints = ControllableMixin.linkPoints;
this.point = ControllableMixin.point;
this.rotate = ControllableMixin.rotate;
this.scale = ControllableMixin.scale;
this.setControlPointsVisibility = ControllableMixin.setControlPointsVisibility;
this.setMarkers = MarkerMixin.setItemMarkers;
this.transform = ControllableMixin.transform;
this.transformPoint = ControllableMixin.transformPoint;
this.translate = ControllableMixin.translate;
this.translatePoint = ControllableMixin.translatePoint;
this.translateShape = ControllableMixin.translateShape;
this.update = ControllableMixin.update;
/**
* @type 'path'
*/
this.type = 'path';
this.init(annotation, options, index);
this.collection = 'shapes';
}
/* *
*
* Functions
*
* */
/**
* Map the controllable path to 'd' path attribute.
*
* @return {Highcharts.SVGPathArray|null}
* A path's d attribute.
*/
ControllablePath.prototype.toD = function () {
var dOption = this.options.d;
if (dOption) {
return typeof dOption === 'function' ?
dOption.call(this) :
dOption;
}
var points = this.points, len = points.length, showPath = len, point = points[0], position = showPath && this.anchor(point).absolutePosition, pointIndex = 0, command, d = [];
if (position) {
d.push(['M', position.x, position.y]);
while (++pointIndex < len && showPath) {
point = points[pointIndex];
command = point.command || 'L';
position = this.anchor(point).absolutePosition;
if (command === 'M') {
d.push([command, position.x, position.y]);
}
else if (command === 'L') {
d.push([command, position.x, position.y]);
}
else if (command === 'Z') {
d.push([command]);
}
showPath = point.series.visible;
}
}
return showPath ?
this.chart.renderer.crispLine(d, this.graphic.strokeWidth()) :
null;
};
ControllablePath.prototype.shouldBeDrawn = function () {
return (ControllableMixin.shouldBeDrawn.call(this) || Boolean(this.options.d));
};
ControllablePath.prototype.render = function (parent) {
var options = this.options, attrs = this.attrsFromOptions(options);
this.graphic = this.annotation.chart.renderer
.path([['M', 0, 0]])
.attr(attrs)
.add(parent);
if (options.className) {
this.graphic.addClass(options.className);
}
this.tracker = this.annotation.chart.renderer
.path([['M', 0, 0]])
.addClass('highcharts-tracker-line')
.attr({
zIndex: 2
})
.add(parent);
if (!this.annotation.chart.styledMode) {
this.tracker.attr({
'stroke-linejoin': 'round',
stroke: TRACKER_FILL,
fill: TRACKER_FILL,
'stroke-width': this.graphic.strokeWidth() +
options.snap * 2
});
}
ControllableMixin.render.call(this);
extend(this.graphic, {
markerStartSetter: MarkerMixin.markerStartSetter,
markerEndSetter: MarkerMixin.markerEndSetter
});
this.setMarkers(this);
};
ControllablePath.prototype.redraw = function (animation) {
var d = this.toD(), action = animation ? 'animate' : 'attr';
if (d) {
this.graphic[action]({ d: d });
this.tracker[action]({ d: d });
}
else {
this.graphic.attr({ d: 'M 0 ' + -9e9 });
this.tracker.attr({ d: 'M 0 ' + -9e9 });
}
this.graphic.placed = this.tracker.placed = Boolean(d);
ControllableMixin.redraw.call(this, animation);
};
/* *
*
* Static Properties
*
* */
/**
* A map object which allows to map options attributes to element attributes
*
* @name Highcharts.AnnotationControllablePath.attrsMap
* @type {Highcharts.Dictionary<string>}
*/
ControllablePath.attrsMap = {
dashStyle: 'dashstyle',
strokeWidth: 'stroke-width',
stroke: 'stroke',
fill: 'fill',
zIndex: 'zIndex'
};
return ControllablePath;
}());
export default ControllablePath;
@@ -0,0 +1,121 @@
/* *
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
import ControllableMixin from '../Mixins/ControllableMixin.js';
import ControllablePath from './ControllablePath.js';
import U from '../../../Core/Utilities.js';
var merge = U.merge;
/**
* @typedef {Annotation.ControllablePath.AttrsMap}
* Annotation.ControllableRect.AttrsMap
* @property {string} width=width
* @property {string} height=height
*/
/* eslint-disable no-invalid-this, valid-jsdoc */
/**
* A controllable rect class.
*
* @requires modules/annotations
*
* @private
* @class
* @name Highcharts.AnnotationControllableRect
*
* @param {Highcharts.Annotation} annotation
* An annotation instance.
*
* @param {Highcharts.AnnotationsShapeOptions} options
* A rect's options.
*
* @param {number} index
* Index of the rectangle
*/
var ControllableRect = /** @class */ (function () {
/* *
*
* Constructors
*
* */
function ControllableRect(annotation, options, index) {
/* *
*
* Properties
*
* */
this.addControlPoints = ControllableMixin.addControlPoints;
this.anchor = ControllableMixin.anchor;
this.attr = ControllableMixin.attr;
this.attrsFromOptions = ControllableMixin.attrsFromOptions;
this.destroy = ControllableMixin.destroy;
this.getPointsOptions = ControllableMixin.getPointsOptions;
this.init = ControllableMixin.init;
this.linkPoints = ControllableMixin.linkPoints;
this.point = ControllableMixin.point;
this.rotate = ControllableMixin.rotate;
this.scale = ControllableMixin.scale;
this.setControlPointsVisibility = ControllableMixin.setControlPointsVisibility;
this.shouldBeDrawn = ControllableMixin.shouldBeDrawn;
this.transform = ControllableMixin.transform;
this.transformPoint = ControllableMixin.transformPoint;
this.translatePoint = ControllableMixin.translatePoint;
this.translateShape = ControllableMixin.translateShape;
this.update = ControllableMixin.update;
/**
* @type 'rect'
*/
this.type = 'rect';
this.translate = ControllableMixin.translateShape;
this.init(annotation, options, index);
this.collection = 'shapes';
}
/* *
*
* Functions
*
* */
ControllableRect.prototype.render = function (parent) {
var attrs = this.attrsFromOptions(this.options);
this.graphic = this.annotation.chart.renderer
.rect(0, -9e9, 0, 0)
.attr(attrs)
.add(parent);
ControllableMixin.render.call(this);
};
ControllableRect.prototype.redraw = function (animation) {
var position = this.anchor(this.points[0]).absolutePosition;
if (position) {
this.graphic[animation ? 'animate' : 'attr']({
x: position.x,
y: position.y,
width: this.options.width,
height: this.options.height
});
}
else {
this.attr({
x: 0,
y: -9e9
});
}
this.graphic.placed = Boolean(position);
ControllableMixin.redraw.call(this, animation);
};
/* *
*
* Static Properties
*
* */
/**
* A map object which allows to map options attributes to element attributes
*
* @type {Annotation.ControllableRect.AttrsMap}
*/
ControllableRect.attrsMap = merge(ControllablePath.attrsMap, {
width: 'width',
height: 'height'
});
return ControllableRect;
}());
export default ControllableRect;
@@ -0,0 +1,369 @@
/* *
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import ControlPoint from '../ControlPoint.js';
import MockPoint from '../MockPoint.js';
import Tooltip from '../../../Core/Tooltip.js';
import U from '../../../Core/Utilities.js';
var isObject = U.isObject, isString = U.isString, merge = U.merge, splat = U.splat;
/**
* An object which denots a controllable's anchor positions - relative and
* absolute.
*
* @private
* @interface Highcharts.AnnotationAnchorObject
*/ /**
* Relative to the plot area position
* @name Highcharts.AnnotationAnchorObject#relativePosition
* @type {Highcharts.BBoxObject}
*/ /**
* Absolute position
* @name Highcharts.AnnotationAnchorObject#absolutePosition
* @type {Highcharts.BBoxObject}
*/
/**
* @interface Highcharts.AnnotationControllable
*/ /**
* @name Highcharts.AnnotationControllable#annotation
* @type {Highcharts.Annotation}
*/ /**
* @name Highcharts.AnnotationControllable#chart
* @type {Highcharts.Chart}
*/ /**
* @name Highcharts.AnnotationControllable#collection
* @type {string}
*/ /**
* @private
* @name Highcharts.AnnotationControllable#controlPoints
* @type {Array<Highcharts.AnnotationControlPoint>}
*/ /**
* @name Highcharts.AnnotationControllable#points
* @type {Array<Highcharts.Point>}
*/
/* eslint-disable no-invalid-this, valid-jsdoc */
/**
* It provides methods for handling points, control points
* and points transformations.
*
* @private
* @mixin
* @name Highcharts.AnnotationControllableMixin
*/
var controllableMixin = {
/**
* Init the controllable
*/
init: function (annotation, options, index) {
this.annotation = annotation;
this.chart = annotation.chart;
this.options = options;
this.points = [];
this.controlPoints = [];
this.index = index;
this.linkPoints();
this.addControlPoints();
},
/**
* Redirect attr usage on the controllable graphic element.
*/
attr: function () {
this.graphic.attr.apply(this.graphic, arguments);
},
/**
* Get the controllable's points options.
*
* @return {Array<Highcharts.PointOptionsObject>}
* An array of points' options.
*/
getPointsOptions: function () {
var options = this.options;
return (options.points || (options.point && splat(options.point)));
},
/**
* Utility function for mapping item's options
* to element's attribute
*
* @param {Highcharts.AnnotationsLabelsOptions|Highcharts.AnnotationsShapesOptions} options
*
* @return {Highcharts.SVGAttributes}
* Mapped options.
*/
attrsFromOptions: function (options) {
var map = this.constructor.attrsMap, attrs = {}, key, mappedKey, styledMode = this.chart.styledMode;
for (key in options) { // eslint-disable-line guard-for-in
mappedKey = map[key];
if (mappedKey &&
(!styledMode ||
['fill', 'stroke', 'stroke-width']
.indexOf(mappedKey) === -1)) {
attrs[mappedKey] = options[key];
}
}
return attrs;
},
/**
* Returns object which denotes anchor position - relative and absolute.
*
* @param {Highcharts.AnnotationPointType} point
* A point like object.
*
* @return {Highcharts.AnnotationAnchorObject} a controllable anchor
*/
anchor: function (point) {
var plotBox = point.series.getPlotBox(), box = point.mock ?
point.toAnchor() :
Tooltip.prototype.getAnchor.call({
chart: point.series.chart
}, point), anchor = {
x: box[0] + (this.options.x || 0),
y: box[1] + (this.options.y || 0),
height: box[2] || 0,
width: box[3] || 0
};
return {
relativePosition: anchor,
absolutePosition: merge(anchor, {
x: anchor.x + plotBox.translateX,
y: anchor.y + plotBox.translateY
})
};
},
/**
* Map point's options to a point-like object.
*
* @param {string|Function|Highcharts.AnnotationMockPointOptionsObject|Highcharts.AnnotationPointType} pointOptions
* Point's options.
*
* @param {Highcharts.AnnotationPointType} point
* A point-like instance.
*
* @return {Highcharts.AnnotationPointType|null}
* if the point is found/set returns this point, otherwise null
*/
point: function (pointOptions, point) {
if (pointOptions && pointOptions.series) {
return pointOptions;
}
if (!point || point.series === null) {
if (isObject(pointOptions)) {
point = new MockPoint(this.chart, this, pointOptions);
}
else if (isString(pointOptions)) {
point = this.chart.get(pointOptions) || null;
}
else if (typeof pointOptions === 'function') {
var pointConfig = pointOptions.call(point, this);
point = pointConfig.series ?
pointConfig :
new MockPoint(this.chart, this, pointOptions);
}
}
return point;
},
/**
* Find point-like objects based on points options.
*
* @return {Array<Annotation.PointLike>} an array of point-like objects
*/
linkPoints: function () {
var pointsOptions = this.getPointsOptions(), points = this.points, len = (pointsOptions && pointsOptions.length) || 0, i, point;
for (i = 0; i < len; i++) {
point = this.point(pointsOptions[i], points[i]);
if (!point) {
points.length = 0;
return;
}
if (point.mock) {
point.refresh();
}
points[i] = point;
}
return points;
},
/**
* Add control points to a controllable.
*/
addControlPoints: function () {
var controlPointsOptions = this.options.controlPoints;
(controlPointsOptions || []).forEach(function (controlPointOptions, i) {
var options = merge(this.options.controlPointOptions, controlPointOptions);
if (!options.index) {
options.index = i;
}
controlPointsOptions[i] = options;
this.controlPoints.push(new ControlPoint(this.chart, this, options));
}, this);
},
/**
* Check if a controllable should be rendered/redrawn.
*
* @return {boolean}
* Whether a controllable should be drawn.
*/
shouldBeDrawn: function () {
return Boolean(this.points.length);
},
/**
* Render a controllable.
*/
render: function (_parentGroup) {
this.controlPoints.forEach(function (controlPoint) {
controlPoint.render();
});
},
/**
* Redraw a controllable.
*
* @param {boolean} [animation]
*/
redraw: function (animation) {
this.controlPoints.forEach(function (controlPoint) {
controlPoint.redraw(animation);
});
},
/**
* Transform a controllable with a specific transformation.
*
* @param {string} transformation a transformation name
* @param {number|null} cx origin x transformation
* @param {number|null} cy origin y transformation
* @param {number} p1 param for the transformation
* @param {number} [p2] param for the transformation
*/
transform: function (transformation, cx, cy, p1, p2) {
if (this.chart.inverted) {
var temp = cx;
cx = cy;
cy = temp;
}
this.points.forEach(function (point, i) {
this.transformPoint(transformation, cx, cy, p1, p2, i);
}, this);
},
/**
* Transform a point with a specific transformation
* If a transformed point is a real point it is replaced with
* the mock point.
*
* @param {string} transformation a transformation name
* @param {number|null} cx origin x transformation
* @param {number|null} cy origin y transformation
* @param {number} p1 param for the transformation
* @param {number|undefined} p2 param for the transformation
* @param {number} i index of the point
*/
transformPoint: function (transformation, cx, cy, p1, p2, i) {
var point = this.points[i];
if (!point.mock) {
point = this.points[i] = MockPoint.fromPoint(point);
}
point[transformation](cx, cy, p1, p2);
},
/**
* Translate a controllable.
*
* @param {number} dx translation for x coordinate
* @param {number} dy translation for y coordinate
**/
translate: function (dx, dy) {
this.transform('translate', null, null, dx, dy);
},
/**
* Translate a specific point within a controllable.
*
* @param {number} dx translation for x coordinate
* @param {number} dy translation for y coordinate
* @param {number} i index of the point
**/
translatePoint: function (dx, dy, i) {
this.transformPoint('translate', null, null, dx, dy, i);
},
/**
* Translate shape within controllable item.
* Replaces `controllable.translate` method.
*
* @param {number} dx translation for x coordinate
* @param {number} dy translation for y coordinate
*/
translateShape: function (dx, dy) {
var chart = this.annotation.chart,
// Annotation.options
shapeOptions = this.annotation.userOptions,
// Chart.options.annotations
annotationIndex = chart.annotations.indexOf(this.annotation), chartOptions = chart.options.annotations[annotationIndex];
this.translatePoint(dx, dy, 0);
// Options stored in:
// - chart (for exporting)
// - current config (for redraws)
chartOptions[this.collection][this.index].point = this.options.point;
shapeOptions[this.collection][this.index].point = this.options.point;
},
/**
* Rotate a controllable.
*
* @param {number} cx origin x rotation
* @param {number} cy origin y rotation
* @param {number} radians
**/
rotate: function (cx, cy, radians) {
this.transform('rotate', cx, cy, radians);
},
/**
* Scale a controllable.
*
* @param {number} cx origin x rotation
* @param {number} cy origin y rotation
* @param {number} sx scale factor x
* @param {number} sy scale factor y
*/
scale: function (cx, cy, sx, sy) {
this.transform('scale', cx, cy, sx, sy);
},
/**
* Set control points' visibility.
*
* @param {boolean} visible
*/
setControlPointsVisibility: function (visible) {
this.controlPoints.forEach(function (controlPoint) {
controlPoint.setVisibility(visible);
});
},
/**
* Destroy a controllable.
*/
destroy: function () {
if (this.graphic) {
this.graphic = this.graphic.destroy();
}
if (this.tracker) {
this.tracker = this.tracker.destroy();
}
this.controlPoints.forEach(function (controlPoint) {
controlPoint.destroy();
});
this.chart = null;
this.points = null;
this.controlPoints = null;
this.options = null;
if (this.annotation) {
this.annotation = null;
}
},
/**
* Update a controllable.
*
* @param {Object} newOptions
*/
update: function (newOptions) {
var annotation = this.annotation, options = merge(true, this.options, newOptions), parentGroup = this.graphic.parentGroup;
this.destroy();
this.constructor(annotation, options);
this.render(parentGroup);
this.redraw();
}
};
export default controllableMixin;
@@ -0,0 +1,212 @@
/* *
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
import H from '../../../Core/Globals.js';
import U from '../../../Core/Utilities.js';
var addEvent = U.addEvent, fireEvent = U.fireEvent, objectEach = U.objectEach, pick = U.pick, removeEvent = U.removeEvent;
/* eslint-disable valid-jsdoc */
/**
* It provides methods for:
* - adding and handling DOM events and a drag event,
* - mapping a mouse move event to the distance between two following events.
* The units of the distance are specific to a transformation,
* e.g. for rotation they are radians, for scaling they are scale factors.
*
* @private
* @mixin
* @memberOf Annotation
*/
var eventEmitterMixin = {
/**
* Add emitter events.
*/
addEvents: function () {
var emitter = this, addMouseDownEvent = function (element) {
addEvent(element, H.isTouchDevice ? 'touchstart' : 'mousedown', function (e) {
emitter.onMouseDown(e);
});
};
addMouseDownEvent(this.graphic.element);
(emitter.labels || []).forEach(function (label) {
if (label.options.useHTML && label.graphic.text) {
// Mousedown event bound to HTML element (#13070).
addMouseDownEvent(label.graphic.text.element);
}
});
objectEach(emitter.options.events, function (event, type) {
var eventHandler = function (e) {
if (type !== 'click' || !emitter.cancelClick) {
event.call(emitter, emitter.chart.pointer.normalize(e), emitter.target);
}
};
if ((emitter.nonDOMEvents || []).indexOf(type) === -1) {
emitter.graphic.on(type, eventHandler);
}
else {
addEvent(emitter, type, eventHandler);
}
});
if (emitter.options.draggable) {
addEvent(emitter, 'drag', emitter.onDrag);
if (!emitter.graphic.renderer.styledMode) {
var cssPointer_1 = {
cursor: {
x: 'ew-resize',
y: 'ns-resize',
xy: 'move'
}[emitter.options.draggable]
};
emitter.graphic.css(cssPointer_1);
(emitter.labels || []).forEach(function (label) {
if (label.options.useHTML && label.graphic.text) {
label.graphic.text.css(cssPointer_1);
}
});
}
}
if (!emitter.isUpdating) {
fireEvent(emitter, 'add');
}
},
/**
* Remove emitter document events.
*/
removeDocEvents: function () {
if (this.removeDrag) {
this.removeDrag = this.removeDrag();
}
if (this.removeMouseUp) {
this.removeMouseUp = this.removeMouseUp();
}
},
/**
* Mouse down handler.
*/
onMouseDown: function (e) {
var emitter = this, pointer = emitter.chart.pointer, prevChartX, prevChartY;
if (e.preventDefault) {
e.preventDefault();
}
// On right click, do nothing:
if (e.button === 2) {
return;
}
e = pointer.normalize(e);
prevChartX = e.chartX;
prevChartY = e.chartY;
emitter.cancelClick = false;
emitter.chart.hasDraggedAnnotation = true;
emitter.removeDrag = addEvent(H.doc, H.isTouchDevice ? 'touchmove' : 'mousemove', function (e) {
emitter.hasDragged = true;
e = pointer.normalize(e);
e.prevChartX = prevChartX;
e.prevChartY = prevChartY;
fireEvent(emitter, 'drag', e);
prevChartX = e.chartX;
prevChartY = e.chartY;
});
emitter.removeMouseUp = addEvent(H.doc, H.isTouchDevice ? 'touchend' : 'mouseup', function (e) {
emitter.cancelClick = emitter.hasDragged;
emitter.hasDragged = false;
emitter.chart.hasDraggedAnnotation = false;
// ControlPoints vs Annotation:
fireEvent(pick(emitter.target, emitter), 'afterUpdate');
emitter.onMouseUp(e);
});
},
/**
* Mouse up handler.
*/
onMouseUp: function (_e) {
var chart = this.chart, annotation = this.target || this, annotationsOptions = chart.options.annotations, index = chart.annotations.indexOf(annotation);
this.removeDocEvents();
annotationsOptions[index] = annotation.options;
},
/**
* Drag and drop event. All basic annotations should share this
* capability as well as the extended ones.
*/
onDrag: function (e) {
if (this.chart.isInsidePlot(e.chartX - this.chart.plotLeft, e.chartY - this.chart.plotTop)) {
var translation = this.mouseMoveToTranslation(e);
if (this.options.draggable === 'x') {
translation.y = 0;
}
if (this.options.draggable === 'y') {
translation.x = 0;
}
if (this.points.length) {
this.translate(translation.x, translation.y);
}
else {
this.shapes.forEach(function (shape) {
shape.translate(translation.x, translation.y);
});
this.labels.forEach(function (label) {
label.translate(translation.x, translation.y);
});
}
this.redraw(false);
}
},
/**
* Map mouse move event to the radians.
*/
mouseMoveToRadians: function (e, cx, cy) {
var prevDy = e.prevChartY - cy, prevDx = e.prevChartX - cx, dy = e.chartY - cy, dx = e.chartX - cx, temp;
if (this.chart.inverted) {
temp = prevDx;
prevDx = prevDy;
prevDy = temp;
temp = dx;
dx = dy;
dy = temp;
}
return Math.atan2(dy, dx) - Math.atan2(prevDy, prevDx);
},
/**
* Map mouse move event to the distance between two following events.
*/
mouseMoveToTranslation: function (e) {
var dx = e.chartX - e.prevChartX, dy = e.chartY - e.prevChartY, temp;
if (this.chart.inverted) {
temp = dy;
dy = dx;
dx = temp;
}
return {
x: dx,
y: dy
};
},
/**
* Map mouse move to the scale factors.
*
* @param {Object} e event
* @param {number} cx center x
* @param {number} cy center y
**/
mouseMoveToScale: function (e, cx, cy) {
var prevDx = e.prevChartX - cx, prevDy = e.prevChartY - cy, dx = e.chartX - cx, dy = e.chartY - cy, sx = (dx || 1) / (prevDx || 1), sy = (dy || 1) / (prevDy || 1), temp;
if (this.chart.inverted) {
temp = sy;
sy = sx;
sx = temp;
}
return {
x: sx,
y: sy
};
},
/**
* Destroy the event emitter.
*/
destroy: function () {
this.removeDocEvents();
removeEvent(this);
this.hcEvents = null;
}
};
export default eventEmitterMixin;
@@ -0,0 +1,159 @@
/* *
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import Chart from '../../../Core/Chart/Chart.js';
import SVGRenderer from '../../../Core/Renderer/SVG/SVGRenderer.js';
import U from '../../../Core/Utilities.js';
var addEvent = U.addEvent, defined = U.defined, merge = U.merge, objectEach = U.objectEach, uniqueKey = U.uniqueKey;
/**
* Options for configuring markers for annotations.
*
* An example of the arrow marker:
* <pre>
* {
* arrow: {
* id: 'arrow',
* tagName: 'marker',
* refY: 5,
* refX: 5,
* markerWidth: 10,
* markerHeight: 10,
* children: [{
* tagName: 'path',
* attrs: {
* d: 'M 0 0 L 10 5 L 0 10 Z',
* strokeWidth: 0
* }
* }]
* }
* }
* </pre>
*
* @sample highcharts/annotations/custom-markers/
* Define a custom marker for annotations
*
* @sample highcharts/css/annotations-markers/
* Define markers in a styled mode
*
* @type {Highcharts.Dictionary<Highcharts.SVGDefinitionObject>}
* @since 6.0.0
* @optionparent defs
*/
var defaultMarkers = {
/**
* @type {Highcharts.SVGDefinitionObject}
*/
arrow: {
tagName: 'marker',
render: false,
id: 'arrow',
refY: 5,
refX: 9,
markerWidth: 10,
markerHeight: 10,
/**
* @type {Array<Highcharts.DefsOptions>}
*/
children: [{
tagName: 'path',
d: 'M 0 0 L 10 5 L 0 10 Z',
strokeWidth: 0
}]
},
/**
* @type {Highcharts.SVGDefinitionObject}
*/
'reverse-arrow': {
tagName: 'marker',
render: false,
id: 'reverse-arrow',
refY: 5,
refX: 1,
markerWidth: 10,
markerHeight: 10,
children: [{
tagName: 'path',
// reverse triangle (used as an arrow)
d: 'M 0 5 L 10 0 L 10 10 Z',
strokeWidth: 0
}]
}
};
SVGRenderer.prototype.addMarker = function (id, markerOptions) {
var options = { id: id };
var attrs = {
stroke: markerOptions.color || 'none',
fill: markerOptions.color || 'rgba(0, 0, 0, 0.75)'
};
options.children = markerOptions.children.map(function (child) {
return merge(attrs, child);
});
var marker = this.definition(merge(true, {
markerWidth: 20,
markerHeight: 20,
refX: 0,
refY: 0,
orient: 'auto'
}, markerOptions, options));
marker.id = id;
return marker;
};
/* eslint-disable no-invalid-this, valid-jsdoc */
/**
* @private
*/
function createMarkerSetter(markerType) {
return function (value) {
this.attr(markerType, 'url(#' + value + ')');
};
}
/**
* @private
* @mixin
* @name Highcharts.AnnotaitonMarkerMixin
*/
var markerMixin = {
markerEndSetter: createMarkerSetter('marker-end'),
markerStartSetter: createMarkerSetter('marker-start'),
/**
* Set markers.
* @private
* @param {Highcharts.AnnotationControllablePath} item
*/
setItemMarkers: function (item) {
var itemOptions = item.options, chart = item.chart, defs = chart.options.defs, fill = itemOptions.fill, color = defined(fill) && fill !== 'none' ?
fill :
itemOptions.stroke, setMarker = function (markerType) {
var markerId = itemOptions[markerType], def, predefinedMarker, key, marker;
if (markerId) {
for (key in defs) { // eslint-disable-line guard-for-in
def = defs[key];
if (markerId === def.id &&
def.tagName === 'marker') {
predefinedMarker = def;
break;
}
}
if (predefinedMarker) {
marker = item[markerType] = chart.renderer
.addMarker((itemOptions.id || uniqueKey()) + '-' +
predefinedMarker.id, merge(predefinedMarker, { color: color }));
item.attr(markerType, marker.attr('id'));
}
}
};
['markerStart', 'markerEnd'].forEach(setMarker);
}
};
addEvent(Chart, 'afterGetContainer', function () {
this.options.defs = merge(defaultMarkers, this.options.defs || {});
objectEach(this.options.defs, function (def) {
if (def.tagName === 'marker' && def.render !== false) {
this.renderer.addMarker(def.id, def);
}
}, this);
});
export default markerMixin;
@@ -0,0 +1,426 @@
/* *
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
import H from '../../Core/Globals.js';
/**
* @private
* @interface Highcharts.AnnotationMockLabelOptionsObject
*/ /**
* Point instance of the point.
* @name Highcharts.AnnotationMockLabelOptionsObject#point
* @type {Highcharts.AnnotationMockPoint}
*/ /**
* X value translated to x axis scale.
* @name Highcharts.AnnotationMockLabelOptionsObject#x
* @type {number|null}
*/ /**
* Y value translated to y axis scale.
* @name Highcharts.AnnotationMockLabelOptionsObject#y
* @type {number|null}
*/
/**
* A mock series instance imitating a real series from a real point.
* @private
* @interface Highcharts.AnnotationMockSeries
*/ /**
* Whether a series is visible.
* @name Highcharts.AnnotationMockSeries#visible
* @type {boolean}
*/ /**
* A chart instance.
* @name Highcharts.AnnotationMockSeries#chart
* @type {Highcharts.Chart}
*/ /**
* @name Highcharts.AnnotationMockSeries#getPlotBox
* @type {Function}
*/
/**
* Indicates if this is a mock point for an annotation.
* @name Highcharts.Point#mock
* @type {boolean|undefined}
*/
import U from '../../Core/Utilities.js';
var defined = U.defined, extend = U.extend, fireEvent = U.fireEvent;
import '../../Core/Axis/Axis.js';
import '../../Core/Series/Series.js';
/* eslint-disable no-invalid-this, valid-jsdoc */
/**
* A trimmed point object which imitates {@link Highchart.Point} class. It is
* created when there is a need of pointing to some chart's position using axis
* values or pixel values
*
* @requires modules/annotations
*
* @private
* @class
* @name Highcharts.AnnotationMockPoint
*
* @hideconstructor
*
* @param {Highcharts.Chart} chart
* The chart instance.
*
* @param {Highcharts.AnnotationControllable|null} target
* The related controllable.
*
* @param {Highcharts.AnnotationMockPointOptionsObject|Function} options
* The options object.
*/
var MockPoint = /** @class */ (function () {
function MockPoint(chart, target, options) {
this.isInside = void 0;
this.plotX = void 0;
this.plotY = void 0;
this.x = void 0;
this.y = void 0;
/* *
*
* Functions
*
* */
/**
* A flag indicating that a point is not the real one.
*
* @type {boolean}
* @default true
*/
this.mock = true;
/**
* A mock series instance imitating a real series from a real point.
*
* @name Annotation.AnnotationMockPoint#series
* @type {Highcharts.AnnotationMockSeries}
*/
this.series = {
visible: true,
chart: chart,
getPlotBox: H.Series.prototype.getPlotBox
};
/**
* @name Annotation.AnnotationMockPoint#target
* @type {Highcharts.AnnotationControllable|null}
*/
this.target = target || null;
/**
* Options for the mock point.
*
* @name Annotation.AnnotationMockPoint#options
* @type {Highcharts.AnnotationsMockPointOptionsObject}
*/
this.options = options;
/**
* If an xAxis is set it represents the point's value in terms of the
* xAxis.
*
* @name Annotation.AnnotationMockPoint#x
* @type {number|undefined}
*/
/**
* If an yAxis is set it represents the point's value in terms of the
* yAxis.
*
* @name Annotation.AnnotationMockPoint#y
* @type {number|undefined}
*/
/**
* It represents the point's pixel x coordinate relative to its plot
* box.
*
* @name Annotation.AnnotationMockPoint#plotX
* @type {number|undefined}
*/
/**
* It represents the point's pixel y position relative to its plot box.
*
* @name Annotation.AnnotationMockPoint#plotY
* @type {number|undefined}
*/
/**
* Whether the point is inside the plot box.
*
* @name Annotation.AnnotationMockPoint#isInside
* @type {boolean|undefined}
*/
this.applyOptions(this.getOptions());
}
/**
* Create a mock point from a real Highcharts point.
*
* @private
* @static
*
* @param {Highcharts.Point} point
*
* @return {Highcharts.AnnotationMockPoint}
* A mock point instance.
*/
MockPoint.fromPoint = function (point) {
return new MockPoint(point.series.chart, null, {
x: point.x,
y: point.y,
xAxis: point.series.xAxis,
yAxis: point.series.yAxis
});
};
/**
* Get the pixel position from the point like object.
*
* @private
* @static
*
* @param {Highcharts.AnnotationPointType} point
*
* @param {boolean} [paneCoordinates]
* whether the pixel position should be relative
*
* @return {Highcharts.PositionObject} pixel position
*/
MockPoint.pointToPixels = function (point, paneCoordinates) {
var series = point.series, chart = series.chart, x = point.plotX, y = point.plotY, plotBox;
if (chart.inverted) {
if (point.mock) {
x = point.plotY;
y = point.plotX;
}
else {
x = chart.plotWidth - point.plotY;
y = chart.plotHeight - point.plotX;
}
}
if (series && !paneCoordinates) {
plotBox = series.getPlotBox();
x += plotBox.translateX;
y += plotBox.translateY;
}
return {
x: x,
y: y
};
};
/**
* Get fresh mock point options from the point like object.
*
* @private
* @static
*
* @param {Highcharts.AnnotationPointType} point
*
* @return {Highcharts.AnnotationMockPointOptionsObject}
* A mock point's options.
*/
MockPoint.pointToOptions = function (point) {
return {
x: point.x,
y: point.y,
xAxis: point.series.xAxis,
yAxis: point.series.yAxis
};
};
/**
* Check if the point has dynamic options.
* @private
* @return {boolean}
* A positive flag if the point has dynamic options.
*/
MockPoint.prototype.hasDynamicOptions = function () {
return typeof this.options === 'function';
};
/**
* Get the point's options.
* @private
* @return {Highcharts.AnnotationMockPointOptionsObject}
* The mock point's options.
*/
MockPoint.prototype.getOptions = function () {
return this.hasDynamicOptions() ?
this.options(this.target) :
this.options;
};
/**
* Apply options for the point.
* @private
* @param {Highcharts.AnnotationMockPointOptionsObject} options
*/
MockPoint.prototype.applyOptions = function (options) {
this.command = options.command;
this.setAxis(options, 'x');
this.setAxis(options, 'y');
this.refresh();
};
/**
* Set x or y axis.
* @private
* @param {Highcharts.AnnotationMockPointOptionsObject} options
* @param {string} xOrY
* 'x' or 'y' string literal
*/
MockPoint.prototype.setAxis = function (options, xOrY) {
var axisName = (xOrY + 'Axis'), axisOptions = options[axisName], chart = this.series.chart;
this.series[axisName] =
axisOptions instanceof H.Axis ?
axisOptions :
defined(axisOptions) ?
(chart[axisName][axisOptions] ||
chart.get(axisOptions)) :
null;
};
/**
* Transform the mock point to an anchor (relative position on the chart).
* @private
* @return {Array<number>}
* A quadruple of numbers which denotes x, y, width and height of the box
**/
MockPoint.prototype.toAnchor = function () {
var anchor = [this.plotX, this.plotY, 0, 0];
if (this.series.chart.inverted) {
anchor[0] = this.plotY;
anchor[1] = this.plotX;
}
return anchor;
};
/**
* Returns a label config object - the same as
* Highcharts.Point.prototype.getLabelConfig
* @private
* @return {Highcharts.AnnotationMockLabelOptionsObject} the point's label config
*/
MockPoint.prototype.getLabelConfig = function () {
return {
x: this.x,
y: this.y,
point: this
};
};
/**
* Check if the point is inside its pane.
* @private
* @return {boolean} A flag indicating whether the point is inside the pane.
*/
MockPoint.prototype.isInsidePlot = function () {
var plotX = this.plotX, plotY = this.plotY, xAxis = this.series.xAxis, yAxis = this.series.yAxis, e = {
x: plotX,
y: plotY,
isInsidePlot: true
};
if (xAxis) {
e.isInsidePlot = defined(plotX) && plotX >= 0 && plotX <= xAxis.len;
}
if (yAxis) {
e.isInsidePlot =
e.isInsidePlot &&
defined(plotY) &&
plotY >= 0 && plotY <= yAxis.len;
}
fireEvent(this.series.chart, 'afterIsInsidePlot', e);
return e.isInsidePlot;
};
/**
* Refresh point values and coordinates based on its options.
* @private
*/
MockPoint.prototype.refresh = function () {
var series = this.series, xAxis = series.xAxis, yAxis = series.yAxis, options = this.getOptions();
if (xAxis) {
this.x = options.x;
this.plotX = xAxis.toPixels(options.x, true);
}
else {
this.x = null;
this.plotX = options.x;
}
if (yAxis) {
this.y = options.y;
this.plotY = yAxis.toPixels(options.y, true);
}
else {
this.y = null;
this.plotY = options.y;
}
this.isInside = this.isInsidePlot();
};
/**
* Translate the point.
*
* @private
*
* @param {number|undefined} cx
* Origin x transformation.
*
* @param {number|undefined} cy
* Origin y transformation.
*
* @param {number} dx
* Translation for x coordinate.
*
* @param {number} dy
* Translation for y coordinate.
**/
MockPoint.prototype.translate = function (_cx, _cy, dx, dy) {
if (!this.hasDynamicOptions()) {
this.plotX += dx;
this.plotY += dy;
this.refreshOptions();
}
};
/**
* Scale the point.
*
* @private
*
* @param {number} cx
* Origin x transformation.
*
* @param {number} cy
* Origin y transformation.
*
* @param {number} sx
* Scale factor x.
*
* @param {number} sy
* Scale factor y.
*/
MockPoint.prototype.scale = function (cx, cy, sx, sy) {
if (!this.hasDynamicOptions()) {
var x = this.plotX * sx, y = this.plotY * sy, tx = (1 - sx) * cx, ty = (1 - sy) * cy;
this.plotX = tx + x;
this.plotY = ty + y;
this.refreshOptions();
}
};
/**
* Rotate the point.
* @private
* @param {number} cx origin x rotation
* @param {number} cy origin y rotation
* @param {number} radians
*/
MockPoint.prototype.rotate = function (cx, cy, radians) {
if (!this.hasDynamicOptions()) {
var cos = Math.cos(radians), sin = Math.sin(radians), x = this.plotX, y = this.plotY, tx, ty;
x -= cx;
y -= cy;
tx = x * cos - y * sin;
ty = x * sin + y * cos;
this.plotX = tx + cx;
this.plotY = ty + cy;
this.refreshOptions();
}
};
/**
* Refresh point options based on its plot coordinates.
* @private
*/
MockPoint.prototype.refreshOptions = function () {
var series = this.series, xAxis = series.xAxis, yAxis = series.yAxis;
this.x = this.options.x = xAxis ?
this.options.x = xAxis.toValue(this.plotX, true) :
this.plotX;
this.y = this.options.y = yAxis ?
yAxis.toValue(this.plotY, true) :
this.plotY;
};
return MockPoint;
}());
export default MockPoint;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,733 @@
/* *
*
* Popup generator for Stock tools
*
* (c) 2009-2017 Sebastian Bochan
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import H from '../../Core/Globals.js';
import NavigationBindings from './NavigationBindings.js';
import Pointer from '../../Core/Pointer.js';
import U from '../../Core/Utilities.js';
var addEvent = U.addEvent, createElement = U.createElement, defined = U.defined, getOptions = U.getOptions, isArray = U.isArray, isObject = U.isObject, isString = U.isString, objectEach = U.objectEach, pick = U.pick, wrap = U.wrap;
var indexFilter = /\d/g, PREFIX = 'highcharts-', DIV = 'div', INPUT = 'input', LABEL = 'label', BUTTON = 'button', SELECT = 'select', OPTION = 'option', SPAN = 'span', UL = 'ul', LI = 'li', H3 = 'h3';
/* eslint-disable no-invalid-this, valid-jsdoc */
// onContainerMouseDown blocks internal popup events, due to e.preventDefault.
// Related issue #4606
wrap(Pointer.prototype, 'onContainerMouseDown', function (proceed, e) {
var popupClass = e.target && e.target.className;
// elements is not in popup
if (!(isString(popupClass) &&
popupClass.indexOf(PREFIX + 'popup-field') >= 0)) {
proceed.apply(this, Array.prototype.slice.call(arguments, 1));
}
});
H.Popup = function (parentDiv, iconsURL) {
this.init(parentDiv, iconsURL);
};
H.Popup.prototype = {
/**
* Initialize the popup. Create base div and add close button.
* @private
* @param {Highcharts.HTMLDOMElement} parentDiv
* Container where popup should be placed
* @param {string} iconsURL
* Icon URL
*/
init: function (parentDiv, iconsURL) {
// create popup div
this.container = createElement(DIV, {
className: PREFIX + 'popup'
}, null, parentDiv);
this.lang = this.getLangpack();
this.iconsURL = iconsURL;
// add close button
this.addCloseBtn();
},
/**
* Create HTML element and attach click event (close popup).
* @private
*/
addCloseBtn: function () {
var _self = this, closeBtn;
// create close popup btn
closeBtn = createElement(DIV, {
className: PREFIX + 'popup-close'
}, null, this.container);
closeBtn.style['background-image'] = 'url(' +
this.iconsURL + 'close.svg)';
['click', 'touchstart'].forEach(function (eventName) {
addEvent(closeBtn, eventName, function () {
_self.closePopup();
});
});
},
/**
* Create two columns (divs) in HTML.
* @private
* @param {Highcharts.HTMLDOMElement} container
* Container of columns
* @return {Highcharts.Dictionary<Highcharts.HTMLDOMElement>}
* Reference to two HTML columns (lhsCol, rhsCol)
*/
addColsContainer: function (container) {
var rhsCol, lhsCol;
// left column
lhsCol = createElement(DIV, {
className: PREFIX + 'popup-lhs-col'
}, null, container);
// right column
rhsCol = createElement(DIV, {
className: PREFIX + 'popup-rhs-col'
}, null, container);
// wrapper content
createElement(DIV, {
className: PREFIX + 'popup-rhs-col-wrapper'
}, null, rhsCol);
return {
lhsCol: lhsCol,
rhsCol: rhsCol
};
},
/**
* Create input with label.
* @private
* @param {string} option
* Chain of fields i.e params.styles.fontSize
* @param {string} type
* Indicator type
* @param {Highhcharts.HTMLDOMElement}
* Container where elements should be added
* @param {string} value
* Default value of input i.e period value is 14, extracted from
* defaultOptions (ADD mode) or series options (EDIT mode)
*/
addInput: function (option, type, parentDiv, value) {
var optionParamList = option.split('.'), optionName = optionParamList[optionParamList.length - 1], lang = this.lang, inputName = PREFIX + type + '-' + optionName;
if (!inputName.match(indexFilter)) {
// add label
createElement(LABEL, {
innerHTML: lang[optionName] || optionName,
htmlFor: inputName
}, null, parentDiv);
}
// add input
createElement(INPUT, {
name: inputName,
value: value[0],
type: value[1],
className: PREFIX + 'popup-field'
}, null, parentDiv).setAttribute(PREFIX + 'data-name', option);
},
/**
* Create button.
* @private
* @param {Highcharts.HTMLDOMElement} parentDiv
* Container where elements should be added
* @param {string} label
* Text placed as button label
* @param {string} type
* add | edit | remove
* @param {Function} callback
* On click callback
* @param {Highcharts.HTMLDOMElement} fieldsDiv
* Container where inputs are generated
* @return {Highcharts.HTMLDOMElement}
* HTML button
*/
addButton: function (parentDiv, label, type, callback, fieldsDiv) {
var _self = this, closePopup = this.closePopup, getFields = this.getFields, button;
button = createElement(BUTTON, {
innerHTML: label
}, null, parentDiv);
['click', 'touchstart'].forEach(function (eventName) {
addEvent(button, eventName, function () {
closePopup.call(_self);
return callback(getFields(fieldsDiv, type));
});
});
return button;
},
/**
* Get values from all inputs and create JSON.
* @private
* @param {Highcharts.HTMLDOMElement} - container where inputs are created
* @param {string} - add | edit | remove
* @return {Highcharts.PopupFieldsObject} - fields
*/
getFields: function (parentDiv, type) {
var inputList = parentDiv.querySelectorAll('input'), optionSeries = '#' + PREFIX + 'select-series > option:checked', optionVolume = '#' + PREFIX + 'select-volume > option:checked', linkedTo = parentDiv.querySelectorAll(optionSeries)[0], volumeTo = parentDiv.querySelectorAll(optionVolume)[0], seriesId, param, fieldsOutput;
fieldsOutput = {
actionType: type,
linkedTo: linkedTo && linkedTo.getAttribute('value'),
fields: {}
};
[].forEach.call(inputList, function (input) {
param = input.getAttribute(PREFIX + 'data-name');
seriesId = input.getAttribute(PREFIX + 'data-series-id');
// params
if (seriesId) {
fieldsOutput.seriesId = input.value;
}
else if (param) {
fieldsOutput.fields[param] = input.value;
}
else {
// type like sma / ema
fieldsOutput.type = input.value;
}
});
if (volumeTo) {
fieldsOutput.fields['params.volumeSeriesID'] = volumeTo.getAttribute('value');
}
return fieldsOutput;
},
/**
* Reset content of the current popup and show.
* @private
*/
showPopup: function () {
var popupDiv = this.container, toolbarClass = PREFIX + 'annotation-toolbar', popupCloseBtn = popupDiv
.querySelectorAll('.' + PREFIX + 'popup-close')[0];
// reset content
popupDiv.innerHTML = '';
// reset toolbar styles if exists
if (popupDiv.className.indexOf(toolbarClass) >= 0) {
popupDiv.classList.remove(toolbarClass);
// reset toolbar inline styles
popupDiv.removeAttribute('style');
}
// add close button
popupDiv.appendChild(popupCloseBtn);
popupDiv.style.display = 'block';
},
/**
* Hide popup.
* @private
*/
closePopup: function () {
this.popup.container.style.display = 'none';
},
/**
* Create content and show popup.
* @private
* @param {string} - type of popup i.e indicators
* @param {Highcharts.Chart} - chart
* @param {Highcharts.AnnotationsOptions} - options
* @param {Function} - on click callback
*/
showForm: function (type, chart, options, callback) {
this.popup = chart.navigationBindings.popup;
// show blank popup
this.showPopup();
// indicator form
if (type === 'indicators') {
this.indicators.addForm.call(this, chart, options, callback);
}
// annotation small toolbar
if (type === 'annotation-toolbar') {
this.annotations.addToolbar.call(this, chart, options, callback);
}
// annotation edit form
if (type === 'annotation-edit') {
this.annotations.addForm.call(this, chart, options, callback);
}
// flags form - add / edit
if (type === 'flag') {
this.annotations.addForm.call(this, chart, options, callback, true);
}
},
/**
* Return lang definitions for popup.
* @private
* @return {Highcharts.Dictionary<string>} - elements translations.
*/
getLangpack: function () {
return getOptions().lang.navigation.popup;
},
annotations: {
/**
* Create annotation simple form. It contains two buttons
* (edit / remove) and text label.
* @private
* @param {Highcharts.Chart} - chart
* @param {Highcharts.AnnotationsOptions} - options
* @param {Function} - on click callback
*/
addToolbar: function (chart, options, callback) {
var _self = this, lang = this.lang, popupDiv = this.popup.container, showForm = this.showForm, toolbarClass = PREFIX + 'annotation-toolbar', button;
// set small size
if (popupDiv.className.indexOf(toolbarClass) === -1) {
popupDiv.className += ' ' + toolbarClass;
}
// set position
popupDiv.style.top = chart.plotTop + 10 + 'px';
// create label
createElement(SPAN, {
innerHTML: pick(
// Advanced annotations:
lang[options.langKey] || options.langKey,
// Basic shapes:
options.shapes && options.shapes[0].type)
}, null, popupDiv);
// add buttons
button = this.addButton(popupDiv, lang.removeButton || 'remove', 'remove', callback, popupDiv);
button.className += ' ' + PREFIX + 'annotation-remove-button';
button.style['background-image'] = 'url(' +
this.iconsURL + 'destroy.svg)';
button = this.addButton(popupDiv, lang.editButton || 'edit', 'edit', function () {
showForm.call(_self, 'annotation-edit', chart, options, callback);
}, popupDiv);
button.className += ' ' + PREFIX + 'annotation-edit-button';
button.style['background-image'] = 'url(' +
this.iconsURL + 'edit.svg)';
},
/**
* Create annotation simple form.
* It contains fields with param names.
* @private
* @param {Highcharts.Chart} chart
* Chart
* @param {Object} options
* Options
* @param {Function} callback
* On click callback
* @param {boolean} [isInit]
* If it is a form declared for init annotation
*/
addForm: function (chart, options, callback, isInit) {
var popupDiv = this.popup.container, lang = this.lang, bottomRow, lhsCol;
// create title of annotations
lhsCol = createElement('h2', {
innerHTML: lang[options.langKey] || options.langKey,
className: PREFIX + 'popup-main-title'
}, null, popupDiv);
// left column
lhsCol = createElement(DIV, {
className: PREFIX + 'popup-lhs-col ' + PREFIX + 'popup-lhs-full'
}, null, popupDiv);
bottomRow = createElement(DIV, {
className: PREFIX + 'popup-bottom-row'
}, null, popupDiv);
this.annotations.addFormFields.call(this, lhsCol, chart, '', options, [], true);
this.addButton(bottomRow, isInit ?
(lang.addButton || 'add') :
(lang.saveButton || 'save'), isInit ? 'add' : 'save', callback, popupDiv);
},
/**
* Create annotation's form fields.
* @private
* @param {Highcharts.HTMLDOMElement} parentDiv
* Div where inputs are placed
* @param {Highcharts.Chart} chart
* Chart
* @param {string} parentNode
* Name of parent to create chain of names
* @param {Highcharts.AnnotationsOptions} options
* Options
* @param {Array<unknown>} storage
* Array where all items are stored
* @param {boolean} [isRoot]
* Recursive flag for root
*/
addFormFields: function (parentDiv, chart, parentNode, options, storage, isRoot) {
var _self = this, addFormFields = this.annotations.addFormFields, addInput = this.addInput, lang = this.lang, parentFullName, titleName;
objectEach(options, function (value, option) {
// create name like params.styles.fontSize
parentFullName = parentNode !== '' ?
parentNode + '.' + option : option;
if (isObject(value)) {
if (
// value is object of options
!isArray(value) ||
// array of objects with params. i.e labels in Fibonacci
(isArray(value) && isObject(value[0]))) {
titleName = lang[option] || option;
if (!titleName.match(indexFilter)) {
storage.push([
true,
titleName,
parentDiv
]);
}
addFormFields.call(_self, parentDiv, chart, parentFullName, value, storage, false);
}
else {
storage.push([
_self,
parentFullName,
'annotation',
parentDiv,
value
]);
}
}
});
if (isRoot) {
storage = storage.sort(function (a) {
return a[1].match(/format/g) ? -1 : 1;
});
storage.forEach(function (genInput) {
if (genInput[0] === true) {
createElement(SPAN, {
className: PREFIX + 'annotation-title',
innerHTML: genInput[1]
}, null, genInput[2]);
}
else {
addInput.apply(genInput[0], genInput.splice(1));
}
});
}
}
},
indicators: {
/**
* Create indicator's form. It contains two tabs (ADD and EDIT) with
* content.
* @private
*/
addForm: function (chart, _options, callback) {
var tabsContainers, indicators = this.indicators, lang = this.lang, buttonParentDiv;
// add tabs
this.tabs.init.call(this, chart);
// get all tabs content divs
tabsContainers = this.popup.container
.querySelectorAll('.' + PREFIX + 'tab-item-content');
// ADD tab
this.addColsContainer(tabsContainers[0]);
indicators.addIndicatorList.call(this, chart, tabsContainers[0], 'add');
buttonParentDiv = tabsContainers[0]
.querySelectorAll('.' + PREFIX + 'popup-rhs-col')[0];
this.addButton(buttonParentDiv, lang.addButton || 'add', 'add', callback, buttonParentDiv);
// EDIT tab
this.addColsContainer(tabsContainers[1]);
indicators.addIndicatorList.call(this, chart, tabsContainers[1], 'edit');
buttonParentDiv = tabsContainers[1]
.querySelectorAll('.' + PREFIX + 'popup-rhs-col')[0];
this.addButton(buttonParentDiv, lang.saveButton || 'save', 'edit', callback, buttonParentDiv);
this.addButton(buttonParentDiv, lang.removeButton || 'remove', 'remove', callback, buttonParentDiv);
},
/**
* Create HTML list of all indicators (ADD mode) or added indicators
* (EDIT mode).
* @private
*/
addIndicatorList: function (chart, parentDiv, listType) {
var _self = this, lhsCol = parentDiv.querySelectorAll('.' + PREFIX + 'popup-lhs-col')[0], rhsCol = parentDiv.querySelectorAll('.' + PREFIX + 'popup-rhs-col')[0], isEdit = listType === 'edit', series = (isEdit ?
chart.series : // EDIT mode
chart.options.plotOptions // ADD mode
), addFormFields = this.indicators.addFormFields, rhsColWrapper, indicatorList, item;
// create wrapper for list
indicatorList = createElement(UL, {
className: PREFIX + 'indicator-list'
}, null, lhsCol);
rhsColWrapper = rhsCol
.querySelectorAll('.' + PREFIX + 'popup-rhs-col-wrapper')[0];
objectEach(series, function (serie, value) {
var seriesOptions = serie.options;
if (serie.params ||
seriesOptions && seriesOptions.params) {
var indicatorNameType = _self.indicators.getNameType(serie, value), indicatorType = indicatorNameType.type;
item = createElement(LI, {
className: PREFIX + 'indicator-list',
innerHTML: indicatorNameType.name
}, null, indicatorList);
['click', 'touchstart'].forEach(function (eventName) {
addEvent(item, eventName, function () {
addFormFields.call(_self, chart, isEdit ? serie : series[indicatorType], indicatorNameType.type, rhsColWrapper);
// add hidden input with series.id
if (isEdit && serie.options) {
createElement(INPUT, {
type: 'hidden',
name: PREFIX + 'id-' + indicatorType,
value: serie.options.id
}, null, rhsColWrapper)
.setAttribute(PREFIX + 'data-series-id', serie.options.id);
}
});
});
}
});
// select first item from the list
if (indicatorList.childNodes.length > 0) {
indicatorList.childNodes[0].click();
}
},
/**
* Extract full name and type of requested indicator.
* @private
* @param {Highcharts.Series} series
* Series which name is needed. (EDIT mode - defaultOptions.series, ADD
* mode - indicator series).
* @param {string} - indicator type like: sma, ema, etc.
* @return {Object} - series name and type like: sma, ema, etc.
*/
getNameType: function (series, type) {
var options = series.options, seriesTypes = H.seriesTypes,
// add mode
seriesName = seriesTypes[type] &&
seriesTypes[type].prototype.nameBase || type.toUpperCase(), seriesType = type;
// edit
if (options && options.type) {
seriesType = series.options.type;
seriesName = series.name;
}
return {
name: seriesName,
type: seriesType
};
},
/**
* List all series with unique ID. Its mandatory for indicators to set
* correct linking.
* @private
* @param {string} type
* Indicator type like: sma, ema, etc.
* @param {string} optionName
* Type of select i.e series or volume.
* @param {Highcharts.Chart} chart
* Chart
* @param {Highcharts.HTMLDOMElement} parentDiv
* Element where created HTML list is added
* @param {string} selectedOption
* optional param for default value in dropdown
*/
listAllSeries: function (type, optionName, chart, parentDiv, selectedOption) {
var selectName = PREFIX + optionName + '-type-' + type, lang = this.lang, selectBox, seriesOptions;
createElement(LABEL, {
innerHTML: lang[optionName] || optionName,
htmlFor: selectName
}, null, parentDiv);
// select type
selectBox = createElement(SELECT, {
name: selectName,
className: PREFIX + 'popup-field'
}, null, parentDiv);
selectBox.setAttribute('id', PREFIX + 'select-' + optionName);
// list all series which have id - mandatory for creating indicator
chart.series.forEach(function (serie) {
seriesOptions = serie.options;
if (!seriesOptions.params &&
seriesOptions.id &&
seriesOptions.id !== PREFIX + 'navigator-series') {
createElement(OPTION, {
innerHTML: seriesOptions.name || seriesOptions.id,
value: seriesOptions.id
}, null, selectBox);
}
});
if (defined(selectedOption)) {
selectBox.value = selectedOption;
}
},
/**
* Create typical inputs for chosen indicator. Fields are extracted from
* defaultOptions (ADD mode) or current indicator (ADD mode). Two extra
* fields are added:
* - hidden input - contains indicator type (required for callback)
* - select - list of series which can be linked with indicator
* @private
* @param {Highcharts.Chart} chart
* Chart
* @param {Highcharts.Series} series
* Indicator
* @param {string} seriesType
* Indicator type like: sma, ema, etc.
* @param {Highcharts.HTMLDOMElement} rhsColWrapper
* Element where created HTML list is added
*/
addFormFields: function (chart, series, seriesType, rhsColWrapper) {
var fields = series.params || series.options.params, getNameType = this.indicators.getNameType;
// reset current content
rhsColWrapper.innerHTML = '';
// create title (indicator name in the right column)
createElement(H3, {
className: PREFIX + 'indicator-title',
innerHTML: getNameType(series, seriesType).name
}, null, rhsColWrapper);
// input type
createElement(INPUT, {
type: 'hidden',
name: PREFIX + 'type-' + seriesType,
value: seriesType
}, null, rhsColWrapper);
// list all series with id
this.indicators.listAllSeries.call(this, seriesType, 'series', chart, rhsColWrapper, series.linkedParent && fields.volumeSeriesID);
if (fields.volumeSeriesID) {
this.indicators.listAllSeries.call(this, seriesType, 'volume', chart, rhsColWrapper, series.linkedParent && series.linkedParent.options.id);
}
// add param fields
this.indicators.addParamInputs.call(this, chart, 'params', fields, seriesType, rhsColWrapper);
},
/**
* Recurent function which lists all fields, from params object and
* create them as inputs. Each input has unique `data-name` attribute,
* which keeps chain of fields i.e params.styles.fontSize.
* @private
* @param {Highcharts.Chart} chart
* Chart
* @param {string} parentNode
* Name of parent to create chain of names
* @param {Highcharts.PopupFieldsDictionary<string>} fields
* Params which are based for input create
* @param {string} type
* Indicator type like: sma, ema, etc.
* @param {Highcharts.HTMLDOMElement} parentDiv
* Element where created HTML list is added
*/
addParamInputs: function (chart, parentNode, fields, type, parentDiv) {
var _self = this, addParamInputs = this.indicators.addParamInputs, addInput = this.addInput, parentFullName;
objectEach(fields, function (value, fieldName) {
// create name like params.styles.fontSize
parentFullName = parentNode + '.' + fieldName;
if (isObject(value)) {
addParamInputs.call(_self, chart, parentFullName, value, type, parentDiv);
}
else if (
// skip volume field which is created by addFormFields
parentFullName !== 'params.volumeSeriesID') {
addInput.call(_self, parentFullName, type, parentDiv, [value, 'text'] // all inputs are text type
);
}
});
},
/**
* Get amount of indicators added to chart.
* @private
* @return {number} - Amount of indicators
*/
getAmount: function () {
var series = this.series, counter = 0;
series.forEach(function (serie) {
var seriesOptions = serie.options;
if (serie.params ||
seriesOptions && seriesOptions.params) {
counter++;
}
});
return counter;
}
},
tabs: {
/**
* Init tabs. Create tab menu items, tabs containers
* @private
* @param {Highcharts.Chart} chart
* Reference to current chart
*/
init: function (chart) {
var tabs = this.tabs, indicatorsCount = this.indicators.getAmount.call(chart), firstTab; // run by default
// create menu items
firstTab = tabs.addMenuItem.call(this, 'add');
tabs.addMenuItem.call(this, 'edit', indicatorsCount);
// create tabs containers
tabs.addContentItem.call(this, 'add');
tabs.addContentItem.call(this, 'edit');
tabs.switchTabs.call(this, indicatorsCount);
// activate first tab
tabs.selectTab.call(this, firstTab, 0);
},
/**
* Create tab menu item
* @private
* @param {string} tabName
* `add` or `edit`
* @param {number} [disableTab]
* Disable tab when 0
* @return {Highcharts.HTMLDOMElement}
* Created HTML tab-menu element
*/
addMenuItem: function (tabName, disableTab) {
var popupDiv = this.popup.container, className = PREFIX + 'tab-item', lang = this.lang, menuItem;
if (disableTab === 0) {
className += ' ' + PREFIX + 'tab-disabled';
}
// tab 1
menuItem = createElement(SPAN, {
innerHTML: lang[tabName + 'Button'] || tabName,
className: className
}, null, popupDiv);
menuItem.setAttribute(PREFIX + 'data-tab-type', tabName);
return menuItem;
},
/**
* Create tab content
* @private
* @return {HTMLDOMElement} - created HTML tab-content element
*/
addContentItem: function () {
var popupDiv = this.popup.container;
return createElement(DIV, {
className: PREFIX + 'tab-item-content'
}, null, popupDiv);
},
/**
* Add click event to each tab
* @private
* @param {number} disableTab
* Disable tab when 0
*/
switchTabs: function (disableTab) {
var _self = this, popupDiv = this.popup.container, tabs = popupDiv.querySelectorAll('.' + PREFIX + 'tab-item'), dataParam;
tabs.forEach(function (tab, i) {
dataParam = tab.getAttribute(PREFIX + 'data-tab-type');
if (dataParam === 'edit' && disableTab === 0) {
return;
}
['click', 'touchstart'].forEach(function (eventName) {
addEvent(tab, eventName, function () {
// reset class on other elements
_self.tabs.deselectAll.call(_self);
_self.tabs.selectTab.call(_self, this, i);
});
});
});
},
/**
* Set tab as visible
* @private
* @param {globals.Element} - current tab
* @param {number} - Index of tab in menu
*/
selectTab: function (tab, index) {
var allTabs = this.popup.container
.querySelectorAll('.' + PREFIX + 'tab-item-content');
tab.className += ' ' + PREFIX + 'tab-item-active';
allTabs[index].className += ' ' + PREFIX + 'tab-item-show';
},
/**
* Set all tabs as invisible.
* @private
*/
deselectAll: function () {
var popupDiv = this.popup.container, tabs = popupDiv
.querySelectorAll('.' + PREFIX + 'tab-item'), tabsContent = popupDiv
.querySelectorAll('.' + PREFIX + 'tab-item-content'), i;
for (i = 0; i < tabs.length; i++) {
tabs[i].classList.remove(PREFIX + 'tab-item-active');
tabsContent[i].classList.remove(PREFIX + 'tab-item-show');
}
}
}
};
addEvent(NavigationBindings, 'showPopup', function (config) {
if (!this.popup) {
// Add popup to main container
this.popup = new H.Popup(this.chart.container, (this.chart.options.navigation.iconsURL ||
(this.chart.options.stockTools &&
this.chart.options.stockTools.gui.iconsURL) ||
'https://code.highcharts.com/8.2.0/gfx/stock-icons/'));
}
this.popup.showForm(config.formType, this.chart, config.options, config.onSubmit);
});
addEvent(NavigationBindings, 'closePopup', function () {
if (this.popup) {
this.popup.closePopup();
}
});
@@ -0,0 +1,178 @@
/* *
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
import Annotation from '../Annotations.js';
import MockPoint from '../MockPoint.js';
import U from '../../../Core/Utilities.js';
var merge = U.merge;
/* eslint-disable no-invalid-this */
var BasicAnnotation = /** @class */ (function (_super) {
__extends(BasicAnnotation, _super);
/* *
*
* Constructors
*
* */
function BasicAnnotation(chart, options) {
return _super.call(this, chart, options) || this;
}
/* *
*
* Functions
*
* */
BasicAnnotation.prototype.addControlPoints = function () {
var options = this.options, controlPoints = BasicAnnotation.basicControlPoints, annotationType = this.basicType, optionsGroup = options.labels || options.shapes;
optionsGroup.forEach(function (group) {
group.controlPoints = controlPoints[annotationType];
});
};
BasicAnnotation.prototype.init = function () {
var options = this.options;
if (options.shapes) {
delete options.labelOptions;
if (options.shapes[0].type === 'circle') {
this.basicType = 'circle';
}
else {
this.basicType = 'rectangle';
}
}
else {
delete options.shapes;
this.basicType = 'label';
}
Annotation.prototype.init.apply(this, arguments);
};
/* *
*
* Static Properties
*
* */
BasicAnnotation.basicControlPoints = {
label: [{
symbol: 'triangle-down',
positioner: function (target) {
if (!target.graphic.placed) {
return {
x: 0,
y: -9e7
};
}
var xy = MockPoint
.pointToPixels(target.points[0]);
return {
x: xy.x - this.graphic.width / 2,
y: xy.y - this.graphic.height / 2
};
},
// TRANSLATE POINT/ANCHOR
events: {
drag: function (e, target) {
var xy = this.mouseMoveToTranslation(e);
target.translatePoint(xy.x, xy.y);
target.annotation.userOptions.labels[0].point =
target.options.point;
target.redraw(false);
}
}
}, {
symbol: 'square',
positioner: function (target) {
if (!target.graphic.placed) {
return {
x: 0,
y: -9e7
};
}
return {
x: target.graphic.alignAttr.x -
this.graphic.width / 2,
y: target.graphic.alignAttr.y -
this.graphic.height / 2
};
},
// TRANSLATE POSITION WITHOUT CHANGING THE
// ANCHOR
events: {
drag: function (e, target) {
var xy = this.mouseMoveToTranslation(e);
target.translate(xy.x, xy.y);
target.annotation.userOptions.labels[0].point =
target.options.point;
target.redraw(false);
}
}
}],
rectangle: [{
positioner: function (annotation) {
var xy = MockPoint
.pointToPixels(annotation.points[2]);
return {
x: xy.x - 4,
y: xy.y - 4
};
},
events: {
drag: function (e, target) {
var annotation = target.annotation, coords = this.chart.pointer.getCoordinates(e), x = coords.xAxis[0].value, y = coords.yAxis[0].value, points = target.options.points;
// Top right point
points[1].x = x;
// Bottom right point (cursor position)
points[2].x = x;
points[2].y = y;
// Bottom left
points[3].y = y;
annotation.userOptions.shapes[0].points =
target.options.points;
annotation.redraw(false);
}
}
}],
circle: [{
positioner: function (target) {
var xy = MockPoint.pointToPixels(target.points[0]), r = target.options.r;
return {
x: xy.x + r * Math.cos(Math.PI / 4) -
this.graphic.width / 2,
y: xy.y + r * Math.sin(Math.PI / 4) -
this.graphic.height / 2
};
},
events: {
// TRANSFORM RADIUS ACCORDING TO Y
// TRANSLATION
drag: function (e, target) {
var annotation = target.annotation, position = this.mouseMoveToTranslation(e);
target.setRadius(Math.max(target.options.r +
position.y /
Math.sin(Math.PI / 4), 5));
annotation.userOptions.shapes[0].r = target.options.r;
annotation.userOptions.shapes[0].point =
target.options.point;
target.redraw(false);
}
}
}]
};
return BasicAnnotation;
}(Annotation));
BasicAnnotation.prototype.defaultOptions = merge(Annotation.prototype.defaultOptions, {});
Annotation.types.basicAnnotation = BasicAnnotation;
export default BasicAnnotation;
@@ -0,0 +1,171 @@
/* *
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
import Annotation from '../Annotations.js';
import ControlPoint from '../ControlPoint.js';
import MockPoint from '../MockPoint.js';
import U from '../../../Core/Utilities.js';
var merge = U.merge;
/* eslint-disable no-invalid-this, valid-jsdoc */
var CrookedLine = /** @class */ (function (_super) {
__extends(CrookedLine, _super);
/* *
*
* Constructors
*
* */
function CrookedLine(chart, options) {
return _super.call(this, chart, options) || this;
}
/* *
*
* Functions
*
* */
/**
* Overrides default setter to get axes from typeOptions.
* @private
*/
CrookedLine.prototype.setClipAxes = function () {
this.clipXAxis = this.chart.xAxis[this.options.typeOptions.xAxis];
this.clipYAxis = this.chart.yAxis[this.options.typeOptions.yAxis];
};
CrookedLine.prototype.getPointsOptions = function () {
var typeOptions = this.options.typeOptions;
return (typeOptions.points || []).map(function (pointOptions) {
pointOptions.xAxis = typeOptions.xAxis;
pointOptions.yAxis = typeOptions.yAxis;
return pointOptions;
});
};
CrookedLine.prototype.getControlPointsOptions = function () {
return this.getPointsOptions();
};
CrookedLine.prototype.addControlPoints = function () {
this.getControlPointsOptions().forEach(function (pointOptions, i) {
var controlPoint = new ControlPoint(this.chart, this, merge(this.options.controlPointOptions, pointOptions.controlPoint), i);
this.controlPoints.push(controlPoint);
pointOptions.controlPoint = controlPoint.options;
}, this);
};
CrookedLine.prototype.addShapes = function () {
var typeOptions = this.options.typeOptions, shape = this.initShape(merge(typeOptions.line, {
type: 'path',
points: this.points.map(function (_point, i) {
return function (target) {
return target.annotation.points[i];
};
})
}), false);
typeOptions.line = shape.options;
};
return CrookedLine;
}(Annotation));
CrookedLine.prototype.defaultOptions = merge(Annotation.prototype.defaultOptions,
/**
* A crooked line annotation.
*
* @sample highcharts/annotations-advanced/crooked-line/
* Crooked line
*
* @product highstock
* @optionparent annotations.crookedLine
*/
{
/**
* @extends annotations.labelOptions
* @apioption annotations.crookedLine.labelOptions
*/
/**
* @extends annotations.shapeOptions
* @apioption annotations.crookedLine.shapeOptions
*/
/**
* Additional options for an annotation with the type.
*/
typeOptions: {
/**
* This number defines which xAxis the point is connected to.
* It refers to either the axis id or the index of the axis
* in the xAxis array.
*/
xAxis: 0,
/**
* This number defines which yAxis the point is connected to.
* It refers to either the axis id or the index of the axis
* in the xAxis array.
*/
yAxis: 0,
/**
* @type {Array<*>}
* @apioption annotations.crookedLine.typeOptions.points
*/
/**
* The x position of the point.
*
* @type {number}
* @apioption annotations.crookedLine.typeOptions.points.x
*/
/**
* The y position of the point.
*
* @type {number}
* @apioption annotations.crookedLine.typeOptions.points.y
*/
/**
* @type {number}
* @excluding positioner, events
* @apioption annotations.crookedLine.typeOptions.points.controlPoint
*/
/**
* Line options.
*
* @excluding height, point, points, r, type, width
*/
line: {
fill: 'none'
}
},
/**
* @excluding positioner, events
*/
controlPointOptions: {
positioner: function (target) {
var graphic = this.graphic, xy = MockPoint.pointToPixels(target.points[this.index]);
return {
x: xy.x - graphic.width / 2,
y: xy.y - graphic.height / 2
};
},
events: {
drag: function (e, target) {
if (target.chart.isInsidePlot(e.chartX - target.chart.plotLeft, e.chartY - target.chart.plotTop)) {
var translation = this.mouseMoveToTranslation(e);
target.translatePoint(translation.x, translation.y, this.index);
// Update options:
target.options.typeOptions.points[this.index].x = target.points[this.index].x;
target.options.typeOptions.points[this.index].y = target.points[this.index].y;
target.redraw(false);
}
}
}
}
});
Annotation.types.crookedLine = CrookedLine;
export default CrookedLine;
@@ -0,0 +1,85 @@
/* *
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
import Annotation from '../Annotations.js';
import CrookedLine from './CrookedLine.js';
import U from '../../../Core/Utilities.js';
var merge = U.merge;
/* eslint-disable no-invalid-this, valid-jsdoc */
var ElliottWave = /** @class */ (function (_super) {
__extends(ElliottWave, _super);
function ElliottWave(chart, options) {
return _super.call(this, chart, options) || this;
}
/* *
*
* Functions
*
* */
ElliottWave.prototype.addLabels = function () {
this.getPointsOptions().forEach(function (point, i) {
var typeOptions = this.options.typeOptions, label = this.initLabel(merge(point.label, {
text: typeOptions.labels[i],
point: function (target) {
return target.annotation.points[i];
}
}), false);
point.label = label.options;
}, this);
};
return ElliottWave;
}(CrookedLine));
ElliottWave.prototype.defaultOptions = merge(CrookedLine.prototype.defaultOptions,
/**
* An elliott wave annotation.
*
* @sample highcharts/annotations-advanced/elliott-wave/
* Elliott wave
*
* @extends annotations.crookedLine
* @product highstock
* @optionparent annotations.elliottWave
*/
{
typeOptions: {
/**
* @extends annotations.crookedLine.labelOptions
* @apioption annotations.elliottWave.typeOptions.points.label
*/
/**
* @ignore-options
*/
labels: ['(0)', '(A)', '(B)', '(C)', '(D)', '(E)'],
line: {
strokeWidth: 1
}
},
labelOptions: {
align: 'center',
allowOverlap: true,
crop: true,
overflow: 'none',
type: 'rect',
backgroundColor: 'none',
borderWidth: 0,
y: -5
}
});
Annotation.types.elliottWave = ElliottWave;
export default ElliottWave;
@@ -0,0 +1,194 @@
/* *
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
import Annotation from '../Annotations.js';
import MockPoint from '../MockPoint.js';
import Tunnel from './Tunnel.js';
import U from '../../../Core/Utilities.js';
var merge = U.merge;
/* eslint-disable no-invalid-this, valid-jsdoc */
var createPathDGenerator = function (retracementIndex, isBackground) {
return function () {
var annotation = this.annotation, leftTop = this.anchor(annotation.startRetracements[retracementIndex]).absolutePosition, rightTop = this.anchor(annotation.endRetracements[retracementIndex]).absolutePosition, d = [
['M', Math.round(leftTop.x), Math.round(leftTop.y)],
['L', Math.round(rightTop.x), Math.round(rightTop.y)]
], rightBottom, leftBottom;
if (isBackground) {
rightBottom = this.anchor(annotation.endRetracements[retracementIndex - 1]).absolutePosition;
leftBottom = this.anchor(annotation.startRetracements[retracementIndex - 1]).absolutePosition;
d.push(['L', Math.round(rightBottom.x), Math.round(rightBottom.y)], ['L', Math.round(leftBottom.x), Math.round(leftBottom.y)]);
}
return d;
};
};
var Fibonacci = /** @class */ (function (_super) {
__extends(Fibonacci, _super);
/* *
*
* Constructors
*
* */
function Fibonacci(chart, options) {
return _super.call(this, chart, options) || this;
}
/* *
*
* Functions
*
* */
Fibonacci.prototype.linkPoints = function () {
_super.prototype.linkPoints.call(this);
this.linkRetracementsPoints();
return;
};
Fibonacci.prototype.linkRetracementsPoints = function () {
var points = this.points, startDiff = points[0].y - points[3].y, endDiff = points[1].y - points[2].y, startX = points[0].x, endX = points[1].x;
Fibonacci.levels.forEach(function (level, i) {
var startRetracement = points[0].y - startDiff * level, endRetracement = points[1].y - endDiff * level;
this.startRetracements = this.startRetracements || [];
this.endRetracements = this.endRetracements || [];
this.linkRetracementPoint(i, startX, startRetracement, this.startRetracements);
this.linkRetracementPoint(i, endX, endRetracement, this.endRetracements);
}, this);
};
Fibonacci.prototype.linkRetracementPoint = function (pointIndex, x, y, retracements) {
var point = retracements[pointIndex], typeOptions = this.options.typeOptions;
if (!point) {
retracements[pointIndex] = new MockPoint(this.chart, this, {
x: x,
y: y,
xAxis: typeOptions.xAxis,
yAxis: typeOptions.yAxis
});
}
else {
point.options.x = x;
point.options.y = y;
point.refresh();
}
};
Fibonacci.prototype.addShapes = function () {
Fibonacci.levels.forEach(function (_level, i) {
this.initShape({
type: 'path',
d: createPathDGenerator(i)
}, false);
if (i > 0) {
this.initShape({
type: 'path',
fill: this.options.typeOptions.backgroundColors[i - 1],
strokeWidth: 0,
d: createPathDGenerator(i, true)
});
}
}, this);
};
Fibonacci.prototype.addLabels = function () {
Fibonacci.levels.forEach(function (level, i) {
var options = this.options.typeOptions, label = this.initLabel(merge(options.labels[i], {
point: function (target) {
var point = MockPoint.pointToOptions(target.annotation.startRetracements[i]);
return point;
},
text: level.toString()
}));
options.labels[i] = label.options;
}, this);
};
/* *
*
* Static properties
*
* */
Fibonacci.levels = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1];
return Fibonacci;
}(Tunnel));
Fibonacci.prototype.defaultOptions = merge(Tunnel.prototype.defaultOptions,
/**
* A fibonacci annotation.
*
* @sample highcharts/annotations-advanced/fibonacci/
* Fibonacci
*
* @extends annotations.crookedLine
* @product highstock
* @optionparent annotations.fibonacci
*/
{
typeOptions: {
/**
* The height of the fibonacci in terms of yAxis.
*/
height: 2,
/**
* An array of background colors:
* Default to:
* ```
* [
* 'rgba(130, 170, 255, 0.4)',
* 'rgba(139, 191, 216, 0.4)',
* 'rgba(150, 216, 192, 0.4)',
* 'rgba(156, 229, 161, 0.4)',
* 'rgba(162, 241, 130, 0.4)',
* 'rgba(169, 255, 101, 0.4)'
* ]
* ```
*/
backgroundColors: [
'rgba(130, 170, 255, 0.4)',
'rgba(139, 191, 216, 0.4)',
'rgba(150, 216, 192, 0.4)',
'rgba(156, 229, 161, 0.4)',
'rgba(162, 241, 130, 0.4)',
'rgba(169, 255, 101, 0.4)'
],
/**
* The color of line.
*/
lineColor: 'grey',
/**
* An array of colors for the lines.
*/
lineColors: [],
/**
* An array with options for the labels.
*
* @type {Array<*>}
* @extends annotations.crookedLine.labelOptions
* @apioption annotations.fibonacci.typeOptions.labels
*/
labels: []
},
labelOptions: {
allowOverlap: true,
align: 'right',
backgroundColor: 'none',
borderWidth: 0,
crop: false,
overflow: 'none',
shape: 'rect',
style: {
color: 'grey'
},
verticalAlign: 'middle',
y: 0
}
});
Annotation.types.fibonacci = Fibonacci;
export default Fibonacci;
@@ -0,0 +1,147 @@
/* *
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
import Annotation from '../Annotations.js';
import CrookedLine from './CrookedLine.js';
import MockPoint from '../MockPoint.js';
import U from '../../../Core/Utilities.js';
var merge = U.merge;
/* eslint-disable no-invalid-this, valid-jsdoc */
var InfinityLine = /** @class */ (function (_super) {
__extends(InfinityLine, _super);
/* *
*
* Constructors
*
* */
function InfinityLine(chart, options) {
return _super.call(this, chart, options) || this;
}
/* *
*
* Static Functions
*
* */
InfinityLine.edgePoint = function (startIndex, endIndex) {
return function (target) {
var annotation = target.annotation, points = annotation.points, type = annotation.options.typeOptions.type;
if (type === 'horizontalLine') {
// Horizontal line has only one point,
// make a copy of it:
points = [
points[0],
new MockPoint(annotation.chart, points[0].target, {
x: points[0].x + 1,
y: points[0].y,
xAxis: points[0].options.xAxis,
yAxis: points[0].options.yAxis
})
];
}
else if (type === 'verticalLine') {
// The same for verticalLine type:
points = [
points[0],
new MockPoint(annotation.chart, points[0].target, {
x: points[0].x,
y: points[0].y + 1,
xAxis: points[0].options.xAxis,
yAxis: points[0].options.yAxis
})
];
}
return InfinityLine.findEdgePoint(points[startIndex], points[endIndex]);
};
};
InfinityLine.findEdgeCoordinate = function (firstPoint, secondPoint, xOrY, edgePointFirstCoordinate) {
var xOrYOpposite = xOrY === 'x' ? 'y' : 'x';
// solves equation for x or y
// y - y1 = (y2 - y1) / (x2 - x1) * (x - x1)
return ((secondPoint[xOrY] - firstPoint[xOrY]) *
(edgePointFirstCoordinate - firstPoint[xOrYOpposite]) /
(secondPoint[xOrYOpposite] - firstPoint[xOrYOpposite]) +
firstPoint[xOrY]);
};
InfinityLine.findEdgePoint = function (firstPoint, secondPoint) {
var xAxis = firstPoint.series.xAxis, yAxis = secondPoint.series.yAxis, firstPointPixels = MockPoint.pointToPixels(firstPoint), secondPointPixels = MockPoint.pointToPixels(secondPoint), deltaX = secondPointPixels.x - firstPointPixels.x, deltaY = secondPointPixels.y - firstPointPixels.y, xAxisMin = xAxis.left, xAxisMax = xAxisMin + xAxis.width, yAxisMin = yAxis.top, yAxisMax = yAxisMin + yAxis.height, xLimit = deltaX < 0 ? xAxisMin : xAxisMax, yLimit = deltaY < 0 ? yAxisMin : yAxisMax, edgePoint = {
x: deltaX === 0 ? firstPointPixels.x : xLimit,
y: deltaY === 0 ? firstPointPixels.y : yLimit
}, edgePointX, edgePointY, swap;
if (deltaX !== 0 && deltaY !== 0) {
edgePointY = InfinityLine.findEdgeCoordinate(firstPointPixels, secondPointPixels, 'y', xLimit);
edgePointX = InfinityLine.findEdgeCoordinate(firstPointPixels, secondPointPixels, 'x', yLimit);
if (edgePointY >= yAxisMin && edgePointY <= yAxisMax) {
edgePoint.x = xLimit;
edgePoint.y = edgePointY;
}
else {
edgePoint.x = edgePointX;
edgePoint.y = yLimit;
}
}
edgePoint.x -= xAxisMin;
edgePoint.y -= yAxisMin;
if (firstPoint.series.chart.inverted) {
swap = edgePoint.x;
edgePoint.x = edgePoint.y;
edgePoint.y = swap;
}
return edgePoint;
};
/* *
*
* Functions
*
* */
InfinityLine.prototype.addShapes = function () {
var typeOptions = this.options.typeOptions, points = [
this.points[0],
InfinityLine.endEdgePoint
];
if (typeOptions.type.match(/Line/g)) {
points[0] = InfinityLine.startEdgePoint;
}
var line = this.initShape(merge(typeOptions.line, {
type: 'path',
points: points
}), false);
typeOptions.line = line.options;
};
/**
*
* Static Properties
*
*/
InfinityLine.endEdgePoint = InfinityLine.edgePoint(0, 1);
InfinityLine.startEdgePoint = InfinityLine.edgePoint(1, 0);
return InfinityLine;
}(CrookedLine));
InfinityLine.prototype.defaultOptions = merge(CrookedLine.prototype.defaultOptions, {});
/**
* An infinity line annotation.
*
* @sample highcharts/annotations-advanced/infinity-line/
* Infinity Line
*
* @extends annotations.crookedLine
* @product highstock
* @apioption annotations.infinityLine
*/
Annotation.types.infinityLine = InfinityLine;
export default InfinityLine;
@@ -0,0 +1,767 @@
/* *
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
import Annotation from '../Annotations.js';
import ControlPoint from '../ControlPoint.js';
import U from '../../../Core/Utilities.js';
var extend = U.extend, isNumber = U.isNumber, merge = U.merge;
/* eslint-disable no-invalid-this, valid-jsdoc */
var Measure = /** @class */ (function (_super) {
__extends(Measure, _super);
/* *
*
* Constructors
*
* */
function Measure(chart, userOptions) {
return _super.call(this, chart, userOptions) || this;
}
/* *
*
* Functions
*
* */
/**
* Init annotation object.
* @private
*/
Measure.prototype.init = function (annotationOrChart, userOptions, index) {
Annotation.prototype.init.call(this, annotationOrChart, userOptions, index);
this.offsetX = 0;
this.offsetY = 0;
this.resizeX = 0;
this.resizeY = 0;
Measure.calculations.init.call(this);
this.addValues();
this.addShapes();
};
/**
* Overrides default setter to get axes from typeOptions.
* @private
*/
Measure.prototype.setClipAxes = function () {
this.clipXAxis = this.chart.xAxis[this.options.typeOptions.xAxis];
this.clipYAxis = this.chart.yAxis[this.options.typeOptions.yAxis];
};
/**
* Get measure points configuration objects.
* @private
* @return {Array<Highcharts.AnnotationMockPointOptionsObject>}
*/
Measure.prototype.pointsOptions = function () {
return this.options.points;
};
/**
* Get points configuration objects for shapes.
* @private
* @return {Array<Highcharts.AnnotationMockPointOptionsObject>}
*/
Measure.prototype.shapePointsOptions = function () {
var options = this.options.typeOptions, xAxis = options.xAxis, yAxis = options.yAxis;
return [
{
x: this.xAxisMin,
y: this.yAxisMin,
xAxis: xAxis,
yAxis: yAxis
},
{
x: this.xAxisMax,
y: this.yAxisMin,
xAxis: xAxis,
yAxis: yAxis
},
{
x: this.xAxisMax,
y: this.yAxisMax,
xAxis: xAxis,
yAxis: yAxis
},
{
x: this.xAxisMin,
y: this.yAxisMax,
xAxis: xAxis,
yAxis: yAxis
}
];
};
Measure.prototype.addControlPoints = function () {
var selectType = this.options.typeOptions.selectType, controlPoint;
controlPoint = new ControlPoint(this.chart, this, this.options.controlPointOptions, 0);
this.controlPoints.push(controlPoint);
// add extra controlPoint for horizontal and vertical range
if (selectType !== 'xy') {
controlPoint = new ControlPoint(this.chart, this, this.options.controlPointOptions, 1);
this.controlPoints.push(controlPoint);
}
};
/**
* Add label with calculated values (min, max, average, bins).
* @private
* @param {boolean} [resize]
* The flag for resize shape
*/
Measure.prototype.addValues = function (resize) {
var typeOptions = this.options.typeOptions, formatter = typeOptions.label.formatter;
// set xAxisMin, xAxisMax, yAxisMin, yAxisMax
Measure.calculations.recalculate.call(this, resize);
if (!typeOptions.label.enabled) {
return;
}
if (this.labels.length > 0) {
this.labels[0].text = (formatter && formatter.call(this)) ||
Measure.calculations.defaultFormatter.call(this);
}
else {
this.initLabel(extend({
shape: 'rect',
backgroundColor: 'none',
color: 'black',
borderWidth: 0,
dashStyle: 'dash',
overflow: 'none',
align: 'left',
vertical: 'top',
crop: true,
point: function (target) {
var annotation = target.annotation, chart = annotation.chart, inverted = chart.inverted, xAxis = chart.xAxis[typeOptions.xAxis], yAxis = chart.yAxis[typeOptions.yAxis], top = chart.plotTop, left = chart.plotLeft;
return {
x: (inverted ? top : 10) +
xAxis.toPixels(annotation.xAxisMin, !inverted),
y: (inverted ? -left + 10 : top) +
yAxis.toPixels(annotation.yAxisMin)
};
},
text: (formatter && formatter.call(this)) ||
Measure.calculations.defaultFormatter.call(this)
}, typeOptions.label));
}
};
/**
* Crosshair, background (rect).
* @private
*/
Measure.prototype.addShapes = function () {
this.addCrosshairs();
this.addBackground();
};
/**
* Add background shape.
* @private
*/
Measure.prototype.addBackground = function () {
var shapePoints = this.shapePointsOptions();
if (typeof shapePoints[0].x === 'undefined') {
return;
}
this.initShape(extend({
type: 'path',
points: this.shapePointsOptions()
}, this.options.typeOptions.background), false);
};
/**
* Add internal crosshair shapes (on top and bottom).
* @private
*/
Measure.prototype.addCrosshairs = function () {
var chart = this.chart, options = this.options.typeOptions, point = this.options.typeOptions.point, xAxis = chart.xAxis[options.xAxis], yAxis = chart.yAxis[options.yAxis], inverted = chart.inverted, xAxisMin = xAxis.toPixels(this.xAxisMin), xAxisMax = xAxis.toPixels(this.xAxisMax), yAxisMin = yAxis.toPixels(this.yAxisMin), yAxisMax = yAxis.toPixels(this.yAxisMax), defaultOptions = {
point: point,
type: 'path'
}, pathH = [], pathV = [], crosshairOptionsX, crosshairOptionsY, temp;
if (inverted) {
temp = xAxisMin;
xAxisMin = yAxisMin;
yAxisMin = temp;
temp = xAxisMax;
xAxisMax = yAxisMax;
yAxisMax = temp;
}
// horizontal line
if (options.crosshairX.enabled) {
pathH = [[
'M',
xAxisMin,
yAxisMin + ((yAxisMax - yAxisMin) / 2)
], [
'L',
xAxisMax,
yAxisMin + ((yAxisMax - yAxisMin) / 2)
]];
}
// vertical line
if (options.crosshairY.enabled) {
pathV = [[
'M',
xAxisMin + ((xAxisMax - xAxisMin) / 2),
yAxisMin
], [
'L',
xAxisMin + ((xAxisMax - xAxisMin) / 2),
yAxisMax
]];
}
// Update existed crosshair
if (this.shapes.length > 0) {
this.shapes[0].options.d = pathH;
this.shapes[1].options.d = pathV;
}
else {
// Add new crosshairs
crosshairOptionsX = merge(defaultOptions, options.crosshairX);
crosshairOptionsY = merge(defaultOptions, options.crosshairY);
this.initShape(extend({
d: pathH
}, crosshairOptionsX), false);
this.initShape(extend({
d: pathV
}, crosshairOptionsY), false);
}
};
Measure.prototype.onDrag = function (e) {
var translation = this.mouseMoveToTranslation(e), selectType = this.options.typeOptions.selectType, x = selectType === 'y' ? 0 : translation.x, y = selectType === 'x' ? 0 : translation.y;
this.translate(x, y);
this.offsetX += x;
this.offsetY += y;
// animation, resize, setStartPoints
this.redraw(false, false, true);
};
/**
* Translate start or end ("left" or "right") side of the measure.
* Update start points (startXMin, startXMax, startYMin, startYMax)
* @private
* @param {number} dx - the amount of x translation
* @param {number} dy - the amount of y translation
* @param {number} cpIndex - index of control point
* @param {Highcharts.AnnotationDraggableValue} selectType - x / y / xy
*/
Measure.prototype.resize = function (dx, dy, cpIndex, selectType) {
// background shape
var bckShape = this.shapes[2];
if (selectType === 'x') {
if (cpIndex === 0) {
bckShape.translatePoint(dx, 0, 0);
bckShape.translatePoint(dx, dy, 3);
}
else {
bckShape.translatePoint(dx, 0, 1);
bckShape.translatePoint(dx, dy, 2);
}
}
else if (selectType === 'y') {
if (cpIndex === 0) {
bckShape.translatePoint(0, dy, 0);
bckShape.translatePoint(0, dy, 1);
}
else {
bckShape.translatePoint(0, dy, 2);
bckShape.translatePoint(0, dy, 3);
}
}
else {
bckShape.translatePoint(dx, 0, 1);
bckShape.translatePoint(dx, dy, 2);
bckShape.translatePoint(0, dy, 3);
}
Measure.calculations.updateStartPoints
.call(this, false, true, cpIndex, dx, dy);
this.options.typeOptions.background.height = Math.abs(this.startYMax - this.startYMin);
this.options.typeOptions.background.width = Math.abs(this.startXMax - this.startXMin);
};
/**
* Redraw event which render elements and update start points if needed.
* @private
* @param {boolean} animation
* @param {boolean} [resize] - flag if resized
* @param {boolean} [setStartPoints] - update position of start points
*/
Measure.prototype.redraw = function (animation, resize, setStartPoints) {
this.linkPoints();
if (!this.graphic) {
this.render();
}
if (setStartPoints) {
Measure.calculations.updateStartPoints.call(this, true, false);
}
// #11174 - clipBox was not recalculate during resize / redraw
if (this.clipRect) {
this.clipRect.animate(this.getClipBox());
}
this.addValues(resize);
this.addCrosshairs();
this.redrawItems(this.shapes, animation);
this.redrawItems(this.labels, animation);
// redraw control point to run positioner
this.controlPoints.forEach(function (controlPoint) {
controlPoint.redraw();
});
};
Measure.prototype.translate = function (dx, dy) {
this.shapes.forEach(function (item) {
item.translate(dx, dy);
});
this.options.typeOptions.point.x = this.startXMin;
this.options.typeOptions.point.y = this.startYMin;
};
/* *
*
* Static Functions
*
* */
Measure.calculations = {
/**
* Set starting points
* @private
*/
init: function () {
var options = this.options.typeOptions, chart = this.chart, getPointPos = Measure.calculations.getPointPos, inverted = chart.inverted, xAxis = chart.xAxis[options.xAxis], yAxis = chart.yAxis[options.yAxis], bck = options.background, width = inverted ? bck.height : bck.width, height = inverted ? bck.width : bck.height, selectType = options.selectType, top = inverted ? xAxis.left : yAxis.top, // #13664
left = inverted ? yAxis.top : xAxis.left; // #13664
this.startXMin = options.point.x;
this.startYMin = options.point.y;
if (isNumber(width)) {
this.startXMax = this.startXMin + width;
}
else {
this.startXMax = getPointPos(xAxis, this.startXMin, parseFloat(width));
}
if (isNumber(height)) {
this.startYMax = this.startYMin - height;
}
else {
this.startYMax = getPointPos(yAxis, this.startYMin, parseFloat(height));
}
// x / y selection type
if (selectType === 'x') {
this.startYMin = yAxis.toValue(top);
this.startYMax = yAxis.toValue(top + yAxis.len);
}
else if (selectType === 'y') {
this.startXMin = xAxis.toValue(left);
this.startXMax = xAxis.toValue(left + xAxis.len);
}
},
/**
* Set current xAxisMin, xAxisMax, yAxisMin, yAxisMax.
* Calculations of measure values (min, max, average, bins).
* @private
* @param {boolean} [resize]
* Flag if shape is resized.
*/
recalculate: function (resize) {
var calc = Measure.calculations, options = this.options.typeOptions, xAxis = this.chart.xAxis[options.xAxis], yAxis = this.chart.yAxis[options.yAxis], getPointPos = Measure.calculations.getPointPos, offsetX = this.offsetX, offsetY = this.offsetY;
this.xAxisMin = getPointPos(xAxis, this.startXMin, offsetX);
this.xAxisMax = getPointPos(xAxis, this.startXMax, offsetX);
this.yAxisMin = getPointPos(yAxis, this.startYMin, offsetY);
this.yAxisMax = getPointPos(yAxis, this.startYMax, offsetY);
this.min = calc.min.call(this);
this.max = calc.max.call(this);
this.average = calc.average.call(this);
this.bins = calc.bins.call(this);
if (resize) {
this.resize(0, 0);
}
},
/**
* Set current xAxisMin, xAxisMax, yAxisMin, yAxisMax.
* Calculations of measure values (min, max, average, bins).
* @private
* @param {Highcharts.Axis} axis
* X or y axis reference
* @param {number} value
* Point's value (x or y)
* @param {number} offset
* Amount of pixels
*/
getPointPos: function (axis, value, offset) {
return axis.toValue(axis.toPixels(value) + offset);
},
/**
* Update position of start points
* (startXMin, startXMax, startYMin, startYMax)
* @private
* @param {boolean} redraw
* Flag if shape is redraw
* @param {boolean} resize
* Flag if shape is resized
* @param {number} cpIndex
* Index of controlPoint
*/
updateStartPoints: function (redraw, resize, cpIndex, dx, dy) {
var options = this.options.typeOptions, selectType = options.selectType, xAxis = this.chart.xAxis[options.xAxis], yAxis = this.chart.yAxis[options.yAxis], getPointPos = Measure.calculations.getPointPos, startXMin = this.startXMin, startXMax = this.startXMax, startYMin = this.startYMin, startYMax = this.startYMax, offsetX = this.offsetX, offsetY = this.offsetY;
if (resize) {
if (selectType === 'x') {
if (cpIndex === 0) {
this.startXMin = getPointPos(xAxis, startXMin, dx);
}
else {
this.startXMax = getPointPos(xAxis, startXMax, dx);
}
}
else if (selectType === 'y') {
if (cpIndex === 0) {
this.startYMin = getPointPos(yAxis, startYMin, dy);
}
else {
this.startYMax = getPointPos(yAxis, startYMax, dy);
}
}
else {
this.startXMax = getPointPos(xAxis, startXMax, dx);
this.startYMax = getPointPos(yAxis, startYMax, dy);
}
}
if (redraw) {
this.startXMin = getPointPos(xAxis, startXMin, offsetX);
this.startXMax = getPointPos(xAxis, startXMax, offsetX);
this.startYMin = getPointPos(yAxis, startYMin, offsetY);
this.startYMax = getPointPos(yAxis, startYMax, offsetY);
this.offsetX = 0;
this.offsetY = 0;
}
},
/**
* Default formatter of label's content
* @private
*/
defaultFormatter: function () {
return 'Min: ' + this.min +
'<br>Max: ' + this.max +
'<br>Average: ' + this.average +
'<br>Bins: ' + this.bins;
},
/**
* Set values for xAxisMin, xAxisMax, yAxisMin, yAxisMax, also
* when chart is inverted
* @private
*/
getExtremes: function (xAxisMin, xAxisMax, yAxisMin, yAxisMax) {
return {
xAxisMin: Math.min(xAxisMax, xAxisMin),
xAxisMax: Math.max(xAxisMax, xAxisMin),
yAxisMin: Math.min(yAxisMax, yAxisMin),
yAxisMax: Math.max(yAxisMax, yAxisMin)
};
},
/**
* Definitions of calculations (min, max, average, bins)
* @private
*/
min: function () {
var min = Infinity, series = this.chart.series, ext = Measure.calculations.getExtremes(this.xAxisMin, this.xAxisMax, this.yAxisMin, this.yAxisMax), isCalculated = false; // to avoid Infinity in formatter
series.forEach(function (serie) {
if (serie.visible &&
serie.options.id !== 'highcharts-navigator-series') {
serie.points.forEach(function (point) {
if (!point.isNull &&
point.y < min &&
point.x > ext.xAxisMin &&
point.x <= ext.xAxisMax &&
point.y > ext.yAxisMin &&
point.y <= ext.yAxisMax) {
min = point.y;
isCalculated = true;
}
});
}
});
if (!isCalculated) {
min = '';
}
return min;
},
max: function () {
var max = -Infinity, series = this.chart.series, ext = Measure.calculations.getExtremes(this.xAxisMin, this.xAxisMax, this.yAxisMin, this.yAxisMax), isCalculated = false; // to avoid Infinity in formatter
series.forEach(function (serie) {
if (serie.visible &&
serie.options.id !== 'highcharts-navigator-series') {
serie.points.forEach(function (point) {
if (!point.isNull &&
point.y > max &&
point.x > ext.xAxisMin &&
point.x <= ext.xAxisMax &&
point.y > ext.yAxisMin &&
point.y <= ext.yAxisMax) {
max = point.y;
isCalculated = true;
}
});
}
});
if (!isCalculated) {
max = '';
}
return max;
},
average: function () {
var average = '';
if (this.max !== '' && this.min !== '') {
average = (this.max + this.min) / 2;
}
return average;
},
bins: function () {
var bins = 0, series = this.chart.series, ext = Measure.calculations.getExtremes(this.xAxisMin, this.xAxisMax, this.yAxisMin, this.yAxisMax), isCalculated = false; // to avoid Infinity in formatter
series.forEach(function (serie) {
if (serie.visible &&
serie.options.id !== 'highcharts-navigator-series') {
serie.points.forEach(function (point) {
if (!point.isNull &&
point.x > ext.xAxisMin &&
point.x <= ext.xAxisMax &&
point.y > ext.yAxisMin &&
point.y <= ext.yAxisMax) {
bins++;
isCalculated = true;
}
});
}
});
if (!isCalculated) {
bins = '';
}
return bins;
}
};
return Measure;
}(Annotation));
Measure.prototype.defaultOptions = merge(Annotation.prototype.defaultOptions,
/**
* A measure annotation.
*
* @extends annotations.crookedLine
* @excluding labels, labelOptions, shapes, shapeOptions
* @sample highcharts/annotations-advanced/measure/
* Measure
* @product highstock
* @optionparent annotations.measure
*/
{
typeOptions: {
/**
* Decides in what dimensions the user can resize by dragging the
* mouse. Can be one of x, y or xy.
*/
selectType: 'xy',
/**
* This number defines which xAxis the point is connected to.
* It refers to either the axis id or the index of the axis
* in the xAxis array.
*/
xAxis: 0,
/**
* This number defines which yAxis the point is connected to.
* It refers to either the axis id or the index of the axis
* in the yAxis array.
*/
yAxis: 0,
background: {
/**
* The color of the rectangle.
*/
fill: 'rgba(130, 170, 255, 0.4)',
/**
* The width of border.
*/
strokeWidth: 0,
/**
* The color of border.
*/
stroke: void 0
},
/**
* Configure a crosshair that is horizontally placed in middle of
* rectangle.
*
*/
crosshairX: {
/**
* Enable or disable the horizontal crosshair.
*
*/
enabled: true,
/**
* The Z index of the crosshair in annotation.
*/
zIndex: 6,
/**
* The dash or dot style of the crosshair's line. For possible
* values, see
* [this demonstration](https://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/plotoptions/series-dashstyle-all/).
*
* @type {Highcharts.DashStyleValue}
* @default Dash
*/
dashStyle: 'Dash',
/**
* The marker-end defines the arrowhead that will be drawn
* at the final vertex of the given crosshair's path.
*
* @type {string}
* @default arrow
*/
markerEnd: 'arrow'
},
/**
* Configure a crosshair that is vertically placed in middle of
* rectangle.
*/
crosshairY: {
/**
* Enable or disable the vertical crosshair.
*
*/
enabled: true,
/**
* The Z index of the crosshair in annotation.
*/
zIndex: 6,
/**
* The dash or dot style of the crosshair's line. For possible
* values, see [this demonstration](https://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/plotoptions/series-dashstyle-all/).
*
* @type {Highcharts.DashStyleValue}
* @default Dash
* @apioption annotations.measure.typeOptions.crosshairY.dashStyle
*
*/
dashStyle: 'Dash',
/**
* The marker-end defines the arrowhead that will be drawn
* at the final vertex of the given crosshair's path.
*
* @type {string}
* @default arrow
* @validvalue ["none", "arrow"]
*
*/
markerEnd: 'arrow'
},
label: {
/**
* Enable or disable the label text (min, max, average,
* bins values).
*
* Defaults to true.
*/
enabled: true,
/**
* CSS styles for the measure label.
*
* @type {Highcharts.CSSObject}
* @default {"color": "#666666", "fontSize": "11px"}
*/
style: {
fontSize: '11px',
color: '#666666'
},
/**
* Formatter function for the label text.
*
* Available data are:
*
* <table>
*
* <tbody>
*
* <tr>
*
* <td>`this.min`</td>
*
* <td>The mininimum value of the points in the selected
* range.</td>
*
* </tr>
*
* <tr>
*
* <td>`this.max`</td>
*
* <td>The maximum value of the points in the selected
* range.</td>
*
* </tr>
*
* <tr>
*
* <td>`this.average`</td>
*
* <td>The average value of the points in the selected
* range.</td>
*
* </tr>
*
* <tr>
*
* <td>`this.bins`</td>
*
* <td>The amount of the points in the selected range.</td>
*
* </tr>
*
* </table>
*
* @type {function}
*
*/
formatter: void 0
}
},
controlPointOptions: {
positioner: function (target) {
var cpIndex = this.index, chart = target.chart, options = target.options, typeOptions = options.typeOptions, selectType = typeOptions.selectType, controlPointOptions = options.controlPointOptions, inverted = chart.inverted, xAxis = chart.xAxis[typeOptions.xAxis], yAxis = chart.yAxis[typeOptions.yAxis], targetX = target.xAxisMax, targetY = target.yAxisMax, ext = Measure.calculations.getExtremes(target.xAxisMin, target.xAxisMax, target.yAxisMin, target.yAxisMax), x, y;
if (selectType === 'x') {
targetY = (ext.yAxisMax - ext.yAxisMin) / 2;
// first control point
if (cpIndex === 0) {
targetX = target.xAxisMin;
}
}
if (selectType === 'y') {
targetX = ext.xAxisMin +
((ext.xAxisMax - ext.xAxisMin) / 2);
// first control point
if (cpIndex === 0) {
targetY = target.yAxisMin;
}
}
if (inverted) {
x = yAxis.toPixels(targetY);
y = xAxis.toPixels(targetX);
}
else {
x = xAxis.toPixels(targetX);
y = yAxis.toPixels(targetY);
}
return {
x: x - (controlPointOptions.width / 2),
y: y - (controlPointOptions.height / 2)
};
},
events: {
drag: function (e, target) {
var translation = this.mouseMoveToTranslation(e), selectType = target.options.typeOptions.selectType, index = this.index, x = selectType === 'y' ? 0 : translation.x, y = selectType === 'x' ? 0 : translation.y;
target.resize(x, y, index, selectType);
target.resizeX += x;
target.resizeY += y;
target.redraw(false, true);
}
}
}
});
Annotation.types.measure = Measure;
export default Measure;
@@ -0,0 +1,183 @@
/* *
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
import Annotation from '../Annotations.js';
import InfinityLine from './InfinityLine.js';
import MockPoint from '../MockPoint.js';
import U from '../../../Core/Utilities.js';
var merge = U.merge;
/* eslint-disable no-invalid-this, valid-jsdoc */
var Pitchfork = /** @class */ (function (_super) {
__extends(Pitchfork, _super);
/* *
*
* Constructors
*
* */
function Pitchfork(chart, options) {
return _super.call(this, chart, options) || this;
}
/* *
*
* Static Functions
*
* */
Pitchfork.outerLineEdgePoint = function (firstPointIndex) {
return function (target) {
var annotation = target.annotation, points = annotation.points;
return Pitchfork.findEdgePoint(points[firstPointIndex], points[0], new MockPoint(annotation.chart, target, annotation.midPointOptions()));
};
};
Pitchfork.findEdgePoint = function (point, firstAnglePoint, secondAnglePoint) {
var angle = Math.atan2(secondAnglePoint.plotY - firstAnglePoint.plotY, secondAnglePoint.plotX - firstAnglePoint.plotX), distance = 1e7;
return {
x: point.plotX + distance * Math.cos(angle),
y: point.plotY + distance * Math.sin(angle)
};
};
Pitchfork.middleLineEdgePoint = function (target) {
var annotation = target.annotation, points = annotation.points;
return InfinityLine.findEdgePoint(points[0], new MockPoint(annotation.chart, target, annotation.midPointOptions()));
};
/* *
*
* Functions
*
* */
Pitchfork.prototype.midPointOptions = function () {
var points = this.points;
return {
x: (points[1].x + points[2].x) / 2,
y: (points[1].y + points[2].y) / 2,
xAxis: points[0].series.xAxis,
yAxis: points[0].series.yAxis
};
};
Pitchfork.prototype.addShapes = function () {
this.addLines();
this.addBackgrounds();
};
Pitchfork.prototype.addLines = function () {
this.initShape({
type: 'path',
points: [
this.points[0],
Pitchfork.middleLineEdgePoint
]
}, false);
this.initShape({
type: 'path',
points: [
this.points[1],
Pitchfork.topLineEdgePoint
]
}, false);
this.initShape({
type: 'path',
points: [
this.points[2],
Pitchfork.bottomLineEdgePoint
]
}, false);
};
Pitchfork.prototype.addBackgrounds = function () {
var shapes = this.shapes, typeOptions = this.options.typeOptions;
var innerBackground = this.initShape(merge(typeOptions.innerBackground, {
type: 'path',
points: [
function (target) {
var annotation = target.annotation, points = annotation.points, midPointOptions = annotation.midPointOptions();
return {
x: (points[1].x + midPointOptions.x) / 2,
y: (points[1].y + midPointOptions.y) / 2,
xAxis: midPointOptions.xAxis,
yAxis: midPointOptions.yAxis
};
},
shapes[1].points[1],
shapes[2].points[1],
function (target) {
var annotation = target.annotation, points = annotation.points, midPointOptions = annotation.midPointOptions();
return {
x: (midPointOptions.x + points[2].x) / 2,
y: (midPointOptions.y + points[2].y) / 2,
xAxis: midPointOptions.xAxis,
yAxis: midPointOptions.yAxis
};
}
]
}));
var outerBackground = this.initShape(merge(typeOptions.outerBackground, {
type: 'path',
points: [
this.points[1],
shapes[1].points[1],
shapes[2].points[1],
this.points[2]
]
}));
typeOptions.innerBackground = innerBackground.options;
typeOptions.outerBackground = outerBackground.options;
};
/**
*
* Static Properties
*
*/
Pitchfork.topLineEdgePoint = Pitchfork.outerLineEdgePoint(1);
Pitchfork.bottomLineEdgePoint = Pitchfork.outerLineEdgePoint(0);
return Pitchfork;
}(InfinityLine));
Pitchfork.prototype.defaultOptions = merge(InfinityLine.prototype.defaultOptions,
/**
* A pitchfork annotation.
*
* @sample highcharts/annotations-advanced/pitchfork/
* Pitchfork
*
* @extends annotations.infinityLine
* @product highstock
* @optionparent annotations.pitchfork
*/
{
typeOptions: {
/**
* Inner background options.
*
* @extends annotations.crookedLine.shapeOptions
* @excluding height, r, type, width
*/
innerBackground: {
fill: 'rgba(130, 170, 255, 0.4)',
strokeWidth: 0
},
/**
* Outer background options.
*
* @extends annotations.crookedLine.shapeOptions
* @excluding height, r, type, width
*/
outerBackground: {
fill: 'rgba(156, 229, 161, 0.4)',
strokeWidth: 0
}
}
});
Annotation.types.pitchfork = Pitchfork;
export default Pitchfork;
@@ -0,0 +1,194 @@
/* *
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
import Annotation from '../Annotations.js';
import ControlPoint from '../ControlPoint.js';
import CrookedLine from './CrookedLine.js';
import MockPoint from '../MockPoint.js';
import U from '../../../Core/Utilities.js';
var merge = U.merge;
/* eslint-disable no-invalid-this, valid-jsdoc */
/**
* @private
*/
function getSecondCoordinate(p1, p2, x) {
return (p2.y - p1.y) / (p2.x - p1.x) * (x - p1.x) + p1.y;
}
var Tunnel = /** @class */ (function (_super) {
__extends(Tunnel, _super);
/* *
*
* Constructors
*
* */
function Tunnel(chart, options) {
return _super.call(this, chart, options) || this;
}
/* *
*
* Functions
*
* */
Tunnel.prototype.getPointsOptions = function () {
var pointsOptions = CrookedLine.prototype.getPointsOptions.call(this);
pointsOptions[2] = this.heightPointOptions(pointsOptions[1]);
pointsOptions[3] = this.heightPointOptions(pointsOptions[0]);
return pointsOptions;
};
Tunnel.prototype.getControlPointsOptions = function () {
return this.getPointsOptions().slice(0, 2);
};
Tunnel.prototype.heightPointOptions = function (pointOptions) {
var heightPointOptions = merge(pointOptions), typeOptions = this.options.typeOptions;
heightPointOptions.y += typeOptions.height;
return heightPointOptions;
};
Tunnel.prototype.addControlPoints = function () {
CrookedLine.prototype.addControlPoints.call(this);
var options = this.options, typeOptions = options.typeOptions, controlPoint = new ControlPoint(this.chart, this, merge(options.controlPointOptions, typeOptions.heightControlPoint), 2);
this.controlPoints.push(controlPoint);
typeOptions.heightControlPoint = controlPoint.options;
};
Tunnel.prototype.addShapes = function () {
this.addLine();
this.addBackground();
};
Tunnel.prototype.addLine = function () {
var line = this.initShape(merge(this.options.typeOptions.line, {
type: 'path',
points: [
this.points[0],
this.points[1],
function (target) {
var pointOptions = MockPoint.pointToOptions(target.annotation.points[2]);
pointOptions.command = 'M';
return pointOptions;
},
this.points[3]
]
}), false);
this.options.typeOptions.line = line.options;
};
Tunnel.prototype.addBackground = function () {
var background = this.initShape(merge(this.options.typeOptions.background, {
type: 'path',
points: this.points.slice()
}));
this.options.typeOptions.background = background.options;
};
/**
* Translate start or end ("left" or "right") side of the tunnel.
* @private
* @param {number} dx - the amount of x translation
* @param {number} dy - the amount of y translation
* @param {boolean} [end] - whether to translate start or end side
*/
Tunnel.prototype.translateSide = function (dx, dy, end) {
var topIndex = Number(end), bottomIndex = topIndex === 0 ? 3 : 2;
this.translatePoint(dx, dy, topIndex);
this.translatePoint(dx, dy, bottomIndex);
};
/**
* Translate height of the tunnel.
* @private
* @param {number} dh - the amount of height translation
*/
Tunnel.prototype.translateHeight = function (dh) {
this.translatePoint(0, dh, 2);
this.translatePoint(0, dh, 3);
this.options.typeOptions.height = this.points[3].y - this.points[0].y;
};
return Tunnel;
}(CrookedLine));
Tunnel.prototype.defaultOptions = merge(CrookedLine.prototype.defaultOptions,
/**
* A tunnel annotation.
*
* @extends annotations.crookedLine
* @sample highcharts/annotations-advanced/tunnel/
* Tunnel
* @product highstock
* @optionparent annotations.tunnel
*/
{
typeOptions: {
xAxis: 0,
yAxis: 0,
/**
* Background options.
*
* @type {Object}
* @excluding height, point, points, r, type, width, markerEnd,
* markerStart
*/
background: {
fill: 'rgba(130, 170, 255, 0.4)',
strokeWidth: 0
},
line: {
strokeWidth: 1
},
/**
* The height of the annotation in terms of yAxis.
*/
height: -2,
/**
* Options for the control point which controls
* the annotation's height.
*
* @extends annotations.crookedLine.controlPointOptions
* @excluding positioner, events
*/
heightControlPoint: {
positioner: function (target) {
var startXY = MockPoint.pointToPixels(target.points[2]), endXY = MockPoint.pointToPixels(target.points[3]), x = (startXY.x + endXY.x) / 2;
return {
x: x - this.graphic.width / 2,
y: getSecondCoordinate(startXY, endXY, x) -
this.graphic.height / 2
};
},
events: {
drag: function (e, target) {
if (target.chart.isInsidePlot(e.chartX - target.chart.plotLeft, e.chartY - target.chart.plotTop)) {
target.translateHeight(this.mouseMoveToTranslation(e).y);
target.redraw(false);
}
}
}
}
},
/**
* @extends annotations.crookedLine.controlPointOptions
* @excluding positioner, events
*/
controlPointOptions: {
events: {
drag: function (e, target) {
if (target.chart.isInsidePlot(e.chartX - target.chart.plotLeft, e.chartY - target.chart.plotTop)) {
var translation = this.mouseMoveToTranslation(e);
target.translateSide(translation.x, translation.y, this.index);
target.redraw(false);
}
}
}
}
});
Annotation.types.tunnel = Tunnel;
export default Tunnel;
@@ -0,0 +1,147 @@
/* *
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
import Annotation from '../Annotations.js';
import MockPoint from '../MockPoint.js';
import U from '../../../Core/Utilities.js';
var merge = U.merge;
/* eslint-disable no-invalid-this, valid-jsdoc */
var VerticalLine = /** @class */ (function (_super) {
__extends(VerticalLine, _super);
/* *
*
* Constructors
*
* */
function VerticalLine(chart, userOptions) {
return _super.call(this, chart, userOptions) || this;
}
/* *
*
* Static Functions
*
* */
VerticalLine.connectorFirstPoint = function (target) {
var annotation = target.annotation, point = annotation.points[0], xy = MockPoint.pointToPixels(point, true), y = xy.y, offset = annotation.options.typeOptions.label.offset;
if (annotation.chart.inverted) {
y = xy.x;
}
return {
x: point.x,
xAxis: point.series.xAxis,
y: y + offset
};
};
VerticalLine.connectorSecondPoint = function (target) {
var annotation = target.annotation, typeOptions = annotation.options.typeOptions, point = annotation.points[0], yOffset = typeOptions.yOffset, xy = MockPoint.pointToPixels(point, true), y = xy[annotation.chart.inverted ? 'x' : 'y'];
if (typeOptions.label.offset < 0) {
yOffset *= -1;
}
return {
x: point.x,
xAxis: point.series.xAxis,
y: y + yOffset
};
};
/* *
*
* Functions
*
* */
VerticalLine.prototype.getPointsOptions = function () {
return [this.options.typeOptions.point];
};
VerticalLine.prototype.addShapes = function () {
var typeOptions = this.options.typeOptions, connector = this.initShape(merge(typeOptions.connector, {
type: 'path',
points: [
VerticalLine.connectorFirstPoint,
VerticalLine.connectorSecondPoint
]
}), false);
typeOptions.connector = connector.options;
};
VerticalLine.prototype.addLabels = function () {
var typeOptions = this.options.typeOptions, labelOptions = typeOptions.label, x = 0, y = labelOptions.offset, verticalAlign = labelOptions.offset < 0 ? 'bottom' : 'top', align = 'center';
if (this.chart.inverted) {
x = labelOptions.offset;
y = 0;
verticalAlign = 'middle';
align = labelOptions.offset < 0 ? 'right' : 'left';
}
var label = this.initLabel(merge(labelOptions, {
verticalAlign: verticalAlign,
align: align,
x: x,
y: y
}));
typeOptions.label = label.options;
};
return VerticalLine;
}(Annotation));
VerticalLine.prototype.defaultOptions = merge(Annotation.prototype.defaultOptions,
/**
* A vertical line annotation.
*
* @sample highcharts/annotations-advanced/vertical-line/
* Vertical line
*
* @extends annotations.crookedLine
* @excluding labels, shapes, controlPointOptions
* @product highstock
* @optionparent annotations.verticalLine
*/
{
typeOptions: {
/**
* @ignore
*/
yOffset: 10,
/**
* Label options.
*
* @extends annotations.crookedLine.labelOptions
*/
label: {
offset: -40,
point: function (target) {
return target.annotation.points[0];
},
allowOverlap: true,
backgroundColor: 'none',
borderWidth: 0,
crop: true,
overflow: 'none',
shape: 'rect',
text: '{y:.2f}'
},
/**
* Connector options.
*
* @extends annotations.crookedLine.shapeOptions
* @excluding height, r, type, width
*/
connector: {
strokeWidth: 1,
markerEnd: 'arrow'
}
}
});
Annotation.types.verticalLine = VerticalLine;
export default VerticalLine;

Some files were not shown because too many files have changed in this diff Show More