chartSonify.js 34 KB

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