chartSonify.js 34 KB

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