An easy-to-use platform for EEG experimentation in the classroom
at main 554 lines 17 kB view raw
1/* eslint-disable */ 2import * as ss from 'simple-statistics'; 3import path from 'pathe'; 4 5export const aggregateBehaviorDataToSave = (data, removeOutliers) => { 6 const processedData = data.map((result) => { 7 if (path.basename(result.meta.datafile).includes('aggregated')) { 8 return transformAggregated(result); 9 } 10 return filterData(result, removeOutliers); 11 }); 12 const aggregatedData = processedData.map((e) => { 13 const conditionsArray = e.map((row) => row.condition); 14 const unsortedConditions = [...new Set(conditionsArray)].sort(); 15 const conditions = unsortedConditions.sort( 16 (a, b) => parseInt(a) - parseInt(b) 17 ); 18 let rtMean = {}, 19 accuracyPercent = {}; 20 for (const condition of conditions) { 21 const rt = e 22 .filter((row) => row.response_given === 'yes') 23 .filter((row) => row.correct_response === 'true') 24 .filter((row) => row.condition === condition) 25 .map((row) => row.reaction_time) 26 .map((value) => parseFloat(value)); 27 rtMean[condition] = Math.round(ss.mean(rt)); 28 const accuracy = e.filter( 29 (row) => 30 row.condition === condition && 31 row.correct_response === 'true' && 32 row.response_given === 'yes' 33 ); 34 accuracyPercent[condition] = accuracy.length 35 ? Math.round( 36 100 * 37 (accuracy.length / 38 e.filter((r) => r.condition === condition).length) 39 ) 40 : ss.mean( 41 e.filter((r) => r.condition === condition).map((r) => r.accuracy) 42 ); 43 } 44 const row = { 45 subject: e.map((r) => r.subject)[0], 46 group: e.map((r) => r.group)[0], 47 session: e.map((r) => r.session)[0], 48 }; 49 for (const condition of conditions) { 50 row[`RT_${condition}`] = rtMean[condition]; 51 row[`Accuracy_${condition}`] = accuracyPercent[condition]; 52 } 53 return row; 54 }); 55 return aggregatedData; 56}; 57 58export const aggregateDataForPlot = ( 59 data, 60 dependentVariable, 61 removeOutliers, 62 showDataPoints, 63 displayMode 64) => { 65 if (!data || data.length < 1) { 66 return; 67 } 68 const processedData = data.map((result) => { 69 if (path.basename(result.meta.datafile).includes('aggregated')) { 70 return transformAggregated(result); 71 } 72 return filterData(result, removeOutliers); 73 }); 74 const colors = ['#28619E', '#3DBBDB']; 75 const conditions = [ 76 ...new Set(processedData[0].map((row) => row.condition)), 77 ].sort((a, b) => parseInt(a) - parseInt(b)); 78 79 switch (dependentVariable) { 80 case 'RT': 81 default: 82 return computeRT( 83 processedData, 84 dependentVariable, 85 conditions, 86 showDataPoints, 87 colors, 88 displayMode 89 ); 90 case 'Accuracy': 91 return computeAccuracy( 92 processedData, 93 dependentVariable, 94 conditions, 95 showDataPoints, 96 colors, 97 displayMode 98 ); 99 } 100}; 101 102const transformAggregated = (result) => { 103 const unsortedConditions = result.meta.fields 104 .filter((field) => field.startsWith('RT_')) 105 .map((c) => c.split('RT_')[1]) 106 .sort(); 107 const conditions = unsortedConditions.sort( 108 (a, b) => parseInt(a) - parseInt(b) 109 ); 110 const transformed = conditions.map((condition) => 111 result.data.map((e) => ({ 112 reaction_time: parseFloat(e[`RT_${condition}`]), 113 subject: path.parse(result.meta.datafile).name, 114 condition, 115 group: e.group, 116 session: e.session, 117 accuracy: parseFloat(e[`Accuracy_${condition}`]), 118 response_given: 'yes', 119 correct_response: 'true', 120 })) 121 ); 122 const data = transformed.reduce((acc, item) => acc.concat(item), []); 123 return data; 124}; 125 126const filterData = (data, removeOutliers) => { 127 let filteredData = data.data 128 .filter((row) => row.trial_number && row.phase !== 'practice') 129 .map((row) => ({ 130 condition: row.condition, 131 subject: path.parse(data.meta.datafile).name.split('-')[0], 132 group: path.parse(data.meta.datafile).name.split('-')[1], 133 session: path.parse(data.meta.datafile).name.split('-')[2], 134 reaction_time: Math.round(parseFloat(row.reaction_time)), 135 correct_response: row.correct_response, 136 trial_number: row.trial_number, 137 response_given: row.response_given, 138 })); 139 if (removeOutliers) { 140 try { 141 const mean = ss.mean( 142 filteredData 143 .filter( 144 (r) => r.response_given === 'yes' && r.correct_response === 'true' 145 ) 146 .map((r) => r.reaction_time) 147 ); 148 const standardDeviation = ss.sampleStandardDeviation( 149 filteredData 150 .filter( 151 (r) => r.response_given === 'yes' && r.correct_response === 'true' 152 ) 153 .map((r) => r.reaction_time) 154 ); 155 const upperBoarder = mean + 2 * standardDeviation; 156 const lowerBoarder = mean - 2 * standardDeviation; 157 filteredData = filteredData.filter( 158 (r) => 159 (r.reaction_time > lowerBoarder && r.reaction_time < upperBoarder) || 160 isNaN(r.reaction_time) 161 ); 162 } catch (err) { 163 alert( 164 'Calculation of the mean and the standard deviation requires at least two completed trials in each condition.' 165 ); 166 return filteredData; 167 } 168 } 169 return filteredData; 170}; 171 172const computeRT = ( 173 data, 174 dependentVariable, 175 conditions, 176 showDataPoints, 177 colors, 178 displayMode 179) => { 180 let dataToPlot = 0; 181 let maxValue = 0; 182 switch (displayMode) { 183 case 'datapoints': 184 default: 185 let tickValuesX, tickTextX; 186 dataToPlot = conditions.reduce((obj, condition, i) => { 187 const xRaw = data 188 .reduce((a, b) => a.concat(b), []) 189 .filter( 190 (r) => r.response_given === 'yes' && r.correct_response === 'true' 191 ) 192 .filter((e) => e.condition === condition) 193 .map((r) => r.subject); 194 const y = data 195 .reduce((a, b) => a.concat(b), []) 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); 201 maxValue = Math.max(...y) > maxValue ? Math.max(...y) : maxValue; 202 const subjects = Array.from(new Set(xRaw)); 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); 207 tickTextX = subjects; 208 obj[condition] = { x, y }; 209 return obj; 210 }, {}); 211 dataToPlot['tickvals'] = tickValuesX; 212 dataToPlot['ticktext'] = tickTextX; 213 dataToPlot['lowerLimit'] = 0; 214 dataToPlot['upperLimit'] = maxValue > 1000 ? maxValue + 100 : 1000; 215 return makeDataPointsGraph( 216 dataToPlot, 217 conditions, 218 colors, 219 dependentVariable 220 ); 221 222 case 'errorbars': 223 let maxValueSE = 0; 224 dataToPlot = conditions.reduce((obj, condition) => { 225 const xRaw = data 226 .reduce((a, b) => a.concat(b), []) 227 .filter( 228 (r) => r.response_given === 'yes' && r.correct_response === 'true' 229 ) 230 .filter((e) => e.condition === condition) 231 .map((r) => r.subject); 232 const x = Array.from(new Set(xRaw)); 233 const data_condition = data.map((d) => 234 d 235 .filter( 236 (r) => r.response_given === 'yes' && r.correct_response === 'true' 237 ) 238 .filter((e) => e.condition == condition) 239 ); 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) 244 ); 245 const y = y_bars_prep 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)); 252 maxValue = Math.max(...y) > maxValue ? Math.max(...y) : maxValue; 253 const stErrorFunction = (array) => 254 ss.sampleStandardDeviation(array) / Math.sqrt(array.length); 255 const stErrors = data_condition 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; 264 obj[condition] = { x, y, stErrors }; 265 return obj; 266 }, {}); 267 dataToPlot['lowerLimit'] = 0; 268 dataToPlot['upperLimit'] = 269 maxValue + maxValueSE > 1000 ? maxValue + maxValueSE + 100 : 1000; 270 return makeBarGraph(dataToPlot, conditions, colors, dependentVariable); 271 272 case 'whiskers': 273 dataToPlot = conditions.reduce((obj, condition, i) => { 274 const x = data 275 .reduce((a, b) => a.concat(b), []) 276 .filter( 277 (r) => r.response_given === 'yes' && r.correct_response === 'true' 278 ) 279 .filter((e) => e.condition === condition) 280 .map((r) => r.subject); 281 const y = data 282 .reduce((a, b) => a.concat(b), []) 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); 288 maxValue = Math.max(...y) > maxValue ? Math.max(...y) : maxValue; 289 obj[condition] = { x, y }; 290 return obj; 291 }, {}); 292 dataToPlot['lowerLimit'] = 0; 293 dataToPlot['upperLimit'] = maxValue > 1000 ? maxValue + 100 : 1000; 294 return makeBoxPlot(dataToPlot, conditions, colors, dependentVariable); 295 } 296}; 297 298const computeAccuracy = ( 299 data, 300 dependentVariable, 301 conditions, 302 showDataPoints, 303 colors, 304 displayMode 305) => { 306 let dataToPlot; 307 308 switch (displayMode) { 309 case 'datapoints': 310 default: 311 let tickValuesX, tickTextX; 312 dataToPlot = conditions.reduce((obj, condition, i) => { 313 const correctDataForCondition = data.map((d) => 314 d.filter((e) => e.condition == condition) 315 ); 316 317 const y = correctDataForCondition 318 .map((d) => { 319 if (d.filter((l) => l.accuracy).length > 0) { 320 return d.map((l) => l.accuracy); 321 } 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); 326 }) 327 // TODO: Remove these useless reduce steps, but confirm it doesn't break anything 328 .reduce((acc, item) => acc.concat(item), []); // ? 329 330 const xRaw = correctDataForCondition 331 .map((d) => { 332 if (d.filter((l) => l.accuracy).length > 0) { 333 return d.map((l) => l.subject); 334 } 335 return d.map((r) => r.subject)[0]; 336 }) 337 .reduce((acc, item) => acc.concat(item), []); // ? 338 const subjects = Array.from(new Set(xRaw)); 339 const x = xRaw.map( 340 (x) => subjects.indexOf(x) + 1 + i / 4 + (Math.random() - 0.5) / 5 341 ); 342 tickValuesX = subjects.map((x) => subjects.indexOf(x) + 1 + 1 / 8); 343 tickTextX = subjects; 344 obj[condition] = { x, y }; 345 return obj; 346 }, {}); 347 dataToPlot['tickvals'] = tickValuesX; 348 dataToPlot['ticktext'] = tickTextX; 349 dataToPlot['lowerLimit'] = 0; 350 dataToPlot['upperLimit'] = 105; 351 return makeDataPointsGraph( 352 dataToPlot, 353 conditions, 354 colors, 355 dependentVariable 356 ); 357 358 case 'errorbars': 359 dataToPlot = conditions.reduce((obj, condition, i) => { 360 const correctDataForCondition = data.map((d) => 361 d.filter((e) => e.condition == condition) 362 ); 363 const transformedData = correctDataForCondition 364 .map((d) => { 365 if (d.filter((l) => l.accuracy).length > 0) { 366 return d.map((l) => ({ 367 accuracy: l.accuracy, 368 subject: l.subject, 369 })); 370 } 371 const c = d.filter( 372 (e) => e.response_given === 'yes' && e.correct_response === 'true' 373 ); 374 return { 375 accuracy: Math.round((c.length / d.length) * 100), 376 subject: d.map((r) => r.subject)[0], 377 }; 378 }) 379 .reduce((acc, item) => acc.concat(item), []); 380 const subjects = Array.from( 381 new Set(transformedData.map((e) => e.subject)) 382 ); 383 const y = subjects.map((subject) => 384 ss.mean( 385 transformedData 386 .filter((e) => e.subject === subject) 387 .map((d) => d.accuracy) 388 ) 389 ); 390 const stErrorFunction = (array) => 391 ss.sampleStandardDeviation(array) / Math.sqrt(array.length); 392 const stErrors = subjects.map((subject) => { 393 const array = transformedData 394 .filter((e) => e.subject === subject) 395 .map((d) => d.accuracy); 396 if (array.length > 1) { 397 return stErrorFunction(array); 398 } 399 return 0; 400 }); 401 obj[condition] = { x: subjects, y, stErrors }; 402 return obj; 403 }, {}); 404 dataToPlot['lowerLimit'] = 0; 405 dataToPlot['upperLimit'] = 105; 406 return makeBarGraph(dataToPlot, conditions, colors, dependentVariable); 407 408 case 'whiskers': 409 dataToPlot = conditions.reduce((obj, condition, i) => { 410 const correctDataForCondition = data.map((d) => 411 d.filter((e) => e.condition == condition) 412 ); 413 const y = correctDataForCondition 414 .map((d) => { 415 if (d.filter((l) => l.accuracy).length > 0) { 416 return d.map((l) => l.accuracy); 417 } 418 const c = d.filter( 419 (e) => e.response_given === 'yes' && e.correct_response === 'true' 420 ); 421 return Math.round((c.length / d.length) * 100); 422 }) 423 .reduce((acc, item) => acc.concat(item), []); 424 const xRaw = correctDataForCondition 425 .map((d) => { 426 if (d.filter((l) => l.accuracy).length > 0) { 427 return d.map((l) => l.subject); 428 } 429 return d.map((r) => r.subject)[0]; 430 }) 431 .reduce((acc, item) => acc.concat(item), []); 432 obj[condition] = { x: xRaw, y }; 433 return obj; 434 }, {}); 435 dataToPlot['lowerLimit'] = 0; 436 dataToPlot['upperLimit'] = 105; 437 return makeBoxPlot(dataToPlot, conditions, colors, dependentVariable); 438 } 439}; 440 441// Rendering functions 442const makeDataPointsGraph = (data, conditions, colors, dependentVariable) => { 443 let dataForCondition; 444 const symbols = ['circle', 'cross', 'diamond', 'square']; 445 const dataToPlot = conditions.map((condition, i) => { 446 dataForCondition = data[condition]; 447 return { 448 x: dataForCondition.x, 449 y: dataForCondition.y, 450 name: condition, 451 type: 'scatter', 452 marker: { 453 color: colors[i], 454 size: 7, 455 symbol: symbols[i], 456 }, 457 mode: 'markers', 458 }; 459 }); 460 const layout = { 461 xaxis: { 462 tickvals: data.tickvals, 463 ticktext: data.ticktext, 464 }, 465 yaxis: { 466 title: `${ 467 dependentVariable == 'Response Time' 468 ? 'Response Time (milliseconds)' 469 : '% correct' 470 }`, 471 range: [data.lowerLimit, data.upperLimit], 472 }, 473 title: `${dependentVariable}`, 474 }; 475 return { 476 dataToPlot, 477 layout, 478 }; 479}; 480 481const makeBarGraph = (data, conditions, colors, dependentVariable) => { 482 const dataToPlot = conditions.map((condition, i) => { 483 const dataForCondition = data[condition]; 484 return { 485 x: dataForCondition.x, 486 y: dataForCondition.y, 487 name: condition, 488 type: 'bar', 489 marker: { 490 color: colors[i], 491 size: 7, 492 }, 493 error_y: { 494 type: 'data', 495 array: dataForCondition.stErrors, 496 visible: true, 497 }, 498 }; 499 }); 500 const layout = { 501 yaxis: { 502 title: `${ 503 dependentVariable == 'Response Time' 504 ? 'Response Time (milliseconds)' 505 : '% correct' 506 }`, 507 zeroline: false, 508 range: [data.lowerLimit, data.upperLimit], 509 }, 510 barmode: 'group', 511 title: `${dependentVariable}`, 512 }; 513 return { 514 dataToPlot, 515 layout, 516 }; 517}; 518 519const makeBoxPlot = (data, conditions, colors, dependentVariable) => { 520 const symbols = ['circle', 'cross', 'diamond', 'square']; 521 const dataToPlot = conditions.map((condition, i) => { 522 const dataForCondition = data[condition]; 523 return { 524 x: dataForCondition.x, 525 y: dataForCondition.y, 526 name: condition, 527 type: 'box', 528 marker: { 529 color: colors[i], 530 size: 7, 531 symbol: symbols[i], 532 }, 533 boxpoints: 'false', 534 pointpos: 0, 535 }; 536 }); 537 const layout = { 538 yaxis: { 539 title: `${ 540 dependentVariable == 'Response Time' 541 ? 'Response Time (milliseconds)' 542 : '% correct' 543 }`, 544 zeroline: false, 545 range: [data.lowerLimit, data.upperLimit], 546 }, 547 boxmode: 'group', 548 title: `${dependentVariable}`, 549 }; 550 return { 551 dataToPlot, 552 layout, 553 }; 554};