/** * @license Highcharts JS v8.1.0 (2020-05-05) * * Sonification module * * (c) 2012-2019 Øystein Moseng * * License: www.highcharts.com/license */ 'use strict'; (function (factory) { if (typeof module === 'object' && module.exports) { factory['default'] = factory; module.exports = factory; } else if (typeof define === 'function' && define.amd) { define('highcharts/modules/sonification', ['highcharts'], function (Highcharts) { factory(Highcharts); factory.Highcharts = Highcharts; return factory; }); } else { factory(typeof Highcharts !== 'undefined' ? Highcharts : undefined); } }(function (Highcharts) { var _modules = Highcharts ? Highcharts._modules : {}; function _registerModule(obj, path, args, fn) { if (!obj.hasOwnProperty(path)) { obj[path] = fn.apply(null, args); } } _registerModule(_modules, 'modules/sonification/Instrument.js', [_modules['parts/Globals.js'], _modules['parts/Utilities.js']], function (H, U) { /* * * * (c) 2009-2020 Øystein Moseng * * Instrument class for sonification module. * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ var error = U.error, merge = U.merge, pick = U.pick, uniqueKey = U.uniqueKey; /** * A set of options for the Instrument class. * * @requires module:modules/sonification * * @interface Highcharts.InstrumentOptionsObject */ /** * The type of instrument. Currently only `oscillator` is supported. Defaults * to `oscillator`. * @name Highcharts.InstrumentOptionsObject#type * @type {string|undefined} */ /** * The unique ID of the instrument. Generated if not supplied. * @name Highcharts.InstrumentOptionsObject#id * @type {string|undefined} */ /** * When using functions to determine frequency or other parameters during * playback, this options specifies how often to call the callback functions. * Number given in milliseconds. Defaults to 20. * @name Highcharts.InstrumentOptionsObject#playCallbackInterval * @type {number|undefined} */ /** * A list of allowed frequencies for this instrument. If trying to play a * frequency not on this list, the closest frequency will be used. Set to `null` * to allow all frequencies to be used. Defaults to `null`. * @name Highcharts.InstrumentOptionsObject#allowedFrequencies * @type {Array|undefined} */ /** * Options specific to oscillator instruments. * @name Highcharts.InstrumentOptionsObject#oscillator * @type {Highcharts.OscillatorOptionsObject|undefined} */ /** * Options for playing an instrument. * * @requires module:modules/sonification * * @interface Highcharts.InstrumentPlayOptionsObject */ /** * The frequency of the note to play. Can be a fixed number, or a function. The * function receives one argument: the relative time of the note playing (0 * being the start, and 1 being the end of the note). It should return the * frequency number for each point in time. The poll interval of this function * is specified by the Instrument.playCallbackInterval option. * @name Highcharts.InstrumentPlayOptionsObject#frequency * @type {number|Function} */ /** * The duration of the note in milliseconds. * @name Highcharts.InstrumentPlayOptionsObject#duration * @type {number} */ /** * The minimum frequency to allow. If the instrument has a set of allowed * frequencies, the closest frequency is used by default. Use this option to * stop too low frequencies from being used. * @name Highcharts.InstrumentPlayOptionsObject#minFrequency * @type {number|undefined} */ /** * The maximum frequency to allow. If the instrument has a set of allowed * frequencies, the closest frequency is used by default. Use this option to * stop too high frequencies from being used. * @name Highcharts.InstrumentPlayOptionsObject#maxFrequency * @type {number|undefined} */ /** * The volume of the instrument. Can be a fixed number between 0 and 1, or a * function. The function receives one argument: the relative time of the note * playing (0 being the start, and 1 being the end of the note). It should * return the volume for each point in time. The poll interval of this function * is specified by the Instrument.playCallbackInterval option. Defaults to 1. * @name Highcharts.InstrumentPlayOptionsObject#volume * @type {number|Function|undefined} */ /** * The panning of the instrument. Can be a fixed number between -1 and 1, or a * function. The function receives one argument: the relative time of the note * playing (0 being the start, and 1 being the end of the note). It should * return the panning value for each point in time. The poll interval of this * function is specified by the Instrument.playCallbackInterval option. * Defaults to 0. * @name Highcharts.InstrumentPlayOptionsObject#pan * @type {number|Function|undefined} */ /** * Callback function to be called when the play is completed. * @name Highcharts.InstrumentPlayOptionsObject#onEnd * @type {Function|undefined} */ /** * @requires module:modules/sonification * * @interface Highcharts.OscillatorOptionsObject */ /** * The waveform shape to use for oscillator instruments. Defaults to `sine`. * @name Highcharts.OscillatorOptionsObject#waveformShape * @type {string|undefined} */ // Default options for Instrument constructor var defaultOptions = { type: 'oscillator', playCallbackInterval: 20, oscillator: { waveformShape: 'sine' } }; /* eslint-disable no-invalid-this, valid-jsdoc */ /** * The Instrument class. Instrument objects represent an instrument capable of * playing a certain pitch for a specified duration. * * @sample highcharts/sonification/instrument/ * Using Instruments directly * @sample highcharts/sonification/instrument-advanced/ * Using callbacks for instrument parameters * * @requires module:modules/sonification * * @class * @name Highcharts.Instrument * * @param {Highcharts.InstrumentOptionsObject} options * Options for the instrument instance. */ function Instrument(options) { this.init(options); } Instrument.prototype.init = function (options) { if (!this.initAudioContext()) { error(29); return; } this.options = merge(defaultOptions, options); this.id = this.options.id = options && options.id || uniqueKey(); // Init the audio nodes var ctx = H.audioContext; this.gainNode = ctx.createGain(); this.setGain(0); this.panNode = ctx.createStereoPanner && ctx.createStereoPanner(); if (this.panNode) { this.setPan(0); this.gainNode.connect(this.panNode); this.panNode.connect(ctx.destination); } else { this.gainNode.connect(ctx.destination); } // Oscillator initialization if (this.options.type === 'oscillator') { this.initOscillator(this.options.oscillator); } // Init timer list this.playCallbackTimers = []; }; /** * Return a copy of an instrument. Only one instrument instance can play at a * time, so use this to get a new copy of the instrument that can play alongside * it. The new instrument copy will receive a new ID unless one is supplied in * options. * * @function Highcharts.Instrument#copy * * @param {Highcharts.InstrumentOptionsObject} [options] * Options to merge in for the copy. * * @return {Highcharts.Instrument} * A new Instrument instance with the same options. */ Instrument.prototype.copy = function (options) { return new Instrument(merge(this.options, { id: null }, options)); }; /** * Init the audio context, if we do not have one. * @private * @return {boolean} True if successful, false if not. */ Instrument.prototype.initAudioContext = function () { var Context = H.win.AudioContext || H.win.webkitAudioContext, hasOldContext = !!H.audioContext; if (Context) { H.audioContext = H.audioContext || new Context(); if (!hasOldContext && H.audioContext && H.audioContext.state === 'running') { H.audioContext.suspend(); // Pause until we need it } return !!(H.audioContext && H.audioContext.createOscillator && H.audioContext.createGain); } return false; }; /** * Init an oscillator instrument. * @private * @param {Highcharts.OscillatorOptionsObject} oscillatorOptions * The oscillator options passed to Highcharts.Instrument#init. * @return {void} */ Instrument.prototype.initOscillator = function (options) { var ctx = H.audioContext; this.oscillator = ctx.createOscillator(); this.oscillator.type = options.waveformShape; this.oscillator.connect(this.gainNode); this.oscillatorStarted = false; }; /** * Set pan position. * @private * @param {number} panValue * The pan position to set for the instrument. * @return {void} */ Instrument.prototype.setPan = function (panValue) { if (this.panNode) { this.panNode.pan.setValueAtTime(panValue, H.audioContext.currentTime); } }; /** * Set gain level. A maximum of 1.2 is allowed before we emit a warning. The * actual volume is not set above this level regardless of input. * @private * @param {number} gainValue * The gain level to set for the instrument. * @param {number} [rampTime=0] * Gradually change the gain level, time given in milliseconds. * @return {void} */ Instrument.prototype.setGain = function (gainValue, rampTime) { if (this.gainNode) { if (gainValue > 1.2) { console.warn(// eslint-disable-line 'Highcharts sonification warning: ' + 'Volume of instrument set too high.'); gainValue = 1.2; } if (rampTime) { this.gainNode.gain.setValueAtTime(this.gainNode.gain.value, H.audioContext.currentTime); this.gainNode.gain.linearRampToValueAtTime(gainValue, H.audioContext.currentTime + rampTime / 1000); } else { this.gainNode.gain.setValueAtTime(gainValue, H.audioContext.currentTime); } } }; /** * Cancel ongoing gain ramps. * @private * @return {void} */ Instrument.prototype.cancelGainRamp = function () { if (this.gainNode) { this.gainNode.gain.cancelScheduledValues(0); } }; /** * Get the closest valid frequency for this instrument. * @private * @param {number} frequency - The target frequency. * @param {number} [min] - Minimum frequency to return. * @param {number} [max] - Maximum frequency to return. * @return {number} The closest valid frequency to the input frequency. */ Instrument.prototype.getValidFrequency = function (frequency, min, max) { var validFrequencies = this.options.allowedFrequencies, maximum = pick(max, Infinity), minimum = pick(min, -Infinity); return !validFrequencies || !validFrequencies.length ? // No valid frequencies for this instrument, return the target frequency : // Use the valid frequencies and return the closest match validFrequencies.reduce(function (acc, cur) { // Find the closest allowed value return Math.abs(cur - frequency) < Math.abs(acc - frequency) && cur < maximum && cur > minimum ? cur : acc; }, Infinity); }; /** * Clear existing play callback timers. * @private * @return {void} */ Instrument.prototype.clearPlayCallbackTimers = function () { this.playCallbackTimers.forEach(function (timer) { clearInterval(timer); }); this.playCallbackTimers = []; }; /** * Set the current frequency being played by the instrument. The closest valid * frequency between the frequency limits is used. * @param {number} frequency * The frequency to set. * @param {Highcharts.Dictionary} [frequencyLimits] * Object with maxFrequency and minFrequency * @return {void} */ Instrument.prototype.setFrequency = function (frequency, frequencyLimits) { var limits = frequencyLimits || {}, validFrequency = this.getValidFrequency(frequency, limits.min, limits.max); if (this.options.type === 'oscillator') { this.oscillatorPlay(validFrequency); } }; /** * Play oscillator instrument. * @private * @param {number} frequency - The frequency to play. */ Instrument.prototype.oscillatorPlay = function (frequency) { if (!this.oscillatorStarted) { this.oscillator.start(); this.oscillatorStarted = true; } this.oscillator.frequency.setValueAtTime(frequency, H.audioContext.currentTime); }; /** * Prepare instrument before playing. Resumes the audio context and starts the * oscillator. * @private */ Instrument.prototype.preparePlay = function () { this.setGain(0.001); if (H.audioContext.state === 'suspended') { H.audioContext.resume(); } if (this.oscillator && !this.oscillatorStarted) { this.oscillator.start(); this.oscillatorStarted = true; } }; /** * Play the instrument according to options. * * @sample highcharts/sonification/instrument/ * Using Instruments directly * @sample highcharts/sonification/instrument-advanced/ * Using callbacks for instrument parameters * * @function Highcharts.Instrument#play * * @param {Highcharts.InstrumentPlayOptionsObject} options * Options for the playback of the instrument. * * @return {void} */ Instrument.prototype.play = function (options) { var instrument = this, duration = options.duration || 0, // Set a value, or if it is a function, set it continously as a timer. // Pass in the value/function to set, the setter function, and any // additional data to pass through to the setter function. setOrStartTimer = function (value, setter, setterData) { var target = options.duration, currentDurationIx = 0, callbackInterval = instrument.options.playCallbackInterval; if (typeof value === 'function') { var timer = setInterval(function () { currentDurationIx++; var curTime = (currentDurationIx * callbackInterval / target); if (curTime >= 1) { instrument[setter](value(1), setterData); clearInterval(timer); } else { instrument[setter](value(curTime), setterData); } }, callbackInterval); instrument.playCallbackTimers.push(timer); } else { instrument[setter](value, setterData); } }; if (!instrument.id) { // No audio support - do nothing return; } // If the AudioContext is suspended we have to resume it before playing if (H.audioContext.state === 'suspended' || this.oscillator && !this.oscillatorStarted) { instrument.preparePlay(); // Try again in 10ms setTimeout(function () { instrument.play(options); }, 10); return; } // Clear any existing play timers if (instrument.playCallbackTimers.length) { instrument.clearPlayCallbackTimers(); } // Clear any gain ramps instrument.cancelGainRamp(); // Clear stop oscillator timer if (instrument.stopOscillatorTimeout) { clearTimeout(instrument.stopOscillatorTimeout); delete instrument.stopOscillatorTimeout; } // If a note is playing right now, clear the stop timeout, and call the // callback. if (instrument.stopTimeout) { clearTimeout(instrument.stopTimeout); delete instrument.stopTimeout; if (instrument.stopCallback) { // We have a callback for the play we are interrupting. We do not // allow this callback to start a new play, because that leads to // chaos. We pass in 'cancelled' to indicate that this note did not // finish, but still stopped. instrument._play = instrument.play; instrument.play = function () { }; instrument.stopCallback('cancelled'); instrument.play = instrument._play; } } // Stop the note without fadeOut if the duration is too short to hear the // note otherwise. var immediate = duration < H.sonification.fadeOutDuration + 20; // Stop the instrument after the duration of the note instrument.stopCallback = options.onEnd; var onStop = function () { delete instrument.stopTimeout; instrument.stop(immediate); }; if (duration) { instrument.stopTimeout = setTimeout(onStop, immediate ? duration : duration - H.sonification.fadeOutDuration); // Play the note setOrStartTimer(options.frequency, 'setFrequency', { minFrequency: options.minFrequency, maxFrequency: options.maxFrequency }); // Set the volume and panning setOrStartTimer(pick(options.volume, 1), 'setGain', 4); // Slight ramp setOrStartTimer(pick(options.pan, 0), 'setPan'); } else { // No note duration, so just stop immediately onStop(); } }; /** * Mute an instrument that is playing. If the instrument is not currently * playing, this function does nothing. * * @function Highcharts.Instrument#mute */ Instrument.prototype.mute = function () { this.setGain(0.0001, H.sonification.fadeOutDuration * 0.8); }; /** * Stop the instrument playing. * * @function Highcharts.Instrument#stop * * @param {boolean} immediately * Whether to do the stop immediately or fade out. * * @param {Function} [onStopped] * Callback function to be called when the stop is completed. * * @param {*} [callbackData] * Data to send to the onEnd callback functions. * * @return {void} */ Instrument.prototype.stop = function (immediately, onStopped, callbackData) { var instr = this, reset = function () { // Remove timeout reference if (instr.stopOscillatorTimeout) { delete instr.stopOscillatorTimeout; } // The oscillator may have stopped in the meantime here, so allow // this function to fail if so. try { instr.oscillator.stop(); } catch (e) { // silent error } instr.oscillator.disconnect(instr.gainNode); // We need a new oscillator in order to restart it instr.initOscillator(instr.options.oscillator); // Done stopping, call the callback from the stop if (onStopped) { onStopped(callbackData); } // Call the callback for the play we finished if (instr.stopCallback) { instr.stopCallback(callbackData); } }; // Clear any existing timers if (instr.playCallbackTimers.length) { instr.clearPlayCallbackTimers(); } if (instr.stopTimeout) { clearTimeout(instr.stopTimeout); } if (immediately) { instr.setGain(0); reset(); } else { instr.mute(); // Stop the oscillator after the mute fade-out has finished instr.stopOscillatorTimeout = setTimeout(reset, H.sonification.fadeOutDuration + 100); } }; return Instrument; }); _registerModule(_modules, 'modules/sonification/musicalFrequencies.js', [], function () { /* * * * (c) 2009-2020 Øystein Moseng * * List of musical frequencies from C0 to C8. * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ var frequencies = [ 16.351597831287414, 17.323914436054505, 18.354047994837977, 19.445436482630058, 20.601722307054366, 21.826764464562746, 23.12465141947715, 24.499714748859326, 25.956543598746574, 27.5, 29.13523509488062, 30.86770632850775, 32.70319566257483, 34.64782887210901, 36.70809598967594, 38.890872965260115, 41.20344461410875, 43.653528929125486, 46.2493028389543, 48.999429497718666, 51.91308719749314, 55, 58.27047018976124, 61.7354126570155, 65.40639132514966, 69.29565774421802, 73.41619197935188, 77.78174593052023, 82.4068892282175, 87.30705785825097, 92.4986056779086, 97.99885899543733, 103.82617439498628, 110, 116.54094037952248, 123.47082531403103, 130.8127826502993, 138.59131548843604, 146.8323839587038, 155.56349186104046, 164.81377845643496, 174.61411571650194, 184.9972113558172, 195.99771799087463, 207.65234878997256, 220, 233.08188075904496, 246.94165062806206, 261.6255653005986, 277.1826309768721, 293.6647679174076, 311.1269837220809, 329.6275569128699, 349.2282314330039, 369.9944227116344, 391.99543598174927, 415.3046975799451, 440, 466.1637615180899, 493.8833012561241, 523.2511306011972, 554.3652619537442, 587.3295358348151, 622.2539674441618, 659.2551138257398, 698.4564628660078, 739.9888454232688, 783.9908719634985, 830.6093951598903, 880, 932.3275230361799, 987.7666025122483, 1046.5022612023945, 1108.7305239074883, 1174.6590716696303, 1244.5079348883237, 1318.5102276514797, 1396.9129257320155, 1479.9776908465376, 1567.981743926997, 1661.2187903197805, 1760, 1864.6550460723597, 1975.533205024496, 2093.004522404789, 2217.4610478149766, 2349.31814333926, 2489.0158697766474, 2637.02045530296, 2793.825851464031, 2959.955381693075, 3135.9634878539946, 3322.437580639561, 3520, 3729.3100921447194, 3951.066410048992, 4186.009044809578 // C8 ]; return frequencies; }); _registerModule(_modules, 'modules/sonification/utilities.js', [_modules['modules/sonification/musicalFrequencies.js'], _modules['parts/Utilities.js']], function (musicalFrequencies, U) { /* * * * (c) 2009-2020 Øystein Moseng * * Utility functions for sonification. * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ var clamp = U.clamp; /* eslint-disable no-invalid-this, valid-jsdoc */ /** * The SignalHandler class. Stores signal callbacks (event handlers), and * provides an interface to register them, and emit signals. The word "event" is * not used to avoid confusion with TimelineEvents. * * @requires module:modules/sonification * * @private * @class * @name Highcharts.SignalHandler * * @param {Array} supportedSignals * List of supported signal names. */ function SignalHandler(supportedSignals) { this.init(supportedSignals || []); } SignalHandler.prototype.init = function (supportedSignals) { this.supportedSignals = supportedSignals; this.signals = {}; }; /** * Register a set of signal callbacks with this SignalHandler. * Multiple signal callbacks can be registered for the same signal. * @private * @param {Highcharts.Dictionary<(Function|undefined)>} signals * An object that contains a mapping from the signal name to the callbacks. Only * supported events are considered. * @return {void} */ SignalHandler.prototype.registerSignalCallbacks = function (signals) { var signalHandler = this; signalHandler.supportedSignals.forEach(function (supportedSignal) { var signal = signals[supportedSignal]; if (signal) { (signalHandler.signals[supportedSignal] = signalHandler.signals[supportedSignal] || []).push(signal); } }); }; /** * Clear signal callbacks, optionally by name. * @private * @param {Array} [signalNames] - A list of signal names to clear. If * not supplied, all signal callbacks are removed. * @return {void} */ SignalHandler.prototype.clearSignalCallbacks = function (signalNames) { var signalHandler = this; if (signalNames) { signalNames.forEach(function (signalName) { if (signalHandler.signals[signalName]) { delete signalHandler.signals[signalName]; } }); } else { signalHandler.signals = {}; } }; /** * Emit a signal. Does nothing if the signal does not exist, or has no * registered callbacks. * @private * @param {string} signalNames * Name of signal to emit. * @param {*} [data] * Data to pass to the callback. * @return {*} */ SignalHandler.prototype.emitSignal = function (signalName, data) { var retval; if (this.signals[signalName]) { this.signals[signalName].forEach(function (handler) { var result = handler(data); retval = typeof result !== 'undefined' ? result : retval; }); } return retval; }; var utilities = { // List of musical frequencies from C0 to C8 musicalFrequencies: musicalFrequencies, // SignalHandler class SignalHandler: SignalHandler, /** * Get a musical scale by specifying the semitones from 1-12 to include. * 1: C, 2: C#, 3: D, 4: D#, 5: E, 6: F, * 7: F#, 8: G, 9: G#, 10: A, 11: Bb, 12: B * @private * @param {Array} semitones * Array of semitones from 1-12 to include in the scale. Duplicate entries * are ignored. * @return {Array} * Array of frequencies from C0 to C8 that are included in this scale. */ getMusicalScale: function (semitones) { return musicalFrequencies.filter(function (freq, i) { var interval = i % 12 + 1; return semitones.some(function (allowedInterval) { return allowedInterval === interval; }); }); }, /** * Calculate the extreme values in a chart for a data prop. * @private * @param {Highcharts.Chart} chart - The chart * @param {string} prop - The data prop to find extremes for * @return {Highcharts.RangeObject} Object with min and max properties */ calculateDataExtremes: function (chart, prop) { return chart.series.reduce(function (extremes, series) { // We use cropped points rather than series.data here, to allow // users to zoom in for better fidelity. series.points.forEach(function (point) { var val = typeof point[prop] !== 'undefined' ? point[prop] : point.options[prop]; extremes.min = Math.min(extremes.min, val); extremes.max = Math.max(extremes.max, val); }); return extremes; }, { min: Infinity, max: -Infinity }); }, /** * Translate a value on a virtual axis. Creates a new, virtual, axis with a * min and max, and maps the relative value onto this axis. * @private * @param {number} value * The relative data value to translate. * @param {Highcharts.RangeObject} DataExtremesObject * The possible extremes for this value. * @param {object} limits * Limits for the virtual axis. * @return {number} * The value mapped to the virtual axis. */ virtualAxisTranslate: function (value, dataExtremes, limits) { var lenValueAxis = dataExtremes.max - dataExtremes.min, lenVirtualAxis = limits.max - limits.min, virtualAxisValue = limits.min + lenVirtualAxis * (value - dataExtremes.min) / lenValueAxis; return lenValueAxis > 0 ? clamp(virtualAxisValue, limits.min, limits.max) : limits.min; } }; return utilities; }); _registerModule(_modules, 'modules/sonification/instrumentDefinitions.js', [_modules['modules/sonification/Instrument.js'], _modules['modules/sonification/utilities.js']], function (Instrument, utilities) { /* * * * (c) 2009-2020 Øystein Moseng * * Instrument definitions for sonification module. * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ var instruments = {}; ['sine', 'square', 'triangle', 'sawtooth'].forEach(function (waveform) { // Add basic instruments instruments[waveform] = new Instrument({ oscillator: { waveformShape: waveform } }); // Add musical instruments instruments[waveform + 'Musical'] = new Instrument({ allowedFrequencies: utilities.musicalFrequencies, oscillator: { waveformShape: waveform } }); // Add scaled instruments instruments[waveform + 'Major'] = new Instrument({ allowedFrequencies: utilities.getMusicalScale([1, 3, 5, 6, 8, 10, 12]), oscillator: { waveformShape: waveform } }); }); return instruments; }); _registerModule(_modules, 'modules/sonification/Earcon.js', [_modules['parts/Globals.js'], _modules['parts/Utilities.js']], function (H, U) { /* * * * (c) 2009-2020 Øystein Moseng * * Earcons for the sonification module in Highcharts. * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ var error = U.error, merge = U.merge, pick = U.pick, uniqueKey = U.uniqueKey; /** * Define an Instrument and the options for playing it. * * @requires module:modules/sonification * * @interface Highcharts.EarconInstrument */ /** * An instrument instance or the name of the instrument in the * Highcharts.sonification.instruments map. * @name Highcharts.EarconInstrument#instrument * @type {string|Highcharts.Instrument} */ /** * The options to pass to Instrument.play. * @name Highcharts.EarconInstrument#playOptions * @type {Highcharts.InstrumentPlayOptionsObject} */ /** * Options for an Earcon. * * @requires module:modules/sonification * * @interface Highcharts.EarconOptionsObject */ /** * The instruments and their options defining this earcon. * @name Highcharts.EarconOptionsObject#instruments * @type {Array} */ /** * The unique ID of the Earcon. Generated if not supplied. * @name Highcharts.EarconOptionsObject#id * @type {string|undefined} */ /** * Global panning of all instruments. Overrides all panning on individual * instruments. Can be a number between -1 and 1. * @name Highcharts.EarconOptionsObject#pan * @type {number|undefined} */ /** * Master volume for all instruments. Volume settings on individual instruments * can still be used for relative volume between the instruments. This setting * does not affect volumes set by functions in individual instruments. Can be a * number between 0 and 1. Defaults to 1. * @name Highcharts.EarconOptionsObject#volume * @type {number|undefined} */ /** * Callback function to call when earcon has finished playing. * @name Highcharts.EarconOptionsObject#onEnd * @type {Function|undefined} */ /* eslint-disable no-invalid-this, valid-jsdoc */ /** * The Earcon class. Earcon objects represent a certain sound consisting of * one or more instruments playing a predefined sound. * * @sample highcharts/sonification/earcon/ * Using earcons directly * * @requires module:modules/sonification * * @class * @name Highcharts.Earcon * * @param {Highcharts.EarconOptionsObject} options * Options for the Earcon instance. */ function Earcon(options) { this.init(options || {}); } Earcon.prototype.init = function (options) { this.options = options; if (!this.options.id) { this.options.id = this.id = uniqueKey(); } this.instrumentsPlaying = {}; }; /** * Play the earcon, optionally overriding init options. * * @sample highcharts/sonification/earcon/ * Using earcons directly * * @function Highcharts.Earcon#sonify * * @param {Highcharts.EarconOptionsObject} options * Override existing options. * * @return {void} */ Earcon.prototype.sonify = function (options) { var playOptions = merge(this.options, options); // Find master volume/pan settings var masterVolume = pick(playOptions.volume, 1), masterPan = playOptions.pan, earcon = this, playOnEnd = options && options.onEnd, masterOnEnd = earcon.options.onEnd; // Go through the instruments and play them playOptions.instruments.forEach(function (opts) { var instrument = typeof opts.instrument === 'string' ? H.sonification.instruments[opts.instrument] : opts.instrument, instrumentOpts = merge(opts.playOptions), instrOnEnd, instrumentCopy, copyId = ''; if (instrument && instrument.play) { if (opts.playOptions) { // Handle master pan/volume if (typeof opts.playOptions.volume !== 'function') { instrumentOpts.volume = pick(masterVolume, 1) * pick(opts.playOptions.volume, 1); } instrumentOpts.pan = pick(masterPan, instrumentOpts.pan); // Handle onEnd instrOnEnd = instrumentOpts.onEnd; instrumentOpts.onEnd = function () { delete earcon.instrumentsPlaying[copyId]; if (instrOnEnd) { instrOnEnd.apply(this, arguments); } if (!Object.keys(earcon.instrumentsPlaying).length) { if (playOnEnd) { playOnEnd.apply(this, arguments); } if (masterOnEnd) { masterOnEnd.apply(this, arguments); } } }; // Play the instrument. Use a copy so we can play multiple at // the same time. instrumentCopy = instrument.copy(); copyId = instrumentCopy.id; earcon.instrumentsPlaying[copyId] = instrumentCopy; instrumentCopy.play(instrumentOpts); } } else { error(30); } }); }; /** * Cancel any current sonification of the Earcon. Calls onEnd functions. * * @function Highcharts.Earcon#cancelSonify * * @param {boolean} [fadeOut=false] * Whether or not to fade out as we stop. If false, the earcon is * cancelled synchronously. * * @return {void} */ Earcon.prototype.cancelSonify = function (fadeOut) { var playing = this.instrumentsPlaying, instrIds = playing && Object.keys(playing); if (instrIds && instrIds.length) { instrIds.forEach(function (instr) { playing[instr].stop(!fadeOut, null, 'cancelled'); }); this.instrumentsPlaying = {}; } }; return Earcon; }); _registerModule(_modules, 'modules/sonification/pointSonify.js', [_modules['parts/Globals.js'], _modules['parts/Utilities.js'], _modules['modules/sonification/utilities.js']], function (H, U, utilities) { /* * * * (c) 2009-2020 Øystein Moseng * * Code for sonifying single points. * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ var error = U.error, merge = U.merge, pick = U.pick; /** * Define the parameter mapping for an instrument. * * @requires module:modules/sonification * * @interface Highcharts.PointInstrumentMappingObject */ /** * Define the volume of the instrument. This can be a string with a data * property name, e.g. `'y'`, in which case this data property is used to define * the volume relative to the `y`-values of the other points. A higher `y` value * would then result in a higher volume. This option can also be a fixed number * or a function. If it is a function, this function is called in regular * intervals while the note is playing. It receives three arguments: The point, * the dataExtremes, and the current relative time - where 0 is the beginning of * the note and 1 is the end. The function should return the volume of the note * as a number between 0 and 1. * @name Highcharts.PointInstrumentMappingObject#volume * @type {string|number|Function} */ /** * Define the duration of the notes for this instrument. This can be a string * with a data property name, e.g. `'y'`, in which case this data property is * used to define the duration relative to the `y`-values of the other points. A * higher `y` value would then result in a longer duration. This option can also * be a fixed number or a function. If it is a function, this function is called * once before the note starts playing, and should return the duration in * milliseconds. It receives two arguments: The point, and the dataExtremes. * @name Highcharts.PointInstrumentMappingObject#duration * @type {string|number|Function} */ /** * Define the panning of the instrument. This can be a string with a data * property name, e.g. `'x'`, in which case this data property is used to define * the panning relative to the `x`-values of the other points. A higher `x` * value would then result in a higher panning value (panned further to the * right). This option can also be a fixed number or a function. If it is a * function, this function is called in regular intervals while the note is * playing. It receives three arguments: The point, the dataExtremes, and the * current relative time - where 0 is the beginning of the note and 1 is the * end. The function should return the panning of the note as a number between * -1 and 1. * @name Highcharts.PointInstrumentMappingObject#pan * @type {string|number|Function|undefined} */ /** * Define the frequency of the instrument. This can be a string with a data * property name, e.g. `'y'`, in which case this data property is used to define * the frequency relative to the `y`-values of the other points. A higher `y` * value would then result in a higher frequency. This option can also be a * fixed number or a function. If it is a function, this function is called in * regular intervals while the note is playing. It receives three arguments: * The point, the dataExtremes, and the current relative time - where 0 is the * beginning of the note and 1 is the end. The function should return the * frequency of the note as a number (in Hz). * @name Highcharts.PointInstrumentMappingObject#frequency * @type {string|number|Function} */ /** * @requires module:modules/sonification * * @interface Highcharts.PointInstrumentOptionsObject */ /** * The minimum duration for a note when using a data property for duration. Can * be overridden by using either a fixed number or a function for * instrumentMapping.duration. Defaults to 20. * @name Highcharts.PointInstrumentOptionsObject#minDuration * @type {number|undefined} */ /** * The maximum duration for a note when using a data property for duration. Can * be overridden by using either a fixed number or a function for * instrumentMapping.duration. Defaults to 2000. * @name Highcharts.PointInstrumentOptionsObject#maxDuration * @type {number|undefined} */ /** * The minimum pan value for a note when using a data property for panning. Can * be overridden by using either a fixed number or a function for * instrumentMapping.pan. Defaults to -1 (fully left). * @name Highcharts.PointInstrumentOptionsObject#minPan * @type {number|undefined} */ /** * The maximum pan value for a note when using a data property for panning. Can * be overridden by using either a fixed number or a function for * instrumentMapping.pan. Defaults to 1 (fully right). * @name Highcharts.PointInstrumentOptionsObject#maxPan * @type {number|undefined} */ /** * The minimum volume for a note when using a data property for volume. Can be * overridden by using either a fixed number or a function for * instrumentMapping.volume. Defaults to 0.1. * @name Highcharts.PointInstrumentOptionsObject#minVolume * @type {number|undefined} */ /** * The maximum volume for a note when using a data property for volume. Can be * overridden by using either a fixed number or a function for * instrumentMapping.volume. Defaults to 1. * @name Highcharts.PointInstrumentOptionsObject#maxVolume * @type {number|undefined} */ /** * The minimum frequency for a note when using a data property for frequency. * Can be overridden by using either a fixed number or a function for * instrumentMapping.frequency. Defaults to 220. * @name Highcharts.PointInstrumentOptionsObject#minFrequency * @type {number|undefined} */ /** * The maximum frequency for a note when using a data property for frequency. * Can be overridden by using either a fixed number or a function for * instrumentMapping.frequency. Defaults to 2200. * @name Highcharts.PointInstrumentOptionsObject#maxFrequency * @type {number|undefined} */ /** * An instrument definition for a point, specifying the instrument to play and * how to play it. * * @interface Highcharts.PointInstrumentObject */ /** * An Instrument instance or the name of the instrument in the * Highcharts.sonification.instruments map. * @name Highcharts.PointInstrumentObject#instrument * @type {Highcharts.Instrument|string} */ /** * Mapping of instrument parameters for this instrument. * @name Highcharts.PointInstrumentObject#instrumentMapping * @type {Highcharts.PointInstrumentMappingObject} */ /** * Options for this instrument. * @name Highcharts.PointInstrumentObject#instrumentOptions * @type {Highcharts.PointInstrumentOptionsObject|undefined} */ /** * Callback to call when the instrument has stopped playing. * @name Highcharts.PointInstrumentObject#onEnd * @type {Function|undefined} */ /** * Options for sonifying a point. * @interface Highcharts.PointSonifyOptionsObject */ /** * The instrument definitions for this point. * @name Highcharts.PointSonifyOptionsObject#instruments * @type {Array} */ /** * Optionally provide the minimum/maximum values for the points. If this is not * supplied, it is calculated from the points in the chart on demand. This * option is supplied in the following format, as a map of point data properties * to objects with min/max values: * ```js * dataExtremes: { * y: { * min: 0, * max: 100 * }, * z: { * min: -10, * max: 10 * } * // Properties used and not provided are calculated on demand * } * ``` * @name Highcharts.PointSonifyOptionsObject#dataExtremes * @type {object|undefined} */ /** * Callback called when the sonification has finished. * @name Highcharts.PointSonifyOptionsObject#onEnd * @type {Function|undefined} */ // Defaults for the instrument options // NOTE: Also change defaults in Highcharts.PointInstrumentOptionsObject if // making changes here. var defaultInstrumentOptions = { minDuration: 20, maxDuration: 2000, minVolume: 0.1, maxVolume: 1, minPan: -1, maxPan: 1, minFrequency: 220, maxFrequency: 2200 }; /* eslint-disable no-invalid-this, valid-jsdoc */ /** * Sonify a single point. * * @sample highcharts/sonification/point-basic/ * Click on points to sonify * @sample highcharts/sonification/point-advanced/ * Sonify bubbles * * @requires module:modules/sonification * * @function Highcharts.Point#sonify * * @param {Highcharts.PointSonifyOptionsObject} options * Options for the sonification of the point. * * @return {void} */ function pointSonify(options) { var point = this, chart = point.series.chart, dataExtremes = options.dataExtremes || {}, // Get the value to pass to instrument.play from the mapping value // passed in. getMappingValue = function (value, makeFunction, allowedExtremes) { // Function. Return new function if we try to use callback, // otherwise call it now and return result. if (typeof value === 'function') { return makeFunction ? function (time) { return value(point, dataExtremes, time); } : value(point, dataExtremes); } // String, this is a data prop. if (typeof value === 'string') { // Find data extremes if we don't have them dataExtremes[value] = dataExtremes[value] || utilities.calculateDataExtremes(point.series.chart, value); // Find the value return utilities.virtualAxisTranslate(pick(point[value], point.options[value]), dataExtremes[value], allowedExtremes); } // Fixed number or something else weird, just use that return value; }; // Register playing point on chart chart.sonification.currentlyPlayingPoint = point; // Keep track of instruments playing point.sonification = point.sonification || {}; point.sonification.instrumentsPlaying = point.sonification.instrumentsPlaying || {}; // Register signal handler for the point var signalHandler = point.sonification.signalHandler = point.sonification.signalHandler || new utilities.SignalHandler(['onEnd']); signalHandler.clearSignalCallbacks(); signalHandler.registerSignalCallbacks({ onEnd: options.onEnd }); // If we have a null point or invisible point, just return if (point.isNull || !point.visible || !point.series.visible) { signalHandler.emitSignal('onEnd'); return; } // Go through instruments and play them options.instruments.forEach(function (instrumentDefinition) { var instrument = typeof instrumentDefinition.instrument === 'string' ? H.sonification.instruments[instrumentDefinition.instrument] : instrumentDefinition.instrument, mapping = instrumentDefinition.instrumentMapping || {}, extremes = merge(defaultInstrumentOptions, instrumentDefinition.instrumentOptions), id = instrument.id, onEnd = function (cancelled) { // Instrument on end if (instrumentDefinition.onEnd) { instrumentDefinition.onEnd.apply(this, arguments); } // Remove currently playing point reference on chart if (chart.sonification && chart.sonification.currentlyPlayingPoint) { delete chart.sonification.currentlyPlayingPoint; } // Remove reference from instruments playing if (point.sonification && point.sonification.instrumentsPlaying) { delete point.sonification.instrumentsPlaying[id]; // This was the last instrument? if (!Object.keys(point.sonification.instrumentsPlaying).length) { signalHandler.emitSignal('onEnd', cancelled); } } }; // Play the note on the instrument if (instrument && instrument.play) { point.sonification.instrumentsPlaying[instrument.id] = instrument; instrument.play({ frequency: getMappingValue(mapping.frequency, true, { min: extremes.minFrequency, max: extremes.maxFrequency }), duration: getMappingValue(mapping.duration, false, { min: extremes.minDuration, max: extremes.maxDuration }), pan: getMappingValue(mapping.pan, true, { min: extremes.minPan, max: extremes.maxPan }), volume: getMappingValue(mapping.volume, true, { min: extremes.minVolume, max: extremes.maxVolume }), onEnd: onEnd, minFrequency: extremes.minFrequency, maxFrequency: extremes.maxFrequency }); } else { error(30); } }); } /** * Cancel sonification of a point. Calls onEnd functions. * * @requires module:modules/sonification * * @function Highcharts.Point#cancelSonify * * @param {boolean} [fadeOut=false] * Whether or not to fade out as we stop. If false, the points are * cancelled synchronously. * * @return {void} */ function pointCancelSonify(fadeOut) { var playing = this.sonification && this.sonification.instrumentsPlaying, instrIds = playing && Object.keys(playing); if (instrIds && instrIds.length) { instrIds.forEach(function (instr) { playing[instr].stop(!fadeOut, null, 'cancelled'); }); this.sonification.instrumentsPlaying = {}; this.sonification.signalHandler.emitSignal('onEnd', 'cancelled'); } } var pointSonifyFunctions = { pointSonify: pointSonify, pointCancelSonify: pointCancelSonify }; return pointSonifyFunctions; }); _registerModule(_modules, 'modules/sonification/chartSonify.js', [_modules['parts/Globals.js'], _modules['parts/Point.js'], _modules['parts/Utilities.js'], _modules['modules/sonification/utilities.js']], function (H, Point, U, utilities) { /* * * * (c) 2009-2020 Øystein Moseng * * Sonification functions for chart/series. * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ /** * An Earcon configuration, specifying an Earcon and when to play it. * * @requires module:modules/sonification * * @interface Highcharts.EarconConfiguration */ /** * An Earcon instance. * @name Highcharts.EarconConfiguration#earcon * @type {Highcharts.Earcon} */ /** * The ID of the point to play the Earcon on. * @name Highcharts.EarconConfiguration#onPoint * @type {string|undefined} */ /** * A function to determine whether or not to play this earcon on a point. The * function is called for every point, receiving that point as parameter. It * should return either a boolean indicating whether or not to play the earcon, * or a new Earcon instance - in which case the new Earcon will be played. * @name Highcharts.EarconConfiguration#condition * @type {Function|undefined} */ /** * Options for sonifying a series. * * @requires module:modules/sonification * * @interface Highcharts.SonifySeriesOptionsObject */ /** * The duration for playing the points. Note that points might continue to play * after the duration has passed, but no new points will start playing. * @name Highcharts.SonifySeriesOptionsObject#duration * @type {number} */ /** * The axis to use for when to play the points. Can be a string with a data * property (e.g. `x`), or a function. If it is a function, this function * receives the point as argument, and should return a numeric value. The points * with the lowest numeric values are then played first, and the time between * points will be proportional to the distance between the numeric values. * @name Highcharts.SonifySeriesOptionsObject#pointPlayTime * @type {string|Function} */ /** * The instrument definitions for the points in this series. * @name Highcharts.SonifySeriesOptionsObject#instruments * @type {Array} */ /** * Earcons to add to the series. * @name Highcharts.SonifySeriesOptionsObject#earcons * @type {Array|undefined} */ /** * Optionally provide the minimum/maximum data values for the points. If this is * not supplied, it is calculated from all points in the chart on demand. This * option is supplied in the following format, as a map of point data properties * to objects with min/max values: * ```js * dataExtremes: { * y: { * min: 0, * max: 100 * }, * z: { * min: -10, * max: 10 * } * // Properties used and not provided are calculated on demand * } * ``` * @name Highcharts.SonifySeriesOptionsObject#dataExtremes * @type {Highcharts.Dictionary|undefined} */ /** * Callback before a point is played. * @name Highcharts.SonifySeriesOptionsObject#onPointStart * @type {Function|undefined} */ /** * Callback after a point has finished playing. * @name Highcharts.SonifySeriesOptionsObject#onPointEnd * @type {Function|undefined} */ /** * Callback after the series has played. * @name Highcharts.SonifySeriesOptionsObject#onEnd * @type {Function|undefined} */ ''; // detach doclets above var find = U.find, isArray = U.isArray, merge = U.merge, pick = U.pick, splat = U.splat; /** * Get the relative time value of a point. * @private * @param {Highcharts.Point} point * The point. * @param {Function|string} timeProp * The time axis data prop or the time function. * @return {number} * The time value. */ function getPointTimeValue(point, timeProp) { return typeof timeProp === 'function' ? timeProp(point) : pick(point[timeProp], point.options[timeProp]); } /** * Get the time extremes of this series. This is handled outside of the * dataExtremes, as we always want to just sonify the visible points, and we * always want the extremes to be the extremes of the visible points. * @private * @param {Highcharts.Series} series * The series to compute on. * @param {Function|string} timeProp * The time axis data prop or the time function. * @return {Highcharts.RangeObject} * Object with min/max extremes for the time values. */ function getTimeExtremes(series, timeProp) { // Compute the extremes from the visible points. return series.points.reduce(function (acc, point) { var value = getPointTimeValue(point, timeProp); acc.min = Math.min(acc.min, value); acc.max = Math.max(acc.max, value); return acc; }, { min: Infinity, max: -Infinity }); } /** * Calculate value extremes for used instrument data properties. * @private * @param {Highcharts.Chart} chart * The chart to calculate extremes from. * @param {Array} instruments * The instrument definitions used. * @param {Highcharts.Dictionary} [dataExtremes] * Predefined extremes for each data prop. * @return {Highcharts.Dictionary} * New extremes with data properties mapped to min/max objects. */ function getExtremesForInstrumentProps(chart, instruments, dataExtremes) { return (instruments || []).reduce(function (newExtremes, instrumentDefinition) { Object.keys(instrumentDefinition.instrumentMapping || {}).forEach(function (instrumentParameter) { var value = instrumentDefinition.instrumentMapping[instrumentParameter]; if (typeof value === 'string' && !newExtremes[value]) { // This instrument parameter is mapped to a data prop. // If we don't have predefined data extremes, find them. newExtremes[value] = utilities.calculateDataExtremes(chart, value); } }); return newExtremes; }, merge(dataExtremes)); } /** * Get earcons for the point if there are any. * @private * @param {Highcharts.Point} point * The point to find earcons for. * @param {Array} earconDefinitions * Earcons to check. * @return {Array} * Array of earcons to be played with this point. */ function getPointEarcons(point, earconDefinitions) { return earconDefinitions.reduce(function (earcons, earconDefinition) { var cond, earcon = earconDefinition.earcon; if (earconDefinition.condition) { // We have a condition. This overrides onPoint cond = earconDefinition.condition(point); if (cond instanceof H.sonification.Earcon) { // Condition returned an earcon earcons.push(cond); } else if (cond) { // Condition returned true earcons.push(earcon); } } else if (earconDefinition.onPoint && point.id === earconDefinition.onPoint) { // We have earcon onPoint earcons.push(earcon); } return earcons; }, []); } /** * Utility function to get a new list of instrument options where all the * instrument references are copies. * @private * @param {Array} instruments * The instrument options. * @return {Array} * Array of copied instrument options. */ function makeInstrumentCopies(instruments) { return instruments.map(function (instrumentDef) { var instrument = instrumentDef.instrument, copy = (typeof instrument === 'string' ? H.sonification.instruments[instrument] : instrument).copy(); return merge(instrumentDef, { instrument: copy }); }); } /** * Create a TimelinePath from a series. Takes the same options as seriesSonify. * To intuitively allow multiple series to play simultaneously we make copies of * the instruments for each series. * @private * @param {Highcharts.Series} series * The series to build from. * @param {Highcharts.SonifySeriesOptionsObject} options * The options for building the TimelinePath. * @return {Highcharts.TimelinePath} * A timeline path with events. */ function buildTimelinePathFromSeries(series, options) { // options.timeExtremes is internal and used so that the calculations from // chart.sonify can be reused. var timeExtremes = options.timeExtremes || getTimeExtremes(series, options.pointPlayTime), // Get time offset for a point, relative to duration pointToTime = function (point) { return utilities.virtualAxisTranslate(getPointTimeValue(point, options.pointPlayTime), timeExtremes, { min: 0, max: options.duration }); }, // Compute any data extremes that aren't defined yet dataExtremes = getExtremesForInstrumentProps(series.chart, options.instruments, options.dataExtremes), // Make copies of the instruments used for this series, to allow // multiple series with the same instrument to play together instruments = makeInstrumentCopies(options.instruments), // Go through the points, convert to events, optionally add Earcons timelineEvents = series.points.reduce(function (events, point) { var earcons = getPointEarcons(point, options.earcons || []), time = pointToTime(point); return events.concat( // Event object for point new H.sonification.TimelineEvent({ eventObject: point, time: time, id: point.id, playOptions: { instruments: instruments, dataExtremes: dataExtremes } }), // Earcons earcons.map(function (earcon) { return new H.sonification.TimelineEvent({ eventObject: earcon, time: time }); })); }, []); // Build the timeline path return new H.sonification.TimelinePath({ events: timelineEvents, onStart: function () { if (options.onStart) { options.onStart(series); } }, onEventStart: function (event) { var eventObject = event.options && event.options.eventObject; if (eventObject instanceof Point) { // Check for hidden series if (!eventObject.series.visible && !eventObject.series.chart.series.some(function (series) { return series.visible; })) { // We have no visible series, stop the path. event.timelinePath.timeline.pause(); event.timelinePath.timeline.resetCursor(); return false; } // Emit onPointStart if (options.onPointStart) { options.onPointStart(event, eventObject); } } }, onEventEnd: function (eventData) { var eventObject = eventData.event && eventData.event.options && eventData.event.options.eventObject; if (eventObject instanceof Point && options.onPointEnd) { options.onPointEnd(eventData.event, eventObject); } }, onEnd: function () { if (options.onEnd) { options.onEnd(series); } } }); } /* eslint-disable no-invalid-this, valid-jsdoc */ /** * Sonify a series. * * @sample highcharts/sonification/series-basic/ * Click on series to sonify * @sample highcharts/sonification/series-earcon/ * Series with earcon * @sample highcharts/sonification/point-play-time/ * Play y-axis by time * @sample highcharts/sonification/earcon-on-point/ * Earcon set on point * * @requires module:modules/sonification * * @function Highcharts.Series#sonify * * @param {Highcharts.SonifySeriesOptionsObject} options * The options for sonifying this series. * * @return {void} */ function seriesSonify(options) { var timelinePath = buildTimelinePathFromSeries(this, options), chartSonification = this.chart.sonification; // Only one timeline can play at a time. If we want multiple series playing // at the same time, use chart.sonify. if (chartSonification.timeline) { chartSonification.timeline.pause(); } // Store reference to duration chartSonification.duration = options.duration; // Create new timeline for this series, and play it. chartSonification.timeline = new H.sonification.Timeline({ paths: [timelinePath] }); chartSonification.timeline.play(); } /** * Utility function to assemble options for creating a TimelinePath from a * series when sonifying an entire chart. * @private * @param {Highcharts.Series} series * The series to return options for. * @param {Highcharts.RangeObject} dataExtremes * Pre-calculated data extremes for the chart. * @param {Highcharts.SonificationOptions} chartSonifyOptions * Options passed in to chart.sonify. * @return {Partial} * Options for buildTimelinePathFromSeries. */ function buildSeriesOptions(series, dataExtremes, chartSonifyOptions) { var seriesOptions = chartSonifyOptions.seriesOptions || {}; return merge({ // Calculated dataExtremes for chart dataExtremes: dataExtremes, // We need to get timeExtremes for each series. We pass this // in when building the TimelinePath objects to avoid // calculating twice. timeExtremes: getTimeExtremes(series, chartSonifyOptions.pointPlayTime), // Some options we just pass on instruments: chartSonifyOptions.instruments, onStart: chartSonifyOptions.onSeriesStart, onEnd: chartSonifyOptions.onSeriesEnd, earcons: chartSonifyOptions.earcons }, // Merge in the specific series options by ID isArray(seriesOptions) ? (find(seriesOptions, function (optEntry) { return optEntry.id === pick(series.id, series.options.id); }) || {}) : seriesOptions, { // Forced options pointPlayTime: chartSonifyOptions.pointPlayTime }); } /** * Utility function to normalize the ordering of timeline paths when sonifying * a chart. * @private * @param {string|Array>} orderOptions - * Order options for the sonification. * @param {Highcharts.Chart} chart - The chart we are sonifying. * @param {Function} seriesOptionsCallback * A function that takes a series as argument, and returns the series options * for that series to be used with buildTimelinePathFromSeries. * @return {Array>} If order is * sequential, we return an array of objects to create series paths from. If * order is simultaneous we return an array of an array with the same. If there * is a custom order, we return an array of arrays of either objects (for * series) or TimelinePaths (for earcons and delays). */ function buildPathOrder(orderOptions, chart, seriesOptionsCallback) { var order; if (orderOptions === 'sequential' || orderOptions === 'simultaneous') { // Just add the series from the chart order = chart.series.reduce(function (seriesList, series) { if (series.visible) { seriesList.push({ series: series, seriesOptions: seriesOptionsCallback(series) }); } return seriesList; }, []); // If order is simultaneous, group all series together if (orderOptions === 'simultaneous') { order = [order]; } } else { // We have a specific order, and potentially custom items - like // earcons or silent waits. order = orderOptions.reduce(function (orderList, orderDef) { // Return set of items to play simultaneously. Could be only one. var simulItems = splat(orderDef).reduce(function (items, item) { var itemObject; // Is this item a series ID? if (typeof item === 'string') { var series = chart.get(item); if (series.visible) { itemObject = { series: series, seriesOptions: seriesOptionsCallback(series) }; } // Is it an earcon? If so, just create the path. } else if (item instanceof H.sonification.Earcon) { // Path with a single event itemObject = new H.sonification.TimelinePath({ events: [new H.sonification.TimelineEvent({ eventObject: item })] }); } // Is this item a silent wait? If so, just create the path. if (item.silentWait) { itemObject = new H.sonification.TimelinePath({ silentWait: item.silentWait }); } // Add to items to play simultaneously if (itemObject) { items.push(itemObject); } return items; }, []); // Add to order list if (simulItems.length) { orderList.push(simulItems); } return orderList; }, []); } return order; } /** * Utility function to add a silent wait after all series. * @private * @param {Array>} order * The order of items. * @param {number} wait * The wait in milliseconds to add. * @return {Array>} * The order with waits inserted. */ function addAfterSeriesWaits(order, wait) { if (!wait) { return order; } return order.reduce(function (newOrder, orderDef, i) { var simultaneousPaths = splat(orderDef); newOrder.push(simultaneousPaths); // Go through the simultaneous paths and see if there is a series there if (i < order.length - 1 && // Do not add wait after last series simultaneousPaths.some(function (item) { return item.series; })) { // We have a series, meaning we should add a wait after these // paths have finished. newOrder.push(new H.sonification.TimelinePath({ silentWait: wait })); } return newOrder; }, []); } /** * Utility function to find the total amout of wait time in the TimelinePaths. * @private * @param {Array>} order - The order of * TimelinePaths/items. * @return {number} The total time in ms spent on wait paths between playing. */ function getWaitTime(order) { return order.reduce(function (waitTime, orderDef) { var def = splat(orderDef); return waitTime + (def.length === 1 && def[0].options && def[0].options.silentWait || 0); }, 0); } /** * Utility function to ensure simultaneous paths have start/end events at the * same time, to sync them. * @private * @param {Array} paths - The paths to sync. */ function syncSimultaneousPaths(paths) { // Find the extremes for these paths var extremes = paths.reduce(function (extremes, path) { var events = path.events; if (events && events.length) { extremes.min = Math.min(events[0].time, extremes.min); extremes.max = Math.max(events[events.length - 1].time, extremes.max); } return extremes; }, { min: Infinity, max: -Infinity }); // Go through the paths and add events to make them fit the same timespan paths.forEach(function (path) { var events = path.events, hasEvents = events && events.length, eventsToAdd = []; if (!(hasEvents && events[0].time <= extremes.min)) { eventsToAdd.push(new H.sonification.TimelineEvent({ time: extremes.min })); } if (!(hasEvents && events[events.length - 1].time >= extremes.max)) { eventsToAdd.push(new H.sonification.TimelineEvent({ time: extremes.max })); } if (eventsToAdd.length) { path.addTimelineEvents(eventsToAdd); } }); } /** * Utility function to find the total duration span for all simul path sets * that include series. * @private * @param {Array>} order - The * order of TimelinePaths/items. * @return {number} The total time value span difference for all series. */ function getSimulPathDurationTotal(order) { return order.reduce(function (durationTotal, orderDef) { return durationTotal + splat(orderDef).reduce(function (maxPathDuration, item) { var timeExtremes = (item.series && item.seriesOptions && item.seriesOptions.timeExtremes); return timeExtremes ? Math.max(maxPathDuration, timeExtremes.max - timeExtremes.min) : maxPathDuration; }, 0); }, 0); } /** * Function to calculate the duration in ms for a series. * @private * @param {number} seriesValueDuration - The duration of the series in value * difference. * @param {number} totalValueDuration - The total duration of all (non * simultaneous) series in value difference. * @param {number} totalDurationMs - The desired total duration for all series * in milliseconds. * @return {number} The duration for the series in milliseconds. */ function getSeriesDurationMs(seriesValueDuration, totalValueDuration, totalDurationMs) { // A series spanning the whole chart would get the full duration. return utilities.virtualAxisTranslate(seriesValueDuration, { min: 0, max: totalValueDuration }, { min: 0, max: totalDurationMs }); } /** * Convert series building objects into paths and return a new list of * TimelinePaths. * @private * @param {Array>} order - The * order list. * @param {number} duration - Total duration to aim for in milliseconds. * @return {Array>} Array of TimelinePath objects * to play. */ function buildPathsFromOrder(order, duration) { // Find time used for waits (custom or after series), and subtract it from // available duration. var totalAvailableDurationMs = Math.max(duration - getWaitTime(order), 0), // Add up simultaneous path durations to find total value span duration // of everything totalUsedDuration = getSimulPathDurationTotal(order); // Go through the order list and convert the items return order.reduce(function (allPaths, orderDef) { var simultaneousPaths = splat(orderDef).reduce(function (simulPaths, item) { if (item instanceof H.sonification.TimelinePath) { // This item is already a path object simulPaths.push(item); } else if (item.series) { // We have a series. // We need to set the duration of the series item.seriesOptions.duration = item.seriesOptions.duration || getSeriesDurationMs(item.seriesOptions.timeExtremes.max - item.seriesOptions.timeExtremes.min, totalUsedDuration, totalAvailableDurationMs); // Add the path simulPaths.push(buildTimelinePathFromSeries(item.series, item.seriesOptions)); } return simulPaths; }, []); // Add in the simultaneous paths allPaths.push(simultaneousPaths); return allPaths; }, []); } /** * @private * @param {Highcharts.Chart} chart The chart to get options for. * @param {Highcharts.SonificationOptions} userOptions * Options to merge with options on chart and default options. * @returns {Highcharts.SonificationOptions} The merged options. */ function getChartSonifyOptions(chart, userOptions) { return merge(chart.options.sonification, userOptions); } /** * Options for sonifying a chart. * * @requires module:modules/sonification * * @interface Highcharts.SonificationOptions */ /** * Duration for sonifying the entire chart. The duration is distributed across * the different series intelligently, but does not take earcons into account. * It is also possible to set the duration explicitly per series, using * `seriesOptions`. Note that points may continue to play after the duration has * passed, but no new points will start playing. * @name Highcharts.SonificationOptions#duration * @type {number} */ /** * Define the order to play the series in. This can be given as a string, or an * array specifying a custom ordering. If given as a string, valid values are * `sequential` - where each series is played in order - or `simultaneous`, * where all series are played at once. For custom ordering, supply an array as * the order. Each element in the array can be either a string with a series ID, * an Earcon object, or an object with a numeric `silentWait` property * designating a number of milliseconds to wait before continuing. Each element * of the array will be played in order. To play elements simultaneously, group * the elements in an array. * @name Highcharts.SonificationOptions#order * @type {string|Array>} */ /** * The axis to use for when to play the points. Can be a string with a data * property (e.g. `x`), or a function. If it is a function, this function * receives the point as argument, and should return a numeric value. The points * with the lowest numeric values are then played first, and the time between * points will be proportional to the distance between the numeric values. This * option can not be overridden per series. * @name Highcharts.SonificationOptions#pointPlayTime * @type {string|Function} */ /** * Milliseconds of silent waiting to add between series. Note that waiting time * is considered part of the sonify duration. * @name Highcharts.SonificationOptions#afterSeriesWait * @type {number|undefined} */ /** * Options as given to `series.sonify` to override options per series. If the * option is supplied as an array of options objects, the `id` property of the * object should correspond to the series' id. If the option is supplied as a * single object, the options apply to all series. * @name Highcharts.SonificationOptions#seriesOptions * @type {Object|Array|undefined} */ /** * The instrument definitions for the points in this chart. * @name Highcharts.SonificationOptions#instruments * @type {Array|undefined} */ /** * Earcons to add to the chart. Note that earcons can also be added per series * using `seriesOptions`. * @name Highcharts.SonificationOptions#earcons * @type {Array|undefined} */ /** * Optionally provide the minimum/maximum data values for the points. If this is * not supplied, it is calculated from all points in the chart on demand. This * option is supplied in the following format, as a map of point data properties * to objects with min/max values: * ```js * dataExtremes: { * y: { * min: 0, * max: 100 * }, * z: { * min: -10, * max: 10 * } * // Properties used and not provided are calculated on demand * } * ``` * @name Highcharts.SonificationOptions#dataExtremes * @type {Highcharts.Dictionary|undefined} */ /** * Callback before a series is played. * @name Highcharts.SonificationOptions#onSeriesStart * @type {Function|undefined} */ /** * Callback after a series has finished playing. * @name Highcharts.SonificationOptions#onSeriesEnd * @type {Function|undefined} */ /** * Callback after the chart has played. * @name Highcharts.SonificationOptions#onEnd * @type {Function|undefined} */ /** * Sonify a chart. * * @sample highcharts/sonification/chart-sequential/ * Sonify a basic chart * @sample highcharts/sonification/chart-simultaneous/ * Sonify series simultaneously * @sample highcharts/sonification/chart-custom-order/ * Custom defined order of series * @sample highcharts/sonification/chart-earcon/ * Earcons on chart * @sample highcharts/sonification/chart-events/ * Sonification events on chart * * @requires module:modules/sonification * * @function Highcharts.Chart#sonify * * @param {Highcharts.SonificationOptions} options * The options for sonifying this chart. * * @return {void} */ function chartSonify(options) { var opts = getChartSonifyOptions(this, options); // Only one timeline can play at a time. if (this.sonification.timeline) { this.sonification.timeline.pause(); } // Store reference to duration this.sonification.duration = opts.duration; // Calculate data extremes for the props used var dataExtremes = getExtremesForInstrumentProps(this, opts.instruments, opts.dataExtremes); // Figure out ordering of series and custom paths var order = buildPathOrder(opts.order, this, function (series) { return buildSeriesOptions(series, dataExtremes, opts); }); // Add waits after simultaneous paths with series in them. order = addAfterSeriesWaits(order, opts.afterSeriesWait || 0); // We now have a list of either TimelinePath objects or series that need to // be converted to TimelinePath objects. Convert everything to paths. var paths = buildPathsFromOrder(order, opts.duration); // Sync simultaneous paths paths.forEach(function (simultaneousPaths) { syncSimultaneousPaths(simultaneousPaths); }); // We have a set of paths. Create the timeline, and play it. this.sonification.timeline = new H.sonification.Timeline({ paths: paths, onEnd: opts.onEnd }); this.sonification.timeline.play(); } /** * Get a list of the points currently under cursor. * * @requires module:modules/sonification * * @function Highcharts.Chart#getCurrentSonifyPoints * * @return {Array} * The points currently under the cursor. */ function getCurrentPoints() { var cursorObj; if (this.sonification.timeline) { cursorObj = this.sonification.timeline.getCursor(); // Cursor per pathID return Object.keys(cursorObj).map(function (path) { // Get the event objects under cursor for each path return cursorObj[path].eventObject; }).filter(function (eventObj) { // Return the events that are points return eventObj instanceof Point; }); } return []; } /** * Set the cursor to a point or set of points in different series. * * @requires module:modules/sonification * * @function Highcharts.Chart#setSonifyCursor * * @param {Highcharts.Point|Array} points * The point or points to set the cursor to. If setting multiple points * under the cursor, the points have to be in different series that are * being played simultaneously. */ function setCursor(points) { var timeline = this.sonification.timeline; if (timeline) { splat(points).forEach(function (point) { // We created the events with the ID of the points, which makes // this easy. Just call setCursor for each ID. timeline.setCursor(point.id); }); } } /** * Pause the running sonification. * * @requires module:modules/sonification * * @function Highcharts.Chart#pauseSonify * * @param {boolean} [fadeOut=true] * Fade out as we pause to avoid clicks. * * @return {void} */ function pause(fadeOut) { if (this.sonification.timeline) { this.sonification.timeline.pause(pick(fadeOut, true)); } else if (this.sonification.currentlyPlayingPoint) { this.sonification.currentlyPlayingPoint.cancelSonify(fadeOut); } } /** * Resume the currently running sonification. Requires series.sonify or * chart.sonify to have been played at some point earlier. * * @requires module:modules/sonification * * @function Highcharts.Chart#resumeSonify * * @param {Function} onEnd * Callback to call when play finished. * * @return {void} */ function resume(onEnd) { if (this.sonification.timeline) { this.sonification.timeline.play(onEnd); } } /** * Play backwards from cursor. Requires series.sonify or chart.sonify to have * been played at some point earlier. * * @requires module:modules/sonification * * @function Highcharts.Chart#rewindSonify * * @param {Function} onEnd * Callback to call when play finished. * * @return {void} */ function rewind(onEnd) { if (this.sonification.timeline) { this.sonification.timeline.rewind(onEnd); } } /** * Cancel current sonification and reset cursor. * * @requires module:modules/sonification * * @function Highcharts.Chart#cancelSonify * * @param {boolean} [fadeOut=true] * Fade out as we pause to avoid clicks. * * @return {void} */ function cancel(fadeOut) { this.pauseSonify(fadeOut); this.resetSonifyCursor(); } /** * Reset cursor to start. Requires series.sonify or chart.sonify to have been * played at some point earlier. * * @requires module:modules/sonification * * @function Highcharts.Chart#resetSonifyCursor * * @return {void} */ function resetCursor() { if (this.sonification.timeline) { this.sonification.timeline.resetCursor(); } } /** * Reset cursor to end. Requires series.sonify or chart.sonify to have been * played at some point earlier. * * @requires module:modules/sonification * * @function Highcharts.Chart#resetSonifyCursorEnd * * @return {void} */ function resetCursorEnd() { if (this.sonification.timeline) { this.sonification.timeline.resetCursorEnd(); } } // Export functions var chartSonifyFunctions = { chartSonify: chartSonify, seriesSonify: seriesSonify, pause: pause, resume: resume, rewind: rewind, cancel: cancel, getCurrentPoints: getCurrentPoints, setCursor: setCursor, resetCursor: resetCursor, resetCursorEnd: resetCursorEnd }; return chartSonifyFunctions; }); _registerModule(_modules, 'modules/sonification/Timeline.js', [_modules['parts/Globals.js'], _modules['parts/Utilities.js'], _modules['modules/sonification/utilities.js']], function (H, U, utilities) { /* * * * (c) 2009-2020 Øystein Moseng * * TimelineEvent class definition. * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ var merge = U.merge, splat = U.splat, uniqueKey = U.uniqueKey; /** * A set of options for the TimelineEvent class. * * @requires module:modules/sonification * * @private * @interface Highcharts.TimelineEventOptionsObject */ /** * The object we want to sonify when playing the TimelineEvent. Can be any * object that implements the `sonify` and `cancelSonify` functions. If this is * not supplied, the TimelineEvent is considered a silent event, and the onEnd * event is immediately called. * @name Highcharts.TimelineEventOptionsObject#eventObject * @type {*} */ /** * Options to pass on to the eventObject when playing it. * @name Highcharts.TimelineEventOptionsObject#playOptions * @type {object|undefined} */ /** * The time at which we want this event to play (in milliseconds offset). This * is not used for the TimelineEvent.play function, but rather intended as a * property to decide when to call TimelineEvent.play. Defaults to 0. * @name Highcharts.TimelineEventOptionsObject#time * @type {number|undefined} */ /** * Unique ID for the event. Generated automatically if not supplied. * @name Highcharts.TimelineEventOptionsObject#id * @type {string|undefined} */ /** * Callback called when the play has finished. * @name Highcharts.TimelineEventOptionsObject#onEnd * @type {Function|undefined} */ /* eslint-disable no-invalid-this, valid-jsdoc */ /** * The TimelineEvent class. Represents a sound event on a timeline. * * @requires module:modules/sonification * * @private * @class * @name Highcharts.TimelineEvent * * @param {Highcharts.TimelineEventOptionsObject} options * Options for the TimelineEvent. */ function TimelineEvent(options) { this.init(options || {}); } TimelineEvent.prototype.init = function (options) { this.options = options; this.time = options.time || 0; this.id = this.options.id = options.id || uniqueKey(); }; /** * Play the event. Does not take the TimelineEvent.time option into account, * and plays the event immediately. * * @function Highcharts.TimelineEvent#play * * @param {Highcharts.TimelineEventOptionsObject} [options] * Options to pass in to the eventObject when playing it. * * @return {void} */ TimelineEvent.prototype.play = function (options) { var eventObject = this.options.eventObject, masterOnEnd = this.options.onEnd, playOnEnd = options && options.onEnd, playOptionsOnEnd = this.options.playOptions && this.options.playOptions.onEnd, playOptions = merge(this.options.playOptions, options); if (eventObject && eventObject.sonify) { // If we have multiple onEnds defined, use all playOptions.onEnd = masterOnEnd || playOnEnd || playOptionsOnEnd ? function () { var args = arguments; [masterOnEnd, playOnEnd, playOptionsOnEnd].forEach(function (onEnd) { if (onEnd) { onEnd.apply(this, args); } }); } : void 0; eventObject.sonify(playOptions); } else { if (playOnEnd) { playOnEnd(); } if (masterOnEnd) { masterOnEnd(); } } }; /** * Cancel the sonification of this event. Does nothing if the event is not * currently sonifying. * * @function Highcharts.TimelineEvent#cancel * * @param {boolean} [fadeOut=false] * Whether or not to fade out as we stop. If false, the event is * cancelled synchronously. */ TimelineEvent.prototype.cancel = function (fadeOut) { this.options.eventObject.cancelSonify(fadeOut); }; /** * A set of options for the TimelinePath class. * * @requires module:modules/ * * @private * @interface Highcharts.TimelinePathOptionsObject */ /** * List of TimelineEvents to play on this track. * @name Highcharts.TimelinePathOptionsObject#events * @type {Array} */ /** * If this option is supplied, this path ignores all events and just waits for * the specified number of milliseconds before calling onEnd. * @name Highcharts.TimelinePathOptionsObject#silentWait * @type {number|undefined} */ /** * Unique ID for this timeline path. Automatically generated if not supplied. * @name Highcharts.TimelinePathOptionsObject#id * @type {string|undefined} */ /** * Callback called before the path starts playing. * @name Highcharts.TimelinePathOptionsObject#onStart * @type {Function|undefined} */ /** * Callback function to call before an event plays. * @name Highcharts.TimelinePathOptionsObject#onEventStart * @type {Function|undefined} */ /** * Callback function to call after an event has stopped playing. * @name Highcharts.TimelinePathOptionsObject#onEventEnd * @type {Function|undefined} */ /** * Callback called when the whole path is finished. * @name Highcharts.TimelinePathOptionsObject#onEnd * @type {Function|undefined} */ /** * The TimelinePath class. Represents a track on a timeline with a list of * sound events to play at certain times relative to each other. * * @requires module:modules/sonification * * @private * @class * @name Highcharts.TimelinePath * * @param {Highcharts.TimelinePathOptionsObject} options * Options for the TimelinePath. */ function TimelinePath(options) { this.init(options); } TimelinePath.prototype.init = function (options) { this.options = options; this.id = this.options.id = options.id || uniqueKey(); this.cursor = 0; this.eventsPlaying = {}; // Handle silent wait, otherwise use events from options this.events = options.silentWait ? [ new TimelineEvent({ time: 0 }), new TimelineEvent({ time: options.silentWait }) ] : this.options.events; // We need to sort our events by time this.sortEvents(); // Get map from event ID to index this.updateEventIdMap(); // Signal events to fire this.signalHandler = new utilities.SignalHandler(['playOnEnd', 'masterOnEnd', 'onStart', 'onEventStart', 'onEventEnd']); this.signalHandler.registerSignalCallbacks(merge(options, { masterOnEnd: options.onEnd })); }; /** * Sort the internal event list by time. * @private */ TimelinePath.prototype.sortEvents = function () { this.events = this.events.sort(function (a, b) { return a.time - b.time; }); }; /** * Update the internal eventId to index map. * @private */ TimelinePath.prototype.updateEventIdMap = function () { this.eventIdMap = this.events.reduce(function (acc, cur, i) { acc[cur.id] = i; return acc; }, {}); }; /** * Add events to the path. Should not be done while the path is playing. * The new events are inserted according to their time property. * @private * @param {Array} newEvents - The new timeline events * to add. */ TimelinePath.prototype.addTimelineEvents = function (newEvents) { this.events = this.events.concat(newEvents); this.sortEvents(); // Sort events by time this.updateEventIdMap(); // Update the event ID to index map }; /** * Get the current TimelineEvent under the cursor. * @private * @return {Highcharts.TimelineEvent} The current timeline event. */ TimelinePath.prototype.getCursor = function () { return this.events[this.cursor]; }; /** * Set the current TimelineEvent under the cursor. * @private * @param {string} eventId * The ID of the timeline event to set as current. * @return {boolean} * True if there is an event with this ID in the path. False otherwise. */ TimelinePath.prototype.setCursor = function (eventId) { var ix = this.eventIdMap[eventId]; if (typeof ix !== 'undefined') { this.cursor = ix; return true; } return false; }; /** * Play the timeline from the current cursor. * @private * @param {Function} onEnd * Callback to call when play finished. Does not override other onEnd callbacks. * @return {void} */ TimelinePath.prototype.play = function (onEnd) { this.pause(); this.signalHandler.emitSignal('onStart'); this.signalHandler.clearSignalCallbacks(['playOnEnd']); this.signalHandler.registerSignalCallbacks({ playOnEnd: onEnd }); this.playEvents(1); }; /** * Play the timeline backwards from the current cursor. * @private * @param {Function} onEnd * Callback to call when play finished. Does not override other onEnd callbacks. * @return {void} */ TimelinePath.prototype.rewind = function (onEnd) { this.pause(); this.signalHandler.emitSignal('onStart'); this.signalHandler.clearSignalCallbacks(['playOnEnd']); this.signalHandler.registerSignalCallbacks({ playOnEnd: onEnd }); this.playEvents(-1); }; /** * Reset the cursor to the beginning. * @private */ TimelinePath.prototype.resetCursor = function () { this.cursor = 0; }; /** * Reset the cursor to the end. * @private */ TimelinePath.prototype.resetCursorEnd = function () { this.cursor = this.events.length - 1; }; /** * Cancel current playing. Leaves the cursor intact. * @private * @param {boolean} [fadeOut=false] - Whether or not to fade out as we stop. If * false, the path is cancelled synchronously. */ TimelinePath.prototype.pause = function (fadeOut) { var timelinePath = this; // Cancel next scheduled play clearTimeout(timelinePath.nextScheduledPlay); // Cancel currently playing events Object.keys(timelinePath.eventsPlaying).forEach(function (id) { if (timelinePath.eventsPlaying[id]) { timelinePath.eventsPlaying[id].cancel(fadeOut); } }); timelinePath.eventsPlaying = {}; }; /** * Play the events, starting from current cursor, and going in specified * direction. * @private * @param {number} direction * The direction to play, 1 for forwards and -1 for backwards. * @return {void} */ TimelinePath.prototype.playEvents = function (direction) { var timelinePath = this, curEvent = timelinePath.events[this.cursor], nextEvent = timelinePath.events[this.cursor + direction], timeDiff, onEnd = function (signalData) { timelinePath.signalHandler.emitSignal('masterOnEnd', signalData); timelinePath.signalHandler.emitSignal('playOnEnd', signalData); }; // Store reference to path on event curEvent.timelinePath = timelinePath; // Emit event, cancel if returns false if (timelinePath.signalHandler.emitSignal('onEventStart', curEvent) === false) { onEnd({ event: curEvent, cancelled: true }); return; } // Play the current event timelinePath.eventsPlaying[curEvent.id] = curEvent; curEvent.play({ onEnd: function (cancelled) { var signalData = { event: curEvent, cancelled: !!cancelled }; // Keep track of currently playing events for cancelling delete timelinePath.eventsPlaying[curEvent.id]; // Handle onEventEnd timelinePath.signalHandler.emitSignal('onEventEnd', signalData); // Reached end of path? if (!nextEvent) { onEnd(signalData); } } }); // Schedule next if (nextEvent) { timeDiff = Math.abs(nextEvent.time - curEvent.time); if (timeDiff < 1) { // Play immediately timelinePath.cursor += direction; timelinePath.playEvents(direction); } else { // Schedule after the difference in ms this.nextScheduledPlay = setTimeout(function () { timelinePath.cursor += direction; timelinePath.playEvents(direction); }, timeDiff); } } }; /* ************************************************************************** * * TIMELINE * * ************************************************************************** */ /** * A set of options for the Timeline class. * * @requires module:modules/sonification * * @private * @interface Highcharts.TimelineOptionsObject */ /** * List of TimelinePaths to play. Multiple paths can be grouped together and * played simultaneously by supplying an array of paths in place of a single * path. * @name Highcharts.TimelineOptionsObject#paths * @type {Array<(Highcharts.TimelinePath|Array)>} */ /** * Callback function to call before a path plays. * @name Highcharts.TimelineOptionsObject#onPathStart * @type {Function|undefined} */ /** * Callback function to call after a path has stopped playing. * @name Highcharts.TimelineOptionsObject#onPathEnd * @type {Function|undefined} */ /** * Callback called when the whole path is finished. * @name Highcharts.TimelineOptionsObject#onEnd * @type {Function|undefined} */ /** * The Timeline class. Represents a sonification timeline with a list of * timeline paths with events to play at certain times relative to each other. * * @requires module:modules/sonification * * @private * @class * @name Highcharts.Timeline * * @param {Highcharts.TimelineOptionsObject} options * Options for the Timeline. */ function Timeline(options) { this.init(options || {}); } Timeline.prototype.init = function (options) { this.options = options; this.cursor = 0; this.paths = options.paths; this.pathsPlaying = {}; this.signalHandler = new utilities.SignalHandler(['playOnEnd', 'masterOnEnd', 'onPathStart', 'onPathEnd']); this.signalHandler.registerSignalCallbacks(merge(options, { masterOnEnd: options.onEnd })); }; /** * Play the timeline forwards from cursor. * @private * @param {Function} [onEnd] * Callback to call when play finished. Does not override other onEnd callbacks. * @return {void} */ Timeline.prototype.play = function (onEnd) { this.pause(); this.signalHandler.clearSignalCallbacks(['playOnEnd']); this.signalHandler.registerSignalCallbacks({ playOnEnd: onEnd }); this.playPaths(1); }; /** * Play the timeline backwards from cursor. * @private * @param {Function} onEnd * Callback to call when play finished. Does not override other onEnd callbacks. * @return {void} */ Timeline.prototype.rewind = function (onEnd) { this.pause(); this.signalHandler.clearSignalCallbacks(['playOnEnd']); this.signalHandler.registerSignalCallbacks({ playOnEnd: onEnd }); this.playPaths(-1); }; /** * Play the timeline in the specified direction. * @private * @param {number} direction * Direction to play in. 1 for forwards, -1 for backwards. * @return {void} */ Timeline.prototype.playPaths = function (direction) { var curPaths = splat(this.paths[this.cursor]), nextPaths = this.paths[this.cursor + direction], timeline = this, signalHandler = this.signalHandler, pathsEnded = 0, // Play a path playPath = function (path) { // Emit signal and set playing state signalHandler.emitSignal('onPathStart', path); timeline.pathsPlaying[path.id] = path; // Do the play path[direction > 0 ? 'play' : 'rewind'](function (callbackData) { // Play ended callback // Data to pass to signal callbacks var cancelled = callbackData && callbackData.cancelled, signalData = { path: path, cancelled: cancelled }; // Clear state and send signal delete timeline.pathsPlaying[path.id]; signalHandler.emitSignal('onPathEnd', signalData); // Handle next paths pathsEnded++; if (pathsEnded >= curPaths.length) { // We finished all of the current paths for cursor. if (nextPaths && !cancelled) { // We have more paths, move cursor along timeline.cursor += direction; // Reset upcoming path cursors before playing splat(nextPaths).forEach(function (nextPath) { nextPath[direction > 0 ? 'resetCursor' : 'resetCursorEnd'](); }); // Play next timeline.playPaths(direction); } else { // If it is the last path in this direction, call onEnd signalHandler.emitSignal('playOnEnd', signalData); signalHandler.emitSignal('masterOnEnd', signalData); } } }); }; // Go through the paths under cursor and play them curPaths.forEach(function (path) { if (path) { // Store reference to timeline path.timeline = timeline; // Leave a timeout to let notes fade out before next play setTimeout(function () { playPath(path); }, H.sonification.fadeOutDuration); } }); }; /** * Stop the playing of the timeline. Cancels all current sounds, but does not * affect the cursor. * @private * @param {boolean} [fadeOut=false] * Whether or not to fade out as we stop. If false, the timeline is cancelled * synchronously. * @return {void} */ Timeline.prototype.pause = function (fadeOut) { var timeline = this; // Cancel currently playing events Object.keys(timeline.pathsPlaying).forEach(function (id) { if (timeline.pathsPlaying[id]) { timeline.pathsPlaying[id].pause(fadeOut); } }); timeline.pathsPlaying = {}; }; /** * Reset the cursor to the beginning of the timeline. * @private * @return {void} */ Timeline.prototype.resetCursor = function () { this.paths.forEach(function (paths) { splat(paths).forEach(function (path) { path.resetCursor(); }); }); this.cursor = 0; }; /** * Reset the cursor to the end of the timeline. * @private * @return {void} */ Timeline.prototype.resetCursorEnd = function () { this.paths.forEach(function (paths) { splat(paths).forEach(function (path) { path.resetCursorEnd(); }); }); this.cursor = this.paths.length - 1; }; /** * Set the current TimelineEvent under the cursor. If multiple paths are being * played at the same time, this function only affects a single path (the one * that contains the eventId that is passed in). * @private * @param {string} eventId * The ID of the timeline event to set as current. * @return {boolean} * True if the cursor was set, false if no TimelineEvent was found for this ID. */ Timeline.prototype.setCursor = function (eventId) { return this.paths.some(function (paths) { return splat(paths).some(function (path) { return path.setCursor(eventId); }); }); }; /** * Get the current TimelineEvents under the cursors. This function will return * the event under the cursor for each currently playing path, as an object * where the path ID is mapped to the TimelineEvent under that path's cursor. * @private * @return {Highcharts.Dictionary} * The TimelineEvents under each path's cursors. */ Timeline.prototype.getCursor = function () { return this.getCurrentPlayingPaths().reduce(function (acc, cur) { acc[cur.id] = cur.getCursor(); return acc; }, {}); }; /** * Check if timeline is reset or at start. * @private * @return {boolean} * True if timeline is at the beginning. */ Timeline.prototype.atStart = function () { return !this.getCurrentPlayingPaths().some(function (path) { return path.cursor; }); }; /** * Get the current TimelinePaths being played. * @private * @return {Array} * The TimelinePaths currently being played. */ Timeline.prototype.getCurrentPlayingPaths = function () { return splat(this.paths[this.cursor]); }; // Export the classes var timelineClasses = { TimelineEvent: TimelineEvent, TimelinePath: TimelinePath, Timeline: Timeline }; return timelineClasses; }); _registerModule(_modules, 'modules/sonification/options.js', [], function () { /* * * * (c) 2009-2020 Øystein Moseng * * Default options for sonification. * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ // Experimental, disabled by default, not exposed in API var options = { sonification: { enabled: false, duration: 2000, afterSeriesWait: 1000, order: 'sequential', pointPlayTime: 'x', instruments: [{ instrument: 'sineMusical', instrumentMapping: { duration: 400, frequency: 'y', volume: 0.7 }, // Start at G4 note, end at C6 instrumentOptions: { minFrequency: 392, maxFrequency: 1046 } }] } }; return options; }); _registerModule(_modules, 'modules/sonification/sonification.js', [_modules['parts/Globals.js'], _modules['parts/Point.js'], _modules['parts/Utilities.js'], _modules['modules/sonification/Instrument.js'], _modules['modules/sonification/instrumentDefinitions.js'], _modules['modules/sonification/Earcon.js'], _modules['modules/sonification/pointSonify.js'], _modules['modules/sonification/chartSonify.js'], _modules['modules/sonification/utilities.js'], _modules['modules/sonification/Timeline.js'], _modules['modules/sonification/options.js']], function (H, Point, U, Instrument, instruments, Earcon, pointSonifyFunctions, chartSonifyFunctions, utilities, TimelineClasses, sonificationOptions) { /* * * * (c) 2009-2020 Øystein Moseng * * Sonification module for Highcharts * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ var addEvent = U.addEvent, extend = U.extend, merge = U.merge; // Expose on the Highcharts object /** * Global classes and objects related to sonification. * * @requires module:modules/sonification * * @name Highcharts.sonification * @type {Highcharts.SonificationObject} */ /** * Global classes and objects related to sonification. * * @requires module:modules/sonification * * @interface Highcharts.SonificationObject */ /** * Note fade-out-time in milliseconds. Most notes are faded out quickly by * default if there is time. This is to avoid abrupt stops which will cause * perceived clicks. * @name Highcharts.SonificationObject#fadeOutDuration * @type {number} */ /** * Utility functions. * @name Highcharts.SonificationObject#utilities * @private * @type {object} */ /** * The Instrument class. * @name Highcharts.SonificationObject#Instrument * @type {Function} */ /** * Predefined instruments, given as an object with a map between the instrument * name and the Highcharts.Instrument object. * @name Highcharts.SonificationObject#instruments * @type {Object} */ /** * The Earcon class. * @name Highcharts.SonificationObject#Earcon * @type {Function} */ /** * The TimelineEvent class. * @private * @name Highcharts.SonificationObject#TimelineEvent * @type {Function} */ /** * The TimelinePath class. * @private * @name Highcharts.SonificationObject#TimelinePath * @type {Function} */ /** * The Timeline class. * @private * @name Highcharts.SonificationObject#Timeline * @type {Function} */ H.sonification = { fadeOutDuration: 20, // Classes and functions utilities: utilities, Instrument: Instrument, instruments: instruments, Earcon: Earcon, TimelineEvent: TimelineClasses.TimelineEvent, TimelinePath: TimelineClasses.TimelinePath, Timeline: TimelineClasses.Timeline }; // Add default options merge(true, H.defaultOptions, sonificationOptions); // Chart specific Point.prototype.sonify = pointSonifyFunctions.pointSonify; Point.prototype.cancelSonify = pointSonifyFunctions.pointCancelSonify; H.Series.prototype.sonify = chartSonifyFunctions.seriesSonify; extend(H.Chart.prototype, { sonify: chartSonifyFunctions.chartSonify, pauseSonify: chartSonifyFunctions.pause, resumeSonify: chartSonifyFunctions.resume, rewindSonify: chartSonifyFunctions.rewind, cancelSonify: chartSonifyFunctions.cancel, getCurrentSonifyPoints: chartSonifyFunctions.getCurrentPoints, setSonifyCursor: chartSonifyFunctions.setCursor, resetSonifyCursor: chartSonifyFunctions.resetCursor, resetSonifyCursorEnd: chartSonifyFunctions.resetCursorEnd }); /* eslint-disable no-invalid-this */ // Prepare charts for sonification on init addEvent(H.Chart, 'init', function () { this.sonification = {}; }); // Update with chart/series/point updates addEvent(H.Chart, 'update', function (e) { var newOptions = e.options.sonification; if (newOptions) { merge(true, this.options.sonification, newOptions); } }); }); _registerModule(_modules, 'masters/modules/sonification.src.js', [], function () { }); }));