diff --git a/src/data/ExperimentHandler.js b/src/data/ExperimentHandler.js index bc50cd8..3bae7f2 100644 --- a/src/data/ExperimentHandler.js +++ b/src/data/ExperimentHandler.js @@ -7,12 +7,10 @@ * @license Distributed under the terms of the MIT License */ - -import * as XLSX from 'xlsx'; -import {PsychObject} from '../util/PsychObject.js'; -import {MonotonicClock} from '../util/Clock.js'; -import * as util from '../util/Util.js'; - +import * as XLSX from "xlsx"; +import { MonotonicClock } from "../util/Clock.js"; +import { PsychObject } from "../util/PsychObject.js"; +import * as util from "../util/Util.js"; /** *

An ExperimentHandler keeps track of multiple loops and handlers. It is particularly useful @@ -29,7 +27,6 @@ import * as util from '../util/Util.js'; */ export class ExperimentHandler extends PsychObject { - /** * Getter for experimentEnded. * @@ -54,7 +51,6 @@ export class ExperimentHandler extends PsychObject this._experimentEnded = ended; } - /** * Legacy experiment getters. */ @@ -68,16 +64,15 @@ export class ExperimentHandler extends PsychObject return this._trialsData; } - constructor({ - psychoJS, - name, - extraInfo - } = {}) + psychoJS, + name, + extraInfo, + } = {}) { super(psychoJS, name); - this._addAttribute('extraInfo', extraInfo); + this._addAttribute("extraInfo", extraInfo); // loop handlers: this._loops = []; @@ -91,7 +86,6 @@ export class ExperimentHandler extends PsychObject this._experimentEnded = false; } - /** * Whether or not the current entry (i.e. trial data) is empty. *

Note: this is mostly useful at the end of an experiment, in order to ensure that the last entry is saved.

@@ -106,7 +100,6 @@ export class ExperimentHandler extends PsychObject return (Object.keys(this._currentTrialData).length > 0); } - /** * Add a loop. * @@ -125,7 +118,6 @@ export class ExperimentHandler extends PsychObject loop.experimentHandler = this; } - /** * Remove the given loop from the list of unfinished loops, e.g. when it has completed. * @@ -143,7 +135,6 @@ export class ExperimentHandler extends PsychObject } } - /** * Add the key/value pair. * @@ -172,7 +163,6 @@ export class ExperimentHandler extends PsychObject this._currentTrialData[key] = value; } - /** * Inform this ExperimentHandler that the current trial has ended. Further calls to {@link addData} * will be associated with the next trial. @@ -184,7 +174,7 @@ export class ExperimentHandler extends PsychObject */ nextEntry(snapshots) { - if (typeof snapshots !== 'undefined') + if (typeof snapshots !== "undefined") { // turn single snapshot into a one-element array: if (!Array.isArray(snapshots)) @@ -203,7 +193,6 @@ export class ExperimentHandler extends PsychObject } } } - } // this is to support legacy generated JavaScript code and does not properly handle // loops within loops: @@ -236,7 +225,6 @@ export class ExperimentHandler extends PsychObject this._currentTrialData = {}; } - /** * Save the results of the experiment. * @@ -254,11 +242,11 @@ export class ExperimentHandler extends PsychObject * @param {Array.} [options.sync] - whether or not to communicate with the server in a synchronous manner */ async save({ - attributes = [], - sync = false - } = {}) + attributes = [], + sync = false, + } = {}) { - this._psychoJS.logger.info('[PsychoJS] Save experiment results.'); + this._psychoJS.logger.info("[PsychoJS] Save experiment results."); // (*) get attributes: if (attributes.length === 0) @@ -286,16 +274,14 @@ export class ExperimentHandler extends PsychObject } } - // (*) get various experiment info: const info = this.extraInfo; - const __experimentName = (typeof info.expName !== 'undefined') ? info.expName : this.psychoJS.config.experiment.name; - const __participant = ((typeof info.participant === 'string' && info.participant.length > 0) ? info.participant : 'PARTICIPANT'); - const __session = ((typeof info.session === 'string' && info.session.length > 0) ? info.session : 'SESSION'); - const __datetime = ((typeof info.date !== 'undefined') ? info.date : MonotonicClock.getDateStr()); + const __experimentName = (typeof info.expName !== "undefined") ? info.expName : this.psychoJS.config.experiment.name; + const __participant = ((typeof info.participant === "string" && info.participant.length > 0) ? info.participant : "PARTICIPANT"); + const __session = ((typeof info.session === "string" && info.session.length > 0) ? info.session : "SESSION"); + const __datetime = ((typeof info.date !== "undefined") ? info.date : MonotonicClock.getDateStr()); const gitlabConfig = this._psychoJS.config.gitlab; - const __projectId = (typeof gitlabConfig !== 'undefined' && typeof gitlabConfig.projectId !== 'undefined') ? gitlabConfig.projectId : undefined; - + const __projectId = (typeof gitlabConfig !== "undefined" && typeof gitlabConfig.projectId !== "undefined") ? gitlabConfig.projectId : undefined; // (*) save to a .csv file: if (this._psychoJS.config.experiment.saveFormat === ExperimentHandler.SaveFormat.CSV) @@ -304,23 +290,23 @@ export class ExperimentHandler extends PsychObject // newlines, etc. const worksheet = XLSX.utils.json_to_sheet(this._trialsData); // prepend BOM - const csv = '\ufeff' + XLSX.utils.sheet_to_csv(worksheet); + const csv = "\ufeff" + XLSX.utils.sheet_to_csv(worksheet); // upload data to the pavlovia server or offer them for download: - const key = __participant + '_' + __experimentName + '_' + __datetime + '.csv'; - if (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER && - this._psychoJS.config.experiment.status === 'RUNNING' && - !this._psychoJS._serverMsg.has('__pilotToken')) + const key = __participant + "_" + __experimentName + "_" + __datetime + ".csv"; + if ( + this._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER + && this._psychoJS.config.experiment.status === "RUNNING" + && !this._psychoJS._serverMsg.has("__pilotToken") + ) { return /*await*/ this._psychoJS.serverManager.uploadData(key, csv, sync); } else { - util.offerDataForDownload(key, csv, 'text/csv'); + util.offerDataForDownload(key, csv, "text/csv"); } } - - // (*) save in the database on the remote server: else if (this._psychoJS.config.experiment.saveFormat === ExperimentHandler.SaveFormat.DATABASE) { @@ -328,7 +314,7 @@ export class ExperimentHandler extends PsychObject for (let r = 0; r < this._trialsData.length; r++) { - let doc = {__projectId, __experimentName, __participant, __session, __datetime}; + let doc = { __projectId, __experimentName, __participant, __session, __datetime }; for (let h = 0; h < attributes.length; h++) { doc[attributes[h]] = this._trialsData[r][attributes[h]]; @@ -338,22 +324,22 @@ export class ExperimentHandler extends PsychObject } // upload data to the pavlovia server or offer them for download: - if (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER && - this._psychoJS.config.experiment.status === 'RUNNING' && - !this._psychoJS._serverMsg.has('__pilotToken')) + if ( + this._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER + && this._psychoJS.config.experiment.status === "RUNNING" + && !this._psychoJS._serverMsg.has("__pilotToken") + ) { - const key = 'results'; // name of the mongoDB collection + const key = "results"; // name of the mongoDB collection return /*await*/ this._psychoJS.serverManager.uploadData(key, JSON.stringify(documents), sync); } else { - util.offerDataForDownload('results.json', JSON.stringify(documents), 'application/json'); + util.offerDataForDownload("results.json", JSON.stringify(documents), "application/json"); } - } } - /** * Get the attribute names and values for the current trial of a given loop. *

Only info relating to the trial execution are returned.

@@ -367,20 +353,20 @@ export class ExperimentHandler extends PsychObject static _getLoopAttributes(loop) { // standard trial attributes: - const properties = ['thisRepN', 'thisTrialN', 'thisN', 'thisIndex', 'stepSizeCurrent', 'ran', 'order']; + const properties = ["thisRepN", "thisTrialN", "thisN", "thisIndex", "stepSizeCurrent", "ran", "order"]; let attributes = {}; const loopName = loop.name; for (const loopProperty in loop) { if (properties.includes(loopProperty)) { - const key = (loopProperty === 'stepSizeCurrent') ? loopName + '.stepSize' : loopName + '.' + loopProperty; + const key = (loopProperty === "stepSizeCurrent") ? loopName + ".stepSize" : loopName + "." + loopProperty; attributes[key] = loop[loopProperty]; } } // specific trial attributes: - if (typeof loop.getCurrentTrial === 'function') + if (typeof loop.getCurrentTrial === "function") { const currentTrial = loop.getCurrentTrial(); for (const trialProperty in currentTrial) @@ -404,7 +390,7 @@ export class ExperimentHandler extends PsychObject else: names.append(loopName+'.thisTrial') vals.append(trial) - + // single StairHandler elif hasattr(loop, 'intensities'): names.append(loopName+'.intensity') @@ -415,10 +401,8 @@ export class ExperimentHandler extends PsychObject return attributes; } - } - /** * Experiment result format * @@ -431,15 +415,14 @@ ExperimentHandler.SaveFormat = { /** * Results are saved to a .csv file */ - CSV: Symbol.for('CSV'), + CSV: Symbol.for("CSV"), /** * Results are saved to a database */ - DATABASE: Symbol.for('DATABASE') + DATABASE: Symbol.for("DATABASE"), }; - /** * Experiment environment. * @@ -448,6 +431,6 @@ ExperimentHandler.SaveFormat = { * @public */ ExperimentHandler.Environment = { - SERVER: Symbol.for('SERVER'), - LOCAL: Symbol.for('LOCAL') + SERVER: Symbol.for("SERVER"), + LOCAL: Symbol.for("LOCAL"), }; diff --git a/src/data/TrialHandler.js b/src/data/TrialHandler.js index e2fc982..098afa7 100644 --- a/src/data/TrialHandler.js +++ b/src/data/TrialHandler.js @@ -9,11 +9,10 @@ * @license Distributed under the terms of the MIT License */ - -import seedrandom from 'seedrandom'; -import * as XLSX from 'xlsx'; -import {PsychObject} from '../util/PsychObject.js'; -import * as util from '../util/Util.js'; +import seedrandom from "seedrandom"; +import * as XLSX from "xlsx"; +import { PsychObject } from "../util/PsychObject.js"; +import * as util from "../util/Util.js"; /** *

A Trial Handler handles the importing and sequencing of conditions.

@@ -31,7 +30,6 @@ import * as util from '../util/Util.js'; */ export class TrialHandler extends PsychObject { - /** * Getter for experimentHandler. * @@ -56,7 +54,6 @@ export class TrialHandler extends PsychObject this._experimentHandler = exp; } - /** * @constructor * @public @@ -64,27 +61,27 @@ export class TrialHandler extends PsychObject * @todo extraInfo is not taken into account, we use the expInfo of the ExperimentHandler instead */ constructor({ - psychoJS, - trialList = [undefined], - nReps, - method = TrialHandler.Method.RANDOM, - extraInfo = [], - seed, - name, - autoLog = true - } = {}) + psychoJS, + trialList = [undefined], + nReps, + method = TrialHandler.Method.RANDOM, + extraInfo = [], + seed, + name, + autoLog = true, + } = {}) { super(psychoJS); - this._addAttribute('trialList', trialList); - this._addAttribute('nReps', nReps); - this._addAttribute('method', method); - this._addAttribute('extraInfo', extraInfo); - this._addAttribute('name', name); - this._addAttribute('autoLog', autoLog); - this._addAttribute('seed', seed); + this._addAttribute("trialList", trialList); + this._addAttribute("nReps", nReps); + this._addAttribute("method", method); + this._addAttribute("extraInfo", extraInfo); + this._addAttribute("name", name); + this._addAttribute("autoLog", autoLog); + this._addAttribute("seed", seed); this._prepareTrialList(trialList); - + // number of stimuli this.nStim = this.trialList.length; @@ -112,7 +109,6 @@ export class TrialHandler extends PsychObject // array of current snapshots: this._snapshots = []; - // setup the trial sequence: this._prepareSequence(); @@ -121,18 +117,17 @@ export class TrialHandler extends PsychObject this._finished = false; } - /** * Helps go through each trial in the sequence one by one, mirrors PsychoPy. */ - next() { + next() + { const trialIterator = this[Symbol.iterator](); const { value } = trialIterator.next(); return value; } - /** * Iterator over the trial sequence. * @@ -168,7 +163,7 @@ export class TrialHandler extends PsychObject if (this.thisRepN >= this.nReps) { this.thisTrial = null; - return {done: true}; + return { done: true }; } this.thisIndex = this._trialSequence[this.thisRepN][this.thisTrialN]; @@ -181,12 +176,11 @@ export class TrialHandler extends PsychObject vals = (self.thisRepN, self.thisTrialN, self.thisTrial) logging.exp(msg % vals, obj=self.thisTrial)*/ - return {value: this.thisTrial, done: false}; - } + return { value: this.thisTrial, done: false }; + }, }; } - /** * Execute the callback for each trial in the sequence. * @@ -208,7 +202,6 @@ export class TrialHandler extends PsychObject } } - /** * @typedef {Object} Snapshot * @property {TrialHandler} handler - the trialHandler @@ -253,12 +246,12 @@ export class TrialHandler extends PsychObject getCurrentTrial: () => this.getTrial(currentIndex), getTrial: (index = 0) => this.getTrial(index), - addData: (key, value) => this.addData(key, value) + addData: (key, value) => this.addData(key, value), }; // add to the snapshots the current trial's attributes: const currentTrial = this.getCurrentTrial(); - const excludedAttributes = ['handler', 'name', 'nStim', 'nRemaining', 'thisRepN', 'thisTrialN', 'thisN', 'thisIndex', 'ran', 'finished']; + const excludedAttributes = ["handler", "name", "nStim", "nRemaining", "thisRepN", "thisTrialN", "thisN", "thisIndex", "ran", "finished"]; const trialAttributes = []; for (const attribute in currentTrial) { @@ -280,7 +273,6 @@ export class TrialHandler extends PsychObject return snapshot; } - /** * Setter for the seed attribute. * @@ -289,9 +281,9 @@ export class TrialHandler extends PsychObject */ setSeed(seed, log) { - this._setAttribute('seed', seed, log); + this._setAttribute("seed", seed, log); - if (typeof seed !== 'undefined') + if (typeof seed !== "undefined") { this._randomNumberGenerator = seedrandom(seed); } @@ -301,7 +293,6 @@ export class TrialHandler extends PsychObject } } - /** * Set the internal state of this trial handler from the given snapshot. * @@ -312,12 +303,11 @@ export class TrialHandler extends PsychObject static fromSnapshot(snapshot) { // if snapshot is undefined, do nothing: - if (typeof snapshot === 'undefined') + if (typeof snapshot === "undefined") { return; } - snapshot.handler.nStim = snapshot.nStim; snapshot.handler.nTotal = snapshot.nTotal; snapshot.handler.nRemaining = snapshot.nRemaining; @@ -330,13 +320,12 @@ export class TrialHandler extends PsychObject snapshot.handler.thisTrial = snapshot.handler.getCurrentTrial(); - // add the snapshot's trial attributes to a global variable, whose name is derived from // that of the handler: loops -> thisLoop (note the dropped s): let name = snapshot.name; - if (name[name.length-1] === 's') + if (name[name.length - 1] === "s") { - name = name.substr(0, name.length-1); + name = name.substr(0, name.length - 1); } name = `this${name[0].toUpperCase()}${name.substr(1)}`; @@ -348,7 +337,6 @@ export class TrialHandler extends PsychObject window[name] = value; } - /** * Getter for the finished attribute. * @@ -359,7 +347,6 @@ export class TrialHandler extends PsychObject return this._finished; } - /** * Setter for the finished attribute. * @@ -368,14 +355,13 @@ export class TrialHandler extends PsychObject set finished(isFinished) { this._finished = isFinished; - - this._snapshots.forEach( snapshot => + + this._snapshots.forEach((snapshot) => { snapshot.finished = isFinished; }); } - /** * Get the trial index. * @@ -387,7 +373,6 @@ export class TrialHandler extends PsychObject return this.thisIndex; } - /** * Set the trial index. * @@ -398,7 +383,6 @@ export class TrialHandler extends PsychObject this.thisIndex = index; } - /** * Get the attributes of the trials. * @@ -424,7 +408,6 @@ export class TrialHandler extends PsychObject return Object.keys(this.trialList[0]); } - /** * Get the current trial. * @@ -436,7 +419,6 @@ export class TrialHandler extends PsychObject return this.trialList[this.thisIndex]; } - /** * Get the nth trial. * @@ -453,7 +435,6 @@ export class TrialHandler extends PsychObject return this.trialList[index]; } - /** * Get the nth future or past trial, without advancing through the trial list. * @@ -472,7 +453,6 @@ export class TrialHandler extends PsychObject return this.trialList[this.thisIndex + n]; } - /** * Get the nth previous trial. *

Note: this is useful for comparisons in n-back tasks.

@@ -486,7 +466,6 @@ export class TrialHandler extends PsychObject return getFutureTrial(-abs(n)); } - /** * Add a key/value pair to data about the current trial held by the experiment handler * @@ -502,7 +481,6 @@ export class TrialHandler extends PsychObject } } - /** * Import a list of conditions from a .xls, .xlsx, .odp, or .csv resource. * @@ -542,8 +520,8 @@ export class TrialHandler extends PsychObject { try { - let resourceExtension = resourceName.split('.').pop(); - if (['csv', 'odp', 'xls', 'xlsx'].indexOf(resourceExtension) > -1) + let resourceExtension = resourceName.split(".").pop(); + if (["csv", "odp", "xls", "xlsx"].indexOf(resourceExtension) > -1) { // (*) read conditions from resource: const resourceValue = serverManager.getResource(resourceName, true); @@ -552,20 +530,20 @@ export class TrialHandler extends PsychObject // which is then read in as a string const decodedResourceMaybe = new Uint8Array(resourceValue); // Could be set to 'buffer' for ASCII .csv - const type = resourceExtension === 'csv' ? 'string' : 'array'; - const decodedResource = type === 'string' ? (new TextDecoder()).decode(decodedResourceMaybe) : decodedResourceMaybe; + const type = resourceExtension === "csv" ? "string" : "array"; + const decodedResource = type === "string" ? (new TextDecoder()).decode(decodedResourceMaybe) : decodedResourceMaybe; const workbook = XLSX.read(decodedResource, { type }); // we consider only the first worksheet: if (workbook.SheetNames.length === 0) { - throw 'workbook should contain at least one worksheet'; + throw "workbook should contain at least one worksheet"; } const sheetName = workbook.SheetNames[0]; const worksheet = workbook.Sheets[sheetName]; // worksheet to array of arrays (the first array contains the fields): - const sheet = XLSX.utils.sheet_to_json(worksheet, {header: 1, blankrows: false}); + const sheet = XLSX.utils.sheet_to_json(worksheet, { header: 1, blankrows: false }); const fields = sheet.shift(); // (*) select conditions: @@ -574,8 +552,8 @@ export class TrialHandler extends PsychObject // (*) return the selected conditions as an array of 'object as map': // [ // {field0: value0-0, field1: value0-1, ...} - // {field0: value1-0, field1: value1-1, ...} - // ... + // {field0: value1-0, field1: value1-1, ...} + // ... // ] let trialList = new Array(selectedRows.length - 1); for (let r = 0; r < selectedRows.length; ++r) @@ -598,7 +576,7 @@ export class TrialHandler extends PsychObject value = arrayMaybe; } - if (typeof value === 'string') + if (typeof value === "string") { const numberMaybe = Number.parseFloat(value); @@ -610,7 +588,7 @@ export class TrialHandler extends PsychObject else { // Parse doubly escaped line feeds - value = value.replace(/(\n)/g, '\n'); + value = value.replace(/(\n)/g, "\n"); } } @@ -621,23 +599,21 @@ export class TrialHandler extends PsychObject return trialList; } - else { - throw 'extension: ' + resourceExtension + ' currently not supported.'; + throw "extension: " + resourceExtension + " currently not supported."; } } catch (error) { throw { - origin: 'TrialHandler.importConditions', + origin: "TrialHandler.importConditions", context: `when importing condition: ${resourceName}`, - error + error, }; } } - /** * Prepare the trial list. * @@ -647,16 +623,15 @@ export class TrialHandler extends PsychObject _prepareTrialList(trialList) { const response = { - origin: 'TrialHandler._prepareTrialList', - context: 'when preparing the trial list' + origin: "TrialHandler._prepareTrialList", + context: "when preparing the trial list", }; // we treat undefined trialList as a list with a single empty entry: - if (typeof trialList === 'undefined') + if (typeof trialList === "undefined") { this.trialList = [undefined]; } - // if trialList is an array, we make sure it is not empty: else if (Array.isArray(trialList)) { @@ -665,30 +640,27 @@ export class TrialHandler extends PsychObject this.trialList = [undefined]; } } - // if trialList is a string, we treat it as the name of the condition resource: - else if (typeof trialList === 'string') + else if (typeof trialList === "string") { this.trialList = TrialHandler.importConditions(this.psychoJS.serverManager, trialList); } - // unknown type: else { throw Object.assign(response, { - error: 'unable to prepare trial list: unknown type: ' + (typeof trialList) + error: "unable to prepare trial list: unknown type: " + (typeof trialList), }); } } - /* * Prepare the sequence of trials. * *

The returned sequence is a matrix (an array of arrays) of trial indices * with nStim columns and nReps rows. Note that this is the transpose of the * matrix return by PsychoPY. - * + * * Example: with 3 trial and 5 repetitions, we get: * - sequential: * [[0 1 2] @@ -711,8 +683,8 @@ export class TrialHandler extends PsychObject _prepareSequence() { const response = { - origin: 'TrialHandler._prepareSequence', - context: 'when preparing a sequence of trials' + origin: "TrialHandler._prepareSequence", + context: "when preparing a sequence of trials", }; // get an array of the indices of the elements of trialList : @@ -722,9 +694,8 @@ export class TrialHandler extends PsychObject { this._trialSequence = Array(this.nReps).fill(indices); // transposed version: - //this._trialSequence = indices.reduce( (seq, e) => { seq.push( Array(this.nReps).fill(e) ); return seq; }, [] ); + // this._trialSequence = indices.reduce( (seq, e) => { seq.push( Array(this.nReps).fill(e) ); return seq; }, [] ); } - else if (this.method === TrialHandler.Method.RANDOM) { this._trialSequence = []; @@ -733,7 +704,6 @@ export class TrialHandler extends PsychObject this._trialSequence.push(util.shuffle(indices.slice(), this._randomNumberGenerator)); } } - else if (this.method === TrialHandler.Method.FULL_RANDOM) { // create a flat sequence with nReps repeats of indices: @@ -755,15 +725,13 @@ export class TrialHandler extends PsychObject } else { - throw Object.assign(response, {error: 'unknown method'}); + throw Object.assign(response, { error: "unknown method" }); } return this._trialSequence; } - } - /** * TrialHandler method * @@ -775,20 +743,20 @@ TrialHandler.Method = { /** * Conditions are presented in the order they are given. */ - SEQUENTIAL: Symbol.for('SEQUENTIAL'), + SEQUENTIAL: Symbol.for("SEQUENTIAL"), /** * Conditions are shuffled within each repeat. */ - RANDOM: Symbol.for('RANDOM'), + RANDOM: Symbol.for("RANDOM"), /** * Conditions are fully randomised across all repeats. */ - FULL_RANDOM: Symbol.for('FULL_RANDOM'), + FULL_RANDOM: Symbol.for("FULL_RANDOM"), /** * Same as above, but named to reflect PsychoPy boileplate. */ - FULLRANDOM: Symbol.for('FULL_RANDOM') + FULLRANDOM: Symbol.for("FULL_RANDOM"), }; diff --git a/src/data/index.js b/src/data/index.js index e8a9929..fcd486e 100644 --- a/src/data/index.js +++ b/src/data/index.js @@ -1,3 +1,3 @@ -export * from './ExperimentHandler.js'; -export * from './TrialHandler.js'; -//export * from './Shelf.js'; +export * from "./ExperimentHandler.js"; +export * from "./TrialHandler.js"; +// export * from './Shelf.js';