An easy-to-use platform for EEG experimentation in the classroom
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};