/** * @license Highstock JS v11.4.1 (2024-04-04) * * Advanced Highcharts Stock tools * * (c) 2010-2024 Highsoft AS * Author: Torstein Honsi * * License: www.highcharts.com/license */ (function (factory) { if (typeof module === 'object' && module.exports) { factory['default'] = factory; module.exports = factory; } else if (typeof define === 'function' && define.amd) { define('highcharts/modules/stock-tools', ['highcharts', 'highcharts/modules/stock'], function (Highcharts) { factory(Highcharts); factory.Highcharts = Highcharts; return factory; }); } else { factory(typeof Highcharts !== 'undefined' ? Highcharts : undefined); } }(function (Highcharts) { 'use strict'; var _modules = Highcharts ? Highcharts._modules : {}; function _registerModule(obj, path, args, fn) { if (!obj.hasOwnProperty(path)) { obj[path] = fn.apply(null, args); if (typeof CustomEvent === 'function') { window.dispatchEvent(new CustomEvent( 'HighchartsModuleLoaded', { detail: { path: path, module: obj[path] } } )); } } } _registerModule(_modules, 'Core/Chart/ChartNavigationComposition.js', [], function () { /** * * (c) 2010-2024 Paweł Fus * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ /* * * * Composition * * */ var ChartNavigationComposition; (function (ChartNavigationComposition) { /* * * * Declarations * * */ /* * * * Functions * * */ /* eslint-disable valid-jsdoc */ /** * @private */ function compose(chart) { if (!chart.navigation) { chart.navigation = new Additions(chart); } return chart; } ChartNavigationComposition.compose = compose; /* * * * Class * * */ /** * Initializes `chart.navigation` object which delegates `update()` methods * to all other common classes (used in exporting and navigationBindings). * @private */ class Additions { /* * * * Constructor * * */ constructor(chart) { this.updates = []; this.chart = chart; } /* * * * Functions * * */ /** * Registers an `update()` method in the `chart.navigation` object. * * @private * @param {UpdateFunction} updateFn * The `update()` method that will be called in `chart.update()`. */ addUpdate(updateFn) { this.chart.navigation.updates.push(updateFn); } /** * @private */ update(options, redraw) { this.updates.forEach((updateFn) => { updateFn.call(this.chart, options, redraw); }); } } ChartNavigationComposition.Additions = Additions; })(ChartNavigationComposition || (ChartNavigationComposition = {})); /* * * * Default Export * * */ return ChartNavigationComposition; }); _registerModule(_modules, 'Extensions/Annotations/NavigationBindingsDefaults.js', [_modules['Extensions/Annotations/NavigationBindingsUtilities.js'], _modules['Core/Utilities.js']], function (NBU, U) { /* * * * (c) 2009-2024 Highsoft, Black Label * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ const { getAssignedAxis } = NBU; const { isNumber, merge } = U; /* * * * Constants * * */ /** * @optionparent lang */ const lang = { /** * Configure the Popup strings in the chart. Requires the * `annotations.js` or `annotations-advanced.src.js` module to be * loaded. * @since 7.0.0 * @product highcharts highstock */ navigation: { /** * Translations for all field names used in popup. * * @product highcharts highstock */ popup: { simpleShapes: 'Simple shapes', lines: 'Lines', circle: 'Circle', ellipse: 'Ellipse', rectangle: 'Rectangle', label: 'Label', shapeOptions: 'Shape options', typeOptions: 'Details', fill: 'Fill', format: 'Text', strokeWidth: 'Line width', stroke: 'Line color', title: 'Title', name: 'Name', labelOptions: 'Label options', labels: 'Labels', backgroundColor: 'Background color', backgroundColors: 'Background colors', borderColor: 'Border color', borderRadius: 'Border radius', borderWidth: 'Border width', style: 'Style', padding: 'Padding', fontSize: 'Font size', color: 'Color', height: 'Height', shapes: 'Shape options' } } }; /** * @optionparent navigation * @product highcharts highstock */ const navigation = { /** * A CSS class name where all bindings will be attached to. Multiple * charts on the same page should have separate class names to prevent * duplicating events. * * Default value of versions < 7.0.4 `highcharts-bindings-wrapper` * * @since 7.0.0 * @type {string} */ bindingsClassName: 'highcharts-bindings-container', /** * Bindings definitions for custom HTML buttons. Each binding implements * simple event-driven interface: * * - `className`: classname used to bind event to * * - `init`: initial event, fired on button click * * - `start`: fired on first click on a chart * * - `steps`: array of sequential events fired one after another on each * of users clicks * * - `end`: last event to be called after last step event * * @type {Highcharts.Dictionary|*} * * @sample {highstock} stock/stocktools/stocktools-thresholds * Custom bindings * @sample {highcharts} highcharts/annotations/bindings/ * Simple binding * @sample {highcharts} highcharts/annotations/bindings-custom-annotation/ * Custom annotation binding * * @since 7.0.0 * @requires modules/annotations * @product highcharts highstock */ bindings: { /** * A circle annotation bindings. Includes `start` and one event in * `steps` array. * * @type {Highcharts.NavigationBindingsOptionsObject} * @default {"className": "highcharts-circle-annotation", "start": function() {}, "steps": [function() {}], "annotationsOptions": {}} */ circleAnnotation: { /** @ignore-option */ className: 'highcharts-circle-annotation', /** @ignore-option */ start: function (e) { const coords = this.chart.pointer?.getCoordinates(e), coordsX = coords && getAssignedAxis(coords.xAxis), coordsY = coords && getAssignedAxis(coords.yAxis), navigation = this.chart.options.navigation; // Exit if clicked out of axes area if (!coordsX || !coordsY) { return; } return this.chart.addAnnotation(merge({ langKey: 'circle', type: 'basicAnnotation', shapes: [{ type: 'circle', point: { x: coordsX.value, y: coordsY.value, xAxis: coordsX.axis.index, yAxis: coordsY.axis.index }, r: 5 }] }, navigation.annotationsOptions, navigation.bindings.circleAnnotation .annotationsOptions)); }, /** @ignore-option */ steps: [ function (e, annotation) { const shapes = annotation.options.shapes, mockPointOpts = ((shapes && shapes[0] && shapes[0].point) || {}); let distance; if (isNumber(mockPointOpts.xAxis) && isNumber(mockPointOpts.yAxis)) { const inverted = this.chart.inverted, x = this.chart.xAxis[mockPointOpts.xAxis] .toPixels(mockPointOpts.x), y = this.chart.yAxis[mockPointOpts.yAxis] .toPixels(mockPointOpts.y); distance = Math.max(Math.sqrt(Math.pow(inverted ? y - e.chartX : x - e.chartX, 2) + Math.pow(inverted ? x - e.chartY : y - e.chartY, 2)), 5); } annotation.update({ shapes: [{ r: distance }] }); } ] }, /** * A ellipse annotation bindings. Includes `start` and two events in * `steps` array. First updates the second point, responsible for a * rx width, and second updates the ry width. * * @type {Highcharts.NavigationBindingsOptionsObject} * @default {"className": "highcharts-ellipse-annotation", "start": function() {}, "steps": [function() {}], "annotationsOptions": {}} */ ellipseAnnotation: { className: 'highcharts-ellipse-annotation', start: function (e) { const coords = this.chart.pointer?.getCoordinates(e), coordsX = coords && getAssignedAxis(coords.xAxis), coordsY = coords && getAssignedAxis(coords.yAxis), navigation = this.chart.options.navigation; if (!coordsX || !coordsY) { return; } return this.chart.addAnnotation(merge({ langKey: 'ellipse', type: 'basicAnnotation', shapes: [ { type: 'ellipse', xAxis: coordsX.axis.index, yAxis: coordsY.axis.index, points: [{ x: coordsX.value, y: coordsY.value }, { x: coordsX.value, y: coordsY.value }], ry: 1 } ] }, navigation.annotationsOptions, navigation.bindings.ellipseAnnotation .annotationOptions)); }, steps: [ function (e, annotation) { const target = annotation.shapes[0], position = target.getAbsolutePosition(target.points[1]); target.translatePoint(e.chartX - position.x, e.chartY - position.y, 1); target.redraw(false); }, function (e, annotation) { const target = annotation.shapes[0], position = target.getAbsolutePosition(target.points[0]), position2 = target.getAbsolutePosition(target.points[1]), newR = target.getDistanceFromLine(position, position2, e.chartX, e.chartY), yAxis = target.getYAxis(), newRY = Math.abs(yAxis.toValue(0) - yAxis.toValue(newR)); target.setYRadius(newRY); target.redraw(false); } ] }, /** * A rectangle annotation bindings. Includes `start` and one event * in `steps` array. * * @type {Highcharts.NavigationBindingsOptionsObject} * @default {"className": "highcharts-rectangle-annotation", "start": function() {}, "steps": [function() {}], "annotationsOptions": {}} */ rectangleAnnotation: { /** @ignore-option */ className: 'highcharts-rectangle-annotation', /** @ignore-option */ start: function (e) { const coords = this.chart.pointer?.getCoordinates(e), coordsX = coords && getAssignedAxis(coords.xAxis), coordsY = coords && getAssignedAxis(coords.yAxis); // Exit if clicked out of axes area if (!coordsX || !coordsY) { return; } const x = coordsX.value, y = coordsY.value, xAxis = coordsX.axis.index, yAxis = coordsY.axis.index, navigation = this.chart.options.navigation; return this.chart.addAnnotation(merge({ langKey: 'rectangle', type: 'basicAnnotation', shapes: [{ type: 'path', points: [ { xAxis, yAxis, x, y }, { xAxis, yAxis, x, y }, { xAxis, yAxis, x, y }, { xAxis, yAxis, x, y }, { command: 'Z' } ] }] }, navigation .annotationsOptions, navigation .bindings .rectangleAnnotation .annotationsOptions)); }, /** @ignore-option */ steps: [ function (e, annotation) { const shapes = annotation.options.shapes, points = ((shapes && shapes[0] && shapes[0].points) || []), coords = this.chart.pointer?.getCoordinates(e), coordsX = coords && getAssignedAxis(coords.xAxis), coordsY = coords && getAssignedAxis(coords.yAxis); if (coordsX && coordsY) { const x = coordsX.value, y = coordsY.value; // 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.update({ shapes: [{ points: points }] }); } } ] }, /** * A label annotation bindings. Includes `start` event only. * * @type {Highcharts.NavigationBindingsOptionsObject} * @default {"className": "highcharts-label-annotation", "start": function() {}, "steps": [function() {}], "annotationsOptions": {}} */ labelAnnotation: { /** @ignore-option */ className: 'highcharts-label-annotation', /** @ignore-option */ start: function (e) { const coords = this.chart.pointer?.getCoordinates(e), coordsX = coords && getAssignedAxis(coords.xAxis), coordsY = coords && getAssignedAxis(coords.yAxis), navigation = this.chart.options.navigation; // Exit if clicked out of axes area if (!coordsX || !coordsY) { return; } return this.chart.addAnnotation(merge({ langKey: 'label', type: 'basicAnnotation', labelOptions: { format: '{y:.2f}', overflow: 'none', crop: true }, labels: [{ point: { xAxis: coordsX.axis.index, yAxis: coordsY.axis.index, x: coordsX.value, y: coordsY.value } }] }, navigation .annotationsOptions, navigation .bindings .labelAnnotation .annotationsOptions)); } } }, /** * Path where Highcharts will look for icons. Change this to use icons * from a different server. * * @type {string} * @default https://code.highcharts.com/11.4.1/gfx/stock-icons/ * @since 7.1.3 * @apioption navigation.iconsURL */ /** * A `showPopup` event. Fired when selecting for example an annotation. * * @type {Function} * @apioption navigation.events.showPopup */ /** * A `closePopup` event. Fired when Popup should be hidden, for example * when clicking on an annotation again. * * @type {Function} * @apioption navigation.events.closePopup */ /** * Event fired on a button click. * * @type {Function} * @sample highcharts/annotations/gui/ * Change icon in a dropddown on event * @sample highcharts/annotations/gui-buttons/ * Change button class on event * @apioption navigation.events.selectButton */ /** * Event fired when button state should change, for example after * adding an annotation. * * @type {Function} * @sample highcharts/annotations/gui/ * Change icon in a dropddown on event * @sample highcharts/annotations/gui-buttons/ * Change button class on event * @apioption navigation.events.deselectButton */ /** * Events to communicate between Stock Tools and custom GUI. * * @since 7.0.0 * @product highcharts highstock * @optionparent navigation.events */ events: {}, /** * Additional options to be merged into all annotations. * * @sample stock/stocktools/navigation-annotation-options * Set red color of all line annotations * * @type {Highcharts.AnnotationsOptions} * @extends annotations * @exclude crookedLine, elliottWave, fibonacci, infinityLine, * measure, pitchfork, tunnel, verticalLine, basicAnnotation * @requires modules/annotations * @apioption navigation.annotationsOptions */ annotationsOptions: { animation: { defer: 0 } } }; /* * * * Default Export * * */ const NavigationBindingDefaults = { lang, navigation }; return NavigationBindingDefaults; }); _registerModule(_modules, 'Extensions/Annotations/NavigationBindings.js', [_modules['Core/Chart/ChartNavigationComposition.js'], _modules['Core/Defaults.js'], _modules['Core/Templating.js'], _modules['Core/Globals.js'], _modules['Extensions/Annotations/NavigationBindingsDefaults.js'], _modules['Extensions/Annotations/NavigationBindingsUtilities.js'], _modules['Core/Utilities.js']], function (ChartNavigationComposition, D, F, H, NavigationBindingDefaults, NBU, U) { /* * * * (c) 2009-2024 Highsoft, Black Label * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ const { setOptions } = D; const { format } = F; const { composed, doc, win } = H; const { getAssignedAxis, getFieldType } = NBU; const { addEvent, attr, defined, fireEvent, isArray, isFunction, isNumber, isObject, merge, objectEach, pick, pushUnique } = U; /* * * * Functions * * */ /** * IE 9-11 polyfill for Element.closest(): * @private */ function closestPolyfill(el, s) { const ElementProto = win.Element.prototype, elementMatches = ElementProto.matches || ElementProto.msMatchesSelector || ElementProto.webkitMatchesSelector; let ret = null; if (ElementProto.closest) { ret = ElementProto.closest.call(el, s); } else { do { if (elementMatches.call(el, s)) { return el; } el = el.parentElement || el.parentNode; } while (el !== null && el.nodeType === 1); } return ret; } /** * @private */ function onAnnotationRemove() { if (this.chart.navigationBindings) { this.chart.navigationBindings.deselectAnnotation(); } } /** * @private */ function onChartDestroy() { if (this.navigationBindings) { this.navigationBindings.destroy(); } } /** * @private */ function onChartLoad() { const options = this.options; if (options && options.navigation && options.navigation.bindings) { this.navigationBindings = new NavigationBindings(this, options.navigation); this.navigationBindings.initEvents(); this.navigationBindings.initUpdate(); } } /** * @private */ function onChartRender() { const navigationBindings = this.navigationBindings, disabledClassName = 'highcharts-disabled-btn'; if (this && navigationBindings) { // Check if the buttons should be enabled/disabled based on // visible series. let buttonsEnabled = false; this.series.forEach((series) => { if (!series.options.isInternal && series.visible) { buttonsEnabled = true; } }); if (this.navigationBindings && this.navigationBindings.container && this.navigationBindings.container[0]) { const container = this.navigationBindings.container[0]; objectEach(navigationBindings.boundClassNames, (value, key) => { // Get the HTML element corresponding to the className taken // from StockToolsBindings. const buttonNode = container.querySelectorAll('.' + key); if (buttonNode) { for (let i = 0; i < buttonNode.length; i++) { const button = buttonNode[i], cls = button.className; if (value.noDataState === 'normal') { // If button has noDataState: 'normal', and has // disabledClassName, remove this className. if (cls.indexOf(disabledClassName) !== -1) { button.classList.remove(disabledClassName); } } else if (!buttonsEnabled) { if (cls.indexOf(disabledClassName) === -1) { button.className += ' ' + disabledClassName; } } else { // Enable all buttons by deleting the className. if (cls.indexOf(disabledClassName) !== -1) { button.classList.remove(disabledClassName); } } } } }); } } } /** * @private */ function onNavigationBindingsClosePopup() { this.deselectAnnotation(); } /** * @private */ function onNavigationBindingsDeselectButton() { this.selectedButtonElement = null; } /** * Show edit-annotation form: * @private */ function selectableAnnotation(annotationType) { const originalClick = annotationType.prototype.defaultOptions.events && annotationType.prototype.defaultOptions.events.click; /** * Select and show popup * @private */ function selectAndShowPopup(eventArguments) { const annotation = this, navigation = annotation.chart.navigationBindings, prevAnnotation = navigation.activeAnnotation; if (originalClick) { originalClick.call(annotation, eventArguments); } if (prevAnnotation !== annotation) { // Select current: navigation.deselectAnnotation(); navigation.activeAnnotation = annotation; annotation.setControlPointsVisibility(true); fireEvent(navigation, 'showPopup', { annotation: annotation, formType: 'annotation-toolbar', options: navigation.annotationToFields(annotation), onSubmit: function (data) { if (data.actionType === 'remove') { navigation.activeAnnotation = false; navigation.chart.removeAnnotation(annotation); } else { const config = {}; navigation.fieldsToOptions(data.fields, config); navigation.deselectAnnotation(); const typeOptions = config.typeOptions; if (annotation.options.type === 'measure') { // Manually disable crooshars according to // stroke width of the shape: typeOptions.crosshairY.enabled = (typeOptions.crosshairY .strokeWidth !== 0); typeOptions.crosshairX.enabled = (typeOptions.crosshairX .strokeWidth !== 0); } annotation.update(config); } } }); } else { // Deselect current: fireEvent(navigation, 'closePopup'); } // Let bubble event to chart.click: eventArguments.activeAnnotation = true; } // #18276, show popup on touchend, but not on touchmove let touchStartX, touchStartY; /** * */ function saveCoords(e) { touchStartX = e.touches[0].clientX; touchStartY = e.touches[0].clientY; } /** * */ function checkForTouchmove(e) { const hasMoved = touchStartX ? Math.sqrt(Math.pow(touchStartX - e.changedTouches[0].clientX, 2) + Math.pow(touchStartY - e.changedTouches[0].clientY, 2)) >= 4 : false; if (!hasMoved) { selectAndShowPopup.call(this, e); } } merge(true, annotationType.prototype.defaultOptions.events, { click: selectAndShowPopup, touchstart: saveCoords, touchend: checkForTouchmove }); } /* * * * Class * * */ /** * @private */ class NavigationBindings { /* * * * Static Functions * * */ static compose(AnnotationClass, ChartClass) { if (pushUnique(composed, 'NavigationBindings')) { addEvent(AnnotationClass, 'remove', onAnnotationRemove); // Basic shapes: selectableAnnotation(AnnotationClass); // Advanced annotations: objectEach(AnnotationClass.types, (annotationType) => { selectableAnnotation(annotationType); }); addEvent(ChartClass, 'destroy', onChartDestroy); addEvent(ChartClass, 'load', onChartLoad); addEvent(ChartClass, 'render', onChartRender); addEvent(NavigationBindings, 'closePopup', onNavigationBindingsClosePopup); addEvent(NavigationBindings, 'deselectButton', onNavigationBindingsDeselectButton); setOptions(NavigationBindingDefaults); } } /* * * * Constructor * * */ constructor(chart, options) { this.boundClassNames = void 0; this.chart = chart; this.options = options; this.eventsToUnbind = []; this.container = this.chart.container.getElementsByClassName(this.options.bindingsClassName || ''); if (!this.container.length) { this.container = doc.getElementsByClassName(this.options.bindingsClassName || ''); } } /* * * * Functions * * */ getCoords(e) { const coords = this.chart.pointer?.getCoordinates(e); return [ coords && getAssignedAxis(coords.xAxis), coords && getAssignedAxis(coords.yAxis) ]; } /** * Init all events connected to NavigationBindings. * * @private * @function Highcharts.NavigationBindings#initEvents */ initEvents() { const navigation = this, chart = navigation.chart, bindingsContainer = navigation.container, options = navigation.options; // Shorthand object for getting events for buttons: navigation.boundClassNames = {}; objectEach((options.bindings || {}), (value) => { navigation.boundClassNames[value.className] = value; }); // Handle multiple containers with the same class names: [].forEach.call(bindingsContainer, (subContainer) => { navigation.eventsToUnbind.push(addEvent(subContainer, 'click', (event) => { const bindings = navigation.getButtonEvents(subContainer, event); if (bindings && (!bindings.button.classList .contains('highcharts-disabled-btn'))) { navigation.bindingsButtonClick(bindings.button, bindings.events, event); } })); }); objectEach((options.events || {}), (callback, eventName) => { if (isFunction(callback)) { navigation.eventsToUnbind.push(addEvent(navigation, eventName, callback, { passive: false })); } }); navigation.eventsToUnbind.push(addEvent(chart.container, 'click', function (e) { if (!chart.cancelClick && chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop, { visiblePlotOnly: true })) { navigation.bindingsChartClick(this, e); } })); navigation.eventsToUnbind.push(addEvent(chart.container, H.isTouchDevice ? 'touchmove' : 'mousemove', function (e) { navigation.bindingsContainerMouseMove(this, e); }, H.isTouchDevice ? { passive: false } : void 0)); } /** * Common chart.update() delegation, shared between bindings and exporting. * * @private * @function Highcharts.NavigationBindings#initUpdate */ initUpdate() { const navigation = this; ChartNavigationComposition .compose(this.chart).navigation .addUpdate((options) => { navigation.update(options); }); } /** * Hook for click on a button, method selects/unselects buttons, * then calls `bindings.init` callback. * * @private * @function Highcharts.NavigationBindings#bindingsButtonClick * * @param {Highcharts.HTMLDOMElement} [button] * Clicked button * * @param {Object} events * Events passed down from bindings (`init`, `start`, `step`, `end`) * * @param {Highcharts.PointerEventObject} clickEvent * Browser's click event */ bindingsButtonClick(button, events, clickEvent) { const navigation = this, chart = navigation.chart, svgContainer = chart.renderer.boxWrapper; let shouldEventBeFired = true; if (navigation.selectedButtonElement) { if (navigation.selectedButtonElement.classList === button.classList) { shouldEventBeFired = false; } fireEvent(navigation, 'deselectButton', { button: navigation.selectedButtonElement }); if (navigation.nextEvent) { // Remove in-progress annotations adders: if (navigation.currentUserDetails && navigation.currentUserDetails.coll === 'annotations') { chart.removeAnnotation(navigation.currentUserDetails); } navigation.mouseMoveEvent = navigation.nextEvent = false; } } if (shouldEventBeFired) { navigation.selectedButton = events; navigation.selectedButtonElement = button; fireEvent(navigation, 'selectButton', { button: button }); // Call "init" event, for example to open modal window if (events.init) { events.init.call(navigation, button, clickEvent); } if (events.start || events.steps) { chart.renderer.boxWrapper.addClass('highcharts-draw-mode'); } } else { chart.stockTools && chart.stockTools.toggleButtonActiveClass(button); svgContainer.removeClass('highcharts-draw-mode'); navigation.nextEvent = false; navigation.mouseMoveEvent = false; navigation.selectedButton = null; } } /** * Hook for click on a chart, first click on a chart calls `start` event, * then on all subsequent clicks iterate over `steps` array. * When finished, calls `end` event. * * @private * @function Highcharts.NavigationBindings#bindingsChartClick * * @param {Highcharts.Chart} chart * Chart that click was performed on. * * @param {Highcharts.PointerEventObject} clickEvent * Browser's click event. */ bindingsChartClick(chart, clickEvent) { chart = this.chart; const navigation = this, activeAnnotation = navigation.activeAnnotation, selectedButton = navigation.selectedButton, svgContainer = chart.renderer.boxWrapper; if (activeAnnotation) { // Click outside popups, should close them and deselect the // annotation if (!activeAnnotation.cancelClick && // #15729 !clickEvent.activeAnnotation && // Element could be removed in the child action, e.g. button clickEvent.target.parentNode && // TO DO: Polyfill for IE11? !closestPolyfill(clickEvent.target, '.highcharts-popup')) { fireEvent(navigation, 'closePopup'); } else if (activeAnnotation.cancelClick) { // Reset cancelClick after the other event handlers have run setTimeout(() => { activeAnnotation.cancelClick = false; }, 0); } } if (!selectedButton || !selectedButton.start) { return; } if (!navigation.nextEvent) { // Call init method: navigation.currentUserDetails = selectedButton.start.call(navigation, clickEvent); // If steps exists (e.g. Annotations), bind them: if (navigation.currentUserDetails && selectedButton.steps) { navigation.stepIndex = 0; navigation.steps = true; navigation.mouseMoveEvent = navigation.nextEvent = selectedButton.steps[navigation.stepIndex]; } else { fireEvent(navigation, 'deselectButton', { button: navigation.selectedButtonElement }); svgContainer.removeClass('highcharts-draw-mode'); navigation.steps = false; navigation.selectedButton = null; // First click is also the last one: if (selectedButton.end) { selectedButton.end.call(navigation, clickEvent, navigation.currentUserDetails); } } } else { navigation.nextEvent(clickEvent, navigation.currentUserDetails); if (navigation.steps) { navigation.stepIndex++; if (selectedButton.steps[navigation.stepIndex]) { // If we have more steps, bind them one by one: navigation.mouseMoveEvent = navigation.nextEvent = selectedButton.steps[navigation.stepIndex]; } else { fireEvent(navigation, 'deselectButton', { button: navigation.selectedButtonElement }); svgContainer.removeClass('highcharts-draw-mode'); // That was the last step, call end(): if (selectedButton.end) { selectedButton.end.call(navigation, clickEvent, navigation.currentUserDetails); } navigation.nextEvent = false; navigation.mouseMoveEvent = false; navigation.selectedButton = null; } } } } /** * Hook for mouse move on a chart's container. It calls current step. * * @private * @function Highcharts.NavigationBindings#bindingsContainerMouseMove * * @param {Highcharts.HTMLDOMElement} container * Chart's container. * * @param {global.Event} moveEvent * Browser's move event. */ bindingsContainerMouseMove(_container, moveEvent) { if (this.mouseMoveEvent) { this.mouseMoveEvent(moveEvent, this.currentUserDetails); } } /** * Translate fields (e.g. `params.period` or `marker.styles.color`) to * Highcharts options object (e.g. `{ params: { period } }`). * * @private * @function Highcharts.NavigationBindings#fieldsToOptions * * @param {Highcharts.Dictionary} fields * Fields from popup form. * * @param {T} config * Default config to be modified. * * @return {T} * Modified config */ fieldsToOptions(fields, config) { objectEach(fields, (value, field) => { const parsedValue = parseFloat(value), path = field.split('.'), pathLength = path.length - 1; // If it's a number (not "format" options), parse it: if (isNumber(parsedValue) && !value.match(/px|em/g) && !field.match(/format/g)) { value = parsedValue; } // Remove values like 0 if (value !== 'undefined') { let parent = config; path.forEach((name, index) => { if (name !== '__proto__' && name !== 'constructor') { const nextName = pick(path[index + 1], ''); if (pathLength === index) { // Last index, put value: parent[name] = value; } else if (!parent[name]) { // Create middle property: parent[name] = nextName.match(/\d/g) ? [] : {}; parent = parent[name]; } else { // Jump into next property parent = parent[name]; } } }); } }); return config; } /** * Shorthand method to deselect an annotation. * * @function Highcharts.NavigationBindings#deselectAnnotation */ deselectAnnotation() { if (this.activeAnnotation) { this.activeAnnotation.setControlPointsVisibility(false); this.activeAnnotation = false; } } /** * Generates API config for popup in the same format as options for * Annotation object. * * @function Highcharts.NavigationBindings#annotationToFields * * @param {Highcharts.Annotation} annotation * Annotations object * * @return {Highcharts.Dictionary} * Annotation options to be displayed in popup box */ annotationToFields(annotation) { const options = annotation.options, editables = NavigationBindings.annotationsEditable, nestedEditables = editables.nestedOptions, type = pick(options.type, options.shapes && options.shapes[0] && options.shapes[0].type, options.labels && options.labels[0] && options.labels[0].type, 'label'), nonEditables = NavigationBindings.annotationsNonEditable[options.langKey] || [], visualOptions = { langKey: options.langKey, type: type }; /** * Nested options traversing. Method goes down to the options and copies * allowed options (with values) to new object, which is last parameter: * "parent". * * @private * * @param {*} option * Atomic type or object/array * * @param {string} key * Option name, for example "visible" or "x", "y" * * @param {Object} parentEditables * Editables from NavigationBindings.annotationsEditable * * @param {Object} parent * Where new options will be assigned */ function traverse(option, key, parentEditables, parent, parentKey) { let nextParent; if (parentEditables && defined(option) && nonEditables.indexOf(key) === -1 && ((parentEditables.indexOf && parentEditables.indexOf(key)) >= 0 || parentEditables[key] || // Nested array parentEditables === true // Simple array )) { // Roots: if (isArray(option)) { parent[key] = []; option.forEach((arrayOption, i) => { if (!isObject(arrayOption)) { // Simple arrays, e.g. [String, Number, Boolean] traverse(arrayOption, 0, nestedEditables[key], parent[key], key); } else { // Advanced arrays, e.g. [Object, Object] parent[key][i] = {}; objectEach(arrayOption, (nestedOption, nestedKey) => { traverse(nestedOption, nestedKey, nestedEditables[key], parent[key][i], key); }); } }); } else if (isObject(option)) { nextParent = {}; if (isArray(parent)) { parent.push(nextParent); nextParent[key] = {}; nextParent = nextParent[key]; } else { parent[key] = nextParent; } objectEach(option, (nestedOption, nestedKey) => { traverse(nestedOption, nestedKey, key === 0 ? parentEditables : nestedEditables[key], nextParent, key); }); } else { // Leaf: if (key === 'format') { parent[key] = [ format(option, annotation.labels[0].points[0]).toString(), 'text' ]; } else if (isArray(parent)) { parent.push([option, getFieldType(parentKey, option)]); } else { parent[key] = [option, getFieldType(key, option)]; } } } } objectEach(options, (option, key) => { if (key === 'typeOptions') { visualOptions[key] = {}; objectEach(options[key], (typeOption, typeKey) => { traverse(typeOption, typeKey, nestedEditables, visualOptions[key], typeKey); }); } else { traverse(option, key, editables[type], visualOptions, key); } }); return visualOptions; } /** * Get all class names for all parents in the element. Iterates until finds * main container. * * @private * @function Highcharts.NavigationBindings#getClickedClassNames * * @param {Highcharts.HTMLDOMElement} container * Container that event is bound to. * * @param {global.Event} event * Browser's event. * * @return {Array>} * Array of class names with corresponding elements */ getClickedClassNames(container, event) { let element = event.target, classNames = [], elemClassName; while (element && element.tagName) { elemClassName = attr(element, 'class'); if (elemClassName) { classNames = classNames.concat(elemClassName .split(' ') // eslint-disable-next-line no-loop-func .map((name) => ([name, element]))); } element = element.parentNode; if (element === container) { return classNames; } } return classNames; } /** * Get events bound to a button. It's a custom event delegation to find all * events connected to the element. * * @private * @function Highcharts.NavigationBindings#getButtonEvents * * @param {Highcharts.HTMLDOMElement} container * Container that event is bound to. * * @param {global.Event} event * Browser's event. * * @return {Object} * Object with events (init, start, steps, and end) */ getButtonEvents(container, event) { const navigation = this, classNames = this.getClickedClassNames(container, event); let bindings; classNames.forEach((className) => { if (navigation.boundClassNames[className[0]] && !bindings) { bindings = { events: navigation.boundClassNames[className[0]], button: className[1] }; } }); return bindings; } /** * Bindings are just events, so the whole update process is simply * removing old events and adding new ones. * * @private * @function Highcharts.NavigationBindings#update */ update(options) { this.options = merge(true, this.options, options); this.removeEvents(); this.initEvents(); } /** * Remove all events created in the navigation. * * @private * @function Highcharts.NavigationBindings#removeEvents */ removeEvents() { this.eventsToUnbind.forEach((unbinder) => unbinder()); } /** * @private * @function Highcharts.NavigationBindings#destroy */ destroy() { this.removeEvents(); } } /* * * * Static Properties * * */ // Define which options from annotations should show up in edit box: NavigationBindings.annotationsEditable = { // `typeOptions` are always available // Nested and shared options: nestedOptions: { labelOptions: ['style', 'format', 'backgroundColor'], labels: ['style'], label: ['style'], style: ['fontSize', 'color'], background: ['fill', 'strokeWidth', 'stroke'], innerBackground: ['fill', 'strokeWidth', 'stroke'], outerBackground: ['fill', 'strokeWidth', 'stroke'], shapeOptions: ['fill', 'strokeWidth', 'stroke'], shapes: ['fill', 'strokeWidth', 'stroke'], line: ['strokeWidth', 'stroke'], backgroundColors: [true], connector: ['fill', 'strokeWidth', 'stroke'], crosshairX: ['strokeWidth', 'stroke'], crosshairY: ['strokeWidth', 'stroke'] }, // Simple shapes: circle: ['shapes'], ellipse: ['shapes'], verticalLine: [], label: ['labelOptions'], // Measure measure: ['background', 'crosshairY', 'crosshairX'], // Others: fibonacci: [], tunnel: ['background', 'line', 'height'], pitchfork: ['innerBackground', 'outerBackground'], rect: ['shapes'], // Crooked lines, elliots, arrows etc: crookedLine: [], basicAnnotation: ['shapes', 'labelOptions'] }; // Define non editable fields per annotation, for example Rectangle inherits // options from Measure, but crosshairs are not available NavigationBindings.annotationsNonEditable = { rectangle: ['crosshairX', 'crosshairY', 'labelOptions'], ellipse: ['labelOptions'], circle: ['labelOptions'] }; /* * * * Default Export * * */ /* * * * API Declarations * * */ /** * A config object for navigation bindings in annotations. * * @interface Highcharts.NavigationBindingsOptionsObject */ /** * ClassName of the element for a binding. * @name Highcharts.NavigationBindingsOptionsObject#className * @type {string|undefined} */ /** * Last event to be fired after last step event. * @name Highcharts.NavigationBindingsOptionsObject#end * @type {Function|undefined} */ /** * Initial event, fired on a button click. * @name Highcharts.NavigationBindingsOptionsObject#init * @type {Function|undefined} */ /** * Event fired on first click on a chart. * @name Highcharts.NavigationBindingsOptionsObject#start * @type {Function|undefined} */ /** * Last event to be fired after last step event. Array of step events to be * called sequentially after each user click. * @name Highcharts.NavigationBindingsOptionsObject#steps * @type {Array|undefined} */ (''); // Keeps doclets above in JS file return NavigationBindings; }); _registerModule(_modules, 'Stock/StockTools/StockToolsUtilities.js', [_modules['Core/Defaults.js'], _modules['Extensions/Annotations/NavigationBindingsUtilities.js'], _modules['Core/Series/Series.js'], _modules['Core/Utilities.js']], function (D, NBU, Series, U) { /** * * Events generator for Stock tools * * (c) 2009-2024 Paweł Fus * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ const { getOptions } = D; const { getAssignedAxis, getFieldType } = NBU; const { defined, fireEvent, isNumber, uniqueKey } = U; /* * * * Constants * * */ /** * @private */ const indicatorsWithAxes = [ 'apo', 'ad', 'aroon', 'aroonoscillator', 'atr', 'ao', 'cci', 'chaikin', 'cmf', 'cmo', 'disparityindex', 'dmi', 'dpo', 'linearRegressionAngle', 'linearRegressionIntercept', 'linearRegressionSlope', 'klinger', 'macd', 'mfi', 'momentum', 'natr', 'obv', 'ppo', 'roc', 'rsi', 'slowstochastic', 'stochastic', 'trix', 'williamsr' ]; /** * @private */ const indicatorsWithVolume = [ 'ad', 'cmf', 'klinger', 'mfi', 'obv', 'vbp', 'vwap' ]; /* * * * Functions * * */ /** * Generates function which will add a flag series using modal in GUI. * Method fires an event "showPopup" with config: * `{type, options, callback}`. * * Example: NavigationBindings.utils.addFlagFromForm('url(...)') - will * generate function that shows modal in GUI. * * @private * @function bindingsUtils.addFlagFromForm * * @param {Highcharts.FlagsShapeValue} type * Type of flag series, e.g. "squarepin" * * @return {Function} * Callback to be used in `start` callback */ function addFlagFromForm(type) { return function (e) { const navigation = this, chart = navigation.chart, toolbar = chart.stockTools, point = attractToPoint(e, chart); if (!point) { return; } const pointConfig = { x: point.x, y: point.y }; const seriesOptions = { type: 'flags', onSeries: point.series.id, shape: type, data: [pointConfig], xAxis: point.xAxis, yAxis: point.yAxis, point: { events: { click: function () { const point = this, options = point.options; fireEvent(navigation, 'showPopup', { point: point, formType: 'annotation-toolbar', options: { langKey: 'flags', type: 'flags', title: [ options.title, getFieldType('title', options.title) ], name: [ options.name, getFieldType('name', options.name) ] }, onSubmit: function (updated) { if (updated.actionType === 'remove') { point.remove(); } else { point.update(navigation.fieldsToOptions(updated.fields, {})); } } }); } } } }; if (!toolbar || !toolbar.guiEnabled) { chart.addSeries(seriesOptions); } fireEvent(navigation, 'showPopup', { formType: 'flag', // Enabled options: options: { langKey: 'flags', type: 'flags', title: ['A', getFieldType('label', 'A')], name: ['Flag A', getFieldType('label', 'Flag A')] }, // Callback on submit: onSubmit: function (data) { navigation.fieldsToOptions(data.fields, seriesOptions.data[0]); chart.addSeries(seriesOptions); } }); }; } /** * @private * @todo * Consider using getHoverData(), but always kdTree (columns?) */ function attractToPoint(e, chart) { const coords = chart.pointer?.getCoordinates(e); let coordsX, coordsY, distX = Number.MAX_VALUE, closestPoint; if (chart.navigationBindings && coords) { coordsX = getAssignedAxis(coords.xAxis); coordsY = getAssignedAxis(coords.yAxis); } // Exit if clicked out of axes area. if (!coordsX || !coordsY) { return; } const x = coordsX.value; const y = coordsY.value; // Search by 'x' but only in yAxis' series. coordsY.axis.series.forEach((series) => { if (series.points) { const point = series.searchPoint(e, true); if (point && distX > Math.abs(point.x - x)) { distX = Math.abs(point.x - x); closestPoint = point; } } }); if (closestPoint && closestPoint.x && closestPoint.y) { return { x: closestPoint.x, y: closestPoint.y, below: y < closestPoint.y, series: closestPoint.series, xAxis: closestPoint.series.xAxis.index || 0, yAxis: closestPoint.series.yAxis.index || 0 }; } } /** * Shorthand to check if given yAxis comes from navigator. * * @private * @function bindingsUtils.isNotNavigatorYAxis * * @param {Highcharts.Axis} axis * Axis to check. * * @return {boolean} * True, if axis comes from navigator. */ function isNotNavigatorYAxis(axis) { return axis.userOptions.className !== 'highcharts-navigator-yaxis'; } /** * Check if any of the price indicators are enabled. * @private * @function bindingsUtils.isLastPriceEnabled * * @param {Array} series * Array of series. * * @return {boolean} * Tells which indicator is enabled. */ function isPriceIndicatorEnabled(series) { return series.some((s) => s.lastVisiblePrice || s.lastPrice); } /** * @private */ function manageIndicators(data) { const chart = this.chart, seriesConfig = { linkedTo: data.linkedTo, type: data.type }; let yAxis, parentSeries, defaultOptions, series; if (data.actionType === 'edit') { this.fieldsToOptions(data.fields, seriesConfig); series = chart.get(data.seriesId); if (series) { series.update(seriesConfig, false); } } else if (data.actionType === 'remove') { series = chart.get(data.seriesId); if (series) { yAxis = series.yAxis; if (series.linkedSeries) { series.linkedSeries.forEach((linkedSeries) => { linkedSeries.remove(false); }); } series.remove(false); if (indicatorsWithAxes.indexOf(series.type) >= 0) { const removedYAxisProps = { height: yAxis.options.height, top: yAxis.options.top }; yAxis.remove(false); this.resizeYAxes(removedYAxisProps); } } } else { seriesConfig.id = uniqueKey(); this.fieldsToOptions(data.fields, seriesConfig); parentSeries = chart.get(seriesConfig.linkedTo); defaultOptions = getOptions().plotOptions; // Make sure that indicator uses the SUM approx if SUM approx is used // by parent series (#13950). if (typeof parentSeries !== 'undefined' && parentSeries instanceof Series && parentSeries.getDGApproximation() === 'sum' && // If indicator has defined approx type, use it (e.g. "ranges") !defined(defaultOptions && defaultOptions[seriesConfig.type] && defaultOptions.dataGrouping && defaultOptions.dataGrouping.approximation)) { seriesConfig.dataGrouping = { approximation: 'sum' }; } if (indicatorsWithAxes.indexOf(data.type) >= 0) { yAxis = chart.addAxis({ id: uniqueKey(), offset: 0, opposite: true, title: { text: '' }, tickPixelInterval: 40, showLastLabel: false, labels: { align: 'left', y: -2 } }, false, false); seriesConfig.yAxis = yAxis.options.id; this.resizeYAxes(); } else { seriesConfig.yAxis = chart.get(data.linkedTo).options.yAxis; } if (indicatorsWithVolume.indexOf(data.type) >= 0) { seriesConfig.params.volumeSeriesID = chart.series.filter(function (series) { return series.options.type === 'column'; })[0].options.id; } chart.addSeries(seriesConfig, false); } fireEvent(this, 'deselectButton', { button: this.selectedButtonElement }); chart.redraw(); } /** * Update height for an annotation. Height is calculated as a difference * between last point in `typeOptions` and current position. It's a value, * not pixels height. * * @private * @function bindingsUtils.updateHeight * * @param {Highcharts.PointerEventObject} e * normalized browser event * * @param {Highcharts.Annotation} annotation * Annotation to be updated */ function updateHeight(e, annotation) { const options = annotation.options.typeOptions, yAxis = isNumber(options.yAxis) && this.chart.yAxis[options.yAxis]; if (yAxis && options.points) { annotation.update({ typeOptions: { height: yAxis.toValue(e[yAxis.horiz ? 'chartX' : 'chartY']) - (options.points[1].y || 0) } }); } } /** * Update each point after specified index, most of the annotations use * this. For example crooked line: logic behind updating each point is the * same, only index changes when adding an annotation. * * Example: NavigationBindings.utils.updateNthPoint(1) - will generate * function that updates all consecutive points except point with index=0. * * @private * @function bindingsUtils.updateNthPoint * * @param {number} startIndex * Index from which point should update * * @return {Function} * Callback to be used in steps array */ function updateNthPoint(startIndex) { return function (e, annotation) { const options = annotation.options.typeOptions, xAxis = isNumber(options.xAxis) && this.chart.xAxis[options.xAxis], yAxis = isNumber(options.yAxis) && this.chart.yAxis[options.yAxis]; if (xAxis && yAxis) { options.points.forEach((point, index) => { if (index >= startIndex) { point.x = xAxis.toValue(e[xAxis.horiz ? 'chartX' : 'chartY']); point.y = yAxis.toValue(e[yAxis.horiz ? 'chartX' : 'chartY']); } }); annotation.update({ typeOptions: { points: options.points } }); } }; } /** * Update size of background (rect) in some annotations: Measure, Simple * Rect. * * @private * @function Highcharts.NavigationBindingsUtilsObject.updateRectSize * * @param {Highcharts.PointerEventObject} event * Normalized browser event * * @param {Highcharts.Annotation} annotation * Annotation to be updated */ function updateRectSize(event, annotation) { const chart = annotation.chart, options = annotation.options.typeOptions, xAxis = isNumber(options.xAxis) && chart.xAxis[options.xAxis], yAxis = isNumber(options.yAxis) && chart.yAxis[options.yAxis]; if (xAxis && yAxis) { const x = xAxis.toValue(event[xAxis.horiz ? 'chartX' : 'chartY']), y = yAxis.toValue(event[yAxis.horiz ? 'chartX' : 'chartY']), width = x - options.point.x, height = options.point.y - y; annotation.update({ typeOptions: { background: { width: chart.inverted ? height : width, height: chart.inverted ? width : height } } }); } } /* * * * Default Export * * */ const StockToolsUtilities = { indicatorsWithAxes, indicatorsWithVolume, addFlagFromForm, attractToPoint, getAssignedAxis, isNotNavigatorYAxis, isPriceIndicatorEnabled, manageIndicators, updateHeight, updateNthPoint, updateRectSize }; return StockToolsUtilities; }); _registerModule(_modules, 'Stock/StockTools/StockToolsBindings.js', [_modules['Core/Globals.js'], _modules['Stock/StockTools/StockToolsUtilities.js'], _modules['Core/Utilities.js']], function (H, STU, U) { /** * * Events generator for Stock tools * * (c) 2009-2024 Paweł Fus * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ const { addFlagFromForm, attractToPoint, isNotNavigatorYAxis, isPriceIndicatorEnabled, manageIndicators, updateHeight, updateNthPoint, updateRectSize } = STU; const { fireEvent, merge } = U; /* * * * Constants * * */ /** * @sample {highstock} stock/stocktools/custom-stock-tools-bindings * Custom stock tools bindings * * @type {Highcharts.Dictionary} * @since 7.0.0 * @optionparent navigation.bindings */ const StockToolsBindings = { // Line type annotations: /** * A segment annotation bindings. Includes `start` and one event in `steps` * array. * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "highcharts-segment", "start": function() {}, "steps": [function() {}], "annotationsOptions": {}} */ segment: { /** @ignore-option */ className: 'highcharts-segment', // eslint-disable-next-line valid-jsdoc /** @ignore-option */ start: function (e) { const [coordsX, coordsY] = this.getCoords(e); // Exit if clicked out of axes area if (!coordsX || !coordsY) { return; } const navigation = this.chart.options.navigation, options = merge({ langKey: 'segment', type: 'crookedLine', typeOptions: { xAxis: coordsX.axis.index, yAxis: coordsY.axis.index, points: [{ x: coordsX.value, y: coordsY.value }, { x: coordsX.value, y: coordsY.value }] } }, navigation.annotationsOptions, navigation.bindings.segment.annotationsOptions); return this.chart.addAnnotation(options); }, /** @ignore-option */ steps: [ updateNthPoint(1) ] }, /** * A segment with an arrow annotation bindings. Includes `start` and one * event in `steps` array. * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "highcharts-arrow-segment", "start": function() {}, "steps": [function() {}], "annotationsOptions": {}} */ arrowSegment: { /** @ignore-option */ className: 'highcharts-arrow-segment', // eslint-disable-next-line valid-jsdoc /** @ignore-option */ start: function (e) { const [coordsX, coordsY] = this.getCoords(e); // Exit if clicked out of axes area if (!coordsX || !coordsY) { return; } const navigation = this.chart.options.navigation, options = merge({ langKey: 'arrowSegment', type: 'crookedLine', typeOptions: { line: { markerEnd: 'arrow' }, xAxis: coordsX.axis.index, yAxis: coordsY.axis.index, points: [{ x: coordsX.value, y: coordsY.value }, { x: coordsX.value, y: coordsY.value }] } }, navigation.annotationsOptions, navigation.bindings.arrowSegment.annotationsOptions); return this.chart.addAnnotation(options); }, /** @ignore-option */ steps: [ updateNthPoint(1) ] }, /** * A ray annotation bindings. Includes `start` and one event in `steps` * array. * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "highcharts-ray", "start": function() {}, "steps": [function() {}], "annotationsOptions": {}} */ ray: { /** @ignore-option */ className: 'highcharts-ray', // eslint-disable-next-line valid-jsdoc /** @ignore-option */ start: function (e) { const [coordsX, coordsY] = this.getCoords(e); // Exit if clicked out of axes area if (!coordsX || !coordsY) { return; } const navigation = this.chart.options.navigation, options = merge({ langKey: 'ray', type: 'infinityLine', typeOptions: { type: 'ray', xAxis: coordsX.axis.index, yAxis: coordsY.axis.index, points: [{ x: coordsX.value, y: coordsY.value }, { x: coordsX.value, y: coordsY.value }] } }, navigation.annotationsOptions, navigation.bindings.ray.annotationsOptions); return this.chart.addAnnotation(options); }, /** @ignore-option */ steps: [ updateNthPoint(1) ] }, /** * A ray with an arrow annotation bindings. Includes `start` and one event * in `steps` array. * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "highcharts-arrow-ray", "start": function() {}, "steps": [function() {}], "annotationsOptions": {}} */ arrowRay: { /** @ignore-option */ className: 'highcharts-arrow-ray', // eslint-disable-next-line valid-jsdoc /** @ignore-option */ start: function (e) { const [coordsX, coordsY] = this.getCoords(e); // Exit if clicked out of axes area if (!coordsX || !coordsY) { return; } const navigation = this.chart.options.navigation, options = merge({ langKey: 'arrowRay', type: 'infinityLine', typeOptions: { type: 'ray', line: { markerEnd: 'arrow' }, xAxis: coordsX.axis.index, yAxis: coordsY.axis.index, points: [{ x: coordsX.value, y: coordsY.value }, { x: coordsX.value, y: coordsY.value }] } }, navigation.annotationsOptions, navigation.bindings.arrowRay.annotationsOptions); return this.chart.addAnnotation(options); }, /** @ignore-option */ steps: [ updateNthPoint(1) ] }, /** * A line annotation. Includes `start` and one event in `steps` array. * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "highcharts-infinity-line", "start": function() {}, "steps": [function() {}], "annotationsOptions": {}} */ infinityLine: { /** @ignore-option */ className: 'highcharts-infinity-line', // eslint-disable-next-line valid-jsdoc /** @ignore-option */ start: function (e) { const [coordsX, coordsY] = this.getCoords(e); // Exit if clicked out of axes area if (!coordsX || !coordsY) { return; } const navigation = this.chart.options.navigation, options = merge({ langKey: 'infinityLine', type: 'infinityLine', typeOptions: { type: 'line', xAxis: coordsX.axis.index, yAxis: coordsY.axis.index, points: [{ x: coordsX.value, y: coordsY.value }, { x: coordsX.value, y: coordsY.value }] } }, navigation.annotationsOptions, navigation.bindings.infinityLine.annotationsOptions); return this.chart.addAnnotation(options); }, /** @ignore-option */ steps: [ updateNthPoint(1) ] }, /** * A line with arrow annotation. Includes `start` and one event in `steps` * array. * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "highcharts-arrow-infinity-line", "start": function() {}, "steps": [function() {}], "annotationsOptions": {}} */ arrowInfinityLine: { /** @ignore-option */ className: 'highcharts-arrow-infinity-line', // eslint-disable-next-line valid-jsdoc /** @ignore-option */ start: function (e) { const [coordsX, coordsY] = this.getCoords(e); // Exit if clicked out of axes area if (!coordsX || !coordsY) { return; } const navigation = this.chart.options.navigation, options = merge({ langKey: 'arrowInfinityLine', type: 'infinityLine', typeOptions: { type: 'line', line: { markerEnd: 'arrow' }, xAxis: coordsX.axis.index, yAxis: coordsY.axis.index, points: [{ x: coordsX.value, y: coordsY.value }, { x: coordsX.value, y: coordsY.value }] } }, navigation.annotationsOptions, navigation.bindings.arrowInfinityLine .annotationsOptions); return this.chart.addAnnotation(options); }, /** @ignore-option */ steps: [ updateNthPoint(1) ] }, /** * A horizontal line annotation. Includes `start` event. * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "highcharts-horizontal-line", "start": function() {}, "annotationsOptions": {}} */ horizontalLine: { /** @ignore-option */ className: 'highcharts-horizontal-line', // eslint-disable-next-line valid-jsdoc /** @ignore-option */ start: function (e) { const [coordsX, coordsY] = this.getCoords(e); // Exit if clicked out of axes area if (!coordsX || !coordsY) { return; } const navigation = this.chart.options.navigation, options = merge({ langKey: 'horizontalLine', type: 'infinityLine', draggable: 'y', typeOptions: { type: 'horizontalLine', xAxis: coordsX.axis.index, yAxis: coordsY.axis.index, points: [{ x: coordsX.value, y: coordsY.value }] } }, navigation.annotationsOptions, navigation.bindings .horizontalLine.annotationsOptions); this.chart.addAnnotation(options); } }, /** * A vertical line annotation. Includes `start` event. * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "highcharts-vertical-line", "start": function() {}, "annotationsOptions": {}} */ verticalLine: { /** @ignore-option */ className: 'highcharts-vertical-line', // eslint-disable-next-line valid-jsdoc /** @ignore-option */ start: function (e) { const [coordsX, coordsY] = this.getCoords(e); // Exit if clicked out of axes area if (!coordsX || !coordsY) { return; } const navigation = this.chart.options.navigation, options = merge({ langKey: 'verticalLine', type: 'infinityLine', draggable: 'x', typeOptions: { type: 'verticalLine', xAxis: coordsX.axis.index, yAxis: coordsY.axis.index, points: [{ x: coordsX.value, y: coordsY.value }] } }, navigation.annotationsOptions, navigation.bindings.verticalLine.annotationsOptions); this.chart.addAnnotation(options); } }, /** * Crooked line (three points) annotation bindings. Includes `start` and two * events in `steps` (for second and third points in crooked line) array. * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "highcharts-crooked3", "start": function() {}, "steps": [function() {}, function() {}], "annotationsOptions": {}} */ // Crooked Line type annotations: crooked3: { /** @ignore-option */ className: 'highcharts-crooked3', // eslint-disable-next-line valid-jsdoc /** @ignore-option */ start: function (e) { const [coordsX, coordsY] = this.getCoords(e); // Exit if clicked out of axes area if (!coordsX || !coordsY) { return; } const x = coordsX.value, y = coordsY.value, navigation = this.chart.options.navigation, options = merge({ langKey: 'crooked3', type: 'crookedLine', typeOptions: { xAxis: coordsX.axis.index, yAxis: coordsY.axis.index, points: [ { x, y }, { x, y }, { x, y } ] } }, navigation.annotationsOptions, navigation.bindings.crooked3.annotationsOptions); return this.chart.addAnnotation(options); }, /** @ignore-option */ steps: [ updateNthPoint(1), updateNthPoint(2) ] }, /** * Crooked line (five points) annotation bindings. Includes `start` and four * events in `steps` (for all consequent points in crooked line) array. * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "highcharts-crooked5", "start": function() {}, "steps": [function() {}, function() {}, function() {}, function() {}], "annotationsOptions": {}} */ crooked5: { /** @ignore-option */ className: 'highcharts-crooked5', // eslint-disable-next-line valid-jsdoc /** @ignore-option */ start: function (e) { const [coordsX, coordsY] = this.getCoords(e); // Exit if clicked out of axes area if (!coordsX || !coordsY) { return; } const x = coordsX.value, y = coordsY.value, navigation = this.chart.options.navigation, options = merge({ langKey: 'crooked5', type: 'crookedLine', typeOptions: { xAxis: coordsX.axis.index, yAxis: coordsY.axis.index, points: [ { x, y }, { x, y }, { x, y }, { x, y }, { x, y } ] } }, navigation.annotationsOptions, navigation.bindings.crooked5.annotationsOptions); return this.chart.addAnnotation(options); }, /** @ignore-option */ steps: [ updateNthPoint(1), updateNthPoint(2), updateNthPoint(3), updateNthPoint(4) ] }, /** * Elliott wave (three points) annotation bindings. Includes `start` and two * events in `steps` (for second and third points) array. * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "highcharts-elliott3", "start": function() {}, "steps": [function() {}, function() {}], "annotationsOptions": {}} */ elliott3: { /** @ignore-option */ className: 'highcharts-elliott3', // eslint-disable-next-line valid-jsdoc /** @ignore-option */ start: function (e) { const [coordsX, coordsY] = this.getCoords(e); // Exit if clicked out of axes area if (!coordsX || !coordsY) { return; } const x = coordsX.value, y = coordsY.value, navigation = this.chart.options.navigation, options = merge({ langKey: 'elliott3', type: 'elliottWave', typeOptions: { xAxis: coordsX.axis.index, yAxis: coordsY.axis.index, points: [ { x, y }, { x, y }, { x, y }, { x, y } ] }, labelOptions: { style: { color: "#666666" /* Palette.neutralColor60 */ } } }, navigation.annotationsOptions, navigation.bindings.elliott3.annotationsOptions); return this.chart.addAnnotation(options); }, /** @ignore-option */ steps: [ updateNthPoint(1), updateNthPoint(2), updateNthPoint(3) ] }, /** * Elliott wave (five points) annotation bindings. Includes `start` and four * event in `steps` (for all consequent points in Elliott wave) array. * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "highcharts-elliott3", "start": function() {}, "steps": [function() {}, function() {}, function() {}, function() {}], "annotationsOptions": {}} */ elliott5: { /** @ignore-option */ className: 'highcharts-elliott5', // eslint-disable-next-line valid-jsdoc /** @ignore-option */ start: function (e) { const [coordsX, coordsY] = this.getCoords(e); // Exit if clicked out of axes area if (!coordsX || !coordsY) { return; } const x = coordsX.value, y = coordsY.value, navigation = this.chart.options.navigation, options = merge({ langKey: 'elliott5', type: 'elliottWave', typeOptions: { xAxis: coordsX.axis.index, yAxis: coordsY.axis.index, points: [ { x, y }, { x, y }, { x, y }, { x, y }, { x, y }, { x, y } ] }, labelOptions: { style: { color: "#666666" /* Palette.neutralColor60 */ } } }, navigation.annotationsOptions, navigation.bindings.elliott5.annotationsOptions); return this.chart.addAnnotation(options); }, /** @ignore-option */ steps: [ updateNthPoint(1), updateNthPoint(2), updateNthPoint(3), updateNthPoint(4), updateNthPoint(5) ] }, /** * A measure (x-dimension) annotation bindings. Includes `start` and one * event in `steps` array. * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "highcharts-measure-x", "start": function() {}, "steps": [function() {}], "annotationsOptions": {}} */ measureX: { /** @ignore-option */ className: 'highcharts-measure-x', // eslint-disable-next-line valid-jsdoc /** @ignore-option */ start: function (e) { const [coordsX, coordsY] = this.getCoords(e); // Exit if clicked out of axes area if (!coordsX || !coordsY) { return; } const x = coordsX.value, y = coordsY.value, navigation = this.chart.options.navigation, options = merge({ langKey: 'measure', type: 'measure', typeOptions: { selectType: 'x', xAxis: coordsX.axis.index, yAxis: coordsY.axis.index, point: { x, y }, crosshairX: { strokeWidth: 1, stroke: "#000000" /* Palette.neutralColor100 */ }, crosshairY: { enabled: false, strokeWidth: 0, stroke: "#000000" /* Palette.neutralColor100 */ }, background: { width: 0, height: 0, strokeWidth: 0, stroke: "#ffffff" /* Palette.backgroundColor */ } }, labelOptions: { style: { color: "#666666" /* Palette.neutralColor60 */ } } }, navigation.annotationsOptions, navigation.bindings.measureX.annotationsOptions); return this.chart.addAnnotation(options); }, /** @ignore-option */ steps: [ updateRectSize ] }, /** * A measure (y-dimension) annotation bindings. Includes `start` and one * event in `steps` array. * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "highcharts-measure-y", "start": function() {}, "steps": [function() {}], "annotationsOptions": {}} */ measureY: { /** @ignore-option */ className: 'highcharts-measure-y', // eslint-disable-next-line valid-jsdoc /** @ignore-option */ start: function (e) { const [coordsX, coordsY] = this.getCoords(e); // Exit if clicked out of axes area if (!coordsX || !coordsY) { return; } const x = coordsX.value, y = coordsY.value, navigation = this.chart.options.navigation, options = merge({ langKey: 'measure', type: 'measure', typeOptions: { selectType: 'y', xAxis: coordsX.axis.index, yAxis: coordsY.axis.index, point: { x, y }, crosshairX: { enabled: false, strokeWidth: 0, stroke: "#000000" /* Palette.neutralColor100 */ }, crosshairY: { strokeWidth: 1, stroke: "#000000" /* Palette.neutralColor100 */ }, background: { width: 0, height: 0, strokeWidth: 0, stroke: "#ffffff" /* Palette.backgroundColor */ } }, labelOptions: { style: { color: "#666666" /* Palette.neutralColor60 */ } } }, navigation.annotationsOptions, navigation.bindings.measureY.annotationsOptions); return this.chart.addAnnotation(options); }, /** @ignore-option */ steps: [ updateRectSize ] }, /** * A measure (xy-dimension) annotation bindings. Includes `start` and one * event in `steps` array. * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "highcharts-measure-xy", "start": function() {}, "steps": [function() {}], "annotationsOptions": {}} */ measureXY: { /** @ignore-option */ className: 'highcharts-measure-xy', // eslint-disable-next-line valid-jsdoc /** @ignore-option */ start: function (e) { const [coordsX, coordsY] = this.getCoords(e); // Exit if clicked out of axes area if (!coordsX || !coordsY) { return; } const x = coordsX.value, y = coordsY.value, navigation = this.chart.options.navigation, options = merge({ langKey: 'measure', type: 'measure', typeOptions: { selectType: 'xy', xAxis: coordsX.axis.index, yAxis: coordsY.axis.index, point: { x, y }, background: { width: 0, height: 0, strokeWidth: 10 }, crosshairX: { strokeWidth: 1, stroke: "#000000" /* Palette.neutralColor100 */ }, crosshairY: { strokeWidth: 1, stroke: "#000000" /* Palette.neutralColor100 */ } }, labelOptions: { style: { color: "#666666" /* Palette.neutralColor60 */ } } }, navigation.annotationsOptions, navigation.bindings.measureXY.annotationsOptions); return this.chart.addAnnotation(options); }, /** @ignore-option */ steps: [ updateRectSize ] }, // Advanced type annotations: /** * A fibonacci annotation bindings. Includes `start` and two events in * `steps` array (updates second point, then height). * * @sample {highstock} stock/stocktools/custom-stock-tools-bindings * Custom stock tools bindings * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "highcharts-fibonacci", "start": function() {}, "steps": [function() {}, function() {}], "annotationsOptions": { "typeOptions": { "reversed": false }}} */ fibonacci: { className: 'highcharts-fibonacci', // eslint-disable-next-line valid-jsdoc /** @ignore-option */ start: function (e) { const [coordsX, coordsY] = this.getCoords(e); // Exit if clicked out of axes area if (!coordsX || !coordsY) { return; } const x = coordsX.value, y = coordsY.value, navigation = this.chart.options.navigation, options = merge({ langKey: 'fibonacci', type: 'fibonacci', typeOptions: { xAxis: coordsX.axis.index, yAxis: coordsY.axis.index, points: [ { x, y }, { x, y } ] }, labelOptions: { style: { color: "#666666" /* Palette.neutralColor60 */ } } }, navigation.annotationsOptions, navigation.bindings.fibonacci.annotationsOptions); return this.chart.addAnnotation(options); }, /** @ignore-option */ steps: [ updateNthPoint(1), updateHeight ] }, /** * A parallel channel (tunnel) annotation bindings. Includes `start` and * two events in `steps` array (updates second point, then height). * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "highcharts-parallel-channel", "start": function() {}, "steps": [function() {}, function() {}], "annotationsOptions": {}} */ parallelChannel: { /** @ignore-option */ className: 'highcharts-parallel-channel', // eslint-disable-next-line valid-jsdoc /** @ignore-option */ start: function (e) { const [coordsX, coordsY] = this.getCoords(e); // Exit if clicked out of axes area if (!coordsX || !coordsY) { return; } const x = coordsX.value, y = coordsY.value, navigation = this.chart.options.navigation, options = merge({ langKey: 'parallelChannel', type: 'tunnel', typeOptions: { xAxis: coordsX.axis.index, yAxis: coordsY.axis.index, points: [ { x, y }, { x, y } ] } }, navigation.annotationsOptions, navigation.bindings.parallelChannel .annotationsOptions); return this.chart.addAnnotation(options); }, /** @ignore-option */ steps: [ updateNthPoint(1), updateHeight ] }, /** * An Andrew's pitchfork annotation bindings. Includes `start` and two * events in `steps` array (sets second and third control points). * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "highcharts-pitchfork", "start": function() {}, "steps": [function() {}, function() {}], "annotationsOptions": {}} */ pitchfork: { /** @ignore-option */ className: 'highcharts-pitchfork', // eslint-disable-next-line valid-jsdoc /** @ignore-option */ start: function (e) { const [coordsX, coordsY] = this.getCoords(e); // Exit if clicked out of axes area if (!coordsX || !coordsY) { return; } const x = coordsX.value, y = coordsY.value, navigation = this.chart.options.navigation, options = merge({ langKey: 'pitchfork', type: 'pitchfork', typeOptions: { xAxis: coordsX.axis.index, yAxis: coordsY.axis.index, points: [{ x: coordsX.value, y: coordsY.value, controlPoint: { style: { fill: "#f21313" /* Palette.negativeColor */ } } }, { x, y }, { x, y } ], innerBackground: { fill: 'rgba(100, 170, 255, 0.8)' } }, shapeOptions: { strokeWidth: 2 } }, navigation.annotationsOptions, navigation.bindings.pitchfork.annotationsOptions); return this.chart.addAnnotation(options); }, /** @ignore-option */ steps: [ updateNthPoint(1), updateNthPoint(2) ] }, // Labels with arrow and auto increments /** * A vertical counter annotation bindings. Includes `start` event. On click, * finds the closest point and marks it with a numeric annotation - * incrementing counter on each add. * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "highcharts-vertical-counter", "start": function() {}, "annotationsOptions": {}} */ verticalCounter: { /** @ignore-option */ className: 'highcharts-vertical-counter', // eslint-disable-next-line valid-jsdoc /** @ignore-option */ start: function (e) { const closestPoint = attractToPoint(e, this.chart); // Exit if clicked out of axes area if (!closestPoint) { return; } this.verticalCounter = this.verticalCounter || 0; const navigation = this.chart.options.navigation, options = merge({ langKey: 'verticalCounter', type: 'verticalLine', typeOptions: { point: { x: closestPoint.x, y: closestPoint.y, xAxis: closestPoint.xAxis, yAxis: closestPoint.yAxis }, label: { offset: closestPoint.below ? 40 : -40, text: this.verticalCounter.toString() } }, labelOptions: { style: { color: "#666666" /* Palette.neutralColor60 */, fontSize: '0.7em' } }, shapeOptions: { stroke: 'rgba(0, 0, 0, 0.75)', strokeWidth: 1 } }, navigation.annotationsOptions, navigation.bindings .verticalCounter.annotationsOptions), annotation = this.chart.addAnnotation(options); this.verticalCounter++; annotation.options.events.click.call(annotation, {}); } }, /** * A time cycles annotation bindings. Includes `start` event and 1 `step` * event. first click marks the beginning of the circle, and the second one * sets its diameter. * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "highcharts-time-cycles", "start": function() {}, "steps": [function (){}] "annotationsOptions": {}} */ timeCycles: { className: 'highcharts-time-cycles', start: function (e) { const closestPoint = attractToPoint(e, this.chart); // Exit if clicked out of axes area if (!closestPoint) { return; } const navigation = this.chart.options.navigation, options = merge({ langKey: 'timeCycles', type: 'timeCycles', typeOptions: { xAxis: closestPoint.xAxis, yAxis: closestPoint.yAxis, points: [{ x: closestPoint.x }, { x: closestPoint.x }], line: { stroke: 'rgba(0, 0, 0, 0.75)', fill: 'transparent', strokeWidth: 2 } } }, navigation.annotationsOptions, navigation.bindings.timeCycles.annotationsOptions), annotation = this.chart.addAnnotation(options); annotation.options.events.click.call(annotation, {}); return annotation; }, steps: [ updateNthPoint(1) ] }, verticalLabel: { /** @ignore-option */ className: 'highcharts-vertical-label', // eslint-disable-next-line valid-jsdoc /** @ignore-option */ start: function (e) { const closestPoint = attractToPoint(e, this.chart); // Exit if clicked out of axes area if (!closestPoint) { return; } const navigation = this.chart.options.navigation, options = merge({ langKey: 'verticalLabel', type: 'verticalLine', typeOptions: { point: { x: closestPoint.x, y: closestPoint.y, xAxis: closestPoint.xAxis, yAxis: closestPoint.yAxis }, label: { offset: closestPoint.below ? 40 : -40 } }, labelOptions: { style: { color: "#666666" /* Palette.neutralColor60 */, fontSize: '0.7em' } }, shapeOptions: { stroke: 'rgba(0, 0, 0, 0.75)', strokeWidth: 1 } }, navigation.annotationsOptions, navigation.bindings .verticalLabel.annotationsOptions), annotation = this.chart.addAnnotation(options); annotation.options.events.click.call(annotation, {}); } }, /** * A vertical arrow annotation bindings. Includes `start` event. On click, * finds the closest point and marks it with an arrow. * `#06b535` is the color of the arrow when * pointing from above and `#f21313` * when pointing from below the point. * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "highcharts-vertical-arrow", "start": function() {}, "annotationsOptions": {}} */ verticalArrow: { /** @ignore-option */ className: 'highcharts-vertical-arrow', // eslint-disable-next-line valid-jsdoc /** @ignore-option */ start: function (e) { const closestPoint = attractToPoint(e, this.chart); // Exit if clicked out of axes area if (!closestPoint) { return; } const navigation = this.chart.options.navigation, options = merge({ langKey: 'verticalArrow', type: 'verticalLine', typeOptions: { point: { x: closestPoint.x, y: closestPoint.y, xAxis: closestPoint.xAxis, yAxis: closestPoint.yAxis }, label: { offset: closestPoint.below ? 40 : -40, format: ' ' }, connector: { fill: 'none', stroke: closestPoint.below ? "#f21313" /* Palette.negativeColor */ : "#06b535" /* Palette.positiveColor */ } }, shapeOptions: { stroke: 'rgba(0, 0, 0, 0.75)', strokeWidth: 1 } }, navigation.annotationsOptions, navigation.bindings .verticalArrow.annotationsOptions), annotation = this.chart.addAnnotation(options); annotation.options.events.click.call(annotation, {}); } }, /** * The Fibonacci Time Zones annotation bindings. Includes `start` and one * event in `steps` array. * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "highcharts-fibonacci-time-zones", "start": function() {}, "steps": [function() {}], "annotationsOptions": {}} */ fibonacciTimeZones: { /** @ignore-option */ className: 'highcharts-fibonacci-time-zones', // eslint-disable-next-line valid-jsdoc /** @ignore-option */ start: function (e) { const [coordsX, coordsY] = this.getCoords(e); // Exit if clicked out of axes area if (!coordsX || !coordsY) { return; } const navigation = this.chart.options.navigation, options = merge({ type: 'fibonacciTimeZones', langKey: 'fibonacciTimeZones', typeOptions: { xAxis: coordsX.axis.index, yAxis: coordsY.axis.index, points: [{ x: coordsX.value }] } }, navigation.annotationsOptions, navigation.bindings.fibonacciTimeZones .annotationsOptions); return this.chart.addAnnotation(options); }, /** @ignore-option */ // eslint-disable-next-line valid-jsdoc steps: [ function (e, annotation) { const mockPointOpts = annotation.options.typeOptions.points, x = mockPointOpts && mockPointOpts[0].x, [coordsX, coordsY] = this.getCoords(e); if (coordsX && coordsY) { annotation.update({ typeOptions: { xAxis: coordsX.axis.index, yAxis: coordsY.axis.index, points: [{ x: x }, { x: coordsX.value }] } }); } } ] }, // Flag types: /** * A flag series bindings. Includes `start` event. On click, finds the * closest point and marks it with a flag with `'circlepin'` shape. * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "highcharts-flag-circlepin", "start": function() {}} */ flagCirclepin: { /** @ignore-option */ className: 'highcharts-flag-circlepin', /** @ignore-option */ start: addFlagFromForm('circlepin') }, /** * A flag series bindings. Includes `start` event. On click, finds the * closest point and marks it with a flag with `'diamondpin'` shape. * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "highcharts-flag-diamondpin", "start": function() {}} */ flagDiamondpin: { /** @ignore-option */ className: 'highcharts-flag-diamondpin', /** @ignore-option */ start: addFlagFromForm('flag') }, /** * A flag series bindings. Includes `start` event. * On click, finds the closest point and marks it with a flag with * `'squarepin'` shape. * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "highcharts-flag-squarepin", "start": function() {}} */ flagSquarepin: { /** @ignore-option */ className: 'highcharts-flag-squarepin', /** @ignore-option */ start: addFlagFromForm('squarepin') }, /** * A flag series bindings. Includes `start` event. * On click, finds the closest point and marks it with a flag without pin * shape. * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "highcharts-flag-simplepin", "start": function() {}} */ flagSimplepin: { /** @ignore-option */ className: 'highcharts-flag-simplepin', /** @ignore-option */ start: addFlagFromForm('nopin') }, // Other tools: /** * Enables zooming in xAxis on a chart. Includes `start` event which * changes [chart.zoomType](#chart.zoomType). * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "highcharts-zoom-x", "init": function() {}} */ zoomX: { /** @ignore-option */ className: 'highcharts-zoom-x', // eslint-disable-next-line valid-jsdoc /** @ignore-option */ init: function (button) { this.chart.update({ chart: { zooming: { type: 'x' } } }); fireEvent(this, 'deselectButton', { button: button }); } }, /** * Enables zooming in yAxis on a chart. Includes `start` event which * changes [chart.zoomType](#chart.zoomType). * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "highcharts-zoom-y", "init": function() {}} */ zoomY: { /** @ignore-option */ className: 'highcharts-zoom-y', // eslint-disable-next-line valid-jsdoc /** @ignore-option */ init: function (button) { this.chart.update({ chart: { zooming: { type: 'y' } } }); fireEvent(this, 'deselectButton', { button: button }); } }, /** * Enables zooming in xAxis and yAxis on a chart. Includes `start` event * which changes [chart.zoomType](#chart.zoomType). * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "highcharts-zoom-xy", "init": function() {}} */ zoomXY: { /** @ignore-option */ className: 'highcharts-zoom-xy', // eslint-disable-next-line valid-jsdoc /** @ignore-option */ init: function (button) { this.chart.update({ chart: { zooming: { type: 'xy' } } }); fireEvent(this, 'deselectButton', { button: button }); } }, /** * Changes main series to `'line'` type. * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "highcharts-series-type-line", "init": function() {}} */ seriesTypeLine: { /** @ignore-option */ className: 'highcharts-series-type-line', // eslint-disable-next-line valid-jsdoc /** @ignore-option */ init: function (button) { this.chart.series[0].update({ type: 'line', useOhlcData: true }); fireEvent(this, 'deselectButton', { button: button }); } }, /** * Changes main series to `'ohlc'` type. * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "highcharts-series-type-ohlc", "init": function() {}} */ seriesTypeOhlc: { /** @ignore-option */ className: 'highcharts-series-type-ohlc', // eslint-disable-next-line valid-jsdoc /** @ignore-option */ init: function (button) { this.chart.series[0].update({ type: 'ohlc' }); fireEvent(this, 'deselectButton', { button: button }); } }, /** * Changes main series to `'candlestick'` type. * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "highcharts-series-type-candlestick", "init": function() {}} */ seriesTypeCandlestick: { /** @ignore-option */ className: 'highcharts-series-type-candlestick', // eslint-disable-next-line valid-jsdoc /** @ignore-option */ init: function (button) { this.chart.series[0].update({ type: 'candlestick' }); fireEvent(this, 'deselectButton', { button: button }); } }, /** * Changes main series to `'heikinashi'` type. * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "highcharts-series-type-heikinashi", "init": function() {}} */ seriesTypeHeikinAshi: { /** @ignore-option */ className: 'highcharts-series-type-heikinashi', // eslint-disable-next-line valid-jsdoc /** @ignore-option */ init: function (button) { this.chart.series[0].update({ type: 'heikinashi' }); fireEvent(this, 'deselectButton', { button: button }); } }, /** * Change main series to `'hlc'` type. * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "highcharts-series-type-hlc", "init": function () {}} */ seriesTypeHLC: { className: 'highcharts-series-type-hlc', init: function (button) { this.chart.series[0].update({ type: 'hlc', useOhlcData: true }); fireEvent(this, 'deselectButton', { button }); } }, /** * Changes main series to `'hollowcandlestick'` type. * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "highcharts-series-type-hollowcandlestick", "init": function() {}} */ seriesTypeHollowCandlestick: { /** @ignore-option */ className: 'highcharts-series-type-hollowcandlestick', // eslint-disable-next-line valid-jsdoc /** @ignore-option */ init: function (button) { this.chart.series[0].update({ type: 'hollowcandlestick' }); fireEvent(this, 'deselectButton', { button: button }); } }, /** * Displays chart in fullscreen. * * **Note**: Fullscreen is not supported on iPhone due to iOS limitations. * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "noDataState": "normal", "highcharts-full-screen", "init": function() {}} */ fullScreen: { /** @ignore-option */ className: 'highcharts-full-screen', noDataState: 'normal', /** @ignore-option */ init: function (button) { if (this.chart.fullscreen) { this.chart.fullscreen.toggle(); } fireEvent(this, 'deselectButton', { button: button }); } }, /** * Hides/shows two price indicators: * - last price in the dataset * - last price in the selected range * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "highcharts-current-price-indicator", "init": function() {}} */ currentPriceIndicator: { /** @ignore-option */ className: 'highcharts-current-price-indicator', // eslint-disable-next-line valid-jsdoc /** @ignore-option */ init: function (button) { const chart = this.chart, series = chart.series, gui = chart.stockTools, priceIndicatorEnabled = isPriceIndicatorEnabled(chart.series); if (gui && gui.guiEnabled) { series.forEach(function (series) { series.update({ lastPrice: { enabled: !priceIndicatorEnabled }, lastVisiblePrice: { enabled: !priceIndicatorEnabled, label: { enabled: true } } }, false); }); chart.redraw(); } fireEvent(this, 'deselectButton', { button: button }); } }, /** * Indicators bindings. Includes `init` event to show a popup. * * Note: In order to show base series from the chart in the popup's * dropdown each series requires * [series.id](https://api.highcharts.com/highstock/series.line.id) to be * defined. * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "highcharts-indicators", "init": function() {}} */ indicators: { /** @ignore-option */ className: 'highcharts-indicators', // eslint-disable-next-line valid-jsdoc /** @ignore-option */ init: function () { const navigation = this; fireEvent(navigation, 'showPopup', { formType: 'indicators', options: {}, // Callback on submit: onSubmit: function (data) { manageIndicators.call(navigation, data); } }); } }, /** * Hides/shows all annotations on a chart. * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "highcharts-toggle-annotations", "init": function() {}} */ toggleAnnotations: { /** @ignore-option */ className: 'highcharts-toggle-annotations', // eslint-disable-next-line valid-jsdoc /** @ignore-option */ init: function (button) { const chart = this.chart, gui = chart.stockTools, iconsURL = gui.getIconsURL(); this.toggledAnnotations = !this.toggledAnnotations; (chart.annotations || []).forEach(function (annotation) { annotation.setVisibility(!this.toggledAnnotations); }, this); if (gui && gui.guiEnabled) { if (this.toggledAnnotations) { button.firstChild.style['background-image'] = 'url("' + iconsURL + 'annotations-hidden.svg")'; } else { button.firstChild.style['background-image'] = 'url("' + iconsURL + 'annotations-visible.svg")'; } } fireEvent(this, 'deselectButton', { button: button }); } }, /** * Save a chart in localStorage under `highcharts-chart` key. * Stored items: * - annotations * - indicators (with yAxes) * - flags * * @type {Highcharts.NavigationBindingsOptionsObject} * @product highstock * @default {"className": "highcharts-save-chart", "noDataState": "normal", "init": function() {}} */ saveChart: { /** @ignore-option */ className: 'highcharts-save-chart', noDataState: 'normal', // eslint-disable-next-line valid-jsdoc /** @ignore-option */ init: function (button) { const navigation = this, chart = navigation.chart, annotations = [], indicators = [], flags = [], yAxes = []; chart.annotations.forEach(function (annotation, index) { annotations[index] = annotation.userOptions; }); chart.series.forEach(function (series) { if (series.is('sma')) { indicators.push(series.userOptions); } else if (series.type === 'flags') { flags.push(series.userOptions); } }); chart.yAxis.forEach(function (yAxis) { if (isNotNavigatorYAxis(yAxis)) { yAxes.push(yAxis.options); } }); H.win.localStorage.setItem('highcharts-chart', JSON.stringify({ annotations: annotations, indicators: indicators, flags: flags, yAxes: yAxes })); fireEvent(this, 'deselectButton', { button: button }); } } }; /* * * * Default Export * * */ return StockToolsBindings; }); _registerModule(_modules, 'Stock/StockTools/StockToolsDefaults.js', [], function () { /* * * * GUI generator for Stock tools * * (c) 2009-2024 Sebastian Bochan * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ /* * * * Constants * * */ /** * @optionparent lang */ const lang = { /** * Configure the stockTools GUI titles(hints) in the chart. Requires * the `stock-tools.js` module to be loaded. * * @product highstock * @since 7.0.0 */ stockTools: { gui: { // Main buttons: simpleShapes: 'Simple shapes', lines: 'Lines', crookedLines: 'Crooked lines', measure: 'Measure', advanced: 'Advanced', toggleAnnotations: 'Toggle annotations', verticalLabels: 'Vertical labels', flags: 'Flags', zoomChange: 'Zoom change', typeChange: 'Type change', saveChart: 'Save chart', indicators: 'Indicators', currentPriceIndicator: 'Current Price Indicators', // Other features: zoomX: 'Zoom X', zoomY: 'Zoom Y', zoomXY: 'Zooom XY', fullScreen: 'Fullscreen', typeOHLC: 'OHLC', typeLine: 'Line', typeCandlestick: 'Candlestick', typeHLC: 'HLC', typeHollowCandlestick: 'Hollow Candlestick', typeHeikinAshi: 'Heikin Ashi', // Basic shapes: circle: 'Circle', ellipse: 'Ellipse', label: 'Label', rectangle: 'Rectangle', // Flags: flagCirclepin: 'Flag circle', flagDiamondpin: 'Flag diamond', flagSquarepin: 'Flag square', flagSimplepin: 'Flag simple', // Measures: measureXY: 'Measure XY', measureX: 'Measure X', measureY: 'Measure Y', // Segment, ray and line: segment: 'Segment', arrowSegment: 'Arrow segment', ray: 'Ray', arrowRay: 'Arrow ray', line: 'Line', arrowInfinityLine: 'Arrow line', horizontalLine: 'Horizontal line', verticalLine: 'Vertical line', infinityLine: 'Infinity line', // Crooked lines: crooked3: 'Crooked 3 line', crooked5: 'Crooked 5 line', elliott3: 'Elliott 3 line', elliott5: 'Elliott 5 line', // Counters: verticalCounter: 'Vertical counter', verticalLabel: 'Vertical label', verticalArrow: 'Vertical arrow', // Advanced: fibonacci: 'Fibonacci', fibonacciTimeZones: 'Fibonacci Time Zones', pitchfork: 'Pitchfork', parallelChannel: 'Parallel channel', timeCycles: 'Time Cycles' } }, navigation: { popup: { // Annotations: circle: 'Circle', ellipse: 'Ellipse', rectangle: 'Rectangle', label: 'Label', segment: 'Segment', arrowSegment: 'Arrow segment', ray: 'Ray', arrowRay: 'Arrow ray', line: 'Line', arrowInfinityLine: 'Arrow line', horizontalLine: 'Horizontal line', verticalLine: 'Vertical line', crooked3: 'Crooked 3 line', crooked5: 'Crooked 5 line', elliott3: 'Elliott 3 line', elliott5: 'Elliott 5 line', verticalCounter: 'Vertical counter', verticalLabel: 'Vertical label', verticalArrow: 'Vertical arrow', fibonacci: 'Fibonacci', fibonacciTimeZones: 'Fibonacci Time Zones', pitchfork: 'Pitchfork', parallelChannel: 'Parallel channel', infinityLine: 'Infinity line', measure: 'Measure', measureXY: 'Measure XY', measureX: 'Measure X', measureY: 'Measure Y', timeCycles: 'Time Cycles', // Flags: flags: 'Flags', // GUI elements: addButton: 'Add', saveButton: 'Save', editButton: 'Edit', removeButton: 'Remove', series: 'Series', volume: 'Volume', connector: 'Connector', // Field names: innerBackground: 'Inner background', outerBackground: 'Outer background', crosshairX: 'Crosshair X', crosshairY: 'Crosshair Y', tunnel: 'Tunnel', background: 'Background', // Indicators' searchbox (#16019): noFilterMatch: 'No match', // Indicators' params (#15170): searchIndicators: 'Search Indicators', clearFilter: '\u2715 clear filter', index: 'Index', period: 'Period', periods: 'Periods', standardDeviation: 'Standard deviation', periodTenkan: 'Tenkan period', periodSenkouSpanB: 'Senkou Span B period', periodATR: 'ATR period', multiplierATR: 'ATR multiplier', shortPeriod: 'Short period', longPeriod: 'Long period', signalPeriod: 'Signal period', decimals: 'Decimals', algorithm: 'Algorithm', topBand: 'Top band', bottomBand: 'Bottom band', initialAccelerationFactor: 'Initial acceleration factor', maxAccelerationFactor: 'Max acceleration factor', increment: 'Increment', multiplier: 'Multiplier', ranges: 'Ranges', highIndex: 'High index', lowIndex: 'Low index', deviation: 'Deviation', xAxisUnit: 'x-axis unit', factor: 'Factor', fastAvgPeriod: 'Fast average period', slowAvgPeriod: 'Slow average period', average: 'Average', /** * Configure the aliases for indicator names. * * @product highstock * @since 9.3.0 */ indicatorAliases: { // Overlays /** * Acceleration Bands alias. * * @default ['Acceleration Bands'] * @type {Array} */ abands: ['Acceleration Bands'], /** * Bollinger Bands alias. * * @default ['Bollinger Bands'] * @type {Array} */ bb: ['Bollinger Bands'], /** * Double Exponential Moving Average alias. * * @default ['Double Exponential Moving Average'] * @type {Array} */ dema: ['Double Exponential Moving Average'], /** * Exponential Moving Average alias. * * @default ['Exponential Moving Average'] * @type {Array} */ ema: ['Exponential Moving Average'], /** * Ichimoku Kinko Hyo alias. * * @default ['Ichimoku Kinko Hyo'] * @type {Array} */ ikh: ['Ichimoku Kinko Hyo'], /** * Keltner Channels alias. * * @default ['Keltner Channels'] * @type {Array} */ keltnerchannels: ['Keltner Channels'], /** * Linear Regression alias. * * @default ['Linear Regression'] * @type {Array} */ linearRegression: ['Linear Regression'], /** * Pivot Points alias. * * @default ['Pivot Points'] * @type {Array} */ pivotpoints: ['Pivot Points'], /** * Price Channel alias. * * @default ['Price Channel'] * @type {Array} */ pc: ['Price Channel'], /** * Price Envelopes alias. * * @default ['Price Envelopes'] * @type {Array} */ priceenvelopes: ['Price Envelopes'], /** * Parabolic SAR alias. * * @default ['Parabolic SAR'] * @type {Array} */ psar: ['Parabolic SAR'], /** * Simple Moving Average alias. * * @default ['Simple Moving Average'] * @type {Array} */ sma: ['Simple Moving Average'], /** * Super Trend alias. * * @default ['Super Trend'] * @type {Array} */ supertrend: ['Super Trend'], /** * Triple Exponential Moving Average alias. * * @default ['Triple Exponential Moving Average'] * @type {Array} */ tema: ['Triple Exponential Moving Average'], /** * Volume by Price alias. * * @default ['Volume by Price'] * @type {Array} */ vbp: ['Volume by Price'], /** * Volume Weighted Moving Average alias. * * @default ['Volume Weighted Moving Average'] * @type {Array} */ vwap: ['Volume Weighted Moving Average'], /** * Weighted Moving Average alias. * * @default ['Weighted Moving Average'] * @type {Array} */ wma: ['Weighted Moving Average'], /** * Zig Zagalias. * * @default ['Zig Zag'] * @type {Array} */ zigzag: ['Zig Zag'], // Oscilators /** * Absolute price indicator alias. * * @default ['Absolute price indicator'] * @type {Array} */ apo: ['Absolute price indicator'], /** * Accumulation/Distribution alias. * * @default ['Accumulation/Distribution’] * @type {Array} */ ad: ['Accumulation/Distribution'], /** * Aroon alias. * * @default ['Aroon'] * @type {Array} */ aroon: ['Aroon'], /** * Aroon oscillator alias. * * @default ['Aroon oscillator'] * @type {Array} */ aroonoscillator: ['Aroon oscillator'], /** * Average True Range alias. * * @default ['Average True Range’] * @type {Array} */ atr: ['Average True Range'], /** * Awesome oscillator alias. * * @default ['Awesome oscillator’] * @type {Array} */ ao: ['Awesome oscillator'], /** * Commodity Channel Index alias. * * @default ['Commodity Channel Index’] * @type {Array} */ cci: ['Commodity Channel Index'], /** * Chaikin alias. * * @default ['Chaikin’] * @type {Array} */ chaikin: ['Chaikin'], /** * Chaikin Money Flow alias. * * @default ['Chaikin Money Flow’] * @type {Array} */ cmf: ['Chaikin Money Flow'], /** * Chande Momentum Oscillator alias. * * @default ['Chande Momentum Oscillator’] * @type {Array} */ cmo: ['Chande Momentum Oscillator'], /** * Disparity Index alias. * * @default ['Disparity Index’] * @type {Array} */ disparityindex: ['Disparity Index'], /** * Directional Movement Index alias. * * @default ['Directional Movement Index’] * @type {Array} */ dmi: ['Directional Movement Index'], /** * Detrended price oscillator alias. * * @default ['Detrended price oscillator’] * @type {Array} */ dpo: ['Detrended price oscillator'], /** * Klinger Oscillator alias. * * @default [‘Klinger Oscillator’] * @type {Array} */ klinger: ['Klinger Oscillator'], /** * Linear Regression Angle alias. * * @default [‘Linear Regression Angle’] * @type {Array} */ linearRegressionAngle: ['Linear Regression Angle'], /** * Linear Regression Intercept alias. * * @default [‘Linear Regression Intercept’] * @type {Array} */ linearRegressionIntercept: ['Linear Regression Intercept'], /** * Linear Regression Slope alias. * * @default [‘Linear Regression Slope’] * @type {Array} */ linearRegressionSlope: ['Linear Regression Slope'], /** * Moving Average Convergence Divergence alias. * * @default ['Moving Average Convergence Divergence’] * @type {Array} */ macd: ['Moving Average Convergence Divergence'], /** * Money Flow Index alias. * * @default ['Money Flow Index’] * @type {Array} */ mfi: ['Money Flow Index'], /** * Momentum alias. * * @default [‘Momentum’] * @type {Array} */ momentum: ['Momentum'], /** * Normalized Average True Range alias. * * @default ['Normalized Average True Range’] * @type {Array} */ natr: ['Normalized Average True Range'], /** * On-Balance Volume alias. * * @default ['On-Balance Volume’] * @type {Array} */ obv: ['On-Balance Volume'], /** * Percentage Price oscillator alias. * * @default ['Percentage Price oscillator’] * @type {Array} */ ppo: ['Percentage Price oscillator'], /** * Rate of Change alias. * * @default ['Rate of Change’] * @type {Array} */ roc: ['Rate of Change'], /** * Relative Strength Index alias. * * @default ['Relative Strength Index’] * @type {Array} */ rsi: ['Relative Strength Index'], /** * Slow Stochastic alias. * * @default [‘Slow Stochastic’] * @type {Array} */ slowstochastic: ['Slow Stochastic'], /** * Stochastic alias. * * @default [‘Stochastic’] * @type {Array} */ stochastic: ['Stochastic'], /** * TRIX alias. * * @default [‘TRIX’] * @type {Array} */ trix: ['TRIX'], /** * Williams %R alias. * * @default [‘Williams %R’] * @type {Array} */ williamsr: ['Williams %R'] } } } }; /** * Configure the stockTools gui strings in the chart. Requires the * [stockTools module]() to be loaded. For a description of the module * and information on its features, see [Highcharts StockTools](). * * @product highstock * * @sample stock/demo/stock-tools-gui Stock Tools GUI * * @sample stock/demo/stock-tools-custom-gui Stock Tools customized GUI * * @since 7.0.0 * @optionparent stockTools */ const stockTools = { /** * Definitions of buttons in Stock Tools GUI. */ gui: { /** * Path where Highcharts will look for icons. Change this to use * icons from a different server. * * Since 7.1.3 use [iconsURL](#navigation.iconsURL) for popup and * stock tools. * * @deprecated * @apioption stockTools.gui.iconsURL * */ /** * Enable or disable the stockTools gui. */ enabled: true, /** * A CSS class name to apply to the stocktools' div, * allowing unique CSS styling for each chart. */ className: 'highcharts-bindings-wrapper', /** * A CSS class name to apply to the container of buttons, * allowing unique CSS styling for each chart. */ toolbarClassName: 'stocktools-toolbar', /** * A collection of strings pointing to config options for the * toolbar items. Each name refers to a unique key from the * definitions object. * * @type {Array} * @default [ * 'indicators', * 'separator', * 'simpleShapes', * 'lines', * 'crookedLines', * 'measure', * 'advanced', * 'toggleAnnotations', * 'separator', * 'verticalLabels', * 'flags', * 'separator', * 'zoomChange', * 'fullScreen', * 'typeChange', * 'separator', * 'currentPriceIndicator', * 'saveChart' * ] */ buttons: [ 'indicators', 'separator', 'simpleShapes', 'lines', 'crookedLines', 'measure', 'advanced', 'toggleAnnotations', 'separator', 'verticalLabels', 'flags', 'separator', 'zoomChange', 'fullScreen', 'typeChange', 'separator', 'currentPriceIndicator', 'saveChart' ], /** * An options object of the buttons definitions. Each name refers to * unique key from buttons array. */ definitions: { separator: { elementType: 'span', /** * A predefined background symbol for the button. */ symbol: 'separator.svg' }, simpleShapes: { /** * A collection of strings pointing to config options for * the items. * * @type {Array} * @default [ * 'label', * 'circle', * 'ellipse', * 'rectangle' * ] * */ items: [ 'label', 'circle', 'ellipse', 'rectangle' ], circle: { /** * A predefined background symbol for the button. * * @type {string} * */ symbol: 'circle.svg' }, ellipse: { /** * A predefined background symbol for the button. * * @type {string} * */ symbol: 'ellipse.svg' }, rectangle: { /** * A predefined background symbol for the button. * * @type {string} * */ symbol: 'rectangle.svg' }, label: { /** * A predefined background symbol for the button. * * @type {string} * */ symbol: 'label.svg' } }, flags: { /** * A collection of strings pointing to config options for * the items. * * @type {Array} * @default [ * 'flagCirclepin', * 'flagDiamondpin', * 'flagSquarepin', * 'flagSimplepin' * ] * */ items: [ 'flagCirclepin', 'flagDiamondpin', 'flagSquarepin', 'flagSimplepin' ], flagSimplepin: { /** * A predefined background symbol for the button. * * @type {string} * */ symbol: 'flag-basic.svg' }, flagDiamondpin: { /** * A predefined background symbol for the button. * * @type {string} * */ symbol: 'flag-diamond.svg' }, flagSquarepin: { /** * A predefined background symbol for the button. * * @type {string} */ symbol: 'flag-trapeze.svg' }, flagCirclepin: { /** * A predefined background symbol for the button. * * @type {string} */ symbol: 'flag-elipse.svg' } }, lines: { /** * A collection of strings pointing to config options for * the items. * * @type {Array} * @default [ * 'segment', * 'arrowSegment', * 'ray', * 'arrowRay', * 'line', * 'arrowInfinityLine', * 'horizontalLine', * 'verticalLine' * ] */ items: [ 'segment', 'arrowSegment', 'ray', 'arrowRay', 'line', 'arrowInfinityLine', 'horizontalLine', 'verticalLine' ], segment: { /** * A predefined background symbol for the button. * * @type {string} */ symbol: 'segment.svg' }, arrowSegment: { /** * A predefined background symbol for the button. * * @type {string} */ symbol: 'arrow-segment.svg' }, ray: { /** * A predefined background symbol for the button. * * @type {string} */ symbol: 'ray.svg' }, arrowRay: { /** * A predefined background symbol for the button. * * @type {string} */ symbol: 'arrow-ray.svg' }, line: { /** * A predefined background symbol for the button. * * @type {string} */ symbol: 'line.svg' }, arrowInfinityLine: { /** * A predefined background symbol for the button. * * @type {string} */ symbol: 'arrow-line.svg' }, verticalLine: { /** * A predefined background symbol for the button. * * @type {string} */ symbol: 'vertical-line.svg' }, horizontalLine: { /** * A predefined background symbol for the button. * * @type {string} */ symbol: 'horizontal-line.svg' } }, crookedLines: { /** * A collection of strings pointing to config options for * the items. * * @type {Array} * @default [ * 'elliott3', * 'elliott5', * 'crooked3', * 'crooked5' * ] * */ items: [ 'elliott3', 'elliott5', 'crooked3', 'crooked5' ], crooked3: { /** * A predefined background symbol for the button. * * @type {string} */ symbol: 'crooked-3.svg' }, crooked5: { /** * A predefined background symbol for the button. * * @type {string} */ symbol: 'crooked-5.svg' }, elliott3: { /** * A predefined background symbol for the button. * * @type {string} */ symbol: 'elliott-3.svg' }, elliott5: { /** * A predefined background symbol for the button. * * @type {string} */ symbol: 'elliott-5.svg' } }, verticalLabels: { /** * A collection of strings pointing to config options for * the items. * * @type {Array} * @default [ * 'verticalCounter', * 'verticalLabel', * 'verticalArrow' * ] */ items: [ 'verticalCounter', 'verticalLabel', 'verticalArrow' ], verticalCounter: { /** * A predefined background symbol for the button. * * @type {string} */ symbol: 'vertical-counter.svg' }, verticalLabel: { /** * A predefined background symbol for the button. * * @type {string} */ symbol: 'vertical-label.svg' }, verticalArrow: { /** * A predefined background symbol for the button. * * @type {string} */ symbol: 'vertical-arrow.svg' } }, advanced: { /** * A collection of strings pointing to config options for * the items. * * @type {Array} * @default [ * 'fibonacci', * 'fibonacciTimeZones', * 'pitchfork', * 'parallelChannel', * 'timeCycles' * ] */ items: [ 'fibonacci', 'fibonacciTimeZones', 'pitchfork', 'parallelChannel', 'timeCycles' ], pitchfork: { /** * A predefined background symbol for the button. * * @type {string} */ symbol: 'pitchfork.svg' }, fibonacci: { /** * A predefined background symbol for the button. * * @type {string} */ symbol: 'fibonacci.svg' }, fibonacciTimeZones: { /** * A predefined background symbol for the button. * * @type {string} */ symbol: 'fibonacci-timezone.svg' }, parallelChannel: { /** * A predefined background symbol for the button. * * @type {string} */ symbol: 'parallel-channel.svg' }, timeCycles: { /** * A predefined background symbol for the button. * * @type {string} */ symbol: 'time-cycles.svg' } }, measure: { /** * A collection of strings pointing to config options for * the items. * * @type {Array} * @default [ * 'measureXY', * 'measureX', * 'measureY' * ] */ items: [ 'measureXY', 'measureX', 'measureY' ], measureX: { /** * A predefined background symbol for the button. * * @type {string} */ symbol: 'measure-x.svg' }, measureY: { /** * A predefined background symbol for the button. * * @type {string} */ symbol: 'measure-y.svg' }, measureXY: { /** * A predefined background symbol for the button. * * @type {string} */ symbol: 'measure-xy.svg' } }, toggleAnnotations: { /** * A predefined background symbol for the button. * * @type {string} */ symbol: 'annotations-visible.svg' }, currentPriceIndicator: { /** * A predefined background symbol for the button. * * @type {string} */ symbol: 'current-price-show.svg' }, indicators: { /** * A predefined background symbol for the button. * * @type {string} */ symbol: 'indicators.svg' }, zoomChange: { /** * A collection of strings pointing to config options for * the items. * * @type {Array} * @default [ * 'zoomX', * 'zoomY', * 'zoomXY' * ] */ items: [ 'zoomX', 'zoomY', 'zoomXY' ], zoomX: { /** * A predefined background symbol for the button. * * @type {string} */ symbol: 'zoom-x.svg' }, zoomY: { /** * A predefined background symbol for the button. * * @type {string} */ symbol: 'zoom-y.svg' }, zoomXY: { /** * A predefined background symbol for the button. * * @type {string} */ symbol: 'zoom-xy.svg' } }, typeChange: { /** * A collection of strings pointing to config options for * the items. * * @type {Array} * @default [ * 'typeOHLC', * 'typeLine', * 'typeCandlestick' * 'typeHollowCandlestick' * ] */ items: [ 'typeOHLC', 'typeLine', 'typeCandlestick', 'typeHollowCandlestick', 'typeHLC', 'typeHeikinAshi' ], typeOHLC: { /** * A predefined background symbol for the button. * * @type {string} */ symbol: 'series-ohlc.svg' }, typeLine: { /** * A predefined background symbol for the button. * * @type {string} */ symbol: 'series-line.svg' }, typeCandlestick: { /** * A predefined background symbol for the button. * * @type {string} */ symbol: 'series-candlestick.svg' }, typeHLC: { /** * A predefined background symbol for the button. * * @type {string} */ symbol: 'series-hlc.svg' }, typeHeikinAshi: { /** * A predefined background symbol for the button. * * @type {string} */ symbol: 'series-heikin-ashi.svg' }, typeHollowCandlestick: { /** * A predefined background symbol for the button. * * @type {string} */ symbol: 'series-hollow-candlestick.svg' } }, fullScreen: { /** * A predefined background symbol for the button. * * @type {string} */ symbol: 'fullscreen.svg' }, saveChart: { /** * A predefined background symbol for the button. * * @type {string} */ symbol: 'save-chart.svg' } } } }; /* * * * Default Exports * * */ const StockToolsDefaults = { lang, stockTools }; return StockToolsDefaults; }); _registerModule(_modules, 'Stock/StockTools/StockTools.js', [_modules['Core/Defaults.js'], _modules['Extensions/Annotations/NavigationBindingsUtilities.js'], _modules['Stock/StockTools/StockToolsBindings.js'], _modules['Stock/StockTools/StockToolsDefaults.js'], _modules['Stock/StockTools/StockToolsUtilities.js'], _modules['Core/Utilities.js']], function (D, NBU, StockToolsBindings, StockToolsDefaults, STU, U) { /** * * Events generator for Stock tools * * (c) 2009-2024 Paweł Fus * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ const { setOptions } = D; const { getAssignedAxis } = NBU; const { isNotNavigatorYAxis, isPriceIndicatorEnabled } = STU; const { correctFloat, defined, isNumber, pick } = U; /* * * * Functions * * */ /** * @private */ function compose(NavigationBindingsClass) { const navigationProto = NavigationBindingsClass.prototype; if (!navigationProto.utils?.manageIndicators) { // Extends NavigationBindings to support indicators and resizers: navigationProto.getYAxisPositions = navigationGetYAxisPositions; navigationProto.getYAxisResizers = navigationGetYAxisResizers; navigationProto.recalculateYAxisPositions = navigationRecalculateYAxisPositions; navigationProto.resizeYAxes = navigationResizeYAxes; navigationProto.utils = navigationProto.utils || {}; navigationProto.utils.indicatorsWithAxes = STU.indicatorsWithAxes; navigationProto.utils.indicatorsWithVolume = STU.indicatorsWithVolume; navigationProto.utils.getAssignedAxis = getAssignedAxis; navigationProto.utils.isPriceIndicatorEnabled = isPriceIndicatorEnabled; navigationProto.utils.manageIndicators = STU.manageIndicators; setOptions(StockToolsDefaults); setOptions({ navigation: { bindings: StockToolsBindings } }); } } /** * Get current positions for all yAxes. If new axis does not have position, * returned is default height and last available top place. * * @private * @function Highcharts.NavigationBindings#getYAxisPositions * * @param {Array} yAxes * Array of yAxes available in the chart. * * @param {number} plotHeight * Available height in the chart. * * @param {number} defaultHeight * Default height in percents. * * @param {Highcharts.AxisPositions} removedYAxisProps * Height and top value of the removed yAxis in percents. * * @return {Highcharts.YAxisPositions} * An object containing an array of calculated positions * in percentages. Format: `{top: Number, height: Number}` * and maximum value of top + height of axes. */ function navigationGetYAxisPositions(yAxes, plotHeight, defaultHeight, removedYAxisProps) { let allAxesHeight = 0, previousAxisHeight, removedHeight, removedTop; /** @private */ function isPercentage(prop) { return defined(prop) && !isNumber(prop) && prop.match('%'); } if (removedYAxisProps) { removedTop = correctFloat((parseFloat(removedYAxisProps.top) / 100)); removedHeight = correctFloat((parseFloat(removedYAxisProps.height) / 100)); } const positions = yAxes.map((yAxis, index) => { let height = correctFloat(isPercentage(yAxis.options.height) ? parseFloat(yAxis.options.height) / 100 : yAxis.height / plotHeight), top = correctFloat(isPercentage(yAxis.options.top) ? parseFloat(yAxis.options.top) / 100 : (yAxis.top - yAxis.chart.plotTop) / plotHeight); if (!removedHeight) { // New axis' height is NaN so we can check if // the axis is newly created this way if (!isNumber(height)) { // Check if the previous axis is the // indicator axis (every indicator inherits from sma) height = yAxes[index - 1].series .every((s) => s.is('sma')) ? previousAxisHeight : defaultHeight / 100; } if (!isNumber(top)) { top = allAxesHeight; } previousAxisHeight = height; allAxesHeight = correctFloat(Math.max(allAxesHeight, (top || 0) + (height || 0))); } else { // Move all axes which were below the removed axis up. if (top > removedTop) { top -= removedHeight; } allAxesHeight = Math.max(allAxesHeight, (top || 0) + (height || 0)); } return { height: height * 100, top: top * 100 }; }); return { positions, allAxesHeight }; } /** * Get current resize options for each yAxis. Note that each resize is * linked to the next axis, except the last one which shouldn't affect * axes in the navigator. Because indicator can be removed with it's yAxis * in the middle of yAxis array, we need to bind closest yAxes back. * * @private * @function Highcharts.NavigationBindings#getYAxisResizers * * @param {Array} yAxes * Array of yAxes available in the chart * * @return {Array} * An array of resizer options. * Format: `{enabled: Boolean, controlledAxis: { next: [String]}}` */ function navigationGetYAxisResizers(yAxes) { const resizers = []; yAxes.forEach(function (_yAxis, index) { const nextYAxis = yAxes[index + 1]; // We have next axis, bind them: if (nextYAxis) { resizers[index] = { enabled: true, controlledAxis: { next: [ pick(nextYAxis.options.id, nextYAxis.index) ] } }; } else { // Remove binding: resizers[index] = { enabled: false }; } }); return resizers; } /** * Utility to modify calculated positions according to the remaining/needed * space. Later, these positions are used in `yAxis.update({ top, height })` * * @private * @function Highcharts.NavigationBindings#recalculateYAxisPositions * @param {Array>} positions * Default positions of all yAxes. * @param {number} changedSpace * How much space should be added or removed. * @param {boolean} modifyHeight * Update only `top` or both `top` and `height`. * @param {number} adder * `-1` or `1`, to determine whether we should add or remove space. * * @return {Array} * Modified positions, */ function navigationRecalculateYAxisPositions(positions, changedSpace, modifyHeight, adder) { positions.forEach(function (position, index) { const prevPosition = positions[index - 1]; position.top = !prevPosition ? 0 : correctFloat(prevPosition.height + prevPosition.top); if (modifyHeight) { position.height = correctFloat(position.height + adder * changedSpace); } }); return positions; } /** * Resize all yAxes (except navigator) to fit the plotting height. Method * checks if new axis is added, if the new axis will fit under previous * axes it is placed there. If not, current plot area is scaled * to make room for new axis. * * If axis is removed, the current plot area stretches to fit into 100% * of the plot area. * * @private */ function navigationResizeYAxes(removedYAxisProps) { // The height of the new axis before rescalling. In %, but as a number. const defaultHeight = 20; const chart = this.chart, // Only non-navigator axes yAxes = chart.yAxis.filter(isNotNavigatorYAxis), plotHeight = chart.plotHeight, // Gather current heights (in %) { positions, allAxesHeight } = this.getYAxisPositions(yAxes, plotHeight, defaultHeight, removedYAxisProps), resizers = this.getYAxisResizers(yAxes); // Check if the axis is being either added or removed and // if the new indicator axis will fit under existing axes. // if so, there is no need to scale them. if (!removedYAxisProps && allAxesHeight <= correctFloat(0.8 + defaultHeight / 100)) { positions[positions.length - 1] = { height: defaultHeight, top: correctFloat(allAxesHeight * 100 - defaultHeight) }; } else { positions.forEach(function (position) { position.height = (position.height / (allAxesHeight * 100)) * 100; position.top = (position.top / (allAxesHeight * 100)) * 100; }); } positions.forEach(function (position, index) { yAxes[index].update({ height: position.height + '%', top: position.top + '%', resize: resizers[index], offset: 0 }, false); }); } /* * * * Default Export * * */ const StockTools = { compose }; return StockTools; }); _registerModule(_modules, 'Stock/StockTools/StockToolbar.js', [_modules['Core/Utilities.js']], function (U) { /* * * * GUI generator for Stock tools * * (c) 2009-2024 Sebastian Bochan * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ const { addEvent, createElement, css, fireEvent, getStyle, isArray, merge, pick } = U; /* * * * Classes * * */ /** * Toolbar Class * * @private * @class * * @param {object} options * Options of toolbar * * @param {Highcharts.Dictionary|undefined} langOptions * Language options * * @param {Highcharts.Chart} chart * Reference to chart */ class Toolbar { /* * * * Constructor * * */ constructor(options, langOptions, chart) { this.chart = chart; this.options = options; this.lang = langOptions; // Set url for icons. this.iconsURL = this.getIconsURL(); this.guiEnabled = options.enabled; this.visible = pick(options.visible, true); this.placed = pick(options.placed, false); // General events collection which should be removed upon // destroy/update: this.eventsToUnbind = []; if (this.guiEnabled) { this.createHTML(); this.init(); this.showHideNavigatorion(); } fireEvent(this, 'afterInit'); } /* * * * Functions * * */ /** * Initialize the toolbar. Create buttons and submenu for each option * defined in `stockTools.gui`. * @private */ init() { const lang = this.lang, guiOptions = this.options, toolbar = this.toolbar, buttons = guiOptions.buttons, defs = guiOptions.definitions, allButtons = toolbar.childNodes; // Create buttons buttons.forEach((btnName) => { const button = this.addButton(toolbar, defs, btnName, lang); this.eventsToUnbind.push(addEvent(button.buttonWrapper, 'click', () => this.eraseActiveButtons(allButtons, button.buttonWrapper))); if (isArray(defs[btnName].items)) { // Create submenu buttons this.addSubmenu(button, defs[btnName]); } }); } /** * Create submenu (list of buttons) for the option. In example main button * is Line, in submenu will be buttons with types of lines. * * @private * * @param {Highcharts.Dictionary} parentBtn * Button which has submenu * * @param {Highcharts.StockToolsGuiDefinitionsButtonsOptions} button * List of all buttons */ addSubmenu(parentBtn, button) { const submenuArrow = parentBtn.submenuArrow, buttonWrapper = parentBtn.buttonWrapper, buttonWidth = getStyle(buttonWrapper, 'width'), wrapper = this.wrapper, menuWrapper = this.listWrapper, allButtons = this.toolbar.childNodes, // Create submenu container submenuWrapper = this.submenu = createElement('ul', { className: 'highcharts-submenu-wrapper' }, void 0, buttonWrapper); // Create submenu buttons and select the first one this.addSubmenuItems(buttonWrapper, button); // Show / hide submenu this.eventsToUnbind.push(addEvent(submenuArrow, 'click', (e) => { e.stopPropagation(); // Erase active class on all other buttons this.eraseActiveButtons(allButtons, buttonWrapper); // Hide menu if (buttonWrapper.className .indexOf('highcharts-current') >= 0) { menuWrapper.style.width = menuWrapper.startWidth + 'px'; buttonWrapper.classList.remove('highcharts-current'); submenuWrapper.style.display = 'none'; } else { // Show menu // to calculate height of element submenuWrapper.style.display = 'block'; let topMargin = submenuWrapper.offsetHeight - buttonWrapper.offsetHeight - 3; // Calculate position of submenu in the box // if submenu is inside, reset top margin if ( // Cut on the bottom !(submenuWrapper.offsetHeight + buttonWrapper.offsetTop > wrapper.offsetHeight && // Cut on the top buttonWrapper.offsetTop > topMargin)) { topMargin = 0; } // Apply calculated styles css(submenuWrapper, { top: -topMargin + 'px', left: buttonWidth + 3 + 'px' }); buttonWrapper.className += ' highcharts-current'; menuWrapper.startWidth = wrapper.offsetWidth; menuWrapper.style.width = menuWrapper.startWidth + getStyle(menuWrapper, 'padding-left') + submenuWrapper.offsetWidth + 3 + 'px'; } })); } /** * Create buttons in submenu * * @private * * @param {Highcharts.HTMLDOMElement} buttonWrapper * Button where submenu is placed * * @param {Highcharts.StockToolsGuiDefinitionsButtonsOptions} button * List of all buttons options */ addSubmenuItems(buttonWrapper, button) { const _self = this, submenuWrapper = this.submenu, lang = this.lang, menuWrapper = this.listWrapper, items = button.items; let submenuBtn; // Add items to submenu items.forEach((btnName) => { // Add buttons to submenu submenuBtn = this.addButton(submenuWrapper, button, btnName, lang); this.eventsToUnbind.push(addEvent(submenuBtn.mainButton, 'click', function () { _self.switchSymbol(this, buttonWrapper, true); menuWrapper.style.width = menuWrapper.startWidth + 'px'; submenuWrapper.style.display = 'none'; })); }); // Select first submenu item const firstSubmenuItem = submenuWrapper.querySelectorAll('li > .highcharts-menu-item-btn')[0]; // Replace current symbol, in main button, with submenu's button style this.switchSymbol(firstSubmenuItem, false); } /** * Erase active class on all other buttons. * @private */ eraseActiveButtons(buttons, currentButton, submenuItems) { [].forEach.call(buttons, (btn) => { if (btn !== currentButton) { btn.classList.remove('highcharts-current'); btn.classList.remove('highcharts-active'); submenuItems = btn.querySelectorAll('.highcharts-submenu-wrapper'); // Hide submenu if (submenuItems.length > 0) { submenuItems[0].style.display = 'none'; } } }); } /** * Create single button. Consist of HTML elements `li`, `button`, and (if * exists) submenu container. * * @private * * @param {Highcharts.HTMLDOMElement} target * HTML reference, where button should be added * * @param {object} options * All options, by btnName refer to particular button * * @param {string} btnName * Button name of functionality mapped for specific class * * @param {Highcharts.Dictionary} lang * All titles, by btnName refer to particular button * * @return {object} * References to all created HTML elements */ addButton(target, options, btnName, lang = {}) { const btnOptions = options[btnName], items = btnOptions.items, classMapping = Toolbar.prototype.classMapping, userClassName = btnOptions.className || ''; // Main button wrapper const buttonWrapper = createElement('li', { className: pick(classMapping[btnName], '') + ' ' + userClassName, title: lang[btnName] || btnName }, void 0, target); // Single button const elementType = (btnOptions.elementType || 'button'); const mainButton = createElement(elementType, { className: 'highcharts-menu-item-btn' }, void 0, buttonWrapper); // Submenu if (items && items.length) { // Arrow is a hook to show / hide submenu const submenuArrow = createElement('button', { className: 'highcharts-submenu-item-arrow ' + 'highcharts-arrow-right' }, void 0, buttonWrapper); submenuArrow.style.backgroundImage = 'url(' + this.iconsURL + 'arrow-bottom.svg)'; return { buttonWrapper, mainButton, submenuArrow }; } mainButton.style.backgroundImage = 'url(' + this.iconsURL + btnOptions.symbol + ')'; return { buttonWrapper, mainButton }; } /** * Create navigation's HTML elements: container and arrows. * @private */ addNavigation() { const wrapper = this.wrapper; // Arrow wrapper this.arrowWrapper = createElement('div', { className: 'highcharts-arrow-wrapper' }); this.arrowUp = createElement('div', { className: 'highcharts-arrow-up' }, void 0, this.arrowWrapper); this.arrowUp.style.backgroundImage = 'url(' + this.iconsURL + 'arrow-right.svg)'; this.arrowDown = createElement('div', { className: 'highcharts-arrow-down' }, void 0, this.arrowWrapper); this.arrowDown.style.backgroundImage = 'url(' + this.iconsURL + 'arrow-right.svg)'; wrapper.insertBefore(this.arrowWrapper, wrapper.childNodes[0]); // Attach scroll events this.scrollButtons(); } /** * Add events to navigation (two arrows) which allows user to scroll * top/down GUI buttons, if container's height is not enough. * @private */ scrollButtons() { const wrapper = this.wrapper, toolbar = this.toolbar, step = 0.1 * wrapper.offsetHeight; // 0.1 = 10% let targetY = 0; this.eventsToUnbind.push(addEvent(this.arrowUp, 'click', () => { if (targetY > 0) { targetY -= step; toolbar.style.marginTop = -targetY + 'px'; } })); this.eventsToUnbind.push(addEvent(this.arrowDown, 'click', () => { if (wrapper.offsetHeight + targetY <= toolbar.offsetHeight + step) { targetY += step; toolbar.style.marginTop = -targetY + 'px'; } })); } /* * Create stockTools HTML main elements. * */ createHTML() { const chart = this.chart, guiOptions = this.options, container = chart.container, navigation = chart.options.navigation, bindingsClassName = navigation && navigation.bindingsClassName; let listWrapper, toolbar; // Create main container const wrapper = this.wrapper = createElement('div', { className: 'highcharts-stocktools-wrapper ' + guiOptions.className + ' ' + bindingsClassName }); container.appendChild(wrapper); // Mimic event behaviour of being outside chart.container [ 'mousedown', 'mousemove', 'click', 'touchstart' ].forEach((eventType) => { addEvent(wrapper, eventType, (e) => e.stopPropagation()); }); addEvent(wrapper, 'mouseover', (e) => chart.pointer?.onContainerMouseLeave(e)); // Toolbar this.toolbar = toolbar = createElement('ul', { className: 'highcharts-stocktools-toolbar ' + guiOptions.toolbarClassName }); // Add container for list of buttons this.listWrapper = listWrapper = createElement('div', { className: 'highcharts-menu-wrapper' }); wrapper.insertBefore(listWrapper, wrapper.childNodes[0]); listWrapper.insertBefore(toolbar, listWrapper.childNodes[0]); this.showHideToolbar(); // Add navigation which allows user to scroll down / top GUI buttons this.addNavigation(); } /** * Function called in redraw verifies if the navigation should be visible. * @private */ showHideNavigatorion() { // Arrows // 50px space for arrows if (this.visible && this.toolbar.offsetHeight > (this.wrapper.offsetHeight - 50)) { this.arrowWrapper.style.display = 'block'; } else { // Reset margin if whole toolbar is visible this.toolbar.style.marginTop = '0px'; // Hide arrows this.arrowWrapper.style.display = 'none'; } } /** * Create button which shows or hides GUI toolbar. * @private */ showHideToolbar() { const chart = this.chart, wrapper = this.wrapper, toolbar = this.listWrapper, submenu = this.submenu, // Show hide toolbar showhideBtn = this.showhideBtn = createElement('div', { className: 'highcharts-toggle-toolbar highcharts-arrow-left' }, void 0, wrapper); let visible = this.visible; showhideBtn.style.backgroundImage = 'url(' + this.iconsURL + 'arrow-right.svg)'; if (!visible) { // Hide if (submenu) { submenu.style.display = 'none'; } showhideBtn.style.left = '0px'; visible = this.visible = false; toolbar.classList.add('highcharts-hide'); showhideBtn.classList.toggle('highcharts-arrow-right'); wrapper.style.height = showhideBtn.offsetHeight + 'px'; } else { wrapper.style.height = '100%'; showhideBtn.style.top = getStyle(toolbar, 'padding-top') + 'px'; showhideBtn.style.left = (wrapper.offsetWidth + getStyle(toolbar, 'padding-left')) + 'px'; } // Toggle menu this.eventsToUnbind.push(addEvent(showhideBtn, 'click', () => { chart.update({ stockTools: { gui: { visible: !visible, placed: true } } }); })); } /* * In main GUI button, replace icon and class with submenu button's * class / symbol. * * @param {HTMLDOMElement} - submenu button * @param {Boolean} - true or false * */ switchSymbol(button, redraw) { const buttonWrapper = button.parentNode, buttonWrapperClass = buttonWrapper.className, // Main button in first level og GUI mainNavButton = buttonWrapper.parentNode.parentNode; // If the button is disabled, don't do anything if (buttonWrapperClass.indexOf('highcharts-disabled-btn') > -1) { return; } // Set class mainNavButton.className = ''; if (buttonWrapperClass) { mainNavButton.classList.add(buttonWrapperClass.trim()); } // Set icon mainNavButton .querySelectorAll('.highcharts-menu-item-btn')[0] .style.backgroundImage = button.style.backgroundImage; // Set active class if (redraw) { this.toggleButtonActiveClass(mainNavButton); } } /** * Set select state (active class) on button. * @private */ toggleButtonActiveClass(button) { const classList = button.classList; if (classList.contains('highcharts-active')) { classList.remove('highcharts-active'); } else { classList.add('highcharts-active'); } } /** * Remove active class from all buttons except defined. * @private */ unselectAllButtons(button) { const activeBtns = button.parentNode .querySelectorAll('.highcharts-active'); [].forEach.call(activeBtns, (activeBtn) => { if (activeBtn !== button) { activeBtn.classList.remove('highcharts-active'); } }); } /** * Update GUI with given options. * @private */ update(options, redraw) { merge(true, this.chart.options.stockTools, options); this.destroy(); this.chart.setStockTools(options); // If Stock Tools are updated, then bindings should be updated too: if (this.chart.navigationBindings) { this.chart.navigationBindings.update(); } this.chart.isDirtyBox = true; if (pick(redraw, true)) { this.chart.redraw(); } } /** * Destroy all HTML GUI elements. * @private */ destroy() { const stockToolsDiv = this.wrapper, parent = stockToolsDiv && stockToolsDiv.parentNode; this.eventsToUnbind.forEach((unbinder) => unbinder()); // Remove the empty element if (parent) { parent.removeChild(stockToolsDiv); } } /** * Redraw, GUI requires to verify if the navigation should be visible. * @private */ redraw() { this.showHideNavigatorion(); } /** * @private */ getIconsURL() { return this.chart.options.navigation.iconsURL || this.options.iconsURL || 'https://code.highcharts.com/11.4.1/gfx/stock-icons/'; } } Toolbar.prototype.classMapping = { circle: 'highcharts-circle-annotation', ellipse: 'highcharts-ellipse-annotation', rectangle: 'highcharts-rectangle-annotation', label: 'highcharts-label-annotation', segment: 'highcharts-segment', arrowSegment: 'highcharts-arrow-segment', ray: 'highcharts-ray', arrowRay: 'highcharts-arrow-ray', line: 'highcharts-infinity-line', arrowInfinityLine: 'highcharts-arrow-infinity-line', verticalLine: 'highcharts-vertical-line', horizontalLine: 'highcharts-horizontal-line', crooked3: 'highcharts-crooked3', crooked5: 'highcharts-crooked5', elliott3: 'highcharts-elliott3', elliott5: 'highcharts-elliott5', pitchfork: 'highcharts-pitchfork', fibonacci: 'highcharts-fibonacci', fibonacciTimeZones: 'highcharts-fibonacci-time-zones', parallelChannel: 'highcharts-parallel-channel', measureX: 'highcharts-measure-x', measureY: 'highcharts-measure-y', measureXY: 'highcharts-measure-xy', timeCycles: 'highcharts-time-cycles', verticalCounter: 'highcharts-vertical-counter', verticalLabel: 'highcharts-vertical-label', verticalArrow: 'highcharts-vertical-arrow', currentPriceIndicator: 'highcharts-current-price-indicator', indicators: 'highcharts-indicators', flagCirclepin: 'highcharts-flag-circlepin', flagDiamondpin: 'highcharts-flag-diamondpin', flagSquarepin: 'highcharts-flag-squarepin', flagSimplepin: 'highcharts-flag-simplepin', zoomX: 'highcharts-zoom-x', zoomY: 'highcharts-zoom-y', zoomXY: 'highcharts-zoom-xy', typeLine: 'highcharts-series-type-line', typeOHLC: 'highcharts-series-type-ohlc', typeHLC: 'highcharts-series-type-hlc', typeCandlestick: 'highcharts-series-type-candlestick', typeHollowCandlestick: 'highcharts-series-type-hollowcandlestick', typeHeikinAshi: 'highcharts-series-type-heikinashi', fullScreen: 'highcharts-full-screen', toggleAnnotations: 'highcharts-toggle-annotations', saveChart: 'highcharts-save-chart', separator: 'highcharts-separator' }; /* * * * Default Export * * */ return Toolbar; }); _registerModule(_modules, 'Stock/StockTools/StockToolsGui.js', [_modules['Core/Defaults.js'], _modules['Stock/StockTools/StockToolsDefaults.js'], _modules['Stock/StockTools/StockToolbar.js'], _modules['Core/Utilities.js']], function (D, StockToolsDefaults, Toolbar, U) { /* * * * GUI generator for Stock tools * * (c) 2009-2024 Sebastian Bochan * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ const { setOptions } = D; const { addEvent, getStyle, merge, pick } = U; /* * * * Functions * * */ /** * Verify if Toolbar should be added. * @private */ function chartSetStockTools(options) { const chartOptions = this.options, lang = chartOptions.lang, guiOptions = merge(chartOptions.stockTools && chartOptions.stockTools.gui, options && options.gui), langOptions = lang && lang.stockTools && lang.stockTools.gui; this.stockTools = new Toolbar(guiOptions, langOptions, this); if (this.stockTools.guiEnabled) { this.isDirtyBox = true; } } /** * @private */ function compose(ChartClass, NavigationBindingsClass) { const chartProto = ChartClass.prototype; if (!chartProto.setStockTools) { addEvent(ChartClass, 'afterGetContainer', onChartAfterGetContainer); addEvent(ChartClass, 'beforeRedraw', onChartBeforeRedraw); addEvent(ChartClass, 'beforeRender', onChartBeforeRedraw); addEvent(ChartClass, 'destroy', onChartDestroy); addEvent(ChartClass, 'getMargins', onChartGetMargins, { order: 0 }); addEvent(ChartClass, 'redraw', onChartRedraw); addEvent(ChartClass, 'render', onChartRender); chartProto.setStockTools = chartSetStockTools; addEvent(NavigationBindingsClass, 'deselectButton', onNavigationBindingsDeselectButton); addEvent(NavigationBindingsClass, 'selectButton', onNavigationBindingsSelectButton); setOptions(StockToolsDefaults); } } /** * Run HTML generator * @private */ function onChartAfterGetContainer() { this.setStockTools(); } /** * Handle beforeRedraw and beforeRender * @private */ function onChartBeforeRedraw() { if (this.stockTools) { const optionsChart = this.options.chart; const listWrapper = this.stockTools.listWrapper, offsetWidth = listWrapper && ((listWrapper.startWidth + getStyle(listWrapper, 'padding-left') + getStyle(listWrapper, 'padding-right')) || listWrapper.offsetWidth); let dirty = false; if (offsetWidth && offsetWidth < this.plotWidth) { const nextX = pick(optionsChart.spacingLeft, optionsChart.spacing && optionsChart.spacing[3], 0) + offsetWidth; const diff = nextX - this.spacingBox.x; this.spacingBox.x = nextX; this.spacingBox.width -= diff; dirty = true; } else if (offsetWidth === 0) { dirty = true; } if (offsetWidth !== this.stockTools.prevOffsetWidth) { this.stockTools.prevOffsetWidth = offsetWidth; if (dirty) { this.isDirtyLegend = true; } } } } /** * @private */ function onChartDestroy() { if (this.stockTools) { this.stockTools.destroy(); } } /** * @private */ function onChartGetMargins() { const listWrapper = this.stockTools && this.stockTools.listWrapper, offsetWidth = listWrapper && ((listWrapper.startWidth + getStyle(listWrapper, 'padding-left') + getStyle(listWrapper, 'padding-right')) || listWrapper.offsetWidth); if (offsetWidth && offsetWidth < this.plotWidth) { this.plotLeft += offsetWidth; this.spacing[3] += offsetWidth; } } /** * @private */ function onChartRedraw() { if (this.stockTools && this.stockTools.guiEnabled) { this.stockTools.redraw(); } } /** * Check if the correct price indicator button is displayed, #15029. * @private */ function onChartRender() { const stockTools = this.stockTools, button = stockTools && stockTools.toolbar && stockTools.toolbar.querySelector('.highcharts-current-price-indicator'); // Change the initial button background. if (stockTools && this.navigationBindings && this.options.series && button) { if (this.navigationBindings.utils ?.isPriceIndicatorEnabled?.(this.series)) { button.firstChild.style['background-image'] = 'url("' + stockTools.getIconsURL() + 'current-price-hide.svg")'; } else { button.firstChild.style['background-image'] = 'url("' + stockTools.getIconsURL() + 'current-price-show.svg")'; } } } /** * @private */ function onNavigationBindingsDeselectButton(event) { const className = 'highcharts-submenu-wrapper', gui = this.chart.stockTools; if (gui && gui.guiEnabled) { let button = event.button; // If deselecting a button from a submenu, select state for it's parent if (button.parentNode.className.indexOf(className) >= 0) { button = button.parentNode.parentNode; } // Set active class on the current button gui.toggleButtonActiveClass(button); } } /** * Communication with bindings * @private */ function onNavigationBindingsSelectButton(event) { const className = 'highcharts-submenu-wrapper', gui = this.chart.stockTools; if (gui && gui.guiEnabled) { let button = event.button; // Unselect other active buttons gui.unselectAllButtons(event.button); // If clicked on a submenu, select state for it's parent if (button.parentNode.className.indexOf(className) >= 0) { button = button.parentNode.parentNode; } // Set active class on the current button gui.toggleButtonActiveClass(button); } } /* * * * Default Export * * */ const StockToolsGui = { compose }; return StockToolsGui; }); _registerModule(_modules, 'masters/modules/stock-tools.src.js', [_modules['Core/Globals.js'], _modules['Extensions/Annotations/NavigationBindings.js'], _modules['Stock/StockTools/StockTools.js'], _modules['Stock/StockTools/StockToolsGui.js'], _modules['Stock/StockTools/StockToolbar.js']], function (Highcharts, NavigationBindings, StockTools, StockToolsGui, Toolbar) { const G = Highcharts; G.NavigationBindings = G.NavigationBindings || NavigationBindings; G.Toolbar = Toolbar; StockTools.compose(G.NavigationBindings); StockToolsGui.compose(G.Chart, G.NavigationBindings); return Highcharts; }); }));