12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341 |
- /**
- * @license Highcharts JS v7.0.2 (2019-01-17)
- * 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<number>|undefined}
- *//**
- * Options specific to oscillator instruments.
- * @name Highcharts.InstrumentOptionsObject#oscillator
- * @type {Highcharts.OscillatorOptionsObject|undefined}
- */
- /**
- * Options for playing an instrument.
- *
- * @requires module:modules/sonification
- *
- * @interface Highcharts.InstrumentPlayOptionsObject
- *//**
- * The frequency of the note to play. Can be a fixed number, or a function. The
- * function receives one argument: the relative time of the note playing (0
- * being the start, and 1 being the end of the note). It should return the
- * frequency number for each point in time. The poll interval of this function
- * is specified by the Instrument.playCallbackInterval option.
- * @name Highcharts.InstrumentPlayOptionsObject#frequency
- * @type {number|Function}
- *//**
- * The duration of the note in milliseconds.
- * @name Highcharts.InstrumentPlayOptionsObject#duration
- * @type {number}
- *//**
- * The minimum frequency to allow. If the instrument has a set of allowed
- * frequencies, the closest frequency is used by default. Use this option to
- * stop too low frequencies from being used.
- * @name Highcharts.InstrumentPlayOptionsObject#minFrequency
- * @type {number|undefined}
- *//**
- * The maximum frequency to allow. If the instrument has a set of allowed
- * frequencies, the closest frequency is used by default. Use this option to
- * stop too high frequencies from being used.
- * @name Highcharts.InstrumentPlayOptionsObject#maxFrequency
- * @type {number|undefined}
- *//**
- * The volume of the instrument. Can be a fixed number between 0 and 1, or a
- * function. The function receives one argument: the relative time of the note
- * playing (0 being the start, and 1 being the end of the note). It should
- * return the volume for each point in time. The poll interval of this function
- * is specified by the Instrument.playCallbackInterval option. Defaults to 1.
- * @name Highcharts.InstrumentPlayOptionsObject#volume
- * @type {number|Function|undefined}
- *//**
- * The panning of the instrument. Can be a fixed number between -1 and 1, or a
- * function. The function receives one argument: the relative time of the note
- * playing (0 being the start, and 1 being the end of the note). It should
- * return the panning value for each point in time. The poll interval of this
- * function is specified by the Instrument.playCallbackInterval option.
- * Defaults to 0.
- * @name Highcharts.InstrumentPlayOptionsObject#pan
- * @type {number|Function|undefined}
- *//**
- * Callback function to be called when the play is completed.
- * @name Highcharts.InstrumentPlayOptionsObject#onEnd
- * @type {Function|undefined}
- */
- /**
- * @requires module:modules/sonification
- *
- * @interface Highcharts.OscillatorOptionsObject
- *//**
- * The waveform shape to use for oscillator instruments. Defaults to `sine`.
- * @name Highcharts.OscillatorOptionsObject#waveformShape
- * @type {string|undefined}
- */
- // 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<string>} supportedSignals
- * List of supported signal names.
- */
- function SignalHandler(supportedSignals) {
- this.init(supportedSignals || []);
- }
- SignalHandler.prototype.init = function (supportedSignals) {
- this.supportedSignals = supportedSignals;
- this.signals = {};
- };
- /**
- * Register a set of signal callbacks with this SignalHandler.
- * Multiple signal callbacks can be registered for the same signal.
- * @private
- * @param {object} signals - An object that contains a mapping from the signal
- * name to the callbacks. Only supported events are considered.
- */
- SignalHandler.prototype.registerSignalCallbacks = function (signals) {
- var signalHandler = this;
- signalHandler.supportedSignals.forEach(function (supportedSignal) {
- if (signals[supportedSignal]) {
- (
- signalHandler.signals[supportedSignal] =
- signalHandler.signals[supportedSignal] || []
- ).push(
- signals[supportedSignal]
- );
- }
- });
- };
- /**
- * Clear signal callbacks, optionally by name.
- * @private
- * @param {Array<string>} [signalNames] - A list of signal names to clear. If
- * not supplied, all signal callbacks are removed.
- */
- SignalHandler.prototype.clearSignalCallbacks = function (signalNames) {
- var signalHandler = this;
- if (signalNames) {
- signalNames.forEach(function (signalName) {
- if (signalHandler.signals[signalName]) {
- delete signalHandler.signals[signalName];
- }
- });
- } else {
- signalHandler.signals = {};
- }
- };
- /**
- * Emit a signal. Does nothing if the signal does not exist, or has no
- * registered callbacks.
- * @private
- * @param {string} signalNames - Name of signal to emit.
- * @param {*} data - Data to pass to the callback.
- */
- SignalHandler.prototype.emitSignal = function (signalName, data) {
- var retval;
- if (this.signals[signalName]) {
- this.signals[signalName].forEach(function (handler) {
- var result = handler(data);
- retval = result !== undefined ? result : retval;
- });
- }
- return retval;
- };
- var utilities = {
- // List of musical frequencies from C0 to C8
- musicalFrequencies: musicalFrequencies,
- // SignalHandler class
- SignalHandler: SignalHandler,
- /**
- * Get a musical scale by specifying the semitones from 1-12 to include.
- * 1: C, 2: C#, 3: D, 4: D#, 5: E, 6: F,
- * 7: F#, 8: G, 9: G#, 10: A, 11: Bb, 12: B
- * @private
- * @param {Array<number>} semitones - Array of semitones from 1-12 to
- * include in the scale. Duplicate entries are ignored.
- * @return {Array<number>} Array of frequencies from C0 to C8 that are
- * included in this scale.
- */
- getMusicalScale: function (semitones) {
- return musicalFrequencies.filter(function (freq, i) {
- var interval = i % 12 + 1;
- return semitones.some(function (allowedInterval) {
- return allowedInterval === interval;
- });
- });
- },
- /**
- * Calculate the extreme values in a chart for a data prop.
- * @private
- * @param {Highcharts.Chart} chart - The chart
- * @param {string} prop - The data prop to find extremes for
- * @return {object} Object with min and max properties
- */
- calculateDataExtremes: function (chart, prop) {
- return chart.series.reduce(function (extremes, series) {
- // We use cropped points rather than series.data here, to allow
- // users to zoom in for better fidelity.
- series.points.forEach(function (point) {
- var val = point[prop] !== undefined ?
- point[prop] : point.options[prop];
- extremes.min = Math.min(extremes.min, val);
- extremes.max = Math.max(extremes.max, val);
- });
- return extremes;
- }, {
- min: Infinity,
- max: -Infinity
- });
- },
- /**
- * Translate a value on a virtual axis. Creates a new, virtual, axis with a
- * min and max, and maps the relative value onto this axis.
- * @private
- * @param {number} value - The relative data value to translate.
- * @param {object} dataExtremes - The possible extremes for this value.
- * @param {object} limits - Limits for the virtual axis.
- * @return {number} The value mapped to the virtual axis.
- */
- virtualAxisTranslate: function (value, dataExtremes, limits) {
- var lenValueAxis = dataExtremes.max - dataExtremes.min,
- lenVirtualAxis = limits.max - limits.min,
- virtualAxisValue = limits.min +
- lenVirtualAxis * (value - dataExtremes.min) / lenValueAxis;
- return lenValueAxis > 0 ?
- Math.max(Math.min(virtualAxisValue, limits.max), limits.min) :
- limits.min;
- }
- };
- 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<Highcharts.EarconInstrument>}
- *//**
- * The unique ID of the Earcon. Generated if not supplied.
- * @name Highcharts.EarconOptionsObject#id
- * @type {string|undefined}
- *//**
- * Global panning of all instruments. Overrides all panning on individual
- * instruments. Can be a number between -1 and 1.
- * @name Highcharts.EarconOptionsObject#pan
- * @type {number|undefined}
- *//**
- * Master volume for all instruments. Volume settings on individual instruments
- * can still be used for relative volume between the instruments. This setting
- * does not affect volumes set by functions in individual instruments. Can be a
- * number between 0 and 1. Defaults to 1.
- * @name Highcharts.EarconOptionsObject#volume
- * @type {number|undefined}
- *//**
- * Callback function to call when earcon has finished playing.
- * @name Highcharts.EarconOptionsObject#onEnd
- * @type {Function|undefined}
- */
- /**
- * The Earcon class. Earcon objects represent a certain sound consisting of
- * one or more instruments playing a predefined sound.
- *
- * @sample highcharts/sonification/earcon/
- * Using earcons directly
- *
- * @requires module:modules/sonification
- *
- * @class
- * @name Highcharts.Earcon
- *
- * @param {Highcharts.EarconOptionsObject} options
- * Options for the Earcon instance.
- */
- function Earcon(options) {
- this.init(options || {});
- }
- Earcon.prototype.init = function (options) {
- this.options = options;
- if (!this.options.id) {
- this.options.id = this.id = H.uniqueKey();
- }
- this.instrumentsPlaying = {};
- };
- /**
- * Play the earcon, optionally overriding init options.
- *
- * @sample highcharts/sonification/earcon/
- * Using earcons directly
- *
- * @function Highcharts.Earcon#sonify
- *
- * @param {Highcharts.EarconOptionsObject} options
- * Override existing options.
- */
- Earcon.prototype.sonify = function (options) {
- var playOptions = H.merge(this.options, options);
- // Find master volume/pan settings
- var masterVolume = H.pick(playOptions.volume, 1),
- masterPan = playOptions.pan,
- earcon = this,
- playOnEnd = options && options.onEnd,
- masterOnEnd = earcon.options.onEnd;
- // Go through the instruments and play them
- playOptions.instruments.forEach(function (opts) {
- var instrument = typeof opts.instrument === 'string' ?
- H.sonification.instruments[opts.instrument] : opts.instrument,
- instrumentOpts = H.merge(opts.playOptions),
- instrOnEnd,
- instrumentCopy,
- copyId;
- if (instrument && instrument.play) {
- if (opts.playOptions) {
- // Handle master pan/volume
- if (typeof opts.playOptions.volume !== 'function') {
- instrumentOpts.volume = H.pick(masterVolume, 1) *
- H.pick(opts.playOptions.volume, 1);
- }
- instrumentOpts.pan = H.pick(masterPan, instrumentOpts.pan);
- // Handle onEnd
- instrOnEnd = instrumentOpts.onEnd;
- instrumentOpts.onEnd = function () {
- delete earcon.instrumentsPlaying[copyId];
- if (instrOnEnd) {
- instrOnEnd.apply(this, arguments);
- }
- if (!Object.keys(earcon.instrumentsPlaying).length) {
- if (playOnEnd) {
- playOnEnd.apply(this, arguments);
- }
- if (masterOnEnd) {
- masterOnEnd.apply(this, arguments);
- }
- }
- };
- // Play the instrument. Use a copy so we can play multiple at
- // the same time.
- instrumentCopy = instrument.copy();
- copyId = instrumentCopy.id;
- earcon.instrumentsPlaying[copyId] = instrumentCopy;
- instrumentCopy.play(instrumentOpts);
- }
- } else {
- H.error(30);
- }
- });
- };
- /**
- * Cancel any current sonification of the Earcon. Calls onEnd functions.
- *
- * @function Highcharts.Earcon#cancelSonify
- *
- * @param {boolean} [fadeOut=false]
- * Whether or not to fade out as we stop. If false, the earcon is
- * cancelled synchronously.
- */
- Earcon.prototype.cancelSonify = function (fadeOut) {
- var playing = this.instrumentsPlaying,
- instrIds = playing && Object.keys(playing);
- if (instrIds && instrIds.length) {
- instrIds.forEach(function (instr) {
- playing[instr].stop(!fadeOut, null, 'cancelled');
- });
- this.instrumentsPlaying = {};
- }
- };
- 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<Highcharts.PointInstrumentObject>}
- *//**
- * Optionally provide the minimum/maximum values for the points. If this is not
- * supplied, it is calculated from the points in the chart on demand. This
- * option is supplied in the following format, as a map of point data properties
- * to objects with min/max values:
- * ```js
- * dataExtremes: {
- * y: {
- * min: 0,
- * max: 100
- * },
- * z: {
- * min: -10,
- * max: 10
- * }
- * // Properties used and not provided are calculated on demand
- * }
- * ```
- * @name Highcharts.PointSonifyOptionsObject#dataExtremes
- * @type {object|undefined}
- *//**
- * Callback called when the sonification has finished.
- * @name Highcharts.PointSonifyOptionsObject#onEnd
- * @type {Function|undefined}
- */
- // 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<Highcharts.PointInstrumentObject>}
- *//**
- * Earcons to add to the series.
- * @name Highcharts.SonifySeriesOptionsObject#earcons
- * @type {Array<Highcharts.EarconConfiguration>|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<Highcharts.PointInstrumentObject>} 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<Highcharts.EarconConfiguration>} earconDefinitions - Earcons to
- * check.
- * @return {Array<Highcharts.Earcon>} 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<Highcharts.PointInstrumentObject>} instruments - The instrument
- * options.
- * @return {Array<Highcharts.PointInstrumentObject>} 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<string|Highcharts.Earcon|Array<string|Highcharts.Earcon>>} 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<object|Array<object|Highcharts.TimelinePath>>} 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<object|Array<object|TimelinePath>>} order - The order of items.
- * @param {number} wait - The wait in milliseconds to add.
- * @return {Array<object|Array<object|TimelinePath>>} 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<object|Array<object|TimelinePath>>} 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<Highcharts.TimelinePath>} 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<object|Array<object|Highcharts.TimelinePath>>} 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<object|Array<object|Highcharts.TimelinePath>>} order - The
- * order list.
- * @param {number} duration - Total duration to aim for in milliseconds.
- * @return {Array<Array<Highcharts.TimelinePath>>} 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<string|Highcharts.Earcon|Array<string|Highcharts.Earcon>>}
- *//**
- * 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<object>|undefined}
- *//**
- * The instrument definitions for the points in this chart.
- * @name Highcharts.SonifyChartOptionsObject#instruments
- * @type {Array<Highcharts.PointInstrumentObject>|undefined}
- *//**
- * Earcons to add to the chart. Note that earcons can also be added per series
- * using `seriesOptions`.
- * @name Highcharts.SonifyChartOptionsObject#earcons
- * @type {Array<Highcharts.EarconConfiguration>|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<Highcharts.Point>}
- * 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<Highcharts.Point>} 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<Highcharts.TimelineEvent>}
- *//**
- * If this option is supplied, this path ignores all events and just waits for
- * the specified number of milliseconds before calling onEnd.
- * @name Highcharts.TimelinePathOptionsObject#silentWait
- * @type {number|undefined}
- *//**
- * Unique ID for this timeline path. Automatically generated if not supplied.
- * @name Highcharts.TimelinePathOptionsObject#id
- * @type {string|undefined}
- *//**
- * Callback called before the path starts playing.
- * @name Highcharts.TimelinePathOptionsObject#onStart
- * @type {Function|undefined}
- *//**
- * Callback function to call before an event plays.
- * @name Highcharts.TimelinePathOptionsObject#onEventStart
- * @type {Function|undefined}
- *//**
- * Callback function to call after an event has stopped playing.
- * @name Highcharts.TimelinePathOptionsObject#onEventEnd
- * @type {Function|undefined}
- *//**
- * Callback called when the whole path is finished.
- * @name Highcharts.TimelinePathOptionsObject#onEnd
- * @type {Function|undefined}
- */
- /**
- * The TimelinePath class. Represents a track on a timeline with a list of
- * sound events to play at certain times relative to each other.
- *
- * @requires module:modules/sonification
- *
- * @private
- * @class
- * @name Highcharts.TimelinePath
- *
- * @param {Highcharts.TimelinePathOptionsObject} options
- * Options for the TimelinePath.
- */
- function TimelinePath(options) {
- this.init(options);
- }
- TimelinePath.prototype.init = function (options) {
- this.options = options;
- this.id = this.options.id = options.id || H.uniqueKey();
- this.cursor = 0;
- this.eventsPlaying = {};
- // Handle silent wait, otherwise use events from options
- this.events = options.silentWait ?
- [
- new TimelineEvent({ time: 0 }),
- new TimelineEvent({ time: options.silentWait })
- ] :
- this.options.events;
- // We need to sort our events by time
- this.sortEvents();
- // Get map from event ID to index
- this.updateEventIdMap();
- // Signal events to fire
- this.signalHandler = new utilities.SignalHandler(
- ['playOnEnd', 'masterOnEnd', 'onStart', 'onEventStart', 'onEventEnd']
- );
- this.signalHandler.registerSignalCallbacks(
- H.merge(options, { masterOnEnd: options.onEnd })
- );
- };
- /**
- * Sort the internal event list by time.
- * @private
- */
- TimelinePath.prototype.sortEvents = function () {
- this.events = this.events.sort(function (a, b) {
- return a.time - b.time;
- });
- };
- /**
- * Update the internal eventId to index map.
- * @private
- */
- TimelinePath.prototype.updateEventIdMap = function () {
- this.eventIdMap = this.events.reduce(function (acc, cur, i) {
- acc[cur.id] = i;
- return acc;
- }, {});
- };
- /**
- * Add events to the path. Should not be done while the path is playing.
- * The new events are inserted according to their time property.
- * @private
- * @param {Array<Highcharts.TimelineEvent>} newEvents - The new timeline events
- * to add.
- */
- TimelinePath.prototype.addTimelineEvents = function (newEvents) {
- this.events = this.events.concat(newEvents);
- this.sortEvents(); // Sort events by time
- this.updateEventIdMap(); // Update the event ID to index map
- };
- /**
- * Get the current TimelineEvent under the cursor.
- * @private
- * @return {Highcharts.TimelineEvent} The current timeline event.
- */
- TimelinePath.prototype.getCursor = function () {
- return this.events[this.cursor];
- };
- /**
- * Set the current TimelineEvent under the cursor.
- * @private
- * @param {string} eventId - The ID of the timeline event to set as current.
- * @return {boolean} True if there is an event with this ID in the path. False
- * otherwise.
- */
- TimelinePath.prototype.setCursor = function (eventId) {
- var ix = this.eventIdMap[eventId];
- if (ix !== undefined) {
- this.cursor = ix;
- return true;
- }
- return false;
- };
- /**
- * Play the timeline from the current cursor.
- * @private
- * @param {Function} onEnd - Callback to call when play finished. Does not
- * override other onEnd callbacks.
- */
- TimelinePath.prototype.play = function (onEnd) {
- this.pause();
- this.signalHandler.emitSignal('onStart');
- this.signalHandler.clearSignalCallbacks(['playOnEnd']);
- this.signalHandler.registerSignalCallbacks({ playOnEnd: onEnd });
- this.playEvents(1);
- };
- /**
- * Play the timeline backwards from the current cursor.
- * @private
- * @param {Function} onEnd - Callback to call when play finished. Does not
- * override other onEnd callbacks.
- */
- TimelinePath.prototype.rewind = function (onEnd) {
- this.pause();
- this.signalHandler.emitSignal('onStart');
- this.signalHandler.clearSignalCallbacks(['playOnEnd']);
- this.signalHandler.registerSignalCallbacks({ playOnEnd: onEnd });
- this.playEvents(-1);
- };
- /**
- * Reset the cursor to the beginning.
- * @private
- */
- TimelinePath.prototype.resetCursor = function () {
- this.cursor = 0;
- };
- /**
- * Reset the cursor to the end.
- * @private
- */
- TimelinePath.prototype.resetCursorEnd = function () {
- this.cursor = this.events.length - 1;
- };
- /**
- * Cancel current playing. Leaves the cursor intact.
- * @private
- * @param {boolean} [fadeOut=false] - Whether or not to fade out as we stop. If
- * false, the path is cancelled synchronously.
- */
- TimelinePath.prototype.pause = function (fadeOut) {
- var timelinePath = this;
- // Cancel next scheduled play
- clearTimeout(timelinePath.nextScheduledPlay);
- // Cancel currently playing events
- Object.keys(timelinePath.eventsPlaying).forEach(function (id) {
- if (timelinePath.eventsPlaying[id]) {
- timelinePath.eventsPlaying[id].cancel(fadeOut);
- }
- });
- timelinePath.eventsPlaying = {};
- };
- /**
- * Play the events, starting from current cursor, and going in specified
- * direction.
- * @private
- * @param {number} direction - The direction to play, 1 for forwards and -1 for
- * backwards.
- */
- TimelinePath.prototype.playEvents = function (direction) {
- var timelinePath = this,
- curEvent = timelinePath.events[this.cursor],
- nextEvent = timelinePath.events[this.cursor + direction],
- timeDiff,
- onEnd = function (signalData) {
- timelinePath.signalHandler.emitSignal(
- 'masterOnEnd', signalData
- );
- timelinePath.signalHandler.emitSignal(
- 'playOnEnd', signalData
- );
- };
- // Store reference to path on event
- curEvent.timelinePath = timelinePath;
- // Emit event, cancel if returns false
- if (
- timelinePath.signalHandler.emitSignal(
- 'onEventStart', curEvent
- ) === false
- ) {
- onEnd({
- event: curEvent,
- cancelled: true
- });
- return;
- }
- // Play the current event
- timelinePath.eventsPlaying[curEvent.id] = curEvent;
- curEvent.play({
- onEnd: function (cancelled) {
- var signalData = {
- event: curEvent,
- cancelled: !!cancelled
- };
- // Keep track of currently playing events for cancelling
- delete timelinePath.eventsPlaying[curEvent.id];
- // Handle onEventEnd
- timelinePath.signalHandler.emitSignal('onEventEnd', signalData);
- // Reached end of path?
- if (!nextEvent) {
- onEnd(signalData);
- }
- }
- });
- // Schedule next
- if (nextEvent) {
- timeDiff = Math.abs(nextEvent.time - curEvent.time);
- if (timeDiff < 1) {
- // Play immediately
- timelinePath.cursor += direction;
- timelinePath.playEvents(direction);
- } else {
- // Schedule after the difference in ms
- this.nextScheduledPlay = setTimeout(function () {
- timelinePath.cursor += direction;
- timelinePath.playEvents(direction);
- }, timeDiff);
- }
- }
- };
- /* ************************************************************************** *
- * TIMELINE *
- * ************************************************************************** */
- /**
- * A set of options for the Timeline class.
- *
- * @requires module:modules/sonification
- *
- * @private
- * @interface Highcharts.TimelineOptionsObject
- *//**
- * List of TimelinePaths to play. Multiple paths can be grouped together and
- * played simultaneously by supplying an array of paths in place of a single
- * path.
- * @name Highcharts.TimelineOptionsObject#paths
- * @type {Array<Highcharts.TimelinePath|Array<Highcharts.TimelinePath>>}
- *//**
- * Callback function to call before a path plays.
- * @name Highcharts.TimelineOptionsObject#onPathStart
- * @type {Function|undefined}
- *//**
- * Callback function to call after a path has stopped playing.
- * @name Highcharts.TimelineOptionsObject#onPathEnd
- * @type {Function|undefined}
- *//**
- * Callback called when the whole path is finished.
- * @name Highcharts.TimelineOptionsObject#onEnd
- * @type {Function|undefined}
- */
- /**
- * The Timeline class. Represents a sonification timeline with a list of
- * timeline paths with events to play at certain times relative to each other.
- *
- * @requires module:modules/sonification
- *
- * @private
- * @class
- * @name Highcharts.Timeline
- *
- * @param {Highcharts.TimelineOptionsObject} options
- * Options for the Timeline.
- */
- function Timeline(options) {
- this.init(options || {});
- }
- Timeline.prototype.init = function (options) {
- this.options = options;
- this.cursor = 0;
- this.paths = options.paths;
- this.pathsPlaying = {};
- this.signalHandler = new utilities.SignalHandler(
- ['playOnEnd', 'masterOnEnd', 'onPathStart', 'onPathEnd']
- );
- this.signalHandler.registerSignalCallbacks(
- H.merge(options, { masterOnEnd: options.onEnd })
- );
- };
- /**
- * Play the timeline forwards from cursor.
- * @private
- * @param {Function} onEnd - Callback to call when play finished. Does not
- * override other onEnd callbacks.
- */
- Timeline.prototype.play = function (onEnd) {
- this.pause();
- this.signalHandler.clearSignalCallbacks(['playOnEnd']);
- this.signalHandler.registerSignalCallbacks({ playOnEnd: onEnd });
- this.playPaths(1);
- };
- /**
- * Play the timeline backwards from cursor.
- * @private
- * @param {Function} onEnd - Callback to call when play finished. Does not
- * override other onEnd callbacks.
- */
- Timeline.prototype.rewind = function (onEnd) {
- this.pause();
- this.signalHandler.clearSignalCallbacks(['playOnEnd']);
- this.signalHandler.registerSignalCallbacks({ playOnEnd: onEnd });
- this.playPaths(-1);
- };
- /**
- * Play the timeline in the specified direction.
- * @private
- * @param {number} direction - Direction to play in. 1 for forwards, -1 for
- * backwards.
- */
- Timeline.prototype.playPaths = function (direction) {
- var curPaths = H.splat(this.paths[this.cursor]),
- nextPaths = this.paths[this.cursor + direction],
- timeline = this,
- signalHandler = this.signalHandler,
- pathsEnded = 0,
- // Play a path
- playPath = function (path) {
- // Emit signal and set playing state
- signalHandler.emitSignal('onPathStart', path);
- timeline.pathsPlaying[path.id] = path;
- // Do the play
- path[direction > 0 ? 'play' : 'rewind'](function (callbackData) {
- // Play ended callback
- // Data to pass to signal callbacks
- var cancelled = callbackData && callbackData.cancelled,
- signalData = {
- path: path,
- cancelled: cancelled
- };
- // Clear state and send signal
- delete timeline.pathsPlaying[path.id];
- signalHandler.emitSignal('onPathEnd', signalData);
- // Handle next paths
- pathsEnded++;
- if (pathsEnded >= curPaths.length) {
- // We finished all of the current paths for cursor.
- if (nextPaths && !cancelled) {
- // We have more paths, move cursor along
- timeline.cursor += direction;
- // Reset upcoming path cursors before playing
- H.splat(nextPaths).forEach(function (nextPath) {
- nextPath[
- direction > 0 ? 'resetCursor' : 'resetCursorEnd'
- ]();
- });
- // Play next
- timeline.playPaths(direction);
- } else {
- // If it is the last path in this direction, call onEnd
- signalHandler.emitSignal('playOnEnd', signalData);
- signalHandler.emitSignal('masterOnEnd', signalData);
- }
- }
- });
- };
- // Go through the paths under cursor and play them
- curPaths.forEach(function (path) {
- if (path) {
- // Store reference to timeline
- path.timeline = timeline;
- // Leave a timeout to let notes fade out before next play
- setTimeout(function () {
- playPath(path);
- }, H.sonification.fadeOutTime);
- }
- });
- };
- /**
- * Stop the playing of the timeline. Cancels all current sounds, but does not
- * affect the cursor.
- * @private
- * @param {boolean} [fadeOut=false] - Whether or not to fade out as we stop. If
- * false, the timeline is cancelled synchronously.
- */
- Timeline.prototype.pause = function (fadeOut) {
- var timeline = this;
- // Cancel currently playing events
- Object.keys(timeline.pathsPlaying).forEach(function (id) {
- if (timeline.pathsPlaying[id]) {
- timeline.pathsPlaying[id].pause(fadeOut);
- }
- });
- timeline.pathsPlaying = {};
- };
- /**
- * Reset the cursor to the beginning of the timeline.
- * @private
- */
- Timeline.prototype.resetCursor = function () {
- this.paths.forEach(function (paths) {
- H.splat(paths).forEach(function (path) {
- path.resetCursor();
- });
- });
- this.cursor = 0;
- };
- /**
- * Reset the cursor to the end of the timeline.
- * @private
- */
- Timeline.prototype.resetCursorEnd = function () {
- this.paths.forEach(function (paths) {
- H.splat(paths).forEach(function (path) {
- path.resetCursorEnd();
- });
- });
- this.cursor = this.paths.length - 1;
- };
- /**
- * Set the current TimelineEvent under the cursor. If multiple paths are being
- * played at the same time, this function only affects a single path (the one
- * that contains the eventId that is passed in).
- * @private
- * @param {string} eventId - The ID of the timeline event to set as current.
- * @return {boolean} True if the cursor was set, false if no TimelineEvent was
- * found for this ID.
- */
- Timeline.prototype.setCursor = function (eventId) {
- return this.paths.some(function (paths) {
- return H.splat(paths).some(function (path) {
- return path.setCursor(eventId);
- });
- });
- };
- /**
- * Get the current TimelineEvents under the cursors. This function will return
- * the event under the cursor for each currently playing path, as an object
- * where the path ID is mapped to the TimelineEvent under that path's cursor.
- * @private
- * @return {object} The TimelineEvents under each path's cursors.
- */
- Timeline.prototype.getCursor = function () {
- return this.getCurrentPlayingPaths().reduce(function (acc, cur) {
- acc[cur.id] = cur.getCursor();
- return acc;
- }, {});
- };
- /**
- * Check if timeline is reset or at start.
- * @private
- * @return {boolean} True if timeline is at the beginning.
- */
- Timeline.prototype.atStart = function () {
- return !this.getCurrentPlayingPaths().some(function (path) {
- return path.cursor;
- });
- };
- /**
- * Get the current TimelinePaths being played.
- * @private
- * @return {Array<Highcharts.TimelinePath>} The TimelinePaths currently being
- * played.
- */
- Timeline.prototype.getCurrentPlayingPaths = function () {
- return H.splat(this.paths[this.cursor]);
- };
- // Export the classes
- var timelineClasses = {
- TimelineEvent: TimelineEvent,
- TimelinePath: TimelinePath,
- Timeline: Timeline
- };
- 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 () {
- }());
- }));
|