ChartSonify.js 42 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108
  1. /* *
  2. *
  3. * (c) 2009-2021 Øystein Moseng
  4. *
  5. * Sonification functions for chart/series.
  6. *
  7. * License: www.highcharts.com/license
  8. *
  9. * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
  10. *
  11. * */
  12. 'use strict';
  13. import H from '../../Core/Globals.js';
  14. /**
  15. * An Earcon configuration, specifying an Earcon and when to play it.
  16. *
  17. * @requires module:modules/sonification
  18. *
  19. * @interface Highcharts.EarconConfiguration
  20. */ /**
  21. * An Earcon instance.
  22. * @name Highcharts.EarconConfiguration#earcon
  23. * @type {Highcharts.Earcon}
  24. */ /**
  25. * The ID of the point to play the Earcon on.
  26. * @name Highcharts.EarconConfiguration#onPoint
  27. * @type {string|undefined}
  28. */ /**
  29. * A function to determine whether or not to play this earcon on a point. The
  30. * function is called for every point, receiving that point as parameter. It
  31. * should return either a boolean indicating whether or not to play the earcon,
  32. * or a new Earcon instance - in which case the new Earcon will be played.
  33. * @name Highcharts.EarconConfiguration#condition
  34. * @type {Function|undefined}
  35. */
  36. /**
  37. * Options for sonifying a series.
  38. *
  39. * @requires module:modules/sonification
  40. *
  41. * @interface Highcharts.SonifySeriesOptionsObject
  42. */ /**
  43. * The duration for playing the points. Note that points might continue to play
  44. * after the duration has passed, but no new points will start playing.
  45. * @name Highcharts.SonifySeriesOptionsObject#duration
  46. * @type {number}
  47. */ /**
  48. * The axis to use for when to play the points. Can be a string with a data
  49. * property (e.g. `x`), or a function. If it is a function, this function
  50. * receives the point as argument, and should return a numeric value. The points
  51. * with the lowest numeric values are then played first, and the time between
  52. * points will be proportional to the distance between the numeric values.
  53. * @name Highcharts.SonifySeriesOptionsObject#pointPlayTime
  54. * @type {string|Function}
  55. */ /**
  56. * The instrument definitions for the points in this series.
  57. * @name Highcharts.SonifySeriesOptionsObject#instruments
  58. * @type {Array<Highcharts.PointInstrumentObject>}
  59. */ /**
  60. * Earcons to add to the series.
  61. * @name Highcharts.SonifySeriesOptionsObject#earcons
  62. * @type {Array<Highcharts.EarconConfiguration>|undefined}
  63. */ /**
  64. * Optionally provide the minimum/maximum data values for the points. If this is
  65. * not supplied, it is calculated from all points in the chart on demand. This
  66. * option is supplied in the following format, as a map of point data properties
  67. * to objects with min/max values:
  68. * ```js
  69. * dataExtremes: {
  70. * y: {
  71. * min: 0,
  72. * max: 100
  73. * },
  74. * z: {
  75. * min: -10,
  76. * max: 10
  77. * }
  78. * // Properties used and not provided are calculated on demand
  79. * }
  80. * ```
  81. * @name Highcharts.SonifySeriesOptionsObject#dataExtremes
  82. * @type {Highcharts.Dictionary<Highcharts.RangeObject>|undefined}
  83. */ /**
  84. * Callback before a point is played.
  85. * @name Highcharts.SonifySeriesOptionsObject#onPointStart
  86. * @type {Function|undefined}
  87. */ /**
  88. * Callback after a point has finished playing.
  89. * @name Highcharts.SonifySeriesOptionsObject#onPointEnd
  90. * @type {Function|undefined}
  91. */ /**
  92. * Callback after the series has played.
  93. * @name Highcharts.SonifySeriesOptionsObject#onEnd
  94. * @type {Function|undefined}
  95. */
  96. ''; // detach doclets above
  97. import Point from '../../Core/Series/Point.js';
  98. import U from '../../Core/Utilities.js';
  99. var find = U.find, isArray = U.isArray, merge = U.merge, pick = U.pick, splat = U.splat, objectEach = U.objectEach;
  100. import utilities from './Utilities.js';
  101. /**
  102. * Get the relative time value of a point.
  103. * @private
  104. * @param {Highcharts.Point} point
  105. * The point.
  106. * @param {Function|string} timeProp
  107. * The time axis data prop or the time function.
  108. * @return {number}
  109. * The time value.
  110. */
  111. function getPointTimeValue(point, timeProp) {
  112. return typeof timeProp === 'function' ?
  113. timeProp(point) :
  114. pick(point[timeProp], point.options[timeProp]);
  115. }
  116. /**
  117. * Get the time extremes of this series. This is handled outside of the
  118. * dataExtremes, as we always want to just sonify the visible points, and we
  119. * always want the extremes to be the extremes of the visible points.
  120. * @private
  121. * @param {Highcharts.Series} series
  122. * The series to compute on.
  123. * @param {Function|string} timeProp
  124. * The time axis data prop or the time function.
  125. * @return {Highcharts.RangeObject}
  126. * Object with min/max extremes for the time values.
  127. */
  128. function getTimeExtremes(series, timeProp) {
  129. // Compute the extremes from the visible points.
  130. return series.points.reduce(function (acc, point) {
  131. var value = getPointTimeValue(point, timeProp);
  132. acc.min = Math.min(acc.min, value);
  133. acc.max = Math.max(acc.max, value);
  134. return acc;
  135. }, {
  136. min: Infinity,
  137. max: -Infinity
  138. });
  139. }
  140. /**
  141. * Calculate value extremes for used instrument data properties on a chart.
  142. * @private
  143. * @param {Highcharts.Chart} chart
  144. * The chart to calculate extremes from.
  145. * @param {Array<Highcharts.PointInstrumentObject>} [instruments]
  146. * Additional instrument definitions to inspect for data props used, in
  147. * addition to the instruments defined in the chart options.
  148. * @param {Highcharts.Dictionary<Highcharts.RangeObject>} [dataExtremes]
  149. * Predefined extremes for each data prop.
  150. * @return {Highcharts.Dictionary<Highcharts.RangeObject>}
  151. * New extremes with data properties mapped to min/max objects.
  152. */
  153. function getExtremesForInstrumentProps(chart, instruments, dataExtremes) {
  154. var allInstrumentDefinitions = (instruments || []).slice(0);
  155. var defaultInstrumentDef = (chart.options.sonification &&
  156. chart.options.sonification.defaultInstrumentOptions);
  157. var optionDefToInstrDef = function (optionDef) { return ({
  158. instrumentMapping: optionDef.mapping
  159. }); };
  160. if (defaultInstrumentDef) {
  161. allInstrumentDefinitions.push(optionDefToInstrDef(defaultInstrumentDef));
  162. }
  163. chart.series.forEach(function (series) {
  164. var instrOptions = (series.options.sonification &&
  165. series.options.sonification.instruments);
  166. if (instrOptions) {
  167. allInstrumentDefinitions = allInstrumentDefinitions.concat(instrOptions.map(optionDefToInstrDef));
  168. }
  169. });
  170. return (allInstrumentDefinitions).reduce(function (newExtremes, instrumentDefinition) {
  171. Object.keys(instrumentDefinition.instrumentMapping || {}).forEach(function (instrumentParameter) {
  172. var value = instrumentDefinition.instrumentMapping[instrumentParameter];
  173. if (typeof value === 'string' && !newExtremes[value]) {
  174. // This instrument parameter is mapped to a data prop.
  175. // If we don't have predefined data extremes, find them.
  176. newExtremes[value] = utilities.calculateDataExtremes(chart, value);
  177. }
  178. });
  179. return newExtremes;
  180. }, merge(dataExtremes));
  181. }
  182. /**
  183. * Get earcons for the point if there are any.
  184. * @private
  185. * @param {Highcharts.Point} point
  186. * The point to find earcons for.
  187. * @param {Array<Highcharts.EarconConfiguration>} earconDefinitions
  188. * Earcons to check.
  189. * @return {Array<Highcharts.Earcon>}
  190. * Array of earcons to be played with this point.
  191. */
  192. function getPointEarcons(point, earconDefinitions) {
  193. return earconDefinitions.reduce(function (earcons, earconDefinition) {
  194. var cond, earcon = earconDefinition.earcon;
  195. if (earconDefinition.condition) {
  196. // We have a condition. This overrides onPoint
  197. cond = earconDefinition.condition(point);
  198. if (cond instanceof H.sonification.Earcon) {
  199. // Condition returned an earcon
  200. earcons.push(cond);
  201. }
  202. else if (cond) {
  203. // Condition returned true
  204. earcons.push(earcon);
  205. }
  206. }
  207. else if (earconDefinition.onPoint &&
  208. point.id === earconDefinition.onPoint) {
  209. // We have earcon onPoint
  210. earcons.push(earcon);
  211. }
  212. return earcons;
  213. }, []);
  214. }
  215. /**
  216. * Utility function to get a new list of instrument options where all the
  217. * instrument references are copies.
  218. * @private
  219. * @param {Array<Highcharts.PointInstrumentObject>} instruments
  220. * The instrument options.
  221. * @return {Array<Highcharts.PointInstrumentObject>}
  222. * Array of copied instrument options.
  223. */
  224. function makeInstrumentCopies(instruments) {
  225. return instruments.map(function (instrumentDef) {
  226. var instrument = instrumentDef.instrument, copy = (typeof instrument === 'string' ?
  227. H.sonification.instruments[instrument] :
  228. instrument).copy();
  229. return merge(instrumentDef, { instrument: copy });
  230. });
  231. }
  232. /**
  233. * Utility function to apply a master volume to a list of instrument
  234. * options.
  235. * @private
  236. * @param {Array<Highcharts.PointInstrumentObject>} instruments
  237. * The instrument options. Only options with Instrument object instances
  238. * will be affected.
  239. * @param {number} masterVolume
  240. * The master volume multiplier to apply to the instruments.
  241. * @return {Array<Highcharts.PointInstrumentObject>}
  242. * Array of instrument options.
  243. */
  244. function applyMasterVolumeToInstruments(instruments, masterVolume) {
  245. instruments.forEach(function (instrOpts) {
  246. var instr = instrOpts.instrument;
  247. if (typeof instr !== 'string') {
  248. instr.setMasterVolume(masterVolume);
  249. }
  250. });
  251. return instruments;
  252. }
  253. /**
  254. * Utility function to find the duration of the final note in a series.
  255. * @private
  256. * @param {Highcharts.Series} series The data series to calculate on.
  257. * @param {Array<Highcharts.PointInstrumentObject>} instruments The instrument options for this series.
  258. * @param {Highcharts.Dictionary<Highcharts.RangeObject>} dataExtremes Value extremes for the data series props.
  259. * @return {number} The duration of the final note in milliseconds.
  260. */
  261. function getFinalNoteDuration(series, instruments, dataExtremes) {
  262. var finalPoint = series.points[series.points.length - 1];
  263. return instruments.reduce(function (duration, instrument) {
  264. var mapping = instrument.instrumentMapping.duration;
  265. var instrumentDuration;
  266. if (typeof mapping === 'string') {
  267. instrumentDuration = 0; // Ignore, no easy way to map this
  268. }
  269. else if (typeof mapping === 'function') {
  270. instrumentDuration = mapping(finalPoint, dataExtremes);
  271. }
  272. else {
  273. instrumentDuration = mapping;
  274. }
  275. return Math.max(duration, instrumentDuration);
  276. }, 0);
  277. }
  278. /**
  279. * Create a TimelinePath from a series. Takes the same options as seriesSonify.
  280. * To intuitively allow multiple series to play simultaneously we make copies of
  281. * the instruments for each series.
  282. * @private
  283. * @param {Highcharts.Series} series
  284. * The series to build from.
  285. * @param {Highcharts.SonifySeriesOptionsObject} options
  286. * The options for building the TimelinePath.
  287. * @return {Highcharts.TimelinePath}
  288. * A timeline path with events.
  289. */
  290. function buildTimelinePathFromSeries(series, options) {
  291. // options.timeExtremes is internal and used so that the calculations from
  292. // chart.sonify can be reused.
  293. var timeExtremes = options.timeExtremes || getTimeExtremes(series, options.pointPlayTime),
  294. // Compute any data extremes that aren't defined yet
  295. dataExtremes = getExtremesForInstrumentProps(series.chart, options.instruments, options.dataExtremes), minimumSeriesDurationMs = 10,
  296. // Get the duration of the final note
  297. finalNoteDuration = getFinalNoteDuration(series, options.instruments, dataExtremes),
  298. // Get time offset for a point, relative to duration
  299. pointToTime = function (point) {
  300. return utilities.virtualAxisTranslate(getPointTimeValue(point, options.pointPlayTime), timeExtremes, { min: 0, max: Math.max(options.duration - finalNoteDuration, minimumSeriesDurationMs) });
  301. }, masterVolume = pick(options.masterVolume, 1),
  302. // Make copies of the instruments used for this series, to allow
  303. // multiple series with the same instrument to play together
  304. instrumentCopies = makeInstrumentCopies(options.instruments), instruments = applyMasterVolumeToInstruments(instrumentCopies, masterVolume),
  305. // Go through the points, convert to events, optionally add Earcons
  306. timelineEvents = series.points.reduce(function (events, point) {
  307. var earcons = getPointEarcons(point, options.earcons || []), time = pointToTime(point);
  308. return events.concat(
  309. // Event object for point
  310. new H.sonification.TimelineEvent({
  311. eventObject: point,
  312. time: time,
  313. id: point.id,
  314. playOptions: {
  315. instruments: instruments,
  316. dataExtremes: dataExtremes,
  317. masterVolume: masterVolume
  318. }
  319. }),
  320. // Earcons
  321. earcons.map(function (earcon) {
  322. return new H.sonification.TimelineEvent({
  323. eventObject: earcon,
  324. time: time,
  325. playOptions: {
  326. volume: masterVolume
  327. }
  328. });
  329. }));
  330. }, []);
  331. // Build the timeline path
  332. return new H.sonification.TimelinePath({
  333. events: timelineEvents,
  334. onStart: function () {
  335. if (options.onStart) {
  336. options.onStart(series);
  337. }
  338. },
  339. onEventStart: function (event) {
  340. var eventObject = event.options && event.options.eventObject;
  341. if (eventObject instanceof Point) {
  342. // Check for hidden series
  343. if (!eventObject.series.visible &&
  344. !eventObject.series.chart.series.some(function (series) {
  345. return series.visible;
  346. })) {
  347. // We have no visible series, stop the path.
  348. event.timelinePath.timeline.pause();
  349. event.timelinePath.timeline.resetCursor();
  350. return false;
  351. }
  352. // Emit onPointStart
  353. if (options.onPointStart) {
  354. options.onPointStart(event, eventObject);
  355. }
  356. }
  357. },
  358. onEventEnd: function (eventData) {
  359. var eventObject = eventData.event && eventData.event.options &&
  360. eventData.event.options.eventObject;
  361. if (eventObject instanceof Point && options.onPointEnd) {
  362. options.onPointEnd(eventData.event, eventObject);
  363. }
  364. },
  365. onEnd: function () {
  366. if (options.onEnd) {
  367. options.onEnd(series);
  368. }
  369. },
  370. targetDuration: options.duration
  371. });
  372. }
  373. /* eslint-disable no-invalid-this, valid-jsdoc */
  374. /**
  375. * Sonify a series.
  376. *
  377. * @sample highcharts/sonification/series-basic/
  378. * Click on series to sonify
  379. * @sample highcharts/sonification/series-earcon/
  380. * Series with earcon
  381. * @sample highcharts/sonification/point-play-time/
  382. * Play y-axis by time
  383. * @sample highcharts/sonification/earcon-on-point/
  384. * Earcon set on point
  385. *
  386. * @requires module:modules/sonification
  387. *
  388. * @function Highcharts.Series#sonify
  389. *
  390. * @param {Highcharts.SonifySeriesOptionsObject} [options]
  391. * The options for sonifying this series. If not provided,
  392. * uses options set on chart and series.
  393. *
  394. * @return {void}
  395. */
  396. function seriesSonify(options) {
  397. var mergedOptions = getSeriesSonifyOptions(this, options);
  398. var timelinePath = buildTimelinePathFromSeries(this, mergedOptions);
  399. var chartSonification = this.chart.sonification;
  400. // Only one timeline can play at a time. If we want multiple series playing
  401. // at the same time, use chart.sonify.
  402. if (chartSonification.timeline) {
  403. chartSonification.timeline.pause();
  404. }
  405. // Store reference to duration
  406. chartSonification.duration = mergedOptions.duration;
  407. // Create new timeline for this series, and play it.
  408. chartSonification.timeline = new H.sonification.Timeline({
  409. paths: [timelinePath]
  410. });
  411. chartSonification.timeline.play();
  412. }
  413. /**
  414. * Utility function to assemble options for creating a TimelinePath from a
  415. * series when sonifying an entire chart.
  416. * @private
  417. * @param {Highcharts.Series} series
  418. * The series to return options for.
  419. * @param {Highcharts.RangeObject} dataExtremes
  420. * Pre-calculated data extremes for the chart.
  421. * @param {Highcharts.SonificationOptions} chartSonifyOptions
  422. * Options passed in to chart.sonify.
  423. * @return {Partial<Highcharts.SonifySeriesOptionsObject>}
  424. * Options for buildTimelinePathFromSeries.
  425. */
  426. function buildChartSonifySeriesOptions(series, dataExtremes, chartSonifyOptions) {
  427. var additionalSeriesOptions = chartSonifyOptions.seriesOptions || {};
  428. var pointPlayTime = (series.chart.options.sonification &&
  429. series.chart.options.sonification.defaultInstrumentOptions &&
  430. series.chart.options.sonification.defaultInstrumentOptions.mapping &&
  431. series.chart.options.sonification.defaultInstrumentOptions.mapping.pointPlayTime ||
  432. 'x');
  433. var configOptions = chartOptionsToSonifySeriesOptions(series);
  434. return merge(
  435. // Options from chart configuration
  436. configOptions,
  437. // Options passed in
  438. {
  439. // Calculated dataExtremes for chart
  440. dataExtremes: dataExtremes,
  441. // We need to get timeExtremes for each series. We pass this
  442. // in when building the TimelinePath objects to avoid
  443. // calculating twice.
  444. timeExtremes: getTimeExtremes(series, pointPlayTime),
  445. // Some options we just pass on
  446. instruments: chartSonifyOptions.instruments || configOptions.instruments,
  447. onStart: chartSonifyOptions.onSeriesStart || configOptions.onStart,
  448. onEnd: chartSonifyOptions.onSeriesEnd || configOptions.onEnd,
  449. earcons: chartSonifyOptions.earcons || configOptions.earcons,
  450. masterVolume: pick(chartSonifyOptions.masterVolume, configOptions.masterVolume)
  451. },
  452. // Merge in the specific series options by ID if any are passed in
  453. isArray(additionalSeriesOptions) ? (find(additionalSeriesOptions, function (optEntry) {
  454. return optEntry.id === pick(series.id, series.options.id);
  455. }) || {}) : additionalSeriesOptions, {
  456. // Forced options
  457. pointPlayTime: pointPlayTime
  458. });
  459. }
  460. /**
  461. * Utility function to normalize the ordering of timeline paths when sonifying
  462. * a chart.
  463. * @private
  464. * @param {string|Array<string|Highcharts.Earcon|Array<string|Highcharts.Earcon>>} orderOptions -
  465. * Order options for the sonification.
  466. * @param {Highcharts.Chart} chart - The chart we are sonifying.
  467. * @param {Function} seriesOptionsCallback
  468. * A function that takes a series as argument, and returns the series options
  469. * for that series to be used with buildTimelinePathFromSeries.
  470. * @return {Array<object|Array<object|Highcharts.TimelinePath>>} If order is
  471. * sequential, we return an array of objects to create series paths from. If
  472. * order is simultaneous we return an array of an array with the same. If there
  473. * is a custom order, we return an array of arrays of either objects (for
  474. * series) or TimelinePaths (for earcons and delays).
  475. */
  476. function buildPathOrder(orderOptions, chart, seriesOptionsCallback) {
  477. var order;
  478. if (orderOptions === 'sequential' || orderOptions === 'simultaneous') {
  479. // Just add the series from the chart
  480. order = chart.series.reduce(function (seriesList, series) {
  481. if (series.visible &&
  482. (series.options.sonification &&
  483. series.options.sonification.enabled) !== false) {
  484. seriesList.push({
  485. series: series,
  486. seriesOptions: seriesOptionsCallback(series)
  487. });
  488. }
  489. return seriesList;
  490. }, []);
  491. // If order is simultaneous, group all series together
  492. if (orderOptions === 'simultaneous') {
  493. order = [order];
  494. }
  495. }
  496. else {
  497. // We have a specific order, and potentially custom items - like
  498. // earcons or silent waits.
  499. order = orderOptions.reduce(function (orderList, orderDef) {
  500. // Return set of items to play simultaneously. Could be only one.
  501. var simulItems = splat(orderDef).reduce(function (items, item) {
  502. var itemObject;
  503. // Is this item a series ID?
  504. if (typeof item === 'string') {
  505. var series = chart.get(item);
  506. if (series.visible) {
  507. itemObject = {
  508. series: series,
  509. seriesOptions: seriesOptionsCallback(series)
  510. };
  511. }
  512. // Is it an earcon? If so, just create the path.
  513. }
  514. else if (item instanceof H.sonification.Earcon) {
  515. // Path with a single event
  516. itemObject = new H.sonification.TimelinePath({
  517. events: [new H.sonification.TimelineEvent({
  518. eventObject: item
  519. })]
  520. });
  521. }
  522. // Is this item a silent wait? If so, just create the path.
  523. if (item.silentWait) {
  524. itemObject = new H.sonification.TimelinePath({
  525. silentWait: item.silentWait
  526. });
  527. }
  528. // Add to items to play simultaneously
  529. if (itemObject) {
  530. items.push(itemObject);
  531. }
  532. return items;
  533. }, []);
  534. // Add to order list
  535. if (simulItems.length) {
  536. orderList.push(simulItems);
  537. }
  538. return orderList;
  539. }, []);
  540. }
  541. return order;
  542. }
  543. /**
  544. * Utility function to add a silent wait after all series.
  545. * @private
  546. * @param {Array<object|Array<object|TimelinePath>>} order
  547. * The order of items.
  548. * @param {number} wait
  549. * The wait in milliseconds to add.
  550. * @return {Array<object|Array<object|TimelinePath>>}
  551. * The order with waits inserted.
  552. */
  553. function addAfterSeriesWaits(order, wait) {
  554. if (!wait) {
  555. return order;
  556. }
  557. return order.reduce(function (newOrder, orderDef, i) {
  558. var simultaneousPaths = splat(orderDef);
  559. newOrder.push(simultaneousPaths);
  560. // Go through the simultaneous paths and see if there is a series there
  561. if (i < order.length - 1 && // Do not add wait after last series
  562. simultaneousPaths.some(function (item) {
  563. return item.series;
  564. })) {
  565. // We have a series, meaning we should add a wait after these
  566. // paths have finished.
  567. newOrder.push(new H.sonification.TimelinePath({
  568. silentWait: wait
  569. }));
  570. }
  571. return newOrder;
  572. }, []);
  573. }
  574. /**
  575. * Utility function to find the total amout of wait time in the TimelinePaths.
  576. * @private
  577. * @param {Array<object|Array<object|TimelinePath>>} order - The order of
  578. * TimelinePaths/items.
  579. * @return {number} The total time in ms spent on wait paths between playing.
  580. */
  581. function getWaitTime(order) {
  582. return order.reduce(function (waitTime, orderDef) {
  583. var def = splat(orderDef);
  584. return waitTime + (def.length === 1 &&
  585. def[0].options &&
  586. def[0].options.silentWait || 0);
  587. }, 0);
  588. }
  589. /**
  590. * Utility function to ensure simultaneous paths have start/end events at the
  591. * same time, to sync them.
  592. * @private
  593. * @param {Array<Highcharts.TimelinePath>} paths - The paths to sync.
  594. */
  595. function syncSimultaneousPaths(paths) {
  596. // Find the extremes for these paths
  597. var extremes = paths.reduce(function (extremes, path) {
  598. var events = path.events;
  599. if (events && events.length) {
  600. extremes.min = Math.min(events[0].time, extremes.min);
  601. extremes.max = Math.max(events[events.length - 1].time, extremes.max);
  602. }
  603. return extremes;
  604. }, {
  605. min: Infinity,
  606. max: -Infinity
  607. });
  608. // Go through the paths and add events to make them fit the same timespan
  609. paths.forEach(function (path) {
  610. var events = path.events, hasEvents = events && events.length, eventsToAdd = [];
  611. if (!(hasEvents && events[0].time <= extremes.min)) {
  612. eventsToAdd.push(new H.sonification.TimelineEvent({
  613. time: extremes.min
  614. }));
  615. }
  616. if (!(hasEvents && events[events.length - 1].time >= extremes.max)) {
  617. eventsToAdd.push(new H.sonification.TimelineEvent({
  618. time: extremes.max
  619. }));
  620. }
  621. if (eventsToAdd.length) {
  622. path.addTimelineEvents(eventsToAdd);
  623. }
  624. });
  625. }
  626. /**
  627. * Utility function to find the total duration span for all simul path sets
  628. * that include series.
  629. * @private
  630. * @param {Array<object|Array<object|Highcharts.TimelinePath>>} order - The
  631. * order of TimelinePaths/items.
  632. * @return {number} The total time value span difference for all series.
  633. */
  634. function getSimulPathDurationTotal(order) {
  635. return order.reduce(function (durationTotal, orderDef) {
  636. return durationTotal + splat(orderDef).reduce(function (maxPathDuration, item) {
  637. var timeExtremes = (item.series &&
  638. item.seriesOptions &&
  639. item.seriesOptions.timeExtremes);
  640. return timeExtremes ?
  641. Math.max(maxPathDuration, timeExtremes.max - timeExtremes.min) : maxPathDuration;
  642. }, 0);
  643. }, 0);
  644. }
  645. /**
  646. * Function to calculate the duration in ms for a series.
  647. * @private
  648. * @param {number} seriesValueDuration - The duration of the series in value
  649. * difference.
  650. * @param {number} totalValueDuration - The total duration of all (non
  651. * simultaneous) series in value difference.
  652. * @param {number} totalDurationMs - The desired total duration for all series
  653. * in milliseconds.
  654. * @return {number} The duration for the series in milliseconds.
  655. */
  656. function getSeriesDurationMs(seriesValueDuration, totalValueDuration, totalDurationMs) {
  657. // A series spanning the whole chart would get the full duration.
  658. return utilities.virtualAxisTranslate(seriesValueDuration, { min: 0, max: totalValueDuration }, { min: 0, max: totalDurationMs });
  659. }
  660. /**
  661. * Convert series building objects into paths and return a new list of
  662. * TimelinePaths.
  663. * @private
  664. * @param {Array<object|Array<object|Highcharts.TimelinePath>>} order - The
  665. * order list.
  666. * @param {number} duration - Total duration to aim for in milliseconds.
  667. * @return {Array<Array<Highcharts.TimelinePath>>} Array of TimelinePath objects
  668. * to play.
  669. */
  670. function buildPathsFromOrder(order, duration) {
  671. // Find time used for waits (custom or after series), and subtract it from
  672. // available duration.
  673. var totalAvailableDurationMs = Math.max(duration - getWaitTime(order), 0),
  674. // Add up simultaneous path durations to find total value span duration
  675. // of everything
  676. totalUsedDuration = getSimulPathDurationTotal(order);
  677. // Go through the order list and convert the items
  678. return order.reduce(function (allPaths, orderDef) {
  679. var simultaneousPaths = splat(orderDef).reduce(function (simulPaths, item) {
  680. if (item instanceof H.sonification.TimelinePath) {
  681. // This item is already a path object
  682. simulPaths.push(item);
  683. }
  684. else if (item.series) {
  685. // We have a series.
  686. // We need to set the duration of the series
  687. item.seriesOptions.duration =
  688. item.seriesOptions.duration || getSeriesDurationMs(item.seriesOptions.timeExtremes.max -
  689. item.seriesOptions.timeExtremes.min, totalUsedDuration, totalAvailableDurationMs);
  690. // Add the path
  691. simulPaths.push(buildTimelinePathFromSeries(item.series, item.seriesOptions));
  692. }
  693. return simulPaths;
  694. }, []);
  695. // Add in the simultaneous paths
  696. allPaths.push(simultaneousPaths);
  697. return allPaths;
  698. }, []);
  699. }
  700. /**
  701. * @private
  702. * @param {Highcharts.Series} series The series to get options for.
  703. * @param {Highcharts.SonifySeriesOptionsObject} options
  704. * Options to merge with user options on series/chart and default options.
  705. * @returns {Array<Highcharts.PointInstrumentObject>} The merged options.
  706. */
  707. function getSeriesInstrumentOptions(series, options) {
  708. if (options && options.instruments) {
  709. return options.instruments;
  710. }
  711. var defaultInstrOpts = (series.chart.options.sonification &&
  712. series.chart.options.sonification.defaultInstrumentOptions ||
  713. {});
  714. var seriesInstrOpts = (series.options.sonification &&
  715. series.options.sonification.instruments ||
  716. [{}]);
  717. var removeNullsFromObject = function (obj) {
  718. objectEach(obj, function (val, key) {
  719. if (val === null) {
  720. delete obj[key];
  721. }
  722. });
  723. };
  724. // Convert series options to PointInstrumentObjects and merge with
  725. // default options
  726. return (seriesInstrOpts).map(function (optionSet) {
  727. // Allow setting option to null to use default
  728. removeNullsFromObject(optionSet.mapping || {});
  729. removeNullsFromObject(optionSet);
  730. return {
  731. instrument: optionSet.instrument || defaultInstrOpts.instrument,
  732. instrumentOptions: merge(defaultInstrOpts, optionSet, {
  733. // Instrument options are lifted to root in the API options
  734. // object, so merge all in order to avoid missing any. But
  735. // remove the following which are not instrumentOptions:
  736. mapping: void 0,
  737. instrument: void 0
  738. }),
  739. instrumentMapping: merge(defaultInstrOpts.mapping, optionSet.mapping)
  740. };
  741. });
  742. }
  743. /**
  744. * Utility function to translate between options set in chart configuration and
  745. * a SonifySeriesOptionsObject.
  746. * @private
  747. * @param {Highcharts.Series} series The series to get options for.
  748. * @returns {Highcharts.SonifySeriesOptionsObject} Options for chart/series.sonify()
  749. */
  750. function chartOptionsToSonifySeriesOptions(series) {
  751. var seriesOpts = series.options.sonification || {};
  752. var chartOpts = series.chart.options.sonification || {};
  753. var chartEvents = chartOpts.events || {};
  754. var seriesEvents = seriesOpts.events || {};
  755. return {
  756. onEnd: seriesEvents.onSeriesEnd || chartEvents.onSeriesEnd,
  757. onStart: seriesEvents.onSeriesStart || chartEvents.onSeriesStart,
  758. onPointEnd: seriesEvents.onPointEnd || chartEvents.onPointEnd,
  759. onPointStart: seriesEvents.onPointStart || chartEvents.onPointStart,
  760. pointPlayTime: (chartOpts.defaultInstrumentOptions &&
  761. chartOpts.defaultInstrumentOptions.mapping &&
  762. chartOpts.defaultInstrumentOptions.mapping.pointPlayTime),
  763. masterVolume: chartOpts.masterVolume,
  764. instruments: getSeriesInstrumentOptions(series),
  765. earcons: seriesOpts.earcons || chartOpts.earcons
  766. };
  767. }
  768. /**
  769. * @private
  770. * @param {Highcharts.Series} series The series to get options for.
  771. * @param {Highcharts.SonifySeriesOptionsObject} options
  772. * Options to merge with user options on series/chart and default options.
  773. * @returns {Highcharts.SonifySeriesOptionsObject} The merged options.
  774. */
  775. function getSeriesSonifyOptions(series, options) {
  776. var chartOpts = series.chart.options.sonification;
  777. var seriesOpts = series.options.sonification;
  778. return merge({
  779. duration: ((seriesOpts && seriesOpts.duration) ||
  780. (chartOpts && chartOpts.duration))
  781. }, chartOptionsToSonifySeriesOptions(series), options);
  782. }
  783. /**
  784. * @private
  785. * @param {Highcharts.Chart} chart The chart to get options for.
  786. * @param {Highcharts.SonificationOptions} options
  787. * Options to merge with user options on chart and default options.
  788. * @returns {Highcharts.SonificationOptions} The merged options.
  789. */
  790. function getChartSonifyOptions(chart, options) {
  791. var chartOpts = chart.options.sonification || {};
  792. return merge({
  793. duration: chartOpts.duration,
  794. afterSeriesWait: chartOpts.afterSeriesWait,
  795. pointPlayTime: (chartOpts.defaultInstrumentOptions &&
  796. chartOpts.defaultInstrumentOptions.mapping &&
  797. chartOpts.defaultInstrumentOptions.mapping.pointPlayTime),
  798. order: chartOpts.order,
  799. onSeriesStart: (chartOpts.events && chartOpts.events.onSeriesStart),
  800. onSeriesEnd: (chartOpts.events && chartOpts.events.onSeriesEnd),
  801. onEnd: (chartOpts.events && chartOpts.events.onEnd)
  802. }, options);
  803. }
  804. /**
  805. * Options for sonifying a chart.
  806. *
  807. * @requires module:modules/sonification
  808. *
  809. * @interface Highcharts.SonificationOptions
  810. */ /**
  811. * Duration for sonifying the entire chart. The duration is distributed across
  812. * the different series intelligently, but does not take earcons into account.
  813. * It is also possible to set the duration explicitly per series, using
  814. * `seriesOptions`. Note that points may continue to play after the duration has
  815. * passed, but no new points will start playing.
  816. * @name Highcharts.SonificationOptions#duration
  817. * @type {number}
  818. */ /**
  819. * Define the order to play the series in. This can be given as a string, or an
  820. * array specifying a custom ordering. If given as a string, valid values are
  821. * `sequential` - where each series is played in order - or `simultaneous`,
  822. * where all series are played at once. For custom ordering, supply an array as
  823. * the order. Each element in the array can be either a string with a series ID,
  824. * an Earcon object, or an object with a numeric `silentWait` property
  825. * designating a number of milliseconds to wait before continuing. Each element
  826. * of the array will be played in order. To play elements simultaneously, group
  827. * the elements in an array.
  828. * @name Highcharts.SonificationOptions#order
  829. * @type {string|Array<string|Highcharts.Earcon|Array<string|Highcharts.Earcon>>}
  830. */ /**
  831. * The axis to use for when to play the points. Can be a string with a data
  832. * property (e.g. `x`), or a function. If it is a function, this function
  833. * receives the point as argument, and should return a numeric value. The points
  834. * with the lowest numeric values are then played first, and the time between
  835. * points will be proportional to the distance between the numeric values. This
  836. * option can not be overridden per series.
  837. * @name Highcharts.SonificationOptions#pointPlayTime
  838. * @type {string|Function}
  839. */ /**
  840. * Milliseconds of silent waiting to add between series. Note that waiting time
  841. * is considered part of the sonify duration.
  842. * @name Highcharts.SonificationOptions#afterSeriesWait
  843. * @type {number|undefined}
  844. */ /**
  845. * Options as given to `series.sonify` to override options per series. If the
  846. * option is supplied as an array of options objects, the `id` property of the
  847. * object should correspond to the series' id. If the option is supplied as a
  848. * single object, the options apply to all series.
  849. * @name Highcharts.SonificationOptions#seriesOptions
  850. * @type {Object|Array<object>|undefined}
  851. */ /**
  852. * The instrument definitions for the points in this chart.
  853. * @name Highcharts.SonificationOptions#instruments
  854. * @type {Array<Highcharts.PointInstrumentObject>|undefined}
  855. */ /**
  856. * Earcons to add to the chart. Note that earcons can also be added per series
  857. * using `seriesOptions`.
  858. * @name Highcharts.SonificationOptions#earcons
  859. * @type {Array<Highcharts.EarconConfiguration>|undefined}
  860. */ /**
  861. * Optionally provide the minimum/maximum data values for the points. If this is
  862. * not supplied, it is calculated from all points in the chart on demand. This
  863. * option is supplied in the following format, as a map of point data properties
  864. * to objects with min/max values:
  865. * ```js
  866. * dataExtremes: {
  867. * y: {
  868. * min: 0,
  869. * max: 100
  870. * },
  871. * z: {
  872. * min: -10,
  873. * max: 10
  874. * }
  875. * // Properties used and not provided are calculated on demand
  876. * }
  877. * ```
  878. * @name Highcharts.SonificationOptions#dataExtremes
  879. * @type {Highcharts.Dictionary<Highcharts.RangeObject>|undefined}
  880. */ /**
  881. * Callback before a series is played.
  882. * @name Highcharts.SonificationOptions#onSeriesStart
  883. * @type {Function|undefined}
  884. */ /**
  885. * Callback after a series has finished playing.
  886. * @name Highcharts.SonificationOptions#onSeriesEnd
  887. * @type {Function|undefined}
  888. */ /**
  889. * Callback after the chart has played.
  890. * @name Highcharts.SonificationOptions#onEnd
  891. * @type {Function|undefined}
  892. */
  893. /**
  894. * Sonify a chart.
  895. *
  896. * @sample highcharts/sonification/chart-sequential/
  897. * Sonify a basic chart
  898. * @sample highcharts/sonification/chart-simultaneous/
  899. * Sonify series simultaneously
  900. * @sample highcharts/sonification/chart-custom-order/
  901. * Custom defined order of series
  902. * @sample highcharts/sonification/chart-earcon/
  903. * Earcons on chart
  904. * @sample highcharts/sonification/chart-events/
  905. * Sonification events on chart
  906. *
  907. * @requires module:modules/sonification
  908. *
  909. * @function Highcharts.Chart#sonify
  910. *
  911. * @param {Highcharts.SonificationOptions} [options]
  912. * The options for sonifying this chart. If not provided,
  913. * uses options set on chart and series.
  914. *
  915. * @return {void}
  916. */
  917. function chartSonify(options) {
  918. var opts = getChartSonifyOptions(this, options);
  919. // Only one timeline can play at a time.
  920. if (this.sonification.timeline) {
  921. this.sonification.timeline.pause();
  922. }
  923. // Store reference to duration
  924. this.sonification.duration = opts.duration;
  925. // Calculate data extremes for the props used
  926. var dataExtremes = getExtremesForInstrumentProps(this, opts.instruments, opts.dataExtremes);
  927. // Figure out ordering of series and custom paths
  928. var order = buildPathOrder(opts.order, this, function (series) {
  929. return buildChartSonifySeriesOptions(series, dataExtremes, opts);
  930. });
  931. // Add waits after simultaneous paths with series in them.
  932. order = addAfterSeriesWaits(order, opts.afterSeriesWait || 0);
  933. // We now have a list of either TimelinePath objects or series that need to
  934. // be converted to TimelinePath objects. Convert everything to paths.
  935. var paths = buildPathsFromOrder(order, opts.duration);
  936. // Sync simultaneous paths
  937. paths.forEach(function (simultaneousPaths) {
  938. syncSimultaneousPaths(simultaneousPaths);
  939. });
  940. // We have a set of paths. Create the timeline, and play it.
  941. this.sonification.timeline = new H.sonification.Timeline({
  942. paths: paths,
  943. onEnd: opts.onEnd
  944. });
  945. this.sonification.timeline.play();
  946. }
  947. /**
  948. * Get a list of the points currently under cursor.
  949. *
  950. * @requires module:modules/sonification
  951. *
  952. * @function Highcharts.Chart#getCurrentSonifyPoints
  953. *
  954. * @return {Array<Highcharts.Point>}
  955. * The points currently under the cursor.
  956. */
  957. function getCurrentPoints() {
  958. var cursorObj;
  959. if (this.sonification.timeline) {
  960. cursorObj = this.sonification.timeline.getCursor(); // Cursor per pathID
  961. return Object.keys(cursorObj).map(function (path) {
  962. // Get the event objects under cursor for each path
  963. return cursorObj[path].eventObject;
  964. }).filter(function (eventObj) {
  965. // Return the events that are points
  966. return eventObj instanceof Point;
  967. });
  968. }
  969. return [];
  970. }
  971. /**
  972. * Set the cursor to a point or set of points in different series.
  973. *
  974. * @requires module:modules/sonification
  975. *
  976. * @function Highcharts.Chart#setSonifyCursor
  977. *
  978. * @param {Highcharts.Point|Array<Highcharts.Point>} points
  979. * The point or points to set the cursor to. If setting multiple points
  980. * under the cursor, the points have to be in different series that are
  981. * being played simultaneously.
  982. */
  983. function setCursor(points) {
  984. var timeline = this.sonification.timeline;
  985. if (timeline) {
  986. splat(points).forEach(function (point) {
  987. // We created the events with the ID of the points, which makes
  988. // this easy. Just call setCursor for each ID.
  989. timeline.setCursor(point.id);
  990. });
  991. }
  992. }
  993. /**
  994. * Pause the running sonification.
  995. *
  996. * @requires module:modules/sonification
  997. *
  998. * @function Highcharts.Chart#pauseSonify
  999. *
  1000. * @param {boolean} [fadeOut=true]
  1001. * Fade out as we pause to avoid clicks.
  1002. *
  1003. * @return {void}
  1004. */
  1005. function pause(fadeOut) {
  1006. if (this.sonification.timeline) {
  1007. this.sonification.timeline.pause(pick(fadeOut, true));
  1008. }
  1009. else if (this.sonification.currentlyPlayingPoint) {
  1010. this.sonification.currentlyPlayingPoint.cancelSonify(fadeOut);
  1011. }
  1012. }
  1013. /**
  1014. * Resume the currently running sonification. Requires series.sonify or
  1015. * chart.sonify to have been played at some point earlier.
  1016. *
  1017. * @requires module:modules/sonification
  1018. *
  1019. * @function Highcharts.Chart#resumeSonify
  1020. *
  1021. * @param {Function} onEnd
  1022. * Callback to call when play finished.
  1023. *
  1024. * @return {void}
  1025. */
  1026. function resume(onEnd) {
  1027. if (this.sonification.timeline) {
  1028. this.sonification.timeline.play(onEnd);
  1029. }
  1030. }
  1031. /**
  1032. * Play backwards from cursor. Requires series.sonify or chart.sonify to have
  1033. * been played at some point earlier.
  1034. *
  1035. * @requires module:modules/sonification
  1036. *
  1037. * @function Highcharts.Chart#rewindSonify
  1038. *
  1039. * @param {Function} onEnd
  1040. * Callback to call when play finished.
  1041. *
  1042. * @return {void}
  1043. */
  1044. function rewind(onEnd) {
  1045. if (this.sonification.timeline) {
  1046. this.sonification.timeline.rewind(onEnd);
  1047. }
  1048. }
  1049. /**
  1050. * Cancel current sonification and reset cursor.
  1051. *
  1052. * @requires module:modules/sonification
  1053. *
  1054. * @function Highcharts.Chart#cancelSonify
  1055. *
  1056. * @param {boolean} [fadeOut=true]
  1057. * Fade out as we pause to avoid clicks.
  1058. *
  1059. * @return {void}
  1060. */
  1061. function cancel(fadeOut) {
  1062. this.pauseSonify(fadeOut);
  1063. this.resetSonifyCursor();
  1064. }
  1065. /**
  1066. * Reset cursor to start. Requires series.sonify or chart.sonify to have been
  1067. * played at some point earlier.
  1068. *
  1069. * @requires module:modules/sonification
  1070. *
  1071. * @function Highcharts.Chart#resetSonifyCursor
  1072. *
  1073. * @return {void}
  1074. */
  1075. function resetCursor() {
  1076. if (this.sonification.timeline) {
  1077. this.sonification.timeline.resetCursor();
  1078. }
  1079. }
  1080. /**
  1081. * Reset cursor to end. Requires series.sonify or chart.sonify to have been
  1082. * played at some point earlier.
  1083. *
  1084. * @requires module:modules/sonification
  1085. *
  1086. * @function Highcharts.Chart#resetSonifyCursorEnd
  1087. *
  1088. * @return {void}
  1089. */
  1090. function resetCursorEnd() {
  1091. if (this.sonification.timeline) {
  1092. this.sonification.timeline.resetCursorEnd();
  1093. }
  1094. }
  1095. // Export functions
  1096. var chartSonifyFunctions = {
  1097. chartSonify: chartSonify,
  1098. seriesSonify: seriesSonify,
  1099. pause: pause,
  1100. resume: resume,
  1101. rewind: rewind,
  1102. cancel: cancel,
  1103. getCurrentPoints: getCurrentPoints,
  1104. setCursor: setCursor,
  1105. resetCursor: resetCursor,
  1106. resetCursorEnd: resetCursorEnd
  1107. };
  1108. export default chartSonifyFunctions;