/** * @license Highcharts JS v7.0.3 (2019-02-06) * 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(function () { return factory; }); } else { factory(typeof Highcharts !== 'undefined' ? Highcharts : undefined); } }(function (Highcharts) { var Instrument = (function (H) { /* * * * (c) 2009-2019 Øystein Moseng * * Instrument class for sonification module. * * License: www.highcharts.com/license * * */ /** * A set of options for the Instrument class. * * @requires module:modules/sonification * * @interface Highcharts.InstrumentOptionsObject *//** * The type of instrument. Currently only `oscillator` is supported. Defaults * to `oscillator`. * @name Highcharts.InstrumentOptionsObject#type * @type {string|undefined} *//** * The unique ID of the instrument. Generated if not supplied. * @name Highcharts.InstrumentOptionsObject#id * @type {string|undefined} *//** * When using functions to determine frequency or other parameters during * playback, this options specifies how often to call the callback functions. * Number given in milliseconds. Defaults to 20. * @name Highcharts.InstrumentOptionsObject#playCallbackInterval * @type {number|undefined} *//** * A list of allowed frequencies for this instrument. If trying to play a * frequency not on this list, the closest frequency will be used. Set to `null` * to allow all frequencies to be used. Defaults to `null`. * @name Highcharts.InstrumentOptionsObject#allowedFrequencies * @type {Array|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' } }; /** * The Instrument class. Instrument objects represent an instrument capable of * playing a certain pitch for a specified duration. * * @sample highcharts/sonification/instrument/ * Using Instruments directly * @sample highcharts/sonification/instrument-advanced/ * Using callbacks for instrument parameters * * @requires module:modules/sonification * * @class * @name Highcharts.Instrument * * @param {Highcharts.InstrumentOptionsObject} options * Options for the instrument instance. */ function Instrument(options) { this.init(options); } Instrument.prototype.init = function (options) { if (!this.initAudioContext()) { H.error(29); return; } this.options = H.merge(defaultOptions, options); this.id = this.options.id = options && options.id || H.uniqueKey(); // Init the audio nodes var ctx = H.audioContext; this.gainNode = ctx.createGain(); this.setGain(0); this.panNode = ctx.createStereoPanner && ctx.createStereoPanner(); if (this.panNode) { this.setPan(0); this.gainNode.connect(this.panNode); this.panNode.connect(ctx.destination); } else { this.gainNode.connect(ctx.destination); } // Oscillator initialization if (this.options.type === 'oscillator') { this.initOscillator(this.options.oscillator); } // Init timer list this.playCallbackTimers = []; }; /** * Return a copy of an instrument. Only one instrument instance can play at a * time, so use this to get a new copy of the instrument that can play alongside * it. The new instrument copy will receive a new ID unless one is supplied in * options. * * @function Highcharts.Instrument#copy * * @param {Highcharts.InstrumentOptionsObject} [options] * Options to merge in for the copy. * * @return {Highcharts.Instrument} * A new Instrument instance with the same options. */ Instrument.prototype.copy = function (options) { return new Instrument(H.merge(this.options, { id: null }, options)); }; /** * Init the audio context, if we do not have one. * @private * @return {boolean} True if successful, false if not. */ Instrument.prototype.initAudioContext = function () { var Context = H.win.AudioContext || H.win.webkitAudioContext, hasOldContext = !!H.audioContext; if (Context) { H.audioContext = H.audioContext || new Context(); if ( !hasOldContext && H.audioContext && H.audioContext.state === 'running' ) { H.audioContext.suspend(); // Pause until we need it } return !!( H.audioContext && H.audioContext.createOscillator && H.audioContext.createGain ); } return false; }; /** * Init an oscillator instrument. * @private * @param {object} oscillatorOptions - The oscillator options passed to * Highcharts.Instrument#init. */ Instrument.prototype.initOscillator = function (options) { var ctx = H.audioContext; this.oscillator = ctx.createOscillator(); this.oscillator.type = options.waveformShape; this.oscillator.connect(this.gainNode); this.oscillatorStarted = false; }; /** * Set pan position. * @private * @param {number} panValue - The pan position to set for the instrument. */ Instrument.prototype.setPan = function (panValue) { if (this.panNode) { this.panNode.pan.setValueAtTime(panValue, H.audioContext.currentTime); } }; /** * Set gain level. A maximum of 1.2 is allowed before we emit a warning. The * actual volume is not set above this level regardless of input. * @private * @param {number} gainValue - The gain level to set for the instrument. * @param {number} [rampTime=0] - Gradually change the gain level, time given in * milliseconds. */ Instrument.prototype.setGain = function (gainValue, rampTime) { if (this.gainNode) { if (gainValue > 1.2) { console.warn( // eslint-disable-line 'Highcharts sonification warning: ' + 'Volume of instrument set too high.' ); gainValue = 1.2; } if (rampTime) { this.gainNode.gain.setValueAtTime( this.gainNode.gain.value, H.audioContext.currentTime ); this.gainNode.gain.linearRampToValueAtTime( gainValue, H.audioContext.currentTime + rampTime / 1000 ); } else { this.gainNode.gain.setValueAtTime( gainValue, H.audioContext.currentTime ); } } }; /** * Cancel ongoing gain ramps. * @private */ Instrument.prototype.cancelGainRamp = function () { if (this.gainNode) { this.gainNode.gain.cancelScheduledValues(0); } }; /** * Get the closest valid frequency for this instrument. * @private * @param {number} frequency - The target frequency. * @param {number} [min] - Minimum frequency to return. * @param {number} [max] - Maximum frequency to return. * @return {number} The closest valid frequency to the input frequency. */ Instrument.prototype.getValidFrequency = function (frequency, min, max) { var validFrequencies = this.options.allowedFrequencies, maximum = H.pick(max, Infinity), minimum = H.pick(min, -Infinity); return !validFrequencies || !validFrequencies.length ? // No valid frequencies for this instrument, return the target frequency : // Use the valid frequencies and return the closest match validFrequencies.reduce(function (acc, cur) { // Find the closest allowed value return Math.abs(cur - frequency) < Math.abs(acc - frequency) && cur < maximum && cur > minimum ? cur : acc; }, Infinity); }; /** * Clear existing play callback timers. * @private */ Instrument.prototype.clearPlayCallbackTimers = function () { this.playCallbackTimers.forEach(function (timer) { clearInterval(timer); }); this.playCallbackTimers = []; }; /** * Set the current frequency being played by the instrument. The closest valid * frequency between the frequency limits is used. * @param {number} frequency - The frequency to set. * @param {object} [frequencyLimits] - Object with maxFrequency and minFrequency */ Instrument.prototype.setFrequency = function (frequency, frequencyLimits) { var limits = frequencyLimits || {}, validFrequency = this.getValidFrequency( frequency, limits.min, limits.max ); if (this.options.type === 'oscillator') { this.oscillatorPlay(validFrequency); } }; /** * Play oscillator instrument. * @private * @param {number} frequency - The frequency to play. */ Instrument.prototype.oscillatorPlay = function (frequency) { if (!this.oscillatorStarted) { this.oscillator.start(); this.oscillatorStarted = true; } this.oscillator.frequency.setValueAtTime( frequency, H.audioContext.currentTime ); }; /** * Prepare instrument before playing. Resumes the audio context and starts the * oscillator. * @private */ Instrument.prototype.preparePlay = function () { this.setGain(0.001); if (H.audioContext.state === 'suspended') { H.audioContext.resume(); } if (this.oscillator && !this.oscillatorStarted) { this.oscillator.start(); this.oscillatorStarted = true; } }; /** * Play the instrument according to options. * * @sample highcharts/sonification/instrument/ * Using Instruments directly * @sample highcharts/sonification/instrument-advanced/ * Using callbacks for instrument parameters * * @function Highcharts.Instrument#play * * @param {Highcharts.InstrumentPlayOptionsObject} options * Options for the playback of the instrument. */ Instrument.prototype.play = function (options) { var instrument = this, duration = options.duration || 0, // Set a value, or if it is a function, set it continously as a timer. // Pass in the value/function to set, the setter function, and any // additional data to pass through to the setter function. setOrStartTimer = function (value, setter, setterData) { var target = options.duration, currentDurationIx = 0, callbackInterval = instrument.options.playCallbackInterval; if (typeof value === 'function') { var timer = setInterval(function () { currentDurationIx++; var curTime = currentDurationIx * callbackInterval / target; if (curTime >= 1) { instrument[setter](value(1), setterData); clearInterval(timer); } else { instrument[setter](value(curTime), setterData); } }, callbackInterval); instrument.playCallbackTimers.push(timer); } else { instrument[setter](value, setterData); } }; if (!instrument.id) { // No audio support - do nothing return; } // If the AudioContext is suspended we have to resume it before playing if ( H.audioContext.state === 'suspended' || this.oscillator && !this.oscillatorStarted ) { instrument.preparePlay(); // Try again in 10ms setTimeout(function () { instrument.play(options); }, 10); return; } // Clear any existing play timers if (instrument.playCallbackTimers.length) { instrument.clearPlayCallbackTimers(); } // Clear any gain ramps instrument.cancelGainRamp(); // Clear stop oscillator timer if (instrument.stopOscillatorTimeout) { clearTimeout(instrument.stopOscillatorTimeout); delete instrument.stopOscillatorTimeout; } // If a note is playing right now, clear the stop timeout, and call the // callback. if (instrument.stopTimeout) { clearTimeout(instrument.stopTimeout); delete instrument.stopTimeout; if (instrument.stopCallback) { // We have a callback for the play we are interrupting. We do not // allow this callback to start a new play, because that leads to // chaos. We pass in 'cancelled' to indicate that this note did not // finish, but still stopped. instrument._play = instrument.play; instrument.play = function () { }; instrument.stopCallback('cancelled'); instrument.play = instrument._play; } } // Stop the note without fadeOut if the duration is too short to hear the // note otherwise. var immediate = duration < H.sonification.fadeOutDuration + 20; // Stop the instrument after the duration of the note instrument.stopCallback = options.onEnd; var onStop = function () { delete instrument.stopTimeout; instrument.stop(immediate); }; if (duration) { instrument.stopTimeout = setTimeout( onStop, immediate ? duration : duration - H.sonification.fadeOutDuration ); // Play the note setOrStartTimer(options.frequency, 'setFrequency', null, { minFrequency: options.minFrequency, maxFrequency: options.maxFrequency }); // Set the volume and panning setOrStartTimer(H.pick(options.volume, 1), 'setGain', 4); // Slight ramp setOrStartTimer(H.pick(options.pan, 0), 'setPan'); } else { // No note duration, so just stop immediately onStop(); } }; /** * Mute an instrument that is playing. If the instrument is not currently * playing, this function does nothing. * * @function Highcharts.Instrument#mute */ Instrument.prototype.mute = function () { this.setGain(0.0001, H.sonification.fadeOutDuration * 0.8); }; /** * Stop the instrument playing. * * @function Highcharts.Instrument#stop * * @param {boolean} immediately * Whether to do the stop immediately or fade out. * * @param {Function} onStopped * Callback function to be called when the stop is completed. * * @param {*} callbackData * Data to send to the onEnd callback functions. */ Instrument.prototype.stop = function (immediately, onStopped, callbackData) { var instr = this, reset = function () { // Remove timeout reference if (instr.stopOscillatorTimeout) { delete instr.stopOscillatorTimeout; } // The oscillator may have stopped in the meantime here, so allow // this function to fail if so. try { instr.oscillator.stop(); } catch (e) {} instr.oscillator.disconnect(instr.gainNode); // We need a new oscillator in order to restart it instr.initOscillator(instr.options.oscillator); // Done stopping, call the callback from the stop if (onStopped) { onStopped(callbackData); } // Call the callback for the play we finished if (instr.stopCallback) { instr.stopCallback(callbackData); } }; // Clear any existing timers if (instr.playCallbackTimers.length) { instr.clearPlayCallbackTimers(); } if (instr.stopTimeout) { clearTimeout(instr.stopTimeout); } if (immediately) { instr.setGain(0); reset(); } else { instr.mute(); // Stop the oscillator after the mute fade-out has finished instr.stopOscillatorTimeout = setTimeout(reset, H.sonification.fadeOutDuration + 100); } }; return Instrument; }(Highcharts)); var frequencies = (function () { /* * * * (c) 2009-2019 Øystein Moseng * * List of musical frequencies from C0 to C8. * * License: www.highcharts.com/license * * */ var frequencies = [ 16.351597831287414, // C0 17.323914436054505, 18.354047994837977, 19.445436482630058, 20.601722307054366, 21.826764464562746, 23.12465141947715, 24.499714748859326, 25.956543598746574, 27.5, // A0 29.13523509488062, 30.86770632850775, 32.70319566257483, // C1 34.64782887210901, 36.70809598967594, 38.890872965260115, 41.20344461410875, 43.653528929125486, 46.2493028389543, 48.999429497718666, 51.91308719749314, 55, // A1 58.27047018976124, 61.7354126570155, 65.40639132514966, // C2 69.29565774421802, 73.41619197935188, 77.78174593052023, 82.4068892282175, 87.30705785825097, 92.4986056779086, 97.99885899543733, 103.82617439498628, 110, // A2 116.54094037952248, 123.47082531403103, 130.8127826502993, // C3 138.59131548843604, 146.8323839587038, 155.56349186104046, 164.81377845643496, 174.61411571650194, 184.9972113558172, 195.99771799087463, 207.65234878997256, 220, // A3 233.08188075904496, 246.94165062806206, 261.6255653005986, // C4 277.1826309768721, 293.6647679174076, 311.1269837220809, 329.6275569128699, 349.2282314330039, 369.9944227116344, 391.99543598174927, 415.3046975799451, 440, // A4 466.1637615180899, 493.8833012561241, 523.2511306011972, // C5 554.3652619537442, 587.3295358348151, 622.2539674441618, 659.2551138257398, 698.4564628660078, 739.9888454232688, 783.9908719634985, 830.6093951598903, 880, // A5 932.3275230361799, 987.7666025122483, 1046.5022612023945, // C6 1108.7305239074883, 1174.6590716696303, 1244.5079348883237, 1318.5102276514797, 1396.9129257320155, 1479.9776908465376, 1567.981743926997, 1661.2187903197805, 1760, // A6 1864.6550460723597, 1975.533205024496, 2093.004522404789, // C7 2217.4610478149766, 2349.31814333926, 2489.0158697766474, 2637.02045530296, 2793.825851464031, 2959.955381693075, 3135.9634878539946, 3322.437580639561, 3520, // A7 3729.3100921447194, 3951.066410048992, 4186.009044809578 // C8 ]; return frequencies; }()); var utilities = (function (musicalFrequencies) { /* * * * (c) 2009-2019 Øystein Moseng * * Utility functions for sonification. * * License: www.highcharts.com/license * * */ /** * 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 {object} signals - An object that contains a mapping from the signal * name to the callbacks. Only supported events are considered. */ SignalHandler.prototype.registerSignalCallbacks = function (signals) { var signalHandler = this; signalHandler.supportedSignals.forEach(function (supportedSignal) { if (signals[supportedSignal]) { ( signalHandler.signals[supportedSignal] = signalHandler.signals[supportedSignal] || [] ).push( signals[supportedSignal] ); } }); }; /** * Clear signal callbacks, optionally by name. * @private * @param {Array} [signalNames] - A list of signal names to clear. If * not supplied, all signal callbacks are removed. */ SignalHandler.prototype.clearSignalCallbacks = function (signalNames) { var signalHandler = this; if (signalNames) { signalNames.forEach(function (signalName) { if (signalHandler.signals[signalName]) { delete signalHandler.signals[signalName]; } }); } else { signalHandler.signals = {}; } }; /** * Emit a signal. Does nothing if the signal does not exist, or has no * registered callbacks. * @private * @param {string} signalNames - Name of signal to emit. * @param {*} data - Data to pass to the callback. */ SignalHandler.prototype.emitSignal = function (signalName, data) { var retval; if (this.signals[signalName]) { this.signals[signalName].forEach(function (handler) { var result = handler(data); retval = result !== undefined ? result : retval; }); } return retval; }; var utilities = { // List of musical frequencies from C0 to C8 musicalFrequencies: musicalFrequencies, // SignalHandler class SignalHandler: SignalHandler, /** * Get a musical scale by specifying the semitones from 1-12 to include. * 1: C, 2: C#, 3: D, 4: D#, 5: E, 6: F, * 7: F#, 8: G, 9: G#, 10: A, 11: Bb, 12: B * @private * @param {Array} 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 {object} Object with min and max properties */ calculateDataExtremes: function (chart, prop) { return chart.series.reduce(function (extremes, series) { // We use cropped points rather than series.data here, to allow // users to zoom in for better fidelity. series.points.forEach(function (point) { var val = point[prop] !== undefined ? point[prop] : point.options[prop]; extremes.min = Math.min(extremes.min, val); extremes.max = Math.max(extremes.max, val); }); return extremes; }, { min: Infinity, max: -Infinity }); }, /** * Translate a value on a virtual axis. Creates a new, virtual, axis with a * min and max, and maps the relative value onto this axis. * @private * @param {number} value - The relative data value to translate. * @param {object} dataExtremes - The possible extremes for this value. * @param {object} limits - Limits for the virtual axis. * @return {number} The value mapped to the virtual axis. */ virtualAxisTranslate: function (value, dataExtremes, limits) { var lenValueAxis = dataExtremes.max - dataExtremes.min, lenVirtualAxis = limits.max - limits.min, virtualAxisValue = limits.min + lenVirtualAxis * (value - dataExtremes.min) / lenValueAxis; return lenValueAxis > 0 ? Math.max(Math.min(virtualAxisValue, limits.max), limits.min) : limits.min; } }; return utilities; }(frequencies)); var instruments = (function (Instrument, utilities) { /* * * * (c) 2009-2019 Øystein Moseng * * Instrument definitions for sonification module. * * License: www.highcharts.com/license * * */ 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; }(Instrument, utilities)); var Earcon = (function (H) { /* * * * (c) 2009-2019 Øystein Moseng * * Earcons for the sonification module in Highcharts. * * License: www.highcharts.com/license * * */ /** * Define an Instrument and the options for playing it. * * @requires module:modules/sonification * * @interface Highcharts.EarconInstrument *//** * An instrument instance or the name of the instrument in the * Highcharts.sonification.instruments map. * @name Highcharts.EarconInstrument#instrument * @type {Highcharts.Instrument|String} *//** * The options to pass to Instrument.play. * @name Highcharts.EarconInstrument#playOptions * @type {object} */ /** * Options for an Earcon. * * @requires module:modules/sonification * * @interface Highcharts.EarconOptionsObject *//** * The instruments and their options defining this earcon. * @name Highcharts.EarconOptionsObject#instruments * @type {Array} *//** * The unique ID of the Earcon. Generated if not supplied. * @name Highcharts.EarconOptionsObject#id * @type {string|undefined} *//** * Global panning of all instruments. Overrides all panning on individual * instruments. Can be a number between -1 and 1. * @name Highcharts.EarconOptionsObject#pan * @type {number|undefined} *//** * Master volume for all instruments. Volume settings on individual instruments * can still be used for relative volume between the instruments. This setting * does not affect volumes set by functions in individual instruments. Can be a * number between 0 and 1. Defaults to 1. * @name Highcharts.EarconOptionsObject#volume * @type {number|undefined} *//** * Callback function to call when earcon has finished playing. * @name Highcharts.EarconOptionsObject#onEnd * @type {Function|undefined} */ /** * The Earcon class. Earcon objects represent a certain sound consisting of * one or more instruments playing a predefined sound. * * @sample highcharts/sonification/earcon/ * Using earcons directly * * @requires module:modules/sonification * * @class * @name Highcharts.Earcon * * @param {Highcharts.EarconOptionsObject} options * Options for the Earcon instance. */ function Earcon(options) { this.init(options || {}); } Earcon.prototype.init = function (options) { this.options = options; if (!this.options.id) { this.options.id = this.id = H.uniqueKey(); } this.instrumentsPlaying = {}; }; /** * Play the earcon, optionally overriding init options. * * @sample highcharts/sonification/earcon/ * Using earcons directly * * @function Highcharts.Earcon#sonify * * @param {Highcharts.EarconOptionsObject} options * Override existing options. */ Earcon.prototype.sonify = function (options) { var playOptions = H.merge(this.options, options); // Find master volume/pan settings var masterVolume = H.pick(playOptions.volume, 1), masterPan = playOptions.pan, earcon = this, playOnEnd = options && options.onEnd, masterOnEnd = earcon.options.onEnd; // Go through the instruments and play them playOptions.instruments.forEach(function (opts) { var instrument = typeof opts.instrument === 'string' ? H.sonification.instruments[opts.instrument] : opts.instrument, instrumentOpts = H.merge(opts.playOptions), instrOnEnd, instrumentCopy, copyId; if (instrument && instrument.play) { if (opts.playOptions) { // Handle master pan/volume if (typeof opts.playOptions.volume !== 'function') { instrumentOpts.volume = H.pick(masterVolume, 1) * H.pick(opts.playOptions.volume, 1); } instrumentOpts.pan = H.pick(masterPan, instrumentOpts.pan); // Handle onEnd instrOnEnd = instrumentOpts.onEnd; instrumentOpts.onEnd = function () { delete earcon.instrumentsPlaying[copyId]; if (instrOnEnd) { instrOnEnd.apply(this, arguments); } if (!Object.keys(earcon.instrumentsPlaying).length) { if (playOnEnd) { playOnEnd.apply(this, arguments); } if (masterOnEnd) { masterOnEnd.apply(this, arguments); } } }; // Play the instrument. Use a copy so we can play multiple at // the same time. instrumentCopy = instrument.copy(); copyId = instrumentCopy.id; earcon.instrumentsPlaying[copyId] = instrumentCopy; instrumentCopy.play(instrumentOpts); } } else { H.error(30); } }); }; /** * Cancel any current sonification of the Earcon. Calls onEnd functions. * * @function Highcharts.Earcon#cancelSonify * * @param {boolean} [fadeOut=false] * Whether or not to fade out as we stop. If false, the earcon is * cancelled synchronously. */ Earcon.prototype.cancelSonify = function (fadeOut) { var playing = this.instrumentsPlaying, instrIds = playing && Object.keys(playing); if (instrIds && instrIds.length) { instrIds.forEach(function (instr) { playing[instr].stop(!fadeOut, null, 'cancelled'); }); this.instrumentsPlaying = {}; } }; return Earcon; }(Highcharts)); var pointSonifyFunctions = (function (H, utilities) { /* * * * (c) 2009-2019 Øystein Moseng * * Code for sonifying single points. * * License: www.highcharts.com/license * * */ /** * Define the parameter mapping for an instrument. * * @requires module:modules/sonification * * @interface Highcharts.PointInstrumentMappingObject *//** * Define the volume of the instrument. This can be a string with a data * property name, e.g. `'y'`, in which case this data property is used to define * the volume relative to the `y`-values of the other points. A higher `y` value * would then result in a higher volume. This option can also be a fixed number * or a function. If it is a function, this function is called in regular * intervals while the note is playing. It receives three arguments: The point, * the dataExtremes, and the current relative time - where 0 is the beginning of * the note and 1 is the end. The function should return the volume of the note * as a number between 0 and 1. * @name Highcharts.PointInstrumentMappingObject#volume * @type {string|number|Function} *//** * Define the duration of the notes for this instrument. This can be a string * with a data property name, e.g. `'y'`, in which case this data property is * used to define the duration relative to the `y`-values of the other points. A * higher `y` value would then result in a longer duration. This option can also * be a fixed number or a function. If it is a function, this function is called * once before the note starts playing, and should return the duration in * milliseconds. It receives two arguments: The point, and the dataExtremes. * @name Highcharts.PointInstrumentMappingObject#duration * @type {string|number|Function} *//** * Define the panning of the instrument. This can be a string with a data * property name, e.g. `'x'`, in which case this data property is used to define * the panning relative to the `x`-values of the other points. A higher `x` * value would then result in a higher panning value (panned further to the * right). This option can also be a fixed number or a function. If it is a * function, this function is called in regular intervals while the note is * playing. It receives three arguments: The point, the dataExtremes, and the * current relative time - where 0 is the beginning of the note and 1 is the * end. The function should return the panning of the note as a number between * -1 and 1. * @name Highcharts.PointInstrumentMappingObject#pan * @type {string|number|Function|undefined} *//** * Define the frequency of the instrument. This can be a string with a data * property name, e.g. `'y'`, in which case this data property is used to define * the frequency relative to the `y`-values of the other points. A higher `y` * value would then result in a higher frequency. This option can also be a * fixed number or a function. If it is a function, this function is called in * regular intervals while the note is playing. It receives three arguments: * The point, the dataExtremes, and the current relative time - where 0 is the * beginning of the note and 1 is the end. The function should return the * frequency of the note as a number (in Hz). * @name Highcharts.PointInstrumentMappingObject#frequency * @type {string|number|Function} */ /** * @requires module:modules/sonification * * @interface Highcharts.PointInstrumentOptionsObject *//** * The minimum duration for a note when using a data property for duration. Can * be overridden by using either a fixed number or a function for * instrumentMapping.duration. Defaults to 20. * @name Highcharts.PointInstrumentOptionsObject#minDuration * @type {number|undefined} *//** * The maximum duration for a note when using a data property for duration. Can * be overridden by using either a fixed number or a function for * instrumentMapping.duration. Defaults to 2000. * @name Highcharts.PointInstrumentOptionsObject#maxDuration * @type {number|undefined} *//** * The minimum pan value for a note when using a data property for panning. Can * be overridden by using either a fixed number or a function for * instrumentMapping.pan. Defaults to -1 (fully left). * @name Highcharts.PointInstrumentOptionsObject#minPan * @type {number|undefined} *//** * The maximum pan value for a note when using a data property for panning. Can * be overridden by using either a fixed number or a function for * instrumentMapping.pan. Defaults to 1 (fully right). * @name Highcharts.PointInstrumentOptionsObject#maxPan * @type {number|undefined} *//** * The minimum volume for a note when using a data property for volume. Can be * overridden by using either a fixed number or a function for * instrumentMapping.volume. Defaults to 0.1. * @name Highcharts.PointInstrumentOptionsObject#minVolume * @type {number|undefined} *//** * The maximum volume for a note when using a data property for volume. Can be * overridden by using either a fixed number or a function for * instrumentMapping.volume. Defaults to 1. * @name Highcharts.PointInstrumentOptionsObject#maxVolume * @type {number|undefined} *//** * The minimum frequency for a note when using a data property for frequency. * Can be overridden by using either a fixed number or a function for * instrumentMapping.frequency. Defaults to 220. * @name Highcharts.PointInstrumentOptionsObject#minFrequency * @type {number|undefined} *//** * The maximum frequency for a note when using a data property for frequency. * Can be overridden by using either a fixed number or a function for * instrumentMapping.frequency. Defaults to 2200. * @name Highcharts.PointInstrumentOptionsObject#maxFrequency * @type {number|undefined} */ /** * An instrument definition for a point, specifying the instrument to play and * how to play it. * * @interface Highcharts.PointInstrumentObject *//** * An Instrument instance or the name of the instrument in the * Highcharts.sonification.instruments map. * @name Highcharts.PointInstrumentObject#instrument * @type {Highcharts.Instrument|string} *//** * Mapping of instrument parameters for this instrument. * @name Highcharts.PointInstrumentObject#instrumentMapping * @type {Highcharts.PointInstrumentMappingObject} *//** * Options for this instrument. * @name Highcharts.PointInstrumentObject#instrumentOptions * @type {Highcharts.PointInstrumentOptionsObject|undefined} *//** * Callback to call when the instrument has stopped playing. * @name Highcharts.PointInstrumentObject#onEnd * @type {Function|undefined} */ /** * Options for sonifying a point. * @interface Highcharts.PointSonifyOptionsObject *//** * The instrument definitions for this point. * @name Highcharts.PointSonifyOptionsObject#instruments * @type {Array} *//** * 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 }; /** * Sonify a single point. * * @sample highcharts/sonification/point-basic/ * Click on points to sonify * @sample highcharts/sonification/point-advanced/ * Sonify bubbles * * @requires module:modules/sonification * * @function Highcharts.Point#sonify * * @param {Highcharts.PointSonifyOptionsObject} options * Options for the sonification of the point. */ function pointSonify(options) { var point = this, chart = point.series.chart, dataExtremes = options.dataExtremes || {}, // Get the value to pass to instrument.play from the mapping value // passed in. getMappingValue = function ( value, makeFunction, allowedExtremes, allowedValues ) { // Fixed number, just use that if (typeof value === 'number' || value === undefined) { return value; } // Function. Return new function if we try to use callback, // otherwise call it now and return result. if (typeof value === 'function') { return makeFunction ? function (time) { return value(point, dataExtremes, time); } : value(point, dataExtremes); } // String, this is a data prop. if (typeof value === 'string') { // Find data extremes if we don't have them dataExtremes[value] = dataExtremes[value] || utilities.calculateDataExtremes( point.series.chart, value ); // Find the value return utilities.virtualAxisTranslate( H.pick(point[value], point.options[value]), dataExtremes[value], allowedExtremes, allowedValues ); } }; // Register playing point on chart chart.sonification.currentlyPlayingPoint = point; // Keep track of instruments playing point.sonification = point.sonification || {}; point.sonification.instrumentsPlaying = point.sonification.instrumentsPlaying || {}; // Register signal handler for the point var signalHandler = point.sonification.signalHandler = point.sonification.signalHandler || new utilities.SignalHandler(['onEnd']); signalHandler.clearSignalCallbacks(); signalHandler.registerSignalCallbacks({ onEnd: options.onEnd }); // If we have a null point or invisible point, just return if (point.isNull || !point.visible || !point.series.visible) { signalHandler.emitSignal('onEnd'); return; } // Go through instruments and play them options.instruments.forEach(function (instrumentDefinition) { var instrument = typeof instrumentDefinition.instrument === 'string' ? H.sonification.instruments[instrumentDefinition.instrument] : instrumentDefinition.instrument, mapping = instrumentDefinition.instrumentMapping || {}, extremes = H.merge( defaultInstrumentOptions, instrumentDefinition.instrumentOptions ), id = instrument.id, onEnd = function (cancelled) { // Instrument on end if (instrumentDefinition.onEnd) { instrumentDefinition.onEnd.apply(this, arguments); } // Remove currently playing point reference on chart if ( chart.sonification && chart.sonification.currentlyPlayingPoint ) { delete chart.sonification.currentlyPlayingPoint; } // Remove reference from instruments playing if ( point.sonification && point.sonification.instrumentsPlaying ) { delete point.sonification.instrumentsPlaying[id]; // This was the last instrument? if ( !Object.keys( point.sonification.instrumentsPlaying ).length ) { signalHandler.emitSignal('onEnd', cancelled); } } }; // Play the note on the instrument if (instrument && instrument.play) { point.sonification.instrumentsPlaying[instrument.id] = instrument; instrument.play({ frequency: getMappingValue( mapping.frequency, true, { min: extremes.minFrequency, max: extremes.maxFrequency } ), duration: getMappingValue( mapping.duration, false, { min: extremes.minDuration, max: extremes.maxDuration } ), pan: getMappingValue( mapping.pan, true, { min: extremes.minPan, max: extremes.maxPan } ), volume: getMappingValue( mapping.volume, true, { min: extremes.minVolume, max: extremes.maxVolume } ), onEnd: onEnd, minFrequency: extremes.minFrequency, maxFrequency: extremes.maxFrequency }); } else { H.error(30); } }); } /** * Cancel sonification of a point. Calls onEnd functions. * * @requires module:modules/sonification * * @function Highcharts.Point#cancelSonify * * @param {boolean} [fadeOut=false] * Whether or not to fade out as we stop. If false, the points are * cancelled synchronously. */ function pointCancelSonify(fadeOut) { var playing = this.sonification && this.sonification.instrumentsPlaying, instrIds = playing && Object.keys(playing); if (instrIds && instrIds.length) { instrIds.forEach(function (instr) { playing[instr].stop(!fadeOut, null, 'cancelled'); }); this.sonification.instrumentsPlaying = {}; this.sonification.signalHandler.emitSignal('onEnd', 'cancelled'); } } var pointSonifyFunctions = { pointSonify: pointSonify, pointCancelSonify: pointCancelSonify }; return pointSonifyFunctions; }(Highcharts, utilities)); var chartSonifyFunctions = (function (H, utilities) { /* * * * (c) 2009-2019 Øystein Moseng * * Sonification functions for chart/series. * * License: www.highcharts.com/license * * */ /** * 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 {object|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} */ /** * 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) : H.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 {object} 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 {object} [dataExtremes] - Predefined extremes for each data prop. * @return {object} 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; }, H.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 H.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 {object} 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, options.dataExtremes ), // 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 H.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 H.Point && options.onPointEnd) { options.onPointEnd(eventData.event, eventObject); } }, onEnd: function () { if (options.onEnd) { options.onEnd(series); } } }); } /** * 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. */ 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(); } // 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 {object} dataExtremes - Pre-calculated data extremes for the chart. * @param {object} chartSonifyOptions - Options passed in to chart.sonify. * @return {object} Options for buildTimelinePathFromSeries. */ function buildSeriesOptions(series, dataExtremes, chartSonifyOptions) { var seriesOptions = chartSonifyOptions.seriesOptions || {}; return H.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 H.isArray(seriesOptions) ? ( H.find(seriesOptions, function (optEntry) { return optEntry.id === H.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 = H.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 = H.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 = H.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 + H.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 = H.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; }, []); } /** * Options for sonifying a chart. * * @requires module:modules/sonification * * @interface Highcharts.SonifyChartOptionsObject *//** * 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.SonifyChartOptionsObject#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.SonifyChartOptionsObject#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.SonifyChartOptionsObject#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.SonifyChartOptionsObject#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.SonifyChartOptionsObject#seriesOptions * @type {Object|Array|undefined} *//** * The instrument definitions for the points in this chart. * @name Highcharts.SonifyChartOptionsObject#instruments * @type {Array|undefined} *//** * Earcons to add to the chart. Note that earcons can also be added per series * using `seriesOptions`. * @name Highcharts.SonifyChartOptionsObject#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.SonifyChartOptionsObject#dataExtremes * @type {object|undefined} *//** * Callback before a series is played. * @name Highcharts.SonifyChartOptionsObject#onSeriesStart * @type {Function|undefined} *//** * Callback after a series has finished playing. * @name Highcharts.SonifyChartOptionsObject#onSeriesEnd * @type {Function|undefined} *//** * Callback after the chart has played. * @name Highcharts.SonifyChartOptionsObject#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.SonifyChartOptionsObject} options * The options for sonifying this chart. */ function chartSonify(options) { // Only one timeline can play at a time. if (this.sonification.timeline) { this.sonification.timeline.pause(); } // Calculate data extremes for the props used var dataExtremes = getExtremesForInstrumentProps( this, options.instruments, options.dataExtremes ); // Figure out ordering of series and custom paths var order = buildPathOrder(options.order, this, function (series) { return buildSeriesOptions(series, dataExtremes, options); }); // Add waits after simultaneous paths with series in them. order = addAfterSeriesWaits(order, options.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, options.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: options.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 H.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) { H.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. */ function pause(fadeOut) { if (this.sonification.timeline) { this.sonification.timeline.pause(H.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. */ 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. */ 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. */ 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 */ 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 */ 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; }(Highcharts, utilities)); var timelineClasses = (function (H, utilities) { /* * * * (c) 2009-2019 Øystein Moseng * * TimelineEvent class definition. * * License: www.highcharts.com/license * * */ /** * A set of options for the TimelineEvent class. * * @requires module:modules/sonification * * @private * @interface Highcharts.TimelineEventOptionsObject *//** * The object we want to sonify when playing the TimelineEvent. Can be any * object that implements the `sonify` and `cancelSonify` functions. If this is * not supplied, the TimelineEvent is considered a silent event, and the onEnd * event is immediately called. * @name Highcharts.TimelineEventOptionsObject#eventObject * @type {*} *//** * Options to pass on to the eventObject when playing it. * @name Highcharts.TimelineEventOptionsObject#playOptions * @type {object|undefined} *//** * The time at which we want this event to play (in milliseconds offset). This * is not used for the TimelineEvent.play function, but rather intended as a * property to decide when to call TimelineEvent.play. Defaults to 0. * @name Highcharts.TimelineEventOptionsObject#time * @type {number|undefined} *//** * Unique ID for the event. Generated automatically if not supplied. * @name Highcharts.TimelineEventOptionsObject#id * @type {string|undefined} *//** * Callback called when the play has finished. * @name Highcharts.TimelineEventOptionsObject#onEnd * @type {Function|undefined} */ /** * The TimelineEvent class. Represents a sound event on a timeline. * * @requires module:modules/sonification * * @private * @class * @name Highcharts.TimelineEvent * * @param {Highcharts.TimelineEventOptionsObject} options * Options for the TimelineEvent. */ function TimelineEvent(options) { this.init(options || {}); } TimelineEvent.prototype.init = function (options) { this.options = options; this.time = options.time || 0; this.id = this.options.id = options.id || H.uniqueKey(); }; /** * Play the event. Does not take the TimelineEvent.time option into account, * and plays the event immediately. * * @function Highcharts.TimelineEvent#play * * @param {Highcharts.TimelineEventOptionsObject} [options] * Options to pass in to the eventObject when playing it. */ TimelineEvent.prototype.play = function (options) { var eventObject = this.options.eventObject, masterOnEnd = this.options.onEnd, playOnEnd = options && options.onEnd, playOptionsOnEnd = this.options.playOptions && this.options.playOptions.onEnd, playOptions = H.merge(this.options.playOptions, options); if (eventObject && eventObject.sonify) { // If we have multiple onEnds defined, use all playOptions.onEnd = masterOnEnd || playOnEnd || playOptionsOnEnd ? function () { var args = arguments; [masterOnEnd, playOnEnd, playOptionsOnEnd].forEach( function (onEnd) { if (onEnd) { onEnd.apply(this, args); } } ); } : undefined; eventObject.sonify(playOptions); } else { if (playOnEnd) { playOnEnd(); } if (masterOnEnd) { masterOnEnd(); } } }; /** * Cancel the sonification of this event. Does nothing if the event is not * currently sonifying. * * @function Highcharts.TimelineEvent#cancel * * @param {boolean} [fadeOut=false] * Whether or not to fade out as we stop. If false, the event is * cancelled synchronously. */ TimelineEvent.prototype.cancel = function (fadeOut) { this.options.eventObject.cancelSonify(fadeOut); }; /** * A set of options for the TimelinePath class. * * @requires module:modules/ * * @private * @interface Highcharts.TimelinePathOptionsObject *//** * List of TimelineEvents to play on this track. * @name Highcharts.TimelinePathOptionsObject#events * @type {Array} *//** * If this option is supplied, this path ignores all events and just waits for * the specified number of milliseconds before calling onEnd. * @name Highcharts.TimelinePathOptionsObject#silentWait * @type {number|undefined} *//** * Unique ID for this timeline path. Automatically generated if not supplied. * @name Highcharts.TimelinePathOptionsObject#id * @type {string|undefined} *//** * Callback called before the path starts playing. * @name Highcharts.TimelinePathOptionsObject#onStart * @type {Function|undefined} *//** * Callback function to call before an event plays. * @name Highcharts.TimelinePathOptionsObject#onEventStart * @type {Function|undefined} *//** * Callback function to call after an event has stopped playing. * @name Highcharts.TimelinePathOptionsObject#onEventEnd * @type {Function|undefined} *//** * Callback called when the whole path is finished. * @name Highcharts.TimelinePathOptionsObject#onEnd * @type {Function|undefined} */ /** * The TimelinePath class. Represents a track on a timeline with a list of * sound events to play at certain times relative to each other. * * @requires module:modules/sonification * * @private * @class * @name Highcharts.TimelinePath * * @param {Highcharts.TimelinePathOptionsObject} options * Options for the TimelinePath. */ function TimelinePath(options) { this.init(options); } TimelinePath.prototype.init = function (options) { this.options = options; this.id = this.options.id = options.id || H.uniqueKey(); this.cursor = 0; this.eventsPlaying = {}; // Handle silent wait, otherwise use events from options this.events = options.silentWait ? [ new TimelineEvent({ time: 0 }), new TimelineEvent({ time: options.silentWait }) ] : this.options.events; // We need to sort our events by time this.sortEvents(); // Get map from event ID to index this.updateEventIdMap(); // Signal events to fire this.signalHandler = new utilities.SignalHandler( ['playOnEnd', 'masterOnEnd', 'onStart', 'onEventStart', 'onEventEnd'] ); this.signalHandler.registerSignalCallbacks( H.merge(options, { masterOnEnd: options.onEnd }) ); }; /** * Sort the internal event list by time. * @private */ TimelinePath.prototype.sortEvents = function () { this.events = this.events.sort(function (a, b) { return a.time - b.time; }); }; /** * Update the internal eventId to index map. * @private */ TimelinePath.prototype.updateEventIdMap = function () { this.eventIdMap = this.events.reduce(function (acc, cur, i) { acc[cur.id] = i; return acc; }, {}); }; /** * Add events to the path. Should not be done while the path is playing. * The new events are inserted according to their time property. * @private * @param {Array} newEvents - The new timeline events * to add. */ TimelinePath.prototype.addTimelineEvents = function (newEvents) { this.events = this.events.concat(newEvents); this.sortEvents(); // Sort events by time this.updateEventIdMap(); // Update the event ID to index map }; /** * Get the current TimelineEvent under the cursor. * @private * @return {Highcharts.TimelineEvent} The current timeline event. */ TimelinePath.prototype.getCursor = function () { return this.events[this.cursor]; }; /** * Set the current TimelineEvent under the cursor. * @private * @param {string} eventId - The ID of the timeline event to set as current. * @return {boolean} True if there is an event with this ID in the path. False * otherwise. */ TimelinePath.prototype.setCursor = function (eventId) { var ix = this.eventIdMap[eventId]; if (ix !== undefined) { this.cursor = ix; return true; } return false; }; /** * Play the timeline from the current cursor. * @private * @param {Function} onEnd - Callback to call when play finished. Does not * override other onEnd callbacks. */ TimelinePath.prototype.play = function (onEnd) { this.pause(); this.signalHandler.emitSignal('onStart'); this.signalHandler.clearSignalCallbacks(['playOnEnd']); this.signalHandler.registerSignalCallbacks({ playOnEnd: onEnd }); this.playEvents(1); }; /** * Play the timeline backwards from the current cursor. * @private * @param {Function} onEnd - Callback to call when play finished. Does not * override other onEnd callbacks. */ TimelinePath.prototype.rewind = function (onEnd) { this.pause(); this.signalHandler.emitSignal('onStart'); this.signalHandler.clearSignalCallbacks(['playOnEnd']); this.signalHandler.registerSignalCallbacks({ playOnEnd: onEnd }); this.playEvents(-1); }; /** * Reset the cursor to the beginning. * @private */ TimelinePath.prototype.resetCursor = function () { this.cursor = 0; }; /** * Reset the cursor to the end. * @private */ TimelinePath.prototype.resetCursorEnd = function () { this.cursor = this.events.length - 1; }; /** * Cancel current playing. Leaves the cursor intact. * @private * @param {boolean} [fadeOut=false] - Whether or not to fade out as we stop. If * false, the path is cancelled synchronously. */ TimelinePath.prototype.pause = function (fadeOut) { var timelinePath = this; // Cancel next scheduled play clearTimeout(timelinePath.nextScheduledPlay); // Cancel currently playing events Object.keys(timelinePath.eventsPlaying).forEach(function (id) { if (timelinePath.eventsPlaying[id]) { timelinePath.eventsPlaying[id].cancel(fadeOut); } }); timelinePath.eventsPlaying = {}; }; /** * Play the events, starting from current cursor, and going in specified * direction. * @private * @param {number} direction - The direction to play, 1 for forwards and -1 for * backwards. */ TimelinePath.prototype.playEvents = function (direction) { var timelinePath = this, curEvent = timelinePath.events[this.cursor], nextEvent = timelinePath.events[this.cursor + direction], timeDiff, onEnd = function (signalData) { timelinePath.signalHandler.emitSignal( 'masterOnEnd', signalData ); timelinePath.signalHandler.emitSignal( 'playOnEnd', signalData ); }; // Store reference to path on event curEvent.timelinePath = timelinePath; // Emit event, cancel if returns false if ( timelinePath.signalHandler.emitSignal( 'onEventStart', curEvent ) === false ) { onEnd({ event: curEvent, cancelled: true }); return; } // Play the current event timelinePath.eventsPlaying[curEvent.id] = curEvent; curEvent.play({ onEnd: function (cancelled) { var signalData = { event: curEvent, cancelled: !!cancelled }; // Keep track of currently playing events for cancelling delete timelinePath.eventsPlaying[curEvent.id]; // Handle onEventEnd timelinePath.signalHandler.emitSignal('onEventEnd', signalData); // Reached end of path? if (!nextEvent) { onEnd(signalData); } } }); // Schedule next if (nextEvent) { timeDiff = Math.abs(nextEvent.time - curEvent.time); if (timeDiff < 1) { // Play immediately timelinePath.cursor += direction; timelinePath.playEvents(direction); } else { // Schedule after the difference in ms this.nextScheduledPlay = setTimeout(function () { timelinePath.cursor += direction; timelinePath.playEvents(direction); }, timeDiff); } } }; /* ************************************************************************** * * TIMELINE * * ************************************************************************** */ /** * A set of options for the Timeline class. * * @requires module:modules/sonification * * @private * @interface Highcharts.TimelineOptionsObject *//** * List of TimelinePaths to play. Multiple paths can be grouped together and * played simultaneously by supplying an array of paths in place of a single * path. * @name Highcharts.TimelineOptionsObject#paths * @type {Array>} *//** * Callback function to call before a path plays. * @name Highcharts.TimelineOptionsObject#onPathStart * @type {Function|undefined} *//** * Callback function to call after a path has stopped playing. * @name Highcharts.TimelineOptionsObject#onPathEnd * @type {Function|undefined} *//** * Callback called when the whole path is finished. * @name Highcharts.TimelineOptionsObject#onEnd * @type {Function|undefined} */ /** * The Timeline class. Represents a sonification timeline with a list of * timeline paths with events to play at certain times relative to each other. * * @requires module:modules/sonification * * @private * @class * @name Highcharts.Timeline * * @param {Highcharts.TimelineOptionsObject} options * Options for the Timeline. */ function Timeline(options) { this.init(options || {}); } Timeline.prototype.init = function (options) { this.options = options; this.cursor = 0; this.paths = options.paths; this.pathsPlaying = {}; this.signalHandler = new utilities.SignalHandler( ['playOnEnd', 'masterOnEnd', 'onPathStart', 'onPathEnd'] ); this.signalHandler.registerSignalCallbacks( H.merge(options, { masterOnEnd: options.onEnd }) ); }; /** * Play the timeline forwards from cursor. * @private * @param {Function} onEnd - Callback to call when play finished. Does not * override other onEnd callbacks. */ Timeline.prototype.play = function (onEnd) { this.pause(); this.signalHandler.clearSignalCallbacks(['playOnEnd']); this.signalHandler.registerSignalCallbacks({ playOnEnd: onEnd }); this.playPaths(1); }; /** * Play the timeline backwards from cursor. * @private * @param {Function} onEnd - Callback to call when play finished. Does not * override other onEnd callbacks. */ Timeline.prototype.rewind = function (onEnd) { this.pause(); this.signalHandler.clearSignalCallbacks(['playOnEnd']); this.signalHandler.registerSignalCallbacks({ playOnEnd: onEnd }); this.playPaths(-1); }; /** * Play the timeline in the specified direction. * @private * @param {number} direction - Direction to play in. 1 for forwards, -1 for * backwards. */ Timeline.prototype.playPaths = function (direction) { var curPaths = H.splat(this.paths[this.cursor]), nextPaths = this.paths[this.cursor + direction], timeline = this, signalHandler = this.signalHandler, pathsEnded = 0, // Play a path playPath = function (path) { // Emit signal and set playing state signalHandler.emitSignal('onPathStart', path); timeline.pathsPlaying[path.id] = path; // Do the play path[direction > 0 ? 'play' : 'rewind'](function (callbackData) { // Play ended callback // Data to pass to signal callbacks var cancelled = callbackData && callbackData.cancelled, signalData = { path: path, cancelled: cancelled }; // Clear state and send signal delete timeline.pathsPlaying[path.id]; signalHandler.emitSignal('onPathEnd', signalData); // Handle next paths pathsEnded++; if (pathsEnded >= curPaths.length) { // We finished all of the current paths for cursor. if (nextPaths && !cancelled) { // We have more paths, move cursor along timeline.cursor += direction; // Reset upcoming path cursors before playing H.splat(nextPaths).forEach(function (nextPath) { nextPath[ direction > 0 ? 'resetCursor' : 'resetCursorEnd' ](); }); // Play next timeline.playPaths(direction); } else { // If it is the last path in this direction, call onEnd signalHandler.emitSignal('playOnEnd', signalData); signalHandler.emitSignal('masterOnEnd', signalData); } } }); }; // Go through the paths under cursor and play them curPaths.forEach(function (path) { if (path) { // Store reference to timeline path.timeline = timeline; // Leave a timeout to let notes fade out before next play setTimeout(function () { playPath(path); }, H.sonification.fadeOutTime); } }); }; /** * Stop the playing of the timeline. Cancels all current sounds, but does not * affect the cursor. * @private * @param {boolean} [fadeOut=false] - Whether or not to fade out as we stop. If * false, the timeline is cancelled synchronously. */ Timeline.prototype.pause = function (fadeOut) { var timeline = this; // Cancel currently playing events Object.keys(timeline.pathsPlaying).forEach(function (id) { if (timeline.pathsPlaying[id]) { timeline.pathsPlaying[id].pause(fadeOut); } }); timeline.pathsPlaying = {}; }; /** * Reset the cursor to the beginning of the timeline. * @private */ Timeline.prototype.resetCursor = function () { this.paths.forEach(function (paths) { H.splat(paths).forEach(function (path) { path.resetCursor(); }); }); this.cursor = 0; }; /** * Reset the cursor to the end of the timeline. * @private */ Timeline.prototype.resetCursorEnd = function () { this.paths.forEach(function (paths) { H.splat(paths).forEach(function (path) { path.resetCursorEnd(); }); }); this.cursor = this.paths.length - 1; }; /** * Set the current TimelineEvent under the cursor. If multiple paths are being * played at the same time, this function only affects a single path (the one * that contains the eventId that is passed in). * @private * @param {string} eventId - The ID of the timeline event to set as current. * @return {boolean} True if the cursor was set, false if no TimelineEvent was * found for this ID. */ Timeline.prototype.setCursor = function (eventId) { return this.paths.some(function (paths) { return H.splat(paths).some(function (path) { return path.setCursor(eventId); }); }); }; /** * Get the current TimelineEvents under the cursors. This function will return * the event under the cursor for each currently playing path, as an object * where the path ID is mapped to the TimelineEvent under that path's cursor. * @private * @return {object} The TimelineEvents under each path's cursors. */ Timeline.prototype.getCursor = function () { return this.getCurrentPlayingPaths().reduce(function (acc, cur) { acc[cur.id] = cur.getCursor(); return acc; }, {}); }; /** * Check if timeline is reset or at start. * @private * @return {boolean} True if timeline is at the beginning. */ Timeline.prototype.atStart = function () { return !this.getCurrentPlayingPaths().some(function (path) { return path.cursor; }); }; /** * Get the current TimelinePaths being played. * @private * @return {Array} The TimelinePaths currently being * played. */ Timeline.prototype.getCurrentPlayingPaths = function () { return H.splat(this.paths[this.cursor]); }; // Export the classes var timelineClasses = { TimelineEvent: TimelineEvent, TimelinePath: TimelinePath, Timeline: Timeline }; return timelineClasses; }(Highcharts, utilities)); (function (H, Instrument, instruments, Earcon, pointSonifyFunctions, chartSonifyFunctions, utilities, TimelineClasses) { /* * * * (c) 2009-2019 Øystein Moseng * * Sonification module for Highcharts * * License: www.highcharts.com/license * * */ // Expose on the Highcharts object /** * Global classes and objects related to sonification. * * @requires module:modules/sonification * * @name Highcharts.sonification * @type {Highcharts.SonificationObject} */ /** * Global classes and objects related to sonification. * * @requires module:modules/sonification * * @interface Highcharts.SonificationObject *//** * Note fade-out-time in milliseconds. Most notes are faded out quickly by * default if there is time. This is to avoid abrupt stops which will cause * perceived clicks. * @name Highcharts.SonificationObject#fadeOutDuration * @type {number} *//** * Utility functions. * @name Highcharts.SonificationObject#utilities * @private * @type {object} *//** * The Instrument class. * @name Highcharts.SonificationObject#Instrument * @type {Function} *//** * Predefined instruments, given as an object with a map between the instrument * name and the Highcharts.Instrument object. * @name Highcharts.SonificationObject#instruments * @type {Object} *//** * The Earcon class. * @name Highcharts.SonificationObject#Earcon * @type {Function} *//** * The TimelineEvent class. * @private * @name Highcharts.SonificationObject#TimelineEvent * @type {Function} *//** * The TimelinePath class. * @private * @name Highcharts.SonificationObject#TimelinePath * @type {Function} *//** * The Timeline class. * @private * @name Highcharts.SonificationObject#Timeline * @type {Function} */ H.sonification = { fadeOutDuration: 20, // Classes and functions utilities: utilities, Instrument: Instrument, instruments: instruments, Earcon: Earcon, TimelineEvent: TimelineClasses.TimelineEvent, TimelinePath: TimelineClasses.TimelinePath, Timeline: TimelineClasses.Timeline }; // Chart specific H.Point.prototype.sonify = pointSonifyFunctions.pointSonify; H.Point.prototype.cancelSonify = pointSonifyFunctions.pointCancelSonify; H.Series.prototype.sonify = chartSonifyFunctions.seriesSonify; H.extend(H.Chart.prototype, { sonify: chartSonifyFunctions.chartSonify, pauseSonify: chartSonifyFunctions.pause, resumeSonify: chartSonifyFunctions.resume, rewindSonify: chartSonifyFunctions.rewind, cancelSonify: chartSonifyFunctions.cancel, getCurrentSonifyPoints: chartSonifyFunctions.getCurrentPoints, setSonifyCursor: chartSonifyFunctions.setCursor, resetSonifyCursor: chartSonifyFunctions.resetCursor, resetSonifyCursorEnd: chartSonifyFunctions.resetCursorEnd, sonification: {} }); }(Highcharts, Instrument, instruments, Earcon, pointSonifyFunctions, chartSonifyFunctions, utilities, timelineClasses)); return (function () { }()); }));