An easy-to-use platform for EEG experimentation in the classroom
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

prettier

+2107 -1561
+8 -8
app/actions/deviceActions.js
··· 12 12 // ------------------------------------------------------------------------- 13 13 // Actions 14 14 15 - export const connectToDevice = (payload) => ({ 15 + export const connectToDevice = payload => ({ 16 16 payload, 17 - type: CONNECT_TO_DEVICE, 17 + type: CONNECT_TO_DEVICE 18 18 }); 19 - export const disconnectFromDevice = (payload) => ({ 19 + export const disconnectFromDevice = payload => ({ 20 20 payload, 21 - type: EXPERIMENT_CLEANUP, 21 + type: EXPERIMENT_CLEANUP 22 22 }); 23 - export const setConnectionStatus = (payload) => ({ 23 + export const setConnectionStatus = payload => ({ 24 24 payload, 25 - type: SET_CONNECTION_STATUS, 25 + type: SET_CONNECTION_STATUS 26 26 }); 27 - export const setDeviceAvailability = (payload) => ({ 27 + export const setDeviceAvailability = payload => ({ 28 28 payload, 29 - type: SET_DEVICE_AVAILABILITY, 29 + type: SET_DEVICE_AVAILABILITY 30 30 });
+13 -13
app/actions/experimentActions.js
··· 24 24 export const start = () => ({ type: START }); 25 25 export const pause = () => ({ type: PAUSE }); 26 26 // export const stop = () => ({ type: STOP }); 27 - export const stop = (payload) => ({ payload, type: STOP }); 28 - export const setType = (payload) => ({ payload, type: SET_TYPE }); 29 - export const setParadigm = (payload) => ({ payload, type: SET_PARADIGM }); 30 - export const setSubject = (payload) => ({ payload, type: SET_SUBJECT }); 31 - export const setGroup = (payload) => ({ payload, type: SET_GROUP }); 32 - export const setSession = (payload) => ({ payload, type: SET_SESSION }); 33 - export const setParams = (payload) => ({ payload, type: SET_PARAMS }); 34 - export const setDescription = (payload) => ({ payload, type: SET_DESCRIPTION }); 35 - export const createNewWorkspace = (payload) => ({ 27 + export const stop = payload => ({ payload, type: STOP }); 28 + export const setType = payload => ({ payload, type: SET_TYPE }); 29 + export const setParadigm = payload => ({ payload, type: SET_PARADIGM }); 30 + export const setSubject = payload => ({ payload, type: SET_SUBJECT }); 31 + export const setGroup = payload => ({ payload, type: SET_GROUP }); 32 + export const setSession = payload => ({ payload, type: SET_SESSION }); 33 + export const setParams = payload => ({ payload, type: SET_PARAMS }); 34 + export const setDescription = payload => ({ payload, type: SET_DESCRIPTION }); 35 + export const createNewWorkspace = payload => ({ 36 36 payload, 37 - type: CREATE_NEW_WORKSPACE, 37 + type: CREATE_NEW_WORKSPACE 38 38 }); 39 39 export const loadDefaultTimeline = () => ({ type: LOAD_DEFAULT_TIMELINE }); 40 - export const setTitle = (payload) => ({ payload, type: SET_TITLE }); 40 + export const setTitle = payload => ({ payload, type: SET_TITLE }); 41 41 export const saveWorkspace = () => ({ type: SAVE_WORKSPACE }); 42 - export const setState = (payload) => ({ payload, type: SET_EXPERIMENT_STATE }); 43 - export const setEEGEnabled = (payload) => ({ payload, type: SET_EEG_ENABLED }); 42 + export const setState = payload => ({ payload, type: SET_EXPERIMENT_STATE }); 43 + export const setEEGEnabled = payload => ({ payload, type: SET_EEG_ENABLED });
+6 -6
app/actions/jupyterActions.js
··· 21 21 22 22 export const sendExecuteRequest = (payload: string) => ({ 23 23 payload, 24 - type: SEND_EXECUTE_REQUEST, 24 + type: SEND_EXECUTE_REQUEST 25 25 }); 26 26 27 27 export const loadEpochs = (payload: Array<string>) => ({ 28 28 payload, 29 - type: LOAD_EPOCHS, 29 + type: LOAD_EPOCHS 30 30 }); 31 31 32 32 export const loadCleanedEpochs = (payload: Array<string>) => ({ 33 33 payload, 34 - type: LOAD_CLEANED_EPOCHS, 34 + type: LOAD_CLEANED_EPOCHS 35 35 }); 36 36 37 37 export const loadPSD = () => ({ 38 - type: LOAD_PSD, 38 + type: LOAD_PSD 39 39 }); 40 40 41 41 export const loadERP = (payload: ?string) => ({ 42 42 payload, 43 - type: LOAD_ERP, 43 + type: LOAD_ERP 44 44 }); 45 45 46 46 export const loadTopo = () => ({ 47 - type: LOAD_TOPO, 47 + type: LOAD_TOPO 48 48 }); 49 49 50 50 export const cleanEpochs = () => ({ type: CLEAN_EPOCHS });
+3 -1
app/app.html
··· 37 37 ); 38 38 39 39 document.write( 40 - scripts.map((script) => `<script defer src="${script}"><\/script>`).join('') 40 + scripts 41 + .map(script => `<script defer src="${script}"><\/script>`) 42 + .join('') 41 43 ); 42 44 } 43 45 </script>
+3 -3
app/components/styles/secondarynav.css
··· 36 36 .activeSecondaryNavSegment:hover { 37 37 } 38 38 39 - .secondaryNavContainerExpName{ 39 + .secondaryNavContainerExpName { 40 40 font-family: Lato; 41 41 font-style: normal; 42 42 font-weight: normal !important; 43 43 font-size: 24px !important; 44 44 line-height: 29px !important; 45 45 letter-spacing: -0.2px !important; 46 - color: #1A1A1A !important; 46 + color: #1a1a1a !important; 47 47 } 48 48 49 49 .settingsButtons { ··· 51 51 grid-template-columns: 2fr 4fr; 52 52 } 53 53 54 - .dropdownSettings{ 54 + .dropdownSettings { 55 55 font-size: 27px; 56 56 color: #666666; 57 57 display: grid !important;
+2 -2
app/components/styles/topnavbar.css
··· 99 99 margin-left: 20px !important; 100 100 } 101 101 102 - .exitWorkspaceBtn{ 102 + .exitWorkspaceBtn { 103 103 font-family: Gothic A1 !important; 104 104 font-style: normal !important; 105 105 font-weight: 900 !important; 106 106 font-size: 24px !important; 107 107 line-height: 30px !important; 108 - color: #007C70 !important; 108 + color: #007c70 !important; 109 109 border: none !important; 110 110 height: 30px; 111 111 min-width: 30px;
+14 -14
app/constants/constants.js
··· 6 6 STROOP: 'Stroop Task', 7 7 MULTI: 'Multi-tasking', 8 8 SEARCH: 'Visual search', 9 - CUSTOM: 'Custom', 9 + CUSTOM: 'Custom' 10 10 }; 11 11 12 12 export const SCREENS = { ··· 17 17 RUN: { route: '/run', title: 'RUN', order: 5 }, 18 18 CLEAN: { route: '/clean', title: 'CLEAN', order: 3 }, 19 19 ANALYZE: { route: '/analyze', title: 'ANALYZE', order: 4 }, 20 - ANALYZEBEHAVIOR: { route: '/analyze', title: 'ANALYZE', order: 3 }, 20 + ANALYZEBEHAVIOR: { route: '/analyze', title: 'ANALYZE', order: 3 } 21 21 }; 22 22 23 23 export const DEVICES = { 24 24 NONE: 'NONE', 25 25 MUSE: 'MUSE', 26 26 EMOTIV: 'EMOTIV', 27 - GANGLION: 'GANGLION', // One day ;) 27 + GANGLION: 'GANGLION' // One day ;) 28 28 }; 29 29 30 30 export const CONNECTION_STATUS = { ··· 34 34 NO_DEVICES: 'NO_DEVICES', 35 35 NOT_YET_CONNECTED: 'NOT_YET_CONNECTED', 36 36 SEARCHING: 'SEARCHING', 37 - BLUETOOTH_DISABLED: 'BLUETOOTH_DISABLED', 37 + BLUETOOTH_DISABLED: 'BLUETOOTH_DISABLED' 38 38 }; 39 39 40 40 export const KERNEL_STATUS = { 41 41 OFFLINE: 'Offline', 42 42 BUSY: 'Busy', 43 43 IDLE: 'Idle', 44 - STARTING: 'Starting', 44 + STARTING: 'Starting' 45 45 }; 46 46 47 47 export const DEVICE_AVAILABILITY = { 48 48 NONE: 'NONE', 49 49 SEARCHING: 'SEARCHING', 50 - AVAILABLE: 'AVAILABLE', 50 + AVAILABLE: 'AVAILABLE' 51 51 }; 52 52 53 53 // Names of variables in the jupyter kernel 54 54 export const JUPYTER_VARIABLE_NAMES = { 55 55 RAW_EPOCHS: 'raw_epochs', 56 - CLEAN_EPOCHS: 'clean_epochs', 56 + CLEAN_EPOCHS: 'clean_epochs' 57 57 }; 58 58 59 59 export const SEARCH_TIMER = 3000; ··· 65 65 STIMULUS_3: 3, 66 66 STIMULUS_4: 4, 67 67 TARGET: 2, 68 - NONTARGET: 1, 68 + NONTARGET: 1 69 69 }; 70 70 71 71 export const CHANNELS = { ··· 89 89 AF7: { index: 1, color: '#7EA0C5' }, 90 90 AF8: { index: 2, color: '#8BD6E9' }, 91 91 TP10: { index: 3, color: '#66B0A9' }, 92 - AUX: { index: 4, color: '#E7789E' }, 92 + AUX: { index: 4, color: '#E7789E' } 93 93 }; 94 94 95 95 export const EMOTIV_CHANNELS = [ ··· 106 106 'FC6', 107 107 'F4', 108 108 'F8', 109 - 'AF4', 109 + 'AF4' 110 110 ]; 111 111 112 112 export const MUSE_CHANNELS = ['TP9', 'AF7', 'AF8', 'TP10', 'AUX']; ··· 119 119 export const VIEWER_DEFAULTS = { 120 120 domain: 5000, // ms 121 121 zoom: 1, 122 - autoScale: false, 122 + autoScale: false 123 123 }; 124 124 125 125 export const SIGNAL_QUALITY = { 126 126 BAD: '#ed5a5a', 127 127 OK: '#FFCD39', 128 128 GREAT: '#66B0A9', 129 - DISCONNECTED: '#BFBFBF', 129 + DISCONNECTED: '#BFBFBF' 130 130 }; 131 131 132 132 export const SIGNAL_QUALITY_THRESHOLDS = { 133 133 BAD: 15, 134 134 OK: 10, 135 - GREAT: 1.5, // Below 1.5 usually indicates not connected to anything 135 + GREAT: 1.5 // Below 1.5 usually indicates not connected to anything 136 136 }; 137 137 138 138 export const FILE_TYPES = { 139 139 STIMULUS_DIR: 'STIMULUS_DIR', 140 - TIMELINE: 'TIMELINE', 140 + TIMELINE: 'TIMELINE' 141 141 };
+4 -4
app/constants/interfaces.js
··· 19 19 // Setting this to any prevents ridiculous flow runtime errors 20 20 showProgessBar: any, 21 21 stimulus1: { dir: string, type: EVENTS, title: string, response: string }, 22 - stimulus2: { dir: string, type: EVENTS, title: string, response: string }, 22 + stimulus2: { dir: string, type: EVENTS, title: string, response: string } 23 23 }; 24 24 25 25 export type ExperimentDescription = { 26 26 question: string, 27 27 hypothesis: string, 28 - methods: string, 28 + methods: string 29 29 }; 30 30 31 31 // Array of timeline and trial ids that will be presented in experiment ··· 38 38 stimulus?: string | StimulusVariable; 39 39 trial_duration?: number | (() => number); 40 40 post_trial_gap?: number; 41 - on_load?: (string) => void | StimulusVariable; 41 + on_load?: string => void | StimulusVariable; 42 42 choices?: Array<string>; 43 43 } 44 44 ··· 47 47 id: string, 48 48 timeline: Array<Trial>, 49 49 sample?: SampleParameter, 50 - timeline_variables?: Array<Object>, 50 + timeline_variables?: Array<Object> 51 51 }; 52 52 53 53 export interface SampleParameter {
+2 -2
app/containers/AnalyzeContainer.js
··· 11 11 type: state.experiment.type, 12 12 deviceType: state.device.deviceType, 13 13 isEEGEnabled: state.experiment.isEEGEnabled, 14 - ...state.jupyter, 14 + ...state.jupyter 15 15 }; 16 16 } 17 17 18 18 function mapDispatchToProps(dispatch) { 19 19 return { 20 20 experimentActions: bindActionCreators(experimentActions, dispatch), 21 - jupyterActions: bindActionCreators(jupyterActions, dispatch), 21 + jupyterActions: bindActionCreators(jupyterActions, dispatch) 22 22 }; 23 23 } 24 24
+1 -1
app/containers/App.js
··· 4 4 import TopNav from './TopNavBarContainer'; 5 5 6 6 type Props = { 7 - children: React.Node, 7 + children: React.Node 8 8 }; 9 9 10 10 export default class App extends React.Component<Props> {
+2 -2
app/containers/CleanContainer.js
··· 13 13 group: state.experiment.group, 14 14 session: state.experiment.session, 15 15 deviceType: state.device.deviceType, 16 - ...state.jupyter, 16 + ...state.jupyter 17 17 }; 18 18 } 19 19 20 20 function mapDispatchToProps(dispatch) { 21 21 return { 22 22 experimentActions: bindActionCreators(experimentActions, dispatch), 23 - jupyterActions: bindActionCreators(jupyterActions, dispatch), 23 + jupyterActions: bindActionCreators(jupyterActions, dispatch) 24 24 }; 25 25 } 26 26
+2 -2
app/containers/CollectContainer.js
··· 8 8 function mapStateToProps(state) { 9 9 return { 10 10 ...state.device, 11 - ...state.experiment, 11 + ...state.experiment 12 12 }; 13 13 } 14 14 15 15 function mapDispatchToProps(dispatch) { 16 16 return { 17 17 deviceActions: bindActionCreators(deviceActions, dispatch), 18 - experimentActions: bindActionCreators(experimentActions, dispatch), 18 + experimentActions: bindActionCreators(experimentActions, dispatch) 19 19 }; 20 20 } 21 21
+2 -2
app/containers/ExperimentDesignContainer.js
··· 6 6 7 7 function mapStateToProps(state) { 8 8 return { 9 - ...state.experiment, 9 + ...state.experiment 10 10 }; 11 11 } 12 12 13 13 function mapDispatchToProps(dispatch) { 14 14 return { 15 - experimentActions: bindActionCreators(experimentActions, dispatch), 15 + experimentActions: bindActionCreators(experimentActions, dispatch) 16 16 }; 17 17 } 18 18
+2 -2
app/containers/HomeContainer.js
··· 9 9 function mapStateToProps(state) { 10 10 return { 11 11 ...state.device, 12 - kernelStatus: state.jupyter.kernelStatus, 12 + kernelStatus: state.jupyter.kernelStatus 13 13 }; 14 14 } 15 15 ··· 17 17 return { 18 18 deviceActions: bindActionCreators(deviceActions, dispatch), 19 19 jupyterActions: bindActionCreators(jupyterActions, dispatch), 20 - experimentActions: bindActionCreators(experimentActions, dispatch), 20 + experimentActions: bindActionCreators(experimentActions, dispatch) 21 21 }; 22 22 } 23 23
+2 -2
app/containers/TopNavBarContainer.js
··· 10 10 location: state.router.location, 11 11 isRunning: state.experiment.isRunning, 12 12 type: state.experiment.type, 13 - isEEGEnabled: state.experiment.isEEGEnabled, 13 + isEEGEnabled: state.experiment.isEEGEnabled 14 14 }; 15 15 } 16 16 17 17 function mapDispatchToProps(dispatch) { 18 18 return { 19 - experimentActions: bindActionCreators(experimentActions, dispatch), 19 + experimentActions: bindActionCreators(experimentActions, dispatch) 20 20 }; 21 21 } 22 22
+63 -46
app/epics/deviceEpics.js
··· 7 7 CONNECT_TO_DEVICE, 8 8 SET_DEVICE_AVAILABILITY, 9 9 setDeviceAvailability, 10 - setConnectionStatus, 10 + setConnectionStatus 11 11 } from '../actions/deviceActions'; 12 12 import { 13 13 getEmotiv, 14 14 connectToEmotiv, 15 15 createRawEmotivObservable, 16 16 createEmotivSignalQualityObservable, 17 - disconnectFromEmotiv, 17 + disconnectFromEmotiv 18 18 } from '../utils/eeg/emotiv'; 19 19 import { 20 20 getMuse, 21 21 connectToMuse, 22 22 createRawMuseObservable, 23 23 createMuseSignalQualityObservable, 24 - disconnectFromMuse, 24 + disconnectFromMuse 25 25 } from '../utils/eeg/muse'; 26 26 import { 27 27 CONNECTION_STATUS, 28 28 DEVICES, 29 29 DEVICE_AVAILABILITY, 30 - SEARCH_TIMER, 30 + SEARCH_TIMER 31 31 } from '../constants/constants'; 32 32 import { EXPERIMENT_CLEANUP } from './experimentEpics'; 33 33 ··· 43 43 // ------------------------------------------------------------------------- 44 44 // Action Creators 45 45 46 - const deviceFound = (payload) => ({ 46 + const deviceFound = payload => ({ 47 47 payload, 48 - type: DEVICE_FOUND, 48 + type: DEVICE_FOUND 49 49 }); 50 50 51 - const setDeviceType = (payload) => ({ 51 + const setDeviceType = payload => ({ 52 52 payload, 53 - type: SET_DEVICE_TYPE, 53 + type: SET_DEVICE_TYPE 54 54 }); 55 55 56 - const setRawObservable = (payload) => ({ 56 + const setRawObservable = payload => ({ 57 57 payload, 58 - type: SET_RAW_OBSERVABLE, 58 + type: SET_RAW_OBSERVABLE 59 59 }); 60 60 61 - const setSignalQualityObservable = (payload) => ({ 61 + const setSignalQualityObservable = payload => ({ 62 62 payload, 63 - type: SET_SIGNAL_OBSERVABLE, 63 + type: SET_SIGNAL_OBSERVABLE 64 64 }); 65 65 66 - const setAvailableDevices = (payload) => ({ 66 + const setAvailableDevices = payload => ({ 67 67 payload, 68 - type: SET_AVAILABLE_DEVICES, 68 + type: SET_AVAILABLE_DEVICES 69 69 }); 70 70 71 - const setDeviceInfo = (payload) => ({ 71 + const setDeviceInfo = payload => ({ 72 72 payload, 73 - type: SET_DEVICE_INFO, 73 + type: SET_DEVICE_INFO 74 74 }); 75 75 76 76 const cleanup = () => ({ type: DEVICE_CLEANUP }); ··· 79 79 // Epics 80 80 81 81 // NOTE: Uses a Promise "then" inside b/c Observable.from leads to loss of user gesture propagation for web bluetooth 82 - const searchMuseEpic = (action$) => 82 + const searchMuseEpic = action$ => 83 83 action$.ofType(SET_DEVICE_AVAILABILITY).pipe( 84 84 pluck('payload'), 85 - filter((status) => status === DEVICE_AVAILABILITY.SEARCHING), 85 + filter(status => status === DEVICE_AVAILABILITY.SEARCHING), 86 86 map(getMuse), 87 - mergeMap((promise) => 87 + mergeMap(promise => 88 88 promise.then( 89 - (devices) => devices, 90 - (error) => { 89 + devices => devices, 90 + error => { 91 91 // This error will fire a bit too promiscuously until we fix windows web bluetooth 92 92 // toast.error(`"Device Error: " ${error.toString()}`); 93 93 return []; 94 94 } 95 95 ) 96 96 ), 97 - filter((devices) => devices), // filter out nulls if running on win7 98 - filter((devices) => devices.length >= 1), 97 + filter(devices => devices), // filter out nulls if running on win7 98 + filter(devices => devices.length >= 1), 99 99 map(deviceFound) 100 100 ); 101 101 102 - const searchEmotivEpic = (action$) => 102 + const searchEmotivEpic = action$ => 103 103 action$.ofType(SET_DEVICE_AVAILABILITY).pipe( 104 104 pluck('payload'), 105 - filter((status) => status === DEVICE_AVAILABILITY.SEARCHING), 105 + filter(status => status === DEVICE_AVAILABILITY.SEARCHING), 106 106 filter(() => process.platform === 'darwin' || process.platform === 'win32'), 107 107 map(getEmotiv), 108 - mergeMap((promise) => 108 + mergeMap(promise => 109 109 promise.then( 110 - (devices) => devices, 111 - (error) => { 110 + devices => devices, 111 + error => { 112 112 if (error.message.includes('client.queryHeadsets')) { 113 113 toast.error( 114 114 'Could not connect to Cortex Service. Please connect to the internet and install Cortex to use Emotiv EEG', ··· 122 122 } 123 123 ) 124 124 ), 125 - filter((devices) => devices.length >= 1), 125 + filter(devices => devices.length >= 1), 126 126 map(deviceFound) 127 127 ); 128 128 129 129 const deviceFoundEpic = (action$, state$) => 130 130 action$.ofType(DEVICE_FOUND).pipe( 131 131 pluck('payload'), 132 - map((foundDevices) => 132 + map(foundDevices => 133 133 foundDevices.reduce((acc, curr) => { 134 - if (acc.find((device) => device.id === curr.id)) { 134 + if (acc.find(device => device.id === curr.id)) { 135 135 return acc; 136 136 } 137 137 return acc.concat(curr); 138 138 }, state$.value.device.availableDevices) 139 139 ), 140 - mergeMap((devices) => 141 - of(setAvailableDevices(devices), setDeviceAvailability(DEVICE_AVAILABILITY.AVAILABLE)) 140 + mergeMap(devices => 141 + of( 142 + setAvailableDevices(devices), 143 + setDeviceAvailability(DEVICE_AVAILABILITY.AVAILABLE) 144 + ) 142 145 ) 143 146 ); 144 147 145 148 const searchTimerEpic = (action$, state$) => 146 149 action$.ofType(SET_DEVICE_AVAILABILITY).pipe( 147 150 pluck('payload'), 148 - filter((status) => status === DEVICE_AVAILABILITY.SEARCHING), 151 + filter(status => status === DEVICE_AVAILABILITY.SEARCHING), 149 152 mergeMap(() => timer(SEARCH_TIMER)), 150 - filter(() => state$.value.device.deviceAvailability === DEVICE_AVAILABILITY.SEARCHING), 153 + filter( 154 + () => 155 + state$.value.device.deviceAvailability === DEVICE_AVAILABILITY.SEARCHING 156 + ), 151 157 map(() => setDeviceAvailability(DEVICE_AVAILABILITY.NONE)) 152 158 ); 153 159 154 - const connectEpic = (action$) => 160 + const connectEpic = action$ => 155 161 action$.ofType(CONNECT_TO_DEVICE).pipe( 156 162 pluck('payload'), 157 - map((device) => (isNil(device.name) ? connectToEmotiv(device) : connectToMuse(device))), 158 - mergeMap((promise) => promise.then((deviceInfo) => deviceInfo)), 159 - mergeMap((deviceInfo) => { 163 + map(device => 164 + isNil(device.name) ? connectToEmotiv(device) : connectToMuse(device) 165 + ), 166 + mergeMap(promise => promise.then(deviceInfo => deviceInfo)), 167 + mergeMap(deviceInfo => { 160 168 if (!isNil(deviceInfo) && !isNil(deviceInfo.samplingRate)) { 161 169 return of( 162 - setDeviceType(deviceInfo.name.includes('Muse') ? DEVICES.MUSE : DEVICES.EMOTIV), 170 + setDeviceType( 171 + deviceInfo.name.includes('Muse') ? DEVICES.MUSE : DEVICES.EMOTIV 172 + ), 163 173 setDeviceInfo(deviceInfo), 164 174 setConnectionStatus(CONNECTION_STATUS.CONNECTED) 165 175 ); ··· 168 178 }) 169 179 ); 170 180 171 - const isConnectingEpic = (action$) => 181 + const isConnectingEpic = action$ => 172 182 action$ 173 183 .ofType(CONNECT_TO_DEVICE) 174 184 .pipe(map(() => setConnectionStatus(CONNECTION_STATUS.CONNECTING))); ··· 187 197 const setSignalQualityObservableEpic = (action$, state$) => 188 198 action$.ofType(SET_RAW_OBSERVABLE).pipe( 189 199 pluck('payload'), 190 - map((rawObservable) => { 200 + map(rawObservable => { 191 201 if (state$.value.device.deviceType === DEVICES.EMOTIV) { 192 202 return createEmotivSignalQualityObservable( 193 203 rawObservable, 194 204 state$.value.device.connectedDevice 195 205 ); 196 206 } 197 - return createMuseSignalQualityObservable(rawObservable, state$.value.device.connectedDevice); 207 + return createMuseSignalQualityObservable( 208 + rawObservable, 209 + state$.value.device.connectedDevice 210 + ); 198 211 }), 199 212 map(setSignalQualityObservable) 200 213 ); 201 214 202 215 const deviceCleanupEpic = (action$, state$) => 203 216 action$.ofType(EXPERIMENT_CLEANUP).pipe( 204 - filter(() => state$.value.device.connectionStatus !== CONNECTION_STATUS.NOT_YET_CONNECTED), 217 + filter( 218 + () => 219 + state$.value.device.connectionStatus !== 220 + CONNECTION_STATUS.NOT_YET_CONNECTED 221 + ), 205 222 map(() => { 206 223 if (state$.value.device.deviceType === DEVICES.EMOTIV) { 207 224 disconnectFromEmotiv(); ··· 224 241 setSignalQualityObservableEpic, 225 242 deviceCleanupEpic 226 243 )(action$, state$).pipe( 227 - catchError((error) => 244 + catchError(error => 228 245 of(error).pipe( 229 - tap((err) => toast.error(`"Device Error: " ${err.toString()}`)), 246 + tap(err => toast.error(`"Device Error: " ${err.toString()}`)), 230 247 map(cleanup) 231 248 ) 232 249 )
+43 -25
app/epics/experimentEpics.js
··· 9 9 takeUntil, 10 10 throttleTime, 11 11 ignoreElements, 12 - tap, 12 + tap 13 13 } from 'rxjs/operators'; 14 14 import { 15 15 setType, ··· 23 23 SAVE_WORKSPACE, 24 24 CREATE_NEW_WORKSPACE, 25 25 SET_SUBJECT, 26 - SET_GROUP, 26 + SET_GROUP 27 27 } from '../actions/experimentActions'; 28 - import { DEVICES, MUSE_CHANNELS, EMOTIV_CHANNELS, CONNECTION_STATUS } from '../constants/constants'; 28 + import { 29 + DEVICES, 30 + MUSE_CHANNELS, 31 + EMOTIV_CHANNELS, 32 + CONNECTION_STATUS 33 + } from '../constants/constants'; 29 34 import { loadProtocol, getBehaviouralData } from '../utils/labjs/functions'; 30 - import { createEEGWriteStream, writeHeader, writeEEGData } from '../utils/filesystem/write'; 35 + import { 36 + createEEGWriteStream, 37 + writeHeader, 38 + writeEEGData 39 + } from '../utils/filesystem/write'; 31 40 import { 32 41 getWorkspaceDir, 33 42 storeExperimentState, 34 43 restoreExperimentState, 35 44 createWorkspaceDir, 36 45 storeBehaviouralData, 37 - readWorkspaceBehaviorData, 46 + readWorkspaceBehaviorData 38 47 } from '../utils/filesystem/storage'; 39 48 40 49 import { createEmotivRecord, stopEmotivRecord } from '../utils/eeg/emotiv'; ··· 48 57 // ------------------------------------------------------------------------- 49 58 // Action Creators 50 59 51 - const setTimeline = (payload) => ({ 60 + const setTimeline = payload => ({ 52 61 payload, 53 - type: SET_TIMELINE, 62 + type: SET_TIMELINE 54 63 }); 55 64 56 - const setIsRunning = (payload) => ({ 65 + const setIsRunning = payload => ({ 57 66 payload, 58 - type: SET_IS_RUNNING, 67 + type: SET_IS_RUNNING 59 68 }); 60 69 61 70 const updateSession = () => ({ type: UPDATE_SESSION }); 62 71 63 - const setSession = (payload) => ({ 72 + const setSession = payload => ({ 64 73 payload, 65 - type: SET_SESSION, 74 + type: SET_SESSION 66 75 }); 67 76 68 77 const cleanup = () => ({ 69 - type: EXPERIMENT_CLEANUP, 78 + type: EXPERIMENT_CLEANUP 70 79 }); 71 80 72 81 // ------------------------------------------------------------------------- 73 82 // Epics 74 83 75 - const createNewWorkspaceEpic = (action$) => 84 + const createNewWorkspaceEpic = action$ => 76 85 action$.ofType(CREATE_NEW_WORKSPACE).pipe( 77 86 pluck('payload'), 78 - tap((workspaceInfo) => createWorkspaceDir(workspaceInfo.title)), 79 - mergeMap((workspaceInfo) => 87 + tap(workspaceInfo => createWorkspaceDir(workspaceInfo.title)), 88 + mergeMap(workspaceInfo => 80 89 of( 81 90 setType(workspaceInfo.type), 82 91 setParadigm(workspaceInfo.paradigm), ··· 97 106 action$.ofType(START).pipe( 98 107 filter(() => !state$.value.experiment.isRunning), 99 108 map(() => { 100 - if (state$.value.device.connectionStatus === CONNECTION_STATUS.CONNECTED) { 109 + if ( 110 + state$.value.device.connectionStatus === CONNECTION_STATUS.CONNECTED 111 + ) { 101 112 const writeStream = createEEGWriteStream( 102 113 state$.value.experiment.title, 103 114 state$.value.experiment.subject, ··· 107 118 108 119 writeHeader( 109 120 writeStream, 110 - state$.value.device.deviceType === DEVICES.EMOTIV ? EMOTIV_CHANNELS : MUSE_CHANNELS 121 + state$.value.device.deviceType === DEVICES.EMOTIV 122 + ? EMOTIV_CHANNELS 123 + : MUSE_CHANNELS 111 124 ); 112 125 113 126 if (state$.value.device.deviceType === DEVICES.EMOTIV) { 114 - createEmotivRecord(state$.value.experiment.subject, state$.value.experiment.session); 127 + createEmotivRecord( 128 + state$.value.experiment.subject, 129 + state$.value.experiment.session 130 + ); 115 131 } 116 132 117 133 state$.value.device.rawObservable 118 134 .pipe(takeUntil(action$.ofType(STOP, EXPERIMENT_CLEANUP))) 119 - .subscribe((eegData) => writeEEGData(writeStream, eegData)); 135 + .subscribe(eegData => writeEEGData(writeStream, eegData)); 120 136 } 121 137 }), 122 138 mapTo(true), ··· 152 168 153 169 const updateSessionEpic = (action$, state$) => 154 170 action$.ofType(UPDATE_SESSION).pipe( 155 - mergeMap(() => from(readWorkspaceBehaviorData(state$.value.experiment.title))), 156 - map((behaviorFiles) => { 171 + mergeMap(() => 172 + from(readWorkspaceBehaviorData(state$.value.experiment.title)) 173 + ), 174 + map(behaviorFiles => { 157 175 if (behaviorFiles.length > 0) { 158 - const subjectFiles = behaviorFiles.filter((filepath) => 176 + const subjectFiles = behaviorFiles.filter(filepath => 159 177 filepath.name.startsWith(state$.value.experiment.subject) 160 178 ); 161 179 return subjectFiles.length + 1; ··· 165 183 map(setSession) 166 184 ); 167 185 168 - const autoSaveEpic = (action$) => 186 + const autoSaveEpic = action$ => 169 187 action$.ofType('@@router/LOCATION_CHANGE').pipe( 170 188 pluck('payload', 'pathname'), 171 - filter((pathname) => pathname !== '/' && pathname !== '/home'), 189 + filter(pathname => pathname !== '/' && pathname !== '/home'), 172 190 map(saveWorkspace) 173 191 ); 174 192 ··· 184 202 const navigationCleanupEpic = (action$, state$) => 185 203 action$.ofType('@@router/LOCATION_CHANGE').pipe( 186 204 pluck('payload', 'pathname'), 187 - filter((pathname) => pathname === '/' || pathname === '/home'), 205 + filter(pathname => pathname === '/' || pathname === '/home'), 188 206 tap(() => restoreExperimentState(state$.value.experiment)), 189 207 map(cleanup) 190 208 );
+111 -77
app/epics/jupyterEpics.js
··· 1 1 import { combineEpics } from 'redux-observable'; 2 2 import { from, of } from 'rxjs'; 3 - import { map, mergeMap, tap, pluck, ignoreElements, filter, take } from 'rxjs/operators'; 3 + import { 4 + map, 5 + mergeMap, 6 + tap, 7 + pluck, 8 + ignoreElements, 9 + filter, 10 + take 11 + } from 'rxjs/operators'; 4 12 import { find } from 'kernelspecs'; 5 13 import { launchSpec } from 'spawnteract'; 6 14 import { createMainChannel } from 'enchannel-zmq-backend'; ··· 20 28 CLEAN_EPOCHS, 21 29 CLOSE_KERNEL, 22 30 loadTopo, 23 - loadERP, 31 + loadERP 24 32 } from '../actions/jupyterActions'; 25 33 import { 26 34 imports, ··· 35 43 plotPSD, 36 44 plotERP, 37 45 plotTopoMap, 38 - saveEpochs, 46 + saveEpochs 39 47 } from '../utils/jupyter/cells'; 40 48 import { 41 49 EMOTIV_CHANNELS, 42 50 EVENTS, 43 51 DEVICES, 44 52 MUSE_CHANNELS, 45 - JUPYTER_VARIABLE_NAMES, 53 + JUPYTER_VARIABLE_NAMES 46 54 } from '../constants/constants'; 47 55 import { 48 56 parseSingleQuoteJSON, 49 57 parseKernelStatus, 50 - debugParseMessage, 58 + debugParseMessage 51 59 } from '../utils/jupyter/functions'; 52 60 53 61 export const GET_EPOCHS_INFO = 'GET_EPOCHS_INFO'; ··· 69 77 // ------------------------------------------------------------------------- 70 78 // Action Creators 71 79 72 - const getEpochsInfo = (payload) => ({ payload, type: GET_EPOCHS_INFO }); 80 + const getEpochsInfo = payload => ({ payload, type: GET_EPOCHS_INFO }); 73 81 74 82 const getChannelInfo = () => ({ type: GET_CHANNEL_INFO }); 75 83 76 - const setKernel = (payload) => ({ 84 + const setKernel = payload => ({ 77 85 payload, 78 - type: SET_KERNEL, 86 + type: SET_KERNEL 79 87 }); 80 88 81 - const setKernelStatus = (payload) => ({ 89 + const setKernelStatus = payload => ({ 82 90 payload, 83 - type: SET_KERNEL_STATUS, 91 + type: SET_KERNEL_STATUS 84 92 }); 85 93 86 - const setKernelInfo = (payload) => ({ 94 + const setKernelInfo = payload => ({ 87 95 payload, 88 - type: SET_KERNEL_INFO, 96 + type: SET_KERNEL_INFO 89 97 }); 90 98 91 - const setMainChannel = (payload) => ({ 99 + const setMainChannel = payload => ({ 92 100 payload, 93 - type: SET_MAIN_CHANNEL, 101 + type: SET_MAIN_CHANNEL 94 102 }); 95 103 96 - const setEpochInfo = (payload) => ({ 104 + const setEpochInfo = payload => ({ 97 105 payload, 98 - type: SET_EPOCH_INFO, 106 + type: SET_EPOCH_INFO 99 107 }); 100 108 101 - const setChannelInfo = (payload) => ({ 109 + const setChannelInfo = payload => ({ 102 110 payload, 103 - type: SET_CHANNEL_INFO, 111 + type: SET_CHANNEL_INFO 104 112 }); 105 113 106 - const setPSDPlot = (payload) => ({ 114 + const setPSDPlot = payload => ({ 107 115 payload, 108 - type: SET_PSD_PLOT, 116 + type: SET_PSD_PLOT 109 117 }); 110 118 111 - const setTopoPlot = (payload) => ({ 119 + const setTopoPlot = payload => ({ 112 120 payload, 113 - type: SET_TOPO_PLOT, 121 + type: SET_TOPO_PLOT 114 122 }); 115 123 116 - const setERPPlot = (payload) => ({ 124 + const setERPPlot = payload => ({ 117 125 payload, 118 - type: SET_ERP_PLOT, 126 + type: SET_ERP_PLOT 119 127 }); 120 128 121 - const receiveExecuteReply = (payload) => ({ 129 + const receiveExecuteReply = payload => ({ 122 130 payload, 123 - type: RECEIVE_EXECUTE_REPLY, 131 + type: RECEIVE_EXECUTE_REPLY 124 132 }); 125 133 126 - const receiveExecuteResult = (payload) => ({ 134 + const receiveExecuteResult = payload => ({ 127 135 payload, 128 - type: RECEIVE_EXECUTE_RESULT, 136 + type: RECEIVE_EXECUTE_RESULT 129 137 }); 130 138 131 - const receiveDisplayData = (payload) => ({ 139 + const receiveDisplayData = payload => ({ 132 140 payload, 133 - type: RECEIVE_DISPLAY_DATA, 141 + type: RECEIVE_DISPLAY_DATA 134 142 }); 135 143 136 - const receiveStream = (payload) => ({ 144 + const receiveStream = payload => ({ 137 145 payload, 138 - type: RECEIVE_STREAM, 146 + type: RECEIVE_STREAM 139 147 }); 140 148 141 149 // ------------------------------------------------------------------------- 142 150 // Epics 143 151 144 - const launchEpic = (action$) => 152 + const launchEpic = action$ => 145 153 action$.ofType(LAUNCH_KERNEL).pipe( 146 154 mergeMap(() => from(find('brainwaves'))), 147 - tap((kernelInfo) => { 155 + tap(kernelInfo => { 148 156 if (isNil(kernelInfo)) { 149 - toast.error("Could not find 'brainwaves' jupyter kernel. Have you installed Python?"); 157 + toast.error( 158 + "Could not find 'brainwaves' jupyter kernel. Have you installed Python?" 159 + ); 150 160 } 151 161 }), 152 - filter((kernelInfo) => !isNil(kernelInfo)), 153 - mergeMap((kernelInfo) => 162 + filter(kernelInfo => !isNil(kernelInfo)), 163 + mergeMap(kernelInfo => 154 164 from( 155 165 launchSpec(kernelInfo.spec, { 156 166 // No STDIN, opt in to STDOUT and STDERR as node streams 157 - stdio: ['ignore', 'pipe', 'pipe'], 167 + stdio: ['ignore', 'pipe', 'pipe'] 158 168 }) 159 169 ) 160 170 ), 161 - tap((kernel) => { 171 + tap(kernel => { 162 172 // Route everything that we won't get in messages to our own stdout 163 - kernel.spawn.stdout.on('data', (data) => { 173 + kernel.spawn.stdout.on('data', data => { 164 174 const text = data.toString(); 165 175 console.log('KERNEL STDOUT: ', text); 166 176 }); 167 - kernel.spawn.stderr.on('data', (data) => { 177 + kernel.spawn.stderr.on('data', data => { 168 178 const text = data.toString(); 169 179 console.log('KERNEL STDERR: ', text); 170 180 toast.error('Jupyter: ', text); ··· 177 187 map(setKernel) 178 188 ); 179 189 180 - const setUpChannelEpic = (action$) => 190 + const setUpChannelEpic = action$ => 181 191 action$.ofType(SET_KERNEL).pipe( 182 192 pluck('payload'), 183 - mergeMap((kernel) => from(createMainChannel(kernel.config))), 184 - tap((mainChannel) => mainChannel.next(executeRequest(imports()))), 185 - tap((mainChannel) => mainChannel.next(executeRequest(utils()))), 193 + mergeMap(kernel => from(createMainChannel(kernel.config))), 194 + tap(mainChannel => mainChannel.next(executeRequest(imports()))), 195 + tap(mainChannel => mainChannel.next(executeRequest(utils()))), 186 196 map(setMainChannel) 187 197 ); 188 198 ··· 190 200 action$.ofType(SET_MAIN_CHANNEL).pipe( 191 201 mergeMap(() => 192 202 state$.value.jupyter.mainChannel.pipe( 193 - map((msg) => { 203 + map(msg => { 194 204 console.log(debugParseMessage(msg)); 195 205 switch (msg['header']['msg_type']) { 196 206 case 'kernel_info_reply': ··· 208 218 default: 209 219 } 210 220 }), 211 - filter((action) => !isNil(action)) 221 + filter(action => !isNil(action)) 212 222 ) 213 223 ) 214 224 ); ··· 223 233 const loadEpochsEpic = (action$, state$) => 224 234 action$.ofType(LOAD_EPOCHS).pipe( 225 235 pluck('payload'), 226 - filter((filePathsArray) => filePathsArray.length >= 1), 227 - map((filePathsArray) => 228 - state$.value.jupyter.mainChannel.next(executeRequest(loadCSV(filePathsArray))) 236 + filter(filePathsArray => filePathsArray.length >= 1), 237 + map(filePathsArray => 238 + state$.value.jupyter.mainChannel.next( 239 + executeRequest(loadCSV(filePathsArray)) 240 + ) 229 241 ), 230 242 awaitOkMessage(action$), 231 243 execute(filterIIR(1, 30), state$), ··· 236 248 [state$.value.experiment.params.stimulus1.title]: EVENTS.STIMULUS_1, 237 249 [state$.value.experiment.params.stimulus2.title]: EVENTS.STIMULUS_2, 238 250 [state$.value.experiment.params.stimulus3.title]: EVENTS.STIMULUS_3, 239 - [state$.value.experiment.params.stimulus4.title]: EVENTS.STIMULUS_4, 251 + [state$.value.experiment.params.stimulus4.title]: EVENTS.STIMULUS_4 240 252 }, 241 253 -0.1, 242 254 0.8 243 255 ) 244 256 ), 245 - tap((e)=> {console.log('e', e)}), 246 - map((epochEventsCommand) => 257 + tap(e => { 258 + console.log('e', e); 259 + }), 260 + map(epochEventsCommand => 247 261 state$.value.jupyter.mainChannel.next(executeRequest(epochEventsCommand)) 248 262 ), 249 263 awaitOkMessage(action$), ··· 253 267 const loadCleanedEpochsEpic = (action$, state$) => 254 268 action$.ofType(LOAD_CLEANED_EPOCHS).pipe( 255 269 pluck('payload'), 256 - filter((filePathsArray) => filePathsArray.length >= 1), 257 - map((filePathsArray) => 258 - state$.value.jupyter.mainChannel.next(executeRequest(loadCleanedEpochs(filePathsArray))) 270 + filter(filePathsArray => filePathsArray.length >= 1), 271 + map(filePathsArray => 272 + state$.value.jupyter.mainChannel.next( 273 + executeRequest(loadCleanedEpochs(filePathsArray)) 274 + ) 259 275 ), 260 276 awaitOkMessage(action$), 261 277 mergeMap(() => 262 - of(getEpochsInfo(JUPYTER_VARIABLE_NAMES.CLEAN_EPOCHS), getChannelInfo(), loadTopo()) 278 + of( 279 + getEpochsInfo(JUPYTER_VARIABLE_NAMES.CLEAN_EPOCHS), 280 + getChannelInfo(), 281 + loadTopo() 282 + ) 263 283 ) 264 284 ); 265 285 ··· 270 290 action$.ofType(RECEIVE_STREAM).pipe( 271 291 pluck('payload'), 272 292 filter( 273 - (msg) => msg.channel === 'iopub' && msg.content.text.includes('Channels marked as bad') 293 + msg => 294 + msg.channel === 'iopub' && 295 + msg.content.text.includes('Channels marked as bad') 274 296 ), 275 297 take(1) 276 298 ) ··· 292 314 const getEpochsInfoEpic = (action$, state$) => 293 315 action$.ofType(GET_EPOCHS_INFO).pipe( 294 316 pluck('payload'), 295 - map((variableName) => 296 - state$.value.jupyter.mainChannel.next(executeRequest(requestEpochsInfo(variableName))) 317 + map(variableName => 318 + state$.value.jupyter.mainChannel.next( 319 + executeRequest(requestEpochsInfo(variableName)) 320 + ) 297 321 ), 298 322 mergeMap(() => 299 323 action$.ofType(RECEIVE_EXECUTE_RESULT).pipe( 300 324 pluck('payload'), 301 - filter((msg) => msg.channel === 'iopub' && !isNil(msg.content.data)), 325 + filter(msg => msg.channel === 'iopub' && !isNil(msg.content.data)), 302 326 pluck('content', 'data', 'text/plain'), 303 - filter((msg) => msg.includes('Drop Percentage')), 327 + filter(msg => msg.includes('Drop Percentage')), 304 328 take(1) 305 329 ) 306 330 ), 307 - map((epochInfoString) => 308 - parseSingleQuoteJSON(epochInfoString).map((infoObj) => ({ 331 + map(epochInfoString => 332 + parseSingleQuoteJSON(epochInfoString).map(infoObj => ({ 309 333 name: Object.keys(infoObj)[0], 310 - value: infoObj[Object.keys(infoObj)[0]], 334 + value: infoObj[Object.keys(infoObj)[0]] 311 335 })) 312 336 ), 313 337 map(setEpochInfo) ··· 319 343 mergeMap(() => 320 344 action$.ofType(RECEIVE_EXECUTE_RESULT).pipe( 321 345 pluck('payload'), 322 - filter((msg) => msg.channel === 'iopub' && !isNil(msg.content.data)), 346 + filter(msg => msg.channel === 'iopub' && !isNil(msg.content.data)), 323 347 pluck('content', 'data', 'text/plain'), 324 348 // Filter to prevent this from reading requestEpochsInfo returns 325 - filter((msg) => !msg.includes('Drop Percentage')), 349 + filter(msg => !msg.includes('Drop Percentage')), 326 350 take(1) 327 351 ) 328 352 ), 329 - map((channelInfoString) => setChannelInfo(parseSingleQuoteJSON(channelInfoString))) 353 + map(channelInfoString => 354 + setChannelInfo(parseSingleQuoteJSON(channelInfoString)) 355 + ) 330 356 ); 331 357 332 358 const loadPSDEpic = (action$, state$) => ··· 336 362 action$.ofType(RECEIVE_DISPLAY_DATA).pipe( 337 363 pluck('payload'), 338 364 // PSD graphs should have two axes 339 - filter((msg) => msg.content.data['text/plain'].includes('2 Axes')), 365 + filter(msg => msg.content.data['text/plain'].includes('2 Axes')), 340 366 pluck('content', 'data'), 341 367 take(1) 342 368 ) ··· 348 374 action$.ofType(LOAD_TOPO).pipe( 349 375 execute(plotTopoMap(), state$), 350 376 mergeMap(() => 351 - action$.ofType(RECEIVE_DISPLAY_DATA).pipe(pluck('payload'), pluck('content', 'data'), take(1)) 377 + action$ 378 + .ofType(RECEIVE_DISPLAY_DATA) 379 + .pipe(pluck('payload'), pluck('content', 'data'), take(1)) 352 380 ), 353 - mergeMap((topoPlot) => 381 + mergeMap(topoPlot => 354 382 of( 355 383 setTopoPlot(topoPlot), 356 384 loadERP( 357 - state$.value.device.deviceType === DEVICES.EMOTIV ? EMOTIV_CHANNELS[0] : MUSE_CHANNELS[0] 385 + state$.value.device.deviceType === DEVICES.EMOTIV 386 + ? EMOTIV_CHANNELS[0] 387 + : MUSE_CHANNELS[0] 358 388 ) 359 389 ) 360 390 ) ··· 363 393 const loadERPEpic = (action$, state$) => 364 394 action$.ofType(LOAD_ERP).pipe( 365 395 pluck('payload'), 366 - map((channelName) => { 396 + map(channelName => { 367 397 if (MUSE_CHANNELS.includes(channelName)) { 368 398 return MUSE_CHANNELS.indexOf(channelName); 369 399 } else if (EMOTIV_CHANNELS.includes(channelName)) { 370 400 return EMOTIV_CHANNELS.indexOf(channelName); 371 401 } 372 - console.warn('channel name supplied to loadERPEpic does not belong to either device'); 402 + console.warn( 403 + 'channel name supplied to loadERPEpic does not belong to either device' 404 + ); 373 405 return EMOTIV_CHANNELS[0]; 374 406 }), 375 - map((channelIndex) => 376 - state$.value.jupyter.mainChannel.next(executeRequest(plotERP(channelIndex))) 407 + map(channelIndex => 408 + state$.value.jupyter.mainChannel.next( 409 + executeRequest(plotERP(channelIndex)) 410 + ) 377 411 ), 378 412 mergeMap(() => 379 413 action$.ofType(RECEIVE_DISPLAY_DATA).pipe( 380 414 pluck('payload'), 381 415 // ERP graphs should have 1 axis according to MNE 382 - filter((msg) => msg.content.data['text/plain'].includes('1 Axes')), 416 + filter(msg => msg.content.data['text/plain'].includes('1 Axes')), 383 417 pluck('content', 'data'), 384 418 take(1) 385 419 )
+10 -4
app/main.dev.js
··· 23 23 sourceMapSupport.install(); 24 24 } 25 25 26 - if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') { 26 + if ( 27 + process.env.NODE_ENV === 'development' || 28 + process.env.DEBUG_PROD === 'true' 29 + ) { 27 30 require('electron-debug')(); 28 31 const path = require('path'); 29 32 const p = path.join(__dirname, '..', 'app', 'node_modules'); ··· 36 39 const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS']; 37 40 38 41 return Promise.all( 39 - extensions.map((name) => installer.default(installer[name], forceDownload)) 42 + extensions.map(name => installer.default(installer[name], forceDownload)) 40 43 ).catch(console.log); 41 44 }; 42 45 ··· 53 56 }); 54 57 55 58 app.on('ready', async () => { 56 - if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') { 59 + if ( 60 + process.env.NODE_ENV === 'development' || 61 + process.env.DEBUG_PROD === 'true' 62 + ) { 57 63 await installExtensions(); 58 64 } 59 65 60 66 mainWindow = new BrowserWindow({ 61 67 show: false, 62 68 width: 1280, 63 - height: 800, 69 + height: 800 64 70 }); 65 71 66 72 mainWindow.setMinimumSize(1075, 708);
+58 -46
app/menu.js
··· 9 9 } 10 10 11 11 buildMenu() { 12 - if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') { 12 + if ( 13 + process.env.NODE_ENV === 'development' || 14 + process.env.DEBUG_PROD === 'true' 15 + ) { 13 16 this.setupDevelopmentEnvironment(); 14 17 } 15 18 16 19 const template = 17 - process.platform === 'darwin' ? this.buildDarwinTemplate() : this.buildDefaultTemplate(); 20 + process.platform === 'darwin' 21 + ? this.buildDarwinTemplate() 22 + : this.buildDefaultTemplate(); 18 23 19 24 const menu = Menu.buildFromTemplate(template); 20 25 Menu.setApplicationMenu(menu); ··· 31 36 label: 'Inspect element', 32 37 click: () => { 33 38 this.mainWindow.inspectElement(x, y); 34 - }, 35 - }, 39 + } 40 + } 36 41 ]).popup(this.mainWindow); 37 42 }); 38 43 } ··· 43 48 submenu: [ 44 49 { 45 50 label: 'About BrainWaves', 46 - selector: 'orderFrontStandardAboutPanel:', 51 + selector: 'orderFrontStandardAboutPanel:' 47 52 }, 48 53 { type: 'separator' }, 49 54 { label: 'Services', submenu: [] }, ··· 51 56 { 52 57 label: 'Hide BrainWaves', 53 58 accelerator: 'Command+H', 54 - selector: 'hide:', 59 + selector: 'hide:' 55 60 }, 56 61 { 57 62 label: 'Hide Others', 58 63 accelerator: 'Command+Shift+H', 59 - selector: 'hideOtherApplications:', 64 + selector: 'hideOtherApplications:' 60 65 }, 61 66 { label: 'Show All', selector: 'unhideAllApplications:' }, 62 67 { type: 'separator' }, ··· 65 70 accelerator: 'Command+Q', 66 71 click: () => { 67 72 app.quit(); 68 - }, 69 - }, 70 - ], 73 + } 74 + } 75 + ] 71 76 }; 72 77 const subMenuEdit = { 73 78 label: 'Edit', ··· 81 86 { 82 87 label: 'Select All', 83 88 accelerator: 'Command+A', 84 - selector: 'selectAll:', 85 - }, 86 - ], 89 + selector: 'selectAll:' 90 + } 91 + ] 87 92 }; 88 93 const subMenuViewDev = { 89 94 label: 'View', ··· 93 98 accelerator: 'Command+R', 94 99 click: () => { 95 100 this.mainWindow.webContents.reload(); 96 - }, 101 + } 97 102 }, 98 103 { 99 104 label: 'Toggle Full Screen', 100 105 accelerator: 'Ctrl+Command+F', 101 106 click: () => { 102 107 this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen()); 103 - }, 108 + } 104 109 }, 105 110 { 106 111 label: 'Toggle Developer Tools', 107 112 accelerator: 'Alt+Command+I', 108 113 click: () => { 109 114 this.mainWindow.toggleDevTools(); 110 - }, 111 - }, 112 - ], 115 + } 116 + } 117 + ] 113 118 }; 114 119 const subMenuViewProd = { 115 120 label: 'View', ··· 119 124 accelerator: 'Ctrl+Command+F', 120 125 click: () => { 121 126 this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen()); 122 - }, 123 - }, 124 - ], 127 + } 128 + } 129 + ] 125 130 }; 126 131 const subMenuWindow = { 127 132 label: 'Window', ··· 129 134 { 130 135 label: 'Minimize', 131 136 accelerator: 'Command+M', 132 - selector: 'performMiniaturize:', 137 + selector: 'performMiniaturize:' 133 138 }, 134 139 { label: 'Close', accelerator: 'Command+W', selector: 'performClose:' }, 135 140 { type: 'separator' }, 136 - { label: 'Bring All to Front', selector: 'arrangeInFront:' }, 137 - ], 141 + { label: 'Bring All to Front', selector: 'arrangeInFront:' } 142 + ] 138 143 }; 139 144 140 - const subMenuView = process.env.NODE_ENV === 'development' ? subMenuViewDev : subMenuViewProd; 145 + const subMenuView = 146 + process.env.NODE_ENV === 'development' ? subMenuViewDev : subMenuViewProd; 141 147 142 148 return [subMenuAbout, subMenuEdit, subMenuView, subMenuWindow]; 143 149 } ··· 149 155 submenu: [ 150 156 { 151 157 label: '&Open', 152 - accelerator: 'Ctrl+O', 158 + accelerator: 'Ctrl+O' 153 159 }, 154 160 { 155 161 label: '&Close', 156 162 accelerator: 'Ctrl+W', 157 163 click: () => { 158 164 this.mainWindow.close(); 159 - }, 160 - }, 161 - ], 165 + } 166 + } 167 + ] 162 168 }, 163 169 { 164 170 label: '&View', ··· 170 176 accelerator: 'Ctrl+R', 171 177 click: () => { 172 178 this.mainWindow.webContents.reload(); 173 - }, 179 + } 174 180 }, 175 181 { 176 182 label: 'Toggle &Full Screen', 177 183 accelerator: 'F11', 178 184 click: () => { 179 - this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen()); 180 - }, 185 + this.mainWindow.setFullScreen( 186 + !this.mainWindow.isFullScreen() 187 + ); 188 + } 181 189 }, 182 190 { 183 191 label: 'Toggle &Developer Tools', 184 192 accelerator: 'Alt+Ctrl+I', 185 193 click: () => { 186 194 this.mainWindow.toggleDevTools(); 187 - }, 188 - }, 195 + } 196 + } 189 197 ] 190 198 : [ 191 199 { 192 200 label: 'Toggle &Full Screen', 193 201 accelerator: 'F11', 194 202 click: () => { 195 - this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen()); 196 - }, 197 - }, 198 - ], 203 + this.mainWindow.setFullScreen( 204 + !this.mainWindow.isFullScreen() 205 + ); 206 + } 207 + } 208 + ] 199 209 }, 200 210 { 201 211 label: 'Help', ··· 204 214 label: 'Learn More', 205 215 click() { 206 216 shell.openExternal('http://electron.atom.io'); 207 - }, 217 + } 208 218 }, 209 219 { 210 220 label: 'Documentation', 211 221 click() { 212 - shell.openExternal('https://github.com/atom/electron/tree/master/docs#readme'); 213 - }, 222 + shell.openExternal( 223 + 'https://github.com/atom/electron/tree/master/docs#readme' 224 + ); 225 + } 214 226 }, 215 227 { 216 228 label: 'Community Discussions', 217 229 click() { 218 230 shell.openExternal('https://discuss.atom.io/c/electron'); 219 - }, 231 + } 220 232 }, 221 233 { 222 234 label: 'Search Issues', 223 235 click() { 224 236 shell.openExternal('https://github.com/atom/electron/issues'); 225 - }, 226 - }, 227 - ], 228 - }, 237 + } 238 + } 239 + ] 240 + } 229 241 ]; 230 242 231 243 return templateDefault;
+18 -11
app/reducers/deviceReducer.js
··· 7 7 SET_CONNECTION_STATUS, 8 8 SET_RAW_OBSERVABLE, 9 9 SET_SIGNAL_OBSERVABLE, 10 - DEVICE_CLEANUP, 10 + DEVICE_CLEANUP 11 11 } from '../epics/deviceEpics'; 12 - import { DEVICES, CONNECTION_STATUS, DEVICE_AVAILABILITY } from '../constants/constants'; 12 + import { 13 + DEVICES, 14 + CONNECTION_STATUS, 15 + DEVICE_AVAILABILITY 16 + } from '../constants/constants'; 13 17 import { ActionType, DeviceInfo } from '../constants/interfaces'; 14 18 import { SET_DEVICE_AVAILABILITY } from '../actions/deviceActions'; 15 19 ··· 30 34 deviceAvailability: DEVICE_AVAILABILITY.NONE, 31 35 rawObservable: null, 32 36 signalQualityObservable: null, 33 - deviceType: DEVICES.EMOTIV, 37 + deviceType: DEVICES.EMOTIV 34 38 }; 35 39 36 - export default function device(state: DeviceStateType = initialState, action: ActionType) { 40 + export default function device( 41 + state: DeviceStateType = initialState, 42 + action: ActionType 43 + ) { 37 44 switch (action.type) { 38 45 case SET_DEVICE_TYPE: 39 46 return { 40 47 ...state, 41 - deviceType: action.payload, 48 + deviceType: action.payload 42 49 }; 43 50 44 51 case SET_DEVICE_INFO: 45 52 return { 46 53 ...state, 47 - connectedDevice: action.payload, 54 + connectedDevice: action.payload 48 55 }; 49 56 50 57 case SET_AVAILABLE_DEVICES: 51 58 return { 52 59 ...state, 53 - availableDevices: action.payload, 60 + availableDevices: action.payload 54 61 }; 55 62 56 63 case SET_CONNECTION_STATUS: 57 64 return { 58 65 ...state, 59 - connectionStatus: action.payload, 66 + connectionStatus: action.payload 60 67 }; 61 68 62 69 case SET_DEVICE_AVAILABILITY: 63 70 return { 64 71 ...state, 65 - deviceAvailability: action.payload, 72 + deviceAvailability: action.payload 66 73 }; 67 74 68 75 case SET_RAW_OBSERVABLE: 69 76 return { 70 77 ...state, 71 - rawObservable: action.payload, 78 + rawObservable: action.payload 72 79 }; 73 80 74 81 case SET_SIGNAL_OBSERVABLE: 75 82 return { 76 83 ...state, 77 - signalQualityObservable: action.payload, 84 + signalQualityObservable: action.payload 78 85 }; 79 86 80 87 case DEVICE_CLEANUP:
+20 -17
app/reducers/experimentReducer.js
··· 3 3 SET_TIMELINE, 4 4 SET_IS_RUNNING, 5 5 SET_SESSION, 6 - EXPERIMENT_CLEANUP, 6 + EXPERIMENT_CLEANUP 7 7 } from '../epics/experimentEpics'; 8 8 import { 9 9 SET_TYPE, ··· 14 14 SET_EXPERIMENT_STATE, 15 15 SET_PARAMS, 16 16 SET_DESCRIPTION, 17 - SET_EEG_ENABLED, 17 + SET_EEG_ENABLED 18 18 } from '../actions/experimentActions'; 19 19 import { EXPERIMENTS } from '../constants/constants'; 20 20 import { ··· 22 22 Trial, 23 23 ActionType, 24 24 ExperimentDescription, 25 - ExperimentParameters, 25 + ExperimentParameters 26 26 } from '../constants/interfaces'; 27 27 28 28 export interface ExperimentStateType { ··· 54 54 session: 1, 55 55 isRunning: false, 56 56 isEEGEnabled: false, 57 - description: { question: '', hypothesis: '', methods: '' }, 57 + description: { question: '', hypothesis: '', methods: '' } 58 58 }; 59 59 60 - export default function experiment(state: ExperimentStateType = initialState, action: ActionType) { 60 + export default function experiment( 61 + state: ExperimentStateType = initialState, 62 + action: ActionType 63 + ) { 61 64 switch (action.type) { 62 65 case SET_TYPE: 63 66 return { 64 67 ...state, 65 - type: action.payload, 68 + type: action.payload 66 69 }; 67 70 68 71 case SET_PARADIGM: 69 72 return { 70 73 ...state, 71 - paradigm: action.payload, 74 + paradigm: action.payload 72 75 }; 73 76 74 77 case SET_SUBJECT: 75 78 return { 76 79 ...state, 77 - subject: action.payload, 80 + subject: action.payload 78 81 }; 79 82 80 83 case SET_GROUP: 81 84 return { 82 85 ...state, 83 - group: action.payload, 86 + group: action.payload 84 87 }; 85 88 86 89 case SET_SESSION: 87 90 return { 88 91 ...state, 89 - session: action.payload, 92 + session: action.payload 90 93 }; 91 94 92 95 case SET_PARAMS: 93 96 return { 94 97 ...state, 95 - params: { ...state.params, ...action.payload }, 98 + params: { ...state.params, ...action.payload } 96 99 }; 97 100 98 101 case SET_TIMELINE: 99 102 return { 100 103 ...state, 101 - ...action.payload, 104 + ...action.payload 102 105 }; 103 106 104 107 case SET_TITLE: 105 108 return { 106 109 ...state, 107 - title: action.payload, 110 + title: action.payload 108 111 }; 109 112 110 113 case SET_DESCRIPTION: 111 114 return { 112 115 ...state, 113 - description: action.payload, 116 + description: action.payload 114 117 }; 115 118 116 119 case SET_IS_RUNNING: 117 120 return { 118 121 ...state, 119 - isRunning: action.payload, 122 + isRunning: action.payload 120 123 }; 121 124 122 125 case SET_EEG_ENABLED: 123 126 return { 124 127 ...state, 125 - isEEGEnabled: action.payload, 128 + isEEGEnabled: action.payload 126 129 }; 127 130 128 131 case SET_EXPERIMENT_STATE: 129 132 return { 130 - ...action.payload, 133 + ...action.payload 131 134 }; 132 135 133 136 case EXPERIMENT_CLEANUP:
+1 -1
app/reducers/index.js
··· 9 9 jupyter, 10 10 device, 11 11 experiment, 12 - router, 12 + router 13 13 }); 14 14 15 15 export default rootReducer;
+15 -12
app/reducers/jupyterReducer.js
··· 9 9 SET_PSD_PLOT, 10 10 SET_TOPO_PLOT, 11 11 SET_ERP_PLOT, 12 - RECEIVE_EXECUTE_RETURN, 12 + RECEIVE_EXECUTE_RETURN 13 13 } from '../epics/jupyterEpics'; 14 14 import { ActionType, Kernel } from '../constants/interfaces'; 15 15 import { KERNEL_STATUS } from '../constants/constants'; ··· 34 34 channelInfo: [], 35 35 psdPlot: null, 36 36 topoPlot: null, 37 - erpPlot: null, 37 + erpPlot: null 38 38 }; 39 39 40 - export default function jupyter(state: JupyterStateType = initialState, action: ActionType) { 40 + export default function jupyter( 41 + state: JupyterStateType = initialState, 42 + action: ActionType 43 + ) { 41 44 switch (action.type) { 42 45 case SET_KERNEL: 43 46 return { 44 47 ...state, 45 - kernel: action.payload, 48 + kernel: action.payload 46 49 }; 47 50 48 51 case SET_KERNEL_STATUS: 49 52 return { 50 53 ...state, 51 - kernelStatus: action.payload, 54 + kernelStatus: action.payload 52 55 }; 53 56 54 57 case SET_MAIN_CHANNEL: 55 58 return { 56 59 ...state, 57 - mainChannel: action.payload, 60 + mainChannel: action.payload 58 61 }; 59 62 60 63 case SET_KERNEL_INFO: ··· 63 66 case SET_EPOCH_INFO: 64 67 return { 65 68 ...state, 66 - epochsInfo: action.payload, 69 + epochsInfo: action.payload 67 70 }; 68 71 69 72 case SET_CHANNEL_INFO: 70 73 return { 71 74 ...state, 72 - channelInfo: action.payload, 75 + channelInfo: action.payload 73 76 }; 74 77 75 78 case SET_PSD_PLOT: 76 79 return { 77 80 ...state, 78 - psdPlot: action.payload, 81 + psdPlot: action.payload 79 82 }; 80 83 81 84 case SET_TOPO_PLOT: 82 85 return { 83 86 ...state, 84 - topoPlot: action.payload, 87 + topoPlot: action.payload 85 88 }; 86 89 87 90 case SET_ERP_PLOT: 88 91 return { 89 92 ...state, 90 - erpPlot: action.payload, 93 + erpPlot: action.payload 91 94 }; 92 95 93 96 case EXPERIMENT_CLEANUP: ··· 95 98 ...state, 96 99 epochsInfo: null, 97 100 psdPlot: null, 98 - erpPlot: null, 101 + erpPlot: null 99 102 }; 100 103 101 104 case RECEIVE_EXECUTE_RETURN:
+17 -8
app/routes.js
··· 11 11 12 12 const renderMergedProps = (component, ...rest) => { 13 13 const finalProps = Object.assign({}, ...rest); 14 - return ( 15 - React.createElement(component, finalProps) 16 - ); 17 - } 14 + return React.createElement(component, finalProps); 15 + }; 18 16 19 17 const PropsRoute = ({ component, ...rest }) => ( 20 18 <Route 21 19 {...rest} 22 20 render={routeProps => renderMergedProps(component, routeProps, rest)} 23 21 /> 24 - ) 22 + ); 25 23 26 24 export default () => ( 27 25 <App> ··· 29 27 <Route path={SCREENS.ANALYZE.route} component={AnalyzeContainer} /> 30 28 <Route path={SCREENS.CLEAN.route} component={CleanContainer} /> 31 29 <Route path={SCREENS.COLLECT.route} component={CollectContainer} /> 32 - <Route path={SCREENS.DESIGN.route} component={ExperimentDesignContainer} /> 33 - <PropsRoute path='/home' component={HomeContainer} activeStep="EXPERIMENT BANK" /> 34 - <PropsRoute path='/' component={HomeContainer} activeStep="MY EXPERIMENTS" /> 30 + <Route 31 + path={SCREENS.DESIGN.route} 32 + component={ExperimentDesignContainer} 33 + /> 34 + <PropsRoute 35 + path="/home" 36 + component={HomeContainer} 37 + activeStep="EXPERIMENT BANK" 38 + /> 39 + <PropsRoute 40 + path="/" 41 + component={HomeContainer} 42 + activeStep="MY EXPERIMENTS" 43 + /> 35 44 </Switch> 36 45 </App> 37 46 );
+3 -3
app/store/configureStore.dev.js
··· 26 26 // Logging Middleware 27 27 const logger = createLogger({ 28 28 level: 'info', 29 - collapsed: true, 29 + collapsed: true 30 30 }); 31 31 32 32 // Skip redux logs in console during the tests ··· 42 42 const actionCreators = { 43 43 ...deviceActions, 44 44 ...jupyterActions, 45 - ...routerActions, 45 + ...routerActions 46 46 }; 47 47 // If Redux DevTools Extension is installed use it, otherwise use Redux compose 48 48 /* eslint-disable no-underscore-dangle */ 49 49 const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ 50 50 ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ 51 51 // Options: http://zalmoxisus.github.io/redux-devtools-extension/API/Arguments.html 52 - actionCreators, 52 + actionCreators 53 53 }) 54 54 : compose; 55 55 /* eslint-enable no-underscore-dangle */
+3 -1
app/store/configureStore.js
··· 2 2 import configureStoreProd from './configureStore.prod'; 3 3 4 4 const selectedConfigureStore = 5 - process.env.NODE_ENV === 'production' ? configureStoreProd : configureStoreDev; 5 + process.env.NODE_ENV === 'production' 6 + ? configureStoreProd 7 + : configureStoreDev; 6 8 7 9 export const { configureStore } = selectedConfigureStore; 8 10
+221 -143
app/utils/behavior/compute.js
··· 2 2 import * as path from 'path'; 3 3 4 4 export const aggregateBehaviorDataToSave = (data, removeOutliers) => { 5 - const processedData = data.map((result) => { 5 + const processedData = data.map(result => { 6 6 if (path.basename(result.meta.datafile).includes('aggregated')) { 7 7 return transformAggregated(result); 8 8 } 9 - return filterData(result, removeOutliers); 10 - 9 + return filterData(result, removeOutliers); 11 10 }); 12 - const aggregatedData = processedData.map((e) => { 13 - const conditionsArray = e.map((row) => row.condition); 11 + const aggregatedData = processedData.map(e => { 12 + const conditionsArray = e.map(row => row.condition); 14 13 const unsortedConditions = [...new Set(conditionsArray)].sort(); 15 - const conditions = unsortedConditions.sort((a, b) => parseInt(a) - parseInt(b)); 14 + const conditions = unsortedConditions.sort( 15 + (a, b) => parseInt(a) - parseInt(b) 16 + ); 16 17 let rtMean = {}, 17 18 accuracyPercent = {}; 18 19 for (const condition of conditions) { 19 20 const rt = e 20 - .filter((row) => row.response_given === 'yes') 21 - .filter((row) => row.correct_response === 'true') 22 - .filter((row) => row.condition === condition) 23 - .map((row) => row.reaction_time) 24 - .map((value) => parseFloat(value)); 21 + .filter(row => row.response_given === 'yes') 22 + .filter(row => row.correct_response === 'true') 23 + .filter(row => row.condition === condition) 24 + .map(row => row.reaction_time) 25 + .map(value => parseFloat(value)); 25 26 rtMean[condition] = Math.round(ss.mean(rt)); 26 27 const accuracy = e.filter( 27 - (row) => 28 + row => 28 29 row.condition === condition && 29 30 row.correct_response === 'true' && 30 31 row.response_given === 'yes' 31 32 ); 32 33 accuracyPercent[condition] = accuracy.length 33 - ? Math.round(100 * (accuracy.length / e.filter((r) => r.condition === condition).length)) 34 - : ss.mean(e.filter((r) => r.condition === condition).map((r) => r.accuracy)); 34 + ? Math.round( 35 + 100 * 36 + (accuracy.length / 37 + e.filter(r => r.condition === condition).length) 38 + ) 39 + : ss.mean( 40 + e.filter(r => r.condition === condition).map(r => r.accuracy) 41 + ); 35 42 } 36 43 const row = { 37 - subject: e.map((r) => r.subject)[0], 38 - group: e.map((r) => r.group)[0], 39 - session: e.map((r) => r.session)[0], 44 + subject: e.map(r => r.subject)[0], 45 + group: e.map(r => r.group)[0], 46 + session: e.map(r => r.session)[0] 40 47 }; 41 48 for (const condition of conditions) { 42 - row[`RT_${ condition}`] = rtMean[condition]; 43 - row[`Accuracy_${ condition}`] = accuracyPercent[condition]; 49 + row[`RT_${condition}`] = rtMean[condition]; 50 + row[`Accuracy_${condition}`] = accuracyPercent[condition]; 44 51 } 45 52 return row; 46 53 }); ··· 55 62 displayMode 56 63 ) => { 57 64 if (data && data.length > 0) { 58 - const processedData = data.map((result) => { 65 + const processedData = data.map(result => { 59 66 if (path.basename(result.meta.datafile).includes('aggregated')) { 60 67 return transformAggregated(result); 61 68 } 62 69 return filterData(result, removeOutliers); 63 70 }); 64 71 const colors = ['#28619E', '#3DBBDB']; 65 - const unsortedConditions = [...new Set(processedData[0].map((row) => row.condition))].sort(); 66 - const conditions = unsortedConditions.sort((a, b) => parseInt(a) - parseInt(b)); 72 + const unsortedConditions = [ 73 + ...new Set(processedData[0].map(row => row.condition)) 74 + ].sort(); 75 + const conditions = unsortedConditions.sort( 76 + (a, b) => parseInt(a) - parseInt(b) 77 + ); 67 78 switch (dependentVariable) { 68 79 case 'RT': 69 80 default: ··· 88 99 } 89 100 }; 90 101 91 - const transformAggregated = (result) => { 102 + const transformAggregated = result => { 92 103 const unsortedConditions = result.meta.fields 93 - .filter((field) => field.startsWith('RT_')) 94 - .map((c) => c.split('RT_')[1]) 104 + .filter(field => field.startsWith('RT_')) 105 + .map(c => c.split('RT_')[1]) 95 106 .sort(); 96 - const conditions = unsortedConditions.sort((a, b) => parseInt(a) - parseInt(b)); 97 - const transformed = conditions.map((condition) => 98 - result.data.map((e) => ({ 107 + const conditions = unsortedConditions.sort( 108 + (a, b) => parseInt(a) - parseInt(b) 109 + ); 110 + const transformed = conditions.map(condition => 111 + result.data.map(e => ({ 99 112 reaction_time: parseFloat(e[`RT_${condition}`]), 100 113 subject: path.parse(result.meta.datafile).name, 101 114 condition, ··· 103 116 session: e.session, 104 117 accuracy: parseFloat(e[`Accuracy_${condition}`]), 105 118 response_given: 'yes', 106 - correct_response: 'true', 119 + correct_response: 'true' 107 120 })) 108 121 ); 109 122 const data = transformed.reduce((acc, item) => acc.concat(item), []); ··· 111 124 }; 112 125 113 126 const filterData = (data, removeOutliers) => { 114 - 115 127 let filteredData = data.data 116 - .filter((row) => row.trial_number && row.phase !== 'practice') 117 - .map((row) => ({ 128 + .filter(row => row.trial_number && row.phase !== 'practice') 129 + .map(row => ({ 118 130 condition: row.condition, 119 131 subject: path.parse(data.meta.datafile).name.split('-')[0], 120 132 group: path.parse(data.meta.datafile).name.split('-')[1], ··· 122 134 reaction_time: Math.round(parseFloat(row.reaction_time)), 123 135 correct_response: row.correct_response, 124 136 trial_number: row.trial_number, 125 - response_given: row.response_given, 137 + response_given: row.response_given 126 138 })); 127 139 if (removeOutliers) { 128 140 try { 129 141 const mean = ss.mean( 130 142 filteredData 131 - .filter((r) => r.response_given === 'yes' && r.correct_response === 'true') 132 - .map((r) => r.reaction_time) 143 + .filter( 144 + r => r.response_given === 'yes' && r.correct_response === 'true' 145 + ) 146 + .map(r => r.reaction_time) 133 147 ); 134 148 const standardDeviation = ss.sampleStandardDeviation( 135 149 filteredData 136 - .filter((r) => r.response_given === 'yes' && r.correct_response === 'true') 137 - .map((r) => r.reaction_time) 150 + .filter( 151 + r => r.response_given === 'yes' && r.correct_response === 'true' 152 + ) 153 + .map(r => r.reaction_time) 138 154 ); 139 155 const upperBoarder = mean + 2 * standardDeviation; 140 156 const lowerBoarder = mean - 2 * standardDeviation; 141 157 filteredData = filteredData.filter( 142 - (r) => 143 - (r.reaction_time > lowerBoarder && r.reaction_time < upperBoarder) || isNaN(r.reaction_time) 158 + r => 159 + (r.reaction_time > lowerBoarder && r.reaction_time < upperBoarder) || 160 + isNaN(r.reaction_time) 144 161 ); 145 162 } catch (err) { 146 - alert('Calculation of the mean and the standard deviation requires at least two completed trials in each condition.'); 163 + alert( 164 + 'Calculation of the mean and the standard deviation requires at least two completed trials in each condition.' 165 + ); 147 166 return filteredData; 148 167 } 149 168 } 150 169 return filteredData; 151 170 }; 152 171 153 - const computeRT = (data, dependentVariable, conditions, showDataPoints, colors, displayMode) => { 172 + const computeRT = ( 173 + data, 174 + dependentVariable, 175 + conditions, 176 + showDataPoints, 177 + colors, 178 + displayMode 179 + ) => { 154 180 let dataToPlot = 0; 155 181 let maxValue = 0; 156 182 switch (displayMode) { ··· 160 186 dataToPlot = conditions.reduce((obj, condition, i) => { 161 187 const xRaw = data 162 188 .reduce((a, b) => a.concat(b), []) 163 - .filter((r) => r.response_given === 'yes' && r.correct_response === 'true') 164 - .filter((e) => e.condition === condition) 165 - .map((r) => r.subject); 189 + .filter( 190 + r => r.response_given === 'yes' && r.correct_response === 'true' 191 + ) 192 + .filter(e => e.condition === condition) 193 + .map(r => r.subject); 166 194 const y = data 167 195 .reduce((a, b) => a.concat(b), []) 168 - .filter((r) => r.response_given === 'yes' && r.correct_response === 'true') 169 - .filter((e) => e.condition === condition) 170 - .map((r) => r.reaction_time); 196 + .filter( 197 + r => r.response_given === 'yes' && r.correct_response === 'true' 198 + ) 199 + .filter(e => e.condition === condition) 200 + .map(r => r.reaction_time); 171 201 maxValue = Math.max(...y) > maxValue ? Math.max(...y) : maxValue; 172 202 const subjects = Array.from(new Set(xRaw)); 173 - const x = xRaw.map((x) => subjects.indexOf(x) + 1 + i / 4 + (Math.random() - 0.5) / 5); 174 - tickValuesX = subjects.map((x) => subjects.indexOf(x) + 1 + 1 / 8); 203 + const x = xRaw.map( 204 + x => subjects.indexOf(x) + 1 + i / 4 + (Math.random() - 0.5) / 5 205 + ); 206 + tickValuesX = subjects.map(x => subjects.indexOf(x) + 1 + 1 / 8); 175 207 tickTextX = subjects; 176 208 obj[condition] = { x, y }; 177 209 return obj; ··· 180 212 dataToPlot['ticktext'] = tickTextX; 181 213 dataToPlot['lowerLimit'] = 0; 182 214 dataToPlot['upperLimit'] = maxValue > 1000 ? maxValue + 100 : 1000; 183 - return makeDataPointsGraph(dataToPlot, conditions, colors, dependentVariable); 215 + return makeDataPointsGraph( 216 + dataToPlot, 217 + conditions, 218 + colors, 219 + dependentVariable 220 + ); 184 221 185 222 case 'errorbars': 186 223 let maxValueSE = 0; 187 224 dataToPlot = conditions.reduce((obj, condition) => { 188 225 const xRaw = data 189 226 .reduce((a, b) => a.concat(b), []) 190 - .filter((r) => r.response_given === 'yes' && r.correct_response === 'true') 191 - .filter((e) => e.condition === condition) 192 - .map((r) => r.subject); 227 + .filter( 228 + r => r.response_given === 'yes' && r.correct_response === 'true' 229 + ) 230 + .filter(e => e.condition === condition) 231 + .map(r => r.subject); 193 232 const x = Array.from(new Set(xRaw)); 194 - const data_condition = data.map((d) => 233 + const data_condition = data.map(d => 195 234 d 196 - .filter((r) => r.response_given === 'yes' && r.correct_response === 'true') 197 - .filter((e) => e.condition == condition) 235 + .filter( 236 + r => r.response_given === 'yes' && r.correct_response === 'true' 237 + ) 238 + .filter(e => e.condition == condition) 198 239 ); 199 - const y_bars_prep = x.map((a) => 200 - data_condition.map((d) => d.filter((e) => e.subject === a)).filter((d) => d.length > 0) 240 + const y_bars_prep = x.map(a => 241 + data_condition 242 + .map(d => d.filter(e => e.subject === a)) 243 + .filter(d => d.length > 0) 201 244 ); 202 245 const y = y_bars_prep 203 - .map((y) => ss.mean(y.reduce((a, b) => a.concat(b), []).map((r) => r.reaction_time))) 204 - .map((v) => Math.round(v)); 246 + .map(y => 247 + ss.mean( 248 + y.reduce((a, b) => a.concat(b), []).map(r => r.reaction_time) 249 + ) 250 + ) 251 + .map(v => Math.round(v)); 205 252 maxValue = Math.max(...y) > maxValue ? Math.max(...y) : maxValue; 206 - const stErrorFunction = (array) => 253 + const stErrorFunction = array => 207 254 ss.sampleStandardDeviation(array) / Math.sqrt(array.length); 208 255 const stErrors = data_condition 209 - .map((a) => (a.length > 1 ? stErrorFunction(a.map((r) => r.reaction_time)) : 0)) 210 - .map((v) => Math.round(v)); 211 - maxValueSE = Math.max(...stErrors) > maxValueSE ? Math.max(...stErrors) : maxValueSE; 256 + .map(a => 257 + a.length > 1 ? stErrorFunction(a.map(r => r.reaction_time)) : 0 258 + ) 259 + .map(v => Math.round(v)); 260 + maxValueSE = 261 + Math.max(...stErrors) > maxValueSE 262 + ? Math.max(...stErrors) 263 + : maxValueSE; 212 264 obj[condition] = { x, y, stErrors }; 213 265 return obj; 214 266 }, {}); 215 267 dataToPlot['lowerLimit'] = 0; 216 - dataToPlot['upperLimit'] = maxValue + maxValueSE > 1000 ? maxValue + maxValueSE + 100 : 1000; 268 + dataToPlot['upperLimit'] = 269 + maxValue + maxValueSE > 1000 ? maxValue + maxValueSE + 100 : 1000; 217 270 return makeBarGraph(dataToPlot, conditions, colors, dependentVariable); 218 271 219 272 case 'whiskers': 220 273 dataToPlot = conditions.reduce((obj, condition, i) => { 221 274 const x = data 222 275 .reduce((a, b) => a.concat(b), []) 223 - .filter((r) => r.response_given === 'yes' && r.correct_response === 'true') 224 - .filter((e) => e.condition === condition) 225 - .map((r) => r.subject); 276 + .filter( 277 + r => r.response_given === 'yes' && r.correct_response === 'true' 278 + ) 279 + .filter(e => e.condition === condition) 280 + .map(r => r.subject); 226 281 const y = data 227 282 .reduce((a, b) => a.concat(b), []) 228 - .filter((r) => r.response_given === 'yes' && r.correct_response === 'true') 229 - .filter((e) => e.condition === condition) 230 - .map((r) => r.reaction_time); 283 + .filter( 284 + r => r.response_given === 'yes' && r.correct_response === 'true' 285 + ) 286 + .filter(e => e.condition === condition) 287 + .map(r => r.reaction_time); 231 288 maxValue = Math.max(...y) > maxValue ? Math.max(...y) : maxValue; 232 289 obj[condition] = { x, y }; 233 290 return obj; ··· 253 310 default: 254 311 let tickValuesX, tickTextX; 255 312 dataToPlot = conditions.reduce((obj, condition, i) => { 256 - const correctDataForCondition = data.map((d) => d.filter((e) => e.condition == condition)); 313 + const correctDataForCondition = data.map(d => 314 + d.filter(e => e.condition == condition) 315 + ); 257 316 258 317 const y = correctDataForCondition 259 - .map((d) => { 260 - if (d.filter((l) => l.accuracy).length > 0) { 261 - return d.map((l) => l.accuracy); 318 + .map(d => { 319 + if (d.filter(l => l.accuracy).length > 0) { 320 + return d.map(l => l.accuracy); 262 321 } 263 - const c = d.filter( 264 - (e) => e.response_given === 'yes' && e.correct_response === 'true' 265 - ); 266 - return Math.round((c.length / d.length) * 100); 267 - 322 + const c = d.filter( 323 + e => e.response_given === 'yes' && e.correct_response === 'true' 324 + ); 325 + return Math.round((c.length / d.length) * 100); 268 326 }) 269 327 .reduce((acc, item) => acc.concat(item), []); 270 328 271 329 const xRaw = correctDataForCondition 272 - .map((d) => { 273 - if (d.filter((l) => l.accuracy).length > 0) { 274 - return d.map((l) => l.subject); 330 + .map(d => { 331 + if (d.filter(l => l.accuracy).length > 0) { 332 + return d.map(l => l.subject); 275 333 } 276 - return d.map((r) => r.subject)[0]; 277 - 334 + return d.map(r => r.subject)[0]; 278 335 }) 279 336 .reduce((acc, item) => acc.concat(item), []); 280 337 const subjects = Array.from(new Set(xRaw)); 281 - const x = xRaw.map((x) => subjects.indexOf(x) + 1 + i / 4 + (Math.random() - 0.5) / 5); 282 - tickValuesX = subjects.map((x) => subjects.indexOf(x) + 1 + 1 / 8); 338 + const x = xRaw.map( 339 + x => subjects.indexOf(x) + 1 + i / 4 + (Math.random() - 0.5) / 5 340 + ); 341 + tickValuesX = subjects.map(x => subjects.indexOf(x) + 1 + 1 / 8); 283 342 tickTextX = subjects; 284 343 obj[condition] = { x, y }; 285 344 return obj; ··· 288 347 dataToPlot['ticktext'] = tickTextX; 289 348 dataToPlot['lowerLimit'] = 0; 290 349 dataToPlot['upperLimit'] = 105; 291 - return makeDataPointsGraph(dataToPlot, conditions, colors, dependentVariable); 350 + return makeDataPointsGraph( 351 + dataToPlot, 352 + conditions, 353 + colors, 354 + dependentVariable 355 + ); 292 356 293 357 case 'errorbars': 294 358 dataToPlot = conditions.reduce((obj, condition, i) => { 295 - const correctDataForCondition = data.map((d) => d.filter((e) => e.condition == condition)); 359 + const correctDataForCondition = data.map(d => 360 + d.filter(e => e.condition == condition) 361 + ); 296 362 const transformedData = correctDataForCondition 297 - .map((d) => { 298 - if (d.filter((l) => l.accuracy).length > 0) { 299 - return d.map((l) => ({ 363 + .map(d => { 364 + if (d.filter(l => l.accuracy).length > 0) { 365 + return d.map(l => ({ 300 366 accuracy: l.accuracy, 301 - subject: l.subject, 367 + subject: l.subject 302 368 })); 303 369 } 304 - const c = d.filter( 305 - (e) => e.response_given === 'yes' && e.correct_response === 'true' 306 - ); 307 - return { 308 - accuracy: Math.round((c.length / d.length) * 100), 309 - subject: d.map((r) => r.subject)[0], 310 - }; 311 - 370 + const c = d.filter( 371 + e => e.response_given === 'yes' && e.correct_response === 'true' 372 + ); 373 + return { 374 + accuracy: Math.round((c.length / d.length) * 100), 375 + subject: d.map(r => r.subject)[0] 376 + }; 312 377 }) 313 378 .reduce((acc, item) => acc.concat(item), []); 314 - const subjects = Array.from(new Set(transformedData.map((e) => e.subject))); 315 - const y = subjects.map((subject) => 316 - ss.mean(transformedData.filter((e) => e.subject === subject).map((d) => d.accuracy)) 379 + const subjects = Array.from( 380 + new Set(transformedData.map(e => e.subject)) 317 381 ); 318 - const stErrorFunction = (array) => 382 + const y = subjects.map(subject => 383 + ss.mean( 384 + transformedData 385 + .filter(e => e.subject === subject) 386 + .map(d => d.accuracy) 387 + ) 388 + ); 389 + const stErrorFunction = array => 319 390 ss.sampleStandardDeviation(array) / Math.sqrt(array.length); 320 - const stErrors = subjects.map((subject) => { 321 - const array = transformedData.filter((e) => e.subject === subject).map((d) => d.accuracy); 391 + const stErrors = subjects.map(subject => { 392 + const array = transformedData 393 + .filter(e => e.subject === subject) 394 + .map(d => d.accuracy); 322 395 if (array.length > 1) { 323 396 return stErrorFunction(array); 324 397 } 325 - return 0; 326 - 398 + return 0; 327 399 }); 328 400 obj[condition] = { x: subjects, y, stErrors }; 329 401 return obj; ··· 334 406 335 407 case 'whiskers': 336 408 dataToPlot = conditions.reduce((obj, condition, i) => { 337 - const correctDataForCondition = data.map((d) => d.filter((e) => e.condition == condition)); 409 + const correctDataForCondition = data.map(d => 410 + d.filter(e => e.condition == condition) 411 + ); 338 412 const y = correctDataForCondition 339 - .map((d) => { 340 - if (d.filter((l) => l.accuracy).length > 0) { 341 - return d.map((l) => l.accuracy); 413 + .map(d => { 414 + if (d.filter(l => l.accuracy).length > 0) { 415 + return d.map(l => l.accuracy); 342 416 } 343 - const c = d.filter( 344 - (e) => e.response_given === 'yes' && e.correct_response === 'true' 345 - ); 346 - return Math.round((c.length / d.length) * 100); 347 - 417 + const c = d.filter( 418 + e => e.response_given === 'yes' && e.correct_response === 'true' 419 + ); 420 + return Math.round((c.length / d.length) * 100); 348 421 }) 349 422 .reduce((acc, item) => acc.concat(item), []); 350 423 const xRaw = correctDataForCondition 351 - .map((d) => { 352 - if (d.filter((l) => l.accuracy).length > 0) { 353 - return d.map((l) => l.subject); 424 + .map(d => { 425 + if (d.filter(l => l.accuracy).length > 0) { 426 + return d.map(l => l.subject); 354 427 } 355 - return d.map((r) => r.subject)[0]; 356 - 428 + return d.map(r => r.subject)[0]; 357 429 }) 358 430 .reduce((acc, item) => acc.concat(item), []); 359 431 obj[condition] = { x: xRaw, y }; ··· 379 451 marker: { 380 452 color: colors[i], 381 453 size: 7, 382 - symbol: symbols[i], 454 + symbol: symbols[i] 383 455 }, 384 - mode: 'markers', 456 + mode: 'markers' 385 457 }; 386 458 }); 387 459 const layout = { 388 460 xaxis: { 389 461 tickvals: data.tickvals, 390 - ticktext: data.ticktext, 462 + ticktext: data.ticktext 391 463 }, 392 464 yaxis: { 393 465 title: `${ 394 - dependentVariable == 'Response Time' ? 'Response Time (milliseconds)' : '% correct' 466 + dependentVariable == 'Response Time' 467 + ? 'Response Time (milliseconds)' 468 + : '% correct' 395 469 }`, 396 - range: [data.lowerLimit, data.upperLimit], 470 + range: [data.lowerLimit, data.upperLimit] 397 471 }, 398 - title: `${dependentVariable}`, 472 + title: `${dependentVariable}` 399 473 }; 400 474 return { 401 475 dataToPlot, 402 - layout, 476 + layout 403 477 }; 404 478 }; 405 479 ··· 413 487 type: 'bar', 414 488 marker: { 415 489 color: colors[i], 416 - size: 7, 490 + size: 7 417 491 }, 418 492 error_y: { 419 493 type: 'data', 420 494 array: dataForCondition.stErrors, 421 - visible: true, 422 - }, 495 + visible: true 496 + } 423 497 }; 424 498 }); 425 499 const layout = { 426 500 yaxis: { 427 501 title: `${ 428 - dependentVariable == 'Response Time' ? 'Response Time (milliseconds)' : '% correct' 502 + dependentVariable == 'Response Time' 503 + ? 'Response Time (milliseconds)' 504 + : '% correct' 429 505 }`, 430 506 zeroline: false, 431 - range: [data.lowerLimit, data.upperLimit], 507 + range: [data.lowerLimit, data.upperLimit] 432 508 }, 433 509 barmode: 'group', 434 - title: `${dependentVariable}`, 510 + title: `${dependentVariable}` 435 511 }; 436 512 return { 437 513 dataToPlot, 438 - layout, 514 + layout 439 515 }; 440 516 }; 441 517 ··· 451 527 marker: { 452 528 color: colors[i], 453 529 size: 7, 454 - symbol: symbols[i], 530 + symbol: symbols[i] 455 531 }, 456 532 boxpoints: 'false', 457 - pointpos: 0, 533 + pointpos: 0 458 534 }; 459 535 }); 460 536 const layout = { 461 537 yaxis: { 462 538 title: `${ 463 - dependentVariable == 'Response Time' ? 'Response Time (milliseconds)' : '% correct' 539 + dependentVariable == 'Response Time' 540 + ? 'Response Time (milliseconds)' 541 + : '% correct' 464 542 }`, 465 543 zeroline: false, 466 - range: [data.lowerLimit, data.upperLimit], 544 + range: [data.lowerLimit, data.upperLimit] 467 545 }, 468 546 boxmode: 'group', 469 - title: `${dependentVariable}`, 547 + title: `${dependentVariable}` 470 548 }; 471 549 return { 472 550 dataToPlot, 473 - layout, 551 + layout 474 552 }; 475 553 };
+28 -16
app/utils/eeg/cortex.js
··· 23 23 24 24 const CORTEX_URL = 'wss://localhost:6868'; 25 25 26 - const safeParse = (msg) => { 26 + const safeParse = msg => { 27 27 try { 28 28 return JSON.parse(msg); 29 29 } catch (_) { ··· 60 60 this._log('ws: Socket closed'); 61 61 }); 62 62 this.verbose = options.verbose !== null ? options.verbose : 1; 63 - this.handleError = (error) => { 63 + this.handleError = error => { 64 64 throw new JSONRPCError(error); 65 65 }; 66 66 67 67 this.ready = new Promise( 68 - (resolve) => this.ws.addEventListener('open', resolve), 68 + resolve => this.ws.addEventListener('open', resolve), 69 69 this.handleError 70 70 ) 71 71 .then(() => this._log('ws: Socket opened')) 72 72 .then(() => this.call('inspectApi')) 73 - .then((methods) => { 74 - methods.forEach((m) => { 73 + .then(methods => { 74 + methods.forEach(m => { 75 75 this.defineMethod(m.methodName, m.params); 76 76 }); 77 77 this._log(`rpc: Added ${methods.length} methods from inspectApi`); ··· 86 86 87 87 if ('id' in data) { 88 88 const id = data.id; 89 - this._log(`[${id}] <-`, data.result ? 'success' : `error (${data.error.message})`); 89 + this._log( 90 + `[${id}] <-`, 91 + data.result ? 'success' : `error (${data.error.message})` 92 + ); 90 93 if (this.requests[id]) { 91 94 this.requests[id](data.error, data.result); 92 95 } else { ··· 94 97 } 95 98 } else if ('sid' in data) { 96 99 const dataKeys = Object.keys(data).filter( 97 - (k) => k !== 'sid' && k !== 'time' && Array.isArray(data[k]) 100 + k => k !== 'sid' && k !== 'time' && Array.isArray(data[k]) 101 + ); 102 + dataKeys.forEach( 103 + k => 104 + this.emit(k, data) || this._warn('no listeners for stream event', k) 98 105 ); 99 - dataKeys.forEach((k) => this.emit(k, data) || this._warn('no listeners for stream event', k)); 100 106 } else { 101 107 this._log('rpc: Unrecognised data', data); 102 108 } ··· 112 118 } 113 119 init({ clientId, clientSecret, license, debit } = {}) { 114 120 const token = this.getUserLogin() 115 - .then((users) => { 121 + .then(users => { 116 122 if (users.length === 0) { 117 123 return Promise.reject(new Error('No logged in user')); 118 124 } ··· 120 126 }) 121 127 .then(({ accessGranted }) => { 122 128 if (!accessGranted) { 123 - return Promise.reject(new Error('Please approve this application in the EMOTIV app')); 129 + return Promise.reject( 130 + new Error('Please approve this application in the EMOTIV app') 131 + ); 124 132 } 125 133 return this.authorize({ 126 134 clientId, 127 135 clientSecret, 128 136 license, 129 - debit, 137 + debit 130 138 }).then(({ cortexToken }) => { 131 139 this._log('init: Got auth token'); 132 140 this._debug('init: Auth token', cortexToken); ··· 138 146 return token; 139 147 } 140 148 close() { 141 - return new Promise((resolve) => { 149 + return new Promise(resolve => { 142 150 this.ws.close(); 143 151 this.ws.once('close', resolve); 144 152 }); ··· 162 170 } 163 171 defineMethod(methodName, paramDefs = []) { 164 172 if (this[methodName]) return; 165 - const needsAuth = paramDefs.some((p) => p.name === 'cortexToken'); 166 - const requiredParams = paramDefs.filter((p) => p.required).map((p) => p.name); 173 + const needsAuth = paramDefs.some(p => p.name === 'cortexToken'); 174 + const requiredParams = paramDefs.filter(p => p.required).map(p => p.name); 167 175 168 176 this[methodName] = (params = {}) => { 169 177 if (needsAuth && this.cortexToken && !params.cortexToken) { 170 178 params = Object.assign({}, params, { cortexToken: this.cortexToken }); 171 179 } 172 - const missingParams = requiredParams.filter((p) => params[p] == null); 180 + const missingParams = requiredParams.filter(p => params[p] == null); 173 181 if (missingParams.length > 0) { 174 182 return this.handleError( 175 - new Error(`Missing required params for ${methodName}: ${missingParams.join(', ')}`) 183 + new Error( 184 + `Missing required params for ${methodName}: ${missingParams.join( 185 + ', ' 186 + )}` 187 + ) 176 188 ); 177 189 } 178 190 return this.call(methodName, params);
+14 -14
app/utils/eeg/emotiv.js
··· 30 30 return devices; 31 31 }; 32 32 33 - export const connectToEmotiv = async (device) => { 33 + export const connectToEmotiv = async device => { 34 34 await client.ready; 35 35 36 36 // Authenticate ··· 39 39 clientId: CLIENT_ID, 40 40 clientSecret: CLIENT_SECRET, 41 41 license: LICENSE_ID, 42 - debit: 1, 42 + debit: 1 43 43 }); 44 44 } catch (err) { 45 45 toast.error(`Authentication failed. ${err.message}`); ··· 56 56 try { 57 57 const newSession = await client.createSession({ 58 58 status: 'active', 59 - headset: device.id, 59 + headset: device.id 60 60 }); 61 61 session = newSession; 62 62 63 63 return { 64 64 name: session.headset.id, 65 65 samplingRate: session.headset.settings.eegRate, 66 - channels: EMOTIV_CHANNELS, 66 + channels: EMOTIV_CHANNELS 67 67 }; 68 68 } catch (err) { 69 69 toast.error(`Session creation failed. ${err.message} `); ··· 74 74 console.log('disconnecting form emotiv'); 75 75 const sessionStatus = await client.updateSession({ 76 76 session: session.id, 77 - status: 'close', 77 + status: 'close' 78 78 }); 79 79 return sessionStatus; 80 80 }; ··· 87 87 try { 88 88 await client.subscribe({ 89 89 session: session.id, 90 - streams: ['eeg', 'dev'], 90 + streams: ['eeg', 'dev'] 91 91 }); 92 92 } catch (err) { 93 93 toast.error(`EEG connection failed. ${err.message}`); ··· 97 97 }; 98 98 99 99 // Creates an observable that will epoch, filter, and add signal quality to EEG stream 100 - export const createEmotivSignalQualityObservable = (rawObservable) => { 100 + export const createEmotivSignalQualityObservable = rawObservable => { 101 101 const signalQualityObservable = fromEvent(client, 'dev'); 102 102 const samplingRate = 128; 103 103 const channels = EMOTIV_CHANNELS; ··· 105 105 return rawObservable.pipe( 106 106 addInfo({ 107 107 samplingRate, 108 - channels, 108 + channels 109 109 }), 110 110 epoch({ 111 111 duration: intervalSamples, 112 - interval: intervalSamples, 112 + interval: intervalSamples 113 113 }), 114 114 bandpassFilter({ 115 115 nbChannels: channels.length, 116 116 lowCutoff: 1, 117 - highCutoff: 50, 117 + highCutoff: 50 118 118 }), 119 119 withLatestFrom(signalQualityObservable, integrateSignalQuality), 120 120 parseEmotivSignalQuality(), ··· 129 129 export const createEmotivRecord = (subjectName, sessionNumber) => { 130 130 client.createRecord({ 131 131 session: session.id, 132 - title: `${subjectName}_${sessionNumber}`, 132 + title: `${subjectName}_${sessionNumber}` 133 133 }); 134 134 }; 135 135 ··· 144 144 // 14 EEG channels in data 145 145 // timestamp in ms 146 146 // Event marker in marker if present 147 - const createEEGSample = (eegEvent) => { 147 + const createEEGSample = eegEvent => { 148 148 const prunedArray = new Array(EMOTIV_CHANNELS.length); 149 149 for (let i = 0; i < EMOTIV_CHANNELS.length; i++) { 150 150 prunedArray[i] = eegEvent.eeg[i + 2]; ··· 163 163 ...newEpoch, 164 164 signalQuality: Object.assign( 165 165 ...devSample.dev[2].map((signalQuality, index) => ({ 166 - [EMOTIV_CHANNELS[index]]: signalQuality, 166 + [EMOTIV_CHANNELS[index]]: signalQuality 167 167 })) 168 - ), 168 + ) 169 169 });
+23 -11
app/utils/eeg/muse.js
··· 1 1 import 'hazardous'; 2 2 import { withLatestFrom, share, startWith, filter } from 'rxjs/operators'; 3 - import { addInfo, epoch, bandpassFilter, addSignalQuality } from '@neurosity/pipes'; 3 + import { 4 + addInfo, 5 + epoch, 6 + bandpassFilter, 7 + addSignalQuality 8 + } from '@neurosity/pipes'; 4 9 import { release } from 'os'; 5 10 import { MUSE_SERVICE, MuseClient, zipSamples } from 'muse-js'; 6 11 import { from } from 'rxjs'; 7 12 import { parseMuseSignalQuality } from './pipes'; 8 - import { MUSE_SAMPLING_RATE, MUSE_CHANNELS, PLOTTING_INTERVAL } from '../../constants/constants'; 13 + import { 14 + MUSE_SAMPLING_RATE, 15 + MUSE_CHANNELS, 16 + PLOTTING_INTERVAL 17 + } from '../../constants/constants'; 9 18 10 19 const INTER_SAMPLE_INTERVAL = -(1 / 256) * 1000; 11 20 ··· 27 36 return null; 28 37 } 29 38 device = await bluetooth.requestDevice({ 30 - filters: [{ services: [MUSE_SERVICE] }], 39 + filters: [{ services: [MUSE_SERVICE] }] 31 40 }); 32 41 } else { 33 42 device = await navigator.bluetooth.requestDevice({ 34 - filters: [{ services: [MUSE_SERVICE] }], 43 + filters: [{ services: [MUSE_SERVICE] }] 35 44 }); 36 45 } 37 46 return [device]; 38 47 }; 39 48 40 49 // Attempts to connect to a muse device. If successful, returns a device info object 41 - export const connectToMuse = async (device) => { 50 + export const connectToMuse = async device => { 42 51 if (process.platform === 'win32') { 43 52 const gatt = await device.gatt.connect(); 44 53 await client.connect(gatt); ··· 48 57 return { 49 58 name: client.deviceName, 50 59 samplingRate: MUSE_SAMPLING_RATE, 51 - channels: MUSE_CHANNELS, 60 + channels: MUSE_CHANNELS 52 61 }; 53 62 }; 54 63 ··· 60 69 const eegStream = await client.eegReadings; 61 70 const markers = await client.eventMarkers.pipe(startWith({ timestamp: 0 })); 62 71 return from(zipSamples(eegStream)).pipe( 63 - filter((sample) => !sample.data.includes(NaN)), 72 + filter(sample => !sample.data.includes(NaN)), 64 73 withLatestFrom(markers, synchronizeTimestamp), 65 74 share() 66 75 ); 67 76 }; 68 77 69 78 // Creates an observable that will epoch, filter, and add signal quality to EEG stream 70 - export const createMuseSignalQualityObservable = (rawObservable, deviceInfo) => { 79 + export const createMuseSignalQualityObservable = ( 80 + rawObservable, 81 + deviceInfo 82 + ) => { 71 83 const { samplingRate, channels: channelNames } = deviceInfo; 72 84 const intervalSamples = (PLOTTING_INTERVAL * samplingRate) / 1000; 73 85 return rawObservable.pipe( 74 86 addInfo({ 75 87 samplingRate, 76 - channelNames, 88 + channelNames 77 89 }), 78 90 epoch({ 79 91 duration: intervalSamples, 80 - interval: intervalSamples, 92 + interval: intervalSamples 81 93 }), 82 94 bandpassFilter({ 83 95 nbChannels: channelNames.length, 84 96 lowCutoff: 1, 85 - highCutoff: 50, 97 + highCutoff: 50 86 98 }), 87 99 addSignalQuality(), 88 100 parseMuseSignalQuality()
+34 -27
app/utils/eeg/pipes.js
··· 1 1 import { pipe } from 'rxjs'; 2 2 import { map } from 'rxjs/operators'; 3 - import { SIGNAL_QUALITY, SIGNAL_QUALITY_THRESHOLDS } from '../../constants/constants'; 3 + import { 4 + SIGNAL_QUALITY, 5 + SIGNAL_QUALITY_THRESHOLDS 6 + } from '../../constants/constants'; 4 7 5 8 export const parseMuseSignalQuality = () => 6 9 pipe( 7 - map((epoch) => ({ 10 + map(epoch => ({ 8 11 ...epoch, 9 12 signalQuality: Object.assign( 10 13 {}, 11 - ...Object.entries(epoch.signalQuality).map(([channelName, signalQuality]) => { 12 - if (signalQuality >= SIGNAL_QUALITY_THRESHOLDS.BAD) { 13 - return { [channelName]: SIGNAL_QUALITY.BAD }; 14 + ...Object.entries(epoch.signalQuality).map( 15 + ([channelName, signalQuality]) => { 16 + if (signalQuality >= SIGNAL_QUALITY_THRESHOLDS.BAD) { 17 + return { [channelName]: SIGNAL_QUALITY.BAD }; 18 + } 19 + if (signalQuality >= SIGNAL_QUALITY_THRESHOLDS.OK) { 20 + return { [channelName]: SIGNAL_QUALITY.OK }; 21 + } 22 + if (signalQuality >= SIGNAL_QUALITY_THRESHOLDS.GREAT) { 23 + return { [channelName]: SIGNAL_QUALITY.GREAT }; 24 + } 25 + return { [channelName]: SIGNAL_QUALITY.DISCONNECTED }; 14 26 } 15 - if (signalQuality >= SIGNAL_QUALITY_THRESHOLDS.OK) { 16 - return { [channelName]: SIGNAL_QUALITY.OK }; 17 - } 18 - if (signalQuality >= SIGNAL_QUALITY_THRESHOLDS.GREAT) { 19 - return { [channelName]: SIGNAL_QUALITY.GREAT }; 20 - } 21 - return { [channelName]: SIGNAL_QUALITY.DISCONNECTED }; 22 - }) 23 - ), 27 + ) 28 + ) 24 29 })) 25 30 ); 26 31 27 32 export const parseEmotivSignalQuality = () => 28 33 pipe( 29 - map((epoch) => ({ 34 + map(epoch => ({ 30 35 ...epoch, 31 36 signalQuality: Object.assign( 32 37 {}, 33 - ...Object.entries(epoch.signalQuality).map(([channelName, signalQuality]) => { 34 - if (signalQuality === 0) { 35 - return { [channelName]: SIGNAL_QUALITY.DISCONNECTED }; 36 - } 37 - if (signalQuality === 3) { 38 - return { [channelName]: SIGNAL_QUALITY.OK }; 39 - } 40 - if (signalQuality === 4) { 41 - return { [channelName]: SIGNAL_QUALITY.GREAT }; 38 + ...Object.entries(epoch.signalQuality).map( 39 + ([channelName, signalQuality]) => { 40 + if (signalQuality === 0) { 41 + return { [channelName]: SIGNAL_QUALITY.DISCONNECTED }; 42 + } 43 + if (signalQuality === 3) { 44 + return { [channelName]: SIGNAL_QUALITY.OK }; 45 + } 46 + if (signalQuality === 4) { 47 + return { [channelName]: SIGNAL_QUALITY.GREAT }; 48 + } 49 + return { [channelName]: SIGNAL_QUALITY.BAD }; 42 50 } 43 - return { [channelName]: SIGNAL_QUALITY.BAD }; 44 - }) 45 - ), 51 + ) 52 + ) 46 53 })) 47 54 );
+6 -6
app/utils/filesystem/dialog.js
··· 17 17 } 18 18 }; 19 19 20 - const selectTimeline = (event) => { 20 + const selectTimeline = event => { 21 21 dialog.showOpenDialog( 22 22 { 23 23 title: 'Select a jsPsych timeline file', 24 - properties: ['openFile', 'promptToCreate'], 24 + properties: ['openFile', 'promptToCreate'] 25 25 }, 26 - (filePaths) => { 26 + filePaths => { 27 27 if (filePaths) { 28 28 event.sender.send('loadDialogReply', filePaths[0]); 29 29 } ··· 31 31 ); 32 32 }; 33 33 34 - const selectStimulusFolder = (event) => { 34 + const selectStimulusFolder = event => { 35 35 dialog.showOpenDialog( 36 36 { 37 37 title: 'Select a folder of images', 38 - properties: ['openDirectory'], 38 + properties: ['openDirectory'] 39 39 }, 40 - (dir) => { 40 + dir => { 41 41 if (dir) { 42 42 event.sender.send('loadDialogReply', dir[0]); 43 43 } else {
+1 -1
app/utils/filesystem/select.js
··· 8 8 import { FILE_TYPES } from '../../constants/constants'; 9 9 10 10 export const loadFromSystemDialog = (fileType: FILE_TYPES) => 11 - new Promise((resolve) => { 11 + new Promise(resolve => { 12 12 ipcRenderer.send('loadDialog', fileType); 13 13 ipcRenderer.on('loadDialogReply', (event, result) => { 14 14 resolve(result);
+54 -34
app/utils/filesystem/storage.js
··· 19 19 // Creating and Getting 20 20 21 21 // Creates a new directory for a given workspace with the passed title if it doesn't already exist 22 - export const createWorkspaceDir = (title: string) => mkdirPathSync(getWorkspaceDir(title)); 22 + export const createWorkspaceDir = (title: string) => 23 + mkdirPathSync(getWorkspaceDir(title)); 23 24 24 25 // Gets the absolute path for a workspace from a given title 25 26 export const getWorkspaceDir = (title: string) => path.join(workspaces, title); 26 27 27 28 // Opens a workspace folder in explorer (or other native OS filesystem browser) 28 - export const openWorkspaceDir = (title: string) => shell.openItem(path.join(workspaces, title)); 29 + export const openWorkspaceDir = (title: string) => 30 + shell.openItem(path.join(workspaces, title)); 29 31 30 32 // ----------------------------------------------------------------------------------------------- 31 33 // Storing ··· 42 44 }; 43 45 44 46 export const restoreExperimentState = (state: ExperimentStateType) => { 45 - if(state.type !== 'NONE'){ 47 + if (state.type !== 'NONE') { 46 48 const timestampedState = { 47 49 ...state, 48 50 subject: '', ··· 54 56 JSON.stringify(timestampedState) 55 57 ); 56 58 } 57 - } 59 + }; 58 60 59 61 export const storeBehaviouralData = ( 60 62 csv: string, ··· 66 68 const dir = path.join(getWorkspaceDir(title), 'Data', subject, 'Behavior'); 67 69 const filename = `${subject}-${group}-${session}-behavior.csv`; 68 70 mkdirPathSync(dir); 69 - fs.writeFile(path.join(dir, filename), csv, (err) => { 71 + fs.writeFile(path.join(dir, filename), csv, err => { 70 72 if (err) { 71 73 console.error(err); 72 74 } ··· 74 76 }; 75 77 76 78 // Stores an image to workspace dir 77 - export const storeJupyterImage = (title: string, imageTitle: string, rawData: Buffer) => { 79 + export const storeJupyterImage = ( 80 + title: string, 81 + imageTitle: string, 82 + rawData: Buffer 83 + ) => { 78 84 const dir = path.join(getWorkspaceDir(title), 'Results', 'Images'); 79 85 const filename = `${imageTitle}.png`; 80 86 mkdirPathSync(dir); 81 - fs.writeFile(path.join(dir, filename), rawData, (err) => { 87 + fs.writeFile(path.join(dir, filename), rawData, err => { 82 88 if (err) { 83 89 console.error(err); 84 90 } ··· 91 97 // Returns a list of workspaces in the workspaces directory. Will make the workspaces dir if it doesn't exist yet 92 98 export const readWorkspaces = () => { 93 99 try { 94 - return fs.readdirSync(workspaces).filter((workspace) => workspace !== '.DS_Store'); 100 + return fs 101 + .readdirSync(workspaces) 102 + .filter(workspace => workspace !== '.DS_Store'); 95 103 } catch (e) { 96 104 if (e.code === 'ENOENT') { 97 105 mkdirPathSync(workspaces); ··· 106 114 try { 107 115 const files = await recursive(getWorkspaceDir(title)); 108 116 const rawFiles = files 109 - .filter((filepath) => filepath.slice(-7).includes('raw.csv')) 110 - .map((filepath) => ({ 117 + .filter(filepath => filepath.slice(-7).includes('raw.csv')) 118 + .map(filepath => ({ 111 119 name: path.basename(filepath), 112 - path: filepath, 120 + path: filepath 113 121 })); 114 122 return rawFiles; 115 123 } catch (e) { ··· 125 133 try { 126 134 const files = await recursive(getWorkspaceDir(title)); 127 135 return files 128 - .filter((filepath) => filepath.slice(-7).includes('epo.fif')) 129 - .map((filepath) => ({ 136 + .filter(filepath => filepath.slice(-7).includes('epo.fif')) 137 + .map(filepath => ({ 130 138 name: path.basename(filepath), 131 - path: filepath, 139 + path: filepath 132 140 })); 133 141 } catch (e) { 134 142 console.log(e); ··· 141 149 try { 142 150 const files = await recursive(getWorkspaceDir(title)); 143 151 const behaviorFiles = files 144 - .filter((filepath) => filepath.slice(-12).includes('behavior.csv')) 145 - .map((filepath) => ({ 152 + .filter(filepath => filepath.slice(-12).includes('behavior.csv')) 153 + .map(filepath => ({ 146 154 name: path.basename(filepath), 147 - path: filepath, 155 + path: filepath 148 156 })); 149 157 return behaviorFiles; 150 158 } catch (e) { ··· 158 166 // Reads an experiment state tree from disk and parses it from JSON 159 167 export const readAndParseState = (dir: string) => { 160 168 try { 161 - return JSON.parse(fs.readFileSync(path.join(workspaces, dir, 'appState.json'))); 169 + return JSON.parse( 170 + fs.readFileSync(path.join(workspaces, dir, 'appState.json')) 171 + ); 162 172 } catch (e) { 163 173 if (e.code === 'ENOENT') { 164 174 console.log('appState does not exist for recent workspace'); ··· 169 179 170 180 // Reads a list of images that are in a directory 171 181 export const readImages = (dir: string) => 172 - fs.readdirSync(dir).filter((filename) => { 182 + fs.readdirSync(dir).filter(filename => { 173 183 const extension = filename.slice(-3).toLowerCase(); 174 184 return ( 175 - extension === 'png' || extension === 'jpg' || extension === 'gif' || extension === 'peg' // support .jpeg? 185 + extension === 'png' || 186 + extension === 'jpg' || 187 + extension === 'gif' || 188 + extension === 'peg' // support .jpeg? 176 189 ); 177 190 }); 178 191 179 192 // Returns an array of images that are used in a timeline for use in preloading 180 193 export const getImages = (params: ExperimentParameters) => 181 194 readdirSync(params.stimulus1.dir) 182 - .map((filename) => path.join(params.stimulus1.dir, filename)) 195 + .map(filename => path.join(params.stimulus1.dir, filename)) 183 196 .concat( 184 - readdirSync(params.stimulus2.dir).map((filename) => path.join(params.stimulus2.dir, filename)) 197 + readdirSync(params.stimulus2.dir).map(filename => 198 + path.join(params.stimulus2.dir, filename) 199 + ) 185 200 ); 186 - 187 201 188 202 // ----------------------------------------------------------------------------------------------- 189 203 // Util 190 204 191 205 export const getSubjectNamesFromFiles = (filePaths: Array<?string>) => 192 206 filePaths 193 - .map((filePath) => path.basename(filePath)) 194 - .map((fileName) => fileName.substring(0, fileName.indexOf('-'))); 207 + .map(filePath => path.basename(filePath)) 208 + .map(fileName => fileName.substring(0, fileName.indexOf('-'))); 195 209 196 210 // Read CSV files with behavioral data and return an object 197 211 export const readBehaviorData = (files: Array<?string>) => { 198 212 try { 199 - return files.map((file) => { 213 + return files.map(file => { 200 214 const csv = fs.readFileSync(file, 'utf-8'); 201 215 const obj = convertCSVToObject(csv); 202 216 obj.meta.datafile = file; ··· 217 231 dialog.showSaveDialog( 218 232 { 219 233 title: 'Select a folder to save the data', 220 - defaultPath: path.join(getWorkspaceDir(title), 'Data', `aggregated.csv`), 234 + defaultPath: path.join(getWorkspaceDir(title), 'Data', `aggregated.csv`) 221 235 }, 222 - (filename) => { 236 + filename => { 223 237 if (filename && typeof filename !== 'undefined') { 224 - fs.writeFile(filename, data, (err) => { 238 + fs.writeFile(filename, data, err => { 225 239 if (err) console.error(err); 226 240 }); 227 241 } ··· 230 244 }; 231 245 232 246 // convert a csv file to an object with Papaparse 233 - const convertCSVToObject = (csv) => { 247 + const convertCSVToObject = csv => { 234 248 const data = Papa.parse(csv, { 235 - header: true, 249 + header: true 236 250 }); 237 251 return data; 238 252 }; 239 253 240 254 // convert an object to a csv file with Papaparse 241 - const convertObjectToSCV = (data) => { 255 + const convertObjectToSCV = data => { 242 256 const csv = Papa.unparse(data); 243 257 return csv; 244 258 }; ··· 250 264 251 265 // Check whether the file with the given name already exists in the filesystem 252 266 export const checkFileExists = (title, subject, filename) => { 253 - const file = path.join(getWorkspaceDir(title), 'Data', subject, 'Behavior', filename); 267 + const file = path.join( 268 + getWorkspaceDir(title), 269 + 'Data', 270 + subject, 271 + 'Behavior', 272 + filename 273 + ); 254 274 const fileExists = fs.existsSync(file); 255 275 return fileExists; 256 - } 276 + };
+5 -2
app/utils/filesystem/write.js
··· 30 30 31 31 // Writes the header for a simple CSV EEG file format. 32 32 // timestamp followed by channels, followed by markers 33 - export const writeHeader = (writeStream: fs.WriteStream, channels: Array<string>) => { 33 + export const writeHeader = ( 34 + writeStream: fs.WriteStream, 35 + channels: Array<string> 36 + ) => { 34 37 try { 35 38 const headerLabels = `Timestamp,${channels.join(',')},Marker\n`; 36 39 writeStream.write(headerLabels); ··· 57 60 // Helper functions 58 61 59 62 // Creates a directory path if it doesn't exist 60 - export const mkdirPathSync = (dirPath) => { 63 + export const mkdirPathSync = dirPath => { 61 64 mkdirp.sync(dirPath); 62 65 };
+28 -15
app/utils/jupyter/cells.js
··· 16 16 'import numpy as np', 17 17 'import seaborn as sns', 18 18 'from matplotlib import pyplot as plt', 19 - "plt.style.use('fivethirtyeight')", 19 + "plt.style.use('fivethirtyeight')" 20 20 ].join('\n'); 21 21 22 - export const utils = () => readFileSync(path.join(__dirname, '/utils/jupyter/utils.py'), 'utf8'); 22 + export const utils = () => 23 + readFileSync(path.join(__dirname, '/utils/jupyter/utils.py'), 'utf8'); 23 24 24 25 export const loadCSV = (filePathArray: Array<string>) => 25 26 [ 26 - `files = [${filePathArray.map((filePath) => formatFilePath(filePath))}]`, 27 + `files = [${filePathArray.map(filePath => formatFilePath(filePath))}]`, 27 28 `replace_ch_names = None`, 28 - `raw = load_data(files, replace_ch_names)`, 29 + `raw = load_data(files, replace_ch_names)` 29 30 ].join('\n'); 30 31 31 32 export const loadCleanedEpochs = (filePathArray: Array<string>) => 32 33 [ 33 - `files = [${filePathArray.map((filePath) => formatFilePath(filePath))}]`, 34 + `files = [${filePathArray.map(filePath => formatFilePath(filePath))}]`, 34 35 `clean_epochs = concatenate_epochs([read_epochs(file) for file in files])`, 35 - `conditions = OrderedDict({key: [value] for (key, value) in clean_epochs.event_id.items()})`, 36 + `conditions = OrderedDict({key: [value] for (key, value) in clean_epochs.event_id.items()})` 36 37 ].join('\n'); 37 38 38 39 // NOTE: this command includes a ';' to prevent returning data 39 40 export const filterIIR = (lowCutoff: number, highCutoff: number) => 40 41 `raw.filter(${lowCutoff}, ${highCutoff}, method='iir');`; 41 42 42 - export const plotPSD = () => [`%matplotlib inline`, `raw.plot_psd(fmin=1, fmax=30)`].join('\n'); 43 + export const plotPSD = () => 44 + [`%matplotlib inline`, `raw.plot_psd(fmin=1, fmax=30)`].join('\n'); 43 45 44 46 export const epochEvents = ( 45 47 eventIDs: { [string]: number }, ··· 47 49 tmax: number, 48 50 reject?: Array<string> | string = 'None' 49 51 ) => { 50 - const IDs = Object.keys(eventIDs).filter(k => k !== "").reduce((res,key) => (res[key] = eventIDs[key], res), {} ) 52 + const IDs = Object.keys(eventIDs) 53 + .filter(k => k !== '') 54 + .reduce((res, key) => ((res[key] = eventIDs[key]), res), {}); 51 55 const command = [ 52 56 `event_id = ${JSON.stringify(IDs)}`, 53 57 `tmin=${tmin}`, ··· 59 63 `raw_epochs = Epochs(raw, events=events, event_id=event_id, 60 64 tmin=tmin, tmax=tmax, baseline=baseline, reject=reject, preload=True, 61 65 verbose=False, picks=picks)`, 62 - `conditions = OrderedDict({key: [value] for (key, value) in raw_epochs.event_id.items()})`, 66 + `conditions = OrderedDict({key: [value] for (key, value) in raw_epochs.event_id.items()})` 63 67 ].join('\n'); 64 68 return command; 65 69 }; 66 70 67 - export const requestEpochsInfo = (variableName: string) => `get_epochs_info(${variableName})`; 71 + export const requestEpochsInfo = (variableName: string) => 72 + `get_epochs_info(${variableName})`; 68 73 69 - export const requestChannelInfo = () => `[ch for ch in clean_epochs.ch_names if ch != 'Marker']`; 74 + export const requestChannelInfo = () => 75 + `[ch for ch in clean_epochs.ch_names if ch != 'Marker']`; 70 76 71 77 export const cleanEpochsPlot = () => 72 78 [ 73 79 `%matplotlib`, 74 - `raw_epochs.plot(scalings='auto', n_epochs=6, title="Clean Data", events=None)`, 80 + `raw_epochs.plot(scalings='auto', n_epochs=6, title="Clean Data", events=None)` 75 81 ].join('\n'); 76 82 77 83 export const plotTopoMap = () => ··· 81 87 [ 82 88 `%matplotlib inline`, 83 89 `X, y = plot_conditions(clean_epochs, ch_ind=${channelIndex}, conditions=conditions, 84 - ci=97.5, n_boot=1000, title='', diff_waveform=None)`, 90 + ci=97.5, n_boot=1000, title='', diff_waveform=None)` 85 91 ].join('\n'); 86 92 87 93 export const saveEpochs = (workspaceDir: string, subject: string) => 88 94 `raw_epochs.save(${formatFilePath( 89 - path.join(workspaceDir, 'Data', subject, 'EEG', `${subject}-cleaned-epo.fif`) 95 + path.join( 96 + workspaceDir, 97 + 'Data', 98 + subject, 99 + 'EEG', 100 + `${subject}-cleaned-epo.fif` 101 + ) 90 102 )})`; 91 103 92 104 // ------------------------------------------- 93 105 // Helper methods 94 106 95 - const formatFilePath = (filePath: string) => `"${filePath.replace(/\\/g, '/')}"`; 107 + const formatFilePath = (filePath: string) => 108 + `"${filePath.replace(/\\/g, '/')}"`;
+2 -1
app/utils/jupyter/functions.js
··· 1 1 import { KERNEL_STATUS } from '../../constants/constants'; 2 2 3 - export const parseSingleQuoteJSON = (string: string) => JSON.parse(string.replace(/'/g, '"')); 3 + export const parseSingleQuoteJSON = (string: string) => 4 + JSON.parse(string.replace(/'/g, '"')); 4 5 5 6 export const parseKernelStatus = (msg: Object) => { 6 7 switch (msg['content']['execution_state']) {
+5 -3
app/utils/jupyter/pipes.js
··· 5 5 6 6 // Refactor this so command can be calculated either up stream or inside pipe 7 7 export const execute = (command, state$) => 8 - pipe(map(() => state$.value.jupyter.mainChannel.next(executeRequest(command)))); 8 + pipe( 9 + map(() => state$.value.jupyter.mainChannel.next(executeRequest(command))) 10 + ); 9 11 10 - export const awaitOkMessage = (action$) => 12 + export const awaitOkMessage = action$ => 11 13 pipe( 12 14 mergeMap(() => 13 15 action$.ofType(RECEIVE_EXECUTE_REPLY).pipe( 14 16 pluck('payload'), 15 - filter((msg) => msg.channel === 'shell' && msg.content.status === 'ok'), 17 + filter(msg => msg.channel === 'shell' && msg.content.status === 'ok'), 16 18 take(1) 17 19 ) 18 20 )
+5 -1
app/utils/labjs/functions.js
··· 9 9 import { buildSearchTimeline } from './protocols/search'; 10 10 import { buildCustomTimeline } from './protocols/custom'; 11 11 12 - import { MainTimeline, Trial, ExperimentParameters } from '../../constants/interfaces'; 12 + import { 13 + MainTimeline, 14 + Trial, 15 + ExperimentParameters 16 + } from '../../constants/interfaces'; 13 17 14 18 // loads a protocol of the experiment 15 19 export const loadProtocol = (paradigm: EXPERIMENTS) => {
+22 -12
app/utils/labjs/index.js
··· 32 32 case 'Faces and Houses': 33 33 faceshouses.parameters = props.settings.params; 34 34 faceshouses.parameters.title = props.settings.title; 35 - faceshouses.files = props.settings.params.stimuli.map(image => ( 36 - { [path.join(image.dir, image.filename)] : path.join(image.dir, image.filename) } 37 - )).reduce((obj, item) => { 38 - obj[Object.keys(item)[0]] = Object.values(item)[0] 35 + faceshouses.files = props.settings.params.stimuli 36 + .map(image => ({ 37 + [path.join(image.dir, image.filename)]: path.join( 38 + image.dir, 39 + image.filename 40 + ) 41 + })) 42 + .reduce((obj, item) => { 43 + obj[Object.keys(item)[0]] = Object.values(item)[0]; 39 44 return obj; 40 45 }, {}); 41 46 this.study = lab.util.fromObject(clonedeep(faceshouses), lab); ··· 44 49 default: 45 50 custom.parameters = props.settings.params; 46 51 custom.parameters.title = props.settings.title; 47 - custom.files = props.settings.params.stimuli.map(image => ( 48 - { [path.join(image.dir, image.filename)] : path.join(image.dir, image.filename) } 49 - )).reduce((obj, item) => { 50 - obj[Object.keys(item)[0]] = Object.values(item)[0] 52 + custom.files = props.settings.params.stimuli 53 + .map(image => ({ 54 + [path.join(image.dir, image.filename)]: path.join( 55 + image.dir, 56 + image.filename 57 + ) 58 + })) 59 + .reduce((obj, item) => { 60 + obj[Object.keys(item)[0]] = Object.values(item)[0]; 51 61 return obj; 52 62 }, {}); 53 63 this.study = lab.util.fromObject(clonedeep(custom), lab); ··· 59 69 this.study = undefined; 60 70 props.settings.on_finish(csv); 61 71 }); 62 - this.study.parameters.callbackForEEG = (e) => { 72 + this.study.parameters.callbackForEEG = e => { 63 73 props.settings.eventCallback(e, new Date().getTime()); 64 74 }; 65 - this.study.options.events['keydown'] = async (e) => { 75 + this.study.options.events['keydown'] = async e => { 66 76 if (e.code === 'Escape') { 67 77 if (this.study) { 68 78 await this.study.internals.controller.audioContext.close(); ··· 85 95 86 96 render() { 87 97 return ( 88 - <div className='container fullscreen' data-labjs-section='main'> 89 - <main className='content-vertical-center content-horizontal-center'> 98 + <div className="container fullscreen" data-labjs-section="main"> 99 + <main className="content-vertical-center content-horizontal-center"> 90 100 <div> 91 101 <h2>Loading Experiment</h2> 92 102 <p>The experiment is loading and should start in a few seconds</p>
+46 -30
app/utils/labjs/lab.css
··· 8 8 --border-radius-container: 5px; 9 9 --border-radius-content: 4px; 10 10 /* Typography */ 11 - --font-family: "Arial", sans-serif; 11 + --font-family: 'Arial', sans-serif; 12 12 --font-family-mono: Droid Mono, Menlo, Consolas, monospace; 13 13 --font-size: 18px; 14 14 --line-height: 1.4em; ··· 26 26 :root { 27 27 box-sizing: border-box; 28 28 } 29 - *, *::before, *::after { 29 + *, 30 + *::before, 31 + *::after { 30 32 box-sizing: inherit; 31 33 } 32 34 ··· 49 51 /* width: 900px; 50 52 width: var(--width-container); */ 51 53 } 52 - header, footer, main { 54 + header, 55 + footer, 56 + main { 53 57 padding: 24px; 54 58 padding: var(--padding-internal); 55 59 } 56 60 /* Individual parts: Height, borders and background */ 57 - header, footer { 61 + header, 62 + footer { 58 63 min-height: 8vh; 59 64 min-height: var(--height-min-header-footer); 60 65 } ··· 113 118 } 114 119 .container.fullscreen { 115 120 /* IE11 miscalculates the height, so add some slack */ 116 - min-height: calc(100vh - 3*24px); 117 - min-height: calc(100vh - 3*var(--padding-internal)); 121 + min-height: calc(100vh - 3 * 24px); 122 + min-height: calc(100vh - 3 * var(--padding-internal)); 118 123 } 119 124 } 120 125 ··· 138 143 139 144 /* Typography */ 140 145 :root { 141 - font-family: "Arial", sans-serif; 146 + font-family: 'Arial', sans-serif; 142 147 font-family: var(--font-family); 143 148 font-size: 18px; 144 149 font-size: var(--font-size); 145 150 line-height: 1.4em; 146 151 line-height: var(--line-height); 147 152 } 148 - header, footer, main { 153 + header, 154 + footer, 155 + main { 149 156 /* Set display style explicitly for legacy browsers 150 157 that are unfamiliar with these elements */ 151 158 display: block; 152 159 text-align: center; 153 160 } 154 - h1, h2, h3 { 161 + h1, 162 + h2, 163 + h3 { 155 164 line-height: 1.4em; 156 165 line-height: var(--line-height); 157 166 } 158 167 hr { 159 168 border: none; 160 169 border-top: 2px solid #e5e5e5; 161 - border-top: 2px solid var(--color-border) 170 + border-top: 2px solid var(--color-border); 162 171 } 163 172 164 173 /* Special elements: Keyboard buttons */ ··· 277 286 .text-muted a { 278 287 color: rgb(60, 89, 156); 279 288 } 280 - small, .small { 289 + small, 290 + .small { 281 291 font-size: 0.9rem; 282 292 } 283 293 .font-weight-bold { ··· 303 313 display: none; 304 314 } 305 315 .hide-if-empty:empty { 306 - display: none 316 + display: none; 307 317 } 308 318 309 319 /* Alerts */ ··· 325 335 } 326 336 327 337 /* Background styles (experimental) */ 328 - .alert, .background-dark { 338 + .alert, 339 + .background-dark { 329 340 background-color: #f8f8f8; 330 341 background-color: var(--color-gray-background); 331 342 } 332 - .alert.alert-danger, .background-danger { 343 + .alert.alert-danger, 344 + .background-danger { 333 345 background-color: #e9afaf; 334 346 } 335 - .alert.alert-warning, .background-warning { 347 + .alert.alert-warning, 348 + .background-warning { 336 349 background-color: #ffe6a5; 337 350 } 338 351 .background-ok { ··· 340 353 } 341 354 342 355 /* Form elements */ 343 - input, select, button, textarea { 344 - font-family: "Arial", sans-serif; 356 + input, 357 + select, 358 + button, 359 + textarea { 360 + font-family: 'Arial', sans-serif; 345 361 font-family: var(--font-family); 346 362 font-size: 0.9rem; 347 363 line-height: 1.4em; ··· 353 369 margin: 8px 0; 354 370 padding: 8px; 355 371 } 356 - input[type="checkbox"] { 372 + input[type='checkbox'] { 357 373 margin: 0 10px; 358 374 } 359 - input[type="range"] { 375 + input[type='range'] { 360 376 border: none; 361 377 } 362 378 input + label { ··· 411 427 width: 100%; 412 428 border-collapse: collapse; 413 429 } 414 - table td, table th { 430 + table td, 431 + table th { 415 432 padding: 10px 8px 8px; 416 433 } 417 434 /* Table borders (except for plain) */ ··· 431 448 /* Striped rows */ 432 449 table.table-striped tr:nth-child(odd) td { 433 450 background-color: #efefef; 434 - background-color: var(--color-border-internal) 451 + background-color: var(--color-border-internal); 435 452 } 436 453 437 454 /* Progress bar */ ··· 459 476 position: absolute; 460 477 top: 0; 461 478 -webkit-animation-duration: 0.5s; 462 - animation-duration: 0.5s; 479 + animation-duration: 0.5s; 463 480 -webkit-animation-name: popover; 464 - animation-name: popover; 481 + animation-name: popover; 465 482 } 466 483 467 484 /* Width, for some reason, needs to be set explicitly */ 468 485 .container.fullscreen .popover { 469 - width: calc(100vw - 2*24px); 470 - width: calc(100vw - 2*var(--padding-internal)); 486 + width: calc(100vw - 2 * 24px); 487 + width: calc(100vw - 2 * var(--padding-internal)); 471 488 } 472 489 473 490 .popover > * { ··· 484 501 @-webkit-keyframes popover { 485 502 from { 486 503 -webkit-transform: translate3d(0, -100%, 0); 487 - transform: translate3d(0, -100%, 0); 504 + transform: translate3d(0, -100%, 0); 488 505 } 489 506 490 507 to { 491 508 -webkit-transform: none; 492 - transform: none; 509 + transform: none; 493 510 } 494 511 } 495 512 @keyframes popover { 496 513 from { 497 514 -webkit-transform: translate3d(0, -100%, 0); 498 - transform: translate3d(0, -100%, 0); 515 + transform: translate3d(0, -100%, 0); 499 516 } 500 517 501 518 to { 502 519 -webkit-transform: none; 503 - transform: none; 520 + transform: none; 504 521 } 505 522 } 506 523 ··· 510 527 padding-top: 24px; 511 528 top: -8px; 512 529 } 513 - 514 530 515 531 /*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uL3NyYy9zdGFydGVya2l0L2xpYi9sYWIuY3NzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLHlCQUF5QjtBQUN6QjtFQUNFLFlBQVk7RUFDWix5QkFBeUI7RUFDekIsNkJBQTZCO0VBQzdCLGdDQUFnQztFQUNoQyx5QkFBeUI7RUFDekIsK0JBQStCO0VBQy9CLDZCQUE2QjtFQUM3QixnQkFBZ0I7RUFDaEIsbUNBQW1DO0VBQ25DLDJEQUEyRDtFQUMzRCxrQkFBa0I7RUFDbEIscUJBQXFCO0VBQ3JCLDhFQUE4RTtFQUM5RSxrQ0FBOEM7RUFBOUMsOENBQThDO0VBQzlDLFlBQVk7RUFDWix3QkFBd0I7RUFDeEIsaUNBQWlDO0VBQ2pDLGlDQUFpQztFQUNqQyw4QkFBOEI7Q0FDL0I7O0FBRUQsMENBQTBDO0FBQzFDO0VBQ0UsdUJBQXVCO0NBQ3hCO0FBQ0Q7RUFDRSxvQkFBb0I7Q0FDckI7O0FBRUQsb0JBQW9CO0FBQ3BCO0VBQ0UsVUFBVTtDQUNYOztBQUVEO0VBQ0UsaUJBQXNDO0VBQXRDLHNDQUFzQztFQUN0QyxnQkFBNEM7RUFBNUMsNENBQTRDO0VBQzVDLHNDQUFzQztFQUN0QyxrQkFBcUM7RUFBckMscUNBQXFDO0VBQ3JDLGFBQThCO0VBQTlCLDhCQUE4QjtDQUMvQjtBQUNEO0VBQ0UsY0FBaUM7RUFBakMsaUNBQWlDO0NBQ2xDO0FBQ0Qsc0RBQXNEO0FBQ3REO0VBQ0UsZ0JBQTRDO0VBQTVDLDRDQUE0QztDQUM3QztBQUNEO0VBQ0UsZ0JBQTRDO0VBQTVDLDRDQUE0QztDQUM3Qzs7QUFFRCx1QkFBdUI7QUFDdkI7RUFDRSwrQkFBK0I7RUFDL0IsYUFBZ0M7RUFBaEMsZ0NBQWdDO0VBQ2hDLGlDQUFvRDtFQUFwRCxvREFBb0Q7RUFDcEQsNEJBQStDO0VBQS9DLCtDQUErQztFQUMvQyxxQ0FBcUM7RUFDckMsY0FBYztFQUNkLHVCQUF1QjtDQUN4QjtBQUNEO0VBQ0Usc0JBQXNCO0VBQ3RCLFFBQVE7Q0FDVDs7QUFFRCxzQkFBc0I7QUFDdEI7RUFDRSxVQUFVO0VBQ1YsYUFBYTtFQUNiLGlCQUFpQjtDQUNsQjtBQUNEO0VBQ0UsYUFBYTtFQUNiLGtCQUFrQjtDQUNuQjs7QUFFRCxtQ0FBbUM7QUFDbkM7RUFDRTtJQUNFLFVBQVU7SUFDVixhQUFhO0lBQ2IsaUJBQWlCO0lBQ2pCLGFBQWE7SUFDYixrQkFBa0I7R0FDbkI7Q0FDRjs7QUFFRCx3RUFBd0U7QUFDeEU7RUFDRTtJQUNFLGNBQWM7R0FDZjtFQUNEO0lBQ0Usc0RBQXNEO0lBQ3RELGlDQUFvRDtJQUFwRCxvREFBb0Q7R0FDckQ7Q0FDRjs7QUFFRCw2QkFBNkI7QUFDN0I7RUFDRSwwQkFBc0M7RUFBdEMsc0NBQXNDO0VBQ3RDLG1CQUE4QztFQUE5Qyw4Q0FBOEM7Q0FDL0M7QUFDRDtFQUNFLGlDQUFzRDtFQUF0RCxzREFBc0Q7Q0FDdkQ7QUFDRDtFQUNFLDhCQUFtRDtFQUFuRCxtREFBbUQ7RUFDbkQsMEJBQStDO0VBQS9DLCtDQUErQztDQUNoRDs7QUFFRCxnQkFBZ0I7QUFDaEI7RUFDRSxpQ0FBZ0M7RUFBaEMsZ0NBQWdDO0VBQ2hDLGdCQUE0QjtFQUE1Qiw0QkFBNEI7RUFDNUIsbUJBQWdDO0VBQWhDLGdDQUFnQztDQUNqQztBQUNEO0VBQ0U7K0NBQzZDO0VBQzdDLGVBQWU7RUFDZixtQkFBbUI7Q0FDcEI7QUFDRDtFQUNFLG1CQUFnQztFQUFoQyxnQ0FBZ0M7Q0FDakM7QUFDRDtFQUNFLGFBQWE7RUFDYiw4QkFBeUM7RUFBekMseUNBQXlDO0NBQzFDOztBQUVELHdDQUF3QztBQUN4QztFQUNFLGlCQUFpQjtFQUNqQixzQkFBc0I7RUFDdEIsa0JBQWtCO0VBQ2xCLG1CQUFtQjtFQUNuQixrQkFBa0I7RUFDbEIscUJBQXFCO0VBQ3JCLFdBQVc7RUFDWCxvREFBcUM7RUFBckMscUNBQXFDO0VBQ3JDLGtCQUFrQjtFQUNsQixtQkFBbUI7RUFDbkIsMkJBQTJCO0VBQzNCLHdCQUF3QjtFQUN4QixtQkFBNEM7RUFBNUMsNENBQTRDO0VBQzVDLHFDQUFxQztDQUN0QztBQUNEO0VBQ0Usa0JBQWtCO0VBQ2xCLHNCQUFzQjtFQUN0Qix3QkFBd0I7Q0FDekI7O0FBRUQsdUJBQXVCO0FBQ3ZCO0VBQ0UsWUFBWTtDQUNiO0FBQ0Q7RUFDRSxpQkFBc0M7RUFBdEMsc0NBQXNDO0NBQ3ZDO0FBQ0Q7RUFDRSw2QkFBa0Q7RUFBbEQsa0RBQWtEO0NBQ25EO0FBQ0Q7RUFDRSwyQkFBZ0Q7RUFBaEQsZ0RBQWdEO0NBQ2pEO0FBQ0Qsc0NBQXNDO0FBQ3RDOzs7Ozs7Ozs7O0VBVUUsY0FBYztDQUNmO0FBQ0Q7RUFDRSx3QkFBd0I7Q0FDekI7QUFDRDtFQUNFLG9CQUFvQjtDQUNyQjtBQUNEO0VBQ0Usc0JBQXNCO0NBQ3ZCO0FBQ0Q7RUFDRSw0QkFBNEI7Q0FDN0I7QUFDRDtFQUNFLHdCQUF3QjtDQUN6QjtBQUNEO0VBQ0UsMEJBQTBCO0NBQzNCO0FBQ0Q7O0VBRUUsK0JBQStCO0NBQ2hDO0FBQ0Q7O0VBRUUsOEJBQThCO0NBQy9CO0FBQ0Q7O0VBRUUsdUJBQXVCO0NBQ3hCO0FBQ0Q7O0VBRUUsd0JBQXdCO0NBQ3pCO0FBQ0Q7O0VBRUUsb0JBQW9CO0NBQ3JCO0FBQ0Q7O0VBRUUsc0JBQXNCO0NBQ3ZCO0FBQ0Qsb0JBQW9CO0FBQ3BCO0VBQ0UsaUJBQWlCO0NBQ2xCO0FBQ0Q7RUFDRSxtQkFBbUI7Q0FDcEI7QUFDRDtFQUNFLGtCQUFrQjtDQUNuQjtBQUNEO0VBQ0Usb0JBQW9CO0NBQ3JCO0FBQ0Q7RUFDRSxlQUFpQztFQUFqQyxpQ0FBaUM7Q0FDbEM7QUFDRDtFQUNFLHdCQUF3QjtDQUN6QjtBQUNEO0VBQ0Usa0JBQWtCO0NBQ25CO0FBQ0Q7RUFDRSxrQkFBa0I7Q0FDbkI7QUFDRDtFQUNFLG1CQUFtQjtDQUNwQjtBQUNEO0VBQ0Usb0RBQXFDO0VBQXJDLHFDQUFxQztFQUNyQywwQkFBK0M7RUFBL0MsK0NBQStDO0VBQy9DLGFBQWE7RUFDYixtQkFBbUI7Q0FDcEI7O0FBRUQsZ0JBQWdCO0FBQ2hCO0VBQ0UsbUJBQW1CO0NBQ3BCO0FBQ0Q7RUFDRSxjQUFjO0NBQ2Y7QUFDRDtFQUNFLGFBQWE7Q0FDZDs7QUFFRCxZQUFZO0FBQ1o7RUFDRSwwQkFBc0M7RUFBdEMsc0NBQXNDO0VBQ3RDLG1CQUE0QztFQUE1Qyw0Q0FBNEM7RUFDNUMsd0JBQXdCO0VBQ3hCLGVBQWU7Q0FDaEI7QUFDRDtFQUNFLGVBQWU7RUFDZixzQkFBc0I7Q0FDdkI7QUFDRDtFQUNFLGVBQWU7RUFDZixzQkFBc0I7Q0FDdkI7O0FBRUQsc0NBQXNDO0FBQ3RDO0VBQ0UsMEJBQStDO0VBQS9DLCtDQUErQztDQUNoRDtBQUNEO0VBQ0UsMEJBQTBCO0NBQzNCO0FBQ0Q7RUFDRSwwQkFBMEI7Q0FDM0I7QUFDRDtFQUNFLDBCQUEwQjtDQUMzQjs7QUFFRCxtQkFBbUI7QUFDbkI7RUFDRSxpQ0FBZ0M7RUFBaEMsZ0NBQWdDO0VBQ2hDLGtCQUFrQjtFQUNsQixtQkFBZ0M7RUFBaEMsZ0NBQWdDO0VBQ2hDLDBCQUFzQztFQUF0QyxzQ0FBc0M7RUFDdEMsbUJBQTRDO0VBQTVDLDRDQUE0QztFQUM1QyxjQUFjO0VBQ2QsYUFBYTtDQUNkO0FBQ0Q7RUFDRSxlQUFlO0NBQ2hCO0FBQ0Q7RUFDRSxhQUFhO0NBQ2Q7QUFDRDtFQUNFLGlCQUFpQjtDQUNsQjtBQUNEO0VBQ0UsaUJBQWlCLENBQUMsNkNBQTZDO0NBQ2hFO0FBQ0Q7RUFDRSx3QkFBd0I7RUFDeEIsbUJBQW1CO0VBQ25CLHFCQUFxQjtDQUN0QjtBQUNEO0VBQ0UsbUJBQW1CO0VBQ25CLDBCQUEwQjtDQUMzQjtBQUNEO0VBQ0UsZUFBZTtFQUNmLGVBQTJDO0VBQTNDLDJDQUEyQztFQUMzQyxpQkFBaUI7Q0FDbEI7QUFDRCxrQkFBa0I7QUFDbEI7RUFDRSxzQkFBc0I7RUFDdEIsY0FBYztDQUNmO0FBQ0Q7RUFDRSxvQkFBb0I7RUFDcEIsbUJBQW1CO0NBQ3BCO0FBQ0Q7RUFDRSxVQUFVO0NBQ1g7QUFDRDtFQUNFLDJCQUEyQjtDQUM1QjtBQUNEO0VBQ0UsMkJBQTJCO0NBQzVCO0FBQ0Q7RUFDRSwwQkFBc0M7RUFBdEMsc0NBQXNDO0VBQ3RDLDBCQUFzQztFQUF0QyxzQ0FBc0M7RUFDdEMsZ0JBQWdCO0NBQ2pCOztBQUVELFdBQVc7QUFDWDtFQUNFLFlBQVk7RUFDWiwwQkFBMEI7Q0FDM0I7QUFDRDtFQUNFLHNCQUFzQjtDQUN2QjtBQUNELHNDQUFzQztBQUN0Qzs7OztFQUlFLGlDQUE2QztFQUE3Qyw2Q0FBNkM7Q0FDOUM7QUFDRDs7OztFQUlFLHFDQUFxQztDQUN0QztBQUNELGtCQUFrQjtBQUNsQjtFQUNFLDBCQUE4QztFQUE5Qyw4Q0FBOEM7Q0FDL0M7O0FBRUQsa0JBQWtCO0FBQ2xCO0VBQ0UsWUFBWTtFQUNaLFlBQVk7RUFDWixpQkFBaUI7RUFDakIsd0JBQXdCO0VBQ3hCLG1CQUFtQjtFQUNuQiwwQkFBc0M7RUFBdEMsc0NBQXNDO0NBQ3ZDO0FBQ0Q7RUFDRSxVQUFVO0VBQ1YsZ0JBQWdCO0VBQ2hCLDBCQUErQztFQUEvQywrQ0FBK0M7RUFDL0MsZ0NBQXFEO0VBQXJELHFEQUFxRDtFQUNyRCx3QkFBd0I7Q0FDekI7O0FBRUQsY0FBYztBQUNkO0VBQ0UsbUJBQW1CO0VBQ25CLE9BQU87RUFDUCxpQ0FBeUI7VUFBekIseUJBQXlCO0VBQ3pCLGdDQUF3QjtVQUF4Qix3QkFBd0I7Q0FDekI7O0FBRUQsd0RBQXdEO0FBQ3hEO0VBQ0UsNEJBQStDO0VBQS9DLCtDQUErQztDQUNoRDs7QUFFRDtFQUNFLFdBQVc7RUFDWCxlQUFlO0NBQ2hCOztBQUVEO0VBQ0UsYUFBOEI7RUFBOUIsOEJBQThCO0NBQy9COztBQUVELDJCQUEyQjtBQUMzQjtFQUNFO0lBQ0UsNENBQW9DO1lBQXBDLG9DQUFvQztHQUNyQzs7RUFFRDtJQUNFLHdCQUFnQjtZQUFoQixnQkFBZ0I7R0FDakI7Q0FDRjtBQVJEO0VBQ0U7SUFDRSw0Q0FBb0M7WUFBcEMsb0NBQW9DO0dBQ3JDOztFQUVEO0lBQ0Usd0JBQWdCO1lBQWhCLGdCQUFnQjtHQUNqQjtDQUNGOztBQUVEO0VBQ0Usa0JBQWtCO0VBQ2xCLG1CQUFtQjtFQUNuQixrQkFBa0I7RUFDbEIsVUFBVTtDQUNYIiwiZmlsZSI6ImxhYi5jc3MiLCJzb3VyY2VzQ29udGVudCI6WyIvKiBCYXNpYyBjb25maWd1cmF0aW9uICovXG46cm9vdCB7XG4gIC8qIExheW91dCAqL1xuICAtLXdpZHRoLWNvbnRhaW5lcjogOTAwcHg7XG4gIC0td2lkdGgtbWluLWNvbnRhaW5lcjogMzIwcHg7XG4gIC0taGVpZ2h0LW1pbi1oZWFkZXItZm9vdGVyOiA4dmg7XG4gIC0tcGFkZGluZy1pbnRlcm5hbDogMjRweDtcbiAgLS1ib3JkZXItcmFkaXVzLWNvbnRhaW5lcjogNXB4O1xuICAtLWJvcmRlci1yYWRpdXMtY29udGVudDogNHB4O1xuICAvKiBUeXBvZ3JhcGh5ICovXG4gIC0tZm9udC1mYW1pbHk6IFwiQXJpYWxcIiwgc2Fucy1zZXJpZjtcbiAgLS1mb250LWZhbWlseS1tb25vOiBEcm9pZCBNb25vLCBNZW5sbywgQ29uc29sYXMsIG1vbm9zcGFjZTtcbiAgLS1mb250LXNpemU6IDE4cHg7XG4gIC0tbGluZS1oZWlnaHQ6IDEuNGVtO1xuICAvKiAobGluZSBoZWlnaHQgaXMgc3BlY2lmaWVkIGluIGVtIHNvIHRoYXQgaXQgYWRhcHRzIHRvIHZhcnlpbmcgZm9udCBzaXplcykgKi9cbiAgLS1wYXJhZ3JhcGgtbWFyZ2luLXZlcnRpY2FsOiB2YXIoLS1mb250LXNpemUpO1xuICAvKiBDb2xvcnMgKi9cbiAgLS1jb2xvci1ib3JkZXI6ICNlNWU1ZTU7XG4gIC0tY29sb3ItYm9yZGVyLWludGVybmFsOiAjZWZlZmVmO1xuICAtLWNvbG9yLWdyYXktYmFja2dyb3VuZDogI2Y4ZjhmODtcbiAgLS1jb2xvci1ncmF5LWNvbnRlbnQ6ICM4ZDhkOGQ7XG59XG5cbi8qIFNldCBib3ggbW9kZWwgdG8gYm9yZGVyLWJveCBnbG9iYWxseSAqL1xuOnJvb3Qge1xuICBib3gtc2l6aW5nOiBib3JkZXItYm94O1xufVxuKiwgKjo6YmVmb3JlLCAqOjphZnRlciB7XG4gIGJveC1zaXppbmc6IGluaGVyaXQ7XG59XG5cbi8qIENvbnRlbnQgbGF5b3V0ICovXG5ib2R5IHtcbiAgbWFyZ2luOiAwO1xufVxuXG4uY29udGFpbmVyIHtcbiAgbWluLXdpZHRoOiB2YXIoLS13aWR0aC1taW4tY29udGFpbmVyKTtcbiAgbWluLWhlaWdodDogdmFyKC0taGVpZ2h0LW1pbi1oZWFkZXItZm9vdGVyKTtcbiAgLyogVXNlIHBhZ2Utc3R5bGUgbGF5b3V0IGJ5IGRlZmF1bHQgKi9cbiAgbWFyZ2luOiB2YXIoLS1wYWRkaW5nLWludGVybmFsKSBhdXRvO1xuICB3aWR0aDogdmFyKC0td2lkdGgtY29udGFpbmVyKTtcbn1cbmhlYWRlciwgZm9vdGVyLCBtYWluIHtcbiAgcGFkZGluZzogdmFyKC0tcGFkZGluZy1pbnRlcm5hbCk7XG59XG4vKiBJbmRpdmlkdWFsIHBhcnRzOiBIZWlnaHQsIGJvcmRlcnMgYW5kIGJhY2tncm91bmQgKi9cbmhlYWRlciwgZm9vdGVyIHtcbiAgbWluLWhlaWdodDogdmFyKC0taGVpZ2h0LW1pbi1oZWFkZXItZm9vdGVyKTtcbn1cbm1haW4ge1xuICBtaW4taGVpZ2h0OiB2YXIoLS1oZWlnaHQtbWluLWhlYWRlci1mb290ZXIpO1xufVxuXG4vKiBGdWxsc2NyZWVuIGxheW91dCAqL1xuLmNvbnRhaW5lci5mdWxsc2NyZWVuIHtcbiAgLyogRnVsbCBzY3JlZW4gbWludXMgbWFyZ2lucyAqL1xuICBtYXJnaW46IHZhcigtLXBhZGRpbmctaW50ZXJuYWwpO1xuICBtaW4taGVpZ2h0OiBjYWxjKDEwMHZoIC0gMip2YXIoLS1wYWRkaW5nLWludGVybmFsKSk7XG4gIHdpZHRoOiBjYWxjKDEwMHZ3IC0gMip2YXIoLS1wYWRkaW5nLWludGVybmFsKSk7XG4gIC8qIERpc3BsYXkgY29udGVudCB1c2luZyBmbGV4Ym94ZXMgKi9cbiAgZGlzcGxheTogZmxleDtcbiAgZmxleC1kaXJlY3Rpb246IGNvbHVtbjtcbn1cbi5jb250YWluZXIuZnVsbHNjcmVlbiBtYWluIHtcbiAgLyogRmxleCBwb3NpdGlvbmluZyAqL1xuICBmbGV4OiAxO1xufVxuXG4vKiBGcmFtZWxlc3MgbGF5b3V0ICovXG4uY29udGFpbmVyLmZyYW1lbGVzcyB7XG4gIG1hcmdpbjogMDtcbiAgYm9yZGVyOiBub25lO1xuICBib3JkZXItcmFkaXVzOiAwO1xufVxuLmNvbnRhaW5lci5mdWxsc2NyZWVuLmZyYW1lbGVzcyB7XG4gIHdpZHRoOiAxMDB2dztcbiAgbWluLWhlaWdodDogMTAwdmg7XG59XG5cbi8qIFJlbW92ZSBmcmFtZSBvbiBzbWFsbCBzY3JlZW5zICovXG5AbWVkaWEgKG1heC13aWR0aDogNjAwcHgpLCAobWF4LWhlaWdodDogNjAwcHgpIHtcbiAgLmNvbnRhaW5lci5mdWxsc2NyZWVuIHtcbiAgICBtYXJnaW46IDA7XG4gICAgYm9yZGVyOiBub25lO1xuICAgIGJvcmRlci1yYWRpdXM6IDA7XG4gICAgd2lkdGg6IDEwMHZ3O1xuICAgIG1pbi1oZWlnaHQ6IDEwMHZoO1xuICB9XG59XG5cbi8qIEZsZXhib3ggZml4IGZvciBJRTExLCBwZXIgaHR0cHM6Ly9naXRodWIuY29tL3BoaWxpcHdhbHRvbi9mbGV4YnVncyAqL1xuQG1lZGlhIGFsbCBhbmQgKC1tcy1oaWdoLWNvbnRyYXN0OiBub25lKSwgKC1tcy1oaWdoLWNvbnRyYXN0OiBhY3RpdmUpIHtcbiAgYm9keSB7XG4gICAgZGlzcGxheTogZmxleDtcbiAgfVxuICAuY29udGFpbmVyLmZ1bGxzY3JlZW4ge1xuICAgIC8qIElFMTEgbWlzY2FsY3VsYXRlcyB0aGUgaGVpZ2h0LCBzbyBhZGQgc29tZSBzbGFjayAqL1xuICAgIG1pbi1oZWlnaHQ6IGNhbGMoMTAwdmggLSAzKnZhcigtLXBhZGRpbmctaW50ZXJuYWwpKTtcbiAgfVxufVxuXG4vKiBCb3JkZXJzIGFuZCBiYWNrZ3JvdW5kcyAqL1xuLmNvbnRhaW5lciB7XG4gIGJvcmRlcjogMXB4IHNvbGlkIHZhcigtLWNvbG9yLWJvcmRlcik7XG4gIGJvcmRlci1yYWRpdXM6IHZhcigtLWJvcmRlci1yYWRpdXMtY29udGFpbmVyKTtcbn1cbmhlYWRlciB7XG4gIGJvcmRlci1ib3R0b206IDFweCBzb2xpZCB2YXIoLS1jb2xvci1ib3JkZXItaW50ZXJuYWwpO1xufVxuZm9vdGVyIHtcbiAgYm9yZGVyLXRvcDogMXB4IHNvbGlkIHZhcigtLWNvbG9yLWJvcmRlci1pbnRlcm5hbCk7XG4gIGJhY2tncm91bmQtY29sb3I6IHZhcigtLWNvbG9yLWdyYXktYmFja2dyb3VuZCk7XG59XG5cbi8qIFR5cG9ncmFwaHkgKi9cbjpyb290IHtcbiAgZm9udC1mYW1pbHk6IHZhcigtLWZvbnQtZmFtaWx5KTtcbiAgZm9udC1zaXplOiB2YXIoLS1mb250LXNpemUpO1xuICBsaW5lLWhlaWdodDogdmFyKC0tbGluZS1oZWlnaHQpO1xufVxuaGVhZGVyLCBmb290ZXIsIG1haW4ge1xuICAvKiBTZXQgZGlzcGxheSBzdHlsZSBleHBsaWNpdGx5IGZvciBsZWdhY3kgYnJvd3NlcnNcbiAgICAgdGhhdCBhcmUgdW5mYW1pbGlhciB3aXRoIHRoZXNlIGVsZW1lbnRzICovXG4gIGRpc3BsYXk6IGJsb2NrO1xuICB0ZXh0LWFsaWduOiBjZW50ZXI7XG59XG5oMSwgaDIsIGgzIHtcbiAgbGluZS1oZWlnaHQ6IHZhcigtLWxpbmUtaGVpZ2h0KTtcbn1cbmhyIHtcbiAgYm9yZGVyOiBub25lO1xuICBib3JkZXItdG9wOiAycHggc29saWQgdmFyKC0tY29sb3ItYm9yZGVyKVxufVxuXG4vKiBTcGVjaWFsIGVsZW1lbnRzOiBLZXlib2FyZCBidXR0b25zICovXG5rYmQge1xuICAvKiBQb3NpdGlvbmluZyAqL1xuICBkaXNwbGF5OiBpbmxpbmUtYmxvY2s7XG4gIG1pbi13aWR0aDogMS41cmVtO1xuICBtaW4taGVpZ2h0OiAxLjVyZW07XG4gIHBhZGRpbmc6IDAgMC4zcmVtO1xuICBwYWRkaW5nLXRvcDogMC4xNXJlbTtcbiAgLyogRm9udHMgKi9cbiAgZm9udC1mYW1pbHk6IHZhcigtLWZvbnQtZmFtaWx5LW1vbm8pO1xuICBmb250LXNpemU6IDAuOXJlbTtcbiAgdGV4dC1hbGlnbjogY2VudGVyO1xuICAvKiBCYWNrZ3JvdW5kIGFuZCBib3JkZXIgKi9cbiAgYmFja2dyb3VuZC1jb2xvcjogd2hpdGU7XG4gIGJvcmRlci1yYWRpdXM6IHZhcigtLWJvcmRlci1yYWRpdXMtY29udGVudCk7XG4gIGJvcmRlcjogMXB4IHNvbGlkIHJnYigxODAsIDE4MCwgMTgwKTtcbn1cbmtiZC5iaWcge1xuICBmb250LXNpemU6IDEuNHJlbTtcbiAgcGFkZGluZy10b3A6IDAuMzc1cmVtO1xuICBib3JkZXItcmFkaXVzOiAwLjEyNXJlbTtcbn1cblxuLyogQWxpZ25tZW50IGhlbHBlcnMgKi9cbi53LTEwMCB7XG4gIHdpZHRoOiAxMDAlO1xufVxuLnctcyB7XG4gIG1heC13aWR0aDogdmFyKC0td2lkdGgtbWluLWNvbnRhaW5lcik7XG59XG4udy1tIHtcbiAgbWF4LXdpZHRoOiBjYWxjKDEuNSAqIHZhcigtLXdpZHRoLW1pbi1jb250YWluZXIpKTtcbn1cbi53LWwge1xuICBtYXgtd2lkdGg6IGNhbGMoMiAqIHZhcigtLXdpZHRoLW1pbi1jb250YWluZXIpKTtcbn1cbi8qIEJsb2NrIGFsaWdubWVudCBiYXNlZCBvbiBmbGV4Ym94ICovXG4uY29udGVudC12ZXJ0aWNhbC10b3AsXG4uY29udGVudC12ZXJ0aWNhbC1jZW50ZXIsXG4uY29udGVudC12ZXJ0aWNhbC1ib3R0b20sXG4uY29udGVudC1ob3Jpem9udGFsLWxlZnQsXG4uY29udGVudC1ob3Jpem9udGFsLWNlbnRlcixcbi5jb250ZW50LWhvcml6b250YWwtcmlnaHQsXG4uY29udGVudC1ob3Jpem9udGFsLXNwYWNlLWJldHdlZW4sXG4uY29udGVudC12ZXJ0aWNhbC1zcGFjZS1iZXR3ZWVuLFxuLmNvbnRlbnQtaG9yaXpvbnRhbC1zcGFjZS1hcm91bmQsXG4uY29udGVudC12ZXJ0aWNhbC1zcGFjZS1hcm91bmQge1xuICBkaXNwbGF5OiBmbGV4O1xufVxuLmNvbnRlbnQtdmVydGljYWwtdG9wIHtcbiAgYWxpZ24taXRlbXM6IGZsZXgtc3RhcnQ7XG59XG4uY29udGVudC12ZXJ0aWNhbC1jZW50ZXIge1xuICBhbGlnbi1pdGVtczogY2VudGVyO1xufVxuLmNvbnRlbnQtdmVydGljYWwtYm90dG9tIHtcbiAgYWxpZ24taXRlbXM6IGZsZXgtZW5kO1xufVxuLmNvbnRlbnQtaG9yaXpvbnRhbC1sZWZ0IHtcbiAganVzdGlmeS1jb250ZW50OiBmbGV4LXN0YXJ0O1xufVxuLmNvbnRlbnQtaG9yaXpvbnRhbC1jZW50ZXIge1xuICBqdXN0aWZ5LWNvbnRlbnQ6IGNlbnRlcjtcbn1cbi5jb250ZW50LWhvcml6b250YWwtcmlnaHQge1xuICBqdXN0aWZ5LWNvbnRlbnQ6IGZsZXgtZW5kO1xufVxuLmNvbnRlbnQtaG9yaXpvbnRhbC1zcGFjZS1iZXR3ZWVuLFxuLmNvbnRlbnQtdmVydGljYWwtc3BhY2UtYmV0d2VlbiB7XG4gIGp1c3RpZnktY29udGVudDogc3BhY2UtYmV0d2Vlbjtcbn1cbi5jb250ZW50LWhvcml6b250YWwtc3BhY2UtYXJvdW5kLFxuLmNvbnRlbnQtdmVydGljYWwtc3BhY2UtYXJvdW5kIHtcbiAganVzdGlmeS1jb250ZW50OiBzcGFjZS1hcm91bmQ7XG59XG4uY29udGVudC12ZXJ0aWNhbC1zcGFjZS1hcm91bmQsXG4uY29udGVudC12ZXJ0aWNhbC1zcGFjZS1iZXR3ZWVuIHtcbiAgZmxleC1kaXJlY3Rpb246IGNvbHVtbjtcbn1cbi5jb250ZW50LXZlcnRpY2FsLXNwYWNlLWJldHdlZW4uY29udGVudC1ob3Jpem9udGFsLXJpZ2h0LFxuLmNvbnRlbnQtdmVydGljYWwtc3BhY2UtYXJvdW5kLmNvbnRlbnQtaG9yaXpvbnRhbC1yaWdodCB7XG4gIGFsaWduLWl0ZW1zOiBmbGV4LXN0YXJ0O1xufVxuLmNvbnRlbnQtdmVydGljYWwtc3BhY2UtYmV0d2Vlbi5jb250ZW50LWhvcml6b250YWwtY2VudGVyLFxuLmNvbnRlbnQtdmVydGljYWwtc3BhY2UtYXJvdW5kLmNvbnRlbnQtaG9yaXpvbnRhbC1jZW50ZXIge1xuICBhbGlnbi1pdGVtczogY2VudGVyO1xufVxuLmNvbnRlbnQtdmVydGljYWwtc3BhY2UtYmV0d2Vlbi5jb250ZW50LWhvcml6b250YWwtcmlnaHQsXG4uY29udGVudC12ZXJ0aWNhbC1zcGFjZS1hcm91bmQuY29udGVudC1ob3Jpem9udGFsLXJpZ2h0IHtcbiAgYWxpZ24taXRlbXM6IGZsZXgtZW5kO1xufVxuLyogVGV4dCBhbGlnbm1lbnQgKi9cbi50ZXh0LWxlZnQge1xuICB0ZXh0LWFsaWduOiBsZWZ0O1xufVxuLnRleHQtY2VudGVyIHtcbiAgdGV4dC1hbGlnbjogY2VudGVyO1xufVxuLnRleHQtcmlnaHQge1xuICB0ZXh0LWFsaWduOiByaWdodDtcbn1cbi50ZXh0LWp1c3RpZnkge1xuICB0ZXh0LWFsaWduOiBqdXN0aWZ5O1xufVxuLnRleHQtbXV0ZWQge1xuICBjb2xvcjogdmFyKC0tY29sb3ItZ3JheS1jb250ZW50KTtcbn1cbi50ZXh0LW11dGVkIGEge1xuICBjb2xvcjogcmdiKDYwLCA4OSwgMTU2KTtcbn1cbnNtYWxsLCAuc21hbGwge1xuICBmb250LXNpemU6IDAuOXJlbTtcbn1cbi5mb250LXdlaWdodC1ib2xkIHtcbiAgZm9udC13ZWlnaHQ6IGJvbGQ7XG59XG4uZm9udC1pdGFsaWMge1xuICBmb250LXN0eWxlOiBpdGFsaWM7XG59XG5jb2RlIHtcbiAgZm9udC1mYW1pbHk6IHZhcigtLWZvbnQtZmFtaWx5LW1vbm8pO1xuICBiYWNrZ3JvdW5kLWNvbG9yOiB2YXIoLS1jb2xvci1ncmF5LWJhY2tncm91bmQpO1xuICBwYWRkaW5nOiAycHg7XG4gIGJvcmRlci1yYWRpdXM6IDJweDtcbn1cblxuLyogVmlzaWJpbGl0eSAqL1xuLmludmlzaWJsZSB7XG4gIHZpc2liaWxpdHk6IGhpZGRlbjtcbn1cbi5oaWRkZW4ge1xuICBkaXNwbGF5OiBub25lO1xufVxuLmhpZGUtaWYtZW1wdHk6ZW1wdHkge1xuICBkaXNwbGF5OiBub25lXG59XG5cbi8qIEFsZXJ0cyAqL1xuLmFsZXJ0IHtcbiAgYm9yZGVyOiAycHggc29saWQgdmFyKC0tY29sb3ItYm9yZGVyKTtcbiAgYm9yZGVyLXJhZGl1czogdmFyKC0tYm9yZGVyLXJhZGl1cy1jb250ZW50KTtcbiAgcGFkZGluZzogMTZweCAxNnB4IDE0cHg7XG4gIG1hcmdpbjogMTZweCAwO1xufVxuLmFsZXJ0LmFsZXJ0LWRhbmdlciB7XG4gIGNvbG9yOiAjYTAyYzJjO1xuICBib3JkZXItY29sb3I6ICNhMDJjMmM7XG59XG4uYWxlcnQuYWxlcnQtd2FybmluZyB7XG4gIGNvbG9yOiAjZjZhOTAyO1xuICBib3JkZXItY29sb3I6ICNmZmI0MDA7XG59XG5cbi8qIEJhY2tncm91bmQgc3R5bGVzIChleHBlcmltZW50YWwpICovXG4uYWxlcnQsIC5iYWNrZ3JvdW5kLWRhcmsge1xuICBiYWNrZ3JvdW5kLWNvbG9yOiB2YXIoLS1jb2xvci1ncmF5LWJhY2tncm91bmQpO1xufVxuLmFsZXJ0LmFsZXJ0LWRhbmdlciwgLmJhY2tncm91bmQtZGFuZ2VyIHtcbiAgYmFja2dyb3VuZC1jb2xvcjogI2U5YWZhZjtcbn1cbi5hbGVydC5hbGVydC13YXJuaW5nLCAuYmFja2dyb3VuZC13YXJuaW5nIHtcbiAgYmFja2dyb3VuZC1jb2xvcjogI2ZmZTZhNTtcbn1cbi5iYWNrZ3JvdW5kLW9rIHtcbiAgYmFja2dyb3VuZC1jb2xvcjogI2MzZTZjYjtcbn1cblxuLyogRm9ybSBlbGVtZW50cyAqL1xuaW5wdXQsIHNlbGVjdCwgYnV0dG9uLCB0ZXh0YXJlYSB7XG4gIGZvbnQtZmFtaWx5OiB2YXIoLS1mb250LWZhbWlseSk7XG4gIGZvbnQtc2l6ZTogMC45cmVtO1xuICBsaW5lLWhlaWdodDogdmFyKC0tbGluZS1oZWlnaHQpO1xuICBib3JkZXI6IDJweCBzb2xpZCB2YXIoLS1jb2xvci1ib3JkZXIpO1xuICBib3JkZXItcmFkaXVzOiB2YXIoLS1ib3JkZXItcmFkaXVzLWNvbnRlbnQpO1xuICBtYXJnaW46IDhweCAwO1xuICBwYWRkaW5nOiA4cHg7XG59XG5pbnB1dFt0eXBlPVwiY2hlY2tib3hcIl0ge1xuICBtYXJnaW46IDAgMTBweDtcbn1cbmlucHV0W3R5cGU9XCJyYW5nZVwiXSB7XG4gIGJvcmRlcjogbm9uZTtcbn1cbmlucHV0ICsgbGFiZWwge1xuICBtYXJnaW4tbGVmdDogMnB4O1xufVxuc2VsZWN0IHtcbiAgcGFkZGluZzogOHB4IDVweDsgLyogc2VsZWN0cyBoYXZlIGEgd2VpcmQgdmVydGljYWwgYWxpZ25tZW50ICovXG59XG5idXR0b24ge1xuICBiYWNrZ3JvdW5kLWNvbG9yOiB3aGl0ZTtcbiAgYm9yZGVyLXJhZGl1czogM3B4O1xuICBwYWRkaW5nOiA4cHggOHB4IDZweDtcbn1cbmJ1dHRvbjpob3ZlciB7XG4gIGJvcmRlci1jb2xvcjogI2RkZDtcbiAgYmFja2dyb3VuZC1jb2xvcjogI2ZjZmNmYztcbn1cbnRleHRhcmVhIHtcbiAgZGlzcGxheTogYmxvY2s7XG4gIG1hcmdpbjogdmFyKC0tcGFyYWdyYXBoLW1hcmdpbi12ZXJ0aWNhbCkgMDtcbiAgcmVzaXplOiB2ZXJ0aWNhbDtcbn1cbi8qIElucHV0IGdyb3VwcyAqL1xuLmlucHV0LWdyb3VwIHtcbiAgZGlzcGxheTogaW5saW5lLXRhYmxlO1xuICBtYXJnaW46IDhweCAwO1xufVxuLmlucHV0LWdyb3VwICoge1xuICBkaXNwbGF5OiB0YWJsZS1jZWxsO1xuICBib3JkZXItcmFkaXVzOiAwcHg7XG59XG4uaW5wdXQtZ3JvdXAgaW5wdXQge1xuICBtYXJnaW46IDA7XG59XG4uaW5wdXQtZ3JvdXAgKjpmaXJzdC1jaGlsZCB7XG4gIGJvcmRlci1yYWRpdXM6IDRweCAwIDAgNHB4O1xufVxuLmlucHV0LWdyb3VwICo6bGFzdC1jaGlsZCB7XG4gIGJvcmRlci1yYWRpdXM6IDAgNHB4IDRweCAwO1xufVxuLmlucHV0LWdyb3VwIC5pbnB1dC1ncm91cC1hZGRvbiB7XG4gIGJhY2tncm91bmQtY29sb3I6IHZhcigtLWNvbG9yLWJvcmRlcik7XG4gIGJvcmRlcjogMnB4IHNvbGlkIHZhcigtLWNvbG9yLWJvcmRlcik7XG4gIHBhZGRpbmc6IDAgMTBweDtcbn1cblxuLyogVGFibGUgKi9cbnRhYmxlIHtcbiAgd2lkdGg6IDEwMCU7XG4gIGJvcmRlci1jb2xsYXBzZTogY29sbGFwc2U7XG59XG50YWJsZSB0ZCwgdGFibGUgdGgge1xuICBwYWRkaW5nOiAxMHB4IDhweCA4cHg7XG59XG4vKiBUYWJsZSBib3JkZXJzIChleGNlcHQgZm9yIHBsYWluKSAqL1xudGFibGU6bm90KC50YWJsZS1wbGFpbikgPiB0ciA+IHRkLFxudGFibGU6bm90KC50YWJsZS1wbGFpbikgPiB0ciA+IHRoLFxudGFibGU6bm90KC50YWJsZS1wbGFpbikgPiB0Ym9keSA+IHRyID4gdGQsXG50YWJsZTpub3QoLnRhYmxlLXBsYWluKSA+IHRib2R5ID4gdHIgPiB0aCB7XG4gIGJvcmRlci1ib3R0b206IDJweCBzb2xpZCB2YXIoLS1jb2xvci1ib3JkZXIpO1xufVxudGFibGU6bm90KC50YWJsZS1wbGFpbikgPiB0cjpsYXN0LWNoaWxkID4gdGQsXG50YWJsZTpub3QoLnRhYmxlLXBsYWluKSA+IHRyOmxhc3QtY2hpbGQgPiB0aCxcbnRhYmxlOm5vdCgudGFibGUtcGxhaW4pID4gdGJvZHkgPiB0cjpsYXN0LWNoaWxkID4gdGQsXG50YWJsZTpub3QoLnRhYmxlLXBsYWluKSA+IHRib2R5ID4gdHI6bGFzdC1jaGlsZCA+IHRoIHtcbiAgYm9yZGVyLWJvdHRvbTogMnB4IHNvbGlkIHRyYW5zcGFyZW50O1xufVxuLyogU3RyaXBlZCByb3dzICovXG50YWJsZS50YWJsZS1zdHJpcGVkIHRyOm50aC1jaGlsZChvZGQpIHRkIHtcbiAgYmFja2dyb3VuZC1jb2xvcjogdmFyKC0tY29sb3ItYm9yZGVyLWludGVybmFsKVxufVxuXG4vKiBQcm9ncmVzcyBiYXIgKi9cbi5wcm9ncmVzcyB7XG4gIHdpZHRoOiAxMDAlO1xuICBoZWlnaHQ6IDhweDtcbiAgb3ZlcmZsb3c6IGhpZGRlbjtcbiAgbWFyZ2luOiAwLjJyZW0gMCAwLjRyZW07XG4gIGJvcmRlci1yYWRpdXM6IDJweDtcbiAgYm9yZGVyOiAxcHggc29saWQgdmFyKC0tY29sb3ItYm9yZGVyKTtcbn1cbi5wcm9ncmVzcyAucHJvZ3Jlc3MtYmFyIHtcbiAgd2lkdGg6IDAlO1xuICBtaW4taGVpZ2h0OiA4cHg7XG4gIGJhY2tncm91bmQtY29sb3I6IHZhcigtLWNvbG9yLWdyYXktYmFja2dyb3VuZCk7XG4gIGJvcmRlci1yaWdodDogMXB4IHNvbGlkIHZhcigtLWNvbG9yLWJvcmRlci1pbnRlcm5hbCk7XG4gIGJveC1zaXppbmc6IGNvbnRlbnQtYm94O1xufVxuXG4vKiBQb3BvdmVycyAqL1xuLnBvcG92ZXIge1xuICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gIHRvcDogMDtcbiAgYW5pbWF0aW9uLWR1cmF0aW9uOiAwLjVzO1xuICBhbmltYXRpb24tbmFtZTogcG9wb3Zlcjtcbn1cblxuLyogV2lkdGgsIGZvciBzb21lIHJlYXNvbiwgbmVlZHMgdG8gYmUgc2V0IGV4cGxpY2l0bHkgKi9cbi5jb250YWluZXIuZnVsbHNjcmVlbiAucG9wb3ZlciB7XG4gIHdpZHRoOiBjYWxjKDEwMHZ3IC0gMip2YXIoLS1wYWRkaW5nLWludGVybmFsKSk7XG59XG5cbi5wb3BvdmVyID4gKiB7XG4gIHdpZHRoOiA4MCU7XG4gIG1hcmdpbjogMCBhdXRvO1xufVxuXG4uY29udGFpbmVyOm5vdCguZnVsbHNjcmVlbikgLnBvcG92ZXIge1xuICB3aWR0aDogdmFyKC0td2lkdGgtY29udGFpbmVyKTtcbn1cblxuLyogU2xpZGUgaW4gZnJvbSB0aGUgdG9wICovXG5Aa2V5ZnJhbWVzIHBvcG92ZXIge1xuICBmcm9tIHtcbiAgICB0cmFuc2Zvcm06IHRyYW5zbGF0ZTNkKDAsIC0xMDAlLCAwKTtcbiAgfVxuXG4gIHRvIHtcbiAgICB0cmFuc2Zvcm06IG5vbmU7XG4gIH1cbn1cblxuLnBvcG92ZXIgPiAuYWxlcnQ6Zmlyc3QtY2hpbGQge1xuICBib3JkZXItd2lkdGg6IDFweDtcbiAgcG9zaXRpb246IHJlbGF0aXZlO1xuICBwYWRkaW5nLXRvcDogMjRweDtcbiAgdG9wOiAtOHB4O1xufVxuXG4iXX0= */
+8 -2
app/utils/labjs/protocols/custom.js
··· 32 32 protocol_condition_second_title: `Houses`, 33 33 protocol_condition_second: `If you see a house, press “9”.`, 34 34 overview_links: [], 35 - background_links: [{name: 'Link 1', address: 'https://www.cnn.com/videos/health/2011/01/04/sacks.face.blindness.cnn'}], 35 + background_links: [ 36 + { 37 + name: 'Link 1', 38 + address: 39 + 'https://www.cnn.com/videos/health/2011/01/04/sacks.face.blindness.cnn' 40 + } 41 + ], 36 42 protocal_links: [], 37 43 params: { 38 44 imageHeight: '500px', ··· 73 79 type: 4, 74 80 response: '' 75 81 }, 76 - stimuli: [], 82 + stimuli: [] 77 83 }, 78 84 mainTimeline: ['intro', 'faceHouseTimeline', 'end'], // array of trial and timeline ids 79 85 trials: {
+81 -24
app/utils/labjs/protocols/faceshouses.js
··· 8 8 const housesDir = path.join(rootFolder, 'assets', 'face_house', 'houses'); 9 9 const fixation = path.join(rootFolder, 'assets', 'common', 'fixationcross.png'); 10 10 11 - const stimuli = ['Face1', 'Face2', 'Face3', 'Face4', 'Face5', 'Face6', 'Face7', 'Face8', 'Face9', 'Face10', 12 - 'Face11', 'Face12', 'Face13', 'Face14', 'Face15', 'Face16', 'Face17', 'Face18', 'Face19', 'Face20', 13 - 'Face21', 'Face22', 'Face23', 'Face24', 'Face25', 'Face26', 'Face27', 'Face28', 'Face29', 'Face30', 14 - 'House1', 'House2', 'House3', 'House4', 'House5', 'House6', 'House7', 'House8', 'House9', 'House10', 15 - 'House11', 'House12', 'House13', 'House14', 'House15', 'House16', 'House17', 'House18', 'House19', 'House20', 16 - 'House21', 'House22', 'House23', 'House24', 'House25', 'House26', 'House27', 'House28', 'House29', 'House30'].map(s => ({ 11 + const stimuli = [ 12 + 'Face1', 13 + 'Face2', 14 + 'Face3', 15 + 'Face4', 16 + 'Face5', 17 + 'Face6', 18 + 'Face7', 19 + 'Face8', 20 + 'Face9', 21 + 'Face10', 22 + 'Face11', 23 + 'Face12', 24 + 'Face13', 25 + 'Face14', 26 + 'Face15', 27 + 'Face16', 28 + 'Face17', 29 + 'Face18', 30 + 'Face19', 31 + 'Face20', 32 + 'Face21', 33 + 'Face22', 34 + 'Face23', 35 + 'Face24', 36 + 'Face25', 37 + 'Face26', 38 + 'Face27', 39 + 'Face28', 40 + 'Face29', 41 + 'Face30', 42 + 'House1', 43 + 'House2', 44 + 'House3', 45 + 'House4', 46 + 'House5', 47 + 'House6', 48 + 'House7', 49 + 'House8', 50 + 'House9', 51 + 'House10', 52 + 'House11', 53 + 'House12', 54 + 'House13', 55 + 'House14', 56 + 'House15', 57 + 'House16', 58 + 'House17', 59 + 'House18', 60 + 'House19', 61 + 'House20', 62 + 'House21', 63 + 'House22', 64 + 'House23', 65 + 'House24', 66 + 'House25', 67 + 'House26', 68 + 'House27', 69 + 'House28', 70 + 'House29', 71 + 'House30' 72 + ].map(s => ({ 17 73 condition: s.startsWith('Face') ? 'Face' : 'House', 18 74 dir: s.startsWith('Face') ? facesDir : housesDir, 19 75 filename: `${s}.jpg`, 20 76 name: s, 21 77 response: s.startsWith('Face') ? '1' : '9', 22 78 phase: 'main', 23 - type: s.startsWith('Face') ? EVENTS.STIMULUS_1 : EVENTS.STIMULUS_2, 24 - })) 79 + type: s.startsWith('Face') ? EVENTS.STIMULUS_1 : EVENTS.STIMULUS_2 80 + })); 25 81 26 82 // phase: ['Face1', 'House1'].includes(s) ? 'practice' : 'main', 27 83 ··· 52 108 background_links: [ 53 109 { 54 110 name: 'Link 1', 55 - address: 'https://www.cnn.com/videos/health/2011/01/04/sacks.face.blindness.cnn', 56 - }, 111 + address: 112 + 'https://www.cnn.com/videos/health/2011/01/04/sacks.face.blindness.cnn' 113 + } 57 114 ], 58 115 protocal_links: [], 59 116 params: { ··· 75 132 dir: facesDir, 76 133 title: 'Face', 77 134 type: EVENTS.STIMULUS_1, 78 - response: '1', 135 + response: '1' 79 136 }, 80 137 stimulus2: { 81 138 dir: housesDir, 82 139 title: 'House', 83 140 type: EVENTS.STIMULUS_2, 84 - response: '9', 141 + response: '9' 85 142 }, 86 143 stimulus3: { 87 144 dir: '', 88 145 title: '', 89 146 type: 3, 90 - response: '', 147 + response: '' 91 148 }, 92 149 stimulus4: { 93 150 dir: '', 94 151 title: '', 95 152 type: 4, 96 - response: '', 153 + response: '' 97 154 }, 98 - stimuli, 155 + stimuli 99 156 }, 100 157 mainTimeline: ['intro', 'faceHouseTimeline', 'end'], // array of trial and timeline ids 101 158 trials: { 102 159 intro: { 103 160 type: 'callback-html-display', 104 161 id: 'intro', 105 - post_trial_gap: 1000, 162 + post_trial_gap: 1000 106 163 }, 107 164 end: { 108 165 id: 'end', 109 166 type: 'callback-html-display', 110 167 stimulus: 'Thanks for participating. Press any key to continue', 111 168 response_ends_trial: true, 112 - post_trial_gap: 500, 113 - }, 169 + post_trial_gap: 500 170 + } 114 171 }, 115 172 timelines: { 116 173 faceHouseTimeline: { ··· 120 177 id: 'interTrial', 121 178 type: 'callback-image-display', 122 179 stimulus: fixation, 123 - response_ends_trial: false, 180 + response_ends_trial: false 124 181 }, 125 182 { 126 183 id: 'trial', 127 - response_ends_trial: false, 128 - }, 129 - ], 130 - }, 131 - }, 184 + response_ends_trial: false 185 + } 186 + ] 187 + } 188 + } 132 189 });
+16 -15
app/utils/labjs/protocols/multi.js
··· 35 35 { 36 36 name: 'Link 1', 37 37 address: 38 - 'https://www.scientificamerican.com/podcast/episode/the-myth-of-multitasking-09-07-15/', 38 + 'https://www.scientificamerican.com/podcast/episode/the-myth-of-multitasking-09-07-15/' 39 39 }, 40 40 { 41 41 name: 'Link 2', 42 - address: 'https://www.scientificamerican.com/article/multitasking-two-tasks/', 43 - }, 42 + address: 43 + 'https://www.scientificamerican.com/article/multitasking-two-tasks/' 44 + } 44 45 ], 45 46 protocal_links: [], 46 47 params: { ··· 55 56 dir: facesDir, 56 57 title: 'No switching', 57 58 type: EVENTS.STIMULUS_1, 58 - response: '1', 59 + response: '1' 59 60 }, 60 61 stimulus2: { 61 62 dir: housesDir, 62 63 title: 'Switching', 63 64 type: EVENTS.STIMULUS_2, 64 - response: '9', 65 - }, 65 + response: '9' 66 + } 66 67 }, 67 68 mainTimeline: ['intro', 'multiTaskingTimeline', 'end'], // array of trial and timeline ids 68 69 trials: { 69 70 intro: { 70 71 type: 'callback-html-display', 71 72 id: 'intro', 72 - post_trial_gap: 1000, 73 + post_trial_gap: 1000 73 74 }, 74 75 end: { 75 76 id: 'end', 76 77 type: 'callback-html-display', 77 78 stimulus: 'Thanks for participating. Press any key to continue', 78 79 response_ends_trial: true, 79 - post_trial_gap: 500, 80 - }, 80 + post_trial_gap: 500 81 + } 81 82 }, 82 83 timelines: { 83 84 faceHouseTimeline: { ··· 87 88 id: 'interTrial', 88 89 type: 'callback-image-display', 89 90 stimulus: fixation, 90 - response_ends_trial: false, 91 + response_ends_trial: false 91 92 }, 92 93 { 93 94 id: 'trial', 94 - response_ends_trial: false, 95 - }, 96 - ], 97 - }, 98 - }, 95 + response_ends_trial: false 96 + } 97 + ] 98 + } 99 + } 99 100 });
+12 -12
app/utils/labjs/protocols/search.js
··· 48 48 dir: facesDir, 49 49 title: '5 and 10 letters', 50 50 type: EVENTS.STIMULUS_1, 51 - response: '1', 51 + response: '1' 52 52 }, 53 53 stimulus2: { 54 54 dir: housesDir, 55 55 title: '15 and 20 letters', 56 56 type: EVENTS.STIMULUS_2, 57 - response: '9', 58 - }, 57 + response: '9' 58 + } 59 59 }, 60 60 mainTimeline: ['intro', 'visualSearchTimeline', 'end'], // array of trial and timeline ids 61 61 trials: { 62 62 intro: { 63 63 type: 'callback-html-display', 64 64 id: 'intro', 65 - post_trial_gap: 1000, 65 + post_trial_gap: 1000 66 66 }, 67 67 end: { 68 68 id: 'end', 69 69 type: 'callback-html-display', 70 70 stimulus: 'Thanks for participating. Press any key to continue', 71 71 response_ends_trial: true, 72 - post_trial_gap: 500, 73 - }, 72 + post_trial_gap: 500 73 + } 74 74 }, 75 75 timelines: { 76 76 faceHouseTimeline: { ··· 80 80 id: 'interTrial', 81 81 type: 'callback-image-display', 82 82 stimulus: fixation, 83 - response_ends_trial: false, 83 + response_ends_trial: false 84 84 }, 85 85 { 86 86 id: 'trial', 87 - response_ends_trial: false, 88 - }, 89 - ], 90 - }, 91 - }, 87 + response_ends_trial: false 88 + } 89 + ] 90 + } 91 + } 92 92 });
+14 -14
app/utils/labjs/protocols/stroop.js
··· 38 38 { 39 39 name: 'Link 1', 40 40 address: 41 - 'https://www.psychologytoday.com/us/blog/play-in-mind/201204/when-red-looks-blue-and-yes-means-no', 42 - }, 41 + 'https://www.psychologytoday.com/us/blog/play-in-mind/201204/when-red-looks-blue-and-yes-means-no' 42 + } 43 43 ], 44 44 protocal_links: [], 45 45 params: { ··· 54 54 dir: facesDir, 55 55 title: 'Incongruent', 56 56 type: EVENTS.STIMULUS_1, 57 - response: '1', 57 + response: '1' 58 58 }, 59 59 stimulus2: { 60 60 dir: housesDir, 61 61 title: 'Congruent', 62 62 type: EVENTS.STIMULUS_2, 63 - response: '9', 64 - }, 63 + response: '9' 64 + } 65 65 }, 66 66 mainTimeline: ['intro', 'stroopTimeline', 'end'], // array of trial and timeline ids 67 67 trials: { 68 68 intro: { 69 69 type: 'callback-html-display', 70 70 id: 'intro', 71 - post_trial_gap: 1000, 71 + post_trial_gap: 1000 72 72 }, 73 73 end: { 74 74 id: 'end', 75 75 type: 'callback-html-display', 76 76 stimulus: 'Thanks for participating. Press any key to continue', 77 77 response_ends_trial: true, 78 - post_trial_gap: 500, 79 - }, 78 + post_trial_gap: 500 79 + } 80 80 }, 81 81 timelines: { 82 82 faceHouseTimeline: { ··· 86 86 id: 'interTrial', 87 87 type: 'callback-image-display', 88 88 stimulus: fixation, 89 - response_ends_trial: false, 89 + response_ends_trial: false 90 90 }, 91 91 { 92 92 id: 'trial', 93 - response_ends_trial: false, 94 - }, 95 - ], 96 - }, 97 - }, 93 + response_ends_trial: false 94 + } 95 + ] 96 + } 97 + } 98 98 });
+450 -407
app/utils/labjs/scripts/custom.js
··· 1 1 // Define study 2 2 const studyObject = { 3 - "title": "root", 4 - "type": "lab.flow.Sequence", 5 - "parameters": {}, 6 - "plugins": [], 7 - "metadata": {}, 8 - "files": {}, 9 - "responses": {}, 10 - "content": [ 3 + title: 'root', 4 + type: 'lab.flow.Sequence', 5 + parameters: {}, 6 + plugins: [], 7 + metadata: {}, 8 + files: {}, 9 + responses: {}, 10 + content: [ 11 11 { 12 - "type": "lab.flow.Sequence", 13 - "files": {}, 14 - "parameters": {}, 15 - "responses": {}, 16 - "messageHandlers": {}, 17 - "title": "The face-house task", 18 - "content": [ 12 + type: 'lab.flow.Sequence', 13 + files: {}, 14 + parameters: {}, 15 + responses: {}, 16 + messageHandlers: {}, 17 + title: 'The face-house task', 18 + content: [ 19 19 { 20 - "type": "lab.html.Screen", 21 - "files": {}, 22 - "parameters": {}, 23 - "responses": { 24 - "keypress(Space)": "continue", 25 - "keypress(q)": "skipPractice" 20 + type: 'lab.html.Screen', 21 + files: {}, 22 + parameters: {}, 23 + responses: { 24 + 'keypress(Space)': 'continue', 25 + 'keypress(q)': 'skipPractice' 26 26 }, 27 - "messageHandlers": {}, 28 - "title": "Instruction", 29 - "content": "\u003Cheader class=\"content-vertical-center content-horizontal-center\"\u003E\n \u003Ch1\u003E${this.parameters.title || \"The face-house task\"}\u003C\u002Fh1\u003E\n\u003C\u002Fheader\u003E\n\n\u003Cmain\u003E\n\n \u003Cp\u003E\n ${this.parameters.intro}\n \u003C\u002Fp\u003E\n \n\u003C\u002Fmain\u003E\n\n\u003Cfooter class=\"content-vertical-center content-horizontal-center\"\u003E\n \n\u003C\u002Ffooter\u003E" 27 + messageHandlers: {}, 28 + title: 'Instruction', 29 + content: 30 + '\u003Cheader class="content-vertical-center content-horizontal-center"\u003E\n \u003Ch1\u003E${this.parameters.title || "The face-house task"}\u003C\u002Fh1\u003E\n\u003C\u002Fheader\u003E\n\n\u003Cmain\u003E\n\n \u003Cp\u003E\n ${this.parameters.intro}\n \u003C\u002Fp\u003E\n \n\u003C\u002Fmain\u003E\n\n\u003Cfooter class="content-vertical-center content-horizontal-center"\u003E\n \n\u003C\u002Ffooter\u003E' 30 31 }, 31 32 { 32 - "type": "lab.flow.Loop", 33 - "files": {}, 34 - "parameters": {}, 35 - "templateParameters": [], 36 - "sample": { 37 - "mode": "draw-shuffle", 38 - "n": "" 33 + type: 'lab.flow.Loop', 34 + files: {}, 35 + parameters: {}, 36 + templateParameters: [], 37 + sample: { 38 + mode: 'draw-shuffle', 39 + n: '' 39 40 }, 40 - "responses": {}, 41 - "messageHandlers": { 42 - "before:prepare": function anonymous( 43 - ) { 44 - let initParameters = [...this.parameters.stimuli] || []; 45 - initParameters = initParameters.filter(t => t.phase === 'practice') || []; 46 - let numberTrials = this.parameters.nbPracticeTrials; 47 - if(initParameters.length === 0){ 48 - numberTrials = 0; 49 - } 50 - const randomize = this.parameters.randomize; 51 - const trialsLength = initParameters.length; 52 - if(numberTrials > trialsLength){ 53 - const append = [...initParameters] 54 - const multiply = Math.ceil(numberTrials / trialsLength); 55 - for (let i = 0; i < multiply; i++){ 56 - initParameters = initParameters.concat(append) 57 - } 58 - } 41 + responses: {}, 42 + messageHandlers: { 43 + 'before:prepare': function anonymous() { 44 + let initParameters = [...this.parameters.stimuli] || []; 45 + initParameters = 46 + initParameters.filter(t => t.phase === 'practice') || []; 47 + let numberTrials = this.parameters.nbPracticeTrials; 48 + if (initParameters.length === 0) { 49 + numberTrials = 0; 50 + } 51 + const randomize = this.parameters.randomize; 52 + const trialsLength = initParameters.length; 53 + if (numberTrials > trialsLength) { 54 + const append = [...initParameters]; 55 + const multiply = Math.ceil(numberTrials / trialsLength); 56 + for (let i = 0; i < multiply; i++) { 57 + initParameters = initParameters.concat(append); 58 + } 59 + } 59 60 60 - function shuffle(a) { 61 - let j, x, i 62 - for (i = a.length - 1; i > 0; i--) { 63 - j = Math.floor(Math.random() * (i + 1)) 64 - x = a[i] 65 - a[i] = a[j] 66 - a[j] = x 67 - } 68 - return a 69 - } 61 + function shuffle(a) { 62 + let j, x, i; 63 + for (i = a.length - 1; i > 0; i--) { 64 + j = Math.floor(Math.random() * (i + 1)); 65 + x = a[i]; 66 + a[i] = a[j]; 67 + a[j] = x; 68 + } 69 + return a; 70 + } 70 71 71 - if(randomize === 'random'){ 72 - shuffle(initParameters); 73 - } 72 + if (randomize === 'random') { 73 + shuffle(initParameters); 74 + } 74 75 75 - const trialConstructor = ( file ) => ({ 76 - 'condition': file.condition, 77 - 'image': `${file.dir}/${file.filename}`, 78 - 'correctResponse': file.response, 79 - 'phase': 'practice', 80 - 'name': file.name, 81 - 'type': file.type, 82 - }) 76 + const trialConstructor = file => ({ 77 + condition: file.condition, 78 + image: `${file.dir}/${file.filename}`, 79 + correctResponse: file.response, 80 + phase: 'practice', 81 + name: file.name, 82 + type: file.type 83 + }); 83 84 84 - // balance design across conditions 85 - const conditions = Array.from(new Set(initParameters.map(p => p.condition))); 86 - const conditionsParameters = {}; 87 - for (const c of conditions) { 88 - conditionsParameters[c] = initParameters.filter(p => p.condition == c) 89 - } 90 - const numberConditionsTrials = Math.ceil(numberTrials / conditions.length); 91 - let balancedParameters = []; 92 - for (let i = 0; i < numberConditionsTrials; i++){ 93 - for (const c of conditions) { 94 - balancedParameters = balancedParameters.concat(conditionsParameters[c][i % conditionsParameters[c].length]) 95 - } 96 - } 97 - initParameters = [...balancedParameters.slice(0, numberTrials)]; 85 + // balance design across conditions 86 + const conditions = Array.from( 87 + new Set(initParameters.map(p => p.condition)) 88 + ); 89 + const conditionsParameters = {}; 90 + for (const c of conditions) { 91 + conditionsParameters[c] = initParameters.filter( 92 + p => p.condition == c 93 + ); 94 + } 95 + const numberConditionsTrials = Math.ceil( 96 + numberTrials / conditions.length 97 + ); 98 + let balancedParameters = []; 99 + for (let i = 0; i < numberConditionsTrials; i++) { 100 + for (const c of conditions) { 101 + balancedParameters = balancedParameters.concat( 102 + conditionsParameters[c][i % conditionsParameters[c].length] 103 + ); 104 + } 105 + } 106 + initParameters = [...balancedParameters.slice(0, numberTrials)]; 98 107 99 - let practiceParameters = []; 100 - for (let i = 0; i < numberTrials; i++){ 101 - practiceParameters = practiceParameters.concat(trialConstructor(initParameters[i])) 102 - } 108 + let practiceParameters = []; 109 + for (let i = 0; i < numberTrials; i++) { 110 + practiceParameters = practiceParameters.concat( 111 + trialConstructor(initParameters[i]) 112 + ); 113 + } 103 114 104 - // assign options values to parameters of this task 105 - this.options.templateParameters = practiceParameters; 106 - if(randomize === 'random'){ 107 - this.options.shuffle = true; 108 - } else { 109 - this.options.shuffle = false; 110 - } 111 - } 115 + // assign options values to parameters of this task 116 + this.options.templateParameters = practiceParameters; 117 + if (randomize === 'random') { 118 + this.options.shuffle = true; 119 + } else { 120 + this.options.shuffle = false; 121 + } 122 + } 112 123 }, 113 - "title": "Practice loop", 114 - "shuffleGroups": [], 115 - "template": { 116 - "type": "lab.flow.Sequence", 117 - "files": {}, 118 - "parameters": {}, 119 - "responses": {}, 120 - "messageHandlers": {}, 121 - "title": "Trial", 122 - "content": [ 124 + title: 'Practice loop', 125 + shuffleGroups: [], 126 + template: { 127 + type: 'lab.flow.Sequence', 128 + files: {}, 129 + parameters: {}, 130 + responses: {}, 131 + messageHandlers: {}, 132 + title: 'Trial', 133 + content: [ 123 134 { 124 - "type": "lab.canvas.Screen", 125 - "content": [ 135 + type: 'lab.canvas.Screen', 136 + content: [ 126 137 { 127 - "type": "rect", 128 - "left": 0, 129 - "top": 0, 130 - "angle": 0, 131 - "width": 10, 132 - "height": "50", 133 - "stroke": null, 134 - "strokeWidth": 1, 135 - "fill": "black" 138 + type: 'rect', 139 + left: 0, 140 + top: 0, 141 + angle: 0, 142 + width: 10, 143 + height: '50', 144 + stroke: null, 145 + strokeWidth: 1, 146 + fill: 'black' 136 147 }, 137 148 { 138 - "type": "rect", 139 - "left": 0, 140 - "top": 0, 141 - "angle": 90, 142 - "width": 10, 143 - "height": "50", 144 - "stroke": null, 145 - "strokeWidth": 1, 146 - "fill": "black" 149 + type: 'rect', 150 + left: 0, 151 + top: 0, 152 + angle: 90, 153 + width: 10, 154 + height: '50', 155 + stroke: null, 156 + strokeWidth: 1, 157 + fill: 'black' 147 158 } 148 159 ], 149 - "files": {}, 150 - "parameters": {}, 151 - "responses": {}, 152 - "messageHandlers": {}, 153 - "viewport": [ 154 - 800, 155 - 600 156 - ], 157 - "title": "Fixation cross", 158 - "timeout": "${parameters.iti}" 160 + files: {}, 161 + parameters: {}, 162 + responses: {}, 163 + messageHandlers: {}, 164 + viewport: [800, 600], 165 + title: 'Fixation cross', 166 + timeout: '${parameters.iti}' 159 167 }, 160 168 { 161 - "type": "lab.html.Screen", 162 - "files": {}, 163 - "responses": {}, 164 - "parameters": {}, 165 - "messageHandlers": { 166 - "before:prepare": function anonymous( 167 - ) { 168 - // This code registers an event listener for this screen. 169 - // We have a timeout for this screen, but we also want to record responses. 170 - // On a keydown event, we record the key and the time of response. 171 - // We also record whether the response was correct (by comparing the pressed key with the correct response which is defined inside the Experiment loop). 172 - // "this" in the code means the lab.js experiment. 173 - const responses = [... new Set(this.parameters.stimuli.map( e => (e.response)))]; 174 - this.data.trial_number = 1 + parseInt(this.options.id.split('_')[this.options.id.split('_').length-2]); 175 - this.data.response_given = 'no'; 169 + type: 'lab.html.Screen', 170 + files: {}, 171 + responses: {}, 172 + parameters: {}, 173 + messageHandlers: { 174 + 'before:prepare': function anonymous() { 175 + // This code registers an event listener for this screen. 176 + // We have a timeout for this screen, but we also want to record responses. 177 + // On a keydown event, we record the key and the time of response. 178 + // We also record whether the response was correct (by comparing the pressed key with the correct response which is defined inside the Experiment loop). 179 + // "this" in the code means the lab.js experiment. 180 + const responses = [ 181 + ...new Set(this.parameters.stimuli.map(e => e.response)) 182 + ]; 183 + this.data.trial_number = 184 + 1 + 185 + parseInt( 186 + this.options.id.split('_')[ 187 + this.options.id.split('_').length - 2 188 + ] 189 + ); 190 + this.data.response_given = 'no'; 176 191 177 - this.options.events = { 178 - 'keydown': (event) => { 179 - if(responses.includes(event.key)){ 180 - this.data.reaction_time = this.timer; 181 - if(this.parameters.phase === 'task') this.data.response_given = 'yes'; 182 - this.data.response = event.key; 183 - if(this.data.response == this.parameters.correctResponse){ 184 - this.data.correct_response = true; 185 - } else { 186 - this.data.correct_response = false; 187 - } 188 - this.end(); 189 - } 190 - } 191 - } 192 - }, 193 - "run": function anonymous( 194 - ) { 195 - this.parameters.callbackForEEG(this.parameters.type); 196 - } 192 + this.options.events = { 193 + keydown: event => { 194 + if (responses.includes(event.key)) { 195 + this.data.reaction_time = this.timer; 196 + if (this.parameters.phase === 'task') 197 + this.data.response_given = 'yes'; 198 + this.data.response = event.key; 199 + if ( 200 + this.data.response == 201 + this.parameters.correctResponse 202 + ) { 203 + this.data.correct_response = true; 204 + } else { 205 + this.data.correct_response = false; 206 + } 207 + this.end(); 208 + } 209 + } 210 + }; 211 + }, 212 + run: function anonymous() { 213 + this.parameters.callbackForEEG(this.parameters.type); 214 + } 197 215 }, 198 - "title": "Stimulus", 199 - "timeout": "${parameters.selfPaced ? '3600000' : parameters.presentationTime}", 200 - "content": "\u003Cmain class=\"content-horizontal-center content-vertical-center\"\u003E\n \u003Cdiv\u003E\n \u003Cimg src=${ this.files[this.parameters.image] } height=${ this.parameters.imageHeight } \u002F\u003E\n \u003C\u002Fdiv\u003E\n\u003C\u002Fmain\u003E\n\n\u003Cfooter class=\"content-vertical-center content-horizontal-center\"\u003E\n \u003Cp\u003E\n ${this.parameters.taskHelp} \n \u003C\u002Fp\u003E\n\u003C\u002Ffooter\u003E" 216 + title: 'Stimulus', 217 + timeout: 218 + "${parameters.selfPaced ? '3600000' : parameters.presentationTime}", 219 + content: 220 + '\u003Cmain class="content-horizontal-center content-vertical-center"\u003E\n \u003Cdiv\u003E\n \u003Cimg src=${ this.files[this.parameters.image] } height=${ this.parameters.imageHeight } \u002F\u003E\n \u003C\u002Fdiv\u003E\n\u003C\u002Fmain\u003E\n\n\u003Cfooter class="content-vertical-center content-horizontal-center"\u003E\n \u003Cp\u003E\n ${this.parameters.taskHelp} \n \u003C\u002Fp\u003E\n\u003C\u002Ffooter\u003E' 201 221 }, 202 222 { 203 - "type": "lab.canvas.Screen", 204 - "content": [ 223 + type: 'lab.canvas.Screen', 224 + content: [ 205 225 { 206 - "type": "i-text", 207 - "left": 0, 208 - "top": 0, 209 - "angle": 0, 210 - "width": 895.3, 211 - "height": 36.16, 212 - "stroke": null, 213 - "strokeWidth": 1, 214 - "fill": "${ state.correct_response ? 'green' : 'red' }", 215 - "text": "${ state.correct_response ? 'Well done!' : 'Please respond accurately' }", 216 - "fontStyle": "normal", 217 - "fontWeight": "bold", 218 - "fontSize": "52", 219 - "fontFamily": "sans-serif", 220 - "lineHeight": 1.16, 221 - "textAlign": "center" 226 + type: 'i-text', 227 + left: 0, 228 + top: 0, 229 + angle: 0, 230 + width: 895.3, 231 + height: 36.16, 232 + stroke: null, 233 + strokeWidth: 1, 234 + fill: "${ state.correct_response ? 'green' : 'red' }", 235 + text: 236 + "${ state.correct_response ? 'Well done!' : 'Please respond accurately' }", 237 + fontStyle: 'normal', 238 + fontWeight: 'bold', 239 + fontSize: '52', 240 + fontFamily: 'sans-serif', 241 + lineHeight: 1.16, 242 + textAlign: 'center' 222 243 } 223 244 ], 224 - "files": {}, 225 - "parameters": {}, 226 - "responses": {}, 227 - "messageHandlers": { 228 - "end": function anonymous() { 229 - this.data.correct_response = false; 230 - }}, 231 - "viewport": [ 232 - 800, 233 - 600 234 - ], 235 - "title": "Feedback", 236 - "tardy": true, 237 - "timeout": "1000", 238 - "skip": "${ parameters.phase === 'task' }" 245 + files: {}, 246 + parameters: {}, 247 + responses: {}, 248 + messageHandlers: { 249 + end: function anonymous() { 250 + this.data.correct_response = false; 251 + } 252 + }, 253 + viewport: [800, 600], 254 + title: 'Feedback', 255 + tardy: true, 256 + timeout: '1000', 257 + skip: "${ parameters.phase === 'task' }" 239 258 } 240 259 ] 241 260 } 242 261 }, 243 262 { 244 - "type": "lab.html.Screen", 245 - "files": {}, 246 - "parameters": {}, 247 - "responses": { 248 - "keypress(Space)": "continue" 263 + type: 'lab.html.Screen', 264 + files: {}, 265 + parameters: {}, 266 + responses: { 267 + 'keypress(Space)': 'continue' 249 268 }, 250 - "messageHandlers": {}, 251 - "title": "Main task", 252 - "content": "\u003Cheader class=\"content-vertical-center content-horizontal-center\"\u003E\n \u003Ch1\u003EReady for the real data collection?\u003C\u002Fh1\u003E\n\u003C\u002Fheader\u003E\n\u003Cmain\u003E\n\n \u003Cp\u003E\n Press the the space bar to start the main task.\n \u003C\u002Fp\u003E\n\n\u003C\u002Fmain\u003E\n\u003Cfooter class=\"content-vertical-center content-horizontal-center\"\u003E\n \n\u003C\u002Ffooter\u003E" 269 + messageHandlers: {}, 270 + title: 'Main task', 271 + content: 272 + '\u003Cheader class="content-vertical-center content-horizontal-center"\u003E\n \u003Ch1\u003EReady for the real data collection?\u003C\u002Fh1\u003E\n\u003C\u002Fheader\u003E\n\u003Cmain\u003E\n\n \u003Cp\u003E\n Press the the space bar to start the main task.\n \u003C\u002Fp\u003E\n\n\u003C\u002Fmain\u003E\n\u003Cfooter class="content-vertical-center content-horizontal-center"\u003E\n \n\u003C\u002Ffooter\u003E' 253 273 }, 254 274 { 255 - "type": "lab.flow.Loop", 256 - "files": {}, 257 - "parameters": {}, 258 - "templateParameters": [], 259 - "sample": { 260 - "mode": "draw-shuffle", 261 - "n": "" 275 + type: 'lab.flow.Loop', 276 + files: {}, 277 + parameters: {}, 278 + templateParameters: [], 279 + sample: { 280 + mode: 'draw-shuffle', 281 + n: '' 262 282 }, 263 - "responses": {}, 264 - "messageHandlers": { 265 - "before:prepare": function anonymous( 266 - ) { 267 - let initialParameters = [...this.parameters.stimuli] || []; 268 - initialParameters = initialParameters.filter(t => t.phase === 'main') || []; 269 - let numberTrials = this.parameters.nbTrials; 270 - if(initialParameters.length === 0){ 271 - numberTrials = 0; 272 - } 273 - const randomize = this.parameters.randomize; 274 - const trialsLength = initialParameters.length; 275 - if(numberTrials > trialsLength){ 276 - const append = [...initialParameters] 277 - const multiply = Math.ceil(numberTrials / trialsLength); 278 - for (let i = 0; i < multiply; i++){ 279 - initialParameters = initialParameters.concat(append) 280 - } 281 - } 283 + responses: {}, 284 + messageHandlers: { 285 + 'before:prepare': function anonymous() { 286 + let initialParameters = [...this.parameters.stimuli] || []; 287 + initialParameters = 288 + initialParameters.filter(t => t.phase === 'main') || []; 289 + let numberTrials = this.parameters.nbTrials; 290 + if (initialParameters.length === 0) { 291 + numberTrials = 0; 292 + } 293 + const randomize = this.parameters.randomize; 294 + const trialsLength = initialParameters.length; 295 + if (numberTrials > trialsLength) { 296 + const append = [...initialParameters]; 297 + const multiply = Math.ceil(numberTrials / trialsLength); 298 + for (let i = 0; i < multiply; i++) { 299 + initialParameters = initialParameters.concat(append); 300 + } 301 + } 282 302 283 - function shuffle(a) { 284 - let j, x, i 285 - for (i = a.length - 1; i > 0; i--) { 286 - j = Math.floor(Math.random() * (i + 1)) 287 - x = a[i] 288 - a[i] = a[j] 289 - a[j] = x 290 - } 291 - return a 292 - } 303 + function shuffle(a) { 304 + let j, x, i; 305 + for (i = a.length - 1; i > 0; i--) { 306 + j = Math.floor(Math.random() * (i + 1)); 307 + x = a[i]; 308 + a[i] = a[j]; 309 + a[j] = x; 310 + } 311 + return a; 312 + } 293 313 294 - if(randomize === 'random'){ 295 - shuffle(initialParameters); 296 - } 314 + if (randomize === 'random') { 315 + shuffle(initialParameters); 316 + } 297 317 298 - const trialConstructor = ( file ) => ({ 299 - 'condition': file.condition, 300 - 'image': `${file.dir}/${file.filename}`, 301 - 'correctResponse': file.response, 302 - 'phase': 'task', 303 - 'name': file.name, 304 - 'type': file.type, 305 - }) 306 - // balance design across conditions 307 - const conditions = Array.from(new Set(initialParameters.map(p => p.condition))); 308 - const conditionsParameters = {}; 309 - for (const c of conditions) { 310 - conditionsParameters[c] = initialParameters.filter(p => p.condition == c) 311 - } 312 - const numberConditionsTrials = Math.ceil(numberTrials / conditions.length); 313 - let balancedParameters = []; 314 - for (let i = 0; i < numberConditionsTrials; i++){ 315 - for (const c of conditions) { 316 - balancedParameters = balancedParameters.concat(conditionsParameters[c][i % conditionsParameters[c].length]) 317 - } 318 - } 319 - initialParameters = [...balancedParameters.slice(0, numberTrials)]; 318 + const trialConstructor = file => ({ 319 + condition: file.condition, 320 + image: `${file.dir}/${file.filename}`, 321 + correctResponse: file.response, 322 + phase: 'task', 323 + name: file.name, 324 + type: file.type 325 + }); 326 + // balance design across conditions 327 + const conditions = Array.from( 328 + new Set(initialParameters.map(p => p.condition)) 329 + ); 330 + const conditionsParameters = {}; 331 + for (const c of conditions) { 332 + conditionsParameters[c] = initialParameters.filter( 333 + p => p.condition == c 334 + ); 335 + } 336 + const numberConditionsTrials = Math.ceil( 337 + numberTrials / conditions.length 338 + ); 339 + let balancedParameters = []; 340 + for (let i = 0; i < numberConditionsTrials; i++) { 341 + for (const c of conditions) { 342 + balancedParameters = balancedParameters.concat( 343 + conditionsParameters[c][i % conditionsParameters[c].length] 344 + ); 345 + } 346 + } 347 + initialParameters = [ 348 + ...balancedParameters.slice(0, numberTrials) 349 + ]; 320 350 321 - let trialParameters = []; 322 - for (let i = 0; i < numberTrials; i++){ 323 - trialParameters = [...trialParameters.concat(trialConstructor(initialParameters[i]))] 324 - } 325 - // assign options values to parameters of this task 326 - this.options.templateParameters = trialParameters; 327 - if(randomize === 'random'){ 328 - this.options.shuffle = true; 329 - } else { 330 - this.options.shuffle = false; 331 - } 332 - } 351 + let trialParameters = []; 352 + for (let i = 0; i < numberTrials; i++) { 353 + trialParameters = [ 354 + ...trialParameters.concat( 355 + trialConstructor(initialParameters[i]) 356 + ) 357 + ]; 358 + } 359 + // assign options values to parameters of this task 360 + this.options.templateParameters = trialParameters; 361 + if (randomize === 'random') { 362 + this.options.shuffle = true; 363 + } else { 364 + this.options.shuffle = false; 365 + } 366 + } 333 367 }, 334 - "title": "Experiment loop", 335 - "shuffleGroups": [], 336 - "template": { 337 - "type": "lab.flow.Sequence", 338 - "files": {}, 339 - "parameters": {}, 340 - "responses": {}, 341 - "messageHandlers": {}, 342 - "title": "Trial", 343 - "content": [ 368 + title: 'Experiment loop', 369 + shuffleGroups: [], 370 + template: { 371 + type: 'lab.flow.Sequence', 372 + files: {}, 373 + parameters: {}, 374 + responses: {}, 375 + messageHandlers: {}, 376 + title: 'Trial', 377 + content: [ 344 378 { 345 - "type": "lab.canvas.Screen", 346 - "content": [ 379 + type: 'lab.canvas.Screen', 380 + content: [ 347 381 { 348 - "type": "rect", 349 - "left": 0, 350 - "top": 0, 351 - "angle": 0, 352 - "width": 10, 353 - "height": "50", 354 - "stroke": null, 355 - "strokeWidth": 1, 356 - "fill": "black" 382 + type: 'rect', 383 + left: 0, 384 + top: 0, 385 + angle: 0, 386 + width: 10, 387 + height: '50', 388 + stroke: null, 389 + strokeWidth: 1, 390 + fill: 'black' 357 391 }, 358 392 { 359 - "type": "rect", 360 - "left": 0, 361 - "top": 0, 362 - "angle": 90, 363 - "width": 10, 364 - "height": "50", 365 - "stroke": null, 366 - "strokeWidth": 1, 367 - "fill": "black" 393 + type: 'rect', 394 + left: 0, 395 + top: 0, 396 + angle: 90, 397 + width: 10, 398 + height: '50', 399 + stroke: null, 400 + strokeWidth: 1, 401 + fill: 'black' 368 402 } 369 403 ], 370 - "files": {}, 371 - "parameters": {}, 372 - "responses": {}, 373 - "messageHandlers": {}, 374 - "viewport": [ 375 - 800, 376 - 600 377 - ], 378 - "title": "Fixation cross", 379 - "timeout": "${parameters.iti}" 404 + files: {}, 405 + parameters: {}, 406 + responses: {}, 407 + messageHandlers: {}, 408 + viewport: [800, 600], 409 + title: 'Fixation cross', 410 + timeout: '${parameters.iti}' 380 411 }, 381 412 { 382 - "type": "lab.html.Screen", 383 - "files": {}, 384 - "responses": {}, 385 - "parameters": {}, 386 - "messageHandlers": { 387 - "before:prepare": function anonymous( 388 - ) { 389 - // This code registers an event listener for this screen. 390 - // We have a timeout for this screen, but we also want to record responses. 391 - // On a keydown event, we record the key and the time of response. 392 - // We also record whether the response was correct (by comparing the pressed key with the correct response which is defined inside the Experiment loop). 393 - // "this" in the code means the lab.js experiment. 394 - const responses = [... new Set(this.parameters.stimuli.map( e => (e.response)))]; 395 - this.data.trial_number = 1 + parseInt(this.options.id.split('_')[this.options.id.split('_').length-2]); 396 - this.data.response_given = 'no'; 413 + type: 'lab.html.Screen', 414 + files: {}, 415 + responses: {}, 416 + parameters: {}, 417 + messageHandlers: { 418 + 'before:prepare': function anonymous() { 419 + // This code registers an event listener for this screen. 420 + // We have a timeout for this screen, but we also want to record responses. 421 + // On a keydown event, we record the key and the time of response. 422 + // We also record whether the response was correct (by comparing the pressed key with the correct response which is defined inside the Experiment loop). 423 + // "this" in the code means the lab.js experiment. 424 + const responses = [ 425 + ...new Set(this.parameters.stimuli.map(e => e.response)) 426 + ]; 427 + this.data.trial_number = 428 + 1 + 429 + parseInt( 430 + this.options.id.split('_')[ 431 + this.options.id.split('_').length - 2 432 + ] 433 + ); 434 + this.data.response_given = 'no'; 397 435 398 - this.options.events = { 399 - 'keydown': (event) => { 400 - if(responses.includes(event.key)){ 401 - this.data.reaction_time = this.timer; 402 - if(this.parameters.phase === 'task') this.data.response_given = 'yes'; 403 - this.data.response = event.key; 404 - if(this.data.response == this.parameters.correctResponse){ 405 - this.data.correct_response = true; 406 - } else { 407 - this.data.correct_response = false; 408 - } 409 - this.end(); 410 - } 411 - } 412 - } 413 - }, 414 - "run": function anonymous( 415 - ) { 416 - this.parameters.callbackForEEG(this.parameters.type); 417 - } 436 + this.options.events = { 437 + keydown: event => { 438 + if (responses.includes(event.key)) { 439 + this.data.reaction_time = this.timer; 440 + if (this.parameters.phase === 'task') 441 + this.data.response_given = 'yes'; 442 + this.data.response = event.key; 443 + if ( 444 + this.data.response == 445 + this.parameters.correctResponse 446 + ) { 447 + this.data.correct_response = true; 448 + } else { 449 + this.data.correct_response = false; 450 + } 451 + this.end(); 452 + } 453 + } 454 + }; 455 + }, 456 + run: function anonymous() { 457 + this.parameters.callbackForEEG(this.parameters.type); 458 + } 418 459 }, 419 - "title": "Stimulus", 420 - "timeout": "${parameters.selfPaced ? '3600000' : parameters.presentationTime}", 421 - "timeout": "${parameters.selfPaced ? '3600000' : parameters.presentationTime}", 422 - "content": "\u003Cmain class=\"content-horizontal-center content-vertical-center\"\u003E\n \u003Cdiv\u003E\n \u003Cimg src=${ this.files[this.parameters.image] } height=${ this.parameters.imageHeight } \u002F\u003E\n \u003C\u002Fdiv\u003E\n\u003C\u002Fmain\u003E\n\n\u003Cfooter class=\"content-vertical-center content-horizontal-center\"\u003E\n \u003Cp\u003E\n ${this.parameters.taskHelp} \n \u003C\u002Fp\u003E\n\u003C\u002Ffooter\u003E" 460 + title: 'Stimulus', 461 + timeout: 462 + "${parameters.selfPaced ? '3600000' : parameters.presentationTime}", 463 + timeout: 464 + "${parameters.selfPaced ? '3600000' : parameters.presentationTime}", 465 + content: 466 + '\u003Cmain class="content-horizontal-center content-vertical-center"\u003E\n \u003Cdiv\u003E\n \u003Cimg src=${ this.files[this.parameters.image] } height=${ this.parameters.imageHeight } \u002F\u003E\n \u003C\u002Fdiv\u003E\n\u003C\u002Fmain\u003E\n\n\u003Cfooter class="content-vertical-center content-horizontal-center"\u003E\n \u003Cp\u003E\n ${this.parameters.taskHelp} \n \u003C\u002Fp\u003E\n\u003C\u002Ffooter\u003E' 423 467 }, 424 468 { 425 - "type": "lab.canvas.Screen", 426 - "content": [ 469 + type: 'lab.canvas.Screen', 470 + content: [ 427 471 { 428 - "type": "i-text", 429 - "left": 0, 430 - "top": 0, 431 - "angle": 0, 432 - "width": 895.3, 433 - "height": 36.16, 434 - "stroke": null, 435 - "strokeWidth": 1, 436 - "fill": "${ state.correct_response ? 'green' : 'red' }", 437 - "text": "${ state.correct_response ? 'Well done!' : 'Please respond accurately' }", 438 - "fontStyle": "normal", 439 - "fontWeight": "bold", 440 - "fontSize": "52", 441 - "fontFamily": "sans-serif", 442 - "lineHeight": 1.16, 443 - "textAlign": "center" 472 + type: 'i-text', 473 + left: 0, 474 + top: 0, 475 + angle: 0, 476 + width: 895.3, 477 + height: 36.16, 478 + stroke: null, 479 + strokeWidth: 1, 480 + fill: "${ state.correct_response ? 'green' : 'red' }", 481 + text: 482 + "${ state.correct_response ? 'Well done!' : 'Please respond accurately' }", 483 + fontStyle: 'normal', 484 + fontWeight: 'bold', 485 + fontSize: '52', 486 + fontFamily: 'sans-serif', 487 + lineHeight: 1.16, 488 + textAlign: 'center' 444 489 } 445 490 ], 446 - "files": {}, 447 - "parameters": {}, 448 - "responses": {}, 449 - "messageHandlers": {}, 450 - "viewport": [ 451 - 800, 452 - 600 453 - ], 454 - "title": "Feedback", 455 - "tardy": true, 456 - "timeout": "1000", 457 - "skip": "${ parameters.phase === 'task' }" 491 + files: {}, 492 + parameters: {}, 493 + responses: {}, 494 + messageHandlers: {}, 495 + viewport: [800, 600], 496 + title: 'Feedback', 497 + tardy: true, 498 + timeout: '1000', 499 + skip: "${ parameters.phase === 'task' }" 458 500 } 459 501 ] 460 502 } 461 503 }, 462 504 { 463 - "type": "lab.html.Screen", 464 - "files": {}, 465 - "parameters": {}, 466 - "responses": { 467 - "keypress(Space)": "end" 505 + type: 'lab.html.Screen', 506 + files: {}, 507 + parameters: {}, 508 + responses: { 509 + 'keypress(Space)': 'end' 468 510 }, 469 - "messageHandlers": {}, 470 - "title": "End", 471 - "content": "\u003Cheader class=\"content-vertical-center content-horizontal-center\"\u003E\n \n\u003C\u002Fheader\u003E\n\n\u003Cmain\u003E\n \u003Ch1\u003E\n Thank you!\n \u003C\u002Fh1\u003E\n \u003Ch1\u003E\n Press the space bar to finish the task.\n \u003C\u002Fh1\u003E\n\u003C\u002Fmain\u003E\n\n" 511 + messageHandlers: {}, 512 + title: 'End', 513 + content: 514 + '\u003Cheader class="content-vertical-center content-horizontal-center"\u003E\n \n\u003C\u002Fheader\u003E\n\n\u003Cmain\u003E\n \u003Ch1\u003E\n Thank you!\n \u003C\u002Fh1\u003E\n \u003Ch1\u003E\n Press the space bar to finish the task.\n \u003C\u002Fh1\u003E\n\u003C\u002Fmain\u003E\n\n' 472 515 } 473 516 ] 474 517 } 475 518 ] 476 - } 519 + }; 477 520 478 521 // export 479 522 export default studyObject;
+110 -68
app/utils/labjs/scripts/faceshouses.js
··· 22 22 parameters: {}, 23 23 responses: { 24 24 'keypress(Space)': 'continue', 25 - 'keypress(q)': 'skipPractice', 25 + 'keypress(q)': 'skipPractice' 26 26 }, 27 27 messageHandlers: {}, 28 28 title: 'Instruction', 29 29 content: 30 - '\u003Cheader class="content-vertical-center content-horizontal-center"\u003E\n \u003Ch1\u003EThe face-house task\u003C\u002Fh1\u003E\n\u003C\u002Fheader\u003E\n\n\u003Cmain\u003E\n\n \u003Cp\u003E\n ${this.parameters.intro}\n \u003C\u002Fp\u003E\n \n\u003C\u002Fmain\u003E\n\n\u003Cfooter class="content-vertical-center content-horizontal-center"\u003E\n \n\u003C\u002Ffooter\u003E', 30 + '\u003Cheader class="content-vertical-center content-horizontal-center"\u003E\n \u003Ch1\u003EThe face-house task\u003C\u002Fh1\u003E\n\u003C\u002Fheader\u003E\n\n\u003Cmain\u003E\n\n \u003Cp\u003E\n ${this.parameters.intro}\n \u003C\u002Fp\u003E\n \n\u003C\u002Fmain\u003E\n\n\u003Cfooter class="content-vertical-center content-horizontal-center"\u003E\n \n\u003C\u002Ffooter\u003E' 31 31 }, 32 32 { 33 33 type: 'lab.flow.Loop', ··· 36 36 templateParameters: [], 37 37 sample: { 38 38 mode: 'draw-shuffle', 39 - n: '', 39 + n: '' 40 40 }, 41 41 responses: {}, 42 42 messageHandlers: { ··· 72 72 shuffle(initParameters); 73 73 } 74 74 75 - const trialConstructor = (file) => ({ 75 + const trialConstructor = file => ({ 76 76 condition: file.condition, 77 77 image: `${file.dir}/${file.filename}`, 78 78 correctResponse: file.response, 79 79 phase: 'practice', 80 80 name: file.name, 81 - type: file.type, 81 + type: file.type 82 82 }); 83 83 84 84 // balance design across conditions 85 - const conditions = Array.from(new Set(initParameters.map((p) => p.condition))); 85 + const conditions = Array.from( 86 + new Set(initParameters.map(p => p.condition)) 87 + ); 86 88 const conditionsParameters = {}; 87 89 for (const c of conditions) { 88 - conditionsParameters[c] = initParameters.filter((p) => p.condition == c); 90 + conditionsParameters[c] = initParameters.filter( 91 + p => p.condition == c 92 + ); 89 93 } 90 - const numberConditionsTrials = Math.ceil(numberTrials / conditions.length); 94 + const numberConditionsTrials = Math.ceil( 95 + numberTrials / conditions.length 96 + ); 91 97 let balancedParameters = []; 92 98 for (let i = 0; i < numberConditionsTrials; i++) { 93 99 for (const c of conditions) { ··· 100 106 101 107 let practiceParameters = []; 102 108 for (let i = 0; i < numberTrials; i++) { 103 - practiceParameters = practiceParameters.concat(trialConstructor(initParameters[i])); 109 + practiceParameters = practiceParameters.concat( 110 + trialConstructor(initParameters[i]) 111 + ); 104 112 } 105 113 106 114 // assign options values to parameters of this task ··· 110 118 } else { 111 119 this.options.shuffle = false; 112 120 } 113 - }, 121 + } 114 122 }, 115 123 title: 'Practice loop', 116 124 shuffleGroups: [], ··· 134 142 height: '50', 135 143 stroke: null, 136 144 strokeWidth: 1, 137 - fill: 'black', 145 + fill: 'black' 138 146 }, 139 147 { 140 148 type: 'rect', ··· 145 153 height: '50', 146 154 stroke: null, 147 155 strokeWidth: 1, 148 - fill: 'black', 149 - }, 156 + fill: 'black' 157 + } 150 158 ], 151 159 files: {}, 152 160 parameters: {}, ··· 154 162 messageHandlers: {}, 155 163 viewport: [800, 600], 156 164 title: 'Fixation cross', 157 - timeout: '${parameters.iti}', 165 + timeout: '${parameters.iti}' 158 166 }, 159 167 { 160 168 type: 'lab.html.Screen', ··· 168 176 // On a keydown event, we record the key and the time of response. 169 177 // We also record whether the response was correct (by comparing the pressed key with the correct response which is defined inside the Experiment loop). 170 178 // "this" in the code means the lab.js experiment. 171 - const responses = [...new Set(this.parameters.stimuli.map((e) => e.response))]; 179 + const responses = [ 180 + ...new Set(this.parameters.stimuli.map(e => e.response)) 181 + ]; 172 182 this.data.trial_number = 173 183 1 + 174 - parseInt(this.options.id.split('_')[this.options.id.split('_').length - 2]); 184 + parseInt( 185 + this.options.id.split('_')[ 186 + this.options.id.split('_').length - 2 187 + ] 188 + ); 175 189 this.data.response_given = 'no'; 176 190 177 191 this.options.events = { 178 - keydown: (event) => { 192 + keydown: event => { 179 193 if (responses.includes(event.key)) { 180 194 this.data.reaction_time = this.timer; 181 - if (this.parameters.phase === 'task') this.data.response_given = 'yes'; 195 + if (this.parameters.phase === 'task') 196 + this.data.response_given = 'yes'; 182 197 this.data.response = event.key; 183 - if (this.data.response == this.parameters.correctResponse) { 198 + if ( 199 + this.data.response == 200 + this.parameters.correctResponse 201 + ) { 184 202 this.data.correct_response = true; 185 203 } else { 186 204 this.data.correct_response = false; 187 205 } 188 206 this.end(); 189 207 } 190 - }, 208 + } 191 209 }; 192 210 }, 193 211 run: function anonymous() { 194 212 this.parameters.callbackForEEG(this.parameters.type); 195 - }, 213 + } 196 214 }, 197 215 title: 'Stimulus', 198 - timeout: "${parameters.selfPaced ? '3600000' : parameters.presentationTime}", 216 + timeout: 217 + "${parameters.selfPaced ? '3600000' : parameters.presentationTime}", 199 218 content: 200 - '\u003Cmain class="content-horizontal-center content-vertical-center"\u003E\n \u003Cdiv\u003E\n \u003Cimg src=${ this.files[this.parameters.image] } height=${ this.parameters.imageHeight } \u002F\u003E\n \u003C\u002Fdiv\u003E\n\u003C\u002Fmain\u003E\n\n\u003Cfooter class="content-vertical-center content-horizontal-center"\u003E\n \u003Cp\u003E\n ${this.parameters.taskHelp} \n \u003C\u002Fp\u003E\n\u003C\u002Ffooter\u003E', 219 + '\u003Cmain class="content-horizontal-center content-vertical-center"\u003E\n \u003Cdiv\u003E\n \u003Cimg src=${ this.files[this.parameters.image] } height=${ this.parameters.imageHeight } \u002F\u003E\n \u003C\u002Fdiv\u003E\n\u003C\u002Fmain\u003E\n\n\u003Cfooter class="content-vertical-center content-horizontal-center"\u003E\n \u003Cp\u003E\n ${this.parameters.taskHelp} \n \u003C\u002Fp\u003E\n\u003C\u002Ffooter\u003E' 201 220 }, 202 221 { 203 222 type: 'lab.canvas.Screen', ··· 219 238 fontSize: '52', 220 239 fontFamily: 'sans-serif', 221 240 lineHeight: 1.16, 222 - textAlign: 'center', 223 - }, 241 + textAlign: 'center' 242 + } 224 243 ], 225 244 files: {}, 226 245 parameters: {}, ··· 228 247 messageHandlers: { 229 248 end: function anonymous() { 230 249 this.data.correct_response = false; 231 - }, 250 + } 232 251 }, 233 252 viewport: [800, 600], 234 253 title: 'Feedback', 235 254 tardy: true, 236 255 timeout: '1000', 237 - skip: "${ parameters.phase === 'task' }", 238 - }, 239 - ], 240 - }, 256 + skip: "${ parameters.phase === 'task' }" 257 + } 258 + ] 259 + } 241 260 }, 242 261 { 243 262 type: 'lab.html.Screen', 244 263 files: {}, 245 264 parameters: {}, 246 265 responses: { 247 - 'keypress(Space)': 'continue', 266 + 'keypress(Space)': 'continue' 248 267 }, 249 268 messageHandlers: {}, 250 269 title: 'Main task', 251 270 content: 252 - '\u003Cheader class="content-vertical-center content-horizontal-center"\u003E\n \u003Ch1\u003EReady for the real data collection?\u003C\u002Fh1\u003E\n\u003C\u002Fheader\u003E\n\u003Cmain\u003E\n\n \u003Cp\u003E\n Press the the space bar to start the main task.\n \u003C\u002Fp\u003E\n\n\u003C\u002Fmain\u003E\n\u003Cfooter class="content-vertical-center content-horizontal-center"\u003E\n \n\u003C\u002Ffooter\u003E', 271 + '\u003Cheader class="content-vertical-center content-horizontal-center"\u003E\n \u003Ch1\u003EReady for the real data collection?\u003C\u002Fh1\u003E\n\u003C\u002Fheader\u003E\n\u003Cmain\u003E\n\n \u003Cp\u003E\n Press the the space bar to start the main task.\n \u003C\u002Fp\u003E\n\n\u003C\u002Fmain\u003E\n\u003Cfooter class="content-vertical-center content-horizontal-center"\u003E\n \n\u003C\u002Ffooter\u003E' 253 272 }, 254 273 { 255 274 type: 'lab.flow.Loop', ··· 258 277 templateParameters: [], 259 278 sample: { 260 279 mode: 'draw-shuffle', 261 - n: '', 280 + n: '' 262 281 }, 263 282 responses: {}, 264 283 messageHandlers: { 265 284 'before:prepare': function anonymous() { 266 285 let initialParameters = [...this.parameters.stimuli] || []; 267 - initialParameters = initialParameters.filter((t) => t.phase === 'main') || []; 286 + initialParameters = 287 + initialParameters.filter(t => t.phase === 'main') || []; 268 288 let numberTrials = this.parameters.nbTrials; 269 289 if (initialParameters.length === 0) { 270 290 numberTrials = 0; ··· 294 314 shuffle(initialParameters); 295 315 } 296 316 297 - const trialConstructor = (file) => ({ 317 + const trialConstructor = file => ({ 298 318 condition: file.condition, 299 319 image: `${file.dir}/${file.filename}`, 300 320 correctResponse: file.response, 301 321 phase: 'task', 302 322 name: file.name, 303 - type: file.type, 323 + type: file.type 304 324 }); 305 325 // balance design across conditions 306 - const conditions = Array.from(new Set(initialParameters.map((p) => p.condition))); 326 + const conditions = Array.from( 327 + new Set(initialParameters.map(p => p.condition)) 328 + ); 307 329 const conditionsParameters = {}; 308 330 for (const c of conditions) { 309 - conditionsParameters[c] = initialParameters.filter((p) => p.condition == c); 331 + conditionsParameters[c] = initialParameters.filter( 332 + p => p.condition == c 333 + ); 310 334 } 311 - const numberConditionsTrials = Math.ceil(numberTrials / conditions.length); 335 + const numberConditionsTrials = Math.ceil( 336 + numberTrials / conditions.length 337 + ); 312 338 let balancedParameters = []; 313 339 for (let i = 0; i < numberConditionsTrials; i++) { 314 340 for (const c of conditions) { ··· 317 343 ); 318 344 } 319 345 } 320 - initialParameters = [...balancedParameters.slice(0, numberTrials)]; 346 + initialParameters = [ 347 + ...balancedParameters.slice(0, numberTrials) 348 + ]; 321 349 322 350 let trialParameters = []; 323 351 for (let i = 0; i < numberTrials; i++) { 324 352 trialParameters = [ 325 - ...trialParameters.concat(trialConstructor(initialParameters[i])), 353 + ...trialParameters.concat( 354 + trialConstructor(initialParameters[i]) 355 + ) 326 356 ]; 327 357 } 328 358 // assign options values to parameters of this task ··· 332 362 } else { 333 363 this.options.shuffle = false; 334 364 } 335 - }, 365 + } 336 366 }, 337 367 title: 'Experiment loop', 338 368 shuffleGroups: [], ··· 356 386 height: '50', 357 387 stroke: null, 358 388 strokeWidth: 1, 359 - fill: 'black', 389 + fill: 'black' 360 390 }, 361 391 { 362 392 type: 'rect', ··· 367 397 height: '50', 368 398 stroke: null, 369 399 strokeWidth: 1, 370 - fill: 'black', 371 - }, 400 + fill: 'black' 401 + } 372 402 ], 373 403 files: {}, 374 404 parameters: {}, ··· 376 406 messageHandlers: {}, 377 407 viewport: [800, 600], 378 408 title: 'Fixation cross', 379 - timeout: '${parameters.iti}', 409 + timeout: '${parameters.iti}' 380 410 }, 381 411 { 382 412 type: 'lab.html.Screen', ··· 390 420 // On a keydown event, we record the key and the time of response. 391 421 // We also record whether the response was correct (by comparing the pressed key with the correct response which is defined inside the Experiment loop). 392 422 // "this" in the code means the lab.js experiment. 393 - const responses = [...new Set(this.parameters.stimuli.map((e) => e.response))]; 423 + const responses = [ 424 + ...new Set(this.parameters.stimuli.map(e => e.response)) 425 + ]; 394 426 this.data.trial_number = 395 427 1 + 396 - parseInt(this.options.id.split('_')[this.options.id.split('_').length - 2]); 428 + parseInt( 429 + this.options.id.split('_')[ 430 + this.options.id.split('_').length - 2 431 + ] 432 + ); 397 433 this.data.response_given = 'no'; 398 434 399 435 this.options.events = { 400 - keydown: (event) => { 436 + keydown: event => { 401 437 if (responses.includes(event.key)) { 402 438 this.data.reaction_time = this.timer; 403 - if (this.parameters.phase === 'task') this.data.response_given = 'yes'; 439 + if (this.parameters.phase === 'task') 440 + this.data.response_given = 'yes'; 404 441 this.data.response = event.key; 405 - if (this.data.response == this.parameters.correctResponse) { 442 + if ( 443 + this.data.response == 444 + this.parameters.correctResponse 445 + ) { 406 446 this.data.correct_response = true; 407 447 } else { 408 448 this.data.correct_response = false; 409 449 } 410 450 this.end(); 411 451 } 412 - }, 452 + } 413 453 }; 414 454 }, 415 455 run: function anonymous() { 416 456 this.parameters.callbackForEEG(this.parameters.type); 417 - }, 457 + } 418 458 }, 419 459 title: 'Stimulus', 420 - timeout: "${parameters.selfPaced ? '3600000' : parameters.presentationTime}", 421 - timeout: "${parameters.selfPaced ? '3600000' : parameters.presentationTime}", 460 + timeout: 461 + "${parameters.selfPaced ? '3600000' : parameters.presentationTime}", 462 + timeout: 463 + "${parameters.selfPaced ? '3600000' : parameters.presentationTime}", 422 464 content: 423 - '\u003Cmain class="content-horizontal-center content-vertical-center"\u003E\n \u003Cdiv\u003E\n \u003Cimg src=${ this.files[this.parameters.image] } height=${ this.parameters.imageHeight } \u002F\u003E\n \u003C\u002Fdiv\u003E\n\u003C\u002Fmain\u003E\n\n\u003Cfooter class="content-vertical-center content-horizontal-center"\u003E\n \u003Cp\u003E\n ${this.parameters.taskHelp} \n \u003C\u002Fp\u003E\n\u003C\u002Ffooter\u003E', 465 + '\u003Cmain class="content-horizontal-center content-vertical-center"\u003E\n \u003Cdiv\u003E\n \u003Cimg src=${ this.files[this.parameters.image] } height=${ this.parameters.imageHeight } \u002F\u003E\n \u003C\u002Fdiv\u003E\n\u003C\u002Fmain\u003E\n\n\u003Cfooter class="content-vertical-center content-horizontal-center"\u003E\n \u003Cp\u003E\n ${this.parameters.taskHelp} \n \u003C\u002Fp\u003E\n\u003C\u002Ffooter\u003E' 424 466 }, 425 467 { 426 468 type: 'lab.canvas.Screen', ··· 442 484 fontSize: '52', 443 485 fontFamily: 'sans-serif', 444 486 lineHeight: 1.16, 445 - textAlign: 'center', 446 - }, 487 + textAlign: 'center' 488 + } 447 489 ], 448 490 files: {}, 449 491 parameters: {}, ··· 453 495 title: 'Feedback', 454 496 tardy: true, 455 497 timeout: '1000', 456 - skip: "${ parameters.phase === 'task' }", 457 - }, 458 - ], 459 - }, 498 + skip: "${ parameters.phase === 'task' }" 499 + } 500 + ] 501 + } 460 502 }, 461 503 { 462 504 type: 'lab.html.Screen', 463 505 files: {}, 464 506 parameters: {}, 465 507 responses: { 466 - 'keypress(Space)': 'end', 508 + 'keypress(Space)': 'end' 467 509 }, 468 510 messageHandlers: {}, 469 511 title: 'End', 470 512 content: 471 - '\u003Cheader class="content-vertical-center content-horizontal-center"\u003E\n \n\u003C\u002Fheader\u003E\n\n\u003Cmain\u003E\n \u003Ch1\u003E\n Thank you!\n \u003C\u002Fh1\u003E\n \u003Ch1\u003E\n Press the space bar to finish the task.\n \u003C\u002Fh1\u003E\n\u003C\u002Fmain\u003E\n\n', 472 - }, 473 - ], 474 - }, 475 - ], 513 + '\u003Cheader class="content-vertical-center content-horizontal-center"\u003E\n \n\u003C\u002Fheader\u003E\n\n\u003Cmain\u003E\n \u003Ch1\u003E\n Thank you!\n \u003C\u002Fh1\u003E\n \u003Ch1\u003E\n Press the space bar to finish the task.\n \u003C\u002Fh1\u003E\n\u003C\u002Fmain\u003E\n\n' 514 + } 515 + ] 516 + } 517 + ] 476 518 }; 477 519 478 520 // export
+204 -158
app/utils/labjs/scripts/multitasking.js
··· 1 1 import * as path from 'path'; 2 2 3 3 const rootFolder = __dirname; 4 - const assetsDirectory = path.join(rootFolder, 'assets', 'labjs', 'multitasking'); 4 + const assetsDirectory = path.join( 5 + rootFolder, 6 + 'assets', 7 + 'labjs', 8 + 'multitasking' 9 + ); 5 10 6 11 // Define study 7 12 const studyObject = { ··· 26 31 files: {}, 27 32 parameters: {}, 28 33 responses: { 29 - 'keypress(Space)': 'continue', 34 + 'keypress(Space)': 'continue' 30 35 }, 31 36 messageHandlers: {}, 32 37 title: 'Intro', 33 38 content: 34 - '\u003Cheader class="content-vertical-center content-horizontal-center"\u003E\n \u003Ch1\u003EThe multi-tasking test\u003C\u002Fh1\u003E\n\u003C\u002Fheader\u003E\n\n\u003Cmain\u003E\n\n \u003Cp\u003E\n ${this.parameters.intro}\n \u003C\u002Fp\u003E\n \n\u003C\u002Fmain\u003E\n\n\u003Cfooter class="content-vertical-center content-horizontal-center"\u003E\n \n\u003C\u002Ffooter\u003E', 39 + '\u003Cheader class="content-vertical-center content-horizontal-center"\u003E\n \u003Ch1\u003EThe multi-tasking test\u003C\u002Fh1\u003E\n\u003C\u002Fheader\u003E\n\n\u003Cmain\u003E\n\n \u003Cp\u003E\n ${this.parameters.intro}\n \u003C\u002Fp\u003E\n \n\u003C\u002Fmain\u003E\n\n\u003Cfooter class="content-vertical-center content-horizontal-center"\u003E\n \n\u003C\u002Ffooter\u003E' 35 40 }, 36 41 { 37 42 type: 'lab.html.Screen', ··· 46 51 'example_2.png': `${assetsDirectory}/example_2.png`, 47 52 'example_3.png': `${assetsDirectory}/example_3.png`, 48 53 'example_4.png': `${assetsDirectory}/example_4.png`, 49 - 'all_conditions.png': `${assetsDirectory}/all_conditions.png`, 54 + 'all_conditions.png': `${assetsDirectory}/all_conditions.png` 50 55 }, 51 56 parameters: {}, 52 57 responses: { 53 - 'keypress(Space)': 'continue', 58 + 'keypress(Space)': 'continue' 54 59 }, 55 60 messageHandlers: { 56 61 'before:prepare': function anonymous() { 57 - this.options.events['keydown'] = (e) => { 62 + this.options.events['keydown'] = e => { 58 63 if (e.code === 'KeyQ') { 59 64 this.data.skipTraining = true; 60 65 this.end(); 61 66 } 62 67 63 68 if (e.code === 'ArrowLeft' || e.code === 'ArrowRight') { 64 - const instructions = document.querySelectorAll('div.instruction'); 69 + const instructions = document.querySelectorAll( 70 + 'div.instruction' 71 + ); 65 72 let notFound = true; 66 - instructions.forEach((i) => { 73 + instructions.forEach(i => { 67 74 if (i.style.display === 'block' && notFound) { 68 75 const cur_id = parseInt(i.id.split('screen_')[1]); 69 76 let next_id; ··· 75 82 } 76 83 if (next_id > 0 && next_id <= 10) { 77 84 i.style.display = 'none'; 78 - next_id = `screen_${ next_id}`; 79 - document.querySelector(`#${next_id}`).style.display = 'block'; 85 + next_id = `screen_${next_id}`; 86 + document.querySelector(`#${next_id}`).style.display = 87 + 'block'; 80 88 notFound = false; 81 89 } 82 90 } 83 91 }); 84 92 } 85 93 }; 86 - }, 94 + } 87 95 }, 88 96 title: 'Instructions', 89 97 content: 90 - '\u003Cmain\u003E\n\n\u003Cdiv class="instruction" id=\'screen_1\' style="display:block"\u003E\n \u003Cp\u003E\n In the following, you will respond to various figures. These are the figures that you will see: diamonds and rectangles with a filling of 2 or 3 dots:\n \u003C\u002Fp\u003E\n \u003Ctable\u003E\n \u003Cthead\u003E\n \u003Ctr\u003E\n \u003Cth\u003EDiamond with filling of 3 dots\u003C\u002Fth\u003E\n \u003Cth\u003EDiamond with filling of 2 dots\u003C\u002Fth\u003E\n \u003Cth\u003ERectangle with filling of 3 dots\u003C\u002Fth\u003E\n \u003Cth\u003ERectangle with filling of 2 dots\u003C\u002Fth\u003E\n \u003C\u002Ftr\u003E\n \u003C\u002Fthead\u003E\n \u003Ctbody\u003E\n \u003Ctr\u003E\n \u003Ctd\u003E\u003Cimg src=${this.files[\'diamond_3.png\']} style="width:100px"\u003E\u003C\u002Ftd\u003E\n \u003Ctd\u003E\u003Cimg src=${this.files[\'diamond_2.png\']} style="width:100px"\u003E\u003C\u002Ftd\u003E\n \u003Ctd\u003E\u003Cimg src=${this.files[\'rectangle_3.png\']} style="width:100px"\u003E\u003C\u002Ftd\u003E\n \u003Ctd\u003E\u003Cimg src=${this.files[\'rectangle_2.png\']} style="width:100px"\u003E\u003C\u002Ftd\u003E\n \u003C\u002Ftr\u003E\n \u003C\u002Ftbody\u003E\n \u003C\u002Ftable\u003E\n \u003Cp\u003E\n Use the right arrow key to move to the next instruction.\n \u003C\u002Fp\u003E\n\n \u003Cp\u003E\n If you want to skip the instruction with practice trials and go directly to the task, press the "q" button on your keyboard.\n \u003C\u002Fp\u003E\n\n\u003C\u002Fdiv\u003E\n\n\u003Cdiv class="instruction" id=\'screen_2\' style="display:none"\u003E\n\n \u003Cp\u003E\n You will be shown these figures in sequences of trials.\n \u003C\u002Fp\u003E\n \u003Cp\u003E\n Each time you will need to respond with a button press.\n \u003C\u002Fp\u003E\n \u003Cp\u003E\n How exactly will be explained in the next screens.\n \u003C\u002Fp\u003E\n\n\u003C\u002Fdiv\u003E\n\n\u003Cdiv class="instruction" id=\'screen_3\' style="display:none"\u003E\n \u003Cp\u003E\n In the shape task, a diamond requires a b button press. \n \u003C\u002Fp\u003E\n \u003Cp\u003E\n In the shape task, a rectangle requires a n button press. \n \u003C\u002Fp\u003E\n \u003Cp\u003E\n In this task, ignore the filling (dots) of the shape!\n \u003C\u002Fp\u003E\n \u003Cimg src=${this.files[\'shape.png\']} style="width:400px"\u003E\n\u003C\u002Fdiv\u003E\n\n\u003Cdiv class="instruction" id=\'screen_4\' style="display:none"\u003E\n \u003Cp\u003E\n In the filling task, a filling of two dots requires a b button press. \n \u003C\u002Fp\u003E\n \u003Cp\u003E\n In the filling task, a filling with three dots requires a n button press. \n \u003C\u002Fp\u003E\n \u003Cp\u003E\n In the filling task, ignore the outer shape!\n \u003C\u002Fp\u003E\n \u003Cimg src=${this.files[\'filling.png\']} style="width:400px"\u003E\n\u003C\u002Fdiv\u003E\n\n\u003Cdiv class="instruction" id=\'screen_5\' style="display:none"\u003E\n \u003Cp\u003E\n Example 1. If you see a figure in the upper part of the frame, you know you need to do the shape task, that is easy, because the word shape is at the top. Here you see a diamond, which requires a button press of the keyboard key \u003Cstrong\u003E\u003Cem\u003Eb\u003C\u002Fem\u003E\u003C\u002Fstrong\u003E\n \u003C\u002Fp\u003E\n \u003Cp\u003E\n In the shape task, entirely ignore the filling dots!!!\n \u003C\u002Fp\u003E\n \u003Cimg src=${this.files[\'example_1.png\']} style="width:400px"\u003E\n\u003C\u002Fdiv\u003E\n\n\u003Cdiv class="instruction" id=\'screen_6\' style="display:none" style="width:400px"\u003E\n \u003Cp\u003E\n Example 2. If you see a figure in the lower part of the frame, you know you need to do the filling task, that is easy, because the word filling is at the bottom. This diamond is filled with 3 dots, and thus press the keyboard key \u003Cstrong\u003E\u003Cem\u003En\u003C\u002Fem\u003E\u003C\u002Fstrong\u003E\n \u003C\u002Fp\u003E\n \u003Cp\u003E\n In the filling task, entirely ignore the outer shape (here a diamond)!!!\n \u003C\u002Fp\u003E\n \u003Cimg src=${this.files[\'example_2.png\']} style="width:400px"\u003E\n\u003C\u002Fdiv\u003E\n\n\u003Cdiv class="instruction" id=\'screen_7\' style="display:none"\u003E\n \u003Cimg src=${this.files[\'example_3.png\']} style="width:400px"\u003E\n \u003Cp\u003E\n Example 3. If you see a figure in the upper part of the frame, you know you need to do the shape task, that is easy, because the word shape is at the top. Here you see a rectangle, which requires the \u003Cstrong\u003E\u003Cem\u003En\u003C\u002Fem\u003E\u003C\u002Fstrong\u003E key.\n \u003C\u002Fp\u003E\n \u003Cp\u003E\n In the shape task, entirely ignore the filling (here 2 dots)!!!\n \u003C\u002Fp\u003E\n\u003C\u002Fdiv\u003E\n\n\u003Cdiv class="instruction" id=\'screen_8\' style="display:none"\u003E\n \u003Cp\u003E\n Example 4. If you see a figure in the lower part of the frame, you know you need to do the filling task, that is easy, because the word filling is at the bottom. This diamond is filled with 2 dots, and thus press \u003Cstrong\u003E\u003Cem\u003Eb\u003C\u002Fem\u003E\u003C\u002Fstrong\u003E key.\n \u003C\u002Fp\u003E\n \u003Cp\u003E\n In the filling task, entirely ignore the outer shape (here a diamond)!!!\n \u003C\u002Fp\u003E\n \u003Cimg src=${this.files[\'example_4.png\']} style="width:400px"\u003E\n\u003C\u002Fdiv\u003E\n\n\n\u003Cdiv class="instruction" id=\'screen_9\' style="display:none"\u003E\n \u003Cp\u003E\n Here, you can see how to respond in all conditions...\n \u003C\u002Fp\u003E\n \u003Cp\u003E\n Just look at shape in the shape task, and at the dots in the filling task...\n \u003C\u002Fp\u003E\n \u003Cimg src=${this.files[\'all_conditions.png\']} style="width:400px"\u003E\n\u003C\u002Fdiv\u003E\n\n\u003Cdiv class="instruction" id=\'screen_10\' style="display:none"\u003E\n \u003Cp\u003E\n Are you ready? Press the space bar on your keyboard to quit the instructions and start doing the practice trials.\n \u003C\u002Fp\u003E\n \u003Cp\u003E\n Or you can browse back to the previous screens until you understand what you need to do in the two tasks (use the arrow keys).\n \u003C\u002Fp\u003E\n \u003Cp\u003E\n If you want to skip practice trials and go directly to the task, press the "q" button on your keyboard.\n \u003C\u002Fp\u003E\n\u003C\u002Fdiv\u003E\n\n\u003C\u002Fmain\u003E\n\n\n\u003Cfooter\u003E\n \u003Cp\u003E\n Use left\u002Fright arrow keys to go further or back.\n \u003C\u002Fp\u003E\n\u003C\u002Ffooter\u003E', 98 + '\u003Cmain\u003E\n\n\u003Cdiv class="instruction" id=\'screen_1\' style="display:block"\u003E\n \u003Cp\u003E\n In the following, you will respond to various figures. These are the figures that you will see: diamonds and rectangles with a filling of 2 or 3 dots:\n \u003C\u002Fp\u003E\n \u003Ctable\u003E\n \u003Cthead\u003E\n \u003Ctr\u003E\n \u003Cth\u003EDiamond with filling of 3 dots\u003C\u002Fth\u003E\n \u003Cth\u003EDiamond with filling of 2 dots\u003C\u002Fth\u003E\n \u003Cth\u003ERectangle with filling of 3 dots\u003C\u002Fth\u003E\n \u003Cth\u003ERectangle with filling of 2 dots\u003C\u002Fth\u003E\n \u003C\u002Ftr\u003E\n \u003C\u002Fthead\u003E\n \u003Ctbody\u003E\n \u003Ctr\u003E\n \u003Ctd\u003E\u003Cimg src=${this.files[\'diamond_3.png\']} style="width:100px"\u003E\u003C\u002Ftd\u003E\n \u003Ctd\u003E\u003Cimg src=${this.files[\'diamond_2.png\']} style="width:100px"\u003E\u003C\u002Ftd\u003E\n \u003Ctd\u003E\u003Cimg src=${this.files[\'rectangle_3.png\']} style="width:100px"\u003E\u003C\u002Ftd\u003E\n \u003Ctd\u003E\u003Cimg src=${this.files[\'rectangle_2.png\']} style="width:100px"\u003E\u003C\u002Ftd\u003E\n \u003C\u002Ftr\u003E\n \u003C\u002Ftbody\u003E\n \u003C\u002Ftable\u003E\n \u003Cp\u003E\n Use the right arrow key to move to the next instruction.\n \u003C\u002Fp\u003E\n\n \u003Cp\u003E\n If you want to skip the instruction with practice trials and go directly to the task, press the "q" button on your keyboard.\n \u003C\u002Fp\u003E\n\n\u003C\u002Fdiv\u003E\n\n\u003Cdiv class="instruction" id=\'screen_2\' style="display:none"\u003E\n\n \u003Cp\u003E\n You will be shown these figures in sequences of trials.\n \u003C\u002Fp\u003E\n \u003Cp\u003E\n Each time you will need to respond with a button press.\n \u003C\u002Fp\u003E\n \u003Cp\u003E\n How exactly will be explained in the next screens.\n \u003C\u002Fp\u003E\n\n\u003C\u002Fdiv\u003E\n\n\u003Cdiv class="instruction" id=\'screen_3\' style="display:none"\u003E\n \u003Cp\u003E\n In the shape task, a diamond requires a b button press. \n \u003C\u002Fp\u003E\n \u003Cp\u003E\n In the shape task, a rectangle requires a n button press. \n \u003C\u002Fp\u003E\n \u003Cp\u003E\n In this task, ignore the filling (dots) of the shape!\n \u003C\u002Fp\u003E\n \u003Cimg src=${this.files[\'shape.png\']} style="width:400px"\u003E\n\u003C\u002Fdiv\u003E\n\n\u003Cdiv class="instruction" id=\'screen_4\' style="display:none"\u003E\n \u003Cp\u003E\n In the filling task, a filling of two dots requires a b button press. \n \u003C\u002Fp\u003E\n \u003Cp\u003E\n In the filling task, a filling with three dots requires a n button press. \n \u003C\u002Fp\u003E\n \u003Cp\u003E\n In the filling task, ignore the outer shape!\n \u003C\u002Fp\u003E\n \u003Cimg src=${this.files[\'filling.png\']} style="width:400px"\u003E\n\u003C\u002Fdiv\u003E\n\n\u003Cdiv class="instruction" id=\'screen_5\' style="display:none"\u003E\n \u003Cp\u003E\n Example 1. If you see a figure in the upper part of the frame, you know you need to do the shape task, that is easy, because the word shape is at the top. Here you see a diamond, which requires a button press of the keyboard key \u003Cstrong\u003E\u003Cem\u003Eb\u003C\u002Fem\u003E\u003C\u002Fstrong\u003E\n \u003C\u002Fp\u003E\n \u003Cp\u003E\n In the shape task, entirely ignore the filling dots!!!\n \u003C\u002Fp\u003E\n \u003Cimg src=${this.files[\'example_1.png\']} style="width:400px"\u003E\n\u003C\u002Fdiv\u003E\n\n\u003Cdiv class="instruction" id=\'screen_6\' style="display:none" style="width:400px"\u003E\n \u003Cp\u003E\n Example 2. If you see a figure in the lower part of the frame, you know you need to do the filling task, that is easy, because the word filling is at the bottom. This diamond is filled with 3 dots, and thus press the keyboard key \u003Cstrong\u003E\u003Cem\u003En\u003C\u002Fem\u003E\u003C\u002Fstrong\u003E\n \u003C\u002Fp\u003E\n \u003Cp\u003E\n In the filling task, entirely ignore the outer shape (here a diamond)!!!\n \u003C\u002Fp\u003E\n \u003Cimg src=${this.files[\'example_2.png\']} style="width:400px"\u003E\n\u003C\u002Fdiv\u003E\n\n\u003Cdiv class="instruction" id=\'screen_7\' style="display:none"\u003E\n \u003Cimg src=${this.files[\'example_3.png\']} style="width:400px"\u003E\n \u003Cp\u003E\n Example 3. If you see a figure in the upper part of the frame, you know you need to do the shape task, that is easy, because the word shape is at the top. Here you see a rectangle, which requires the \u003Cstrong\u003E\u003Cem\u003En\u003C\u002Fem\u003E\u003C\u002Fstrong\u003E key.\n \u003C\u002Fp\u003E\n \u003Cp\u003E\n In the shape task, entirely ignore the filling (here 2 dots)!!!\n \u003C\u002Fp\u003E\n\u003C\u002Fdiv\u003E\n\n\u003Cdiv class="instruction" id=\'screen_8\' style="display:none"\u003E\n \u003Cp\u003E\n Example 4. If you see a figure in the lower part of the frame, you know you need to do the filling task, that is easy, because the word filling is at the bottom. This diamond is filled with 2 dots, and thus press \u003Cstrong\u003E\u003Cem\u003Eb\u003C\u002Fem\u003E\u003C\u002Fstrong\u003E key.\n \u003C\u002Fp\u003E\n \u003Cp\u003E\n In the filling task, entirely ignore the outer shape (here a diamond)!!!\n \u003C\u002Fp\u003E\n \u003Cimg src=${this.files[\'example_4.png\']} style="width:400px"\u003E\n\u003C\u002Fdiv\u003E\n\n\n\u003Cdiv class="instruction" id=\'screen_9\' style="display:none"\u003E\n \u003Cp\u003E\n Here, you can see how to respond in all conditions...\n \u003C\u002Fp\u003E\n \u003Cp\u003E\n Just look at shape in the shape task, and at the dots in the filling task...\n \u003C\u002Fp\u003E\n \u003Cimg src=${this.files[\'all_conditions.png\']} style="width:400px"\u003E\n\u003C\u002Fdiv\u003E\n\n\u003Cdiv class="instruction" id=\'screen_10\' style="display:none"\u003E\n \u003Cp\u003E\n Are you ready? Press the space bar on your keyboard to quit the instructions and start doing the practice trials.\n \u003C\u002Fp\u003E\n \u003Cp\u003E\n Or you can browse back to the previous screens until you understand what you need to do in the two tasks (use the arrow keys).\n \u003C\u002Fp\u003E\n \u003Cp\u003E\n If you want to skip practice trials and go directly to the task, press the "q" button on your keyboard.\n \u003C\u002Fp\u003E\n\u003C\u002Fdiv\u003E\n\n\u003C\u002Fmain\u003E\n\n\n\u003Cfooter\u003E\n \u003Cp\u003E\n Use left\u002Fright arrow keys to go further or back.\n \u003C\u002Fp\u003E\n\u003C\u002Ffooter\u003E' 91 99 }, 92 100 { 93 101 type: 'lab.canvas.Frame', ··· 108 116 block: 'shape', 109 117 task: 'training', 110 118 num_trials: '10', 111 - cond: 'No switching', 119 + cond: 'No switching' 112 120 }, 113 121 { 114 122 block: 'filling', 115 123 task: 'training', 116 124 num_trials: '10', 117 - cond: 'No switching', 125 + cond: 'No switching' 118 126 }, 119 127 { 120 128 block: 'mixed', 121 129 task: 'training', 122 130 num_trials: '20', 123 - cond: 'Switching', 131 + cond: 'Switching' 124 132 }, 125 133 { 126 134 block: 'shape', 127 135 task: 'main', 128 136 num_trials: '20', 129 - cond: 'No switching', 137 + cond: 'No switching' 130 138 }, 131 139 { 132 140 block: 'filling', 133 141 task: 'main', 134 142 num_trials: '20', 135 - cond: 'No switching', 143 + cond: 'No switching' 136 144 }, 137 145 { 138 146 block: 'mixed', 139 147 task: 'main', 140 148 num_trials: '40', 141 - cond: 'Switching', 142 - }, 149 + cond: 'Switching' 150 + } 143 151 ], 144 152 sample: { 145 153 mode: 'sequential', 146 - n: '6', 154 + n: '6' 147 155 }, 148 156 responses: {}, 149 157 messageHandlers: {}, ··· 157 165 responses: {}, 158 166 messageHandlers: {}, 159 167 title: 'Block sequence', 160 - skip: "${this.parameters.task === 'training' && this.state.skipTraining === true}", 168 + skip: 169 + "${this.parameters.task === 'training' && this.state.skipTraining === true}", 161 170 content: [ 162 171 { 163 172 type: 'lab.canvas.Screen', ··· 208 217 textBackgroundColor: '', 209 218 charSpacing: 0, 210 219 id: '277', 211 - styles: {}, 220 + styles: {} 212 221 }, 213 222 { 214 223 type: 'i-text', ··· 257 266 textBackgroundColor: '', 258 267 charSpacing: 0, 259 268 id: '278', 260 - styles: {}, 269 + styles: {} 261 270 }, 262 271 { 263 272 type: 'i-text', ··· 305 314 textBackgroundColor: '', 306 315 charSpacing: 0, 307 316 id: '279', 308 - styles: {}, 309 - }, 317 + styles: {} 318 + } 310 319 ], 311 320 files: {}, 312 321 parameters: {}, 313 322 responses: { 314 - 'keypress(Space)': 'continue', 323 + 'keypress(Space)': 'continue' 315 324 }, 316 325 messageHandlers: {}, 317 326 viewport: [800, 600], 318 - title: 'Ready', 327 + title: 'Ready' 319 328 }, 320 329 { 321 330 type: 'lab.canvas.Screen', ··· 325 334 version: '2.7.0', 326 335 originX: 'left', 327 336 originY: 'center', 328 - left: "${this.parameters.block === 'mixed' ? 1000 : -120}", 337 + left: 338 + "${this.parameters.block === 'mixed' ? 1000 : -120}", 329 339 top: -64, 330 340 width: 372.81, 331 341 height: 78.11, ··· 380 390 overline: false, 381 391 linethrough: false, 382 392 deltaY: 0, 383 - textBackgroundColor: '', 393 + textBackgroundColor: '' 384 394 }, 385 395 '5': { 386 396 stroke: null, ··· 394 404 overline: false, 395 405 linethrough: false, 396 406 deltaY: 0, 397 - textBackgroundColor: '', 407 + textBackgroundColor: '' 398 408 }, 399 409 '6': { 400 410 stroke: null, ··· 408 418 overline: false, 409 419 linethrough: false, 410 420 deltaY: 0, 411 - textBackgroundColor: '', 421 + textBackgroundColor: '' 412 422 }, 413 423 '7': { 414 424 stroke: null, ··· 422 432 overline: false, 423 433 linethrough: false, 424 434 deltaY: 0, 425 - textBackgroundColor: '', 435 + textBackgroundColor: '' 426 436 }, 427 437 '8': { 428 438 stroke: null, ··· 436 446 overline: false, 437 447 linethrough: false, 438 448 deltaY: 0, 439 - textBackgroundColor: '', 449 + textBackgroundColor: '' 440 450 }, 441 451 '9': { 442 452 stroke: null, ··· 450 460 overline: false, 451 461 linethrough: false, 452 462 deltaY: 0, 453 - textBackgroundColor: '', 463 + textBackgroundColor: '' 454 464 }, 455 465 '10': { 456 466 stroke: null, ··· 464 474 overline: false, 465 475 linethrough: false, 466 476 deltaY: 0, 467 - textBackgroundColor: '', 477 + textBackgroundColor: '' 468 478 }, 469 479 '11': { 470 480 stroke: null, ··· 478 488 overline: false, 479 489 linethrough: false, 480 490 deltaY: 0, 481 - textBackgroundColor: '', 491 + textBackgroundColor: '' 482 492 }, 483 493 '12': { 484 494 stroke: null, ··· 492 502 overline: false, 493 503 linethrough: false, 494 504 deltaY: 0, 495 - textBackgroundColor: '', 505 + textBackgroundColor: '' 496 506 }, 497 507 '13': { 498 508 stroke: null, ··· 506 516 overline: false, 507 517 linethrough: false, 508 518 deltaY: 0, 509 - textBackgroundColor: '', 519 + textBackgroundColor: '' 510 520 }, 511 521 '14': { 512 522 stroke: null, ··· 520 530 overline: false, 521 531 linethrough: false, 522 532 deltaY: 0, 523 - textBackgroundColor: '', 533 + textBackgroundColor: '' 524 534 }, 525 535 '15': { 526 536 stroke: null, ··· 534 544 overline: false, 535 545 linethrough: false, 536 546 deltaY: 0, 537 - textBackgroundColor: '', 547 + textBackgroundColor: '' 538 548 }, 539 549 '16': { 540 550 stroke: null, ··· 548 558 overline: false, 549 559 linethrough: false, 550 560 deltaY: 0, 551 - textBackgroundColor: '', 561 + textBackgroundColor: '' 552 562 }, 553 563 '17': { 554 564 stroke: null, ··· 562 572 overline: false, 563 573 linethrough: false, 564 574 deltaY: 0, 565 - textBackgroundColor: '', 575 + textBackgroundColor: '' 566 576 }, 567 577 '18': { 568 578 stroke: null, ··· 576 586 overline: false, 577 587 linethrough: false, 578 588 deltaY: 0, 579 - textBackgroundColor: '', 589 + textBackgroundColor: '' 580 590 }, 581 591 '19': { 582 592 stroke: null, ··· 590 600 overline: false, 591 601 linethrough: false, 592 602 deltaY: 0, 593 - textBackgroundColor: '', 603 + textBackgroundColor: '' 594 604 }, 595 605 '20': { 596 606 stroke: null, ··· 604 614 overline: false, 605 615 linethrough: false, 606 616 deltaY: 0, 607 - textBackgroundColor: '', 617 + textBackgroundColor: '' 608 618 }, 609 619 '21': { 610 620 stroke: null, ··· 618 628 overline: false, 619 629 linethrough: false, 620 630 deltaY: 0, 621 - textBackgroundColor: '', 631 + textBackgroundColor: '' 622 632 }, 623 633 '22': { 624 634 stroke: null, ··· 632 642 overline: false, 633 643 linethrough: false, 634 644 deltaY: 0, 635 - textBackgroundColor: '', 645 + textBackgroundColor: '' 636 646 }, 637 647 '23': { 638 648 stroke: null, ··· 646 656 overline: false, 647 657 linethrough: false, 648 658 deltaY: 0, 649 - textBackgroundColor: '', 659 + textBackgroundColor: '' 650 660 }, 651 661 '24': { 652 662 stroke: null, ··· 660 670 overline: false, 661 671 linethrough: false, 662 672 deltaY: 0, 663 - textBackgroundColor: '', 673 + textBackgroundColor: '' 664 674 }, 665 675 '25': { 666 676 stroke: null, ··· 674 684 overline: false, 675 685 linethrough: false, 676 686 deltaY: 0, 677 - textBackgroundColor: '', 687 + textBackgroundColor: '' 678 688 }, 679 689 '26': { 680 690 stroke: null, ··· 688 698 overline: false, 689 699 linethrough: false, 690 700 deltaY: 0, 691 - textBackgroundColor: '', 701 + textBackgroundColor: '' 692 702 }, 693 703 '27': { 694 704 stroke: null, ··· 702 712 overline: false, 703 713 linethrough: false, 704 714 deltaY: 0, 705 - textBackgroundColor: '', 706 - }, 707 - }, 708 - }, 715 + textBackgroundColor: '' 716 + } 717 + } 718 + } 709 719 }, 710 720 { 711 721 type: 'i-text', ··· 754 764 textBackgroundColor: '', 755 765 charSpacing: 0, 756 766 id: '300', 757 - styles: {}, 767 + styles: {} 758 768 }, 759 769 { 760 770 type: 'i-text', 761 771 version: '2.7.0', 762 772 originX: 'left', 763 773 originY: 'center', 764 - left: "${this.parameters.block === 'mixed' ? -120 : 1000}", 774 + left: 775 + "${this.parameters.block === 'mixed' ? -120 : 1000}", 765 776 top: -75, 766 777 width: 247.08, 767 778 height: 120.05, ··· 789 800 transformMatrix: null, 790 801 skewX: 0, 791 802 skewY: 0, 792 - text: 'A block with a mix\nof the shape & \nthe filling task', 803 + text: 804 + 'A block with a mix\nof the shape & \nthe filling task', 793 805 fontSize: 32, 794 806 fontWeight: 'normal', 795 807 fontFamily: 'sans-serif', ··· 802 814 textBackgroundColor: '', 803 815 charSpacing: 0, 804 816 id: '397', 805 - styles: {}, 806 - }, 817 + styles: {} 818 + } 807 819 ], 808 820 files: {}, 809 821 parameters: {}, 810 822 responses: { 811 - 'keypress(Space)': 'continue', 823 + 'keypress(Space)': 'continue' 812 824 }, 813 825 messageHandlers: {}, 814 826 viewport: [800, 600], 815 - title: 'Block description', 827 + title: 'Block description' 816 828 }, 817 829 { 818 830 type: 'lab.canvas.Screen', ··· 863 875 textBackgroundColor: '', 864 876 charSpacing: 0, 865 877 id: '276', 866 - styles: {}, 867 - }, 878 + styles: {} 879 + } 868 880 ], 869 881 files: {}, 870 882 parameters: {}, ··· 872 884 messageHandlers: {}, 873 885 viewport: [800, 600], 874 886 title: 'Concentrate', 875 - timeout: '1000', 887 + timeout: '1000' 876 888 }, 877 889 { 878 890 type: 'lab.canvas.Screen', ··· 913 925 radius: 27.5, 914 926 startAngle: 0, 915 927 endAngle: 6.283185307179586, 916 - id: '273', 928 + id: '273' 917 929 }, 918 930 { 919 931 type: 'i-text', ··· 961 973 textBackgroundColor: '', 962 974 charSpacing: 0, 963 975 id: '274', 964 - styles: {}, 965 - }, 976 + styles: {} 977 + } 966 978 ], 967 979 files: {}, 968 980 parameters: {}, ··· 970 982 messageHandlers: {}, 971 983 viewport: [800, 600], 972 984 title: '3', 973 - timeout: '1000', 985 + timeout: '1000' 974 986 }, 975 987 { 976 988 type: 'lab.canvas.Screen', ··· 1011 1023 radius: 27.5, 1012 1024 startAngle: 0, 1013 1025 endAngle: 6.283185307179586, 1014 - id: '273', 1026 + id: '273' 1015 1027 }, 1016 1028 { 1017 1029 type: 'i-text', ··· 1059 1071 textBackgroundColor: '', 1060 1072 charSpacing: 0, 1061 1073 id: '274', 1062 - styles: {}, 1063 - }, 1074 + styles: {} 1075 + } 1064 1076 ], 1065 1077 files: {}, 1066 1078 parameters: {}, ··· 1068 1080 messageHandlers: {}, 1069 1081 viewport: [800, 600], 1070 1082 title: '2', 1071 - timeout: '1000', 1083 + timeout: '1000' 1072 1084 }, 1073 1085 { 1074 1086 type: 'lab.canvas.Screen', ··· 1109 1121 radius: 27.5, 1110 1122 startAngle: 0, 1111 1123 endAngle: 6.283185307179586, 1112 - id: '273', 1124 + id: '273' 1113 1125 }, 1114 1126 { 1115 1127 type: 'i-text', ··· 1157 1169 textBackgroundColor: '', 1158 1170 charSpacing: 0, 1159 1171 id: '274', 1160 - styles: {}, 1161 - }, 1172 + styles: {} 1173 + } 1162 1174 ], 1163 1175 files: {}, 1164 1176 parameters: {}, ··· 1166 1178 messageHandlers: {}, 1167 1179 viewport: [800, 600], 1168 1180 title: '1', 1169 - timeout: '1000', 1181 + timeout: '1000' 1170 1182 }, 1171 1183 { 1172 1184 type: 'lab.flow.Loop', ··· 1175 1187 templateParameters: [], 1176 1188 sample: { 1177 1189 mode: 'draw', 1178 - n: '', 1190 + n: '' 1179 1191 }, 1180 1192 responses: {}, 1181 1193 messageHandlers: { ··· 1197 1209 ? ['shape', 'filling'] 1198 1210 : [this.parameters.block, this.parameters.block]; 1199 1211 1200 - function trialConstructor(block, dots, form, cor_response) { 1212 + function trialConstructor( 1213 + block, 1214 + dots, 1215 + form, 1216 + cor_response 1217 + ) { 1201 1218 return { 1202 1219 type: block, 1203 1220 dots, 1204 1221 form, 1205 - cor_response, 1222 + cor_response 1206 1223 }; 1207 1224 } 1208 1225 1209 - const numberBlocks = Math.ceil(this.parameters.num_trials / 4); 1226 + const numberBlocks = Math.ceil( 1227 + this.parameters.num_trials / 4 1228 + ); 1210 1229 1211 1230 for (let i = 1; i <= numberBlocks; i++) { 1212 1231 for (const block of blocks) { ··· 1246 1265 0, 1247 1266 this.parameters.num_trials 1248 1267 ); 1249 - }, 1268 + } 1250 1269 }, 1251 1270 title: 'Trial loop', 1252 1271 shuffleGroups: [], ··· 1296 1315 skewY: 0, 1297 1316 rx: 0, 1298 1317 ry: 0, 1299 - id: '17', 1318 + id: '17' 1300 1319 }, 1301 1320 { 1302 1321 type: 'line', ··· 1335 1354 x1: -200, 1336 1355 x2: 200, 1337 1356 y1: 0, 1338 - y2: 0, 1357 + y2: 0 1339 1358 }, 1340 1359 { 1341 1360 type: 'rect', 1342 1361 version: '2.7.0', 1343 1362 originX: 'center', 1344 1363 originY: 'center', 1345 - left: "${this.parameters.form === 'square' ? 0 : 1000}", 1346 - top: "${this.parameters.type === 'shape' ? -75 : 75}", 1364 + left: 1365 + "${this.parameters.form === 'square' ? 0 : 1000}", 1366 + top: 1367 + "${this.parameters.type === 'shape' ? -75 : 75}", 1347 1368 width: '100', 1348 1369 height: '100', 1349 1370 fill: 'transparent', ··· 1372 1393 skewY: 0, 1373 1394 rx: 0, 1374 1395 ry: 0, 1375 - id: '91', 1396 + id: '91' 1376 1397 }, 1377 1398 { 1378 1399 type: 'circle', ··· 1380 1401 originX: 'center', 1381 1402 originY: 'center', 1382 1403 left: '0', 1383 - top: "${this.parameters.type === 'shape' ? -50 : 50}", 1404 + top: 1405 + "${this.parameters.type === 'shape' ? -50 : 50}", 1384 1406 width: 20, 1385 1407 height: 20, 1386 1408 fill: 'black', ··· 1410 1432 radius: 10, 1411 1433 startAngle: 0, 1412 1434 endAngle: 6.283185307179586, 1413 - id: '105', 1435 + id: '105' 1414 1436 }, 1415 1437 { 1416 1438 type: 'rect', 1417 1439 version: '2.7.0', 1418 1440 originX: 'center', 1419 1441 originY: 'center', 1420 - left: "${this.parameters.form === 'diamond' ? 0 : 1000}", 1421 - top: "${this.parameters.type === 'shape' ? -75 : 75}", 1442 + left: 1443 + "${this.parameters.form === 'diamond' ? 0 : 1000}", 1444 + top: 1445 + "${this.parameters.type === 'shape' ? -75 : 75}", 1422 1446 width: 80, 1423 1447 height: 80, 1424 1448 fill: 'transparent', ··· 1447 1471 skewY: 0, 1448 1472 rx: 0, 1449 1473 ry: 0, 1450 - id: '98', 1474 + id: '98' 1451 1475 }, 1452 1476 { 1453 1477 type: 'circle', ··· 1455 1479 originX: 'center', 1456 1480 originY: 'center', 1457 1481 left: '${this.parameters.dots === 3 ? 0 : 1000}', 1458 - top: "${this.parameters.type === 'shape' ? -75 : 75}", 1482 + top: 1483 + "${this.parameters.type === 'shape' ? -75 : 75}", 1459 1484 width: 20, 1460 1485 height: 20, 1461 1486 fill: 'black', ··· 1485 1510 radius: 10, 1486 1511 startAngle: 0, 1487 1512 endAngle: 6.283185307179586, 1488 - id: '103', 1513 + id: '103' 1489 1514 }, 1490 1515 { 1491 1516 type: 'circle', ··· 1493 1518 originX: 'center', 1494 1519 originY: 'center', 1495 1520 left: '0', 1496 - top: "${this.parameters.type === 'shape' ? -100 : 100}", 1521 + top: 1522 + "${this.parameters.type === 'shape' ? -100 : 100}", 1497 1523 width: 20, 1498 1524 height: 20, 1499 1525 fill: 'black', ··· 1523 1549 radius: 10, 1524 1550 startAngle: 0, 1525 1551 endAngle: 6.283185307179586, 1526 - id: '104', 1552 + id: '104' 1527 1553 }, 1528 1554 { 1529 1555 type: 'i-text', ··· 1571 1597 textBackgroundColor: '', 1572 1598 charSpacing: 0, 1573 1599 id: '112', 1574 - styles: {}, 1600 + styles: {} 1575 1601 }, 1576 1602 { 1577 1603 type: 'i-text', ··· 1619 1645 textBackgroundColor: '', 1620 1646 charSpacing: 0, 1621 1647 id: '113', 1622 - styles: {}, 1623 - }, 1648 + styles: {} 1649 + } 1624 1650 ], 1625 1651 files: {}, 1626 1652 parameters: {}, 1627 1653 responses: { 1628 1654 'keypress(b)': 'b', 1629 - 'keypress(n)': 'n', 1655 + 'keypress(n)': 'n' 1630 1656 }, 1631 1657 messageHandlers: { 1632 1658 run: function anonymous() { ··· 1634 1660 this.parameters.cond === 'Switching' ? 1 : 2 1635 1661 ); 1636 1662 this.data.correct = 'empty'; 1637 - }, 1663 + } 1638 1664 }, 1639 1665 viewport: [800, 600], 1640 1666 title: 'Stimulus', 1641 1667 correctResponse: '${parameters.cor_response}', 1642 - timeout: '5000', 1668 + timeout: '5000' 1643 1669 }, 1644 1670 { 1645 1671 type: 'lab.canvas.Screen', ··· 1677 1703 transformMatrix: null, 1678 1704 skewX: 0, 1679 1705 skewY: 0, 1680 - text: "${this.state.correct ? '' : 'That was the wrong key.'} ", 1706 + text: 1707 + "${this.state.correct ? '' : 'That was the wrong key.'} ", 1681 1708 fontSize: 32, 1682 1709 fontWeight: 'normal', 1683 1710 fontFamily: 'sans-serif', ··· 1690 1717 textBackgroundColor: '', 1691 1718 charSpacing: 0, 1692 1719 id: '37', 1693 - styles: {}, 1720 + styles: {} 1694 1721 }, 1695 1722 { 1696 1723 type: 'i-text', ··· 1725 1752 transformMatrix: null, 1726 1753 skewX: 0, 1727 1754 skewY: 0, 1728 - text: "${this.state.correct === 'empty' ? 'The time is up' : ''} ", 1755 + text: 1756 + "${this.state.correct === 'empty' ? 'The time is up' : ''} ", 1729 1757 fontSize: 32, 1730 1758 fontWeight: 'normal', 1731 1759 fontFamily: 'sans-serif', ··· 1738 1766 textBackgroundColor: '', 1739 1767 charSpacing: 0, 1740 1768 id: '310', 1741 - styles: {}, 1769 + styles: {} 1742 1770 }, 1743 1771 { 1744 1772 type: 'rect', ··· 1775 1803 skewY: 0, 1776 1804 rx: 0, 1777 1805 ry: 0, 1778 - id: '36', 1806 + id: '36' 1779 1807 }, 1780 1808 { 1781 1809 type: 'line', ··· 1814 1842 x1: -200, 1815 1843 x2: 200, 1816 1844 y1: 0, 1817 - y2: 0, 1845 + y2: 0 1818 1846 }, 1819 1847 { 1820 1848 type: 'i-text', ··· 1862 1890 textBackgroundColor: '', 1863 1891 charSpacing: 0, 1864 1892 id: '126', 1865 - styles: {}, 1893 + styles: {} 1866 1894 }, 1867 1895 { 1868 1896 type: 'i-text', ··· 1910 1938 textBackgroundColor: '', 1911 1939 charSpacing: 0, 1912 1940 id: '127', 1913 - styles: {}, 1914 - }, 1941 + styles: {} 1942 + } 1915 1943 ], 1916 1944 files: {}, 1917 1945 parameters: {}, ··· 1921 1949 this.data.trial_number = 1922 1950 1 + 1923 1951 parseInt( 1924 - this.options.id.split('_')[this.options.id.split('_').length - 2] 1952 + this.options.id.split('_')[ 1953 + this.options.id.split('_').length - 2 1954 + ] 1925 1955 ); 1926 1956 this.data.condition = this.parameters.cond; 1927 1957 this.data.reaction_time = this.state.duration; 1928 1958 1929 - if (this.state.response === this.parameters.cor_response) { 1959 + if ( 1960 + this.state.response === 1961 + this.parameters.cor_response 1962 + ) { 1930 1963 this.data.correct_response = true; 1931 1964 } else { 1932 1965 this.data.correct_response = false; ··· 1938 1971 } else { 1939 1972 this.data.phase = 'practice'; 1940 1973 } 1941 - }, 1974 + } 1942 1975 }, 1943 1976 viewport: [800, 600], 1944 1977 title: 'Feedback', 1945 1978 timeout: '${this.parameters.iti}', 1946 - tardy: true, 1979 + tardy: true 1947 1980 }, 1948 1981 { 1949 1982 type: 'lab.canvas.Screen', ··· 1983 2016 skewY: 0, 1984 2017 rx: 0, 1985 2018 ry: 0, 1986 - id: '123', 2019 + id: '123' 1987 2020 }, 1988 2021 { 1989 2022 type: 'line', ··· 2022 2055 x1: -50, 2023 2056 x2: 50, 2024 2057 y1: 0, 2025 - y2: 0, 2058 + y2: 0 2026 2059 }, 2027 2060 { 2028 2061 type: 'i-text', ··· 2033 2066 top: -195, 2034 2067 width: 78.2, 2035 2068 height: 36.16, 2036 - fill: "${this.parameters.type === 'shape' ? 'black' : 'lightgrey'}", 2069 + fill: 2070 + "${this.parameters.type === 'shape' ? 'black' : 'lightgrey'}", 2037 2071 stroke: null, 2038 2072 strokeWidth: 1, 2039 2073 strokeDashArray: null, ··· 2070 2104 textBackgroundColor: '', 2071 2105 charSpacing: 0, 2072 2106 id: '128', 2073 - styles: {}, 2107 + styles: {} 2074 2108 }, 2075 2109 { 2076 2110 type: 'i-text', ··· 2081 2115 top: '195', 2082 2116 width: 85.36, 2083 2117 height: 36.16, 2084 - fill: "${this.parameters.type === 'filling' ? 'black' : 'lightgrey'}", 2118 + fill: 2119 + "${this.parameters.type === 'filling' ? 'black' : 'lightgrey'}", 2085 2120 stroke: null, 2086 2121 strokeWidth: 1, 2087 2122 strokeDashArray: null, ··· 2118 2153 textBackgroundColor: '', 2119 2154 charSpacing: 0, 2120 2155 id: '129', 2121 - styles: {}, 2156 + styles: {} 2122 2157 }, 2123 2158 { 2124 2159 type: 'rect', ··· 2126 2161 originX: 'center', 2127 2162 originY: 'center', 2128 2163 left: '120', 2129 - top: "${this.parameters.type === 'shape' ? -90 : 1000}", 2164 + top: 2165 + "${this.parameters.type === 'shape' ? -90 : 1000}", 2130 2166 width: 80, 2131 2167 height: 80, 2132 2168 fill: 'transparent', ··· 2155 2191 skewY: 0, 2156 2192 rx: 0, 2157 2193 ry: 0, 2158 - id: '130', 2194 + id: '130' 2159 2195 }, 2160 2196 { 2161 2197 type: 'rect', ··· 2163 2199 originX: 'center', 2164 2200 originY: 'center', 2165 2201 left: '-120', 2166 - top: "${this.parameters.type === 'shape' ? -90 : 1000}", 2202 + top: 2203 + "${this.parameters.type === 'shape' ? -90 : 1000}", 2167 2204 width: 60, 2168 2205 height: 60, 2169 2206 fill: 'transparent', ··· 2192 2229 skewY: 0, 2193 2230 rx: 0, 2194 2231 ry: 0, 2195 - id: '131', 2232 + id: '131' 2196 2233 }, 2197 2234 { 2198 2235 type: 'i-text', ··· 2200 2237 originX: 'center', 2201 2238 originY: 'center', 2202 2239 left: '-120', 2203 - top: "${this.parameters.type === 'shape' ? -25 : 1000}", 2240 + top: 2241 + "${this.parameters.type === 'shape' ? -25 : 1000}", 2204 2242 width: 16, 2205 2243 height: 36.16, 2206 2244 fill: 'black', ··· 2240 2278 textBackgroundColor: '', 2241 2279 charSpacing: 0, 2242 2280 id: '132', 2243 - styles: {}, 2281 + styles: {} 2244 2282 }, 2245 2283 { 2246 2284 type: 'i-text', ··· 2248 2286 originX: 'center', 2249 2287 originY: 'center', 2250 2288 left: '120', 2251 - top: "${this.parameters.type === 'shape' ? -25 : 1000}", 2289 + top: 2290 + "${this.parameters.type === 'shape' ? -25 : 1000}", 2252 2291 width: 16, 2253 2292 height: 36.16, 2254 2293 fill: 'black', ··· 2288 2327 textBackgroundColor: '', 2289 2328 charSpacing: 0, 2290 2329 id: '133', 2291 - styles: {}, 2330 + styles: {} 2292 2331 }, 2293 2332 { 2294 2333 type: 'i-text', ··· 2296 2335 originX: 'center', 2297 2336 originY: 'center', 2298 2337 left: '-120', 2299 - top: "${this.parameters.type === 'filling' ? 125 : 1000}", 2338 + top: 2339 + "${this.parameters.type === 'filling' ? 125 : 1000}", 2300 2340 width: 16, 2301 2341 height: 36.16, 2302 2342 fill: 'black', ··· 2336 2376 textBackgroundColor: '', 2337 2377 charSpacing: 0, 2338 2378 id: '134', 2339 - styles: {}, 2379 + styles: {} 2340 2380 }, 2341 2381 { 2342 2382 type: 'i-text', ··· 2344 2384 originX: 'center', 2345 2385 originY: 'center', 2346 2386 left: '120', 2347 - top: "${this.parameters.type === 'filling' ? 125 : 1000}", 2387 + top: 2388 + "${this.parameters.type === 'filling' ? 125 : 1000}", 2348 2389 width: 16, 2349 2390 height: 36.16, 2350 2391 fill: 'black', ··· 2384 2425 textBackgroundColor: '', 2385 2426 charSpacing: 0, 2386 2427 id: '135', 2387 - styles: {}, 2428 + styles: {} 2388 2429 }, 2389 2430 { 2390 2431 type: 'circle', ··· 2392 2433 originX: 'center', 2393 2434 originY: 'center', 2394 2435 left: '-120', 2395 - top: "${this.parameters.type === 'filling' ? 30 : 1000}", 2436 + top: 2437 + "${this.parameters.type === 'filling' ? 30 : 1000}", 2396 2438 width: 20, 2397 2439 height: 20, 2398 2440 fill: 'black', ··· 2422 2464 radius: 10, 2423 2465 startAngle: 0, 2424 2466 endAngle: 6.283185307179586, 2425 - id: '136', 2467 + id: '136' 2426 2468 }, 2427 2469 { 2428 2470 type: 'circle', ··· 2430 2472 originX: 'center', 2431 2473 originY: 'center', 2432 2474 left: '-120', 2433 - top: "${this.parameters.type === 'filling' ? 90 : 1000}", 2475 + top: 2476 + "${this.parameters.type === 'filling' ? 90 : 1000}", 2434 2477 width: 20, 2435 2478 height: 20, 2436 2479 fill: 'black', ··· 2460 2503 radius: 10, 2461 2504 startAngle: 0, 2462 2505 endAngle: 6.283185307179586, 2463 - id: '137', 2506 + id: '137' 2464 2507 }, 2465 2508 { 2466 2509 type: 'circle', ··· 2468 2511 originX: 'center', 2469 2512 originY: 'center', 2470 2513 left: '120', 2471 - top: "${this.parameters.type === 'filling' ? 90 : 1000}", 2514 + top: 2515 + "${this.parameters.type === 'filling' ? 90 : 1000}", 2472 2516 width: 20, 2473 2517 height: 20, 2474 2518 fill: 'black', ··· 2498 2542 radius: 10, 2499 2543 startAngle: 0, 2500 2544 endAngle: 6.283185307179586, 2501 - id: '138', 2545 + id: '138' 2502 2546 }, 2503 2547 { 2504 2548 type: 'circle', ··· 2506 2550 originX: 'center', 2507 2551 originY: 'center', 2508 2552 left: '120', 2509 - top: "${this.parameters.type === 'filling' ? 60 : 1000}", 2553 + top: 2554 + "${this.parameters.type === 'filling' ? 60 : 1000}", 2510 2555 width: 20, 2511 2556 height: 20, 2512 2557 fill: 'black', ··· 2536 2581 radius: 10, 2537 2582 startAngle: 0, 2538 2583 endAngle: 6.283185307179586, 2539 - id: '139', 2584 + id: '139' 2540 2585 }, 2541 2586 { 2542 2587 type: 'circle', ··· 2544 2589 originX: 'center', 2545 2590 originY: 'center', 2546 2591 left: '120', 2547 - top: "${this.parameters.type === 'filling' ? 30 : 1000}", 2592 + top: 2593 + "${this.parameters.type === 'filling' ? 30 : 1000}", 2548 2594 width: 20, 2549 2595 height: 20, 2550 2596 fill: 'black', ··· 2574 2620 radius: 10, 2575 2621 startAngle: 0, 2576 2622 endAngle: 6.283185307179586, 2577 - id: '140', 2578 - }, 2623 + id: '140' 2624 + } 2579 2625 ], 2580 2626 files: {}, 2581 2627 parameters: {}, ··· 2585 2631 title: 'Pause', 2586 2632 skip: '${this.state.correct === true}', 2587 2633 timeout: '3000', 2588 - tardy: true, 2589 - }, 2590 - ], 2591 - }, 2592 - }, 2593 - ], 2594 - }, 2595 - }, 2634 + tardy: true 2635 + } 2636 + ] 2637 + } 2638 + } 2639 + ] 2640 + } 2641 + } 2596 2642 }, 2597 2643 { 2598 2644 type: 'lab.html.Screen', 2599 2645 files: {}, 2600 2646 parameters: {}, 2601 2647 responses: { 2602 - 'keypress(Space)': 'end', 2648 + 'keypress(Space)': 'end' 2603 2649 }, 2604 2650 messageHandlers: {}, 2605 2651 title: 'End', 2606 2652 content: 2607 - '\u003Cheader class="content-vertical-center content-horizontal-center"\u003E\n \n\u003C\u002Fheader\u003E\n\n\u003Cmain\u003E\n \u003Ch1\u003E\n Thank you!\n \u003C\u002Fh1\u003E\n \u003Ch1\u003E\n Press the space bar to finish the task.\n \u003C\u002Fh1\u003E\n\u003C\u002Fmain\u003E\n\n', 2608 - }, 2609 - ], 2610 - }, 2611 - ], 2653 + '\u003Cheader class="content-vertical-center content-horizontal-center"\u003E\n \n\u003C\u002Fheader\u003E\n\n\u003Cmain\u003E\n \u003Ch1\u003E\n Thank you!\n \u003C\u002Fh1\u003E\n \u003Ch1\u003E\n Press the space bar to finish the task.\n \u003C\u002Fh1\u003E\n\u003C\u002Fmain\u003E\n\n' 2654 + } 2655 + ] 2656 + } 2657 + ] 2612 2658 }; 2613 2659 2614 2660 // export
+106 -88
app/utils/labjs/scripts/stroop.js
··· 22 22 type: 'lab.html.Screen', 23 23 responses: { 24 24 'keypress(Space)': 'continue', 25 - 'keypress(q)': 'skipPractice', 25 + 'keypress(q)': 'skipPractice' 26 26 }, 27 27 title: 'Instruction', 28 28 content: 29 29 '\u003Cheader class="content-vertical-center content-horizontal-center"\u003E\n \u003Ch1\u003EStroop Task\u003C\u002Fh1\u003E\n\u003C\u002Fheader\u003E\n\n\u003Cmain\u003E\n \u003Cp\u003E\n Welcome to the \u003Cstrong\u003EStroop experiment\u003C\u002Fstrong\u003E!\n \u003C\u002Fp\u003E\n \u003Cp\u003E\n ${this.parameters.intro}\n \u003C\u002Fp\u003E\n \u003Cp\u003E\n To indicate the color of the word, please use the keys \u003Cstrong\u003Er\u003C\u002Fstrong\u003E, \u003Cstrong\u003Eg\u003C\u002Fstrong\u003E, \u003Cstrong\u003Eb\u003C\u002Fstrong\u003E and \u003Cstrong\u003Ey\u003C\u002Fstrong\u003E for \u003Cspan style="color: red;"\u003Ered\u003C\u002Fspan\u003E, \u003Cspan style="color: green;"\u003Egreen\u003C\u002Fspan\u003E, \u003Cspan style="color: blue;"\u003Eblue\u003C\u002Fspan\u003E and \u003Cspan style="color: #c5ad0b;"\u003Eyellow\u003C\u002Fspan\u003E, respectively.\n \u003Cbr\u003E\n Please answer quickly, and as accurately as you can.\n \u003C\u002Fp\u003E\n \u003Cp\u003E\n Press the the space bar on your keyboard to start doing the practice trials.\n \u003C\u002Fp\u003E\n \u003Cp\u003E\n If you want to skip the practice trials and go directly to the task, press the "q" button on your keyboard.\n \u003C\u002Fp\u003E\n\u003C\u002Fmain\u003E\n\n\u003Cfooter class="content-vertical-center content-horizontal-center"\u003E\n \n\u003C\u002Ffooter\u003E\n\n\n', 30 30 parameters: {}, 31 - files: {}, 31 + files: {} 32 32 }, 33 33 { 34 34 type: 'lab.canvas.Frame', ··· 50 50 { 51 51 color: 'red', 52 52 word: 'red', 53 - phase: 'practice', 53 + phase: 'practice' 54 54 }, 55 55 { 56 56 color: 'green', 57 57 word: 'green', 58 - phase: 'practice', 58 + phase: 'practice' 59 59 }, 60 60 { 61 61 color: 'blue', 62 62 word: 'blue', 63 - phase: 'practice', 63 + phase: 'practice' 64 64 }, 65 65 { 66 66 color: '#ffe32a', 67 67 word: 'yellow', 68 - phase: 'practice', 68 + phase: 'practice' 69 69 }, 70 70 { 71 71 color: 'red', 72 72 word: 'green', 73 - phase: 'practice', 73 + phase: 'practice' 74 74 }, 75 75 { 76 76 color: 'green', 77 77 word: 'blue', 78 - phase: 'practice', 78 + phase: 'practice' 79 79 }, 80 80 { 81 81 color: 'blue', 82 82 word: 'yellow', 83 - phase: 'practice', 83 + phase: 'practice' 84 84 }, 85 85 { 86 86 color: '#ffe32a', 87 87 word: 'red', 88 - phase: 'practice', 89 - }, 88 + phase: 'practice' 89 + } 90 90 ], 91 91 title: 'Practice task', 92 92 parameters: {}, 93 93 files: {}, 94 94 sample: { 95 - mode: 'draw-shuffle', 95 + mode: 'draw-shuffle' 96 96 }, 97 97 shuffleGroups: [], 98 98 template: { ··· 152 152 textBackgroundColor: '', 153 153 charSpacing: 0, 154 154 id: '5', 155 - styles: {}, 156 - }, 155 + styles: {} 156 + } 157 157 ], 158 158 files: {}, 159 159 parameters: {}, ··· 161 161 messageHandlers: { 162 162 run: function anonymous() { 163 163 this.data.correct = 'empty'; 164 - }, 164 + } 165 165 }, 166 166 viewport: [800, 600], 167 167 title: 'Fixation cross', 168 - timeout: '${this.parameters.iti}', 168 + timeout: '${this.parameters.iti}' 169 169 }, 170 170 { 171 171 type: 'lab.canvas.Screen', ··· 216 216 textBackgroundColor: '', 217 217 charSpacing: 0, 218 218 id: '6', 219 - styles: {}, 220 - }, 219 + styles: {} 220 + } 221 221 ], 222 222 files: {}, 223 223 parameters: {}, ··· 225 225 'keydown(r)': 'red', 226 226 'keydown(g)': 'green', 227 227 'keydown(b)': 'blue', 228 - 'keydown(y)': '#ffe32a', 228 + 'keydown(y)': '#ffe32a' 229 229 }, 230 230 messageHandlers: {}, 231 231 viewport: [800, 600], 232 232 title: 'Stroop screen', 233 - correctResponse: '${ this.parameters.color }', 233 + correctResponse: '${ this.parameters.color }' 234 234 }, 235 235 { 236 236 type: 'lab.canvas.Screen', ··· 268 268 transformMatrix: null, 269 269 skewX: 0, 270 270 skewY: 0, 271 - text: "${ state.correct ? 'Well done!' : 'Please respond accurately' }", 271 + text: 272 + "${ state.correct ? 'Well done!' : 'Please respond accurately' }", 272 273 fontSize: '52', 273 274 fontWeight: 'bold', 274 275 fontFamily: 'sans-serif', ··· 281 282 textBackgroundColor: '', 282 283 charSpacing: 0, 283 284 id: '24', 284 - styles: {}, 285 - }, 285 + styles: {} 286 + } 286 287 ], 287 288 files: {}, 288 289 parameters: {}, ··· 291 292 'before:prepare': function anonymous() { 292 293 this.data.trial_number = 293 294 1 + 294 - parseInt(this.options.id.split('_')[this.options.id.split('_').length - 2]); 295 + parseInt( 296 + this.options.id.split('_')[ 297 + this.options.id.split('_').length - 2 298 + ] 299 + ); 295 300 296 301 this.data.condition = 297 - this.parameters.congruent === 'yes' ? 'Match' : 'Mismatch'; 302 + this.parameters.congruent === 'yes' 303 + ? 'Match' 304 + : 'Mismatch'; 298 305 299 306 this.data.reaction_time = this.state.duration; 300 307 ··· 304 311 this.data.correct_response = false; 305 312 } 306 313 307 - this.data.response_given = this.state.correct === 'empty' ? 'no' : 'yes'; 308 - }, 314 + this.data.response_given = 315 + this.state.correct === 'empty' ? 'no' : 'yes'; 316 + } 309 317 }, 310 318 viewport: [800, 600], 311 319 title: 'Inter-trial interval', 312 320 timeout: "${ parameters.phase == 'practice' ? 1000 : 500 }", 313 - tardy: true, 314 - }, 315 - ], 316 - }, 317 - }, 321 + tardy: true 322 + } 323 + ] 324 + } 325 + } 318 326 }, 319 327 { 320 328 messageHandlers: {}, 321 329 type: 'lab.html.Screen', 322 330 responses: { 323 - 'keypress(Space)': 'continue', 331 + 'keypress(Space)': 'continue' 324 332 }, 325 333 title: 'Main task', 326 334 content: 327 335 '\u003Cheader class="content-vertical-center content-horizontal-center"\u003E\n \u003Ch1\u003EReady for the real data collection?\u003C\u002Fh1\u003E\n\u003C\u002Fheader\u003E\n\u003Cmain\u003E\n\n \u003Cp\u003E\n Press the the space bar to start the main task.\n \u003C\u002Fp\u003E\n\n\u003C\u002Fmain\u003E\n\u003Cfooter class="content-vertical-center content-horizontal-center"\u003E\n \n\u003C\u002Ffooter\u003E\n\n\n', 328 336 parameters: {}, 329 - files: {}, 337 + files: {} 330 338 }, 331 339 { 332 340 type: 'lab.canvas.Frame', ··· 347 355 color: 'red', 348 356 word: 'red', 349 357 phase: 'task', 350 - congruent: 'yes', 358 + congruent: 'yes' 351 359 }, 352 360 { 353 361 color: 'red', 354 362 word: 'red', 355 363 phase: 'task', 356 - congruent: 'yes', 364 + congruent: 'yes' 357 365 }, 358 366 { 359 367 color: 'red', 360 368 word: 'red', 361 369 phase: 'task', 362 - congruent: 'yes', 370 + congruent: 'yes' 363 371 }, 364 372 { 365 373 color: 'red', 366 374 word: 'green', 367 375 phase: 'task', 368 - congruent: 'no', 376 + congruent: 'no' 369 377 }, 370 378 { 371 379 color: 'red', 372 380 word: 'blue', 373 381 phase: 'task', 374 - congruent: 'no', 382 + congruent: 'no' 375 383 }, 376 384 { 377 385 color: 'red', 378 386 word: 'yellow', 379 387 phase: 'task', 380 - congruent: 'no', 388 + congruent: 'no' 381 389 }, 382 390 { 383 391 color: 'green', 384 392 word: 'red', 385 393 phase: 'task', 386 - congruent: 'no', 394 + congruent: 'no' 387 395 }, 388 396 { 389 397 color: 'green', 390 398 word: 'green', 391 399 phase: 'task', 392 - congruent: 'yes', 400 + congruent: 'yes' 393 401 }, 394 402 { 395 403 color: 'green', 396 404 word: 'green', 397 405 phase: 'task', 398 - congruent: 'yes', 406 + congruent: 'yes' 399 407 }, 400 408 { 401 409 color: 'green', 402 410 word: 'green', 403 411 phase: 'task', 404 - congruent: 'yes', 412 + congruent: 'yes' 405 413 }, 406 414 { 407 415 color: 'green', 408 416 word: 'blue', 409 417 phase: 'task', 410 - congruent: 'no', 418 + congruent: 'no' 411 419 }, 412 420 { 413 421 color: 'green', 414 422 word: 'yellow', 415 423 phase: 'task', 416 - congruent: 'no', 424 + congruent: 'no' 417 425 }, 418 426 { 419 427 color: 'blue', 420 428 word: 'red', 421 429 phase: 'task', 422 - congruent: 'no', 430 + congruent: 'no' 423 431 }, 424 432 { 425 433 color: 'blue', 426 434 word: 'green', 427 435 phase: 'task', 428 - congruent: 'no', 436 + congruent: 'no' 429 437 }, 430 438 { 431 439 color: 'blue', 432 440 word: 'blue', 433 441 phase: 'task', 434 - congruent: 'yes', 442 + congruent: 'yes' 435 443 }, 436 444 { 437 445 color: 'blue', 438 446 word: 'blue', 439 447 phase: 'task', 440 - congruent: 'yes', 448 + congruent: 'yes' 441 449 }, 442 450 { 443 451 color: 'blue', 444 452 word: 'blue', 445 453 phase: 'task', 446 - congruent: 'yes', 454 + congruent: 'yes' 447 455 }, 448 456 { 449 457 color: 'blue', 450 458 word: 'yellow', 451 459 phase: 'task', 452 - congruent: 'no', 460 + congruent: 'no' 453 461 }, 454 462 { 455 463 color: '#ffe32a', 456 464 word: 'red', 457 465 phase: 'task', 458 - congruent: 'no', 466 + congruent: 'no' 459 467 }, 460 468 { 461 469 color: '#ffe32a', 462 470 word: 'green', 463 471 phase: 'task', 464 - congruent: 'no', 472 + congruent: 'no' 465 473 }, 466 474 { 467 475 color: '#ffe32a', 468 476 word: 'blue', 469 477 phase: 'task', 470 - congruent: 'no', 478 + congruent: 'no' 471 479 }, 472 480 { 473 481 color: '#ffe32a', 474 482 word: 'yellow', 475 483 phase: 'task', 476 - congruent: 'yes', 484 + congruent: 'yes' 477 485 }, 478 486 { 479 487 color: '#ffe32a', 480 488 word: 'yellow', 481 489 phase: 'task', 482 - congruent: 'yes', 490 + congruent: 'yes' 483 491 }, 484 492 { 485 493 color: '#ffe32a', 486 494 word: 'yellow', 487 495 phase: 'task', 488 - congruent: 'yes', 489 - }, 496 + congruent: 'yes' 497 + } 490 498 ], 491 499 title: 'Stroop task', 492 500 parameters: {}, 493 501 files: {}, 494 502 sample: { 495 503 mode: 'draw-shuffle', 496 - n: '96', 504 + n: '96' 497 505 }, 498 506 shuffleGroups: [], 499 507 template: { ··· 553 561 textBackgroundColor: '', 554 562 charSpacing: 0, 555 563 id: '5', 556 - styles: {}, 557 - }, 564 + styles: {} 565 + } 558 566 ], 559 567 files: {}, 560 568 parameters: {}, ··· 562 570 messageHandlers: { 563 571 run: function anonymous() { 564 572 this.data.correct = 'empty'; 565 - }, 573 + } 566 574 }, 567 575 viewport: [800, 600], 568 576 title: 'Fixation cross', 569 - timeout: '${this.parameters.iti}', 577 + timeout: '${this.parameters.iti}' 570 578 }, 571 579 { 572 580 type: 'lab.canvas.Screen', ··· 617 625 textBackgroundColor: '', 618 626 charSpacing: 0, 619 627 id: '6', 620 - styles: {}, 621 - }, 628 + styles: {} 629 + } 622 630 ], 623 631 files: {}, 624 632 parameters: {}, ··· 626 634 'keydown(r)': 'red', 627 635 'keydown(g)': 'green', 628 636 'keydown(b)': 'blue', 629 - 'keydown(y)': '#ffe32a', 637 + 'keydown(y)': '#ffe32a' 630 638 }, 631 639 messageHandlers: { 632 640 run: function anonymous() { 633 - this.parameters.callbackForEEG(this.parameters.congruent === 'yes' ? 1 : 2); 634 - }, 641 + this.parameters.callbackForEEG( 642 + this.parameters.congruent === 'yes' ? 1 : 2 643 + ); 644 + } 635 645 }, 636 646 viewport: [800, 600], 637 647 title: 'Stroop screen', 638 - correctResponse: '${ this.parameters.color }', 648 + correctResponse: '${ this.parameters.color }' 639 649 }, 640 650 { 641 651 type: 'lab.canvas.Screen', ··· 673 683 transformMatrix: null, 674 684 skewX: 0, 675 685 skewY: 0, 676 - text: "${ state.correct ? 'Well done!' : 'Please respond accurately' }", 686 + text: 687 + "${ state.correct ? 'Well done!' : 'Please respond accurately' }", 677 688 fontSize: '52', 678 689 fontWeight: 'bold', 679 690 fontFamily: 'sans-serif', ··· 686 697 textBackgroundColor: '', 687 698 charSpacing: 0, 688 699 id: '24', 689 - styles: {}, 690 - }, 700 + styles: {} 701 + } 691 702 ], 692 703 files: {}, 693 704 parameters: {}, ··· 696 707 'before:prepare': function anonymous() { 697 708 this.data.trial_number = 698 709 1 + 699 - parseInt(this.options.id.split('_')[this.options.id.split('_').length - 2]); 710 + parseInt( 711 + this.options.id.split('_')[ 712 + this.options.id.split('_').length - 2 713 + ] 714 + ); 700 715 701 716 this.data.condition = 702 - this.parameters.congruent === 'yes' ? 'Match' : 'Mismatch'; 717 + this.parameters.congruent === 'yes' 718 + ? 'Match' 719 + : 'Mismatch'; 703 720 704 721 this.data.reaction_time = this.state.duration; 705 722 ··· 709 726 this.data.correct_response = false; 710 727 } 711 728 712 - this.data.response_given = this.state.correct === 'empty' ? 'no' : 'yes'; 713 - }, 729 + this.data.response_given = 730 + this.state.correct === 'empty' ? 'no' : 'yes'; 731 + } 714 732 }, 715 733 viewport: [800, 600], 716 734 title: 'Inter-trial interval', 717 735 timeout: "${ parameters.phase == 'practice' ? 1000 : 500 }", 718 - tardy: true, 719 - }, 720 - ], 721 - }, 722 - }, 736 + tardy: true 737 + } 738 + ] 739 + } 740 + } 723 741 }, 724 742 { 725 743 messageHandlers: {}, 726 744 type: 'lab.html.Screen', 727 745 responses: { 728 - 'keypress(Space)': 'end', 746 + 'keypress(Space)': 'end' 729 747 }, 730 748 title: 'Thanks', 731 749 content: 732 750 '\u003Cheader class="content-vertical-center content-horizontal-center"\u003E\n \n\u003C\u002Fheader\u003E\n\n\u003Cmain\u003E\n \u003Ch1\u003E\n Thank you!\n \u003C\u002Fh1\u003E\n \u003Ch1\u003E\n Press the space bar to finish the task.\n \u003C\u002Fh1\u003E\n\u003C\u002Fmain\u003E\n\n', 733 751 parameters: {}, 734 - files: {}, 735 - }, 736 - ], 737 - }, 738 - ], 752 + files: {} 753 + } 754 + ] 755 + } 756 + ] 739 757 }; 740 758 741 759 // export
+120 -75
app/utils/labjs/scripts/visualsearch.js
··· 22 22 parameters: {}, 23 23 responses: { 24 24 'keypress(Space)': 'next', 25 - 'keypress(q)': 'skipPractice', 25 + 'keypress(q)': 'skipPractice' 26 26 }, 27 27 messageHandlers: {}, 28 28 title: 'Instruction', 29 - "content": "\u003Cstyle\u003E\n .letter{\n font-size: 90px;\n font-weight: bold;\n }\n\u003C\u002Fstyle\u003E\n\n\u003Cheader\u003E\n \u003Ch1\u003EVisual search task\u003C\u002Fh1\u003E\n\u003C\u002Fheader\u003E\n\n\u003Cmain\u003E\n \n \u003Cp\u003E\n ${this.parameters.intro}\n \u003C\u002Fp\u003E\n \n \u003Cp\u003E\n Again, all you need to do is to find an \u003Cb\u003Eorange T\u003C\u002Fb\u003E. If you see the \u003Cb\u003Eorange T\u003C\u002Fb\u003E, press \u003Ckbd\u003Eb\u003C\u002Fkbd\u003E. Ignore the upside-down orange T, as well as blue Ts! IF THERE IS NO ORANGE T, press \u003Ckbd\u003En\u003C\u002Fkbd\u003E.\n It is very important to respond \u003Cb\u003EAS FAST AS YOU CAN\u003C\u002Fb\u003E.\n \u003C\u002Fp\u003E\n\n \u003Cdiv style=\"display:grid; grid-template-columns:1fr 1fr;\"\u003E\n \u003Cdiv\u003E\n \u003Cp\u003E\n Find \n \u003C\u002Fp\u003E\n \u003Cbr\u003E\n \u003Cdiv class=\"letter\" style=\"color:orange; height: 100px;\"\u003E\n T\n \u003C\u002Fdiv\u003E\n \u003C\u002Fdiv\u003E\n \u003Cdiv\u003E\n \u003Cp\u003E\n But do not respond to any of these distractors:\n \u003C\u002Fp\u003E\n \u003Cbr\u003E\n \u003Cdiv style=\"display:grid; grid-template-columns: 100px 50px; justify-content: center; \"\u003E\n \u003Cdiv class=\"letter\" style=\"color:lightblue;\"\u003E\n T\n \u003C\u002Fdiv\u003E\n \u003Cdiv class=\"letter\" style=\"color:orange; transform: rotate(-180deg);\"\u003E\n T\n \u003C\u002Fdiv\u003E\n \u003C\u002Fdiv\u003E\n \u003C\u002Fdiv\u003E\n \u003C\u002Fdiv\u003E\n\n \u003Cp\u003E\n Press the space bar on your keyboard to start doing the practice trials.\n If you want to skip the practice trials and go directly to the task, press the \"q\" button on your keyboard.\n \u003C\u002Fp\u003E\n\u003C\u002Fmain\u003E" 29 + content: 30 + '\u003Cstyle\u003E\n .letter{\n font-size: 90px;\n font-weight: bold;\n }\n\u003C\u002Fstyle\u003E\n\n\u003Cheader\u003E\n \u003Ch1\u003EVisual search task\u003C\u002Fh1\u003E\n\u003C\u002Fheader\u003E\n\n\u003Cmain\u003E\n \n \u003Cp\u003E\n ${this.parameters.intro}\n \u003C\u002Fp\u003E\n \n \u003Cp\u003E\n Again, all you need to do is to find an \u003Cb\u003Eorange T\u003C\u002Fb\u003E. If you see the \u003Cb\u003Eorange T\u003C\u002Fb\u003E, press \u003Ckbd\u003Eb\u003C\u002Fkbd\u003E. Ignore the upside-down orange T, as well as blue Ts! IF THERE IS NO ORANGE T, press \u003Ckbd\u003En\u003C\u002Fkbd\u003E.\n It is very important to respond \u003Cb\u003EAS FAST AS YOU CAN\u003C\u002Fb\u003E.\n \u003C\u002Fp\u003E\n\n \u003Cdiv style="display:grid; grid-template-columns:1fr 1fr;"\u003E\n \u003Cdiv\u003E\n \u003Cp\u003E\n Find \n \u003C\u002Fp\u003E\n \u003Cbr\u003E\n \u003Cdiv class="letter" style="color:orange; height: 100px;"\u003E\n T\n \u003C\u002Fdiv\u003E\n \u003C\u002Fdiv\u003E\n \u003Cdiv\u003E\n \u003Cp\u003E\n But do not respond to any of these distractors:\n \u003C\u002Fp\u003E\n \u003Cbr\u003E\n \u003Cdiv style="display:grid; grid-template-columns: 100px 50px; justify-content: center; "\u003E\n \u003Cdiv class="letter" style="color:lightblue;"\u003E\n T\n \u003C\u002Fdiv\u003E\n \u003Cdiv class="letter" style="color:orange; transform: rotate(-180deg);"\u003E\n T\n \u003C\u002Fdiv\u003E\n \u003C\u002Fdiv\u003E\n \u003C\u002Fdiv\u003E\n \u003C\u002Fdiv\u003E\n\n \u003Cp\u003E\n Press the space bar on your keyboard to start doing the practice trials.\n If you want to skip the practice trials and go directly to the task, press the "q" button on your keyboard.\n \u003C\u002Fp\u003E\n\u003C\u002Fmain\u003E' 30 31 }, 31 32 { 32 33 type: 'lab.flow.Loop', ··· 34 35 parameters: {}, 35 36 templateParameters: [], 36 37 sample: { 37 - mode: 'draw-shuffle', 38 + mode: 'draw-shuffle' 38 39 }, 39 40 responses: {}, 40 41 messageHandlers: { ··· 52 53 return a; 53 54 } 54 55 55 - const randomBetween = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min; 56 + const randomBetween = (min, max) => 57 + Math.floor(Math.random() * (max - min + 1)) + min; 56 58 57 59 const makeStimuliArray = (arrLen, stLen, isTarget) => { 58 60 const arr = Array(arrLen).fill(0); 59 - const shuffled = shuffle([...Array(arrLen).keys()]).slice(0, stLen); 61 + const shuffled = shuffle([...Array(arrLen).keys()]).slice( 62 + 0, 63 + stLen 64 + ); 60 65 for (const p of shuffled) { 61 66 if (randomBetween(0, 1) === 0) { 62 67 arr[p] = 1; ··· 77 82 stimuli: makeStimuliArray(arrLength, stimLength, isTarget), 78 83 target: isTarget, 79 84 size: stimLength, 80 - phase: 'practice', 85 + phase: 'practice' 81 86 }; 82 87 } 83 88 ··· 112 117 // assign options values to parameters of this task 113 118 this.options.templateParameters = practiceTrialParameters; 114 119 this.options.shuffle = true; // already shuffled before 115 - }, 120 + } 116 121 }, 117 122 title: 'Practice task', 118 123 tardy: true, ··· 185 190 textBackgroundColor: '', 186 191 charSpacing: 0, 187 192 id: '15', 188 - styles: {}, 189 - }, 193 + styles: {} 194 + } 190 195 ], 191 196 files: {}, 192 197 parameters: {}, ··· 195 200 run: function anonymous() { 196 201 this.data.response = 'noresponse'; 197 202 this.data.correct = false; 198 - }, 203 + } 199 204 }, 200 205 viewport: [800, 600], 201 206 title: 'Fixation cross', 202 - timeout: '${this.parameters.iti}', 203 - }, 207 + timeout: '${this.parameters.iti}' 208 + } 204 209 }, 205 210 { 206 211 type: 'lab.html.Screen', ··· 208 213 parameters: {}, 209 214 responses: { 210 215 'keypress(b)': 'yes', 211 - 'keypress(n)': 'no', 216 + 'keypress(n)': 'no' 212 217 }, 213 218 messageHandlers: { 214 219 run: function anonymous() { ··· 239 244 d.appendChild(el); 240 245 taskgrid.appendChild(d); 241 246 } 242 - }, 247 + } 243 248 }, 244 249 title: 'Stimuli', 245 250 content: 246 251 '\u003Cstyle\u003E\n #taskgrid{\n display: grid;\n grid-template-columns: repeat(5, 100px);\n grid-template-rows: repeat(5, 100px);\n grid-row-gap: 10px;\n grid-column-gap: 10px;\n }\n .box{\n display: grid;\n align-items: center;\n } \n .letter{\n font-size: 90px;\n font-weight: bold;\n }\n\u003C\u002Fstyle\u003E\n\n\u003Cmain class="content-vertical-center content-horizontal-center"\u003E\n\n \u003Cdiv id="taskgrid"\u003E\n\n\n \u003C\u002Fdiv\u003E\n\n\u003C\u002Fmain\u003E\n\n\n\u003Cfooter class="content-vertical-center content-horizontal-center"\u003E\n \u003Cp\u003E\n Press \u003Ckbd\u003Eb\u003C\u002Fkbd\u003E if you see the orange T, press \u003Ckbd\u003En\u003C\u002Fkbd\u003E if there is no orange T.\n \u003C\u002Fp\u003E\n\u003C\u002Ffooter\u003E', 247 - correctResponse: '${this.parameters.target}', 252 + correctResponse: '${this.parameters.target}' 248 253 }, 249 254 { 250 255 type: 'lab.html.Screen', ··· 283 288 } 284 289 285 290 if (this.state.response === 'noresponse') { 286 - document.querySelector('#feedback').innerHTML = 'Please respond.'; 291 + document.querySelector('#feedback').innerHTML = 292 + 'Please respond.'; 287 293 return; 288 294 } 289 295 290 296 if (this.state.correct) { 291 - document.querySelector('#feedback').innerHTML = 'Well done!'; 297 + document.querySelector('#feedback').innerHTML = 298 + 'Well done!'; 292 299 document.querySelector('#feedback').style.color = 'green'; 293 300 } else if (this.parameters.target === 'yes') { 294 - document.querySelector('#feedback').innerHTML = 'Error! There was one!'; 295 - document.querySelector('#target').style.border = 'solid'; 296 - } else { 297 - document.querySelector('#feedback').innerHTML = 'Error! There was none!'; 298 - } 301 + document.querySelector('#feedback').innerHTML = 302 + 'Error! There was one!'; 303 + document.querySelector('#target').style.border = 'solid'; 304 + } else { 305 + document.querySelector('#feedback').innerHTML = 306 + 'Error! There was none!'; 307 + } 299 308 }, 300 309 'before:prepare': function anonymous() { 301 310 this.data.trial_number = 302 311 1 + 303 - parseInt(this.options.id.split('_')[this.options.id.split('_').length - 2]); 312 + parseInt( 313 + this.options.id.split('_')[ 314 + this.options.id.split('_').length - 2 315 + ] 316 + ); 304 317 305 - this.data.condition = `${this.parameters.size } letters`; 318 + this.data.condition = `${this.parameters.size} letters`; 306 319 307 320 this.data.reaction_time = this.state.duration; 308 321 // this.data.target = this.parameters.target; ··· 314 327 } 315 328 316 329 this.data.response_given = 317 - this.parameters.phase === 'practice' || this.state.response === 'noresponse' 330 + this.parameters.phase === 'practice' || 331 + this.state.response === 'noresponse' 318 332 ? 'no' 319 333 : 'yes'; 320 - }, 334 + } 321 335 }, 322 336 title: 'Feedback', 323 337 content: 324 338 '\u003Cstyle\u003E\n #taskgrid{\n display: grid;\n grid-template-columns: repeat(5, 100px);\n grid-template-rows: repeat(5, 100px);\n grid-row-gap: 10px;\n grid-column-gap: 10px;\n }\n .box{\n display: grid;\n align-items: center;\n } \n .letter{\n font-size: 90px;\n font-weight: bold;\n }\n .feedback{\n position: absolute;\n top: 50px;\n font-size: 2rem;\n font-weight: bold;\n color: #da5b1c;\n }\n\u003C\u002Fstyle\u003E\n\n\u003Cmain class="content-vertical-center content-horizontal-center"\u003E\n\n \u003Cdiv id="taskgrid"\u003E\n\n\n \u003C\u002Fdiv\u003E\n\n \u003Cdiv id="feedback" class="feedback"\u003E\n Feedback\n \u003C\u002Fdiv\u003E\n\n\u003C\u002Fmain\u003E\n\n\n\u003Cfooter class="content-vertical-center content-horizontal-center"\u003E\n \u003Cp\u003E\n Press \u003Ckbd\u003Eb\u003C\u002Fkbd\u003E if you see the orange T, press \u003Ckbd\u003En\u003C\u002Fkbd\u003E if there is no orange T.\n \u003C\u002Fp\u003E\n\u003C\u002Ffooter\u003E\n\n', 325 339 timeout: '2000', 326 340 tardy: true, 327 - skip: "${ parameters.phase === 'main' }", 328 - }, 329 - ], 330 - }, 341 + skip: "${ parameters.phase === 'main' }" 342 + } 343 + ] 344 + } 331 345 }, 332 346 { 333 347 type: 'lab.html.Screen', 334 348 files: {}, 335 349 parameters: {}, 336 350 responses: { 337 - 'keypress(Space)': 'continue', 351 + 'keypress(Space)': 'continue' 338 352 }, 339 353 messageHandlers: {}, 340 354 title: 'Main task instruction', 341 355 content: 342 - '\u003Cheader class="content-vertical-center content-horizontal-center"\u003E\n \u003Ch1\u003EReady for the real data collection?\u003C\u002Fh1\u003E\n\u003C\u002Fheader\u003E\n\u003Cmain\u003E\n\n \u003Cp\u003E\n Press the the space bar to start the main task.\n \u003C\u002Fp\u003E\n\n\u003C\u002Fmain\u003E\n\u003Cfooter class="content-vertical-center content-horizontal-center"\u003E\n \n\u003C\u002Ffooter\u003E\n', 356 + '\u003Cheader class="content-vertical-center content-horizontal-center"\u003E\n \u003Ch1\u003EReady for the real data collection?\u003C\u002Fh1\u003E\n\u003C\u002Fheader\u003E\n\u003Cmain\u003E\n\n \u003Cp\u003E\n Press the the space bar to start the main task.\n \u003C\u002Fp\u003E\n\n\u003C\u002Fmain\u003E\n\u003Cfooter class="content-vertical-center content-horizontal-center"\u003E\n \n\u003C\u002Ffooter\u003E\n' 343 357 }, 344 358 { 345 359 type: 'lab.flow.Loop', ··· 347 361 parameters: {}, 348 362 templateParameters: [], 349 363 sample: { 350 - mode: 'draw-shuffle', 364 + mode: 'draw-shuffle' 351 365 }, 352 366 responses: {}, 353 367 messageHandlers: { ··· 365 379 return a; 366 380 } 367 381 368 - const randomBetween = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min; 382 + const randomBetween = (min, max) => 383 + Math.floor(Math.random() * (max - min + 1)) + min; 369 384 370 385 const makeStimuliArray = (arrLen, stLen, isTarget) => { 371 386 const arr = Array(arrLen).fill(0); 372 - const shuffled = shuffle([...Array(arrLen).keys()]).slice(0, stLen); 387 + const shuffled = shuffle([...Array(arrLen).keys()]).slice( 388 + 0, 389 + stLen 390 + ); 373 391 for (const p of shuffled) { 374 392 if (randomBetween(0, 1) === 0) { 375 393 arr[p] = 1; ··· 390 408 stimuli: makeStimuliArray(arrLength, stimLength, isTarget), 391 409 target: isTarget, 392 410 size: stimLength, 393 - phase: 'main', 411 + phase: 'main' 394 412 }; 395 413 } 396 414 397 415 const numberTrials = 10; 398 416 for (let i = 1; i <= numberTrials; i++) { 399 - trialParameters = trialParameters.concat(trialConstructor(i, 5, 'yes')); 400 - trialParameters = trialParameters.concat(trialConstructor(i, 5, 'no')); 401 - trialParameters = trialParameters.concat(trialConstructor(i, 10, 'yes')); 402 - trialParameters = trialParameters.concat(trialConstructor(i, 10, 'no')); 403 - trialParameters = trialParameters.concat(trialConstructor(i, 15, 'yes')); 404 - trialParameters = trialParameters.concat(trialConstructor(i, 15, 'no')); 405 - trialParameters = trialParameters.concat(trialConstructor(i, 20, 'yes')); 406 - trialParameters = trialParameters.concat(trialConstructor(i, 20, 'no')); 417 + trialParameters = trialParameters.concat( 418 + trialConstructor(i, 5, 'yes') 419 + ); 420 + trialParameters = trialParameters.concat( 421 + trialConstructor(i, 5, 'no') 422 + ); 423 + trialParameters = trialParameters.concat( 424 + trialConstructor(i, 10, 'yes') 425 + ); 426 + trialParameters = trialParameters.concat( 427 + trialConstructor(i, 10, 'no') 428 + ); 429 + trialParameters = trialParameters.concat( 430 + trialConstructor(i, 15, 'yes') 431 + ); 432 + trialParameters = trialParameters.concat( 433 + trialConstructor(i, 15, 'no') 434 + ); 435 + trialParameters = trialParameters.concat( 436 + trialConstructor(i, 20, 'yes') 437 + ); 438 + trialParameters = trialParameters.concat( 439 + trialConstructor(i, 20, 'no') 440 + ); 407 441 } 408 442 409 443 // assign options values to parameters of this task 410 444 this.options.templateParameters = trialParameters; 411 445 this.options.shuffle = true; // already shuffled before 412 - }, 446 + } 413 447 }, 414 448 title: 'Main task', 415 449 shuffleGroups: [], ··· 480 514 textBackgroundColor: '', 481 515 charSpacing: 0, 482 516 id: '15', 483 - styles: {}, 484 - }, 517 + styles: {} 518 + } 485 519 ], 486 520 files: {}, 487 521 parameters: {}, ··· 490 524 run: function anonymous() { 491 525 this.data.response = 'noresponse'; 492 526 this.data.correct = false; 493 - }, 527 + } 494 528 }, 495 529 viewport: [800, 600], 496 530 title: 'Fixation cross', 497 - timeout: '${this.parameters.iti}', 498 - }, 531 + timeout: '${this.parameters.iti}' 532 + } 499 533 }, 500 534 { 501 535 type: 'lab.html.Screen', ··· 503 537 parameters: {}, 504 538 responses: { 505 539 'keypress(b)': 'yes', 506 - 'keypress(n)': 'no', 540 + 'keypress(n)': 'no' 507 541 }, 508 542 messageHandlers: { 509 543 run: function anonymous() { 510 - this.parameters.callbackForEEG(parseInt(this.parameters.size) < 13 ? 2 : 1); 544 + this.parameters.callbackForEEG( 545 + parseInt(this.parameters.size) < 13 ? 2 : 1 546 + ); 511 547 const taskgrid = document.querySelector('#taskgrid'); 512 548 const stimuli = this.parameters.stimuli; 513 549 ··· 535 571 d.appendChild(el); 536 572 taskgrid.appendChild(d); 537 573 } 538 - }, 574 + } 539 575 }, 540 576 title: 'Stimuli', 541 577 content: 542 578 '\u003Cstyle\u003E\n #taskgrid{\n display: grid;\n grid-template-columns: repeat(5, 100px);\n grid-template-rows: repeat(5, 100px);\n grid-row-gap: 10px;\n grid-column-gap: 10px;\n }\n .box{\n display: grid;\n align-items: center;\n } \n .letter{\n font-size: 90px;\n font-weight: bold;\n }\n\u003C\u002Fstyle\u003E\n\n\u003Cmain class="content-vertical-center content-horizontal-center"\u003E\n\n \u003Cdiv id="taskgrid"\u003E\n\n\n \u003C\u002Fdiv\u003E\n\n\u003C\u002Fmain\u003E\n\n\n\u003Cfooter class="content-vertical-center content-horizontal-center"\u003E\n \u003Cp\u003E\n Press \u003Ckbd\u003Eb\u003C\u002Fkbd\u003E if you see the orange T, press \u003Ckbd\u003En\u003C\u002Fkbd\u003E if there is no orange T.\n \u003C\u002Fp\u003E\n\u003C\u002Ffooter\u003E', 543 - correctResponse: '${this.parameters.target}', 579 + correctResponse: '${this.parameters.target}' 544 580 }, 545 581 { 546 582 type: 'lab.html.Screen', ··· 579 615 } 580 616 581 617 if (this.state.response === 'noresponse') { 582 - document.querySelector('#feedback').innerHTML = 'Please respond!'; 618 + document.querySelector('#feedback').innerHTML = 619 + 'Please respond!'; 583 620 return; 584 621 } 585 622 586 623 if (this.state.correct) { 587 - document.querySelector('#feedback').innerHTML = 'Well done!'; 624 + document.querySelector('#feedback').innerHTML = 625 + 'Well done!'; 588 626 document.querySelector('#feedback').style.color = 'green'; 589 627 } else if (this.parameters.target === 'yes') { 590 - document.querySelector('#feedback').innerHTML = 'Error! There was one!'; 591 - document.querySelector('#target').style.border = 'solid'; 592 - } else { 593 - document.querySelector('#feedback').innerHTML = 'Error! There was none!'; 594 - } 628 + document.querySelector('#feedback').innerHTML = 629 + 'Error! There was one!'; 630 + document.querySelector('#target').style.border = 'solid'; 631 + } else { 632 + document.querySelector('#feedback').innerHTML = 633 + 'Error! There was none!'; 634 + } 595 635 }, 596 636 'before:prepare': function anonymous() { 597 637 this.data.trial_number = 598 638 1 + 599 - parseInt(this.options.id.split('_')[this.options.id.split('_').length - 2]); 639 + parseInt( 640 + this.options.id.split('_')[ 641 + this.options.id.split('_').length - 2 642 + ] 643 + ); 600 644 601 - this.data.condition = `${this.parameters.size } letters`; 645 + this.data.condition = `${this.parameters.size} letters`; 602 646 603 647 this.data.reaction_time = this.state.duration; 604 648 // this.data.target = this.parameters.target; ··· 610 654 } 611 655 612 656 this.data.response_given = 613 - this.parameters.phase === 'practice' || this.state.response === 'noresponse' 657 + this.parameters.phase === 'practice' || 658 + this.state.response === 'noresponse' 614 659 ? 'no' 615 660 : 'yes'; 616 - }, 661 + } 617 662 }, 618 663 title: 'Feedback', 619 664 content: 620 665 '\u003Cstyle\u003E\n #taskgrid{\n display: grid;\n grid-template-columns: repeat(5, 100px);\n grid-template-rows: repeat(5, 100px);\n grid-row-gap: 10px;\n grid-column-gap: 10px;\n }\n .box{\n display: grid;\n align-items: center;\n } \n .letter{\n font-size: 90px;\n font-weight: bold;\n }\n .feedback{\n position: absolute;\n top: 50px;\n font-size: 2rem;\n font-weight: bold;\n color: #da5b1c;\n }\n\u003C\u002Fstyle\u003E\n\n\u003Cmain class="content-vertical-center content-horizontal-center"\u003E\n\n \u003Cdiv id="taskgrid"\u003E\n\n\n \u003C\u002Fdiv\u003E\n\n \u003Cdiv id="feedback" class="feedback"\u003E\n Feedback\n \u003C\u002Fdiv\u003E\n\n\u003C\u002Fmain\u003E\n\n\n\u003Cfooter class="content-vertical-center content-horizontal-center"\u003E\n \u003Cp\u003E\n Press \u003Ckbd\u003Eb\u003C\u002Fkbd\u003E if you see the orange T, press \u003Ckbd\u003En\u003C\u002Fkbd\u003E if there is no orange T.\n \u003C\u002Fp\u003E\n\u003C\u002Ffooter\u003E\n\n', 621 666 timeout: '2000', 622 667 tardy: true, 623 - skip: "${ parameters.phase === 'main' }", 624 - }, 625 - ], 626 - }, 668 + skip: "${ parameters.phase === 'main' }" 669 + } 670 + ] 671 + } 627 672 }, 628 673 { 629 674 type: 'lab.html.Screen', 630 675 files: {}, 631 676 parameters: {}, 632 677 responses: { 633 - 'keypress(Space)': 'end', 678 + 'keypress(Space)': 'end' 634 679 }, 635 680 messageHandlers: {}, 636 681 title: 'End', 637 682 content: 638 - '\u003Cheader class="content-vertical-center content-horizontal-center"\u003E\n \n\u003C\u002Fheader\u003E\n\n\u003Cmain\u003E\n \u003Ch1\u003E\n Thank you!\n \u003C\u002Fh1\u003E\n \u003Ch1\u003E\n Press the space bar to finish the task.\n \u003C\u002Fh1\u003E\n\u003C\u002Fmain\u003E\n\n', 639 - }, 640 - ], 641 - }, 642 - ], 683 + '\u003Cheader class="content-vertical-center content-horizontal-center"\u003E\n \n\u003C\u002Fheader\u003E\n\n\u003Cmain\u003E\n \u003Ch1\u003E\n Thank you!\n \u003C\u002Fh1\u003E\n \u003Ch1\u003E\n Press the space bar to finish the task.\n \u003C\u002Fh1\u003E\n\u003C\u002Fmain\u003E\n\n' 684 + } 685 + ] 686 + } 687 + ] 643 688 }; 644 689 // export 645 690 export default studyObject;
+50 -53
app/viewer.html
··· 1 1 <!DOCTYPE html> 2 2 <html> 3 - 4 - <head> 5 - <meta charset="utf-8"> 3 + <head> 4 + <meta charset="utf-8" /> 6 5 <title>Viewer</title> 7 6 <style> 8 - body { 9 - position: relative; 10 - color: #484848; 11 - width: 95vw; 12 - height: 95vh; 13 - background-color: rgba(255, 255, 255, 0); 14 - font-family: Helvetica Neue, serif; 15 - } 7 + body { 8 + position: relative; 9 + color: #484848; 10 + width: 95vw; 11 + height: 95vh; 12 + background-color: rgba(255, 255, 255, 0); 13 + font-family: Helvetica Neue, serif; 14 + } 16 15 17 - #graph { 18 - width: 100%; 19 - height: 100%; 20 - padding-left: 20px; 21 - } 16 + #graph { 17 + width: 100%; 18 + height: 100%; 19 + padding-left: 20px; 20 + } 22 21 23 - .axis line, 24 - .axis path, 25 - .axis { 26 - stroke: none; 27 - fill: none; 28 - } 22 + .axis line, 23 + .axis path, 24 + .axis { 25 + stroke: none; 26 + fill: none; 27 + } 29 28 30 - .axis text { 31 - font-size: 14px; 32 - fill: #484848; 33 - stroke: none; 34 - } 29 + .axis text { 30 + font-size: 14px; 31 + fill: #484848; 32 + stroke: none; 33 + } 35 34 36 - .legend { 37 - font-size: 14px; 38 - } 35 + .legend { 36 + font-size: 14px; 37 + } 39 38 40 - .line { 41 - fill: none; 42 - } 39 + .line { 40 + fill: none; 41 + } 43 42 </style> 44 - </head> 43 + </head> 45 44 46 - <body> 45 + <body> 47 46 <svg id="graph" /> 48 - </body> 49 - <script> 47 + </body> 48 + <script> 50 49 { 51 - const scripts = []; 50 + const scripts = []; 52 51 53 - // Insert the app script in the viewer process 54 - if (process.env.NODE_ENV === 'development') { 55 - scripts.push('./viewer.js'); 56 - } 57 - else { 58 - scripts.push('./dist/viewer.entry.js'); 59 - } 52 + // Insert the app script in the viewer process 53 + if (process.env.NODE_ENV === 'development') { 54 + scripts.push('./viewer.js'); 55 + } else { 56 + scripts.push('./dist/viewer.entry.js'); 57 + } 60 58 61 - document.write( 62 - scripts 63 - .map(script => '<script defer src="' + script + '"><\/script>') 64 - .join('') 65 - ); 59 + document.write( 60 + scripts 61 + .map(script => '<script defer src="' + script + '"><\/script>') 62 + .join('') 63 + ); 66 64 } 67 - </script> 68 - 69 - </html> 65 + </script> 66 + </html>
+1 -1
appveyor.yml
··· 6 6 - nodejs_version: 9 7 7 8 8 cache: 9 - - "%LOCALAPPDATA%/Yarn" 9 + - '%LOCALAPPDATA%/Yarn' 10 10 - node_modules -> package.json 11 11 - app/node_modules -> app/package.json 12 12
+13 -13
environment.yml
··· 1 1 name: brainwaves 2 2 channels: 3 - - defaults 3 + - defaults 4 4 dependencies: 5 - - python>=3.6 6 - - pip 7 - - numpy 8 - - scipy 9 - - matplotlib 10 - - pyqt>=5.9 11 - - pandas 12 - - seaborn 13 - - jupyter 14 - - pip: 15 - - mne 16 - - pyzmq>=18.0.1 5 + - python>=3.6 6 + - pip 7 + - numpy 8 + - scipy 9 + - matplotlib 10 + - pyqt>=5.9 11 + - pandas 12 + - seaborn 13 + - jupyter 14 + - pip: 15 + - mne 16 + - pyzmq>=18.0.1