pointSonify.js 14.2 KB
/* *
 *
 *  (c) 2009-2019 Øystein Moseng
 *
 *  Code for sonifying single points.
 *
 *  License: www.highcharts.com/license
 *
 * */


/**
 * Define the parameter mapping for an instrument.
 *
 * @requires module:modules/sonification
 *
 * @interface Highcharts.PointInstrumentMappingObject
 *//**
 * Define the volume of the instrument. This can be a string with a data
 * property name, e.g. `'y'`, in which case this data property is used to define
 * the volume relative to the `y`-values of the other points. A higher `y` value
 * would then result in a higher volume. This option can also be a fixed number
 * or a function. If it is a function, this function is called in regular
 * intervals while the note is playing. It receives three arguments: The point,
 * the dataExtremes, and the current relative time - where 0 is the beginning of
 * the note and 1 is the end. The function should return the volume of the note
 * as a number between 0 and 1.
 * @name Highcharts.PointInstrumentMappingObject#volume
 * @type {string|number|Function}
 *//**
 * Define the duration of the notes for this instrument. This can be a string
 * with a data property name, e.g. `'y'`, in which case this data property is
 * used to define the duration relative to the `y`-values of the other points. A
 * higher `y` value would then result in a longer duration. This option can also
 * be a fixed number or a function. If it is a function, this function is called
 * once before the note starts playing, and should return the duration in
 * milliseconds. It receives two arguments: The point, and the dataExtremes.
 * @name Highcharts.PointInstrumentMappingObject#duration
 * @type {string|number|Function}
 *//**
 * Define the panning of the instrument. This can be a string with a data
 * property name, e.g. `'x'`, in which case this data property is used to define
 * the panning relative to the `x`-values of the other points. A higher `x`
 * value would then result in a higher panning value (panned further to the
 * right). This option can also be a fixed number or a function. If it is a
 * function, this function is called in regular intervals while the note is
 * playing. It receives three arguments: The point, the dataExtremes, and the
 * current relative time - where 0 is the beginning of the note and 1 is the
 * end. The function should return the panning of the note as a number between
 * -1 and 1.
 * @name Highcharts.PointInstrumentMappingObject#pan
 * @type {string|number|Function|undefined}
 *//**
 * Define the frequency of the instrument. This can be a string with a data
 * property name, e.g. `'y'`, in which case this data property is used to define
 * the frequency relative to the `y`-values of the other points. A higher `y`
 * value would then result in a higher frequency. This option can also be a
 * fixed number or a function. If it is a function, this function is called in
 * regular intervals while the note is playing. It receives three arguments:
 * The point, the dataExtremes, and the current relative time - where 0 is the
 * beginning of the note and 1 is the end. The function should return the
 * frequency of the note as a number (in Hz).
 * @name Highcharts.PointInstrumentMappingObject#frequency
 * @type {string|number|Function}
 */


/**
 * @requires module:modules/sonification
 *
 * @interface Highcharts.PointInstrumentOptionsObject
 *//**
 * The minimum duration for a note when using a data property for duration. Can
 * be overridden by using either a fixed number or a function for
 * instrumentMapping.duration. Defaults to 20.
 * @name Highcharts.PointInstrumentOptionsObject#minDuration
 * @type {number|undefined}
 *//**
 * The maximum duration for a note when using a data property for duration. Can
 * be overridden by using either a fixed number or a function for
 * instrumentMapping.duration. Defaults to 2000.
 * @name Highcharts.PointInstrumentOptionsObject#maxDuration
 * @type {number|undefined}
 *//**
 * The minimum pan value for a note when using a data property for panning. Can
 * be overridden by using either a fixed number or a function for
 * instrumentMapping.pan. Defaults to -1 (fully left).
 * @name Highcharts.PointInstrumentOptionsObject#minPan
 * @type {number|undefined}
 *//**
 * The maximum pan value for a note when using a data property for panning. Can
 * be overridden by using either a fixed number or a function for
 * instrumentMapping.pan. Defaults to 1 (fully right).
 * @name Highcharts.PointInstrumentOptionsObject#maxPan
 * @type {number|undefined}
 *//**
 * The minimum volume for a note when using a data property for volume. Can be
 * overridden by using either a fixed number or a function for
 * instrumentMapping.volume. Defaults to 0.1.
 * @name Highcharts.PointInstrumentOptionsObject#minVolume
 * @type {number|undefined}
 *//**
 * The maximum volume for a note when using a data property for volume. Can be
 * overridden by using either a fixed number or a function for
 * instrumentMapping.volume. Defaults to 1.
 * @name Highcharts.PointInstrumentOptionsObject#maxVolume
 * @type {number|undefined}
 *//**
 * The minimum frequency for a note when using a data property for frequency.
 * Can be overridden by using either a fixed number or a function for
 * instrumentMapping.frequency. Defaults to 220.
 * @name Highcharts.PointInstrumentOptionsObject#minFrequency
 * @type {number|undefined}
 *//**
 * The maximum frequency for a note when using a data property for frequency.
 * Can be overridden by using either a fixed number or a function for
 * instrumentMapping.frequency. Defaults to 2200.
 * @name Highcharts.PointInstrumentOptionsObject#maxFrequency
 * @type {number|undefined}
 */


/**
 * An instrument definition for a point, specifying the instrument to play and
 * how to play it.
 *
 * @interface Highcharts.PointInstrumentObject
 *//**
 * An Instrument instance or the name of the instrument in the
 * Highcharts.sonification.instruments map.
 * @name Highcharts.PointInstrumentObject#instrument
 * @type {Highcharts.Instrument|string}
 *//**
 * Mapping of instrument parameters for this instrument.
 * @name Highcharts.PointInstrumentObject#instrumentMapping
 * @type {Highcharts.PointInstrumentMappingObject}
 *//**
 * Options for this instrument.
 * @name Highcharts.PointInstrumentObject#instrumentOptions
 * @type {Highcharts.PointInstrumentOptionsObject|undefined}
 *//**
 * Callback to call when the instrument has stopped playing.
 * @name Highcharts.PointInstrumentObject#onEnd
 * @type {Function|undefined}
 */


/**
 * Options for sonifying a point.
 * @interface Highcharts.PointSonifyOptionsObject
 *//**
 * The instrument definitions for this point.
 * @name Highcharts.PointSonifyOptionsObject#instruments
 * @type {Array<Highcharts.PointInstrumentObject>}
 *//**
 * Optionally provide the minimum/maximum values for the points. If this is not
 * supplied, it is calculated from the points in the chart on demand. This
 * option is supplied in the following format, as a map of point data properties
 * to objects with min/max values:
 *  ```js
 *      dataExtremes: {
 *          y: {
 *              min: 0,
 *              max: 100
 *          },
 *          z: {
 *              min: -10,
 *              max: 10
 *          }
 *          // Properties used and not provided are calculated on demand
 *      }
 *  ```
 * @name Highcharts.PointSonifyOptionsObject#dataExtremes
 * @type {object|undefined}
 *//**
 * Callback called when the sonification has finished.
 * @name Highcharts.PointSonifyOptionsObject#onEnd
 * @type {Function|undefined}
 */


'use strict';

import H from '../../parts/Globals.js';
import utilities from 'utilities.js';

// Defaults for the instrument options
// NOTE: Also change defaults in Highcharts.PointInstrumentOptionsObject if
//       making changes here.
var defaultInstrumentOptions = {
    minDuration: 20,
    maxDuration: 2000,
    minVolume: 0.1,
    maxVolume: 1,
    minPan: -1,
    maxPan: 1,
    minFrequency: 220,
    maxFrequency: 2200
};


/**
 * Sonify a single point.
 *
 * @sample highcharts/sonification/point-basic/
 *         Click on points to sonify
 * @sample highcharts/sonification/point-advanced/
 *         Sonify bubbles
 *
 * @requires module:modules/sonification
 *
 * @function Highcharts.Point#sonify
 *
 * @param {Highcharts.PointSonifyOptionsObject} options
 *        Options for the sonification of the point.
 */
function pointSonify(options) {
    var point = this,
        chart = point.series.chart,
        dataExtremes = options.dataExtremes || {},
        // Get the value to pass to instrument.play from the mapping value
        // passed in.
        getMappingValue = function (
            value, makeFunction, allowedExtremes, allowedValues
        ) {
            // Fixed number, just use that
            if (typeof value === 'number' || value === undefined) {
                return value;
            }
            // Function. Return new function if we try to use callback,
            // otherwise call it now and return result.
            if (typeof value === 'function') {
                return makeFunction ?
                    function (time) {
                        return value(point, dataExtremes, time);
                    } :
                    value(point, dataExtremes);
            }
            // String, this is a data prop.
            if (typeof value === 'string') {
                // Find data extremes if we don't have them
                dataExtremes[value] = dataExtremes[value] ||
                    utilities.calculateDataExtremes(
                        point.series.chart, value
                    );
                // Find the value
                return utilities.virtualAxisTranslate(
                    H.pick(point[value], point.options[value]),
                    dataExtremes[value],
                    allowedExtremes,
                    allowedValues
                );
            }
        };

    // Register playing point on chart
    chart.sonification.currentlyPlayingPoint = point;

    // Keep track of instruments playing
    point.sonification = point.sonification || {};
    point.sonification.instrumentsPlaying =
        point.sonification.instrumentsPlaying || {};

    // Register signal handler for the point
    var signalHandler = point.sonification.signalHandler =
        point.sonification.signalHandler ||
        new utilities.SignalHandler(['onEnd']);

    signalHandler.clearSignalCallbacks();
    signalHandler.registerSignalCallbacks({ onEnd: options.onEnd });

    // If we have a null point or invisible point, just return
    if (point.isNull || !point.visible || !point.series.visible) {
        signalHandler.emitSignal('onEnd');
        return;
    }

    // Go through instruments and play them
    options.instruments.forEach(function (instrumentDefinition) {
        var instrument = typeof instrumentDefinition.instrument === 'string' ?
                H.sonification.instruments[instrumentDefinition.instrument] :
                instrumentDefinition.instrument,
            mapping = instrumentDefinition.instrumentMapping || {},
            extremes = H.merge(
                defaultInstrumentOptions,
                instrumentDefinition.instrumentOptions
            ),
            id = instrument.id,
            onEnd = function (cancelled) {
                // Instrument on end
                if (instrumentDefinition.onEnd) {
                    instrumentDefinition.onEnd.apply(this, arguments);
                }

                // Remove currently playing point reference on chart
                if (
                    chart.sonification &&
                    chart.sonification.currentlyPlayingPoint
                ) {
                    delete chart.sonification.currentlyPlayingPoint;
                }

                // Remove reference from instruments playing
                if (
                    point.sonification && point.sonification.instrumentsPlaying
                ) {
                    delete point.sonification.instrumentsPlaying[id];

                    // This was the last instrument?
                    if (
                        !Object.keys(
                            point.sonification.instrumentsPlaying
                        ).length
                    ) {
                        signalHandler.emitSignal('onEnd', cancelled);
                    }
                }
            };

        // Play the note on the instrument
        if (instrument && instrument.play) {
            point.sonification.instrumentsPlaying[instrument.id] = instrument;
            instrument.play({
                frequency: getMappingValue(
                    mapping.frequency,
                    true,
                    { min: extremes.minFrequency, max: extremes.maxFrequency }
                ),
                duration: getMappingValue(
                    mapping.duration,
                    false,
                    { min: extremes.minDuration, max: extremes.maxDuration }
                ),
                pan: getMappingValue(
                    mapping.pan,
                    true,
                    { min: extremes.minPan, max: extremes.maxPan }
                ),
                volume: getMappingValue(
                    mapping.volume,
                    true,
                    { min: extremes.minVolume, max: extremes.maxVolume }
                ),
                onEnd: onEnd,
                minFrequency: extremes.minFrequency,
                maxFrequency: extremes.maxFrequency
            });
        } else {
            H.error(30);
        }
    });
}


/**
 * Cancel sonification of a point. Calls onEnd functions.
 *
 * @requires module:modules/sonification
 *
 * @function Highcharts.Point#cancelSonify
 *
 * @param {boolean} [fadeOut=false]
 *        Whether or not to fade out as we stop. If false, the points are
 *        cancelled synchronously.
 */
function pointCancelSonify(fadeOut) {
    var playing = this.sonification && this.sonification.instrumentsPlaying,
        instrIds = playing && Object.keys(playing);

    if (instrIds && instrIds.length) {
        instrIds.forEach(function (instr) {
            playing[instr].stop(!fadeOut, null, 'cancelled');
        });
        this.sonification.instrumentsPlaying = {};
        this.sonification.signalHandler.emitSignal('onEnd', 'cancelled');
    }
}


var pointSonifyFunctions = {
    pointSonify: pointSonify,
    pointCancelSonify: pointCancelSonify
};

export default pointSonifyFunctions;