diff --git a/README.md b/README.md index 342800f8..9172b2a1 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,58 @@ Interactive Brain Playground Logo

+# REMIX2 - server and client architecture + +To start, you will need to install [Homebrew](https://brew.sh) and [npm](https://nodejs.org/en/). These are easy to install with the following Terminal / `bash` commands: + +```sh +## Install homebrew +/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" + +## Install npm +# NOTE: this will also install Node.js if it is not already installed. +brew install node + +# NOTE: Node.js must be version 10.x for Muse interaction + +# Thus, if you are getting version issues, install n, with the following command: +sudo npm install -g n + +# Then, you can switch to version 10.x with the following command: +sudo n 10.16.0 +``` + +Then, in Terminal/`bash`, clone this Git repository and change directory into the newly cloned folder: + +```sh +git clone https://github.com/kylemath/EEGEdu +cd EEGEdu +``` + +Then, you can install the required `npm` packages for EEGEdu: + +```sh +npm install +``` + +## Local Development Environment +Then, you can run the *Local Development Environment* of EEGEdu: + +```sh +npm start +``` + +Then go to https://localhost:3000 in the browser to start a client instance. In another browser window go to https://localhost:3000/together to see the server side, where all the clients data is streamed into. + + + + + + + + +# OLD STUFF + `EEGEdu` is an Interactive Brain Playground. `EEGEdu` is served live at [https://eegedu.com/](https://eegedu.com). This website is served from the software in this repository. So, all you need to do to try the system out is head to [EEGEdu](https://eegedu.com/). diff --git a/package.json b/package.json index 1531b414..1b536e82 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "npm": "5.x", "node": "10.x" }, + "proxy": "http://localhost:8080", "name": "eegedu", "version": "0.1.0", "private": true, @@ -10,6 +11,7 @@ "@neurosity/pipes": "^3.0.2", "@shopify/polaris": "^4.9.0", "chart.js": "^2.7.2", + "express": "^4.17.1", "file-saver": "^2.0.2", "firebase": "^7.5.0", "firebase-tools": "^7.9.0", @@ -25,10 +27,12 @@ "react-p5-wrapper": "^2.0.0", "react-scripts": "^3.2.0", "react-youtube": "^7.9.0", - "rxjs": "^6.5.3" + "rxjs": "^6.5.3", + "socket.io": "^2.3.0" }, "scripts": { - "start": "react-scripts --max_old_space_size=4096 start", + "dev": "react-scripts --max_old_space_size=4096 start", + "start": "node server.js", "build": "react-scripts --max_old_space_size=4096 build", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" diff --git a/public/index.html b/public/index.html index be6c1d8d..2a419cc9 100644 --- a/public/index.html +++ b/public/index.html @@ -43,5 +43,11 @@ } }); + + + diff --git a/public/together.html b/public/together.html new file mode 100644 index 00000000..87d35da7 --- /dev/null +++ b/public/together.html @@ -0,0 +1,67 @@ + + + + + + + + + EEGEdu + + + + + + +
+

+ Start streaming data from subscribers +

+

Plots will appear for each user connected:

+ + + + + + + + + + + +
+ + + + + diff --git a/server.js b/server.js new file mode 100644 index 00000000..df60b281 --- /dev/null +++ b/server.js @@ -0,0 +1,43 @@ +const express = require("express"); +const bodyParser = require("body-parser"); +const path = require("path"); + +const app = express(); +var http = require('http').createServer(app); +var io = require('socket.io')(http); + +app.use(express.static(path.join(__dirname, "build"))); + +app.get("/", function(req, res) { + res.sendFile(path.join(__dirname, "build", "index.html")); +}); + +app.get("/together", function(req, res) { + res.sendFile(path.join(__dirname, "build", "together.html")); +}); + +// app.listen(process.env.PORT || 3000); + +io.on('connection', function(socket){ + console.log("New client connected"); + + //Here we listen on a new namespace called "incoming data" + socket.on("incoming data", (data)=>{ + // Here we broadcast it out to all other sockets EXCLUDING the + // socket which sent us the data + // The broadcast flag is special because it allows us to emit data to every + // client EXCEPT the one that sent us the data. There's no point in sending + // data back to the producer so we broadcast on yet another namespace, + // outgoing data. + socket.broadcast.emit("outgoing data", {num: data}); + console.log('broadcast outgoing data', data.userName, data.info.startTime); + + }); + + //A special namespace "disconnect" for when a client disconnects + socket.on("disconnect", () => console.log("Client disconnected")); +}); + +http.listen(3000, function(){ + console.log('listening on *:3000'); +}); \ No newline at end of file diff --git a/src/components/App/App.js b/src/components/App/App.js index 2cf2204f..8912586d 100644 --- a/src/components/App/App.js +++ b/src/components/App/App.js @@ -2,15 +2,18 @@ import React from "react"; import { PageSwitcher } from "../PageSwitcher/PageSwitcher"; import { AppProvider, Card, Page, Link } from "@shopify/polaris"; import enTranslations from "@shopify/polaris/locales/en.json"; -import * as translations from "./translations/en.json"; export function App() { return ( - + -

{translations.footer} +

{"EEGEdu - An Interactive Electrophysiology Tutorial with the Muse brought to you by Mathewson Sons."} A Ky Kor diff --git a/src/components/App/translations/en.json b/src/components/App/translations/en.json deleted file mode 100644 index cbeee3ce..00000000 --- a/src/components/App/translations/en.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "title": "EEGEdu", - "subtitle": [ - "Welcome to the EEGEdu live EEG tutorial. ", - "This tutorial will help you learn about how neurons produce electrical activity we can measure. ", - "By sticking electrodes on the head we can pick up these changes in electricity. ", - "The tutorial will walk you through the basics of EEG signal generation, data collection, and analysis with a focus on live control based on physiological signals. ", - "All demos are done in this browser. ", - "This tutorial is designed to be used with the Muse and the Muse 2 headbands from Interaxon. ", - "If you do not have one handy, there is an option to stream mock data as well. ", - "Muse with two auxillary ports made in 2014 will not work. ", - "This tutorial has been tested on Android Pixels (Mobile) and Mac OSX (laptop) with the latest chrome browser. ", - "The first step will be to turn on your Muse headband and click the connect button. ", - "This will open a screen and will list available Muse devices. ", - "Select the serial number written on your Muse. ", - "If you do not have a Muse headband you can click the Mock Data button to use simluated data. ", - "Then scroll down to see you live brain activity!" - ], - "footer": "EEGEdu - An Interactive Electrophysiology Tutorial with the Muse brought to you by Mathewson Sons. " - -} diff --git a/src/components/PageSwitcher/PageSwitcher.js b/src/components/PageSwitcher/PageSwitcher.js index ca338094..75c7fb5c 100644 --- a/src/components/PageSwitcher/PageSwitcher.js +++ b/src/components/PageSwitcher/PageSwitcher.js @@ -1,395 +1,265 @@ import React, { useState, useCallback } from "react"; -import { MuseClient } from "muse-js"; -import { Select, Card, Stack, Button, ButtonGroup, Checkbox } from "@shopify/polaris"; - +import { MuseClient, zipSamples } from "muse-js"; +import { Card, Stack, Button, ButtonGroup, Checkbox , RangeSlider, Modal, TextContainer, TextField} from "@shopify/polaris"; import { mockMuseEEG } from "./utils/mockMuseEEG"; -import * as translations from "./translations/en.json"; -import * as generalTranslations from "./components/translations/en"; -import { emptyAuxChannelData } from "./components/chartOptions"; - -import * as funIntro from "./components/EEGEduIntro/EEGEduIntro" -import * as funHeartRaw from "./components/EEGEduHeartRaw/EEGEduHeartRaw" -import * as funHeartSpectra from "./components/EEGEduHeartSpectra/EEGEduHeartSpectra" -import * as funRaw from "./components/EEGEduRaw/EEGEduRaw"; -import * as funSpectra from "./components/EEGEduSpectra/EEGEduSpectra"; -import * as funBands from "./components/EEGEduBands/EEGEduBands"; -import * as funAnimate from "./components/EEGEduAnimate/EEGEduAnimate"; -import * as funSpectro from "./components/EEGEduSpectro/EEGEduSpectro"; -import * as funAlpha from "./components/EEGEduAlpha/EEGEduAlpha"; -import * as funSsvep from "./components/EEGEduSsvep/EEGEduSsvep"; -import * as funEvoked from "./components/EEGEduEvoked/EEGEduEvoked"; -import * as funPredict from "./components/EEGEduPredict/EEGEduPredict"; - -const intro = translations.types.intro; -const heartRaw = translations.types.heartRaw; -const heartSpectra = translations.types.heartSpectra; -const raw = translations.types.raw; -const spectra = translations.types.spectra; -const bands = translations.types.bands; -const animate = translations.types.animate; -const spectro = translations.types.spectro; -const alpha = translations.types.alpha; -const ssvep = translations.types.ssvep; -const evoked = translations.types.evoked; -const predict = translations.types.predict; +import * as connectionText from "./utils/connectionText"; +import { emptyAuxChannelData } from "./utils/chartOptions"; +import { bandpassFilter, epoch, fft, sliceFFT} from "@neurosity/pipes"; +import { Subject } from "rxjs"; +import { multicast } from "rxjs/operators"; +import { takeUntil } from "rxjs/operators"; +import { timer } from "rxjs"; + +import { RenderModule } from "./components/EEGEduSpectra/EEGEduSpectra"; + +let showAux = true; // if it is even available to press (to prevent in some modules) +let source = {}; + +// ADDED ADDED ADDED +let socket = require('socket.io-client')('http://127.0.0.1:3000'); +// + + +//-----Setup Constants +let thisMulticast = null; +let subscription = null; + +function getSettings() { + return { + cutOffLow: 1, + cutOffHigh: 100, + interval: 100, + bins: 256, + sliceFFTLow: 1, + sliceFFTHigh: 100, + duration: 1024, + srate: 256, + name: 'Spectra', + secondsToSave: 1000, + nchans: 4 + } +}; + + export function PageSwitcher() { + // connection status + const [status, setStatus] = useState(connectionText.connect); + + // data pulled out of multicast$ + const [Data, setData] = useState(emptyAuxChannelData); + + // pipe settings + const [Settings, setSettings] = useState(getSettings); + + // for popup flag when recording + const [recordPop, setRecordPop] = useState(false); + const recordPopChange = useCallback(() => setRecordPop(!recordPop), [recordPop]); + // For auxEnable settings const [checked, setChecked] = useState(false); const handleChange = useCallback((newChecked) => setChecked(newChecked), []); + + + const [userName, setUserName] = useState('RandomUser' + Math.floor(Math.random() * 101)); + const userNameChange = useCallback((newValue) => setUserName(newValue), []); + + + // ---- Manage Auxillary channel + window.enableAux = checked; if (window.enableAux) { window.nchans = 5; } else { window.nchans = 4; } - let showAux = true; // if it is even available to press (to prevent in some modules) - // data pulled out of multicast$ - const [introData, setIntroData] = useState(emptyAuxChannelData) - const [heartRawData, setHeartRawData] = useState(emptyAuxChannelData); - const [heartSpectraData, setHeartSpectraData] = useState(emptyAuxChannelData); - const [rawData, setRawData] = useState(emptyAuxChannelData); - const [spectraData, setSpectraData] = useState(emptyAuxChannelData); - const [bandsData, setBandsData] = useState(emptyAuxChannelData); - const [animateData, setAnimateData] = useState(emptyAuxChannelData); - const [spectroData, setSpectroData] = useState(emptyAuxChannelData); - const [alphaData, setAlphaData] = useState(emptyAuxChannelData); - const [ssvepData, setSsvepData] = useState(emptyAuxChannelData); - const [evokedData, setEvokedData] = useState(emptyAuxChannelData); - const [predictData, setPredictData] = useState(emptyAuxChannelData); - - // pipe settings - const [introSettings] = useState(funIntro.getSettings); - const [heartRawSettings] = useState(funHeartRaw.getSettings); - const [heartSpectraSettings, setHeartSpectraSettings] = useState(funHeartSpectra.getSettings); - const [rawSettings, setRawSettings] = useState(funRaw.getSettings); - const [spectraSettings, setSpectraSettings] = useState(funSpectra.getSettings); - const [bandsSettings, setBandsSettings] = useState(funBands.getSettings); - const [animateSettings, setAnimateSettings] = useState(funAnimate.getSettings); - const [spectroSettings, setSpectroSettings] = useState(funSpectro.getSettings); - const [alphaSettings, setAlphaSettings] = useState(funAlpha.getSettings); - const [ssvepSettings, setSsvepSettings] = useState(funSsvep.getSettings); - const [evokedSettings, setEvokedSettings] = useState(funEvoked.getSettings); - const [predictSettings, setPredictSettings] = useState(funPredict.getSettings); + showAux = true; - // connection status - const [status, setStatus] = useState(generalTranslations.connect); - - // for picking a new module - const [selected, setSelected] = useState(intro); - const handleSelectChange = useCallback(value => { - setSelected(value); - - console.log("Switching to: " + value); - - if (window.subscriptionIntro) window.subscriptionIntro.unsubscribe(); - if (window.subscriptionHeartRaw) window.subscriptionHeartRaw.unsubscribe(); - if (window.subscriptionHeartSpectra) window.subscriptionHeartSpectra.unsubscribe(); - if (window.subscriptionRaw) window.subscriptionRaw.unsubscribe(); - if (window.subscriptionSpectra) window.subscriptionSpectra.unsubscribe(); - if (window.subscriptionBands) window.subscriptionBands.unsubscribe(); - if (window.subscriptionAnimate) window.subscriptionAnimate.unsubscribe(); - if (window.subscriptionSpectro) window.subscriptionSpectro.unsubscribe(); - if (window.subscriptionAlpha) window.subscriptionAlpha.unsubscribe(); - if (window.subscriptionSsvep) window.subscriptionSsvep.unsubscribe(); - if (window.subscriptionEvoked) window.subscriptionEvoked.unsubscribe(); - if (window.subscriptionPredict) window.subscriptionPredict.unsubscribe(); - - subscriptionSetup(value); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + //---- Main functions to build and setup called once connect pressed - // for popup flag when recording - const [recordPop, setRecordPop] = useState(false); - const recordPopChange = useCallback(() => setRecordPop(!recordPop), [recordPop]); + function buildPipes() { + if (subscription) subscription.unsubscribe(); - // for popup flag when recording 2nd condition - const [recordTwoPop, setRecordTwoPop] = useState(false); - const recordTwoPopChange = useCallback(() => setRecordTwoPop(!recordTwoPop), [recordTwoPop]); - - switch (selected) { - case intro: - showAux = false; - break - case heartRaw: - showAux = false; - break - case heartSpectra: - showAux = false; - break - case raw: - showAux = true; - break - case spectra: - showAux = true; - break - case bands: - showAux = true; - break - case animate: - showAux = false; - break - case spectro: - showAux = false; - break - case alpha: - showAux = true; - break - case ssvep: - showAux = true; - break - case evoked: - showAux = true; - break - case predict: - showAux = false; - break - default: - console.log("Error on showAux"); + console.log("Building Multicast for " + Settings.name); + // Build Pipe + thisMulticast = zipSamples(source.eegReadings$).pipe( + bandpassFilter({ + cutoffFrequencies: [Settings.cutOffLow, Settings.cutOffHigh], + nbChannels: Settings.nchans }), + epoch({ + duration: Settings.duration, + interval: Settings.interval, + samplingRate: Settings.srate + }), + fft({ bins: Settings.bins }), + sliceFFT([Settings.sliceFFTLow, Settings.sliceFFTHigh]) + ).pipe(multicast(() => new Subject())); } + function subscriptionSetup() { + console.log("Subscribing to " + Settings.name); - const chartTypes = [ - { label: intro, value: intro }, - { label: heartRaw, value: heartRaw }, - { label: heartSpectra, value: heartSpectra }, - { label: raw, value: raw }, - { label: spectra, value: spectra }, - { label: bands, value: bands }, - { label: animate, value: animate }, - { label: spectro, value: spectro }, - { label: alpha, value: alpha }, - { label: ssvep, value: ssvep }, - { label: evoked, value: evoked }, - { label: predict, value: predict } - - ]; - - function buildPipes(value) { - funIntro.buildPipe(introSettings); - funHeartRaw.buildPipe(heartRawSettings); - funHeartSpectra.buildPipe(heartSpectraSettings); - funRaw.buildPipe(rawSettings); - funSpectra.buildPipe(spectraSettings); - funBands.buildPipe(bandsSettings); - funAnimate.buildPipe(animateSettings); - funSpectro.buildPipe(spectroSettings); - funAlpha.buildPipe(alphaSettings); - funSsvep.buildPipe(ssvepSettings); - funEvoked.buildPipe(evokedSettings); - funPredict.buildPipe(predictSettings); - } + if (thisMulticast) { + thisMulticast.connect(); + subscription = thisMulticast.subscribe(data => { + setData(inData => { + Object.values(inData).forEach((channel, index) => { + channel.datasets[0].data = data.psd[index]; + channel.xLabels = data.freqs; + }); - function subscriptionSetup(value) { - switch (value) { - case intro: - funIntro.setup(setIntroData, introSettings); - break; - case heartRaw: - funHeartRaw.setup(setHeartRawData, heartRawSettings); - break; - case heartSpectra: - funHeartSpectra.setup(setHeartSpectraData, heartSpectraSettings); - break; - case raw: - funRaw.setup(setRawData, rawSettings); - break; - case spectra: - funSpectra.setup(setSpectraData, spectraSettings); - break; - case bands: - funBands.setup(setBandsData, bandsSettings); - break; - case animate: - funAnimate.setup(setAnimateData, animateSettings); - break; - case spectro: - funSpectro.setup(setSpectroData, spectroSettings); - break; - case alpha: - funAlpha.setup(setAlphaData, alphaSettings); - break; - case ssvep: - funSsvep.setup(setSsvepData, ssvepSettings); - break; - case evoked: - funEvoked.setup(setEvokedData, evokedSettings); - break; - case predict: - funPredict.setup(setPredictData, predictSettings); - break; - default: - console.log( - "Error on handle Subscriptions. Couldn't switch to: " + value - ); + return { + ch0: inData.ch0, + ch1: inData.ch1, + ch2: inData.ch2, + ch3: inData.ch3, + ch4: inData.ch4 + }; + }); + }); + console.log("Subscribed to " + Settings.name); } } + // --- Once connect button pressed + async function connect() { try { + + if (window.debugWithMock) { // Debug with Mock EEG Data - setStatus(generalTranslations.connectingMock); - window.source = {}; - window.source.connectionStatus = {}; - window.source.connectionStatus.value = true; - window.source.eegReadings$ = mockMuseEEG(256); - setStatus(generalTranslations.connectedMock); + setStatus(connectionText.connectingMock); + source = {}; + source.connectionStatus = {}; + source.connectionStatus.value = true; + source.eegReadings$ = mockMuseEEG(256); + setStatus(connectionText.connectedMock); + } else { // Connect with the Muse EEG Client - setStatus(generalTranslations.connecting); - window.source = new MuseClient(); - window.source.enableAux = window.enableAux; - await window.source.connect(); - await window.source.start(); - window.source.eegReadings$ = window.source.eegReadings; - setStatus(generalTranslations.connected); + setStatus(connectionText.connecting); + source = new MuseClient(); + source.enableAux = window.enableAux; + await source.connect(); + await source.start(); + source.eegReadings$ = source.eegReadings; + setStatus(connectionText.connected); + } if ( - window.source.connectionStatus.value === true && - window.source.eegReadings$ + source.connectionStatus.value === true && + source.eegReadings$ ) { - buildPipes(selected); - subscriptionSetup(selected); + + //Build and Setup + buildPipes(); + subscriptionSetup(); } + } catch (err) { - setStatus(generalTranslations.connect); + setStatus(connectionText.connect); console.log("Connection error: " + err); } } + // For disconnect button function refreshPage(){ window.location.reload(); } - function pipeSettingsDisplay() { - switch(selected) { - case intro: - return null - case heartRaw: - return null - case heartSpectra: - return null - case raw: - return ( - funRaw.renderSliders(setRawData, setRawSettings, status, rawSettings) - ); - case spectra: - return ( - funSpectra.renderSliders(setSpectraData, setSpectraSettings, status, spectraSettings) - ); - case bands: - return ( - funBands.renderSliders(setBandsData, setBandsSettings, status, bandsSettings) - ); - case animate: - return ( - funAnimate.renderSliders(setAnimateData, setAnimateSettings, status, animateSettings) - ); - case spectro: - return ( - funSpectro.renderSliders(setSpectroData, setSpectroSettings, status, spectroSettings) - ); - case alpha: - return ( - funAlpha.renderSliders(setAlphaData, setAlphaSettings, status, alphaSettings) - ); - case ssvep: - return ( - funSsvep.renderSliders(setSsvepData, setSsvepSettings, status, ssvepSettings) - ); - case evoked: - return null - case predict: - return ( - funPredict.renderSliders(setPredictData, setPredictSettings, status, predictSettings) - ); - default: console.log('Error rendering settings display'); - } - } - + // Show the chart function renderModules() { - switch (selected) { - case intro: - return ; - case heartRaw: - return ; - case heartSpectra: - return ; - case raw: - return ; - case spectra: - return ; - case bands: - return ; - case animate: - return ; - case spectro: - return ; - case alpha: - return ; - case ssvep: - return ; - case evoked: - return ; - case predict: - return ; - default: - console.log("Error on renderCharts switch."); - } + return ; } + // Show the record button function renderRecord() { - switch (selected) { - case intro: - return null - case heartRaw: - return ( - funHeartRaw.renderRecord(recordPopChange, recordPop, status, heartRawSettings) - ) - case heartSpectra: - return ( - funHeartSpectra.renderRecord(recordPopChange, recordPop, status, heartSpectraSettings, setHeartSpectraSettings) - ) - case raw: - return ( - funRaw.renderRecord(recordPopChange, recordPop, status, rawSettings, setRawSettings) - ) - case spectra: - return ( - funSpectra.renderRecord(recordPopChange, recordPop, status, spectraSettings, setSpectraSettings) - ) - case bands: - return ( - funBands.renderRecord(recordPopChange, recordPop, status, bandsSettings, setBandsSettings) - ) - case animate: - return null - case spectro: - return null - case alpha: - return ( - funAlpha.renderRecord(recordPopChange, recordPop, status, alphaSettings, recordTwoPopChange, recordTwoPop, setAlphaSettings) - ) - case ssvep: - return ( - funSsvep.renderRecord(recordPopChange, recordPop, status, ssvepSettings, recordTwoPopChange, recordTwoPop, setSsvepSettings) - ) - case evoked: - return ( - funEvoked.renderRecord(recordPopChange, recordPop, status, evokedSettings, setEvokedSettings) - ) - case predict: - return ( - funPredict.renderRecord(recordPopChange, status) - ) - default: - console.log("Error on renderRecord."); + function handleSecondsToSaveRangeSliderChange(value) { + setSettings(prevState => ({...prevState, secondsToSave: value})); } + + return( + + + + + + + + + + +

+ Your data is currently recording, + once complete it will be downloaded as a .csv file + and can be opened with your favorite spreadsheet program. + Close this window once the download completes. +

+ + + + +
+ ) } + async function saveToCSV(Settings) { + // var socket = await io.connect('75.152.213.182:8080'); + + console.log('Streaming ' + Settings.secondsToSave + ' seconds...'); + var localObservable$ = null; + + // Create timer + const timer$ = timer(Settings.secondsToSave * 1000); + + // put selected observable object into local and start taking samples + localObservable$ = thisMulticast.pipe( + takeUntil(timer$) + ); + + // now with header in place subscribe to each epoch and log it + localObservable$.subscribe({ + next(x) { + // console.log('Next packet socket emit eeg data: ', x) + + // The client webpage will emit the data to the 'incoming data' socket + // namespace. This should be handled by the server. + x['userName'] = userName; + socket.emit('incoming data', x ) + console.log('emit incoming data from user: ', x ) + + }, + error(err) { console.log(err); }, + complete() { + console.log('Done streaming') + } + }); + } + + // Render the entire page using above functions return ( @@ -397,8 +267,8 @@ export function PageSwitcher() { - - - - - - - - ); - } else { - return null - } - }); - } - - return ( - - - - - -

{specificTranslations.description}

-
-
-
- -
{RenderCharts()}
-
-
- ); -} - -export function renderSliders(setData, setSettings, status, Settings) { - - function resetPipeSetup(value) { - buildPipe(Settings); - setup(setData, Settings); - } - - function handleIntervalRangeSliderChange(value) { - setSettings(prevState => ({...prevState, interval: value})); - resetPipeSetup(); - } - - function handleCutoffLowRangeSliderChange(value) { - setSettings(prevState => ({...prevState, cutOffLow: value})); - resetPipeSetup(); - } - - function handleCutoffHighRangeSliderChange(value) { - setSettings(prevState => ({...prevState, cutOffHigh: value})); - resetPipeSetup(); - } - - function handleDurationRangeSliderChange(value) { - setSettings(prevState => ({...prevState, duration: value})); - resetPipeSetup(); - } - - return ( - - - - - - - ) -} - diff --git a/src/components/PageSwitcher/components/EEGEduAnimate/sketchBands.js b/src/components/PageSwitcher/components/EEGEduAnimate/sketchBands.js deleted file mode 100644 index cbbece99..00000000 --- a/src/components/PageSwitcher/components/EEGEduAnimate/sketchBands.js +++ /dev/null @@ -1,80 +0,0 @@ -export default function sketchBands (p) { - let delta = 0; - let theta = 0; - let alpha = 0; - let beta = 0; - let gamma = 0; - - p.setup = function () { - p.createCanvas(p.windowWidth*.5, p.windowWidth*.5, p.WEBGL); - }; - - p.windowResized = function() { - p.resizeCanvas(p.windowWidth*.5, p.windowWidth*.5); - } - - p.myCustomRedrawAccordingToNewPropsHandler = function (props) { - delta = props.delta; - theta = props.theta; - alpha = props.alpha; - beta = props.beta; - gamma = props.gamma; - }; - - p.draw = function () { - - let unit = p.width/5; - - p.background(256); - p.ambientMaterial(250); - p.noStroke(); - - let locX = p.mouseX - p.height / 2; - let locY = p.mouseY - p.width / 2; - - p.ambientLight(60, 60, 60); - p.pointLight(255, 255, 255, locX, locY, 100); - - - p.push(); - p.fill(255,0,0); - p.translate(-unit,0); - p.rotateY(50); - p.rotateX(50); - p.box(unit/2, delta* 10, unit); - p.pop(); - - p.push(); - p.fill(0,255,0); - p.translate(-unit/2,0); - p.rotateY(50); - p.rotateX(50); - p.box(unit/2, theta* 10, unit); - p.pop(); - - p.push(); - p.fill(0,0,255); - p.translate(0,0); - p.rotateY(50); - p.rotateX(50); - p.box(unit/2, alpha* 10, unit); - p.pop(); - - p.push(); - p.fill(0,128, 128); - p.translate(unit/2,0); - p.rotateY(50); - p.rotateX(50); - p.box(unit/2, beta* 10, unit); - p.pop(); - - p.push(); - p.fill(128,0,128); - p.translate(unit,0); - p.rotateY(50); - p.rotateX(50); - p.box(unit/2, gamma* 10, unit); - p.pop(); - }; - -}; \ No newline at end of file diff --git a/src/components/PageSwitcher/components/EEGEduAnimate/sketchCube.js b/src/components/PageSwitcher/components/EEGEduAnimate/sketchCube.js deleted file mode 100644 index 7f6b967c..00000000 --- a/src/components/PageSwitcher/components/EEGEduAnimate/sketchCube.js +++ /dev/null @@ -1,41 +0,0 @@ -export default function sketchCube (p) { - let delta = 0; - // let theta = 0; - // let alpha = 0; - // let beta = 0; - // let gamma = 0; - - let rotation = 0; - - p.setup = function () { - p.createCanvas(p.windowWidth*.6, 800, p.WEBGL); - }; - - p.windowResized = function() { - p.resizeCanvas(p.windowWidth*.6, 800); - } - - p.myCustomRedrawAccordingToNewPropsHandler = function (props) { - delta = Math.floor(props.delta); - // theta = props.theta; - // alpha = props.alpha; - // beta = props.beta; - // gamma = props.gamma; - - if (props.alpha){ - rotation = props.alpha * Math.PI / 180; - } - }; - - p.draw = function () { - p.background(256); - p.normalMaterial(); - p.noStroke(); - p.push(); - p.rotateX(25 + p.frameCount/10); - p.rotateY(rotation); - p.box(delta*10); - p.pop(); - }; - -}; \ No newline at end of file diff --git a/src/components/PageSwitcher/components/EEGEduAnimate/sketchDraw.js b/src/components/PageSwitcher/components/EEGEduAnimate/sketchDraw.js deleted file mode 100644 index c0fdafc2..00000000 --- a/src/components/PageSwitcher/components/EEGEduAnimate/sketchDraw.js +++ /dev/null @@ -1,53 +0,0 @@ -export default function sketchDraw (p) { - let delta = 0; - let theta = 0; - let alpha = 0; - let beta = 0; - let gamma = 0; - - let xVar = 0; - let yVar = 0; - - let brushWidth = 50; - - p.setup = function () { - p.createCanvas(p.windowWidth*.6, 500); - }; - - p.windowResized = function() { - p.createCanvas(p.windowWidth*.6, 500); - } - - p.myCustomRedrawAccordingToNewPropsHandler = function (props) { - delta = Math.floor((props.delta/20) * 255); - theta = Math.floor((props.theta/10) * 255); - alpha = Math.floor((props.alpha/5) * p.width); - beta = Math.floor((props.beta/2) * p.height); - gamma = Math.floor((props.gamma/2) * 255); - - xVar = alpha; - yVar = beta; - - if (xVar > p.width) { - xVar = p.width-brushWidth/2; - } - if (yVar > p.height) { - yVar = p.height-brushWidth/2; - } - - // console.log(xVar) - // console.log(yVar) - }; - - - p.mousePressed = function () { - p.background(256); - } - - p.draw = function () { - p.fill(theta, delta, gamma, 20); - p.noStroke(); - p.ellipse(xVar, yVar, brushWidth); - } - -}; \ No newline at end of file diff --git a/src/components/PageSwitcher/components/EEGEduAnimate/sketchFlock.js b/src/components/PageSwitcher/components/EEGEduAnimate/sketchFlock.js deleted file mode 100644 index ec2376c9..00000000 --- a/src/components/PageSwitcher/components/EEGEduAnimate/sketchFlock.js +++ /dev/null @@ -1,241 +0,0 @@ -import p5 from "p5"; -import "p5/lib/addons/p5.sound"; - -export default function sketchFlock (p) { - - let alpha = 0; - let beta = 0; - - - let xVar = 0; - let yVar = 0; - var flock; - - - p.setup = function () { - p.createCanvas(p.windowWidth*.6, 500); - flock = new p.Flock(); - - for (var i = 0; i < 150; i++) { - var b = new p.Boid(p.width / 2, p.height / 2); - flock.addBoid(b) - } - }; - - p.windowResized = function() { - p.createCanvas(p.windowWidth*.6, 500); - } - p.myCustomRedrawAccordingToNewPropsHandler = function (props) { - - alpha = Math.floor((props.alpha/5) * p.width); - beta = Math.floor((props.beta/2) * p.height); - - - xVar = alpha; - yVar = beta; - - if (xVar > p.width) { - xVar = p.width-5; - } - if (yVar > p.height) { - yVar = p.height-5; - } - - // console.log(xVar) - // console.log(yVar) - }; - - p.draw = function () { - p.background(255); - p.fill(255,0,0) - p.push(); - p.translate(xVar, yVar); - p.ellipse(0,0,10,10); - p.pop(); - flock.run(); - } - - p.mouseDragged = function () { - flock.addBoid(new p.Boid(p.mouseX, p.mouseY)); - } - - p.Flock = function() { - this.boids = []; //Initialize the array - } - - p.Flock.prototype.addBoid = function(b) { - this.boids.push(b); - } - - p.Flock.prototype.run = function() { - for (var i=0; i < this.boids.length; i++) { - this.boids[i].run(this.boids); - } - } - - p.Boid = function(x, y) { - this.acceleration = p.createVector(0, 0); - this.velocity = p.createVector(p.random(-1, 1), p.random(-1, 1)); - this.position = p.createVector(x, y); - this.r = 2.0; //Size of object Boid - this.maxspeed = 5; // Maximum speed - this.maxforce = 0.1; // Maximum steer ing force - } - - p.Boid.prototype.run = function(boids) { - this.flock(boids); - this.update(); - this.borders(); - this.render(); - }; - - p.Boid.prototype.applyForce = function(force) { - // We could add mass here if we want A = F / M - this.acceleration.add(force); - }; - - p.Boid.prototype.flock = function(boids) { - var sep = this.separate(boids); - var ali = this.align(boids); - var coh = this.cohesion(boids); - var mows = this.mouuse(boids); - - sep.mult(1.5); - ali.mult(1.0); - coh.mult(1.0); - mows.mult(3.0); - - this.applyForce(sep); - this.applyForce(ali); - this.applyForce(coh); - this.applyForce(mows); - }; - - - p.Boid.prototype.update = function() { - this.velocity.add(this.acceleration); - this.velocity.limit(this.maxspeed); - this.position.add(this.velocity); - this.acceleration.mult(0); - }; - - - p.Boid.prototype.seek = function(target) { - var desired = p5.Vector.sub(target, this.position); - desired.normalize(); - desired.mult(this.maxspeed); - var steer = p5.Vector.sub(desired, this.velocity); - steer.limit(this.maxforce); - return steer; - }; - - p.Boid.prototype.render = function() { - // Draw a triangle rotated in the direction of velocity - var theta = this.velocity.heading() + p.radians(90); - p.fill(0); - p.noStroke(); - p.push(); - p.translate(this.position.x, this.position.y); - // p.ellipse(0,0,5,5); - p.rotate(theta); - p.beginShape(); - p.vertex(0, -this.r * 2); - p.vertex(-this.r, this.r * 2); - p.vertex(this.r, this.r * 2); - p.endShape(p.CLOSE); - p.pop(); - }; - - p.Boid.prototype.borders = function() { - if (this.position.x < -this.r) this.position.x = p.width + this.r; - if (this.position.y < -this.r) this.position.y = p.height + this.r; - if (this.position.x > p.width + this.r) this.position.x = -this.r; - if (this.position.y > p.height + this.r) this.position.y = -this.r; - }; - - p.Boid.prototype.separate = function(boids) { - var desiredseparation = 25.0; - var steer = p.createVector(0, 0); - var count = 0; - // For every boid in the system, check if it's too close - for (var i = 0; i < boids.length; i++) { - var d = p5.Vector.dist(this.position, boids[i].position); - // If the distance is greater than 0 and less than an arbitrary amount (0 when you are yourself) - if ((d > 0) && (d < desiredseparation)) { - // Calculate vector pointing away from neighbor - var diff = p5.Vector.sub(this.position, boids[i].position); - diff.normalize(); - diff.div(d); // Weight by distance - steer.add(diff); - count++; // Keep track of how many - } - } - if (count > 0) { - steer.div(count); - } - if (steer.mag() > 0) { - steer.normalize(); - steer.mult(this.maxspeed); - steer.sub(this.velocity); - steer.limit(this.maxforce); - } - return steer; - }; - - // Alignment - // For every nearby boid in the system, calculate the average velocity - p.Boid.prototype.align = function(boids) { - var neighbordist = 100; - var sum = p.createVector(0, 0); - var count = 0; - for (var i = 0; i < boids.length; i++) { - var d = p5.Vector.dist(this.position, boids[i].position); - if ((d > 0) && (d < neighbordist)) { - sum.add(boids[i].velocity); - count++; - } - } - if (count > 0) { - sum.div(count); - sum.normalize(); - sum.mult(this.maxspeed); - var steer = p5.Vector.sub(sum, this.velocity); - steer.limit(this.maxforce); - return steer; - } else { - return p.createVector(0, 0); - } - }; - - // Cohesion - // For the average location (i.e. center) of all nearby boids, calculate steering vector towards that location - p.Boid.prototype.cohesion = function(boids) { - var neighbordist = 100; - var sum = p.createVector(0, 0); // Start with empty vector to accumulate all locations - var count = 0; - for (var i = 0; i < boids.length; i++) { - var d = p5.Vector.dist(this.position, boids[i].position); - if ((d > 0) && (d < neighbordist)) { - sum.add(boids[i].position); // Add location - count++; - } - } - if (count > 0) { - sum.div(count); - return this.seek(sum); // Steer towards the location - } else { - return p.createVector(0, 0); - } - }; - - p.Boid.prototype.mouuse = function(boids) { - var neighbordist = 500; - var m = p.createVector(xVar, yVar); - var d = p5.Vector.dist(this.position, m); - if ((d > 0) && (d < neighbordist)) { - return this.seek(m); // Steer towards the mouse location - } else { - return p.createVector(0, 0); - } - }; -}; \ No newline at end of file diff --git a/src/components/PageSwitcher/components/EEGEduAnimate/sketchFlock3D.js b/src/components/PageSwitcher/components/EEGEduAnimate/sketchFlock3D.js deleted file mode 100644 index 179bdd4d..00000000 --- a/src/components/PageSwitcher/components/EEGEduAnimate/sketchFlock3D.js +++ /dev/null @@ -1,540 +0,0 @@ -import p5 from 'p5'; - -export default function sketchFlock3D (p) { - - const flock = []; // Array of boids - let depth = 800; // The Z location of the boid tend to stay between +depth/2 and -depth/2 - let gap = 300; // Boids can go further than the edges, this further distance is the gap - let quadTree; // A quad tree to minimize the cost of distance calculation - - let useQuadTree = true; // Toogle the use of a quad tree - let showPerceptionRadius = false; // Toogle vizualization of perception radius - - let boidsSlider, perceptionSlider, alignmentSlider, cohesionSlider, separationSlider; // Sliders - let boidsP, perceptionP, alignmentP, cohesionP, separationP; // Paragraphs - let startingBoids = 50; // Amount of boid at the start of the sketch - let startingPerception = 90; // Perception radius at the start of the sketch - let t = 0; // Counts the frame from the time boids go out of the middle of space - - let theta = 0; - let alpha = 0; - let beta = 0; - let xVar = 0; - let yVar = 0; - let zVar = 0; - - // SETUP FUNCTION --------------------------------------------------- - // Make the canvas, declare some variables, create the DOM elements and the initial boid population - p.setup = function () { - // Declaration of a canvas to allow canvas download - p.createCanvas(p.windowWidth*.5, p.windowWidth*.5, p.WEBGL); // You can change the resolution here - - // Declaration of depth (z axis), unit vectors, and the camera - p.depth = p.height; - let cameraX = 1000 / 600 * p.width; - let cameraY = -800 / 600 * p.height; - let cameraZ = -200 / 500 * p.depth; - p.camera(cameraX, cameraY, cameraZ, 0, 0, 0, 0, 0, 1); - - // Create the DOM elements: sliders and paragraphs - createDOMs(); - - // Create an initial population of 100 boids - for (let i = 0; i < boidsSlider.value(); i++) { - pushRandomBoid(); - } - } - - p.windowResized = function() { - p.createCanvas(p.windowWidth*.5, p.windowWidth*.5); - p.depth = p.height; - let cameraX = 1000 / 600 * p.width; - let cameraY = -800 / 600 * p.height; - let cameraZ = -200 / 500 * p.depth; - p.camera(cameraX, cameraY, cameraZ, 0, 0, 0, 0, 0, 1); - - } - - p.myCustomRedrawAccordingToNewPropsHandler = function (props) { - theta = Math.floor((props.theta/10) * p.width); - alpha = Math.floor((props.alpha/10) * p.height); - beta = Math.floor((props.beta/10) * p.depth); - - xVar = theta-(p.width/2)+200; - yVar = alpha-(p.height/2)+200; - zVar = beta-(p.depth/2)+200; - - if (Math.abs(xVar) > p.width/2) { - xVar = Math.sign(xVar) * (p.width/2); - } - if (Math.abs(yVar) > p.height/2) { - yVar = Math.sign(yVar) * (p.height/2); - } - if (Math.abs(zVar) > p.depth/2) { - zVar = Math.sign(zVar) * (p.depth/2); - } - - // console.log(xVar + ' ' + yVar + ' ' + zVar) - }; - - // DRAW FUNCTION --------------------------------------------------- - p.draw = function () { - // Background and lightning - p.background(200); - //drag to move the world. - p.orbitControl(); - - p.directionalLight(150, 150, 150, 1, 1, 0); - p.ambientLight(150); - - // Draw the corners of a box showing the space where boids can fly - p.stroke(80); - p.strokeWeight(8); - p.noFill(); - p.box(p.width + gap/2, p.height + gap/2, p.depth + gap/2); - - p.noStroke(); - p.fill(255); - p.ambientMaterial(0, 0, 255); - p.push() - p.translate(xVar, yVar, zVar); - p.sphere(5); // A sphere where the boid is - p.pop(); - - - // Make the quad tree - let boundary = new Cube(0, 0, 0, p.width + 2 * gap, p.height + 2 * gap, p.depth + 2 * gap); - quadTree = new QuadTree(boundary, 4); - for (let boid of flock) { - quadTree.insert(boid); - } - - // Each boid determines its acceleration for the next frame - for (let boid of flock) { - boid.flock(flock, quadTree); - } - // Each boid updates its position and velocity, and is displayed on screen - for (let boid of flock) { - boid.update(gap); - boid.show(); - } - - // Adjust the amount of boids on screen according to the slider value - let maxBoids = boidsSlider.value(); - let difference = flock.length - maxBoids; - if (difference < 0) { - for (let i = 0; i < -difference; i++) { - pushRandomBoid(); // Add boids if there are less boids than the slider value - } - } else if (difference > 0) { - for (let i = 0; i < difference; i++) { - flock.pop(); // Remove boids if there are more boids than the slider value - } - } - - // Update the DOM elements - boidsP.html(`Boids: ${boidsSlider.value()}`); - perceptionP.html(`Perception: ${perceptionSlider.value()}`); - alignmentP.html(`Alignment: ${alignmentSlider.value()}`); - cohesionP.html(`Cohesion: ${cohesionSlider.value()}`); - separationP.html(`Separation: ${separationSlider.value()}`); - - t++; // t counts the number of frames, it is used to not have cohesion in the first 40 frames - } - - - // Create the DOM elements - function createDOMs() { - // Create the paragraphs and sliders - boidsP = p.createP('Boids'); - perceptionP = p.createP('Perception'); - alignmentP = p.createP('Alignment'); - cohesionP = p.createP('Cohesion'); - separationP = p.createP('Separation'); - - if (p.windowWidth * p.windowHeight > 1200 * 1200) startingPerception = 150; // Larger perception on a larger screen - boidsSlider = p.createSlider(1, 500, startingBoids, 1); - perceptionSlider = p.createSlider(0, 1000, startingPerception, 1); - alignmentSlider = p.createSlider(0, 5, 0.2, 0.1); - cohesionSlider = p.createSlider(0, 5, 0.3, 0.1); - separationSlider = p.createSlider(0, 5, 0.7, 0.1); - - // Position the DOM elements on the top left corner - let DOMoffset = 1050; // Place the DOM elements underneath the canvas when we want to download the canvas - let DOMgap = 5; // Gap between the DOM elements - let leftGap = 200; - boidsSlider.position( leftGap + DOMgap, DOMoffset + boidsSlider.height * 0 + 1 * DOMgap); - perceptionSlider.position(leftGap + DOMgap, DOMoffset + boidsSlider.height * 1 + 2 * DOMgap); - alignmentSlider.position( leftGap + DOMgap, DOMoffset + boidsSlider.height * 2 + 3 * DOMgap); - cohesionSlider.position( leftGap + DOMgap, DOMoffset + boidsSlider.height * 3 + 4 * DOMgap); - separationSlider.position(leftGap + DOMgap, DOMoffset + boidsSlider.height * 4 + 5 * DOMgap); - boidsP.position( leftGap + boidsSlider.width + DOMgap * 2, DOMoffset + boidsSlider.height * 0 + 0 * DOMgap + 2); - perceptionP.position( leftGap + boidsSlider.width + DOMgap * 2, DOMoffset + boidsSlider.height * 1 + 1 * DOMgap + 2); - alignmentP.position( leftGap + boidsSlider.width + DOMgap * 2, DOMoffset + boidsSlider.height * 2 + 2 * DOMgap + 2); - cohesionP.position( leftGap + boidsSlider.width + DOMgap * 2, DOMoffset + boidsSlider.height * 3 + 3 * DOMgap + 2); - separationP.position( leftGap + boidsSlider.width + DOMgap * 2, DOMoffset + boidsSlider.height * 4 + 4 * DOMgap + 2); - } - - // Make a new boid - function pushRandomBoid() { - //let pos = createVector(random(width), random(height), random(-depth/2, depth/2)); // Uncomment and comment next line to create boids at random position - let pos = p.createVector(0, 0, 0); // Create a boid at the center of space - let vel = p5.Vector.random3D().mult(p.random(0.5, 3)); // Give a random velocity - let boid = new Boid(pos, vel); // Create a new boid - flock.push(boid); // Add the new boid to the flock - } - - ///--- - ///--- - ///--- - - - - // Boid class with flocking behavior - class Boid { - constructor(pos, vel) { - this.pos = pos; // Position - this.vel = vel; // Velocity - this.acc = p.createVector(0, 0, 0); // Acceleration - this.maxForce = 1; // Maximum steering force for alignment, cohesion, separation - this.maxSpeed = 10; // Desired velocity for the steering behaviors - this.r = 255; // red color of the boid - this.g = p.floor(p.random(50, 120)); // green color of the boid - this.b = p.floor(p.random(50, 120)); // blue color of the boid - } - - // Alignment rule - // Steering to average neighbors velocity - alignment(neighbors) { - let steering = p.createVector(); - for (let other of neighbors) steering.add(other.vel); // Sum of neighbor velocities - if (neighbors.length > 0) { - steering.div(neighbors.length); // Average neighbors velocity - steering.setMag(this.maxSpeed); // Desired velocity - steering.sub(this.vel); // Actual steering - steering.limit(this.maxForce); // Steering limited to maxForce - } - return steering; - } - - // Cohesion rule - // Steering to the average neighbors position - cohesion(neighbors) { - let steering = p.createVector(); - for (let other of neighbors) steering.add(other.pos); // Sum of neighbor positions - if (neighbors.length > 0) { - steering.div(neighbors.length); // Average neighbors position - steering.sub(this.pos); // Orientation of the desired velocity - steering.setMag(this.maxSpeed); // Desired velocity - steering.sub(this.vel); // Actual steering - steering.limit(this.maxForce); // Steering limited to maxForce - } - return steering; - } - - // Separation rule - // Steering to avoid proximity of the neighbors - separation(neighbors) { - let steering = p.createVector(); - for (let other of neighbors) { - let diff = p5.Vector.sub(this.pos, other.pos); // Vector from other boid to this boid - let d = p.max(other.distance, 0.01); // Distance between other boid and this boid - steering.add(diff.div(d)); // Magnitude inversely proportional to the distance - } - if (neighbors.length > 0) { - steering.div(neighbors.length); // Orientation of the desired velocity - steering.setMag(this.maxSpeed); // Desired velocity - steering.sub(this.vel); // Actual steering - steering.limit(this.maxForce); // Steering limited to maxForce - } - return steering; - } - - // Application of the rules - flock(boids, quadTree) { - // Go to the middle if goMiddle is true - // Create a large force towards the middle, apply it to the boid, and "return" to not apply other forces - let force = p.createVector(xVar-this.pos.x, yVar-this.pos.y, zVar-this.pos.z); - force.setMag(this.maxForce); - this.acc.add(force); - - let radius = perceptionSlider.value(); // Max distance of a neighbor - let neighbors = []; - - if (useQuadTree === true) { - // VERSION WITH QUADTREE - // Make an array of neighbors, i.e. all boids closer than the perception radius - // The array will be passed to the different flocking behaviors - let range = new Cube(this.pos.x, this.pos.y, this.pos.z, radius, radius, radius); - let maybeNeighbors = quadTree.query(range); - for (let other of maybeNeighbors) { - let distance = this.pos.dist(other.pos); - if (other !== this && distance < radius) { - other.distance = distance; // Record the distance so it can be used later - neighbors.push(other); // Put this neighbor in the "neighbors" array - } - } - } else { - // VERSION WITHOUT QUADTREE - // Make an array of neighbors, i.e. all boids closer than the perception radius - // The array will be passed to the different flocking behaviors - for (let other of boids) { - let distance = this.pos.dist(other.pos); - if (other !== this && distance < radius) { - other.distance = distance; // Record the distance so it can be used later - neighbors.push(other); // Put this neighbor in the "neighbors" array - } - } - } - - - - // Calculate the force of alignments and apply it to the boid - let alignment = this.alignment(neighbors); - alignment.mult(alignmentSlider.value()); - this.acc.add(alignment); - - // Calculate the force of cohesion and apply it to the boid - if (t > 2) { // No cohesion in the first 40 frames - let cohesion = this.cohesion(neighbors); - cohesion.mult(cohesionSlider.value()); - this.acc.add(cohesion); - } - - // Calculate the force of separation and apply it to the boid - let separation = this.separation(neighbors); - separation.mult(separationSlider.value()); - this.acc.add(separation); - - // If the boid is flies too high or too low, apply another force to make it fly around the middle of space's depth - if (this.pos.z < -depth/8 || this.pos.z > depth/8) { - let force = p.createVector(0, 0, -this.pos.z / depth * this.maxForce * 2); - this.acc.add(force); - } - - // If the boid has no neighbor, apply random forces so it can go find other boids - if (neighbors.length === 0) { - let force = p5.Vector.random3D().mult(this.maxForce/4); - force.z = 0; // Only go find other in an XY plane - this.acc.add(force); - } - } - - // Update position, velocity, and acceleration - update(gap) { - // Apply physics - this.pos.add(this.vel); - this.vel.add(this.acc); - this.vel.mult(0.999); // Some friction - this.vel.limit(this.maxSpeed); - this.acc.mult(0); - - // Teleport to opposite side if the boid goes further than a side of space (X and Y axis) - // Except for the Z axis, as there is already a force keeping the boid from getting too far - if (this.pos.x > p.width/2 + gap) this.pos.x -= p.width + 1.7 * gap; - if (this.pos.x < -(p.width/2 + gap)) this.pos.x += p.width + 1.7 * gap; - if (this.pos.y > p.height/2 + gap) this.pos.y -= p.height + 1.7 * gap; - if (this.pos.y < -(p.height/2 + gap)) this.pos.y += p.eight + 1.7 * gap; - } - - // Show the boid on screen - show() { - p.noStroke(); - p.fill(255); - p.ambientMaterial(this.r, this.g, this.b); - - p.push() - p.translate(this.pos.x, this.pos.y, this.pos.z); - p.sphere(10); // A sphere where the boid is - let arrow = p.createVector(this.vel.x, this.vel.y, this.vel.z).setMag(10); - p.translate(arrow.x, arrow.y, arrow.z); - p.sphere(5); // Another sphere, smaller, in the direction of the boid's velocity - p.pop(); - - // Show perception radius, all circles are drawn at z = 0 - if (showPerceptionRadius) { - p.stroke(255, 255, 255, 100); - p.noFill(); - p.strokeWeight(1); - let perception = perceptionSlider.value() * 2; - p.push(); - p.translate(0,0,this.pos.z) - p.ellipse(this.pos.x, this.pos.y, perception, perception); - p.pop(); - } - } - } - - /// - /// - /// - - // This file contains the QuadTree class - // as well as the Cube classe used by the QuadTree - - // Cube -------------------------------------------------- - // A cube delimiting the volume of a quad tree - // or the volume used for asking boids from a quad tree - class Cube { - constructor(x, y, z, w, h, d) { - this.x = x; - this.y = y; - this.z = z; - this.w = w; - this.h = h; - this.d = d; - - this.xMin = x - w; - this.xMax = x + w; - this.yMin = y - h; - this.yMax = y + h; - this.zMin = z - d; - this.zMax = z + d; - } - - // Checks if a boid is inside the cube - contains(boid) { - let pos = boid.pos; - return (pos.x >= this.xMin && pos.x <= this.xMax && - pos.y >= this.yMin && pos.y <= this.yMax && - pos.z >= this.zMin && pos.z <= this.zMax); - } - - // Check if two cubes intersect - intersects(range) { - return !(this.xMax < range.xMin || this.xMin > range.xMax || - this.yMax < range.yMin || this.yMin > range.yMax || - this.zMax < range.zMin || this.zMin > range.zMax); - } - } - - // QUAD TREE -------------------------------------------------- - // The quad tree stores points in a tree structure - // to minimize the cost of distance calculation - class QuadTree { - constructor(boundary, capacity) { - this.boundary = boundary; // cube giving the borders of the quad tree - this.capacity = capacity; // Maximum amount of points that can be stored in the quad tree - this.boids = []; // Array storing the boids in the quad tree - this.divided = false; // True when the quad tree subdivides - } - - // Insert a boid in the quad tree - insert(boid) { - // Return if the boid is not in the area of this layer of quad tree - if (!this.boundary.contains(boid)) { - return false; - } - - // Add the boid at this layer or a deeper layer depending on capacity - if (this.boids.length < this.capacity) { - // Add the point to this layer if there is still room for it - this.boids.push(boid); - return true; - } else { - // Otherwise, subdivide to make room for the new boid - // Subdivision divides the quad tree area into 8 new children quad trees - if (!this.divided) { - this.subdivide(); - } - - // Add the boid to the relevant subdivision - // N = North, S = South, E = East, W = West, B = Bottom, T = Top - if (this.NWT.insert(boid)) { - return true; - } else if (this.NET.insert(boid)) { - return true; - } else if (this.SET.insert(boid)) { - return true; - } else if (this.SWT.insert(boid)) { - return true; - } else if (this.NWB.insert(boid)) { - return true; - } else if (this.NEB.insert(boid)) { - return true; - } else if (this.SEB.insert(boid)) { - return true; - } else if (this.SWB.insert(boid)) { - return true; - } - } - } - - // Subdivides the quad tree if it is at full capacity, creating 8 new children quad trees - subdivide() { - this.divided = true; // Informs of the subdivision to only subdivide once - - let x = this.boundary.x; - let y = this.boundary.y; - let z = this.boundary.z; - let w = this.boundary.w / 2; - let h = this.boundary.h / 2; - let d = this.boundary.d / 2; - - // Creates the 8 children quad trees with the relevant positions and area - // North West Top quad tree - let NWTBoundary = new Cube(x - w, y - h, z - d, w, h, d); - this.NWT = new QuadTree(NWTBoundary, this.capacity); - - // North East Top quad tree - let NETBoundary = new Cube(x + w, y - h, z - d, w, h, d); - this.NET = new QuadTree(NETBoundary, this.capacity); - - // South East Top quad tree - let SETBoundary = new Cube(x + w, y + h, z - d, w, h, d); - this.SET = new QuadTree(SETBoundary, this.capacity); - - // South West Top quad tree - let SWTBoundary = new Cube(x - w, y + h, z - d, w, h, d); - this.SWT = new QuadTree(SWTBoundary, this.capacity); - - // North West Bot quad tree - let NWBBoundary = new Cube(x - w, y - h, z + d, w, h, d); - this.NWB = new QuadTree(NWBBoundary, this.capacity); - - // North East Bot quad tree - let NEBBoundary = new Cube(x + w, y - h, z + d, w, h, d); - this.NEB = new QuadTree(NEBBoundary, this.capacity); - - // South East Bot quad tree - let SEBBoundary = new Cube(x + w, y + h, z + d, w, h, d); - this.SEB = new QuadTree(SEBBoundary, this.capacity); - - // South West Bot quad tree - let SWBBoundary = new Cube(x - w, y + h, z + d, w, h, d); - this.SWB = new QuadTree(SWBBoundary, this.capacity); - } - - // Returns all the points in a given range (Cube) and put them in the "found" array - query(range, found) { - // The array "found" will check all quad trees intersecting with the range, - // looking for points intersecting with the range - if (!found) found = []; // Creates the array at the beginning of the recursion - - if (!this.boundary.intersects(range)) { - return found; // No intersection between the quad tree and the range, no need to check for points - } else { - // If the range intersects this quad tree, check for the intersection of its points with the range - for (let boid of this.boids) { - if (range.contains(boid)) { - found.push(boid); // Add the points intersecting with the range to "found" - } - } - - // This quad tree intersects with the range, now do the same for its children quad trees - if (this.divided) { - this.NWT.query(range, found); - this.NET.query(range, found); - this.SET.query(range, found); - this.SWT.query(range, found); - this.NWB.query(range, found); - this.NEB.query(range, found); - this.SEB.query(range, found); - this.SWB.query(range, found); - } - } - - return found; - } - } -} \ No newline at end of file diff --git a/src/components/PageSwitcher/components/EEGEduAnimate/sketchTone.js b/src/components/PageSwitcher/components/EEGEduAnimate/sketchTone.js deleted file mode 100644 index 1139060d..00000000 --- a/src/components/PageSwitcher/components/EEGEduAnimate/sketchTone.js +++ /dev/null @@ -1,63 +0,0 @@ -import p5 from "p5"; -import "p5/lib/addons/p5.sound"; - - -export default function sketchTone (p) { - let delta = 0; - let theta = 0; - let alpha = 0; - let beta = 0; - let gamma = 0; - - let osc, envelope, fft; - - let scaleArray = [delta+30, theta+50 , beta+60, gamma*10+70]; - let note = 0; - - p.setup = function () { - p.createCanvas(710, 200); - osc = new p5.SinOsc(); - // Instantiate the envelope - envelope = new p5.Env(); - // set attackTime, decayTime, sustainRatio, releaseTime - envelope.setADSR(0.001, 0.5, 0.1, 0.5); - // set attackLevel, releaseLevel - envelope.setRange(1, 0); - osc.start(); - fft = new p5.FFT(); - p.noStroke(); - }; - - p.myCustomRedrawAccordingToNewPropsHandler = function (props) { - delta = Math.floor(props.delta); - theta = Math.floor(props.theta); - alpha = Math.floor(props.alpha); - beta = Math.floor(props.beta); - gamma = Math.floor(props.gamma); - }; - - p.windowResized = function() { - p.resizeCanvas(p.windowWidth*.6, 200); - } - - p.draw = function () { - p.background(20); - - if (p.frameCount % alpha === 0 || p.frameCount === 1) { - let midiValue = scaleArray[note]; - let freqValue = p.midiToFreq(midiValue); - osc.freq(freqValue); - - envelope.play(osc, 0, 0.1); - note = (note + 1) % scaleArray.length; - } - - let spectrum = fft.analyze(); - for (let i = 0; i < spectrum.length / 20; i++) { - p.fill(spectrum[i], spectrum[i] / 10, 0); - let x = p.map(i, 0, spectrum.length / 20, 0, p.width); - let h = p.map(spectrum[i], 0, 255, 0, p.height); - p.rect(x, p.height, spectrum.length / 20, -h); - } - }; -}; \ No newline at end of file diff --git a/src/components/PageSwitcher/components/EEGEduAnimate/translations/en.json b/src/components/PageSwitcher/components/EEGEduAnimate/translations/en.json deleted file mode 100644 index 3962912d..00000000 --- a/src/components/PageSwitcher/components/EEGEduAnimate/translations/en.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "title": "P5js integration", - "description": [ - "In the next demo we look at the traditional frequency bands. ", - "This time instead of graphing them we pipe them into a live web animation." - ], - "xlabel": "Frequency (Hz)", - "ylabel": "Power (\u03BCV\u00B2)" -} diff --git a/src/components/PageSwitcher/components/EEGEduBands/EEGEduBands.js b/src/components/PageSwitcher/components/EEGEduBands/EEGEduBands.js deleted file mode 100644 index 05b39fb9..00000000 --- a/src/components/PageSwitcher/components/EEGEduBands/EEGEduBands.js +++ /dev/null @@ -1,315 +0,0 @@ -import React from "react"; -import { catchError, multicast } from "rxjs/operators"; - -import { TextContainer, Card, Stack, RangeSlider, Button, ButtonGroup, Modal } from "@shopify/polaris"; -import { saveAs } from 'file-saver'; -import { takeUntil } from "rxjs/operators"; -import { Subject, timer } from "rxjs"; - -import { channelNames } from "muse-js"; -import { Bar } from "react-chartjs-2"; - -import { zipSamples } from "muse-js"; - -import { - bandpassFilter, - epoch, - fft, - powerByBand -} from "@neurosity/pipes"; - -import { chartStyles, generalOptions } from "../chartOptions"; - -import * as generalTranslations from "../translations/en"; -import * as specificTranslations from "./translations/en"; -import { bandLabels } from "../../utils/chartUtils"; - -export function getSettings () { - return { - cutOffLow: 2, - cutOffHigh: 20, - interval: 100, - bins: 256, - duration: 1024, - srate: 256, - name: 'Bands', - secondsToSave: 10 - } -}; - -export function buildPipe(Settings) { - if (window.subscriptionBands) window.subscriptionBands.unsubscribe(); - - window.pipeBands$ = null; - window.multicastBands$ = null; - window.subscriptionBands = null; - - // Build Pipe - window.pipeBands$ = zipSamples(window.source.eegReadings$).pipe( - bandpassFilter({ - cutoffFrequencies: [Settings.cutOffLow, Settings.cutOffHigh], - nbChannels: window.nchans }), - epoch({ - duration: Settings.duration, - interval: Settings.interval, - samplingRate: Settings.srate - }), - fft({ bins: Settings.bins }), - powerByBand(), - catchError(err => { - console.log(err); - }) - ); - window.multicastBands$ = window.pipeBands$.pipe( - multicast(() => new Subject()) - ); -} - -export function setup(setData, Settings) { - console.log("Subscribing to " + Settings.name); - - if (window.multicastBands$) { - window.subscriptionBands = window.multicastBands$.subscribe(data => { - setData(bandsData => { - Object.values(bandsData).forEach((channel, index) => { - channel.datasets[0].data = [ - data.delta[index], - data.theta[index], - data.alpha[index], - data.beta[index], - data.gamma[index] - ]; - channel.xLabels = bandLabels; - }); - - return { - ch0: bandsData.ch0, - ch1: bandsData.ch1, - ch2: bandsData.ch2, - ch3: bandsData.ch3, - ch4: bandsData.ch4 - }; - }); - }); - - window.multicastBands$.connect(); - console.log("Subscribed to " + Settings.name); - } -} - -export function renderModule(channels) { - function renderCharts() { - return Object.values(channels.data).map((channel, index) => { - if (index < window.nchans) { - const options = { - ...generalOptions, - scales: { - xAxes: [ - { - scaleLabel: { - ...generalOptions.scales.xAxes[0].scaleLabel, - labelString: specificTranslations.xlabel - } - } - ], - yAxes: [ - { - scaleLabel: { - ...generalOptions.scales.yAxes[0].scaleLabel, - labelString: specificTranslations.ylabel - }, - ticks: { - max: 25, - min: 0 - } - } - ] - }, - title: { - ...generalOptions.title, - text: generalTranslations.channel + channelNames[index] - } - }; - - return ( - - - - ); - } else { - return null - } - }); - } - - return ( - - - - -

{specificTranslations.description}

-
-
-
- -
{renderCharts()}
-
-
- ); -} - -export function renderSliders(setData, setSettings, status, Settings) { - - function resetPipeSetup(value) { - buildPipe(Settings); - setup(setData, Settings); - } - - function handleIntervalRangeSliderChange(value) { - setSettings(prevState => ({...prevState, interval: value})); - resetPipeSetup(); - } - - function handleCutoffLowRangeSliderChange(value) { - setSettings(prevState => ({...prevState, cutOffLow: value})); - resetPipeSetup(); - } - - function handleCutoffHighRangeSliderChange(value) { - setSettings(prevState => ({...prevState, cutOffHigh: value})); - resetPipeSetup(); - } - - function handleDurationRangeSliderChange(value) { - setSettings(prevState => ({...prevState, duration: value})); - resetPipeSetup(); - } - - return ( - - - - - - - ) -} - -export function renderRecord(recordPopChange, recordPop, status, Settings, setSettings) { - - function handleSecondsToSaveRangeSliderChange(value) { - setSettings(prevState => ({...prevState, secondsToSave: value})); - } - - return ( - - - - - - - - - -

- Your data is currently recording, - once complete it will be downloaded as a .csv file - and can be opened with your favorite spreadsheet program. - Close this window once the download completes. -

-
-
-
-
-
-) -} - - -function saveToCSV(Settings) { - console.log('Saving ' + Settings.secondsToSave + ' seconds...'); - var localObservable$ = null; - const dataToSave = []; - - console.log('making ' + Settings.name + ' headers') - - dataToSave.push( - "Timestamp (ms),", - "delta0,delta1,delta2,delta3,deltaAux,", - "theta0,theta1,theta2,theta3,thetaAux,", - "alpha0,alpha1,alpha2,alpha3,alphaAux,", - "beta0,beta1,beta2,beta3,betaAux,", - "delta0,delta1,delta2,delta3,deltaAux\n" - ); - - // Create timer - const timer$ = timer(Settings.secondsToSave * 1000); - - // put selected observable object into local and start taking samples - localObservable$ = window.multicastBands$.pipe( - takeUntil(timer$) - ); - - // now with header in place subscribe to each epoch and log it - localObservable$.subscribe({ - next(x) { - dataToSave.push(Date.now() + "," + Object.values(x).join(",") + "\n"); - // logging is useful for debugging -yup - // console.log(x); - }, - error(err) { console.log(err); }, - complete() { - console.log('Trying to save') - var blob = new Blob( - dataToSave, - {type: "text/plain;charset=utf-8"} - ); - saveAs(blob, Settings.name + "_Recording_" + Date.now() + ".csv"); - console.log('Completed'); - } - }); -} \ No newline at end of file diff --git a/src/components/PageSwitcher/components/EEGEduBands/translations/en.json b/src/components/PageSwitcher/components/EEGEduBands/translations/en.json deleted file mode 100644 index b2bb6571..00000000 --- a/src/components/PageSwitcher/components/EEGEduBands/translations/en.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "title": "Frequency Bands Data", - "description": [ - "In the next demo we look at the traditional frequency bands. ", - "We take the same spectra that was computed in Spectra and divide into four bands. ", - "Delta (1-4 Hz), Theta (4-7 Hz), Alpha (7-12 Hz), Beta (12-30 Hz), and Gamme (30+ Hz). ", - "See if you can pick one of the bars and try to control its height by relaxing." - ], - "xlabel": "Frequency (Hz)", - "ylabel": "Power (\u03BCV\u00B2)" -} diff --git a/src/components/PageSwitcher/components/EEGEduEvoked/EEGEduEvoked.js b/src/components/PageSwitcher/components/EEGEduEvoked/EEGEduEvoked.js deleted file mode 100644 index 4e88874d..00000000 --- a/src/components/PageSwitcher/components/EEGEduEvoked/EEGEduEvoked.js +++ /dev/null @@ -1,316 +0,0 @@ -import React from "react"; -import { catchError, multicast } from "rxjs/operators"; -import { Subject, timer } from "rxjs"; - -import { TextContainer, Card, Stack, RangeSlider, Button, ButtonGroup, Modal } from "@shopify/polaris"; -import { saveAs } from 'file-saver'; -import { take, takeUntil } from "rxjs/operators"; - - -import { zipSamples } from "muse-js"; - -import { - bandpassFilter, - epoch -} from "@neurosity/pipes"; - -import { chartStyles } from "../chartOptions"; - -import * as generalTranslations from "../translations/en"; -import * as specificTranslations from "./translations/en"; - -import { generateXTics, standardDeviation } from "../../utils/chartUtils"; - -import P5Wrapper from 'react-p5-wrapper'; -import sketchEvoked from './sketchEvoked' - -export function getSettings () { - return { - cutOffLow: 2, - cutOffHigh: 20, - interval: 1, - srate: 256, - duration: 1, - name: 'Evoked', - secondsToSave: 60 - } -}; - -export function buildPipe(Settings) { - if (window.subscriptionEvoked) window.subscriptionEvoked.unsubscribe(); - - window.pipeEvoked$ = null; - window.multicastEvoked$ = null; - window.subscriptionEvoked = null; - - // Build Pipe - window.pipeEvoked$ = zipSamples(window.source.eegReadings$).pipe( - bandpassFilter({ - cutoffFrequencies: [Settings.cutOffLow, Settings.cutOffHigh], - nbChannels: window.nchans }), - epoch({ - duration: Settings.duration, - interval: Settings.interval, - samplingRate: Settings.srate - }), - catchError(err => { - console.log(err); - }) - ); - window.multicastEvoked$ = window.pipeEvoked$.pipe( - multicast(() => new Subject()) - ); -} - -export function setup(setData, Settings) { - console.log("Subscribing to " + Settings.name); - - if (window.multicastEvoked$) { - window.subscriptionEvoked = window.multicastEvoked$.subscribe(data => { - setData(evokedData => { - Object.values(evokedData).forEach((channel, index) => { - channel.datasets[0].data = data.data[index]; - channel.xLabels = generateXTics(Settings.srate, Settings.duration); - channel.datasets[0].qual = standardDeviation(data.data[index]) - }); - - return { - ch0: evokedData.ch0, - ch1: evokedData.ch1, - ch2: evokedData.ch2, - ch3: evokedData.ch3, - ch4: evokedData.ch4 - - }; - }); - }); - - window.multicastEvoked$.connect(); - console.log("Subscribed to Evoked"); - } -} - -export function renderModule(channels) { - function renderCharts() { - return null - } - - return ( - - - - -

{specificTranslations.description}

-
-
-
- -
{renderCharts()}
-
-
- ); -} - -export function renderSliders(setData, setSettings, status, Settings) { - - function resetPipeSetup(value) { - buildPipe(Settings); - setup(setData, Settings) - } - - function handleIntervalRangeSliderChange(value) { - setSettings(prevState => ({...prevState, interval: value})); - resetPipeSetup(); - } - - function handleCutoffLowRangeSliderChange(value) { - setSettings(prevState => ({...prevState, cutOffLow: value})); - resetPipeSetup(); - } - - function handleCutoffHighRangeSliderChange(value) { - setSettings(prevState => ({...prevState, cutOffHigh: value})); - resetPipeSetup(); - } - - function handleDurationRangeSliderChange(value) { - setSettings(prevState => ({...prevState, duration: value})); - resetPipeSetup(); - } - - return ( - - - - - - - ) -} - -export function renderRecord(recordPopChange, recordPop, status, Settings, setSettings) { - - function handleSecondsToSaveRangeSliderChange(value) { - setSettings(prevState => ({...prevState, secondsToSave: value})); - } - - return ( - - -

- {"Clicking this button will begin the experiment so check your data quality on the raw module first. "} - {"A window will pop up when you click the button and a series of circles will appear. Stare at the cross in the center. "} - {"There will be red and blue circles, ignore the blue ones."} - {"Whenever you see a red circle, as fast as you can either press spacebar on a keyboard, or tap the touchscreen on a tablet or phone. "} - {"This entire time the eeg data will be saved along with a column indicating which target was on the screen, and another for the responses. "} - {"The task will continue for a few minutes and once it is finished a .csv file will automatically download. "} - {"This .csv file has a row for each time point, a column for each electrode, and the columns indicating when targets appeared, and when responses were made. "} - {"It saves a marker of 20 when the target is on, a marker of 10 when the blue standards are on, and a peak when the spacebar or touchscreen are pressed, here you can see those synced with an eeg channel: "} - -

-

- dataExample -

-
- - - - - - - - - - - - -

- Your data is currently recording, - once complete it will be downloaded as a .csv file - and can be opened with your favorite spreadsheet program. - Close this window once the download completes. -

-
-
-
- -
-
- ) -} - -function saveToCSV(Settings) { - console.log('Saving ' + Settings.secondsToSave + ' seconds...'); - var localObservable$ = null; - const dataToSave = []; - window.marker = 0; - window.responseMarker = 0; - window.touchMarker = 0; - - console.log('making ' + Settings.name + ' headers') - - // for each module subscribe to multicast and make header - // take one sample from selected observable object for headers - localObservable$ = window.multicastEvoked$.pipe( - take(1) - ); - //take one sample to get header info - localObservable$.subscribe({ - next(x) { - dataToSave.push( - "Timestamp (ms),", - "Marker,", - "SpaceBar,", - "TouchMarker,", - generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch0_" + f + "ms"}) + ",", - generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch1_" + f + "ms"}) + ",", - generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch2_" + f + "ms"}) + ",", - generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch3_" + f + "ms"}) + ",", - generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "chAux_" + f + "ms"}) + ",", - "info", - "\n" - ); - } - }); - - //create timer - const timer$ = timer(Settings.secondsToSave * 1000) - - // put selected observable object into local and start taking samples - localObservable$ = window.multicastEvoked$.pipe( - takeUntil(timer$) - ); - - // now with header in place subscribe to each epoch and log it - localObservable$.subscribe({ - next(x) { - dataToSave.push( - Date.now() + "," + - window.marker + "," + - window.responseMarker + "," + - window.touchMarker + "," + - Object.values(x).join(",") + "\n"); - // logging is useful for debugging -yup - // console.log(x); - }, - error(err) { console.log(err); }, - complete() { - console.log('Trying to save') - var blob = new Blob( - dataToSave, - {type: "text/plain;charset=utf-8"} - ); - saveAs(blob, Settings.name + "_Recording_" + Date.now() + ".csv"); - console.log('Completed'); - } - }); -} \ No newline at end of file diff --git a/src/components/PageSwitcher/components/EEGEduEvoked/dataExample.png b/src/components/PageSwitcher/components/EEGEduEvoked/dataExample.png deleted file mode 100644 index e569826b..00000000 Binary files a/src/components/PageSwitcher/components/EEGEduEvoked/dataExample.png and /dev/null differ diff --git a/src/components/PageSwitcher/components/EEGEduEvoked/sketchEvoked.js b/src/components/PageSwitcher/components/EEGEduEvoked/sketchEvoked.js deleted file mode 100644 index 30c1dfec..00000000 --- a/src/components/PageSwitcher/components/EEGEduEvoked/sketchEvoked.js +++ /dev/null @@ -1,79 +0,0 @@ -export default function sketchEvoked (p) { - - let x = 0; - let thisRand = 0.5; //for random choice of target type - let targProp = 0.25; - let isTarget = false; - let ellapsedTime = 0; - let nextDelay = 1000; - let newOnset = true; - let startTime = 0; - let targCount = 0; - - p.setup = function () { - p.createCanvas(300, 300); - p.frameRate(60); - p.noStroke(); - }; - - p.windowResized = function() { - p.createCanvas(300, 300); - } - - p.touchStarted = function() { - if (window.touchMarker === 0) { - window.touchMarker = 255; - } else { - window.touchMarker = 0; - } -} - - p.draw = function () { - - - if (p.keyIsPressed === true) { - window.responseMarker = p.keyCode; - } else { - window.responseMarker = 0; - } - p.background(255); - x = x+1; - ellapsedTime = p.millis()-startTime; - - if (ellapsedTime > nextDelay) { - newOnset = true; - } else { - newOnset = false; - } - - if (newOnset) { - targCount++; - nextDelay = 500 + p.int(p.random() * 1000); - console.log(targCount, nextDelay) - startTime = p.millis(); - - thisRand = p.random(); - if (thisRand < targProp) { // targets 20% of the time - isTarget = true; - } else { - isTarget = false; - } - - if (isTarget) { - p.fill(250, 150, 150); - window.marker = 20; - } else { - p.fill(150,150,250); - window.marker = 10; - } - - } else { // during time between targets - p.fill(255, 255, 255); - window.marker = 0; - } - p.ellipse(p.width/2, p.height/2, 300); - p.fill(255,0,0); - p.text("+", p.width/2, p.height/2); - } - -}; \ No newline at end of file diff --git a/src/components/PageSwitcher/components/EEGEduEvoked/translations/en.json b/src/components/PageSwitcher/components/EEGEduEvoked/translations/en.json deleted file mode 100644 index df4af9ac..00000000 --- a/src/components/PageSwitcher/components/EEGEduEvoked/translations/en.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "title": "Stimulus Evoked Event-related potential (ERP)", - "description": [ - "The electrical activity evoked by individual presentations of stimuli does create small changes in electrical voltage. ", - "These changes are, however, very small, about 1-10 microVolts (\u03BCV), and the noise from other things like eye movements and muscles is sometimes 10x as large. ", - "This noise can be mitigated by averaging this evoked activity over repeated presentations of the same stimulus. ", - "This average voltage in response to a stimulus is called an Event-related potential (ERP) or evoked potential (EP). ", - "Here..." - ], - "xlabel": "Time (ms)", - "ylabel": "Votage (\u03BCV)" -} diff --git a/src/components/PageSwitcher/components/EEGEduHeartRaw/ECG_Principle_fast.gif b/src/components/PageSwitcher/components/EEGEduHeartRaw/ECG_Principle_fast.gif deleted file mode 100644 index 38a3b88d..00000000 Binary files a/src/components/PageSwitcher/components/EEGEduHeartRaw/ECG_Principle_fast.gif and /dev/null differ diff --git a/src/components/PageSwitcher/components/EEGEduHeartRaw/EEGEduHeartRaw.js b/src/components/PageSwitcher/components/EEGEduHeartRaw/EEGEduHeartRaw.js deleted file mode 100644 index e1031db9..00000000 --- a/src/components/PageSwitcher/components/EEGEduHeartRaw/EEGEduHeartRaw.js +++ /dev/null @@ -1,398 +0,0 @@ -import React from "react"; -import { catchError, multicast, take } from "rxjs/operators"; -import { Subject } from "rxjs"; - -import { TextContainer, Card, Stack, ButtonGroup, Button, Modal, Link } from "@shopify/polaris"; -import { channelNames } from "muse-js"; -import { Line } from "react-chartjs-2"; -import { saveAs } from 'file-saver'; -import YouTube from 'react-youtube' - -import { zipSamples } from "muse-js"; - -import { - bandpassFilter, - epoch -} from "@neurosity/pipes"; - -import { chartStyles, generalOptions } from "../chartOptions"; - -import * as generalTranslations from "../translations/en"; -import * as specificTranslations from "./translations/en"; - -import { generateXTics, standardDeviation } from "../../utils/chartUtils"; - -export function getSettings () { - return { - cutOffLow: 2, - cutOffHigh: 20, - interval: 10, - srate: 256, - duration: 2560, - name: 'HeartRaw' - } -}; - -export function buildPipe(Settings) { - if (window.subscriptionHeartRaw) window.subscriptionHeartRaw.unsubscribe(); - - window.pipeHeartRaw$ = null; - window.multicastHeartRaw$ = null; - window.subscriptionHeartRaw = null; - - // Build Pipe - window.pipeHeartRaw$ = zipSamples(window.source.eegReadings$).pipe( - bandpassFilter({ - cutoffFrequencies: [Settings.cutOffLow, Settings.cutOffHigh], - nbChannels: window.nchans }), - epoch({ - duration: Settings.duration, - interval: Settings.interval, - samplingRate: Settings.srate - }), - catchError(err => { - console.log(err); - }) - ); - window.multicastHeartRaw$ = window.pipeHeartRaw$.pipe( - multicast(() => new Subject()) - ); -} - -export function setup(setData, Settings) { - console.log("Subscribing to " + Settings.name); - - if (window.multicastHeartRaw$) { - window.subscriptionHeartRaw = window.multicastHeartRaw$.subscribe(data => { - setData(heartRawData => { - Object.values(heartRawData).forEach((channel, index) => { - channel.datasets[0].data = data.data[index]; - channel.xLabels = generateXTics(Settings.srate, Settings.duration).map(function(x) {return x / 1000});; - channel.datasets[0].qual = standardDeviation(data.data[index]) - }); - return { - ch0: heartRawData.ch0, - ch1: heartRawData.ch1 - }; - }); - }); - - window.multicastHeartRaw$.connect(); - console.log("Subscribed to HeartRaw"); - } -} - -export function renderModule(channels) { - function renderCharts() { - return Object.values(channels.data).map((channel, index) => { - if (index === 1) { - const options = { - ...generalOptions, - scales: { - xAxes: [{ - scaleLabel: { - ...generalOptions.scales.xAxes[0].scaleLabel, - labelString: specificTranslations.xlabel - }, - gridLines: { - color: "rgba(50,50,50)" - }, - ticks: { - maxTicksLimit: 10 - } - - }], - yAxes: [ - { - scaleLabel: { - ...generalOptions.scales.yAxes[0].scaleLabel, - labelString: specificTranslations.ylabel - } - } - ] - }, - elements: { - line: { - borderColor: 'rgba(' + channel.datasets[0].qual + ', 128, 128)', - fill: false - }, - point: { - radius: 0 - } - }, - animation: { - duration: 0 - }, - title: { - ...generalOptions.title, - text: generalTranslations.channel + channelNames[index] + ' --- SD: ' + channel.datasets[0].qual - }, - - }; - - return ( - - - - ); - } else { - return null - } - }); - } - - return ( - - - - - -

{[ - "As a first introduction to measurement of electrical potentials from the body we will look at something accessible. ", - "As the heart beats and pumps blood throuhgouts our body, a series of electrical potentials are created, which can be measured using electrodes placed around the heart. ", - "This is referred to as the Electrocardiogram (ECG). You have seen this hundreds of times in movies and TV beside hospital beds. " - ]}

-
-
-
- ECGPrinciples - Image Source - Wikipedia -
- -

{[ - "The ECG is best measured by comparing the electrical potential accross the left vs. right side of the body. ", - "Depending on where on the body the measurements are taken, they pick up a different view of the electrical potentials generated by the heart. ", - "One of the easiest places to measure is comparing the voltage between the left and right hands. ", - "For example, this is how treadmills can read your heart rate when you place one hand on each of the two holds. " - ]}

-
-
- LeftHand - Image Source - Wikipedia -
- -

{[ - "Therefore, you can see your own ECG today using the muse. ", - "You can take off the muse from your head and place a finger on your right hand on the muse's reference electrode (in the center of the forehead). ", - "We can then place a finger of our left hand on one of the eeg electrodes. Lets use the left forehead electrode (position AF7). ", - "So place your left fingers pinching the left forehead electrode, and your right fingers pinching the center electrode. ", - "Otherwise try to hold the Muse as still as possible, and relax your body. " - ]}

-
-
-
- LeftHand - RightHand -
- -
- F7Electrode - Image Source - EEG101 - - - -

{[ - "If you haven't already, connect your muse and try to plot your heart rate before moving on to the data recording and analysis below. ", - "Soon you should see spikes in voltage measured between those two electrodes each time your heart beats. ", - "Watch what happens when you move your body or fingers, and notice how delicate the signal is. " - ]}

-
-
- - -
-
-
- - - -

{[ - "Here time is on the horizontal axis, and the voltage is on the vertical axis. ", - "There are 10 Seconds of data shown, with the current time shown on the right. ", - "There is a vertical line every second so you can estimate your heart rate roughly. ", - "Below we will save data and use it to estimate your heart more accurately in a spreadsheet. ", - "the signal will be red if it is noisy, and when you relax and hold still it will turn green/black" - ]}

-
-
{renderCharts()}
-
-
-
- ); -} - -export function renderRecord(recordPopChange, recordPop, status, Settings) { - - const opts = { - height: '195', - width: '320', - playerVars: { // https://developers.google.com/youtube/player_parameters - autoplay: false - } - }; - - return ( - - - - -

{[ - "Clicking this button will immediately record a 10 second long segment of data just like it is shown in the plot above. ", - "Therefore, make sure the chart above looks clean and you can see you heart beat clearly before pressing record. ", - "We are going to compare two conditions that show a clear difference in heart rate due to blood pressure changes: ", - "Standing and Sitting. ", - "You will record two sessions, one standing and one sitting, pick the order randomly but keep track of which output file is which" - ]}

-
- - - - -

{[ - "A .csv file will be saved that can be opened in your favorite spreadsheet software like Microsoft Excel or in our examples, Google Sheets. ", - "Remember for each person at your computer to record two files, one while they are standing and one while they are sitting. ", - "Here is an example of what the data will look like once loaded. ", - "The top row shows the millsecond ellapsed in the data segment from 4 to 10,000. ", - "The bottom row shows the voltage measured from the difference between the two electrode, for each of those moments in time. ", - "The plot is plotting the data for each time point, this time with zero on the left. " - - ]}

-
- F7Electrode - -

{[ - "The following youtube video will show you how to open the file in Google Sheets, rename it, plot the data, find the peaks, ", - "Record them and find their difference in time, then take the average difference to estimate your average heart period. ", - "This value is then used to estimate your heart rate in beats per minute. " - ]} - - Link to example google sheet from video. - -

-
- -
-
- -
- -

{[ - "Finally each of you will enter this number for both sitting and standing into a google sheet that we are sharing as a class. ", - "We will use this shared google sheet which combines all our data in order to compute group statistics. " - ]}

-
-
-
-
- - - - -

- Your data is currently recording, - once complete it will be downloaded as a .csv file - and can be opened with your favorite spreadsheet program. - Close this window once the download completes. -

-
-
-
- -
- ); -} - -function saveToCSV(Settings) { - console.log('Saving ' + Settings.secondsToSave + ' seconds...'); - var localObservable$ = null; - const dataToSave = []; - - console.log('making ' + Settings.name + ' headers') - - - // for each module subscribe to multicast and make header - // take one sample from selected observable object for headers - localObservable$ = window.multicastHeartRaw$.pipe( - take(1) - ); - //take one sample to get header info - localObservable$.subscribe({ - next(x) { - dataToSave.push( - generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(t) {return t }) + ",", - "\n" - ); - } - }); - - // put selected observable object into local and start taking samples - localObservable$ = window.multicastHeartRaw$.pipe( - take(1) - ); - - // now with header in place subscribe to each epoch and log it - localObservable$.subscribe({ - next(x) { - console.log(x) - dataToSave.push(Object.values(x.data[1]).join(",") + "\n"); - // logging is useful for debugging -yup - // console.log(x); - }, - error(err) { console.log(err); }, - complete() { - console.log('Trying to save') - var blob = new Blob( - dataToSave, - {type: "text/plain;charset=utf-8"} - ); - saveAs(blob, Settings.name + "_Recording_" + Date.now() + ".csv"); - console.log('Completed'); - } - }); -} \ No newline at end of file diff --git a/src/components/PageSwitcher/components/EEGEduHeartRaw/LeftHand.png b/src/components/PageSwitcher/components/EEGEduHeartRaw/LeftHand.png deleted file mode 100644 index aa09c89f..00000000 Binary files a/src/components/PageSwitcher/components/EEGEduHeartRaw/LeftHand.png and /dev/null differ diff --git a/src/components/PageSwitcher/components/EEGEduHeartRaw/LimbLead.png b/src/components/PageSwitcher/components/EEGEduHeartRaw/LimbLead.png deleted file mode 100644 index 03c36bed..00000000 Binary files a/src/components/PageSwitcher/components/EEGEduHeartRaw/LimbLead.png and /dev/null differ diff --git a/src/components/PageSwitcher/components/EEGEduHeartRaw/PlotEKG_Sheets.png b/src/components/PageSwitcher/components/EEGEduHeartRaw/PlotEKG_Sheets.png deleted file mode 100644 index 38d97b67..00000000 Binary files a/src/components/PageSwitcher/components/EEGEduHeartRaw/PlotEKG_Sheets.png and /dev/null differ diff --git a/src/components/PageSwitcher/components/EEGEduHeartRaw/RightHand.png b/src/components/PageSwitcher/components/EEGEduHeartRaw/RightHand.png deleted file mode 100644 index 7c4c1a3e..00000000 Binary files a/src/components/PageSwitcher/components/EEGEduHeartRaw/RightHand.png and /dev/null differ diff --git a/src/components/PageSwitcher/components/EEGEduHeartRaw/electrodediagram2.png b/src/components/PageSwitcher/components/EEGEduHeartRaw/electrodediagram2.png deleted file mode 100644 index e6a7208f..00000000 Binary files a/src/components/PageSwitcher/components/EEGEduHeartRaw/electrodediagram2.png and /dev/null differ diff --git a/src/components/PageSwitcher/components/EEGEduHeartRaw/translations/en.json b/src/components/PageSwitcher/components/EEGEduHeartRaw/translations/en.json deleted file mode 100644 index c98ce5b1..00000000 --- a/src/components/PageSwitcher/components/EEGEduHeartRaw/translations/en.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "title": "Electrocardiogram (Heart Beats)", - "description": [ - "As a first introduction to measurement of electrical potentials from the body we will look at something accessible. ", - "As the heart beats and pumps blood throuhgouts our body, a series of electrical potentials are created, which can be measured using electrodes placed around the heart. ", - "This is referred to as the Electrocardiogram (ECG), and is best measured comparing the potential accross the left vs. right side of the body. ", - "Therefore, we can take off the muse and place a finger on one hand on the muse's reference electrode (in the center of the forehead). ", - "We can then place a finger of our opposite hand on one of the eeg electrodes. For this example pick the left forehead electrode. ", - "So place your left fingers pinching the left forehead electrode, and your right fingers pinching the center electrode. ", - "Rest the muse on the table as you do this, and relax your body. Soon you should see spikes in voltage measured between thsoe two electrodes each time your heart beats. " - ], - "xlabel": "Time (Seconds)", - "ylabel": "Voltage (\u03BCV)" -} diff --git a/src/components/PageSwitcher/components/EEGEduHeartSpectra/EEGEduHeartSpectra.js b/src/components/PageSwitcher/components/EEGEduHeartSpectra/EEGEduHeartSpectra.js deleted file mode 100644 index 7598c96e..00000000 --- a/src/components/PageSwitcher/components/EEGEduHeartSpectra/EEGEduHeartSpectra.js +++ /dev/null @@ -1,613 +0,0 @@ -import React from "react"; -import { catchError, multicast } from "rxjs/operators"; - -import { TextContainer, Card, Stack, RangeSlider, Button, ButtonGroup, Link, Modal } from "@shopify/polaris"; -import { saveAs } from 'file-saver'; -import { takeUntil } from "rxjs/operators"; -import { Subject, timer } from "rxjs"; - -import { channelNames } from "muse-js"; -import { Line } from "react-chartjs-2"; -import YouTube from 'react-youtube' - -import { zipSamples } from "muse-js"; - -import { - bandpassFilter, - epoch, - fft, - sliceFFT -} from "@neurosity/pipes"; - -import { chartStyles, generalOptions } from "../chartOptions"; - -import * as generalTranslations from "../translations/en"; -import * as specificTranslations from "./translations/en"; - -export function getSettings() { - return { - cutOffLow: .01, - cutOffHigh: 20, - interval: 100, - bins: 8192, - sliceFFTLow: 0.5, - sliceFFTHigh: 2.5, - duration: 2048, - srate: 256, - name: 'HeartSpectra', - secondsToSave: 10 - } -}; - - -export function buildPipe(Settings) { - if (window.subscriptionHeartHeartSpectra) window.subscriptionHeartSpectra.unsubscribe(); - - window.pipeHeartSpectra$ = null; - window.multicastHeartSpectra$ = null; - window.subscriptionHeartSpectra = null; - - // Build Pipe - window.pipeHeartSpectra$ = zipSamples(window.source.eegReadings$).pipe( - bandpassFilter({ - cutoffFrequencies: [Settings.cutOffLow, Settings.cutOffHigh], - nbChannels: window.nchans }), - epoch({ - duration: Settings.duration, - interval: Settings.interval, - samplingRate: Settings.srate - }), - fft({ bins: Settings.bins }), - sliceFFT([Settings.sliceFFTLow, Settings.sliceFFTHigh]), - catchError(err => { - console.log(err); - }) - ); - - window.multicastHeartSpectra$ = window.pipeHeartSpectra$.pipe( - multicast(() => new Subject()) - ); -} - -export function setup(setData, Settings) { - console.log("Subscribing to " + Settings.name); - - if (window.multicastHeartSpectra$) { - window.subscriptionHeartSpectra = window.multicastHeartSpectra$.subscribe(data => { - setData(heartSpectraData => { - Object.values(heartSpectraData).forEach((channel, index) => { - channel.datasets[0].data = data.psd[1]; - channel.xLabels = data.freqs.map(function(x) {return x * 60}); - }); - - return { - ch1: heartSpectraData.ch1, - }; - }); - }); - - window.multicastHeartSpectra$.connect(); - console.log("Subscribed to " + Settings.name); - } -} - -export function renderModule(channels) { - function renderCharts() { - return Object.values(channels.data).map((channel, index) => { - const options = { - ...generalOptions, - scales: { - xAxes: [ - { - scaleLabel: { - ...generalOptions.scales.xAxes[0].scaleLabel, - labelString: specificTranslations.xlabel - } - } - ], - yAxes: [ - { - scaleLabel: { - ...generalOptions.scales.yAxes[0].scaleLabel, - labelString: specificTranslations.ylabel - }, - ticks: { - min: 0 - } - } - ] - }, - elements: { - point: { - radius: 8 - } - }, - animation: { - duration: 200 - }, - title: { - ...generalOptions.title, - text: generalTranslations.channel + - channelNames[1] + - " - Estimated HR: " + - channel.peakF + " BPM" - } - }; - - if (index === 0) { - if (channel.xLabels) { - channel.peakInd = indexOfMax(channel.datasets[0].data); - channel.peakF = channel.xLabels[channel.peakInd]; - channel.peakVal = channel.datasets[0].data[channel.peakInd] - const newData = { - datasets: [{ - label: 'Peak', - borderColor: 'rgba(0,0,0)', - backgroundColor: 'rgba(231,41,138)', - data: [{ - x: channel.peakF, - y: channel.peakVal - }], - fill: false - }, { - label: channelNames[0], - borderColor: 'rgba(180,180,180)', - data: channel.datasets[0].data, - fill: true - } ], - xLabels: channel.xLabels - } - return ( - - - - ); - } else { - return( - - -

{[ - "Press connect above to see the chart." - ]} -

-
-
- ) - } - - } else { - return null - } - }); - } - - const opts = { - height: '195', - width: '320', - playerVars: { // https://developers.google.com/youtube/player_parameters - autoplay: false - } - }; - - return ( - - - - - -

{[ - "In the previous module we each estimated our heart rate in two conditions, while we were sitting, and while we were standing. ", - "We used a shared google sheet which combines all our data in order to compute group statistics. ", - "The following video shows how to use the data to make plots of the data, compute statistics, and test the difference. " - ]} - - A copy of the anonymized data that you can use to follow along with the video can be found here. - -

-
-
-
- -
- -

{[ - "There are a few problems with the way we estimated heart rate in the previous module. ", - "We used only a single 10 second segment of data which adds variability. ", - "We also used peak detection to find each heart beat, which takes alot of time and is difficult with noisy data. ", - "This required us to make arbirary thesholds to find peaks, and led to some poor estimates in heart rate, as indicated by the number of outliers we needed to remove (~15). ", - "Therefore in this module we will use some signal processing to get a better estimate of heart rate. " - ]}

-
-
-
- - - -

{[ - "Here is an example of some ECG data from our experiment in Module 2. ", - "Notice that just like many other aspects of our bodies function, our heart rate is a rhythm, that is, it repeats in time at regular intervals. " - ]}

-
- ECG - -

{[ - "Therefore, we can use mathematical techniques such as a fourier transform to estimate what frequency is present in the ECG data. ", - "A fourier transform turns any series of numbers into a summed set of sine waves of different sizes. ", - "The following animation shows how a single time-series of data, can be thought of as the sum of different frequencies of sine waves, each of a different magnitude. ", - "The blue line chart in the animation shows what is called the spectra, and indicates the power at each frequency." - ]}

-
-
- FFT -
- - Image Source - Wikipedia - -
- -

{[ - "Now we can use the muse to estimate our heart rates, but in different ways. Instead of looking at the voltage over time, ", - "We now transform the data to show us what frequencies are present in the continuous signal. ", - "In this frequency domain, we now ignore time and consider how much power of each frequency there is in a segment of data. ", - "If you place your fingers the same way you did when looking at your ECG, you should start to see a peak. ", - "It may help to return to Module 2 and look at the raw data first. " - ]}

-
- -

{[ - "If you missed Module 2, here are the instructions: You can take off the muse from your head and place a finger on your right hand on the muse's reference electrode (in the center of the forehead). ", - "We can then place a finger of our left hand on one of the eeg electrodes. Lets use the left forehead electrode (position AF7). ", - "So place your left fingers pinching the left forehead electrode, and your right fingers pinching the center electrode. ", - "Otherwise try to hold the Muse as still as possible, and relax your body. " - ]}

-
-
-
- LeftHand - RightHand -
- -
- F7Electrode - - Image Source - EEG101 - - -

{[ - "In this new plot, Along the horizontal axis is the beats per minute, or the frequency of the peaks in your ECG. ", - "The vertical y-axis shows the power of the rhythms in the data at each frequency, or how large the changes are between peak and through of the oscillations. ", - "The pink ball shows the estimated peak of the spectra, or your estimated heart rate. ", - "In the following experiment, it is only this pink value that will be saved over time while we record. ", - "Here is an example of what the plot should look like with a strong heart rate signal. ", - "Notice that there are two peaks, 60 BPM and 120 BPM. The larger peak is called the 1st Harmonic, a multiple of the true heart rate. ", - "These harmonics are common in the frequency domain: " - ]}

-
- exampleSpectra -
-
-
- - -
{renderCharts()}
-
-
-
- ); -} - -export function renderSliders(setData, setSettings, status, Settings) { - - function resetPipeSetup(value) { - buildPipe(Settings); - setup(setData, Settings) - } - - function handleIntervalRangeSliderChange(value) { - setSettings(prevState => ({...prevState, interval: value})); - resetPipeSetup(); - } - - function handleCutoffLowRangeSliderChange(value) { - setSettings(prevState => ({...prevState, cutOffLow: value})); - resetPipeSetup(); - } - - function handleCutoffHighRangeSliderChange(value) { - setSettings(prevState => ({...prevState, cutOffHigh: value})); - resetPipeSetup(); - } - - function handleSliceFFTLowRangeSliderChange(value) { - setSettings(prevState => ({...prevState, sliceFFTLow: value})); - resetPipeSetup(); - } - - function handleSliceFFTHighRangeSliderChange(value) { - setSettings(prevState => ({...prevState, sliceFFTHigh: value})); - resetPipeSetup(); - } - - function handleDurationRangeSliderChange(value) { - setSettings(prevState => ({...prevState, duration: value})); - resetPipeSetup(); - } - - return ( - - - - - - - - - ) -} - -export function renderRecord(recordPopChange, recordPop, status, Settings, setSettings) { - - function handleSecondsToSaveRangeSliderChange(value) { - setSettings(prevState => ({...prevState, secondsToSave: value})); - } - - const opts = { - height: '195', - width: '320', - playerVars: { // https://developers.google.com/youtube/player_parameters - autoplay: false - } - }; - - return( - - - -

{[ - "Clicking this button will immediately record the value of the pink dot above for ", - Settings.secondsToSave, - " seconds. ", - "Therefore, make sure the chart above looks clean and you can see a clear peak in the spectra before pressing record. ", - "We are going to compare two conditions that show a clear difference in heart rate due to blood pressure changes: ", - "Standing and Sitting. ", - "You will record two sessions, one standing and one sitting, pick the order randomly but keep track of which output file is which" - ]}

-
- - - - - -

{[ - "A .csv file will be saved that can be opened in Google Sheets. ", - "Remember for each person to record two files, one while standing and one while sitting. ", - "Here is an example of what the data will look like once loaded. ", - "The first column shows the time in msec of each estimate of heart rate. The second column shows the heart rate estimate in BPM. ", - "Each row represents an estimate from the previous 8 seconds of data. ", - "This is because you need a long segment of time to estimate frequency of rhythms over time. ", - "Each subsequent row is from an 8 second chunk of data, but is taken about 400 ms after the previous row. ", - "You can see the time values increase about 10000 ms during the recording, representing the 10 seconds of data. ", - "So 10000 milliseconds divided into ~400 ms shifts per row gives us the rough number of rows (~25). ", - "The graph shows the values plotted over time, showing that the 1st Harmonic value of 120 BPM was incorrectly recorded on a few windows. " - ]}

-
- exampleOutput - -

{[ - "In this module the analysis will be much easier, since much of the work has been done by the webpage with the fourier transform and peak detection. ", - "The following youtube video will show you how to open the file in Google Sheets, rename it, plot the data, remove any harmonics or outliers, ", - "then take the average heart rate over the window as an estimate of your heart rate. " - ]} - - - Link to example google sheet from video. - -

-
-
- - -

{[ - "Finally each of you will enter this estimated heart rate for both sitting and standing into a new anonymized google sheet that we are sharing as a class. ", - "We will use this shared google sheet which combines all our data in order to compute group statistics and to compare the two methods of estimating our heart rate. " - ]}

- -
    -
  1. Open up the Module 2&3 Anonymized Data Log and make a copy to work on:  - - https://docs.google.com/spreadsheets/d/1Ip8Xitp548DVXikZhL55Ll-Vgg9UAFU0-N40_3-e8sw/edit?usp=sharing - -
  2. -
  3. This time your report will focus on comparing the results from module 2 to module 3. Start by explaining that in your report, and explain what the difference was between the two modules measurement.
  4. -
  5. I have already removed outliers from Module 2, Why are there no outliers in Module 3? Include this in your report.
  6. -
  7. Recompute the statistics you did for the Module 2 data comparing sitting vs standing, and do the same for module 3. Compute the average, count, standard deviation, and standard error for each column. In our summary Report the mean and standard deviation in each of the four conditions. 
  8. -
  9. Notice how some participants only have a score in one module, let us ignore that for now.
  10. -
  11. Now for both module 2 and module 3, compute a paired samples t-test comparing sitting vs standing. (You have already done this for module 2). Report the two result in apa format (eg. - t(df) = 3.904; p = .003). 
  12. -
  13. Include bar graphs showing the sitting and standing HR in each of the four conditions.  
  14. -
  15. For both module 2 and module 3, make a new column and compute the difference in heart rate Sitting minus standing (Sitting - Standing). Report the average difference in each condition, as well as the standard deviation of the difference. Show a bar graph of the differences and their standard error.
  16. -
  17. In which module is the standard deviation larger? What does this mean
  18. It seems we found the same effect in module 2 and module 3. Both t-tests should be significant, but we also want to test how the scores on each module are related. 
  19. -
  20. The following two steps require individuals to have a score for all four conditions, so first REMOVE ROWS where individuals only had values from one of the modules. This should leave 65 individuals (remember the row names are on the first row). 
  21. -
  22. First we want to test if there is a difference between the differences. This would occur if our methods in module 2 and our methods in module 3 gave different changes in heart rate. Compute a t-test comparing the two difference columns. Report the results in APA format. Interpret the results.
  23. -
  24. Compute the correlation between the difference measured in module 2 and the difference measured in module 3, show a scatter plot comparing them as well. Interpret the strength of the relationship between the change we measured in module 2 and the change we measured in module 3.
  25. -
  26. Make a final concluding statement about which methods is better for estimating heart rate and why.
  27. -
- -
-
- - - - -

- Your data is currently recording, - once complete it will be downloaded as a .csv file - and can be opened with your favorite spreadsheet program. - Close this window once the download completes. -

-
-
-
-
-
- ) -} - - -function saveToCSV(Settings) { - console.log('Saving ' + Settings.secondsToSave + ' seconds...'); - var localObservable$ = null; - const dataToSave = []; - - console.log('making ' + Settings.name + ' headers') - - dataToSave.push( - "Timestamp (ms),", - "Estimated BPM", - "\n" - ); - - // Create timer - const timer$ = timer(Settings.secondsToSave * 1000); - - // put selected observable object into local and start taking samples - localObservable$ = window.multicastHeartSpectra$.pipe( - takeUntil(timer$) - ); - - // now with header in place subscribe to each epoch and log it - localObservable$.subscribe({ - next(x) { - dataToSave.push(Date.now() + "," + x.freqs[indexOfMax(x.psd[1])]*60 + "\n"); - // logging is useful for debugging -yup - console.log(); - }, - error(err) { console.log(err); }, - complete() { - console.log('Trying to save') - var blob = new Blob( - dataToSave, - {type: "text/plain;charset=utf-8"} - ); - saveAs(blob, Settings.name + "_Recording_" + Date.now() + ".csv"); - console.log('Completed'); - } - }); -} - -// Find the index of the max value in an array -function indexOfMax(arr) { - if (arr.length === 0) { - return -1; - } - var max = arr[0]; - var maxIndex = 0; - for (var i = 1; i < arr.length; i++) { - if (arr[i] > max) { - maxIndex = i; - max = arr[i]; - } - } - return maxIndex; -} diff --git a/src/components/PageSwitcher/components/EEGEduHeartSpectra/LeftHand.png b/src/components/PageSwitcher/components/EEGEduHeartSpectra/LeftHand.png deleted file mode 100644 index aa09c89f..00000000 Binary files a/src/components/PageSwitcher/components/EEGEduHeartSpectra/LeftHand.png and /dev/null differ diff --git a/src/components/PageSwitcher/components/EEGEduHeartSpectra/RightHand.png b/src/components/PageSwitcher/components/EEGEduHeartSpectra/RightHand.png deleted file mode 100644 index 7c4c1a3e..00000000 Binary files a/src/components/PageSwitcher/components/EEGEduHeartSpectra/RightHand.png and /dev/null differ diff --git a/src/components/PageSwitcher/components/EEGEduHeartSpectra/electrodediagram2.png b/src/components/PageSwitcher/components/EEGEduHeartSpectra/electrodediagram2.png deleted file mode 100644 index e6a7208f..00000000 Binary files a/src/components/PageSwitcher/components/EEGEduHeartSpectra/electrodediagram2.png and /dev/null differ diff --git a/src/components/PageSwitcher/components/EEGEduHeartSpectra/exampleECG.png b/src/components/PageSwitcher/components/EEGEduHeartSpectra/exampleECG.png deleted file mode 100644 index a40a5c07..00000000 Binary files a/src/components/PageSwitcher/components/EEGEduHeartSpectra/exampleECG.png and /dev/null differ diff --git a/src/components/PageSwitcher/components/EEGEduHeartSpectra/exampleOutput.png b/src/components/PageSwitcher/components/EEGEduHeartSpectra/exampleOutput.png deleted file mode 100644 index 89a50b1b..00000000 Binary files a/src/components/PageSwitcher/components/EEGEduHeartSpectra/exampleOutput.png and /dev/null differ diff --git a/src/components/PageSwitcher/components/EEGEduHeartSpectra/exampleSpectra.png b/src/components/PageSwitcher/components/EEGEduHeartSpectra/exampleSpectra.png deleted file mode 100644 index a069c168..00000000 Binary files a/src/components/PageSwitcher/components/EEGEduHeartSpectra/exampleSpectra.png and /dev/null differ diff --git a/src/components/PageSwitcher/components/EEGEduHeartSpectra/fft_animation.gif b/src/components/PageSwitcher/components/EEGEduHeartSpectra/fft_animation.gif deleted file mode 100644 index 352c95c1..00000000 Binary files a/src/components/PageSwitcher/components/EEGEduHeartSpectra/fft_animation.gif and /dev/null differ diff --git a/src/components/PageSwitcher/components/EEGEduHeartSpectra/translations/en.json b/src/components/PageSwitcher/components/EEGEduHeartSpectra/translations/en.json deleted file mode 100644 index dec7fa60..00000000 --- a/src/components/PageSwitcher/components/EEGEduHeartSpectra/translations/en.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "title": "Heart Rate (Beats per minute)", - "xlabel": "Heart Frequency (BPM)", - "ylabel": "Power (\u03BCV\u00B2)" -} diff --git a/src/components/PageSwitcher/components/EEGEduIntro/EEGEduIntro.js b/src/components/PageSwitcher/components/EEGEduIntro/EEGEduIntro.js deleted file mode 100644 index 14e135ee..00000000 --- a/src/components/PageSwitcher/components/EEGEduIntro/EEGEduIntro.js +++ /dev/null @@ -1,345 +0,0 @@ -import React from "react"; -import { catchError, multicast } from "rxjs/operators"; -import { Subject } from "rxjs"; - -import { Card, Link } from "@shopify/polaris"; - -import { Line } from "react-chartjs-2"; - -import { zipSamples } from "muse-js"; - -import { - bandpassFilter, - epoch -} from "@neurosity/pipes"; - -import { chartStyles, generalOptions } from "../chartOptions"; - -import * as specificTranslations from "./translations/en"; - -import { generateXTics, standardDeviation } from "../../utils/chartUtils"; - -export function getSettings () { - return { - name: "Intro", - cutOffLow: 2, - cutOffHigh: 20, - interval: 2, - srate: 256, - duration: 512 - } -}; - -export function buildPipe(Settings) { - if (window.subscriptionIntro$) window.subscriptionIntro$.unsubscribe(); - - window.pipeIntro$ = null; - window.multicastIntro$ = null; - window.subscriptionIntro = null; - - // Build Pipe - window.pipeIntro$ = zipSamples(window.source.eegReadings$).pipe( - bandpassFilter({ - cutoffFrequencies: [Settings.cutOffLow, Settings.cutOffHigh], - nbChannels: window.nchans }), - epoch({ - duration: Settings.duration, - interval: Settings.interval, - samplingRate: Settings.srate - }), - catchError(err => { - console.log(err); - }) - ); - window.multicastIntro$ = window.pipeIntro$.pipe( - multicast(() => new Subject()) - ); -} - -export function setup(setData, Settings) { - console.log("Subscribing to " + Settings.name); - - if (window.multicastIntro$) { - window.subscriptionIntro = window.multicastIntro$.subscribe(data => { - setData(introData => { - Object.values(introData).forEach((channel, index) => { - if (index === 0) { - channel.datasets[0].data = data.data[index]; - channel.xLabels = generateXTics(Settings.srate, Settings.duration); - channel.datasets[0].qual = standardDeviation(data.data[index]) - } - }); - - return { - ch0: introData.ch0 - }; - }); - }); - - window.multicastIntro$.connect(); - console.log("Subscribed to " + Settings.name); - } -} - -export function renderModule(channels) { - function renderCharts() { - return Object.values(channels.data).map((channel, index) => { - const options = { - ...generalOptions, - scales: { - xAxes: [ - { - scaleLabel: { - ...generalOptions.scales.xAxes[0].scaleLabel, - labelString: specificTranslations.xlabel - } - } - ], - yAxes: [ - { - scaleLabel: { - ...generalOptions.scales.yAxes[0].scaleLabel, - labelString: specificTranslations.ylabel - }, - ticks: { - max: 300, - min: -300 - } - } - ] - }, - elements: { - line: { - borderColor: 'rgba(' + channel.datasets[0].qual*10 + ', 128, 128)', - fill: false - }, - point: { - radius: 0 - } - }, - animation: { - duration: 0 - }, - title: { - ...generalOptions.title, - text: 'Voltage signal over time' - } - }; - - if (index === 0) { - return ( - - - - ); - } else { - return null - }; - }); - } - - return ( - - - - -

- {specificTranslations.intro1} -

-
- {renderCharts()} -
-

- {specificTranslations.intro2} -

-
-
- - - -

- {specificTranslations.neurons1} -

- Single Neuron - Image Source - EEG101 -
-
-

- {specificTranslations.neurons2} -

- Multiple Neurons - Image Source - EEG101 -
-
-

- {specificTranslations.neurons3} -

-
-
- - - -

- {specificTranslations.oscillations1} -

- Awake/Asleep - Image Source - EEG101 -
-
-

- {specificTranslations.oscillations2} -

-
-
- - - -

- {specificTranslations.hardware1} -
-
- {specificTranslations.hardware2} -

-
- electrode locations -
-
- Image Source - EEG101 -
-
-

- {specificTranslations.hardware3} -
-
- {specificTranslations.hardware4} -
- DAQ diagram - Image Source - Wikipedia -
-
- {specificTranslations.hardware5} -

-
-
- - - -

- {specificTranslations.muse1} -
-
- Awake/Asleep -
-
- Image Source - @urish -
-
- {specificTranslations.muse2} -
- Muse Electrodes -
- ElectrodeLegend -
-
- Image Source - EEG101 -
-
- {specificTranslations.muse3} -

-
-
- - - -

- {specificTranslations.signal1} -
- SingleElectrode -
- Image Source - EEG101 -
-
-

-
- {renderCharts()} -
-

- {specificTranslations.signal2} -

-
-
- - - - -

- {specificTranslations.credits1} - NeurotechEdu. -

-

- {specificTranslations.credits2} - Interaxon. -

-

- {specificTranslations.credits3} - muse-js - {specificTranslations.credits4} - A Techy's Introduction to Neuroscience. -

-

- {specificTranslations.credits5} - eeg-pipes - {specificTranslations.credits6} - Muse 2016 Headband + Web Bluetooth. -

-
-
- -
- ); -} diff --git a/src/components/PageSwitcher/components/EEGEduIntro/assets/DigitalDAQv2.png b/src/components/PageSwitcher/components/EEGEduIntro/assets/DigitalDAQv2.png deleted file mode 100644 index be4a4ed0..00000000 Binary files a/src/components/PageSwitcher/components/EEGEduIntro/assets/DigitalDAQv2.png and /dev/null differ diff --git a/src/components/PageSwitcher/components/EEGEduIntro/assets/awakeasleep.gif b/src/components/PageSwitcher/components/EEGEduIntro/assets/awakeasleep.gif deleted file mode 100644 index ed2d7f26..00000000 Binary files a/src/components/PageSwitcher/components/EEGEduIntro/assets/awakeasleep.gif and /dev/null differ diff --git a/src/components/PageSwitcher/components/EEGEduIntro/assets/electrodediagram.png b/src/components/PageSwitcher/components/EEGEduIntro/assets/electrodediagram.png deleted file mode 100644 index 4befe9b5..00000000 Binary files a/src/components/PageSwitcher/components/EEGEduIntro/assets/electrodediagram.png and /dev/null differ diff --git a/src/components/PageSwitcher/components/EEGEduIntro/assets/electrodediagram1.png b/src/components/PageSwitcher/components/EEGEduIntro/assets/electrodediagram1.png deleted file mode 100644 index ad1b00f2..00000000 Binary files a/src/components/PageSwitcher/components/EEGEduIntro/assets/electrodediagram1.png and /dev/null differ diff --git a/src/components/PageSwitcher/components/EEGEduIntro/assets/electrodelegend.png b/src/components/PageSwitcher/components/EEGEduIntro/assets/electrodelegend.png deleted file mode 100644 index 6f31adc2..00000000 Binary files a/src/components/PageSwitcher/components/EEGEduIntro/assets/electrodelegend.png and /dev/null differ diff --git a/src/components/PageSwitcher/components/EEGEduIntro/assets/electrodelocations.png b/src/components/PageSwitcher/components/EEGEduIntro/assets/electrodelocations.png deleted file mode 100644 index 79140f46..00000000 Binary files a/src/components/PageSwitcher/components/EEGEduIntro/assets/electrodelocations.png and /dev/null differ diff --git a/src/components/PageSwitcher/components/EEGEduIntro/assets/musepicture.png b/src/components/PageSwitcher/components/EEGEduIntro/assets/musepicture.png deleted file mode 100644 index 542e205c..00000000 Binary files a/src/components/PageSwitcher/components/EEGEduIntro/assets/musepicture.png and /dev/null differ diff --git a/src/components/PageSwitcher/components/EEGEduIntro/assets/neuronarrow.png b/src/components/PageSwitcher/components/EEGEduIntro/assets/neuronarrow.png deleted file mode 100644 index 521a8f37..00000000 Binary files a/src/components/PageSwitcher/components/EEGEduIntro/assets/neuronarrow.png and /dev/null differ diff --git a/src/components/PageSwitcher/components/EEGEduIntro/assets/neuronmultiarrow.png b/src/components/PageSwitcher/components/EEGEduIntro/assets/neuronmultiarrow.png deleted file mode 100644 index b44452df..00000000 Binary files a/src/components/PageSwitcher/components/EEGEduIntro/assets/neuronmultiarrow.png and /dev/null differ diff --git a/src/components/PageSwitcher/components/EEGEduIntro/translations/en.json b/src/components/PageSwitcher/components/EEGEduIntro/translations/en.json deleted file mode 100644 index 2f659ed9..00000000 --- a/src/components/PageSwitcher/components/EEGEduIntro/translations/en.json +++ /dev/null @@ -1,129 +0,0 @@ -{ - "title": "Introduction", - "intro1": [ - "Below you are now looking at an live measurement of the electrical potential created by your brain. ", - "Just like a AA battery stores 1.5 volts of electrical potential energy accross its positive and negative leads, ", - "two points on your head also have a much smaller electrical potential when measured accross them. ", - "Here we are watching that live, with the voltage on the vertical axis (\u03BCV are microvolts, 1 million microvolts are in a volt). ", - "Time is shown in milleseconds along the horizontal axis, with the right side of the chart being the current moment:" - ], - "intro2": [ - "Read on to find out where this electrical potential comes from and what it means. " - ], - "neuronsHead": "Neurons", - "neurons1": [ - "The brain is made up of cells called neurons. ", - "Neurons communicate using chemical messages that change the electrical potential of the cells they connect with. ", - "This change in electrical potential, if large enough, can make those cells send messages as well, and so on. ", - "For example, an excitatory neuron releases Glutamate on another neuron, which lets in positively charged Sodium ions into the cell and make its interior less negative compared to outside. ", - "These changes in electrical potential create small electrical fields, which act as tiny electrical dipoles like batteries. " - ], - "neurons2": [ - "The electrical potential accross the cell membrane is small, around -70 mV at rest (1000 microvolts in a millivolt), and it changes around -20 mV during electrical changes in the cell. ", - "However, if a large group of these tiny dipoles are aligned in space and their electrical potentials change at the same time, ", - "they can create electrical potentials which are large enough to conduct through the brain tissue and be measurable comparing different points on the head. " - ], - "neurons3": [ - "As you can see above, these electrical potentials measured on the outside of the head fluctuature between about -200 and 200 \u03BCV. ", - "You can also see cycles between high and low voltage, called oscillations, which can occur in the human brain at a number of frequencies. ", - "You may have heard of some of these brain waves before, but what do they mean? " - ], - "oscillationsHead": "Oscillations", - "oscillations1": [ - "Large groups of aligned neurons are all becoming more and less active together in groups. ", - "And these fluctuations in activity seem to occur within certain frequency bands. ", - "It has been proposed that these different frequencies of neural activity serve functional mechanisms in the brain. ", - "That is, one of the ways the brain uses to process and communicate information is through these rhythmic processes. ", - "These oscillations can change during different behaviours. One of the most drastic is the difference in the EEG when we fall asleep: " - ], - "oscillations2": [ - "When we are awake, our EEG signal is dominated by high frequency activity called Beta waves. ", - "When we fall asleep, our brain slows down. Larger and larger groups of neurons all fire together, in slow oscillations called Delta waves. ", - "We will learn more in the modules on the frequency spectra and frequency bands about what the various oscillations represent and how they change. ", - "We use the power of these brain waves to provide real time feedback about the state of your brain and practice Neurofeedback applications. ", - "We can also use these measures to control oscillations, or control some Brain Machine Interfaces (BMIs). ", - "But first, a little more about the tecnology we are using to measure and vizualize these signals. " - ], - "hardwareHead": "EEG Hardware", - "hardware1": [ - "Because the brain has a great deal of salty water in it, it conducts electricity. ", - "This electrical field gets smeared by the slightly electrically resistive skull and scalp. ", - "Therefore the signal on the outside of the head has very little spatial information about where the signal came from. ", - "Even worse, any potential measured on the head could have an infinite number of dipole configurations inside the head creating it. ", - "Nonetheless, there are still difference in voltage between different parts of the head that may be interesting. " - ], - "hardware2": [ - "To measure the spatial distribution of the voltage signals, EEG is traditionally placed in a regular grid of electrode locations covering the surface of the head. ", - "Each location is given a name, with the letter indicating the location of the head (F-Frontal; C-Central; P-Parietal; T-Temporal; O-Occipital; Fp-Fronto-polar). ", - "The suffix has a z if along the midline, odd numbers over the left hemisphere, and even over the right. ", - "Numbers start along the midline and get larger for more lateral sites on the head. " - ], - "hardware3": [ - "Voltage is electrical potential, and like a battery, is measured as the difference between two locations. ", - "In the case of EEG, we use a reference electrode, shown here in black, to compare each of the other electrode locations against. ", - "The EEG device must therefore measure the difference in voltage at each of its sensors compared to some reference location. ", - "It must then amplify this very small signal, and convert this voltage to some signal that can be saved by a computer (digitization), and in the case of wireless EEG, transmit the signal. ", - "A computer must then receive this signal, and display it, process it, or save it for later analysis. " - ], - "hardware4": [ - "The amplification and digitization turns the continuous voltage into a digitized signal. ", - "This signal now has descrete time steps and descrete difference in voltage. ", - "The hardware's sampling rate controls how many samples of voltage per second (in Hz) are recorded. For example Muse 2 records 256 samples per second. ", - "The digitization's bit depth, or how many memory bits are used to represent each voltage value, influence the smallest change in voltage that a system can measure. ", - "Finally, since there are multiple electrode locations, these individual signals need to be digitized quickly one after another each recording cycle, this is called mulitplexing. " - ], - "hardware5": [ - "One important consideration is the electrical conductivity between the head and the sensor. ", - "An electrode is a conductive piece of material that takes the voltage difference between locations on the head and transmits it along a wire to the amplifier/digitizer. ", - "The signal will therefore be greatly affected by the conductivity of the electrode to head connection. ", - "The inverse of conductivity we call electrical resistance, and since the EEG oscillates like an alternating current power source, we call this impedance. ", - "Notice that the muse uses two different types of sensor material, gold on the forehead, and conductive rubber behind the ears. " - ], - "museHead": "Interaxon Muse EEG", - "muse1": [ - "A decade ago, before the revolution in wireless and battery powered electronics, EEG devices were large and combersome. ", - "EEG devices reqired large amplifiers and digitizers, with dedicated power supplies, and desktop computers for data recording and analysis. ", - "Computing limitations limited live data processing and experimentation. ", - "Within the last decade, a series of new consumer focused EEG devices have been devleoped, drastically reducing the price and portability of the technology. ", - "One of the most common is the Muse and Muse 2 created by Toronto based Interaxon Inc. " - ], - "muse2": [ - "The Muse is sold as an interactive mediation device, for under 300$ US. ", - "Researchers have compared the signals with traditional expensive EEG devices and found very positive results. ", - "Therefore the muse makes for an excellent teaching tool to integrate real time brain measurement into the classroom. ", - "The muse records EEG data at 256 Hz, from four electrode locations shown here: " - ], - "muse3": [ - "In the subsequent modules in this EEGEdu tutorial, you will use the live data from these four electrode locations. " - ], - "signalHead": "EEG Signal", - "signal1": [ - "So now you have some background on how this electrical signal from your brain is generated. ", - "This particular signal is from behind your left ear, Electrode 1 at TP9. ", - "As the amplitude of the noise in the signal decreases, the line should get darker. " - ], - "signal2": [ - "After playing around with this live signal you are ready to move onto some modules, select one from the menu above. " - ], - "creditsHead": "Credits", - "credits1": [ - "EEGEdu is an open source collaborative project with NeurotechX's " - ], - "credits2": [ - "This is also created in collaboration with " - ], - "credits3": [ - "This online tutorial is made using a Muse connection by Web Bluetooth using " - ], - "credits4": [ - "by Uri Shaked who has an excellent introduction to EEG " - ], - "credits5": [ - "The data is processed using Neurosity's " - ], - "credits6": [ - "by Alex Castillo who also has an excellent post about EEG and the web called " - ], - "xlabel": "Time (msec)", - "ylabel": "Voltage (\u03BCV)" -} diff --git a/src/components/PageSwitcher/components/EEGEduPredict/EEGEduPredict.js b/src/components/PageSwitcher/components/EEGEduPredict/EEGEduPredict.js deleted file mode 100644 index 278da6e2..00000000 --- a/src/components/PageSwitcher/components/EEGEduPredict/EEGEduPredict.js +++ /dev/null @@ -1,251 +0,0 @@ -import React from "react"; -import { catchError, multicast } from "rxjs/operators"; - -import { TextContainer, Card, Stack, Button, ButtonGroup } from "@shopify/polaris"; -import { Subject } from "rxjs"; - -import { zipSamples } from "muse-js"; - -import { - bandpassFilter, - epoch, - fft, - sliceFFT -} from "@neurosity/pipes"; - -import { chartStyles } from "../chartOptions"; - -import * as generalTranslations from "../translations/en"; -import * as specificTranslations from "./translations/en"; - -import P5Wrapper from 'react-p5-wrapper'; -import sketchPredict from './sketchPredictSound'; - -import ml5 from 'ml5' - -let knnClassifier = ml5.KNNClassifier(); - - -export function getSettings() { - return { - cutOffLow: 2, - cutOffHigh: 20, - interval: 256, - bins: 256, - sliceFFTLow: 1, - sliceFFTHigh: 30, - duration: 512, - srate: 256, - name: 'Predict' - } -}; - -export function buildPipe(Settings) { - if (window.subscriptionPredict) window.subscriptionPredict.unsubscribe(); - - window.pipePredict$ = null; - window.multicastPredict$ = null; - window.subscriptionPredict = null; - - // Build Pipe - window.pipePredict$ = zipSamples(window.source.eegReadings$).pipe( - bandpassFilter({ - cutoffFrequencies: [Settings.cutOffLow, Settings.cutOffHigh], - nbChannels: window.nchans }), - epoch({ - duration: Settings.duration, - interval: Settings.interval, - samplingRate: Settings.srate - }), - fft({ bins: Settings.bins }), - sliceFFT([Settings.sliceFFTLow, Settings.sliceFFTHigh]), - catchError(err => { - console.log(err); - }) - ); - - window.multicastPredict$ = window.pipePredict$.pipe( - multicast(() => new Subject()) - ); -} - -export function setup(setData, Settings) { - console.log("Subscribing to " + Settings.name); - - if (window.multicastPredict$) { - window.subscriptionPredict = window.multicastPredict$.subscribe(data => { - setData(predictData => { - Object.values(predictData).forEach((channel, index) => { - channel.datasets[0].data = data.psd[index]; - channel.xLabels = data.freqs; - }); - - return { - ch0: predictData.ch0, - ch1: predictData.ch1, - ch2: predictData.ch2, - ch3: predictData.ch3, - ch4: predictData.ch4 - }; - }); - }); - - window.multicastPredict$.connect(); - console.log("Subscribed to " + Settings.name); - } -} - -export function renderModule(channels) { - function renderCharts() { - return Object.values(channels.data).map((channel, index) => { - if (index === 0) { - - if (channel.datasets[0].data) { - window.psd = channel.datasets[0].data; - window.freqs = channel.xLabels; - if (channel.xLabels) { - window.bins = channel.xLabels.length; - } - } - return null - } else { - return null - } - }); - } - - return ( - - - - -

{specificTranslations.description}

-
-
-
{renderCharts()}
-
-
- ); -} - -export function renderSliders(setData, setSettings, status, Settings) { - return null -} - -// Classification algorithm (using renderRecord function) -window.exampleCounts = {A: 0, B: 0, C: 0}; -window.thisLabel = 'A'; -window.confidences = {A: 1, B: 0, C: 0}; - -window.isPredicting = false; -window.enoughLabels = false; - -export function renderRecord(recordPopChange, status) { - - // Adds example from current incoming psd - function addExample (label) { - if (window.psd) { - knnClassifier.addExample(window.psd, label); - window.exampleCounts[label]++; - - const numLabels = knnClassifier.getNumLabels(); - if (numLabels === 3) { - window.enoughLabels = true; - } - } - } - - // Classifies current incoming psd and outputs results - function classify () { - window.isPredicting = true; - knnClassifier.classify(window.psd, gotResults) - } - - // callback from classify to assign results to window and recurse - function gotResults(err, result) { - if (result.confidencesByLabel) { - window.confidences = result.confidencesByLabel; - if (result.label) { - switch (result.label) { - case 'A': - window.thisLabel = 'A'; - break; - case 'B': - window.thisLabel = 'B'; - break; - case 'C': - window.thisLabel = 'C'; - break; - default: - console.log('error with prediction label'); - } - } - } - classify(); //recursive so it continues to run - } - - //buttons for training at prediction - return( - - - - - - - - - - - - - - - - -
- -

{'Click or tap on the rectangle to toggle sound'}

-
- - - -
-
- -
- ) -} \ No newline at end of file diff --git a/src/components/PageSwitcher/components/EEGEduPredict/sketchPredict.js b/src/components/PageSwitcher/components/EEGEduPredict/sketchPredict.js deleted file mode 100644 index 31327a0a..00000000 --- a/src/components/PageSwitcher/components/EEGEduPredict/sketchPredict.js +++ /dev/null @@ -1,44 +0,0 @@ -export default function sketchPredict (p) { - - let label; - let confidence; - - p.setup = function () { - p.createCanvas(p.windowWidth*.6, 300); - - }; - - p.windowResized = function() { - p.resizeCanvas(p.windowWidth*.6, 300); - } - - p.myCustomRedrawAccordingToNewPropsHandler = function (props) { - label = props.label; - confidence = props.confidences[label]; - }; - - p.draw = function () { - p.background(250, 250, 150); - p.fill(0); - p.strokeWeight(5); - p.line(p.width/2, 0, p.width/2, p.height); - p.textSize(30); - p.text('A', p.width/4, 30); - p.text('B', p.width-p.width/4, 30) - if (label === 'A') { - p.fill(120, 120, 250); - if (confidence > .8) { - p.ellipse(p.width/6, p.height/2, 60); - } else { - p.ellipse(p.width/3, p.height/2, 20); - } - } else { - p.fill(120, 250, 120); - if (confidence > .8) { - p.ellipse(p.width-p.width/6, p.height/2, 60); - } else { - p.ellipse(p.width-p.width/3, p.height/2, 20); - } - } - } -}; \ No newline at end of file diff --git a/src/components/PageSwitcher/components/EEGEduPredict/sketchPredictSound.js b/src/components/PageSwitcher/components/EEGEduPredict/sketchPredictSound.js deleted file mode 100644 index ff9b9af4..00000000 --- a/src/components/PageSwitcher/components/EEGEduPredict/sketchPredictSound.js +++ /dev/null @@ -1,89 +0,0 @@ -import p5 from "p5"; -import "p5/lib/addons/p5.sound"; - - - export default function sketchPredict (p) { - - let label; - let confidence; - let osc; - let soundOn = false; - - p.setup = function () { - p.createCanvas(p.windowWidth*.6, 300); - p.background(255); - osc = new p5.Oscillator(); - osc.setType('sine'); - }; - - p.startSound = function () { - osc.freq(262); - osc.amp(0); - osc.start(); - } - - p.mouseClicked = function () { - if (p.mouseX > 1 && p.mouseX < p.windowWidth*.6 && p.mouseY > 1 && p.mouseY < 300) { - if (soundOn) { - osc.amp(0, .05) - osc.stop(); - soundOn = false; - } else { - p.startSound(); - soundOn = true; - } - } - } - - p.windowResized = function() { - p.resizeCanvas(p.windowWidth*.6, 300); - } - - p.myCustomRedrawAccordingToNewPropsHandler = function (props) { - label = props.label; - confidence = props.confidences[label]; - }; - - // The notes C, E, and G have frequencies in the ratio of 4:5:6. - // When they are played together, the three notes blend very well - // and are pleasant to the ear; these notes form a major triad or a major chord. - - p.draw = function () { - p.fill(0); - p.strokeWeight(5); - if (label === 'A') { - if (confidence > .5) { - p.fill(120, 120, 250); - osc.amp(1, .1); - osc.freq(262); - } else { - p.fill(240, 240, 255); - osc.amp(.25, .1); - osc.freq(262); - } - } else if (label === 'B') { - if (confidence > .5) { - p.fill(120, 250, 120); - osc.amp(1, .1); - osc.freq(327.5); - } else { - p.fill(240, 255, 240); - osc.amp(.25, .1) - osc.freq(327.5); - } - } else { - if (confidence > .5) { - p.fill(250, 120, 120); - osc.amp(1, .1); - osc.freq(393); - } else { - p.fill(255, 240, 240); - osc.amp(.25, .1) - osc.freq(393); - } - - } - p.rect(20,20,p.windowWidth*.5,200); - - } -}; \ No newline at end of file diff --git a/src/components/PageSwitcher/components/EEGEduPredict/translations/en.json b/src/components/PageSwitcher/components/EEGEduPredict/translations/en.json deleted file mode 100644 index f1f9ef01..00000000 --- a/src/components/PageSwitcher/components/EEGEduPredict/translations/en.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "title": "Predict brain states with a trained classifier", - "description": [ - "In the next module we will train and test classifiers of brain data like we have been looking at so far. ", - "We will collect data in two different conditions, with the goal of inducing two different brain states. ", - "We will then train a classifier based on the pattern of activity over time, frequency, and space on the head. ", - "We will then use the classifier to predict on real time which of the two brain states are currently happening. ", - "That is, we will attempt to predict if peoples brain activity more closely resembles condition A or conditoin B ", - "Of the training data. " - ], - "xlabel": "Frequency (Hz)", - "ylabel": "Power (\u03BCV\u00B2)" -} diff --git a/src/components/PageSwitcher/components/EEGEduRaw/EEGEduRaw.js b/src/components/PageSwitcher/components/EEGEduRaw/EEGEduRaw.js deleted file mode 100644 index 3cbecaf2..00000000 --- a/src/components/PageSwitcher/components/EEGEduRaw/EEGEduRaw.js +++ /dev/null @@ -1,580 +0,0 @@ - import React from "react"; -import { catchError, multicast } from "rxjs/operators"; -import { Subject, timer } from "rxjs"; - -import { TextContainer, Card, Stack, RangeSlider, Button, ButtonGroup, Modal, Link } from "@shopify/polaris"; -import { saveAs } from 'file-saver'; -import { take, takeUntil } from "rxjs/operators"; - -import { channelNames } from "muse-js"; -import { Line } from "react-chartjs-2"; - -import { zipSamples } from "muse-js"; -import YouTube from 'react-youtube' - -import { - bandpassFilter, - epoch -} from "@neurosity/pipes"; - -import { chartStyles, generalOptions } from "../chartOptions"; - -import * as generalTranslations from "../translations/en"; -import * as specificTranslations from "./translations/en"; - -import { generateXTics, standardDeviation } from "../../utils/chartUtils"; - -export function getSettings () { - return { - cutOffLow: .1, - cutOffHigh: 100, - interval: 25, - srate: 256, - duration: 1024, - name: 'Raw', - secondsToSave: 10 - } -}; - -export function buildPipe(Settings) { - if (window.subscriptionRaw) window.subscriptionRaw.unsubscribe(); - - window.pipeRaw$ = null; - window.multicastRaw$ = null; - window.subscriptionRaw = null; - - // Build Pipe - window.pipeRaw$ = zipSamples(window.source.eegReadings$).pipe( - bandpassFilter({ - cutoffFrequencies: [Settings.cutOffLow, Settings.cutOffHigh], - nbChannels: window.nchans }), - epoch({ - duration: Settings.duration, - interval: Settings.interval, - samplingRate: Settings.srate - }), - catchError(err => { - console.log(err); - }) - ); - window.multicastRaw$ = window.pipeRaw$.pipe( - multicast(() => new Subject()) - ); -} - -export function setup(setData, Settings) { - console.log("Subscribing to " + Settings.name); - - if (window.multicastRaw$) { - window.subscriptionRaw = window.multicastRaw$.subscribe(data => { - setData(rawData => { - Object.values(rawData).forEach((channel, index) => { - channel.datasets[0].data = data.data[index]; - channel.xLabels = generateXTics(Settings.srate, Settings.duration); - channel.datasets[0].qual = standardDeviation(data.data[index]) - }); - - return { - ch0: rawData.ch0, - ch1: rawData.ch1, - ch2: rawData.ch2, - ch3: rawData.ch3, - ch4: rawData.ch4 - }; - }); - }); - - window.multicastRaw$.connect(); - console.log("Subscribed to Raw"); - } -} - -export function renderModule(channels) { - function renderCharts() { - const options = { - ...generalOptions, - scales: { - xAxes: [ - { - scaleLabel: { - ...generalOptions.scales.xAxes[0].scaleLabel, - labelString: specificTranslations.xlabel - } - } - ], - yAxes: [ - { - scaleLabel: { - ...generalOptions.scales.yAxes[0].scaleLabel, - labelString: specificTranslations.ylabel - }, - } - ] - }, - animation: { - duration: 0 - }, - title: { - ...generalOptions.title, - text: 'Raw data from EEG electrdoes' - }, - legend: { - display: true - } - }; - - if (channels.data.ch3.datasets[0].data) { - const newData = { - datasets: [{ - label: channelNames[0], - borderColor: 'rgba(217,95,2, ' + Math.max(0.2,(2-channels.data.ch0.datasets[0].qual/200)) + ')', - data: channels.data.ch0.datasets[0].data.map(function(x) {return x + 300}), - fill: false - }, { - label: channelNames[1], - borderColor: 'rgba(27,158,119, ' + Math.max(0.2,(2-channels.data.ch1.datasets[0].qual/200)) + ')', - data: channels.data.ch1.datasets[0].data.map(function(x) {return x + 200}), - fill: false - }, { - label: channelNames[2], - borderColor: 'rgba(117,112,179, ' + Math.max(0.2,(2-channels.data.ch2.datasets[0].qual/200)) + ')', - data: channels.data.ch2.datasets[0].data.map(function(x) {return x + 100}), - fill: false - }, { - label: channelNames[3], - borderColor: 'rgba(231,41,138, ' + Math.max(0.2,(2-channels.data.ch3.datasets[0].qual/200)) + ')', - data: channels.data.ch3.datasets[0].data.map(function(x) {return x + 0}), - fill: false - }, { - label: channelNames[4], - borderColor: 'rgba(20,20,20, ' + Math.max(0.2,(2-channels.data.ch4.datasets[0].qual/200)) + ')', - data: channels.data.ch4.datasets[0].data.map(function(x) {return x + -100}), - fill: false - }], - xLabels: channels.data.ch0.xLabels - } - - return ( - - - - ); - } else { - return( - - -

{[ - "Press connect above to see the chart." - ]} -

-
-
- ) - } - } - - const opts = { - height: '195', - width: '320', - playerVars: { // https://developers.google.com/youtube/player_parameters - autoplay: false - } - }; - - return ( - - - - -

{[ - "Next we will move onto the head and look at the raw EEG data that is recorded and transmitted by the EEG headset. ", - "If you have not already, spend a few minutes going through the Introduction Module 1, which will give you an intro into the EEG signal. " - ]}

-

{specificTranslations.description}

-
- exampleSpectra -
-
- Image Source - EEG101 -
{renderCharts()}
- -
- - -

{[ - "If you have not already, it is now time to place the muse on your head. ", - "Getting a good connection between the device and your head is crucial. ", - "The following video gives tips on how to get a great connection: " - ]}

- -

{[ - "The above chart shows the data coming from each of the four eeg electrodes. ", - "Time, again, is on the horizontal X-axis and voltage is on the Y-axis. ", - "An offset has been added to the electrodes to separate them vertically. ", - "The saturation of the lines is controlled by the amount of noise in the data. ", - "That is, the cleaner the data, the more rich the colours will become. ", - "Conversely, when the line gets dim, there is too much noise present in the signal. ", - "I have set the filter settings above very loose (.1 to 100 Hz) to let in as much noise as possible so we can learn, we will restrict them in future modules. " - ]}

-
-
- - -

{[ - "Before we try to use the EEG to estimate brain activity, we first need to observe what other things can influence the signal. ", - "Artifacts refer to non-EEG noise in the EEG recording that will cloud the results we want from the brain. ", - "There are many sources of noise that the EEG can pickup because all it is doing is recording the voltage changes on the head. ", - "Any other source of voltage or change in resistance of the sensor can affect the signal. ", - "Here is a great overview of the range of possible EEG artifacts that you might come accross. " - ]}

-
-
- -
- EEG artifacts - from - Sudhakar Marella -
-
- - -

{[ - "Your assignment this week will be to record plots of four different types of EEG artifact. ", - "I will show you examples that I made using screenshots of the plot above. ", - "To take a screenshot on a mac, press (⌘Command + ⇧Shift + 4) and select the area of the screen you want to save. ", - "You can also record data into a .csv file below and plot artifacts using Google Sheets. ", - "After making your plots, play around with the filter settings above (cutoff Low and cutoff High) and observe how they change the plotted data. ", - "How can we use these filters to help collect cleaner data free from some of these artifacts?" - ]}

-
- -
- - - -

{[ - "The eye balls create an electrical field, as they move around they create electrical potentials that are picked up by the EEG sensors. ", - "Also, the eye lids, as the pass over the eye, create an electrical field that is picked up by the EEG electrodes. ", - "Eye movement and Blink artifacts are some of the largest and most difficult to remove from the data. ", - "The best way to get clean data is to try to limit blinking and eye movements. ", - "These eye movement signals can also be treated as a signal of interest, called the Electrooculogram (EOG)" - ]}

-
-
-

{"Eye Blinks: "}

- blinks -
-

{"Horizontal Eye Movements: "}

- horizontal -
-

{"Vertical Eye Movements: "}

- vertical -
- - - -

{[ - "When signals are sent from our brain to our muscles the motor neurons release Acetylcholine onto the muscle fibres. ", - "This release causes the muscles to contract, and these contractions create electrical potentials that are also picked up by electrodes. ", - "This signal is called the Electromyogram (EMG), and can be measured over any muscle on your body. ", - "EEG electrodes therefore pickup muscle signals from your face muscles when you smile or frown, and from your chewing muscles. ", - "You should make sure to not be chewing gum for this reason while recording EEG. " - ]}

-
-
-

{"Example of Jaw Clenching: "}

- muscle -
-

{"Zoomed in chewing (notice the difference horizontal axis time range): "}

- muscleClose -
- - - -

{[ - "The EEG electrodes measure the voltage of the human body between two points. Voltage, according to Ohms' law, ", - "is modified both by the current and by the resistance (V = IR). ", - "Therefore, if current stays the same, changes in the electrical resistance of the electrode-body connection can change the voltage. ", - "These changes in voltage will not be distinguishable from real changes in voltage from the brain. ", - "This is one reason that mobile recording of EEG is difficult, movement of the electrodes against the head leads to large voltage changes not due to brain activity. ", - "Therefore we want the muse to be firmly secured to the head, as tight as possible, and we want to avoid excessive movement during recording. " - ]}

-
-
-

{"Tapping on the muse moves the electrodes against the head: "}

- mechanical -
- - - -

{[ - "Resistance between the sensor and the body leads to less current and smaller measured voltage. ", - "While out salty wet skin conducts electricity well, the dry skin and air does not, nor do oils and dirt that our skin is covered in. ", - "Because voltage changes as resistance changes, we can sometimes see slow drifts in our EEG data as the connection strength changes. ", - "One example is the moment you place the muse on your head, as the resistance decreases the voltage decreases as well as you can see here. ", - "These slow drifts can also occur due to sweating, which decreases the resistance slowly over time between the head and the sensor. " - ]}

-
-
-

{"When you first put on the muse or when you sweat long drifts occur: "}

- drift -
- - - -

{[ - "Finally, there is electrical noise all around you. Buildings are supplied with 120 Volts of alternating current to power our lights and electronics. ", - "This electrical alternates in polarity 60 times a second, allowing for much more efficient transportation accross distance locations. ", - "Most of our modern electronics convert this AC electrity to DC power (usually around 5 Volts and 2 Amps). ", - "All the computers in the room take in AC power from the building and convert it to DC in their power supply. ", - "Therefore, there is alot of 60 Hz electrical noise in any building. ", - "Most EEG systems have filters to remove some of this noise, but even still, it is so strong that it is easily picked up by EEG electrodes. ", - "We can use filtering to remove this type of noise from our data. " - ]}

-
-
-

{"Electical noise at 60Hz from the AC power in the room (I held my wired headphones up to one side then the other of my head): "}

- lineNoise -
-

{"Zoomed in 60-Hz electrical noise (notice the difference horizontal axis time range): "}

- lineNoiseClose -
- -
- ); -} - -export function renderSliders(setData, setSettings, status, Settings) { - - function resetPipeSetup(value) { - buildPipe(Settings); - setup(setData, Settings) - } - - function handleIntervalRangeSliderChange(value) { - setSettings(prevState => ({...prevState, interval: value})); - resetPipeSetup(); - } - - function handleCutoffLowRangeSliderChange(value) { - setSettings(prevState => ({...prevState, cutOffLow: value})); - resetPipeSetup(); - } - - function handleCutoffHighRangeSliderChange(value) { - setSettings(prevState => ({...prevState, cutOffHigh: value})); - resetPipeSetup(); - } - - function handleDurationRangeSliderChange(value) { - setSettings(prevState => ({...prevState, duration: value})); - resetPipeSetup(); - } - - return ( - - - - - - - ) -} - -export function renderRecord(recordPopChange, recordPop, status, Settings, setSettings) { - - function handleSecondsToSaveRangeSliderChange(value) { - setSettings(prevState => ({...prevState, secondsToSave: value})); - } - - return ( - - -

- {"When you are recording raw data it is recommended you "} - {"first set the sampling point between epochs to 1, then set the epoch duration to 1. "} - {"Once the live chart disappears entirely you have done it correctly. "} - {"This will make it so every row of the output file is a single time point and make the data much easier to work with."} -

-
- - - - - - - - - -

- Your data is currently recording, - once complete it will be downloaded as a .csv file - and can be opened with your favorite spreadsheet program. - Close this window once the download completes. -

-
-
-
-
-
- ) -} - - - -function saveToCSV(Settings) { - console.log('Saving ' + Settings.secondsToSave + ' seconds...'); - var localObservable$ = null; - const dataToSave = []; - - console.log('making ' + Settings.name + ' headers') - - - // for each module subscribe to multicast and make header - // take one sample from selected observable object for headers - localObservable$ = window.multicastRaw$.pipe( - take(1) - ); - //take one sample to get header info - localObservable$.subscribe({ - next(x) { - dataToSave.push( - "Timestamp (ms),", - generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch0_" + f + "ms"}) + ",", - generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch1_" + f + "ms"}) + ",", - generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch2_" + f + "ms"}) + ",", - generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch3_" + f + "ms"}) + ",", - generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "chAux_" + f + "ms"}) + ",", - "info", - "\n" - ); - } - }); - - // Create timer - const timer$ = timer(Settings.secondsToSave * 1000); - - // put selected observable object into local and start taking samples - localObservable$ = window.multicastRaw$.pipe( - takeUntil(timer$) - ); - - // now with header in place subscribe to each epoch and log it - localObservable$.subscribe({ - next(x) { - dataToSave.push(Date.now() + "," + Object.values(x).join(",") + "\n"); - // logging is useful for debugging -yup - // console.log(x); - }, - error(err) { console.log(err); }, - complete() { - console.log('Trying to save') - var blob = new Blob( - dataToSave, - {type: "text/plain;charset=utf-8"} - ); - saveAs(blob, Settings.name + "_Recording_" + Date.now() + ".csv"); - console.log('Completed'); - } - }); -} \ No newline at end of file diff --git a/src/components/PageSwitcher/components/EEGEduRaw/artifact.png b/src/components/PageSwitcher/components/EEGEduRaw/artifact.png deleted file mode 100644 index 24277cb7..00000000 Binary files a/src/components/PageSwitcher/components/EEGEduRaw/artifact.png and /dev/null differ diff --git a/src/components/PageSwitcher/components/EEGEduRaw/blinks.png b/src/components/PageSwitcher/components/EEGEduRaw/blinks.png deleted file mode 100644 index a4485e34..00000000 Binary files a/src/components/PageSwitcher/components/EEGEduRaw/blinks.png and /dev/null differ diff --git a/src/components/PageSwitcher/components/EEGEduRaw/drift.png b/src/components/PageSwitcher/components/EEGEduRaw/drift.png deleted file mode 100644 index e611c934..00000000 Binary files a/src/components/PageSwitcher/components/EEGEduRaw/drift.png and /dev/null differ diff --git a/src/components/PageSwitcher/components/EEGEduRaw/electrodelocations.png b/src/components/PageSwitcher/components/EEGEduRaw/electrodelocations.png deleted file mode 100644 index 79140f46..00000000 Binary files a/src/components/PageSwitcher/components/EEGEduRaw/electrodelocations.png and /dev/null differ diff --git a/src/components/PageSwitcher/components/EEGEduRaw/horizontal.png b/src/components/PageSwitcher/components/EEGEduRaw/horizontal.png deleted file mode 100644 index 5df6db57..00000000 Binary files a/src/components/PageSwitcher/components/EEGEduRaw/horizontal.png and /dev/null differ diff --git a/src/components/PageSwitcher/components/EEGEduRaw/lineNoise.png b/src/components/PageSwitcher/components/EEGEduRaw/lineNoise.png deleted file mode 100644 index 8129d966..00000000 Binary files a/src/components/PageSwitcher/components/EEGEduRaw/lineNoise.png and /dev/null differ diff --git a/src/components/PageSwitcher/components/EEGEduRaw/lineNoiseClose.png b/src/components/PageSwitcher/components/EEGEduRaw/lineNoiseClose.png deleted file mode 100644 index d3cb97b5..00000000 Binary files a/src/components/PageSwitcher/components/EEGEduRaw/lineNoiseClose.png and /dev/null differ diff --git a/src/components/PageSwitcher/components/EEGEduRaw/mechanical.png b/src/components/PageSwitcher/components/EEGEduRaw/mechanical.png deleted file mode 100644 index 07f88f13..00000000 Binary files a/src/components/PageSwitcher/components/EEGEduRaw/mechanical.png and /dev/null differ diff --git a/src/components/PageSwitcher/components/EEGEduRaw/muscle.png b/src/components/PageSwitcher/components/EEGEduRaw/muscle.png deleted file mode 100644 index da9bed6d..00000000 Binary files a/src/components/PageSwitcher/components/EEGEduRaw/muscle.png and /dev/null differ diff --git a/src/components/PageSwitcher/components/EEGEduRaw/muscleClose.png b/src/components/PageSwitcher/components/EEGEduRaw/muscleClose.png deleted file mode 100644 index 98fbd1d6..00000000 Binary files a/src/components/PageSwitcher/components/EEGEduRaw/muscleClose.png and /dev/null differ diff --git a/src/components/PageSwitcher/components/EEGEduRaw/translations/en.json b/src/components/PageSwitcher/components/EEGEduRaw/translations/en.json deleted file mode 100644 index 2b07cb80..00000000 --- a/src/components/PageSwitcher/components/EEGEduRaw/translations/en.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "title": "Raw data", - "description": "First we look at the raw voltage signals coming from each of the four sensors on the muse. TP9 and TP10 are on the ears, AF7 and AF8 are on the forehead. In general EEG electrodes are odd on the left hemisphere and even on the right, and have suffixed with z along the midline.", - "xlabel": "Time (msec)", - "ylabel": "Voltage (\u03BCV)" -} diff --git a/src/components/PageSwitcher/components/EEGEduRaw/vertical.png b/src/components/PageSwitcher/components/EEGEduRaw/vertical.png deleted file mode 100644 index 7b5477f5..00000000 Binary files a/src/components/PageSwitcher/components/EEGEduRaw/vertical.png and /dev/null differ diff --git a/src/components/PageSwitcher/components/EEGEduSpectra/EEGEduSpectra.js b/src/components/PageSwitcher/components/EEGEduSpectra/EEGEduSpectra.js index d9732db8..6a362e26 100644 --- a/src/components/PageSwitcher/components/EEGEduSpectra/EEGEduSpectra.js +++ b/src/components/PageSwitcher/components/EEGEduSpectra/EEGEduSpectra.js @@ -1,109 +1,13 @@ import React from "react"; -import { catchError, multicast } from "rxjs/operators"; - -import { TextContainer, Card, Stack, RangeSlider, Button, ButtonGroup, Modal, Link } from "@shopify/polaris"; -import { saveAs } from 'file-saver'; -import { take, takeUntil } from "rxjs/operators"; -import { Subject, timer } from "rxjs"; - -import { channelNames } from "muse-js"; +import { TextContainer, Card, Stack, } from "@shopify/polaris"; import { Line } from "react-chartjs-2"; -import YouTube from 'react-youtube' - -import { zipSamples } from "muse-js"; - -import { - bandpassFilter, - epoch, - fft, - sliceFFT -} from "@neurosity/pipes"; - -import { chartStyles, generalOptions } from "../chartOptions"; - -import * as generalTranslations from "../translations/en"; +import { channelNames } from "muse-js"; +import { chartStyles, generalOptions } from "../../utils/chartOptions"; import * as specificTranslations from "./translations/en"; -export function getSettings() { - return { - cutOffLow: 1, - cutOffHigh: 100, - interval: 100, - bins: 256, - sliceFFTLow: 1, - sliceFFTHigh: 100, - duration: 1024, - srate: 256, - name: 'Spectra', - secondsToSave: 10 - - } -}; - - -export function buildPipe(Settings) { - if (window.subscriptionSpectra) window.subscriptionSpectra.unsubscribe(); - - window.pipeSpectra$ = null; - window.multicastSpectra$ = null; - window.subscriptionSpectra = null; - - // Build Pipe - window.pipeSpectra$ = zipSamples(window.source.eegReadings$).pipe( - bandpassFilter({ - cutoffFrequencies: [Settings.cutOffLow, Settings.cutOffHigh], - nbChannels: window.nchans }), - epoch({ - duration: Settings.duration, - interval: Settings.interval, - samplingRate: Settings.srate - }), - fft({ bins: Settings.bins }), - sliceFFT([Settings.sliceFFTLow, Settings.sliceFFTHigh]), - catchError(err => { - console.log(err); - }) - ); - - window.multicastSpectra$ = window.pipeSpectra$.pipe( - multicast(() => new Subject()) - ); -} - -export function setup(setData, Settings) { - console.log("Subscribing to " + Settings.name); - - if (window.multicastSpectra$) { - window.subscriptionSpectra = window.multicastSpectra$.subscribe(data => { - setData(spectraData => { - Object.values(spectraData).forEach((channel, index) => { - channel.datasets[0].data = data.psd[index]; - channel.xLabels = data.freqs; - }); - - return { - ch0: spectraData.ch0, - ch1: spectraData.ch1, - ch2: spectraData.ch2, - ch3: spectraData.ch3, - ch4: spectraData.ch4 - }; - }); - }); - - window.multicastSpectra$.connect(); - console.log("Subscribed to " + Settings.name); - } -} - -export function renderModule(channels) { +export function RenderModule(channels) { function renderCharts() { - let vertLim = Math.floor(Math.max(...[].concat.apply([], [channels.data.ch0.datasets[0].data, - channels.data.ch1.datasets[0].data, - channels.data.ch2.datasets[0].data, - channels.data.ch3.datasets[0].data, - channels.data.ch4.datasets[0].data]) - )); + const options = { ...generalOptions, scales: { @@ -122,8 +26,8 @@ export function renderModule(channels) { labelString: specificTranslations.ylabel }, ticks: { - max: vertLim, - min: vertLim * -1 + max: 10, + min: 0 } } ] @@ -148,27 +52,27 @@ export function renderModule(channels) { datasets: [{ label: channelNames[0], borderColor: 'rgba(217,95,2)', - data: channels.data.ch0.datasets[0].data.map(function(x) {return x * -1}), + data: channels.data.ch0.datasets[0].data, fill: false }, { label: channelNames[1], borderColor: 'rgba(27,158,119)', - data: channels.data.ch1.datasets[0].data.map(function(x) {return x * -1}), + data: channels.data.ch1.datasets[0].data, fill: false }, { label: channelNames[2], borderColor: 'rgba(117,112,179)', - data: channels.data.ch2.datasets[0].data.map(function(x) {return x + 0}), + data: channels.data.ch2.datasets[0].data, fill: false }, { label: channelNames[3], borderColor: 'rgba(231,41,138)', - data: channels.data.ch3.datasets[0].data.map(function(x) {return x + 0}), + data: channels.data.ch3.datasets[0].data, fill: false }, { label: channelNames[4], borderColor: 'rgba(20,20,20)', - data: channels.data.ch4.datasets[0].data.map(function(x) {return x + 0}), + data: channels.data.ch4.datasets[0].data, fill: false }], xLabels: channels.data.ch0.xLabels @@ -190,369 +94,12 @@ export function renderModule(channels) { ) } - - } - - const opts = { - height: '195', - width: '320', - playerVars: { // https://developers.google.com/youtube/player_parameters - autoplay: false - } - }; - return ( - - - - - -

{specificTranslations.description}

-
-
-
- -
{renderCharts()}
-
-
- - - - -

{[ - "In module 3 we saw how we can use a mathematical technique called the fourier transform to estimate what frequency is present in the ECG data, to estimate heart rate. ", - "A fourier transform turns any series of numbers into a summed set of sine waves of different sizes. ", - "For review, the following animation shows how a single time-series of data can be thought of as the sum of different frequencies of sine waves, each of a different magnitude. ", - "The blue bar chart at the end of the animation shows what is called the frequency spectra, and indicates the power at each frequency." - ]}

-
-
- FFT -
- - - Image Source - Wikipedia -
- -

{[ - "The Fourier transform is a mathematical technique originally developed in the early 1800s in order to mathematically model the movement of heat. ", - "A discrete fourier transform (DFT) is this mathematical technique applied to digital data (a function sampled at different time points), like EEG data. ", - "In order to compute this efficiently, a pair of psychologists and statisticians created the FAST fourier transform (FFT) in the 60's, during the cold war ", - "as a signal processing technique needed to triangulate possible Soviet nuclear launches from a hypothetical array of sensors around the Soviet Union. ", - "The FFT has gone on to be one of the most useful and used algorithms ever created, and is used in a wide array of digital tools. ", - ]} - - Follow this link to an excellent interactive tutorial on the graphical understanding of the fourier transforms using sound, drawings, and images - -

-

{[ - "We will not cover the mathematical formula or algorithm behind the DFT or FFT here. ", - "A fourier transform turns any series of numbers into a summed set of sine waves of different sizes. ", - "The following animation shows how a single time-series of data, can be thought of as the sum of different frequencies of sine waves, each of a different magnitude. ", - "Amazingly, before digital computers in the late 1800's, in order to model details of light movement, a physical machine was constructed to perform these calculations by hand. ", - "This amazing Harmonic Analyzer still provides an excellent intuition into the mechanics behind decomposing a time series signal into a sum of sin waves. " - ]}

-
- - -

{[ - "Most computer programming languages now provide an easy way to compute the FFT on time series data. ", - "Other methods of converting time series data into the frequency domain also exist, such as wavelet analysis, in which the product of the data and a family of different frequency wavelets is used to esitmate the data decomposition. ", - "The resolution in frequency of the FFT depends on the NUMBER OF TIME POINTS. ", - "The range of frequencies provided by the FFT depends on the sampling rate of the data , in our case 256 Hz provides frequencies up to 128 Hz (half). " - ]}

-

{[ - "Importantly, the uncertainty principle applies to the FFT as well. ", - "It is impossible to know both exactly when something happens, and what frequency it happens at, at the same time. ", - "As an intuition, imagine that in order to estimate your heart rate you obviously need more than a single time point of data, you need multiple beats of the heart (at least one). ", - "Therefore, the resultant estimate of heart rate will not be precise in time, and will apply to the entire time window you put into the FFT. " - ]}

- -
-
-
- - - - -

{[ - "In module 4 we plotted the range of various artifacts that are common in EEG data. ", - "Obviously, these artifacts remain (if not filtered out) in the window of data that is used to compute the FFT. ", - "Therefore, NOT ALL THE DATA IN THE SPECTRA IS FROM THE BRAIN!!! ", - "For instance, blinks show up as low frequencies, and you have already seen how heart artifacts in the data would show up as low frequency power as well, but these are not brain activity. ", - "For your assignment this module, for each type of artifact you observed in Module 4, now you will observe what the spectra looks like, take a screenshot of the result and save these in a google doc. ", - "To take a screenshot on a mac, press (⌘Command + ⇧Shift + 4) and select the area of the screen you want to save." - ]}

-
-
-
-
-
- ); -} - -export function renderSliders(setData, setSettings, status, Settings) { - - function resetPipeSetup(value) { - buildPipe(Settings); - setup(setData, Settings) - } - - function handleIntervalRangeSliderChange(value) { - setSettings(prevState => ({...prevState, interval: value})); - resetPipeSetup(); - } - - function handleCutoffLowRangeSliderChange(value) { - setSettings(prevState => ({...prevState, cutOffLow: value})); - resetPipeSetup(); - } - - function handleCutoffHighRangeSliderChange(value) { - setSettings(prevState => ({...prevState, cutOffHigh: value})); - resetPipeSetup(); - } - - function handleSliceFFTLowRangeSliderChange(value) { - setSettings(prevState => ({...prevState, sliceFFTLow: value})); - resetPipeSetup(); - } - - function handleSliceFFTHighRangeSliderChange(value) { - setSettings(prevState => ({...prevState, sliceFFTHigh: value})); - resetPipeSetup(); - } - - function handleDurationRangeSliderChange(value) { - setSettings(prevState => ({...prevState, duration: value})); - resetPipeSetup(); - } - - return ( - - - - - - - + +
{renderCharts()}
- ) -} - -export function renderRecord(recordPopChange, recordPop, status, Settings, setSettings) { - - function handleSecondsToSaveRangeSliderChange(value) { - setSettings(prevState => ({...prevState, secondsToSave: value})); - } - - const opts = { - height: '195', - width: '320', - playerVars: { // https://developers.google.com/youtube/player_parameters - autoplay: false - } - }; - - return( - - - -

{[ - "Press the following button after adjusting the settings above in order to record the live spectra over time into a .csv file. " - ]}

-
- - - - - -

{[ - "A .csv file will be saved that can be opened in Google Sheets. ", - "Here is an example of what the data will look like once loaded. ", - "The first column shows the time in msec of each estimate of the spectra.", - "Each row therefore represents an estimate from the previous 8 seconds of data EEG data, and the windows used to compute each row are overlapping. ", - "Again, this is because you need a long segment of time to estimate frequency of rhythms over time. ", - "You can see the time values increase about 10000 ms during the recording, representing the 10 seconds of data. ", - "So 10000 milliseconds divided into ~400 ms shifts per row gives us the rough number of rows (~25). " - ]}

- exampleOutput -

{[ - "The spectra are then shown on each row. Each column represents the power at a different frequency. ", - "The first row shows the frequency and channel label for all the data in that column. ", - "So the first 30 columns after the timestamp are the 30 frequencies from the TP9 electrode ", - "(where 30 is the number of frequencies saved which can be adjusted with the FFT Slice Settings). ", - "After that, the next electrode starts, with another 30 frequencies.", - "After columns for all 30 frequencies from all four electrodes, another 30 columns show zeros, this is for an optional auxillary channel we are not using here. ", - "Finally columns are saved to record the exact frequencies of each bin of the FFT (redundant with the column names). " - ]}

-
- - -

{[ - "The second part of the assignment for this module involves parsing this output file to organize the data into a format that can be plotted like the live plot on the page. ", - "Data will be averaged over time, and then data for each electrode will be organized and used to make a chart of the spectra" - ]} - - - Link to example google sheet from video. - -

-
-
- - - - - - - -

- Your data is currently recording, - once complete it will be downloaded as a .csv file - and can be opened with your favorite spreadsheet program. - Close this window once the download completes. -

-
-
-
-
-
- ) -} - - -function saveToCSV(Settings) { - console.log('Saving ' + Settings.secondsToSave + ' seconds...'); - var localObservable$ = null; - const dataToSave = []; - - console.log('making ' + Settings.name + ' headers') - - // take one sample from selected observable object for headers - localObservable$ = window.multicastSpectra$.pipe( - take(1) ); - - localObservable$.subscribe({ - next(x) { - let freqs = Object.values(x.freqs); - dataToSave.push( - "Timestamp (ms),", - freqs.map(function(f) {return "ch0_" + f + "Hz"}) + ",", - freqs.map(function(f) {return "ch1_" + f + "Hz"}) + ",", - freqs.map(function(f) {return "ch2_" + f + "Hz"}) + ",", - freqs.map(function(f) {return "ch3_" + f + "Hz"}) + ",", - freqs.map(function(f) {return "chAux_" + f + "Hz"}) + ",", - freqs.map(function(f) {return "f_" + f + "Hz"}) + "," , - "info", - "\n" - ); - } - }); - - // Create timer - const timer$ = timer(Settings.secondsToSave * 1000); - - // put selected observable object into local and start taking samples - localObservable$ = window.multicastSpectra$.pipe( - takeUntil(timer$) - ); - - - // now with header in place subscribe to each epoch and log it - localObservable$.subscribe({ - next(x) { - dataToSave.push(Date.now() + "," + Object.values(x).join(",") + "\n"); - // logging is useful for debugging -yup - // console.log(x); - }, - error(err) { console.log(err); }, - complete() { - console.log('Trying to save') - var blob = new Blob( - dataToSave, - {type: "text/plain;charset=utf-8"} - ); - saveAs(blob, Settings.name + "_Recording_" + Date.now() + ".csv"); - console.log('Completed'); - } - }); } + diff --git a/src/components/PageSwitcher/components/EEGEduSpectra/exampleOutput.png b/src/components/PageSwitcher/components/EEGEduSpectra/exampleOutput.png deleted file mode 100644 index 89a50b1b..00000000 Binary files a/src/components/PageSwitcher/components/EEGEduSpectra/exampleOutput.png and /dev/null differ diff --git a/src/components/PageSwitcher/components/EEGEduSpectra/exampleRecording.png b/src/components/PageSwitcher/components/EEGEduSpectra/exampleRecording.png deleted file mode 100644 index f3a6cb10..00000000 Binary files a/src/components/PageSwitcher/components/EEGEduSpectra/exampleRecording.png and /dev/null differ diff --git a/src/components/PageSwitcher/components/EEGEduSpectra/fft_animation.gif b/src/components/PageSwitcher/components/EEGEduSpectra/fft_animation.gif deleted file mode 100644 index 352c95c1..00000000 Binary files a/src/components/PageSwitcher/components/EEGEduSpectra/fft_animation.gif and /dev/null differ diff --git a/src/components/PageSwitcher/components/EEGEduSpectra/translations/en.json b/src/components/PageSwitcher/components/EEGEduSpectra/translations/en.json index a2e18430..66fde666 100644 --- a/src/components/PageSwitcher/components/EEGEduSpectra/translations/en.json +++ b/src/components/PageSwitcher/components/EEGEduSpectra/translations/en.json @@ -1,6 +1,5 @@ { "title": "Frequency Domain Data", - "description": "In the next demo we will look at the same raw EEG data but this time in the frequency domain. We want to identify the magnitude of oscillations of different frequencies in our live signal. We use the fast fourier transform (FFT) to convert the voltage values over time to the power at each frequency. To use the FFT we pick a particular chunk of data and get an output called a spectra. Every update of the chart is based on subsequent windows of time.", "xlabel": "Frequency (Hz)", "ylabel": "Power (\u03BCV\u00B2)" } diff --git a/src/components/PageSwitcher/components/EEGEduSpectro/EEGEduSpectro.js b/src/components/PageSwitcher/components/EEGEduSpectro/EEGEduSpectro.js deleted file mode 100644 index baf338c7..00000000 --- a/src/components/PageSwitcher/components/EEGEduSpectro/EEGEduSpectro.js +++ /dev/null @@ -1,223 +0,0 @@ -import React from "react"; -import { catchError, multicast } from "rxjs/operators"; - -import { Card, Stack, TextContainer, RangeSlider} from "@shopify/polaris"; -import { Subject } from "rxjs"; - -import { zipSamples } from "muse-js"; - -import { - bandpassFilter, - epoch, - fft, - sliceFFT -} from "@neurosity/pipes"; - -import { chartStyles } from "../chartOptions"; - -import * as generalTranslations from "../translations/en"; -import * as specificTranslations from "./translations/en"; - -import sketchSpectro from './sketchSpectro' - -import P5Wrapper from 'react-p5-wrapper'; - -export function getSettings () { - return { - cutOffLow: 1, - cutOffHigh: 100, - interval: 16, - bins: 128, - duration: 128, - srate: 256, - name: 'Spectro', - sliceFFTLow: 1, - sliceFFTHigh: 100, - } -}; - -export function buildPipe(Settings) { - if (window.subscriptionSpectro) window.subscriptionSpectro.unsubscribe(); - - window.pipeSpectro$ = null; - window.multicastSpectro$ = null; - window.subscriptionSpectro = null; - - // Build Pipe - window.pipeSpectro$ = zipSamples(window.source.eegReadings$).pipe( - bandpassFilter({ - cutoffFrequencies: [Settings.cutOffLow, Settings.cutOffHigh], - nbChannels: window.nchans }), - epoch({ - duration: Settings.duration, - interval: Settings.interval, - samplingRate: Settings.srate - }), - fft({ bins: Settings.bins }), - sliceFFT([Settings.sliceFFTLow, Settings.sliceFFTHigh]), - catchError(err => { - console.log(err); - }) - ); - window.multicastSpectro$ = window.pipeSpectro$.pipe( - multicast(() => new Subject()) - ); -} - -export function setup(setData, Settings) { - console.log("Subscribing to " + Settings.name); - - if (window.multicastSpectro$) { - window.subscriptionSpectro = window.multicastSpectro$.subscribe(data => { - setData(spectroData => { - Object.values(spectroData).forEach((channel, index) => { - channel.datasets[0].data = data.psd[index]; - channel.xLabels = data.freqs - }); - - return { - ch0: spectroData.ch0, - ch1: spectroData.ch1, - ch2: spectroData.ch2, - ch3: spectroData.ch3, - ch4: spectroData.ch4 - }; - }); - }); - - window.multicastSpectro$.connect(); - console.log("Subscribed to " + Settings.name); - } -} - -export function renderModule(channels) { - function RenderCharts() { - return Object.values(channels.data).map((channel, index) => { - if (channel.datasets[0].data) { - window.psd = channel.datasets[0].data; - window.freqs = channel.xLabels; - if (channel.xLabels) { - window.bins = channel.xLabels.length; - } - } - - //only left frontal channel - if (index === 1) { - return ( - - - - - - ); - } else { - return null - } - }); - } - - return ( - - - - - -

{specificTranslations.description}

-
-
-
- -
{RenderCharts()}
-
-
- ); -} - -export function renderSliders(setData, setSettings, status, Settings) { - - function resetPipeSetup(value) { - buildPipe(Settings); - setup(setData, Settings); - } - - function handleIntervalRangeSliderChange(value) { - setSettings(prevState => ({...prevState, interval: value})); - resetPipeSetup(); - } - - function handleCutoffLowRangeSliderChange(value) { - setSettings(prevState => ({...prevState, cutOffLow: value})); - resetPipeSetup(); - } - - function handleCutoffHighRangeSliderChange(value) { - setSettings(prevState => ({...prevState, cutOffHigh: value})); - resetPipeSetup(); - } - - function handleDurationRangeSliderChange(value) { - setSettings(prevState => ({...prevState, duration: value})); - resetPipeSetup(); - } - - function handleSliceFFTHighRangeSliderChange(value) { - setSettings(prevState => ({...prevState, sliceFFTHigh: value})); - resetPipeSetup(); - } - - function handleSliceFFTLowRangeSliderChange(value) { - setSettings(prevState => ({...prevState, sliceFFTLow: value})); - resetPipeSetup(); - } - - return ( - - - - - - - - - ) -} - diff --git a/src/components/PageSwitcher/components/EEGEduSpectro/sketchSpectro.js b/src/components/PageSwitcher/components/EEGEduSpectro/sketchSpectro.js deleted file mode 100644 index e7f64b49..00000000 --- a/src/components/PageSwitcher/components/EEGEduSpectro/sketchSpectro.js +++ /dev/null @@ -1,45 +0,0 @@ -import "p5/lib/addons/p5.sound"; - -export default function sketchSpectro (p) { - - let spectrum; - let binCount; - let speed = 4; - - // canvas is global so we can copy it - let cnv; - - p.setup = function () { - cnv = p.createCanvas(p.windowWidth*.6, 400); - p.noStroke(); - p.colorMode(p.RGB); - }; - - p.myCustomRedrawAccordingToNewPropsHandler = function (props) { - spectrum = props.psd; - binCount = props.bins; - }; - - p.windowResized = function() { - p.resizeCanvas(p.windowWidth*.6, 400); - } - - p.draw = function () { - if (spectrum) { - // copy the sketch and move it over based on the speed - p.copy(cnv, 0, 0, p.width.toFixed(0), p.height.toFixed(0), -speed, 0, p.width.toFixed(0), p.height.toFixed(0)); - - // iterate thru current freq spectrum - for (let i = 0; i < binCount; i++) { - let value; - value = spectrum[i]; - - let c = (value/5)*255; - p.fill(255-c, 255-c, 255-c); - let percent = i / binCount; - let y = percent * p.height; - p.rect(p.width - speed, p.height - y, speed, p.height / binCount); - } - } - } -}; \ No newline at end of file diff --git a/src/components/PageSwitcher/components/EEGEduSpectro/translations/en.json b/src/components/PageSwitcher/components/EEGEduSpectro/translations/en.json deleted file mode 100644 index a6cd711a..00000000 --- a/src/components/PageSwitcher/components/EEGEduSpectro/translations/en.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "title": "Spectrogram (Spectra over time)", - "description": [ - "Back to the full spectra, we can also look at how this changes over time in a single image. ", - "This image, with time on the horizontal x-axis and freqency on the vertical y-axis is like a heat map or topographic map. ", - "Darker regions represent frequencies and times with larger power. ", - "Remember that on the spectra power is the vertical dimension, here is the is the colour." - ], - "xlabel": "Time (ms)", - "ylabel": "Frequency (Hz)", - "zlabel": "Power (\u03BCV\u00B2)" -} diff --git a/src/components/PageSwitcher/components/EEGEduSsvep/EEGEduSsvep.js b/src/components/PageSwitcher/components/EEGEduSsvep/EEGEduSsvep.js deleted file mode 100644 index 053ba5d8..00000000 --- a/src/components/PageSwitcher/components/EEGEduSsvep/EEGEduSsvep.js +++ /dev/null @@ -1,397 +0,0 @@ -import React from "react"; -import { catchError, multicast } from "rxjs/operators"; - -import { TextContainer, Card, Stack, RangeSlider, Button, ButtonGroup, Modal } from "@shopify/polaris"; -import { saveAs } from 'file-saver'; -import { take, takeUntil } from "rxjs/operators"; -import { Subject, timer } from "rxjs"; - -import { channelNames } from "muse-js"; -import { Line } from "react-chartjs-2"; - -import { zipSamples } from "muse-js"; - -import { - bandpassFilter, - epoch, - fft, - sliceFFT -} from "@neurosity/pipes"; - -import { chartStyles, generalOptions } from "../chartOptions"; - -import * as generalTranslations from "../translations/en"; -import * as specificTranslations from "./translations/en"; - -import P5Wrapper from 'react-p5-wrapper'; -import sketchFlashSlow from './sketchFlashSlow'; -import sketchFlashFast from './sketchFlashFast'; - -export function getSettings() { - return { - cutOffLow: 2, - cutOffHigh: 20, - interval: 100, - bins: 256, - sliceFFTLow: 1, - sliceFFTHigh: 30, - duration: 1024, - srate: 256, - name: 'Ssvep', - secondsToSave: 10 - } -}; - -export function buildPipe(Settings) { - if (window.subscriptionSsvep) window.subscriptionSsvep.unsubscribe(); - - window.pipeSsvep$ = null; - window.multicastSsvep$ = null; - window.subscriptionSsvep = null; - - // Build Pipe - window.pipeSsvep$ = zipSamples(window.source.eegReadings$).pipe( - bandpassFilter({ - cutoffFrequencies: [Settings.cutOffLow, Settings.cutOffHigh], - nbChannels: window.nchans }), - epoch({ - duration: Settings.duration, - interval: Settings.interval, - samplingRate: Settings.srate - }), - fft({ bins: Settings.bins }), - sliceFFT([Settings.sliceFFTLow, Settings.sliceFFTHigh]), - catchError(err => { - console.log(err); - }) - ); - - window.multicastSsvep$ = window.pipeSsvep$.pipe( - multicast(() => new Subject()) - ); -} - -export function setup(setData, Settings) { - console.log("Subscribing to " + Settings.name); - - if (window.multicastSsvep$) { - window.subscriptionSsvep = window.multicastSsvep$.subscribe(data => { - setData(ssvepData => { - Object.values(ssvepData).forEach((channel, index) => { - channel.datasets[0].data = data.psd[index]; - channel.xLabels = data.freqs; - - }); - - return { - ch0: ssvepData.ch0, - ch1: ssvepData.ch1, - ch2: ssvepData.ch2, - ch3: ssvepData.ch3, - ch4: ssvepData.ch4 - }; - }); - }); - - window.multicastSsvep$.connect(); - console.log("Subscribed to " + Settings.name); - } -} - -export function renderModule(channels) { - function renderCharts() { - return Object.values(channels.data).map((channel, index) => { - const options = { - ...generalOptions, - scales: { - xAxes: [ - { - scaleLabel: { - ...generalOptions.scales.xAxes[0].scaleLabel, - labelString: specificTranslations.xlabel - } - } - ], - yAxes: [ - { - scaleLabel: { - ...generalOptions.scales.yAxes[0].scaleLabel, - labelString: specificTranslations.ylabel - }, - ticks: { - max: 25, - min: 0 - } - } - ] - }, - elements: { - point: { - radius: 3 - } - }, - title: { - ...generalOptions.title, - text: generalTranslations.channel + channelNames[index] - } - }; - - if (index === 0) { - return ( - - - - ); - } else { - return null - } - }); - } - - return ( - - - - -

{specificTranslations.description}

-
-
-
- -
{renderCharts()}
-
-
- ); -} - -export function renderSliders(setData, setSettings, status, Settings) { - - function resetPipeSetup(value) { - buildPipe(Settings); - setup(setData, Settings) - } - - function handleIntervalRangeSliderChange(value) { - setSettings(prevState => ({...prevState, interval: value})); - resetPipeSetup(); - } - - function handleCutoffLowRangeSliderChange(value) { - setSettings(prevState => ({...prevState, cutOffLow: value})); - resetPipeSetup(); - } - - function handleCutoffHighRangeSliderChange(value) { - setSettings(prevState => ({...prevState, cutOffHigh: value})); - resetPipeSetup(); - } - - function handleSliceFFTLowRangeSliderChange(value) { - setSettings(prevState => ({...prevState, sliceFFTLow: value})); - resetPipeSetup(); - } - - function handleSliceFFTHighRangeSliderChange(value) { - setSettings(prevState => ({...prevState, sliceFFTHigh: value})); - resetPipeSetup(); - } - - function handleDurationRangeSliderChange(value) { - setSettings(prevState => ({...prevState, duration: value})); - resetPipeSetup(); - } - - return ( - - - - - - - - - ) -} - -export function renderRecord(recordPopChange, recordPop, status, Settings, recordTwoPopChange, recordTwoPop, setSettings) { - const cond1 = "Slow Frequency"; - const cond2 = "Fast Frequency"; - - function handleSecondsToSaveRangeSliderChange(value) { - setSettings(prevState => ({...prevState, secondsToSave: value})); - } - - return( - - - - - - - - - - - - - - - -

- Your data is currently recording, - once complete it will be downloaded as a .csv file - and can be opened with your favorite spreadsheet program. - Close this window once the download completes. -

-
-
-
- - - - - - - -

- Your data is currently recording, - once complete it will be downloaded as a .csv file - and can be opened with your favorite spreadsheet program. - Close this window once the download completes. -

-
-
-
- -
-
- ) -} - - -function saveToCSV(Settings, condition) { - console.log('Saving ' + Settings.secondsToSave + ' seconds...'); - var localObservable$ = null; - const dataToSave = []; - - console.log('making ' + Settings.name + ' headers') - - // take one sample from selected observable object for headers - localObservable$ = window.multicastSsvep$.pipe( - take(1) - ); - - localObservable$.subscribe({ - next(x) { - let freqs = Object.values(x.freqs); - dataToSave.push( - "Timestamp (ms),", - freqs.map(function(f) {return "ch0_" + f + "Hz"}) + ",", - freqs.map(function(f) {return "ch1_" + f + "Hz"}) + ",", - freqs.map(function(f) {return "ch2_" + f + "Hz"}) + ",", - freqs.map(function(f) {return "ch3_" + f + "Hz"}) + ",", - freqs.map(function(f) {return "chAux_" + f + "Hz"}) + ",", - freqs.map(function(f) {return "f_" + f + "Hz"}) + "," , - "info", - "\n" - ); - } - }); - - //create timer - const timer$ = timer(Settings.secondsToSave * 1000); - - // put selected observable object into local and start taking samples - localObservable$ = window.multicastSsvep$.pipe( - takeUntil(timer$) - ); - - - // now with header in place subscribe to each epoch and log it - localObservable$.subscribe({ - next(x) { - dataToSave.push(Date.now() + "," + Object.values(x).join(",") + "\n"); - // logging is useful for debugging -yup - // console.log(x); - }, - error(err) { console.log(err); }, - complete() { - console.log('Trying to save') - var blob = new Blob( - dataToSave, - {type: "text/plain;charset=utf-8"} - ); - saveAs(blob, Settings.name + "_" + condition + "_Recording_" + Date.now() + ".csv"); - console.log('Completed'); - } - }); -} diff --git a/src/components/PageSwitcher/components/EEGEduSsvep/sketchFlashFast.js b/src/components/PageSwitcher/components/EEGEduSsvep/sketchFlashFast.js deleted file mode 100644 index 18a900cf..00000000 --- a/src/components/PageSwitcher/components/EEGEduSsvep/sketchFlashFast.js +++ /dev/null @@ -1,43 +0,0 @@ -export default function sketchFlash (p) { - - - const freq = 11; - let x = 0; - let startTime = 0; - let newOnset = true; - const delay = 1000/freq; - - p.setup = function () { - p.createCanvas(300, 300); - p.frameRate(60); - }; - - p.windowResized = function() { - p.createCanvas(300, 300); - } - - - p.mousePressed = function () { - p.background(256); - } - - p.draw = function () { - p.background(255); - x = x+1; - if ((p.millis() - startTime) > delay) { - newOnset = true; - } else { - newOnset = false; - } - if (newOnset) { - p.fill(0, 0, 0); - startTime = p.millis(); - } else { - p.fill(255, 255, 255); - } - p.noStroke(); - p.ellipse(p.width/2, p.height/2, 300); - p.fill(255,0,0); - p.text("+", p.width/2, p.height/2); - } -}; \ No newline at end of file diff --git a/src/components/PageSwitcher/components/EEGEduSsvep/sketchFlashSlow.js b/src/components/PageSwitcher/components/EEGEduSsvep/sketchFlashSlow.js deleted file mode 100644 index 2999086d..00000000 --- a/src/components/PageSwitcher/components/EEGEduSsvep/sketchFlashSlow.js +++ /dev/null @@ -1,43 +0,0 @@ -export default function sketchFlash (p) { - - - const freq = 4; - let x = 0; - let startTime = 0; - let newOnset = true; - const delay = 1000/freq; - - p.setup = function () { - p.createCanvas(300, 300); - p.frameRate(60); - }; - - p.windowResized = function() { - p.createCanvas(300, 300); - } - - - p.mousePressed = function () { - p.background(256); - } - - p.draw = function () { - p.background(255); - x = x+1; - if ((p.millis() - startTime) > delay) { - newOnset = true; - } else { - newOnset = false; - } - if (newOnset) { - p.fill(0, 0, 0); - startTime = p.millis(); - } else { - p.fill(255, 255, 255); - } - p.noStroke(); - p.ellipse(p.width/2, p.height/2, 300); - p.fill(255,0,0); - p.text("+", p.width/2, p.height/2); - } -}; \ No newline at end of file diff --git a/src/components/PageSwitcher/components/EEGEduSsvep/translations/en.json b/src/components/PageSwitcher/components/EEGEduSsvep/translations/en.json deleted file mode 100644 index 8db1a6ed..00000000 --- a/src/components/PageSwitcher/components/EEGEduSsvep/translations/en.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "title": "Steady-State Visual Evoked Potential (SSVEP) Experiment", - "description": [ - "In the next demo we run our second experiment, comparing the spectra in another two conditions. ", - "When we see or hear rhythms in our environemnts (music, flashing lights, etc.), our brain picks up on those rhythms. ", - "The brain starts to pick up on the rhythms and begins to oscillate at the same frequency. ", - "This is called a steady-state response, or sometimes entrainement, and depends on cognitive factors like attention as well. ", - "Here we will compare the spectra in two conditions of different rhythmic visual stimulation.", - "WARNING: The following experiment utilizes flashing stimuli" - ], - "xlabel": "Frequency (Hz)", - "ylabel": "Power (\u03BCV\u00B2)" -} diff --git a/src/components/PageSwitcher/translations/en.json b/src/components/PageSwitcher/translations/en.json deleted file mode 100644 index d6337288..00000000 --- a/src/components/PageSwitcher/translations/en.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "title": "Choose your Module", - "types": { - "intro": "1. Introduction", - "heartRaw": "2. Electrocardiogram (Heart beats)", - "heartSpectra": "3. Heart Rate (Beats per minute)", - "raw": "4. Raw and Filtered Data", - "spectra": "5. Frequency Spectra", - "bands": "6. Frequency Bands", - "animate": "7. Brain Controlled Animation", - "spectro": "8. Spectrogram (spectra over time)", - "alpha": "9. Eyes open vs. Eyes closed Experiment", - "ssvep": "10. Steady-State Visual Evoked Potential (SSVEP) Experiment", - "evoked": "11. Stimulus Evoked Event-related potential (ERP)", - "predict": "12. Predict brain states with a trained classifier" - } -} diff --git a/src/components/PageSwitcher/components/chartOptions.js b/src/components/PageSwitcher/utils/chartOptions.js similarity index 90% rename from src/components/PageSwitcher/components/chartOptions.js rename to src/components/PageSwitcher/utils/chartOptions.js index c5ef4127..fb640e8a 100644 --- a/src/components/PageSwitcher/components/chartOptions.js +++ b/src/components/PageSwitcher/utils/chartOptions.js @@ -1,5 +1,3 @@ -import * as generalTranslations from "./translations/en"; - export const chartStyles = { wrapperStyle: { display: "flex", @@ -72,7 +70,7 @@ export const generalOptions = { }, title: { display: true, - text: generalTranslations.channel + text: 'Channel: ' }, responsive: true, tooltips: { enabled: false }, diff --git a/src/components/PageSwitcher/components/translations/en.json b/src/components/PageSwitcher/utils/connectionText.json similarity index 84% rename from src/components/PageSwitcher/components/translations/en.json rename to src/components/PageSwitcher/utils/connectionText.json index b88bd72a..809d17b7 100644 --- a/src/components/PageSwitcher/components/translations/en.json +++ b/src/components/PageSwitcher/utils/connectionText.json @@ -6,6 +6,5 @@ "connected": "Muse Headband Connected", "connectedMock": "Mock Data Connected", "connectionFailed": "Connection failed", - "disconnect": "Disconnect", - "channel": "Channel: " + "disconnect": "Disconnect" } diff --git a/yarn.lock b/yarn.lock index c305b447..9c21b3fa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2275,6 +2275,11 @@ adjust-sourcemap-loader@2.0.0: object-path "0.11.4" regex-parser "2.2.10" +after@0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" + integrity sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8= + agent-base@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" @@ -2558,6 +2563,11 @@ array-unique@^0.3.2: resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= +arraybuffer.slice@~0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675" + integrity sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog== + arrify@^1.0.0, arrify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" @@ -2891,6 +2901,11 @@ babylon@^6.18.0: resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ== +backo2@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" + integrity sha1-MasayLEpNjRj41s+u2n038+6eUc= + balanced-match@^0.4.2: version "0.4.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" @@ -2906,11 +2921,21 @@ base62@^1.1.0: resolved "https://registry.yarnpkg.com/base62/-/base62-1.2.8.tgz#1264cb0fb848d875792877479dbe8bae6bae3428" integrity sha512-V6YHUbjLxN1ymqNLb1DPHoU1CpfdL7d2YTIp5W3U4hhoG4hhxNmsFDs66M9EXxBiSEke5Bt5dwdfMwwZF70iLA== +base64-arraybuffer@0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" + integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg= + base64-js@^1.0.2, base64-js@^1.2.3, base64-js@^1.3.0: version "1.3.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== +base64id@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" + integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== + base@^0.11.1: version "0.11.2" resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" @@ -2948,6 +2973,13 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" +better-assert@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522" + integrity sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI= + dependencies: + callsite "1.0.0" + big-integer@^1.6.17: version "1.6.48" resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.48.tgz#8fd88bd1632cba4a1c8c3e3d7159f08bb95b4b9e" @@ -3000,6 +3032,11 @@ bl@^3.0.0: dependencies: readable-stream "^3.0.1" +blob@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683" + integrity sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig== + bluebird@^3.5.5: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" @@ -3358,6 +3395,11 @@ caller-path@^2.0.0: dependencies: caller-callsite "^2.0.0" +callsite@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20" + integrity sha1-KAOY5dZkvXQDi28JBRU+borxvCA= + callsites@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" @@ -3946,11 +3988,26 @@ compare-semver@^1.0.0: dependencies: semver "^5.0.1" +component-bind@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1" + integrity sha1-AMYIq33Nk4l8AAllGx06jh5zu9E= + +component-emitter@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" + integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY= + component-emitter@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== +component-inherit@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143" + integrity sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM= + compose-function@3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/compose-function/-/compose-function-3.0.3.tgz#9ed675f13cc54501d30950a486ff6a7ba3ab185f" @@ -4112,6 +4169,11 @@ cookie-signature@1.0.6: resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= +cookie@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" + integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s= + cookie@0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" @@ -4818,13 +4880,20 @@ debug@^3.0.0, debug@^3.1.0, debug@^3.1.1, debug@^3.2.5, debug@^3.2.6: dependencies: ms "^2.1.1" -debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: +debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@~4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== dependencies: ms "^2.1.1" +debug@~3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -5256,6 +5325,46 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1: dependencies: once "^1.4.0" +engine.io-client@~3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.4.0.tgz#82a642b42862a9b3f7a188f41776b2deab643700" + integrity sha512-a4J5QO2k99CM2a0b12IznnyQndoEvtA4UAldhGzKqnHf42I3Qs2W5SPnDvatZRcMaNZs4IevVicBPayxYt6FwA== + dependencies: + component-emitter "1.2.1" + component-inherit "0.0.3" + debug "~4.1.0" + engine.io-parser "~2.2.0" + has-cors "1.1.0" + indexof "0.0.1" + parseqs "0.0.5" + parseuri "0.0.5" + ws "~6.1.0" + xmlhttprequest-ssl "~1.5.4" + yeast "0.1.2" + +engine.io-parser@~2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-2.2.0.tgz#312c4894f57d52a02b420868da7b5c1c84af80ed" + integrity sha512-6I3qD9iUxotsC5HEMuuGsKA0cXerGz+4uGcXQEkfBidgKf0amsjrrtwcbwK/nzpZBxclXlV7gGl9dgWvu4LF6w== + dependencies: + after "0.8.2" + arraybuffer.slice "~0.0.7" + base64-arraybuffer "0.1.5" + blob "0.0.5" + has-binary2 "~1.0.2" + +engine.io@~3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-3.4.0.tgz#3a962cc4535928c252759a00f98519cb46c53ff3" + integrity sha512-XCyYVWzcHnK5cMz7G4VTu2W7zJS7SM1QkcelghyIk/FmobWBtXE7fwhBusEKvCSqc3bMh8fNFMlUkCKTFRxH2w== + dependencies: + accepts "~1.3.4" + base64id "2.0.0" + cookie "0.3.1" + debug "~4.1.0" + engine.io-parser "~2.2.0" + ws "^7.1.2" + enhanced-resolve@^0.9.0, enhanced-resolve@~0.9.0: version "0.9.1" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-0.9.1.tgz#4d6e689b3725f86090927ccc86cd9f1635b89e2e" @@ -5746,7 +5855,7 @@ expect@^24.9.0: jest-message-util "^24.9.0" jest-regex-util "^24.9.0" -express@^4.16.2, express@^4.16.4: +express@^4.16.2, express@^4.16.4, express@^4.17.1: version "4.17.1" resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== @@ -6808,6 +6917,18 @@ has-ansi@^2.0.0: dependencies: ansi-regex "^2.0.0" +has-binary2@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-binary2/-/has-binary2-1.0.3.tgz#7776ac627f3ea77250cfc332dab7ddf5e4f5d11d" + integrity sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw== + dependencies: + isarray "2.0.1" + +has-cors@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39" + integrity sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk= + has-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" @@ -7762,6 +7883,11 @@ isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= +isarray@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e" + integrity sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4= + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -9791,6 +9917,11 @@ object-assign@^2.0.0: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-2.1.1.tgz#43c36e5d569ff8e4816c4efa8be02d26967c18aa" integrity sha1-Q8NuXVaf+OSBbE76i+AtJpZ8GKo= +object-component@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291" + integrity sha1-8MaapQ78lbhmwYb0AKM3acsvEpE= + object-copy@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" @@ -10248,6 +10379,20 @@ parse5@5.1.0: resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.0.tgz#c59341c9723f414c452975564c7c00a68d58acd2" integrity sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ== +parseqs@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d" + integrity sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0= + dependencies: + better-assert "~1.0.0" + +parseuri@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.5.tgz#80204a50d4dbb779bfdc6ebe2778d90e4bce320a" + integrity sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo= + dependencies: + better-assert "~1.0.0" + parseurl@~1.3.2, parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" @@ -12805,6 +12950,61 @@ snapdragon@^0.8.1: source-map-resolve "^0.5.0" use "^3.1.0" +socket.io-adapter@~1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz#ab3f0d6f66b8fc7fca3959ab5991f82221789be9" + integrity sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g== + +socket.io-client@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.3.0.tgz#14d5ba2e00b9bcd145ae443ab96b3f86cbcc1bb4" + integrity sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA== + dependencies: + backo2 "1.0.2" + base64-arraybuffer "0.1.5" + component-bind "1.0.0" + component-emitter "1.2.1" + debug "~4.1.0" + engine.io-client "~3.4.0" + has-binary2 "~1.0.2" + has-cors "1.1.0" + indexof "0.0.1" + object-component "0.0.3" + parseqs "0.0.5" + parseuri "0.0.5" + socket.io-parser "~3.3.0" + to-array "0.1.4" + +socket.io-parser@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.3.0.tgz#2b52a96a509fdf31440ba40fed6094c7d4f1262f" + integrity sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng== + dependencies: + component-emitter "1.2.1" + debug "~3.1.0" + isarray "2.0.1" + +socket.io-parser@~3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.4.0.tgz#370bb4a151df2f77ce3345ff55a7072cc6e9565a" + integrity sha512-/G/VOI+3DBp0+DJKW4KesGnQkQPFmUCbA/oO2QGT6CWxU7hLGWqU3tyuzeSK/dqcyeHsQg1vTe9jiZI8GU9SCQ== + dependencies: + component-emitter "1.2.1" + debug "~4.1.0" + isarray "2.0.1" + +socket.io@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.3.0.tgz#cd762ed6a4faeca59bc1f3e243c0969311eb73fb" + integrity sha512-2A892lrj0GcgR/9Qk81EaY2gYhCBxurV0PfmmESO6p27QPrUK1J3zdns+5QPqvUYK2q657nSj0guoIil9+7eFg== + dependencies: + debug "~4.1.0" + engine.io "~3.4.0" + has-binary2 "~1.0.2" + socket.io-adapter "~1.1.0" + socket.io-client "2.3.0" + socket.io-parser "~3.4.0" + sockjs-client@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.3.0.tgz#12fc9d6cb663da5739d3dc5fb6e8687da95cb177" @@ -13528,6 +13728,11 @@ tmpl@1.0.x: resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE= +to-array@0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890" + integrity sha1-F+bBH3PdTz10zaek/zI46a2b+JA= + to-arraybuffer@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" @@ -14966,6 +15171,18 @@ ws@^6.1.2: dependencies: async-limiter "~1.0.0" +ws@^7.1.2: + version "7.2.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.1.tgz#03ed52423cd744084b2cf42ed197c8b65a936b8e" + integrity sha512-sucePNSafamSKoOqoNfBd8V0StlkzJKL2ZAhGQinCfNQ+oacw+Pk7lcdAElecBF2VkLNZRiIb5Oi1Q5lVUVt2A== + +ws@~6.1.0: + version "6.1.4" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.4.tgz#5b5c8800afab925e94ccb29d153c8d02c1776ef9" + integrity sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA== + dependencies: + async-limiter "~1.0.0" + xdg-basedir@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-2.0.0.tgz#edbc903cc385fc04523d966a335504b5504d1bd2" @@ -14998,6 +15215,11 @@ xmldom@0.1.x: resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9" integrity sha1-1QH5ezvbQDr4757MIFcxh6rawOk= +xmlhttprequest-ssl@~1.5.4: + version "1.5.5" + resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e" + integrity sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4= + xmlhttprequest@1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc" @@ -15130,6 +15352,11 @@ yargs@~3.10.0: decamelize "^1.0.0" window-size "0.1.0" +yeast@0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" + integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk= + youtube-player@^5.5.1: version "5.5.2" resolved "https://registry.yarnpkg.com/youtube-player/-/youtube-player-5.5.2.tgz#052b86b1eabe21ff331095ffffeae285fa7f7cb5"