all files / src/ AudioChart.js

94.44% Statements 51/54
93.55% Branches 29/31
87.5% Functions 7/8
94.44% Lines 51/54
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249                                                                                                                                                                                                                        15×     14×     13×     12×       10×                                                                                                       13×           13×             13×         13×       11× 11× 11×       11×             13×      
/** @module */
/* exported AudioChart */
/* global getAudioContext FrequencyPitchMapper Sounder Player KeyboardHandler GoogleDataWrapper googleVisualCallbackMaker JSONDataWrapper HTMLTableDataWrapper htmlTableVisualCallbackMaker C3DataWrapper c3VisualCallbackMaker */
 
/**
 * Array index number (starts at zero).
 * Used to specify series and row in visual callbacks.
 * @typedef {integer} index
 */
 
 
/**
 * A function that highlights the current datum, in all series, visually.
 * Different callbacks must be created for different types of chart.
 * @callback VisualCallback
 * @param {index} row - The row of the cell to highlight
 */
 
/**
 * @typedef {Object} AudioChartOptions
 * @todo move the documentation here? (Downside is that the branching/groups
 *       of different options required for different chart types would be less
 *       clear.)
 */
 
/**
 * @typedef {Object} WrapperAndCallbackResults
 * @property {Class} WrapperClass - the data wrapper class
 * @property {Object|HTMLTableElement} dataSource - the data source to wrap
 * @property {VisualCallback} visualCallback - created if requested by the user
 */
 
/** Main object for API consumers */
class AudioChart {
	/**
	 * Create an AudioChart object.
	 * This first checks to see if the Web Audio API is available, and throws
	 * an {Error} if not. Then check the options given by the user.
	 * @param {AudioChartOptions} options - AudioChart options
	 */
	constructor(options) {
		const context = getAudioContext()
		if (context === null) {
			throw Error(
				"Sorry, your browser doesn't support the Web Audio API.")
		}
		this._setUp(context, options)
	}
 
	/**
	 * Passes through play/pause commands to the Player
	 */
	playPause() {
		this.player.playPause()
	}
 
	/**
	 * Returns the current set of options (passed in at object creation, or
	 * computed when options were updated).
	 */
	get options() {
		return this._options
	}
 
	/**
	 * Updates an AudioChart object to reflect new options. Can accept a subset
	 * of the standard options, so if only, for example, duration changes, then
	 * you need only specify the new duration and not the type and other
	 * paramaters.
	 * @param {AudioChartOptions} newOptions - Partial/full AudioChart options
	 */
	updateOptions(newOptions) {
		if (newOptions === undefined || Object.keys(newOptions).length === 0) {
			throw Error('No new options given')
		}
		const patchedOptions = Object.assign({}, this._options, newOptions)
		this._setUp(getAudioContext(), patchedOptions)
	}
 
	/**
	 * Checks options (either when a new AudioChart object is created, or when
	 * the user has asked for them to be updated) and then set everything up.
	 * @param {AudioContext} context - the Web Audio context
	 * @param {AudioChartOptions} options - AudioChart options
	 * @private
	 */
	_setUp(context, options) {
		// The testing for this next bit is a bit of a fudge as curerntly I've
		// not come up with a beter way than having the testing done on static
		// functions and checking that they've been called with appropraite
		// values, return appropriate values, or if they throw an exception.
		//
		// The thing blocking this is that I don't know how to stub out global
		// ES6 classes/functions *or* how to run each test via Karma in an
		// isolated environment where I can mock those global classes.
		//
		// TODO, as per https://github.com/matatk/audiochart/issues/37
 
		// Testing this separately allows us to check the options-checking code
		// without having to pass in functioning source data objects.
		AudioChart._checkOptions(options)
 
		// This is not currently tested; to mitigate, it doesn't make decisions.
		// Actually, it does call _assignWrapperCallback() but that /is/ tested.
		this._wireUpStuff(context, options)
 
		// Re _assignWrapperCallback(): that is also tested separately to avoid
		// the need for very detailed mocks for the data sources.
 
		this._options = Object.freeze(options)
	}
 
	/**
	 * Checks the passed-in options opbject for validity. This does not perform
	 * detailed checks that are covered in the various components' constructors;
	 * they run such checks themselves.
	 * @param {AudioChartOptions} options - AudioChart options
	 * @private
	 */
	static _checkOptions(options) {
		if (!options.hasOwnProperty('duration')) {
			throw Error('No duration given')
		}
 
		if (!options.hasOwnProperty('frequencyLow')) {
			throw Error('No minimum frequency given')
		}
 
		if (!options.hasOwnProperty('frequencyHigh')) {
			throw Error('No maximum frequency given')
		}
 
		switch (options.type) {
			case 'google':
			case 'c3':
			case 'json':
				if (!options.hasOwnProperty('data')) {
					throw Error("Options must include a 'data' key")
				}
				break
			case 'htmlTable':
				Eif (!options.hasOwnProperty('table')) {
					throw Error("Options must include a 'table' key")
				}
				break
			default:
				throw Error(`Invalid data type '${options.type}' given.`)
		}
	}
 
	/**
	 * Make a data wrapper of the appropriate class, instantiated with the
	 * appropriate data paramater (from options.data) and, optionally, make
	 * a visual callback (which may use options.chart, or other options if
	 * it's an HTML table visual callback).
	 * FIXME: Test somehow
	 * @private
	 * @param {AudioContext} context - the Web Audio context
	 * @param {AudioChartOptions} options - given by the user
	 */
	_wireUpStuff(context, options) {
		const assigned = AudioChart._assignWrapperCallback(options)
		const dataWrapper = new assigned.WrapperClass(assigned.dataSource)
		const callback = assigned.visualCallback
 
		const numberOfSeries = dataWrapper.numSeries()
 
		const seriesInfo = []
		for (let i = 0; i < numberOfSeries; i++ ) {
			seriesInfo.push({
				minimumDatum: dataWrapper.seriesMin(i),
				maximumDatum: dataWrapper.seriesMax(i),
				minimumFrequency: options.frequencyLow,
				maximumFrequency: options.frequencyHigh
			})
		}
 
		const frequencyPitchMapper = new FrequencyPitchMapper(seriesInfo)
 
		const sounder = new Sounder(context, numberOfSeries)
 
		this.player = new Player(
			options.duration,
			dataWrapper,
			frequencyPitchMapper,
			sounder,
			callback)
 
		Iif (options.chartContainer) {
			new KeyboardHandler(
				options.chartContainer,
				this.player)
		}
	}
 
	/**
	 * Works out which data source wrapper and visual callback (if requested)
	 * should be used with this chart.
	 * @param {AudioChartOptions} options - given by the user
	 * @returns {WrapperAndCallbackResults}
	 *	- data wrapper, data source and callback (if applicable) for this chart
	 * @private
	 */
	static _assignWrapperCallback(options) {
		const result = {
			'WrapperClass': null,
			'dataSource': null,
			'visualCallback': null
		}
 
		const chartTypeToWrapperClass = {
			google: GoogleDataWrapper,
			json: JSONDataWrapper,
			htmlTable: HTMLTableDataWrapper,
			c3: C3DataWrapper
		}
 
		const chartTypeToVisualCallbackMaker = {
			google: googleVisualCallbackMaker,
			c3: c3VisualCallbackMaker
		}
 
		switch (options.type) {
			case 'google':
			case 'json':
			case 'c3':
				result.WrapperClass = chartTypeToWrapperClass[options.type]
				result.dataSource = options.data
				if (options.hasOwnProperty('chart')) {
					result.visualCallback =
						chartTypeToVisualCallbackMaker[options.type](
							options.chart)
				}
				break
			case 'htmlTable':
				result.WrapperClass = HTMLTableDataWrapper
				result.dataSource = options.table
				if (options.hasOwnProperty('highlightClass')) {
					result.visualCallback = htmlTableVisualCallbackMaker(
						options.table,
						options.highlightClass)
				}
				break
		}
 
		return result
	}
}