import React, { Component } from 'react'; import { Button } from './ui/button'; import { isNil } from 'lodash'; import Plot from 'react-plotly.js'; import type { Data as PlotlyData } from 'plotly.js'; import { DEVICES, MUSE_CHANNELS, EMOTIV_CHANNELS, EXPERIMENTS, } from '../constants/constants'; import { readWorkspaceCleanedEEGData, getSubjectNamesFromFiles, readWorkspaceBehaviorData, readBehaviorData, storeAggregatedBehaviorData, } from '../utils/filesystem/storage'; import { aggregateDataForPlot, aggregateBehaviorDataToSave, } from '../utils/behavior/compute'; import SecondaryNavComponent from './SecondaryNavComponent'; import ClickableHeadDiagramSVG from './svgs/ClickableHeadDiagramSVG'; import PyodidePlotWidget from './PyodidePlotWidget'; import { HelpButton } from './CollectComponent/HelpSidebar'; import { PyodideActions } from '../actions/pyodideActions'; import { cn } from './ui/utils'; const ANALYZE_STEPS = { OVERVIEW: 'OVERVIEW', ERP: 'ERP', BEHAVIOR: 'BEHAVIOR', }; const ANALYZE_STEPS_BEHAVIOR = { BEHAVIOR: 'BEHAVIOR', }; interface Props { title: string; type: EXPERIMENTS; deviceType: DEVICES; isEEGEnabled: boolean; epochsInfo: Array<{ [key: string]: number | string; }>; channelInfo: Array; psdPlot: { [key: string]: string }; topoPlot: { [key: string]: string }; erpPlot: { [key: string]: string }; PyodideActions: typeof PyodideActions; } interface State { activeStep: string; selectedChannel: string; eegFilePaths: Array<{ key: string; text: string; value: { name: string; dir: string }; }>; behaviorFilePaths: Array<{ key: string; text: string; value: string }>; selectedFilePaths: Array; selectedBehaviorFilePaths: Array; selectedSubjects: Array; selectedDependentVariable: string; removeOutliers: boolean; showDataPoints: boolean; isSidebarVisible: boolean; displayMode: string; dataToPlot: PlotlyData[]; layout: Record; helpMode: string; dependentVariables: Array<{ key: string; text: string; value: string }>; } export default class Analyze extends Component { constructor(props: Props) { super(props); this.state = { activeStep: this.props.isEEGEnabled === true ? ANALYZE_STEPS.OVERVIEW : ANALYZE_STEPS.BEHAVIOR, eegFilePaths: [{ key: '', text: '', value: { name: '', dir: '' } }], behaviorFilePaths: [{ key: '', text: '', value: '' }], dependentVariables: [{ key: '', text: '', value: '' }], dataToPlot: [] as PlotlyData[], layout: {}, selectedDependentVariable: '', removeOutliers: true, showDataPoints: false, isSidebarVisible: false, displayMode: 'errorbars', helpMode: 'errorbars', selectedFilePaths: [], selectedBehaviorFilePaths: [], selectedSubjects: [], selectedChannel: props.deviceType === DEVICES.EMOTIV ? EMOTIV_CHANNELS[0] : MUSE_CHANNELS[0], }; this.handleChannelSelect = this.handleChannelSelect.bind(this); this.handleDatasetChange = this.handleDatasetChange.bind(this); this.handleBehaviorDatasetChange = this.handleBehaviorDatasetChange.bind(this); this.handleDependentVariableChange = this.handleDependentVariableChange.bind(this); this.handleRemoveOutliers = this.handleRemoveOutliers.bind(this); this.handleDisplayModeChange = this.handleDisplayModeChange.bind(this); this.handleDataPoints = this.handleDataPoints.bind(this); this.saveSelectedDatasets = this.saveSelectedDatasets.bind(this); this.handleStepClick = this.handleStepClick.bind(this); this.handleDropdownClick = this.handleDropdownClick.bind(this); this.toggleDisplayInfoVisibility = this.toggleDisplayInfoVisibility.bind(this); } async componentDidMount() { const workspaceCleanData = await readWorkspaceCleanedEEGData( this.props.title ); const behavioralData = await readWorkspaceBehaviorData(this.props.title); this.setState({ eegFilePaths: workspaceCleanData.map((filepath) => ({ key: filepath.name, text: filepath.name, value: filepath.path, })), behaviorFilePaths: behavioralData.map((filepath) => ({ key: filepath.name, text: filepath.name, value: filepath.path, })), dependentVariables: ['Response Time', 'Accuracy'].map((dv) => ({ key: dv, text: dv, value: dv, })), selectedDependentVariable: 'Response Time', }); } concatSubjectNames = (subjects: Array) => { if (subjects.length < 1) return ''; return subjects.reduce((acc, curr) => `${acc}-${curr}`); }; handleDatasetChange(e: React.ChangeEvent) { const values = Array.from(e.target.selectedOptions, (o) => o.value); this.setState({ selectedFilePaths: values, selectedSubjects: getSubjectNamesFromFiles(values), }); this.props.PyodideActions.LoadCleanedEpochs(values); } handleBehaviorDatasetChange(e: React.ChangeEvent) { const values = Array.from(e.target.selectedOptions, (o) => o.value); const aggregatedData = aggregateDataForPlot( readBehaviorData(values), this.state.selectedDependentVariable, this.state.removeOutliers, this.state.showDataPoints, this.state.displayMode ); if (!aggregatedData) return; const { dataToPlot, layout } = aggregatedData; this.setState({ selectedBehaviorFilePaths: values, selectedSubjects: getSubjectNamesFromFiles(values), dataToPlot, layout, }); } async handleDropdownClick() { const behavioralData = await readWorkspaceBehaviorData(this.props.title); if (behavioralData.length !== this.state.behaviorFilePaths.length) { this.setState({ behaviorFilePaths: behavioralData.map((filepath) => ({ key: filepath.name, text: filepath.name, value: filepath.path, })), }); } } handleDependentVariableChange(e: React.ChangeEvent) { const { value } = e.target; const aggregatedData = aggregateDataForPlot( readBehaviorData(this.state.selectedBehaviorFilePaths), value, this.state.removeOutliers, this.state.showDataPoints, this.state.displayMode ); if (!aggregatedData) return; const { dataToPlot, layout } = aggregatedData; this.setState({ selectedDependentVariable: value, dataToPlot, layout }); } handleRemoveOutliers() { const aggregatedData = aggregateDataForPlot( readBehaviorData(this.state.selectedBehaviorFilePaths), this.state.selectedDependentVariable, !this.state.removeOutliers, this.state.showDataPoints, this.state.displayMode ); if (!aggregatedData) return; const { dataToPlot, layout } = aggregatedData; this.setState({ removeOutliers: !this.state.removeOutliers, dataToPlot, layout, helpMode: 'outliers', }); } handleDataPoints() { const aggregatedData = aggregateDataForPlot( readBehaviorData(this.state.selectedBehaviorFilePaths), this.state.selectedDependentVariable, this.state.removeOutliers, !this.state.showDataPoints, this.state.displayMode ); if (!aggregatedData) return; const { dataToPlot, layout } = aggregatedData; this.setState({ showDataPoints: !this.state.showDataPoints, dataToPlot, layout, }); } handleDisplayModeChange(displayMode) { if ( this.state.selectedBehaviorFilePaths && this.state.selectedBehaviorFilePaths.length > 0 ) { const aggregatedData = aggregateDataForPlot( readBehaviorData(this.state.selectedBehaviorFilePaths), this.state.selectedDependentVariable, this.state.removeOutliers, this.state.showDataPoints, displayMode ); if (!aggregatedData) return; const { dataToPlot, layout } = aggregatedData; this.setState({ dataToPlot, layout, displayMode, helpMode: displayMode }); } } toggleDisplayInfoVisibility() { this.setState({ isSidebarVisible: !this.state.isSidebarVisible }); } saveSelectedDatasets() { const data = readBehaviorData(this.state.selectedBehaviorFilePaths); const aggregatedData = aggregateBehaviorDataToSave( data, this.state.removeOutliers ); storeAggregatedBehaviorData(aggregatedData, this.props.title); } handleChannelSelect(channelName: string) { this.setState({ selectedChannel: channelName }); this.props.PyodideActions.LoadERP(channelName); } handleStepClick(step: string) { this.setState({ activeStep: step }); } renderEpochLabels() { if ( !isNil(this.props.epochsInfo) && this.state.selectedFilePaths.length >= 1 ) { const numberConditions = this.props.epochsInfo.filter( (infoObj) => infoObj.name !== 'Drop Percentage' && infoObj.name !== 'Total Epochs' ).length; const colors = numberConditions === 4 ? ['red', 'yellow', 'green', 'blue'] : ['red', 'green', 'teal', 'orange']; return (
{this.props.epochsInfo .filter( (infoObj) => infoObj.name !== 'Drop Percentage' && infoObj.name !== 'Total Epochs' ) .map((infoObj, index) => (

{infoObj.name}

● {infoObj.value}
))}
); } return
; } renderHelpContent() { switch (this.state.helpMode) { case 'datapoints': return this.renderHelp( 'Data Points', `In this graph, each dot refers to one data point, clustered by group (e.g., conditions). It's the most "neutral" way of presenting the data, of course, but it may be difficult to see any patterns. Why is it always a good idea to look at all your datapoints before interpreting any trends in the data?` ); case 'errorbars': return this.renderHelp( 'Bar Graph', `Bar graphs are the most common way to summarize data. It allows you to compare mean values between groups of datapoints (e.g., conditions), and the error bars give some indication of the variance (here: the standard error of the mean). Importantly, this way of summarizing data assumes that the mean is in fact representative of the data. Many researchers have veered away from bar graphs because they can be deceptive, especially without error bars. Can you think of any such cases?` ); case 'whiskers': return this.renderHelp( 'Box Plot', `Box plots summarize the data in a more informative way: they actually tell you something about the distribution of datapoints within a group, by taking the median as its reference point instead of the mean. The boxes represent so-called "quartiles". The lines ("whiskers") show how much variability there is in the data outside of those quartiles; any outliers are shown as individual points.` ); case 'outliers': default: return this.renderHelp( 'Outliers', `A datapoint is tagged as an "outlier" if its value exceeds 2 standard deviations below or above the mean of all data in the group. Removing such outliers can help unskew the data.` ); } } renderHelp(header: string, content: string) { return (

{header}

{content}
); } renderSectionContent() { switch (this.state.activeStep) { case ANALYZE_STEPS.OVERVIEW: default: return ( <>

Overview

Load cleaned datasets from different subjects and view how the EEG differs between electrodes

Select Clean Datasets

{this.renderEpochLabels()}
); case ANALYZE_STEPS.ERP: return ( <>

ERP

The event-related potential represents EEG activity elicited by a particular sensory event

{this.renderEpochLabels()}
); case ANALYZE_STEPS.BEHAVIOR: return ( <>

Overview

Load datasets from different subjects and view behavioral results

Datasets

Dependent Variable

{(['datapoints', 'errorbars', 'whiskers'] as const).map( (mode) => ( ) )}
{this.state.isSidebarVisible && (
{this.renderHelpContent()}
)}
); } } render() { return (
{this.renderSectionContent()}
); } }