chartSonify.js 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098
  1. /* *
  2. *
  3. * (c) 2009-2020 Ø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 _a;
  155. var allInstrumentDefinitions = (instruments || []).slice(0);
  156. var defaultInstrumentDef = (_a = chart.options.sonification) === null || _a === void 0 ? void 0 : _a.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 _a;
  165. var instrOptions = (_a = series.options.sonification) === null || _a === void 0 ? void 0 : _a.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 _a, _b, _c;
  428. var additionalSeriesOptions = chartSonifyOptions.seriesOptions || {};
  429. var pointPlayTime = ((_c = (_b = (_a = series.chart.options.sonification) === null || _a === void 0 ? void 0 : _a.defaultInstrumentOptions) === null || _b === void 0 ? void 0 : _b.mapping) === null || _c === void 0 ? void 0 : _c.pointPlayTime) || 'x';
  430. var configOptions = chartOptionsToSonifySeriesOptions(series);
  431. return merge(
  432. // Options from chart configuration
  433. configOptions,
  434. // Options passed in
  435. {
  436. // Calculated dataExtremes for chart
  437. dataExtremes: dataExtremes,
  438. // We need to get timeExtremes for each series. We pass this
  439. // in when building the TimelinePath objects to avoid
  440. // calculating twice.
  441. timeExtremes: getTimeExtremes(series, pointPlayTime),
  442. // Some options we just pass on
  443. instruments: chartSonifyOptions.instruments || configOptions.instruments,
  444. onStart: chartSonifyOptions.onSeriesStart || configOptions.onStart,
  445. onEnd: chartSonifyOptions.onSeriesEnd || configOptions.onEnd,
  446. earcons: chartSonifyOptions.earcons || configOptions.earcons,
  447. masterVolume: pick(chartSonifyOptions.masterVolume, configOptions.masterVolume)
  448. },
  449. // Merge in the specific series options by ID if any are passed in
  450. isArray(additionalSeriesOptions) ? (find(additionalSeriesOptions, function (optEntry) {
  451. return optEntry.id === pick(series.id, series.options.id);
  452. }) || {}) : additionalSeriesOptions, {
  453. // Forced options
  454. pointPlayTime: pointPlayTime
  455. });
  456. }
  457. /**
  458. * Utility function to normalize the ordering of timeline paths when sonifying
  459. * a chart.
  460. * @private
  461. * @param {string|Array<string|Highcharts.Earcon|Array<string|Highcharts.Earcon>>} orderOptions -
  462. * Order options for the sonification.
  463. * @param {Highcharts.Chart} chart - The chart we are sonifying.
  464. * @param {Function} seriesOptionsCallback
  465. * A function that takes a series as argument, and returns the series options
  466. * for that series to be used with buildTimelinePathFromSeries.
  467. * @return {Array<object|Array<object|Highcharts.TimelinePath>>} If order is
  468. * sequential, we return an array of objects to create series paths from. If
  469. * order is simultaneous we return an array of an array with the same. If there
  470. * is a custom order, we return an array of arrays of either objects (for
  471. * series) or TimelinePaths (for earcons and delays).
  472. */
  473. function buildPathOrder(orderOptions, chart, seriesOptionsCallback) {
  474. var order;
  475. if (orderOptions === 'sequential' || orderOptions === 'simultaneous') {
  476. // Just add the series from the chart
  477. order = chart.series.reduce(function (seriesList, series) {
  478. var _a;
  479. if (series.visible && ((_a = series.options.sonification) === null || _a === void 0 ? void 0 : _a.enabled) !== false) {
  480. seriesList.push({
  481. series: series,
  482. seriesOptions: seriesOptionsCallback(series)
  483. });
  484. }
  485. return seriesList;
  486. }, []);
  487. // If order is simultaneous, group all series together
  488. if (orderOptions === 'simultaneous') {
  489. order = [order];
  490. }
  491. }
  492. else {
  493. // We have a specific order, and potentially custom items - like
  494. // earcons or silent waits.
  495. order = orderOptions.reduce(function (orderList, orderDef) {
  496. // Return set of items to play simultaneously. Could be only one.
  497. var simulItems = splat(orderDef).reduce(function (items, item) {
  498. var itemObject;
  499. // Is this item a series ID?
  500. if (typeof item === 'string') {
  501. var series = chart.get(item);
  502. if (series.visible) {
  503. itemObject = {
  504. series: series,
  505. seriesOptions: seriesOptionsCallback(series)
  506. };
  507. }
  508. // Is it an earcon? If so, just create the path.
  509. }
  510. else if (item instanceof H.sonification.Earcon) {
  511. // Path with a single event
  512. itemObject = new H.sonification.TimelinePath({
  513. events: [new H.sonification.TimelineEvent({
  514. eventObject: item
  515. })]
  516. });
  517. }
  518. // Is this item a silent wait? If so, just create the path.
  519. if (item.silentWait) {
  520. itemObject = new H.sonification.TimelinePath({
  521. silentWait: item.silentWait
  522. });
  523. }
  524. // Add to items to play simultaneously
  525. if (itemObject) {
  526. items.push(itemObject);
  527. }
  528. return items;
  529. }, []);
  530. // Add to order list
  531. if (simulItems.length) {
  532. orderList.push(simulItems);
  533. }
  534. return orderList;
  535. }, []);
  536. }
  537. return order;
  538. }
  539. /**
  540. * Utility function to add a silent wait after all series.
  541. * @private
  542. * @param {Array<object|Array<object|TimelinePath>>} order
  543. * The order of items.
  544. * @param {number} wait
  545. * The wait in milliseconds to add.
  546. * @return {Array<object|Array<object|TimelinePath>>}
  547. * The order with waits inserted.
  548. */
  549. function addAfterSeriesWaits(order, wait) {
  550. if (!wait) {
  551. return order;
  552. }
  553. return order.reduce(function (newOrder, orderDef, i) {
  554. var simultaneousPaths = splat(orderDef);
  555. newOrder.push(simultaneousPaths);
  556. // Go through the simultaneous paths and see if there is a series there
  557. if (i < order.length - 1 && // Do not add wait after last series
  558. simultaneousPaths.some(function (item) {
  559. return item.series;
  560. })) {
  561. // We have a series, meaning we should add a wait after these
  562. // paths have finished.
  563. newOrder.push(new H.sonification.TimelinePath({
  564. silentWait: wait
  565. }));
  566. }
  567. return newOrder;
  568. }, []);
  569. }
  570. /**
  571. * Utility function to find the total amout of wait time in the TimelinePaths.
  572. * @private
  573. * @param {Array<object|Array<object|TimelinePath>>} order - The order of
  574. * TimelinePaths/items.
  575. * @return {number} The total time in ms spent on wait paths between playing.
  576. */
  577. function getWaitTime(order) {
  578. return order.reduce(function (waitTime, orderDef) {
  579. var def = splat(orderDef);
  580. return waitTime + (def.length === 1 &&
  581. def[0].options &&
  582. def[0].options.silentWait || 0);
  583. }, 0);
  584. }
  585. /**
  586. * Utility function to ensure simultaneous paths have start/end events at the
  587. * same time, to sync them.
  588. * @private
  589. * @param {Array<Highcharts.TimelinePath>} paths - The paths to sync.
  590. */
  591. function syncSimultaneousPaths(paths) {
  592. // Find the extremes for these paths
  593. var extremes = paths.reduce(function (extremes, path) {
  594. var events = path.events;
  595. if (events && events.length) {
  596. extremes.min = Math.min(events[0].time, extremes.min);
  597. extremes.max = Math.max(events[events.length - 1].time, extremes.max);
  598. }
  599. return extremes;
  600. }, {
  601. min: Infinity,
  602. max: -Infinity
  603. });
  604. // Go through the paths and add events to make them fit the same timespan
  605. paths.forEach(function (path) {
  606. var events = path.events, hasEvents = events && events.length, eventsToAdd = [];
  607. if (!(hasEvents && events[0].time <= extremes.min)) {
  608. eventsToAdd.push(new H.sonification.TimelineEvent({
  609. time: extremes.min
  610. }));
  611. }
  612. if (!(hasEvents && events[events.length - 1].time >= extremes.max)) {
  613. eventsToAdd.push(new H.sonification.TimelineEvent({
  614. time: extremes.max
  615. }));
  616. }
  617. if (eventsToAdd.length) {
  618. path.addTimelineEvents(eventsToAdd);
  619. }
  620. });
  621. }
  622. /**
  623. * Utility function to find the total duration span for all simul path sets
  624. * that include series.
  625. * @private
  626. * @param {Array<object|Array<object|Highcharts.TimelinePath>>} order - The
  627. * order of TimelinePaths/items.
  628. * @return {number} The total time value span difference for all series.
  629. */
  630. function getSimulPathDurationTotal(order) {
  631. return order.reduce(function (durationTotal, orderDef) {
  632. return durationTotal + splat(orderDef).reduce(function (maxPathDuration, item) {
  633. var timeExtremes = (item.series &&
  634. item.seriesOptions &&
  635. item.seriesOptions.timeExtremes);
  636. return timeExtremes ?
  637. Math.max(maxPathDuration, timeExtremes.max - timeExtremes.min) : maxPathDuration;
  638. }, 0);
  639. }, 0);
  640. }
  641. /**
  642. * Function to calculate the duration in ms for a series.
  643. * @private
  644. * @param {number} seriesValueDuration - The duration of the series in value
  645. * difference.
  646. * @param {number} totalValueDuration - The total duration of all (non
  647. * simultaneous) series in value difference.
  648. * @param {number} totalDurationMs - The desired total duration for all series
  649. * in milliseconds.
  650. * @return {number} The duration for the series in milliseconds.
  651. */
  652. function getSeriesDurationMs(seriesValueDuration, totalValueDuration, totalDurationMs) {
  653. // A series spanning the whole chart would get the full duration.
  654. return utilities.virtualAxisTranslate(seriesValueDuration, { min: 0, max: totalValueDuration }, { min: 0, max: totalDurationMs });
  655. }
  656. /**
  657. * Convert series building objects into paths and return a new list of
  658. * TimelinePaths.
  659. * @private
  660. * @param {Array<object|Array<object|Highcharts.TimelinePath>>} order - The
  661. * order list.
  662. * @param {number} duration - Total duration to aim for in milliseconds.
  663. * @return {Array<Array<Highcharts.TimelinePath>>} Array of TimelinePath objects
  664. * to play.
  665. */
  666. function buildPathsFromOrder(order, duration) {
  667. // Find time used for waits (custom or after series), and subtract it from
  668. // available duration.
  669. var totalAvailableDurationMs = Math.max(duration - getWaitTime(order), 0),
  670. // Add up simultaneous path durations to find total value span duration
  671. // of everything
  672. totalUsedDuration = getSimulPathDurationTotal(order);
  673. // Go through the order list and convert the items
  674. return order.reduce(function (allPaths, orderDef) {
  675. var simultaneousPaths = splat(orderDef).reduce(function (simulPaths, item) {
  676. if (item instanceof H.sonification.TimelinePath) {
  677. // This item is already a path object
  678. simulPaths.push(item);
  679. }
  680. else if (item.series) {
  681. // We have a series.
  682. // We need to set the duration of the series
  683. item.seriesOptions.duration =
  684. item.seriesOptions.duration || getSeriesDurationMs(item.seriesOptions.timeExtremes.max -
  685. item.seriesOptions.timeExtremes.min, totalUsedDuration, totalAvailableDurationMs);
  686. // Add the path
  687. simulPaths.push(buildTimelinePathFromSeries(item.series, item.seriesOptions));
  688. }
  689. return simulPaths;
  690. }, []);
  691. // Add in the simultaneous paths
  692. allPaths.push(simultaneousPaths);
  693. return allPaths;
  694. }, []);
  695. }
  696. /**
  697. * @private
  698. * @param {Highcharts.Series} series The series to get options for.
  699. * @param {Highcharts.SonifySeriesOptionsObject} options
  700. * Options to merge with user options on series/chart and default options.
  701. * @returns {Array<Highcharts.PointInstrumentObject>} The merged options.
  702. */
  703. function getSeriesInstrumentOptions(series, options) {
  704. var _a, _b;
  705. if (options === null || options === void 0 ? void 0 : options.instruments) {
  706. return options.instruments;
  707. }
  708. var defaultInstrOpts = ((_a = series.chart.options.sonification) === null || _a === void 0 ? void 0 : _a.defaultInstrumentOptions) || {};
  709. var seriesInstrOpts = ((_b = series.options.sonification) === null || _b === void 0 ? void 0 : _b.instruments) || [{}];
  710. var removeNullsFromObject = function (obj) {
  711. objectEach(obj, function (val, key) {
  712. if (val === null) {
  713. delete obj[key];
  714. }
  715. });
  716. };
  717. // Convert series options to PointInstrumentObjects and merge with
  718. // default options
  719. return (seriesInstrOpts).map(function (optionSet) {
  720. // Allow setting option to null to use default
  721. removeNullsFromObject(optionSet.mapping || {});
  722. removeNullsFromObject(optionSet);
  723. return {
  724. instrument: optionSet.instrument || defaultInstrOpts.instrument,
  725. instrumentOptions: merge(defaultInstrOpts, optionSet, {
  726. // Instrument options are lifted to root in the API options
  727. // object, so merge all in order to avoid missing any. But
  728. // remove the following which are not instrumentOptions:
  729. mapping: void 0,
  730. instrument: void 0
  731. }),
  732. instrumentMapping: merge(defaultInstrOpts.mapping, optionSet.mapping)
  733. };
  734. });
  735. }
  736. /**
  737. * Utility function to translate between options set in chart configuration and
  738. * a SonifySeriesOptionsObject.
  739. * @private
  740. * @param {Highcharts.Series} series The series to get options for.
  741. * @returns {Highcharts.SonifySeriesOptionsObject} Options for chart/series.sonify()
  742. */
  743. function chartOptionsToSonifySeriesOptions(series) {
  744. var _a, _b;
  745. var seriesOpts = series.options.sonification || {};
  746. var chartOpts = series.chart.options.sonification || {};
  747. var chartEvents = chartOpts.events || {};
  748. var seriesEvents = seriesOpts.events || {};
  749. return {
  750. onEnd: seriesEvents.onSeriesEnd || chartEvents.onSeriesEnd,
  751. onStart: seriesEvents.onSeriesStart || chartEvents.onSeriesStart,
  752. onPointEnd: seriesEvents.onPointEnd || chartEvents.onPointEnd,
  753. onPointStart: seriesEvents.onPointStart || chartEvents.onPointStart,
  754. pointPlayTime: (_b = (_a = chartOpts.defaultInstrumentOptions) === null || _a === void 0 ? void 0 : _a.mapping) === null || _b === void 0 ? void 0 : _b.pointPlayTime,
  755. masterVolume: chartOpts.masterVolume,
  756. instruments: getSeriesInstrumentOptions(series),
  757. earcons: seriesOpts.earcons || chartOpts.earcons
  758. };
  759. }
  760. /**
  761. * @private
  762. * @param {Highcharts.Series} series The series to get options for.
  763. * @param {Highcharts.SonifySeriesOptionsObject} options
  764. * Options to merge with user options on series/chart and default options.
  765. * @returns {Highcharts.SonifySeriesOptionsObject} The merged options.
  766. */
  767. function getSeriesSonifyOptions(series, options) {
  768. var chartOpts = series.chart.options.sonification;
  769. var seriesOpts = series.options.sonification;
  770. return merge({
  771. duration: (seriesOpts === null || seriesOpts === void 0 ? void 0 : seriesOpts.duration) || (chartOpts === null || chartOpts === void 0 ? void 0 : chartOpts.duration)
  772. }, chartOptionsToSonifySeriesOptions(series), options);
  773. }
  774. /**
  775. * @private
  776. * @param {Highcharts.Chart} chart The chart to get options for.
  777. * @param {Highcharts.SonificationOptions} options
  778. * Options to merge with user options on chart and default options.
  779. * @returns {Highcharts.SonificationOptions} The merged options.
  780. */
  781. function getChartSonifyOptions(chart, options) {
  782. var _a, _b, _c, _d, _e;
  783. var chartOpts = chart.options.sonification || {};
  784. return merge({
  785. duration: chartOpts.duration,
  786. afterSeriesWait: chartOpts.afterSeriesWait,
  787. pointPlayTime: (_b = (_a = chartOpts.defaultInstrumentOptions) === null || _a === void 0 ? void 0 : _a.mapping) === null || _b === void 0 ? void 0 : _b.pointPlayTime,
  788. order: chartOpts.order,
  789. onSeriesStart: (_c = chartOpts.events) === null || _c === void 0 ? void 0 : _c.onSeriesStart,
  790. onSeriesEnd: (_d = chartOpts.events) === null || _d === void 0 ? void 0 : _d.onSeriesEnd,
  791. onEnd: (_e = chartOpts.events) === null || _e === void 0 ? void 0 : _e.onEnd
  792. }, options);
  793. }
  794. /**
  795. * Options for sonifying a chart.
  796. *
  797. * @requires module:modules/sonification
  798. *
  799. * @interface Highcharts.SonificationOptions
  800. */ /**
  801. * Duration for sonifying the entire chart. The duration is distributed across
  802. * the different series intelligently, but does not take earcons into account.
  803. * It is also possible to set the duration explicitly per series, using
  804. * `seriesOptions`. Note that points may continue to play after the duration has
  805. * passed, but no new points will start playing.
  806. * @name Highcharts.SonificationOptions#duration
  807. * @type {number}
  808. */ /**
  809. * Define the order to play the series in. This can be given as a string, or an
  810. * array specifying a custom ordering. If given as a string, valid values are
  811. * `sequential` - where each series is played in order - or `simultaneous`,
  812. * where all series are played at once. For custom ordering, supply an array as
  813. * the order. Each element in the array can be either a string with a series ID,
  814. * an Earcon object, or an object with a numeric `silentWait` property
  815. * designating a number of milliseconds to wait before continuing. Each element
  816. * of the array will be played in order. To play elements simultaneously, group
  817. * the elements in an array.
  818. * @name Highcharts.SonificationOptions#order
  819. * @type {string|Array<string|Highcharts.Earcon|Array<string|Highcharts.Earcon>>}
  820. */ /**
  821. * The axis to use for when to play the points. Can be a string with a data
  822. * property (e.g. `x`), or a function. If it is a function, this function
  823. * receives the point as argument, and should return a numeric value. The points
  824. * with the lowest numeric values are then played first, and the time between
  825. * points will be proportional to the distance between the numeric values. This
  826. * option can not be overridden per series.
  827. * @name Highcharts.SonificationOptions#pointPlayTime
  828. * @type {string|Function}
  829. */ /**
  830. * Milliseconds of silent waiting to add between series. Note that waiting time
  831. * is considered part of the sonify duration.
  832. * @name Highcharts.SonificationOptions#afterSeriesWait
  833. * @type {number|undefined}
  834. */ /**
  835. * Options as given to `series.sonify` to override options per series. If the
  836. * option is supplied as an array of options objects, the `id` property of the
  837. * object should correspond to the series' id. If the option is supplied as a
  838. * single object, the options apply to all series.
  839. * @name Highcharts.SonificationOptions#seriesOptions
  840. * @type {Object|Array<object>|undefined}
  841. */ /**
  842. * The instrument definitions for the points in this chart.
  843. * @name Highcharts.SonificationOptions#instruments
  844. * @type {Array<Highcharts.PointInstrumentObject>|undefined}
  845. */ /**
  846. * Earcons to add to the chart. Note that earcons can also be added per series
  847. * using `seriesOptions`.
  848. * @name Highcharts.SonificationOptions#earcons
  849. * @type {Array<Highcharts.EarconConfiguration>|undefined}
  850. */ /**
  851. * Optionally provide the minimum/maximum data values for the points. If this is
  852. * not supplied, it is calculated from all points in the chart on demand. This
  853. * option is supplied in the following format, as a map of point data properties
  854. * to objects with min/max values:
  855. * ```js
  856. * dataExtremes: {
  857. * y: {
  858. * min: 0,
  859. * max: 100
  860. * },
  861. * z: {
  862. * min: -10,
  863. * max: 10
  864. * }
  865. * // Properties used and not provided are calculated on demand
  866. * }
  867. * ```
  868. * @name Highcharts.SonificationOptions#dataExtremes
  869. * @type {Highcharts.Dictionary<Highcharts.RangeObject>|undefined}
  870. */ /**
  871. * Callback before a series is played.
  872. * @name Highcharts.SonificationOptions#onSeriesStart
  873. * @type {Function|undefined}
  874. */ /**
  875. * Callback after a series has finished playing.
  876. * @name Highcharts.SonificationOptions#onSeriesEnd
  877. * @type {Function|undefined}
  878. */ /**
  879. * Callback after the chart has played.
  880. * @name Highcharts.SonificationOptions#onEnd
  881. * @type {Function|undefined}
  882. */
  883. /**
  884. * Sonify a chart.
  885. *
  886. * @sample highcharts/sonification/chart-sequential/
  887. * Sonify a basic chart
  888. * @sample highcharts/sonification/chart-simultaneous/
  889. * Sonify series simultaneously
  890. * @sample highcharts/sonification/chart-custom-order/
  891. * Custom defined order of series
  892. * @sample highcharts/sonification/chart-earcon/
  893. * Earcons on chart
  894. * @sample highcharts/sonification/chart-events/
  895. * Sonification events on chart
  896. *
  897. * @requires module:modules/sonification
  898. *
  899. * @function Highcharts.Chart#sonify
  900. *
  901. * @param {Highcharts.SonificationOptions} [options]
  902. * The options for sonifying this chart. If not provided,
  903. * uses options set on chart and series.
  904. *
  905. * @return {void}
  906. */
  907. function chartSonify(options) {
  908. var opts = getChartSonifyOptions(this, options);
  909. // Only one timeline can play at a time.
  910. if (this.sonification.timeline) {
  911. this.sonification.timeline.pause();
  912. }
  913. // Store reference to duration
  914. this.sonification.duration = opts.duration;
  915. // Calculate data extremes for the props used
  916. var dataExtremes = getExtremesForInstrumentProps(this, opts.instruments, opts.dataExtremes);
  917. // Figure out ordering of series and custom paths
  918. var order = buildPathOrder(opts.order, this, function (series) {
  919. return buildChartSonifySeriesOptions(series, dataExtremes, opts);
  920. });
  921. // Add waits after simultaneous paths with series in them.
  922. order = addAfterSeriesWaits(order, opts.afterSeriesWait || 0);
  923. // We now have a list of either TimelinePath objects or series that need to
  924. // be converted to TimelinePath objects. Convert everything to paths.
  925. var paths = buildPathsFromOrder(order, opts.duration);
  926. // Sync simultaneous paths
  927. paths.forEach(function (simultaneousPaths) {
  928. syncSimultaneousPaths(simultaneousPaths);
  929. });
  930. // We have a set of paths. Create the timeline, and play it.
  931. this.sonification.timeline = new H.sonification.Timeline({
  932. paths: paths,
  933. onEnd: opts.onEnd
  934. });
  935. this.sonification.timeline.play();
  936. }
  937. /**
  938. * Get a list of the points currently under cursor.
  939. *
  940. * @requires module:modules/sonification
  941. *
  942. * @function Highcharts.Chart#getCurrentSonifyPoints
  943. *
  944. * @return {Array<Highcharts.Point>}
  945. * The points currently under the cursor.
  946. */
  947. function getCurrentPoints() {
  948. var cursorObj;
  949. if (this.sonification.timeline) {
  950. cursorObj = this.sonification.timeline.getCursor(); // Cursor per pathID
  951. return Object.keys(cursorObj).map(function (path) {
  952. // Get the event objects under cursor for each path
  953. return cursorObj[path].eventObject;
  954. }).filter(function (eventObj) {
  955. // Return the events that are points
  956. return eventObj instanceof Point;
  957. });
  958. }
  959. return [];
  960. }
  961. /**
  962. * Set the cursor to a point or set of points in different series.
  963. *
  964. * @requires module:modules/sonification
  965. *
  966. * @function Highcharts.Chart#setSonifyCursor
  967. *
  968. * @param {Highcharts.Point|Array<Highcharts.Point>} points
  969. * The point or points to set the cursor to. If setting multiple points
  970. * under the cursor, the points have to be in different series that are
  971. * being played simultaneously.
  972. */
  973. function setCursor(points) {
  974. var timeline = this.sonification.timeline;
  975. if (timeline) {
  976. splat(points).forEach(function (point) {
  977. // We created the events with the ID of the points, which makes
  978. // this easy. Just call setCursor for each ID.
  979. timeline.setCursor(point.id);
  980. });
  981. }
  982. }
  983. /**
  984. * Pause the running sonification.
  985. *
  986. * @requires module:modules/sonification
  987. *
  988. * @function Highcharts.Chart#pauseSonify
  989. *
  990. * @param {boolean} [fadeOut=true]
  991. * Fade out as we pause to avoid clicks.
  992. *
  993. * @return {void}
  994. */
  995. function pause(fadeOut) {
  996. if (this.sonification.timeline) {
  997. this.sonification.timeline.pause(pick(fadeOut, true));
  998. }
  999. else if (this.sonification.currentlyPlayingPoint) {
  1000. this.sonification.currentlyPlayingPoint.cancelSonify(fadeOut);
  1001. }
  1002. }
  1003. /**
  1004. * Resume the currently running sonification. Requires series.sonify or
  1005. * chart.sonify to have been played at some point earlier.
  1006. *
  1007. * @requires module:modules/sonification
  1008. *
  1009. * @function Highcharts.Chart#resumeSonify
  1010. *
  1011. * @param {Function} onEnd
  1012. * Callback to call when play finished.
  1013. *
  1014. * @return {void}
  1015. */
  1016. function resume(onEnd) {
  1017. if (this.sonification.timeline) {
  1018. this.sonification.timeline.play(onEnd);
  1019. }
  1020. }
  1021. /**
  1022. * Play backwards from cursor. Requires series.sonify or chart.sonify to have
  1023. * been played at some point earlier.
  1024. *
  1025. * @requires module:modules/sonification
  1026. *
  1027. * @function Highcharts.Chart#rewindSonify
  1028. *
  1029. * @param {Function} onEnd
  1030. * Callback to call when play finished.
  1031. *
  1032. * @return {void}
  1033. */
  1034. function rewind(onEnd) {
  1035. if (this.sonification.timeline) {
  1036. this.sonification.timeline.rewind(onEnd);
  1037. }
  1038. }
  1039. /**
  1040. * Cancel current sonification and reset cursor.
  1041. *
  1042. * @requires module:modules/sonification
  1043. *
  1044. * @function Highcharts.Chart#cancelSonify
  1045. *
  1046. * @param {boolean} [fadeOut=true]
  1047. * Fade out as we pause to avoid clicks.
  1048. *
  1049. * @return {void}
  1050. */
  1051. function cancel(fadeOut) {
  1052. this.pauseSonify(fadeOut);
  1053. this.resetSonifyCursor();
  1054. }
  1055. /**
  1056. * Reset cursor to start. Requires series.sonify or chart.sonify to have been
  1057. * played at some point earlier.
  1058. *
  1059. * @requires module:modules/sonification
  1060. *
  1061. * @function Highcharts.Chart#resetSonifyCursor
  1062. *
  1063. * @return {void}
  1064. */
  1065. function resetCursor() {
  1066. if (this.sonification.timeline) {
  1067. this.sonification.timeline.resetCursor();
  1068. }
  1069. }
  1070. /**
  1071. * Reset cursor to end. Requires series.sonify or chart.sonify to have been
  1072. * played at some point earlier.
  1073. *
  1074. * @requires module:modules/sonification
  1075. *
  1076. * @function Highcharts.Chart#resetSonifyCursorEnd
  1077. *
  1078. * @return {void}
  1079. */
  1080. function resetCursorEnd() {
  1081. if (this.sonification.timeline) {
  1082. this.sonification.timeline.resetCursorEnd();
  1083. }
  1084. }
  1085. // Export functions
  1086. var chartSonifyFunctions = {
  1087. chartSonify: chartSonify,
  1088. seriesSonify: seriesSonify,
  1089. pause: pause,
  1090. resume: resume,
  1091. rewind: rewind,
  1092. cancel: cancel,
  1093. getCurrentPoints: getCurrentPoints,
  1094. setCursor: setCursor,
  1095. resetCursor: resetCursor,
  1096. resetCursorEnd: resetCursorEnd
  1097. };
  1098. export default chartSonifyFunctions;