chartSonify.js 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910
  1. /* *
  2. *
  3. * (c) 2009-2019 Ø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 '../../parts/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. import U from '../../parts/Utilities.js';
  97. var isArray = U.isArray, pick = U.pick, splat = U.splat;
  98. import utilities from './utilities.js';
  99. /**
  100. * Get the relative time value of a point.
  101. * @private
  102. * @param {Highcharts.Point} point
  103. * The point.
  104. * @param {Function|string} timeProp
  105. * The time axis data prop or the time function.
  106. * @return {number}
  107. * The time value.
  108. */
  109. function getPointTimeValue(point, timeProp) {
  110. return typeof timeProp === 'function' ?
  111. timeProp(point) :
  112. pick(point[timeProp], point.options[timeProp]);
  113. }
  114. /**
  115. * Get the time extremes of this series. This is handled outside of the
  116. * dataExtremes, as we always want to just sonify the visible points, and we
  117. * always want the extremes to be the extremes of the visible points.
  118. * @private
  119. * @param {Highcharts.Series} series
  120. * The series to compute on.
  121. * @param {Function|string} timeProp
  122. * The time axis data prop or the time function.
  123. * @return {Highcharts.RangeObject}
  124. * Object with min/max extremes for the time values.
  125. */
  126. function getTimeExtremes(series, timeProp) {
  127. // Compute the extremes from the visible points.
  128. return series.points.reduce(function (acc, point) {
  129. var value = getPointTimeValue(point, timeProp);
  130. acc.min = Math.min(acc.min, value);
  131. acc.max = Math.max(acc.max, value);
  132. return acc;
  133. }, {
  134. min: Infinity,
  135. max: -Infinity
  136. });
  137. }
  138. /**
  139. * Calculate value extremes for used instrument data properties.
  140. * @private
  141. * @param {Highcharts.Chart} chart
  142. * The chart to calculate extremes from.
  143. * @param {Array<Highcharts.PointInstrumentObject>} instruments
  144. * The instrument definitions used.
  145. * @param {Highcharts.Dictionary<Highcharts.RangeObject>} [dataExtremes]
  146. * Predefined extremes for each data prop.
  147. * @return {Highcharts.Dictionary<Highcharts.RangeObject>}
  148. * New extremes with data properties mapped to min/max objects.
  149. */
  150. function getExtremesForInstrumentProps(chart, instruments, dataExtremes) {
  151. return (instruments || []).reduce(function (newExtremes, instrumentDefinition) {
  152. Object.keys(instrumentDefinition.instrumentMapping || {}).forEach(function (instrumentParameter) {
  153. var value = instrumentDefinition.instrumentMapping[instrumentParameter];
  154. if (typeof value === 'string' && !newExtremes[value]) {
  155. // This instrument parameter is mapped to a data prop.
  156. // If we don't have predefined data extremes, find them.
  157. newExtremes[value] = utilities.calculateDataExtremes(chart, value);
  158. }
  159. });
  160. return newExtremes;
  161. }, H.merge(dataExtremes));
  162. }
  163. /**
  164. * Get earcons for the point if there are any.
  165. * @private
  166. * @param {Highcharts.Point} point
  167. * The point to find earcons for.
  168. * @param {Array<Highcharts.EarconConfiguration>} earconDefinitions
  169. * Earcons to check.
  170. * @return {Array<Highcharts.Earcon>}
  171. * Array of earcons to be played with this point.
  172. */
  173. function getPointEarcons(point, earconDefinitions) {
  174. return earconDefinitions.reduce(function (earcons, earconDefinition) {
  175. var cond, earcon = earconDefinition.earcon;
  176. if (earconDefinition.condition) {
  177. // We have a condition. This overrides onPoint
  178. cond = earconDefinition.condition(point);
  179. if (cond instanceof H.sonification.Earcon) {
  180. // Condition returned an earcon
  181. earcons.push(cond);
  182. }
  183. else if (cond) {
  184. // Condition returned true
  185. earcons.push(earcon);
  186. }
  187. }
  188. else if (earconDefinition.onPoint &&
  189. point.id === earconDefinition.onPoint) {
  190. // We have earcon onPoint
  191. earcons.push(earcon);
  192. }
  193. return earcons;
  194. }, []);
  195. }
  196. /**
  197. * Utility function to get a new list of instrument options where all the
  198. * instrument references are copies.
  199. * @private
  200. * @param {Array<Highcharts.PointInstrumentObject>} instruments
  201. * The instrument options.
  202. * @return {Array<Highcharts.PointInstrumentObject>}
  203. * Array of copied instrument options.
  204. */
  205. function makeInstrumentCopies(instruments) {
  206. return instruments.map(function (instrumentDef) {
  207. var instrument = instrumentDef.instrument, copy = (typeof instrument === 'string' ?
  208. H.sonification.instruments[instrument] :
  209. instrument).copy();
  210. return H.merge(instrumentDef, { instrument: copy });
  211. });
  212. }
  213. /**
  214. * Create a TimelinePath from a series. Takes the same options as seriesSonify.
  215. * To intuitively allow multiple series to play simultaneously we make copies of
  216. * the instruments for each series.
  217. * @private
  218. * @param {Highcharts.Series} series
  219. * The series to build from.
  220. * @param {Highcharts.SonifySeriesOptionsObject} options
  221. * The options for building the TimelinePath.
  222. * @return {Highcharts.TimelinePath}
  223. * A timeline path with events.
  224. */
  225. function buildTimelinePathFromSeries(series, options) {
  226. // options.timeExtremes is internal and used so that the calculations from
  227. // chart.sonify can be reused.
  228. var timeExtremes = options.timeExtremes || getTimeExtremes(series, options.pointPlayTime, options.dataExtremes),
  229. // Get time offset for a point, relative to duration
  230. pointToTime = function (point) {
  231. return utilities.virtualAxisTranslate(getPointTimeValue(point, options.pointPlayTime), timeExtremes, { min: 0, max: options.duration });
  232. },
  233. // Compute any data extremes that aren't defined yet
  234. dataExtremes = getExtremesForInstrumentProps(series.chart, options.instruments, options.dataExtremes),
  235. // Make copies of the instruments used for this series, to allow
  236. // multiple series with the same instrument to play together
  237. instruments = makeInstrumentCopies(options.instruments),
  238. // Go through the points, convert to events, optionally add Earcons
  239. timelineEvents = series.points.reduce(function (events, point) {
  240. var earcons = getPointEarcons(point, options.earcons || []), time = pointToTime(point);
  241. return events.concat(
  242. // Event object for point
  243. new H.sonification.TimelineEvent({
  244. eventObject: point,
  245. time: time,
  246. id: point.id,
  247. playOptions: {
  248. instruments: instruments,
  249. dataExtremes: dataExtremes
  250. }
  251. }),
  252. // Earcons
  253. earcons.map(function (earcon) {
  254. return new H.sonification.TimelineEvent({
  255. eventObject: earcon,
  256. time: time
  257. });
  258. }));
  259. }, []);
  260. // Build the timeline path
  261. return new H.sonification.TimelinePath({
  262. events: timelineEvents,
  263. onStart: function () {
  264. if (options.onStart) {
  265. options.onStart(series);
  266. }
  267. },
  268. onEventStart: function (event) {
  269. var eventObject = event.options && event.options.eventObject;
  270. if (eventObject instanceof H.Point) {
  271. // Check for hidden series
  272. if (!eventObject.series.visible &&
  273. !eventObject.series.chart.series.some(function (series) {
  274. return series.visible;
  275. })) {
  276. // We have no visible series, stop the path.
  277. event.timelinePath.timeline.pause();
  278. event.timelinePath.timeline.resetCursor();
  279. return false;
  280. }
  281. // Emit onPointStart
  282. if (options.onPointStart) {
  283. options.onPointStart(event, eventObject);
  284. }
  285. }
  286. },
  287. onEventEnd: function (eventData) {
  288. var eventObject = eventData.event && eventData.event.options &&
  289. eventData.event.options.eventObject;
  290. if (eventObject instanceof H.Point && options.onPointEnd) {
  291. options.onPointEnd(eventData.event, eventObject);
  292. }
  293. },
  294. onEnd: function () {
  295. if (options.onEnd) {
  296. options.onEnd(series);
  297. }
  298. }
  299. });
  300. }
  301. /* eslint-disable no-invalid-this, valid-jsdoc */
  302. /**
  303. * Sonify a series.
  304. *
  305. * @sample highcharts/sonification/series-basic/
  306. * Click on series to sonify
  307. * @sample highcharts/sonification/series-earcon/
  308. * Series with earcon
  309. * @sample highcharts/sonification/point-play-time/
  310. * Play y-axis by time
  311. * @sample highcharts/sonification/earcon-on-point/
  312. * Earcon set on point
  313. *
  314. * @requires module:modules/sonification
  315. *
  316. * @function Highcharts.Series#sonify
  317. *
  318. * @param {Highcharts.SonifySeriesOptionsObject} options
  319. * The options for sonifying this series.
  320. *
  321. * @return {void}
  322. */
  323. function seriesSonify(options) {
  324. var timelinePath = buildTimelinePathFromSeries(this, options), chartSonification = this.chart.sonification;
  325. // Only one timeline can play at a time. If we want multiple series playing
  326. // at the same time, use chart.sonify.
  327. if (chartSonification.timeline) {
  328. chartSonification.timeline.pause();
  329. }
  330. // Create new timeline for this series, and play it.
  331. chartSonification.timeline = new H.sonification.Timeline({
  332. paths: [timelinePath]
  333. });
  334. chartSonification.timeline.play();
  335. }
  336. /**
  337. * Utility function to assemble options for creating a TimelinePath from a
  338. * series when sonifying an entire chart.
  339. * @private
  340. * @param {Highcharts.Series} series
  341. * The series to return options for.
  342. * @param {Highcharts.RangeObject} dataExtremes
  343. * Pre-calculated data extremes for the chart.
  344. * @param {Highcharts.SonifyChartOptionsObject} chartSonifyOptions
  345. * Options passed in to chart.sonify.
  346. * @return {Partial<Highcharts.SonifySeriesOptionsObject>}
  347. * Options for buildTimelinePathFromSeries.
  348. */
  349. function buildSeriesOptions(series, dataExtremes, chartSonifyOptions) {
  350. var seriesOptions = chartSonifyOptions.seriesOptions || {};
  351. return H.merge({
  352. // Calculated dataExtremes for chart
  353. dataExtremes: dataExtremes,
  354. // We need to get timeExtremes for each series. We pass this
  355. // in when building the TimelinePath objects to avoid
  356. // calculating twice.
  357. timeExtremes: getTimeExtremes(series, chartSonifyOptions.pointPlayTime),
  358. // Some options we just pass on
  359. instruments: chartSonifyOptions.instruments,
  360. onStart: chartSonifyOptions.onSeriesStart,
  361. onEnd: chartSonifyOptions.onSeriesEnd,
  362. earcons: chartSonifyOptions.earcons
  363. },
  364. // Merge in the specific series options by ID
  365. isArray(seriesOptions) ? (H.find(seriesOptions, function (optEntry) {
  366. return optEntry.id === pick(series.id, series.options.id);
  367. }) || {}) : seriesOptions, {
  368. // Forced options
  369. pointPlayTime: chartSonifyOptions.pointPlayTime
  370. });
  371. }
  372. /**
  373. * Utility function to normalize the ordering of timeline paths when sonifying
  374. * a chart.
  375. * @private
  376. * @param {string|Array<string|Highcharts.Earcon|Array<string|Highcharts.Earcon>>} orderOptions -
  377. * Order options for the sonification.
  378. * @param {Highcharts.Chart} chart - The chart we are sonifying.
  379. * @param {Function} seriesOptionsCallback
  380. * A function that takes a series as argument, and returns the series options
  381. * for that series to be used with buildTimelinePathFromSeries.
  382. * @return {Array<object|Array<object|Highcharts.TimelinePath>>} If order is
  383. * sequential, we return an array of objects to create series paths from. If
  384. * order is simultaneous we return an array of an array with the same. If there
  385. * is a custom order, we return an array of arrays of either objects (for
  386. * series) or TimelinePaths (for earcons and delays).
  387. */
  388. function buildPathOrder(orderOptions, chart, seriesOptionsCallback) {
  389. var order;
  390. if (orderOptions === 'sequential' || orderOptions === 'simultaneous') {
  391. // Just add the series from the chart
  392. order = chart.series.reduce(function (seriesList, series) {
  393. if (series.visible) {
  394. seriesList.push({
  395. series: series,
  396. seriesOptions: seriesOptionsCallback(series)
  397. });
  398. }
  399. return seriesList;
  400. }, []);
  401. // If order is simultaneous, group all series together
  402. if (orderOptions === 'simultaneous') {
  403. order = [order];
  404. }
  405. }
  406. else {
  407. // We have a specific order, and potentially custom items - like
  408. // earcons or silent waits.
  409. order = orderOptions.reduce(function (orderList, orderDef) {
  410. // Return set of items to play simultaneously. Could be only one.
  411. var simulItems = splat(orderDef).reduce(function (items, item) {
  412. var itemObject;
  413. // Is this item a series ID?
  414. if (typeof item === 'string') {
  415. var series = chart.get(item);
  416. if (series.visible) {
  417. itemObject = {
  418. series: series,
  419. seriesOptions: seriesOptionsCallback(series)
  420. };
  421. }
  422. // Is it an earcon? If so, just create the path.
  423. }
  424. else if (item instanceof H.sonification.Earcon) {
  425. // Path with a single event
  426. itemObject = new H.sonification.TimelinePath({
  427. events: [new H.sonification.TimelineEvent({
  428. eventObject: item
  429. })]
  430. });
  431. }
  432. // Is this item a silent wait? If so, just create the path.
  433. if (item.silentWait) {
  434. itemObject = new H.sonification.TimelinePath({
  435. silentWait: item.silentWait
  436. });
  437. }
  438. // Add to items to play simultaneously
  439. if (itemObject) {
  440. items.push(itemObject);
  441. }
  442. return items;
  443. }, []);
  444. // Add to order list
  445. if (simulItems.length) {
  446. orderList.push(simulItems);
  447. }
  448. return orderList;
  449. }, []);
  450. }
  451. return order;
  452. }
  453. /**
  454. * Utility function to add a silent wait after all series.
  455. * @private
  456. * @param {Array<object|Array<object|TimelinePath>>} order
  457. * The order of items.
  458. * @param {number} wait
  459. * The wait in milliseconds to add.
  460. * @return {Array<object|Array<object|TimelinePath>>}
  461. * The order with waits inserted.
  462. */
  463. function addAfterSeriesWaits(order, wait) {
  464. if (!wait) {
  465. return order;
  466. }
  467. return order.reduce(function (newOrder, orderDef, i) {
  468. var simultaneousPaths = splat(orderDef);
  469. newOrder.push(simultaneousPaths);
  470. // Go through the simultaneous paths and see if there is a series there
  471. if (i < order.length - 1 && // Do not add wait after last series
  472. simultaneousPaths.some(function (item) {
  473. return item.series;
  474. })) {
  475. // We have a series, meaning we should add a wait after these
  476. // paths have finished.
  477. newOrder.push(new H.sonification.TimelinePath({
  478. silentWait: wait
  479. }));
  480. }
  481. return newOrder;
  482. }, []);
  483. }
  484. /**
  485. * Utility function to find the total amout of wait time in the TimelinePaths.
  486. * @private
  487. * @param {Array<object|Array<object|TimelinePath>>} order - The order of
  488. * TimelinePaths/items.
  489. * @return {number} The total time in ms spent on wait paths between playing.
  490. */
  491. function getWaitTime(order) {
  492. return order.reduce(function (waitTime, orderDef) {
  493. var def = splat(orderDef);
  494. return waitTime + (def.length === 1 &&
  495. def[0].options &&
  496. def[0].options.silentWait || 0);
  497. }, 0);
  498. }
  499. /**
  500. * Utility function to ensure simultaneous paths have start/end events at the
  501. * same time, to sync them.
  502. * @private
  503. * @param {Array<Highcharts.TimelinePath>} paths - The paths to sync.
  504. */
  505. function syncSimultaneousPaths(paths) {
  506. // Find the extremes for these paths
  507. var extremes = paths.reduce(function (extremes, path) {
  508. var events = path.events;
  509. if (events && events.length) {
  510. extremes.min = Math.min(events[0].time, extremes.min);
  511. extremes.max = Math.max(events[events.length - 1].time, extremes.max);
  512. }
  513. return extremes;
  514. }, {
  515. min: Infinity,
  516. max: -Infinity
  517. });
  518. // Go through the paths and add events to make them fit the same timespan
  519. paths.forEach(function (path) {
  520. var events = path.events, hasEvents = events && events.length, eventsToAdd = [];
  521. if (!(hasEvents && events[0].time <= extremes.min)) {
  522. eventsToAdd.push(new H.sonification.TimelineEvent({
  523. time: extremes.min
  524. }));
  525. }
  526. if (!(hasEvents && events[events.length - 1].time >= extremes.max)) {
  527. eventsToAdd.push(new H.sonification.TimelineEvent({
  528. time: extremes.max
  529. }));
  530. }
  531. if (eventsToAdd.length) {
  532. path.addTimelineEvents(eventsToAdd);
  533. }
  534. });
  535. }
  536. /**
  537. * Utility function to find the total duration span for all simul path sets
  538. * that include series.
  539. * @private
  540. * @param {Array<object|Array<object|Highcharts.TimelinePath>>} order - The
  541. * order of TimelinePaths/items.
  542. * @return {number} The total time value span difference for all series.
  543. */
  544. function getSimulPathDurationTotal(order) {
  545. return order.reduce(function (durationTotal, orderDef) {
  546. return durationTotal + splat(orderDef).reduce(function (maxPathDuration, item) {
  547. var timeExtremes = (item.series &&
  548. item.seriesOptions &&
  549. item.seriesOptions.timeExtremes);
  550. return timeExtremes ?
  551. Math.max(maxPathDuration, timeExtremes.max - timeExtremes.min) : maxPathDuration;
  552. }, 0);
  553. }, 0);
  554. }
  555. /**
  556. * Function to calculate the duration in ms for a series.
  557. * @private
  558. * @param {number} seriesValueDuration - The duration of the series in value
  559. * difference.
  560. * @param {number} totalValueDuration - The total duration of all (non
  561. * simultaneous) series in value difference.
  562. * @param {number} totalDurationMs - The desired total duration for all series
  563. * in milliseconds.
  564. * @return {number} The duration for the series in milliseconds.
  565. */
  566. function getSeriesDurationMs(seriesValueDuration, totalValueDuration, totalDurationMs) {
  567. // A series spanning the whole chart would get the full duration.
  568. return utilities.virtualAxisTranslate(seriesValueDuration, { min: 0, max: totalValueDuration }, { min: 0, max: totalDurationMs });
  569. }
  570. /**
  571. * Convert series building objects into paths and return a new list of
  572. * TimelinePaths.
  573. * @private
  574. * @param {Array<object|Array<object|Highcharts.TimelinePath>>} order - The
  575. * order list.
  576. * @param {number} duration - Total duration to aim for in milliseconds.
  577. * @return {Array<Array<Highcharts.TimelinePath>>} Array of TimelinePath objects
  578. * to play.
  579. */
  580. function buildPathsFromOrder(order, duration) {
  581. // Find time used for waits (custom or after series), and subtract it from
  582. // available duration.
  583. var totalAvailableDurationMs = Math.max(duration - getWaitTime(order), 0),
  584. // Add up simultaneous path durations to find total value span duration
  585. // of everything
  586. totalUsedDuration = getSimulPathDurationTotal(order);
  587. // Go through the order list and convert the items
  588. return order.reduce(function (allPaths, orderDef) {
  589. var simultaneousPaths = splat(orderDef).reduce(function (simulPaths, item) {
  590. if (item instanceof H.sonification.TimelinePath) {
  591. // This item is already a path object
  592. simulPaths.push(item);
  593. }
  594. else if (item.series) {
  595. // We have a series.
  596. // We need to set the duration of the series
  597. item.seriesOptions.duration =
  598. item.seriesOptions.duration || getSeriesDurationMs(item.seriesOptions.timeExtremes.max -
  599. item.seriesOptions.timeExtremes.min, totalUsedDuration, totalAvailableDurationMs);
  600. // Add the path
  601. simulPaths.push(buildTimelinePathFromSeries(item.series, item.seriesOptions));
  602. }
  603. return simulPaths;
  604. }, []);
  605. // Add in the simultaneous paths
  606. allPaths.push(simultaneousPaths);
  607. return allPaths;
  608. }, []);
  609. }
  610. /**
  611. * Options for sonifying a chart.
  612. *
  613. * @requires module:modules/sonification
  614. *
  615. * @interface Highcharts.SonifyChartOptionsObject
  616. */ /**
  617. * Duration for sonifying the entire chart. The duration is distributed across
  618. * the different series intelligently, but does not take earcons into account.
  619. * It is also possible to set the duration explicitly per series, using
  620. * `seriesOptions`. Note that points may continue to play after the duration has
  621. * passed, but no new points will start playing.
  622. * @name Highcharts.SonifyChartOptionsObject#duration
  623. * @type {number}
  624. */ /**
  625. * Define the order to play the series in. This can be given as a string, or an
  626. * array specifying a custom ordering. If given as a string, valid values are
  627. * `sequential` - where each series is played in order - or `simultaneous`,
  628. * where all series are played at once. For custom ordering, supply an array as
  629. * the order. Each element in the array can be either a string with a series ID,
  630. * an Earcon object, or an object with a numeric `silentWait` property
  631. * designating a number of milliseconds to wait before continuing. Each element
  632. * of the array will be played in order. To play elements simultaneously, group
  633. * the elements in an array.
  634. * @name Highcharts.SonifyChartOptionsObject#order
  635. * @type {string|Array<string|Highcharts.Earcon|Array<string|Highcharts.Earcon>>}
  636. */ /**
  637. * The axis to use for when to play the points. Can be a string with a data
  638. * property (e.g. `x`), or a function. If it is a function, this function
  639. * receives the point as argument, and should return a numeric value. The points
  640. * with the lowest numeric values are then played first, and the time between
  641. * points will be proportional to the distance between the numeric values. This
  642. * option can not be overridden per series.
  643. * @name Highcharts.SonifyChartOptionsObject#pointPlayTime
  644. * @type {string|Function}
  645. */ /**
  646. * Milliseconds of silent waiting to add between series. Note that waiting time
  647. * is considered part of the sonify duration.
  648. * @name Highcharts.SonifyChartOptionsObject#afterSeriesWait
  649. * @type {number|undefined}
  650. */ /**
  651. * Options as given to `series.sonify` to override options per series. If the
  652. * option is supplied as an array of options objects, the `id` property of the
  653. * object should correspond to the series' id. If the option is supplied as a
  654. * single object, the options apply to all series.
  655. * @name Highcharts.SonifyChartOptionsObject#seriesOptions
  656. * @type {Object|Array<object>|undefined}
  657. */ /**
  658. * The instrument definitions for the points in this chart.
  659. * @name Highcharts.SonifyChartOptionsObject#instruments
  660. * @type {Array<Highcharts.PointInstrumentObject>|undefined}
  661. */ /**
  662. * Earcons to add to the chart. Note that earcons can also be added per series
  663. * using `seriesOptions`.
  664. * @name Highcharts.SonifyChartOptionsObject#earcons
  665. * @type {Array<Highcharts.EarconConfiguration>|undefined}
  666. */ /**
  667. * Optionally provide the minimum/maximum data values for the points. If this is
  668. * not supplied, it is calculated from all points in the chart on demand. This
  669. * option is supplied in the following format, as a map of point data properties
  670. * to objects with min/max values:
  671. * ```js
  672. * dataExtremes: {
  673. * y: {
  674. * min: 0,
  675. * max: 100
  676. * },
  677. * z: {
  678. * min: -10,
  679. * max: 10
  680. * }
  681. * // Properties used and not provided are calculated on demand
  682. * }
  683. * ```
  684. * @name Highcharts.SonifyChartOptionsObject#dataExtremes
  685. * @type {Highcharts.Dictionary<Highcharts.RangeObject>|undefined}
  686. */ /**
  687. * Callback before a series is played.
  688. * @name Highcharts.SonifyChartOptionsObject#onSeriesStart
  689. * @type {Function|undefined}
  690. */ /**
  691. * Callback after a series has finished playing.
  692. * @name Highcharts.SonifyChartOptionsObject#onSeriesEnd
  693. * @type {Function|undefined}
  694. */ /**
  695. * Callback after the chart has played.
  696. * @name Highcharts.SonifyChartOptionsObject#onEnd
  697. * @type {Function|undefined}
  698. */
  699. /**
  700. * Sonify a chart.
  701. *
  702. * @sample highcharts/sonification/chart-sequential/
  703. * Sonify a basic chart
  704. * @sample highcharts/sonification/chart-simultaneous/
  705. * Sonify series simultaneously
  706. * @sample highcharts/sonification/chart-custom-order/
  707. * Custom defined order of series
  708. * @sample highcharts/sonification/chart-earcon/
  709. * Earcons on chart
  710. * @sample highcharts/sonification/chart-events/
  711. * Sonification events on chart
  712. *
  713. * @requires module:modules/sonification
  714. *
  715. * @function Highcharts.Chart#sonify
  716. *
  717. * @param {Highcharts.SonifyChartOptionsObject} options
  718. * The options for sonifying this chart.
  719. *
  720. * @return {void}
  721. */
  722. function chartSonify(options) {
  723. // Only one timeline can play at a time.
  724. if (this.sonification.timeline) {
  725. this.sonification.timeline.pause();
  726. }
  727. // Calculate data extremes for the props used
  728. var dataExtremes = getExtremesForInstrumentProps(this, options.instruments, options.dataExtremes);
  729. // Figure out ordering of series and custom paths
  730. var order = buildPathOrder(options.order, this, function (series) {
  731. return buildSeriesOptions(series, dataExtremes, options);
  732. });
  733. // Add waits after simultaneous paths with series in them.
  734. order = addAfterSeriesWaits(order, options.afterSeriesWait || 0);
  735. // We now have a list of either TimelinePath objects or series that need to
  736. // be converted to TimelinePath objects. Convert everything to paths.
  737. var paths = buildPathsFromOrder(order, options.duration);
  738. // Sync simultaneous paths
  739. paths.forEach(function (simultaneousPaths) {
  740. syncSimultaneousPaths(simultaneousPaths);
  741. });
  742. // We have a set of paths. Create the timeline, and play it.
  743. this.sonification.timeline = new H.sonification.Timeline({
  744. paths: paths,
  745. onEnd: options.onEnd
  746. });
  747. this.sonification.timeline.play();
  748. }
  749. /**
  750. * Get a list of the points currently under cursor.
  751. *
  752. * @requires module:modules/sonification
  753. *
  754. * @function Highcharts.Chart#getCurrentSonifyPoints
  755. *
  756. * @return {Array<Highcharts.Point>}
  757. * The points currently under the cursor.
  758. */
  759. function getCurrentPoints() {
  760. var cursorObj;
  761. if (this.sonification.timeline) {
  762. cursorObj = this.sonification.timeline.getCursor(); // Cursor per pathID
  763. return Object.keys(cursorObj).map(function (path) {
  764. // Get the event objects under cursor for each path
  765. return cursorObj[path].eventObject;
  766. }).filter(function (eventObj) {
  767. // Return the events that are points
  768. return eventObj instanceof H.Point;
  769. });
  770. }
  771. return [];
  772. }
  773. /**
  774. * Set the cursor to a point or set of points in different series.
  775. *
  776. * @requires module:modules/sonification
  777. *
  778. * @function Highcharts.Chart#setSonifyCursor
  779. *
  780. * @param {Highcharts.Point|Array<Highcharts.Point>} points
  781. * The point or points to set the cursor to. If setting multiple points
  782. * under the cursor, the points have to be in different series that are
  783. * being played simultaneously.
  784. */
  785. function setCursor(points) {
  786. var timeline = this.sonification.timeline;
  787. if (timeline) {
  788. splat(points).forEach(function (point) {
  789. // We created the events with the ID of the points, which makes
  790. // this easy. Just call setCursor for each ID.
  791. timeline.setCursor(point.id);
  792. });
  793. }
  794. }
  795. /**
  796. * Pause the running sonification.
  797. *
  798. * @requires module:modules/sonification
  799. *
  800. * @function Highcharts.Chart#pauseSonify
  801. *
  802. * @param {boolean} [fadeOut=true]
  803. * Fade out as we pause to avoid clicks.
  804. *
  805. * @return {void}
  806. */
  807. function pause(fadeOut) {
  808. if (this.sonification.timeline) {
  809. this.sonification.timeline.pause(pick(fadeOut, true));
  810. }
  811. else if (this.sonification.currentlyPlayingPoint) {
  812. this.sonification.currentlyPlayingPoint.cancelSonify(fadeOut);
  813. }
  814. }
  815. /**
  816. * Resume the currently running sonification. Requires series.sonify or
  817. * chart.sonify to have been played at some point earlier.
  818. *
  819. * @requires module:modules/sonification
  820. *
  821. * @function Highcharts.Chart#resumeSonify
  822. *
  823. * @param {Function} onEnd
  824. * Callback to call when play finished.
  825. *
  826. * @return {void}
  827. */
  828. function resume(onEnd) {
  829. if (this.sonification.timeline) {
  830. this.sonification.timeline.play(onEnd);
  831. }
  832. }
  833. /**
  834. * Play backwards from cursor. Requires series.sonify or chart.sonify to have
  835. * been played at some point earlier.
  836. *
  837. * @requires module:modules/sonification
  838. *
  839. * @function Highcharts.Chart#rewindSonify
  840. *
  841. * @param {Function} onEnd
  842. * Callback to call when play finished.
  843. *
  844. * @return {void}
  845. */
  846. function rewind(onEnd) {
  847. if (this.sonification.timeline) {
  848. this.sonification.timeline.rewind(onEnd);
  849. }
  850. }
  851. /**
  852. * Cancel current sonification and reset cursor.
  853. *
  854. * @requires module:modules/sonification
  855. *
  856. * @function Highcharts.Chart#cancelSonify
  857. *
  858. * @param {boolean} [fadeOut=true]
  859. * Fade out as we pause to avoid clicks.
  860. *
  861. * @return {void}
  862. */
  863. function cancel(fadeOut) {
  864. this.pauseSonify(fadeOut);
  865. this.resetSonifyCursor();
  866. }
  867. /**
  868. * Reset cursor to start. Requires series.sonify or chart.sonify to have been
  869. * played at some point earlier.
  870. *
  871. * @requires module:modules/sonification
  872. *
  873. * @function Highcharts.Chart#resetSonifyCursor
  874. *
  875. * @return {void}
  876. */
  877. function resetCursor() {
  878. if (this.sonification.timeline) {
  879. this.sonification.timeline.resetCursor();
  880. }
  881. }
  882. /**
  883. * Reset cursor to end. Requires series.sonify or chart.sonify to have been
  884. * played at some point earlier.
  885. *
  886. * @requires module:modules/sonification
  887. *
  888. * @function Highcharts.Chart#resetSonifyCursorEnd
  889. *
  890. * @return {void}
  891. */
  892. function resetCursorEnd() {
  893. if (this.sonification.timeline) {
  894. this.sonification.timeline.resetCursorEnd();
  895. }
  896. }
  897. // Export functions
  898. var chartSonifyFunctions = {
  899. chartSonify: chartSonify,
  900. seriesSonify: seriesSonify,
  901. pause: pause,
  902. resume: resume,
  903. rewind: rewind,
  904. cancel: cancel,
  905. getCurrentPoints: getCurrentPoints,
  906. setCursor: setCursor,
  907. resetCursor: resetCursor,
  908. resetCursorEnd: resetCursorEnd
  909. };
  910. export default chartSonifyFunctions;