{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Data visualization\n",
    "    \n",
    "Data visualization are based on [Matplotlib-FigureStream](https://figurestream.readthedocs.io/en/latest/), this interface inherits all features from it and extends the utilities with an specific ones.  \n",
    "All topics explained in [Data analysis](70-data_analysis.ipynb) are valid here too, since visualizations is a special case of _Data analysis_."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Bare minimum"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# visualizations backend classes\n",
    "from bci_framework.extensions.visualizations import EEGStream\n",
    "from bci_framework.extensions.visualizations.utils import loop_consumer\n",
    "\n",
    "# Main class must inherit `EEGStream`\n",
    "class Stream(EEGStream):  # This is `matplotlib.Figure` based class\n",
    "    def __init__(self):\n",
    "        # Initialize `StimuliAPI` class\n",
    "        super().__init__()\n",
    "        \n",
    "        # -------------------------------------------------------------\n",
    "        self.axis = self.add_subplot(111)\n",
    "        self.axis.set_title('Title')\n",
    "        self.axis.set_xlabel('Time')\n",
    "        self.axis.set_ylabel('Amplitude')\n",
    "        self.axis.grid(True)        \n",
    "        self.stream()\n",
    "        # -------------------------------------------------------------\n",
    "\n",
    "    @loop_consumer('eeg')\n",
    "    def stream(self, *args, **kwargs):\n",
    "        self.feed()\n",
    "\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    # Create and run the server\n",
    "    Stream()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<center>\n",
    "<img src='images/visualization_bare_minimum.png'></img>\n",
    "</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Data stream access\n",
    "\n",
    "The decorator `@loop_consumer` is explained in [Data analysis](70-data_analysis.ipynb#Data-stream-access)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "@loop_consumer('eeg')\n",
    "def stream(self, data):\n",
    "    eeg, aux = data\n",
    "\n",
    "    # First eeg channel\n",
    "    ch0 = eeg[0]\n",
    "    self.line.set_ydata(ch0)\n",
    "    self.axis.set_ylim(ch0.min(), ch0.max()) \n",
    "\n",
    "    # Time axis\n",
    "    time = range(len(ch0))\n",
    "    self.line.set_xdata(time)\n",
    "    self.axis.set_xlim(time[0], time[-1]) \n",
    "\n",
    "    self.feed()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<center>\n",
    "<img src='images/visualization-simple.gif'></img>\n",
    "</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So chaotic, lets create a buffer to visualize more than 100 samples."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Buffer\n",
    "\n",
    "We can use `self.create_buffer` to implement an automatic buffer with a fixed time view:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "self.create_buffer(30)  # create a buffer of 30 seconds "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The data can be accessed with `self.buffer_eeg`, `self.buffer_aux` and `self.buffer_timestamp`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "@loop_consumer('eeg')\n",
    "def stream(self):\n",
    "    eeg = self.buffer_eeg\n",
    "\n",
    "    # First eeg channel\n",
    "    ch0 = eeg[0]\n",
    "    self.line.set_ydata(ch0)\n",
    "    self.axis.set_ylim(-1, 1)\n",
    "\n",
    "    # Time axis\n",
    "    time = np.linspace(0, 30, len(ch0))\n",
    "    self.line.set_xdata(time)\n",
    "    self.axis.set_xlim(time[0], time[-1])\n",
    "\n",
    "    self.feed()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<center>\n",
    "<img src='images/visualization-full.gif'></img>\n",
    "</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Resampling\n",
    "\n",
    "This visualization has a big issue, there is a lot of data visualize, in this particular case it's sampling at `1000 kHz` and `30` seconds, which means `30000` points to visualize on the window with (likely) an inferior number of pixels, moreover, this amount of data will affect the speed of the visualization, `matplotlib` use all data to render the plot.\n",
    "\n",
    "We can resample the data, to speed up the visualization."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "self.create_buffer(30, resampling=1000)  # create a buffer of 30 seconds and a resampling to 1000 samples"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "@loop_consumer('eeg')\n",
    "def stream(self):\n",
    "    eeg = self.buffer_eeg_resampled\n",
    "\n",
    "    # First eeg channel\n",
    "    ch0 = eeg[0]\n",
    "    self.line.set_ydata(ch0)\n",
    "    self.axis.set_ylim(-1, 1)\n",
    "\n",
    "    # Time axis\n",
    "    time = np.linspace(0, 30, len(ch0))\n",
    "    self.line.set_xdata(time)\n",
    "    self.axis.set_xlim(time[0], time[-1])\n",
    "\n",
    "    self.feed()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<center>\n",
    "<img src='images/visualization-1000.gif'></img>\n",
    "</center>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "self.create_buffer(30, resampling=300) "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<center>\n",
    "<img src='images/visualization-300.gif'></img>\n",
    "</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Plot raw data\n",
    "\n",
    "There is a method to create `matplotlib.lines` automatically for the current configurations."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "self.axis, self.time, self.lines = self.create_lines(time=-30, window=1000)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "It will return the `axis` object the `time` array and a list of `lines` according to the number of channels.\n",
    "\n",
    "We can also configure the visualization with channel names as ticks."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "self.axis.set_ylim(0, len(prop.CHANNELS) + 1)\n",
    "self.axis.set_yticks(range(1, len(prop.CHANNELS) + 1))\n",
    "self.axis.set_yticklabels(prop.CHANNELS.values())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To update the visualization we only need to iterate over `self.lines` and `self.buffer_eeg_resampled`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "@loop_consumer('eeg')\n",
    "def stream(self):\n",
    "    eeg = self.buffer_eeg_resampled\n",
    "    for i, line in enumerate(self.lines):\n",
    "        line.set_data(self.time, eeg[i] + 1 + i)\n",
    "    self.feed()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<center>\n",
    "<img src='images/visualization-8ch.gif'></img>\n",
    "</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The complete example looks like:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from bci_framework.extensions.visualizations import EEGStream\n",
    "from bci_framework.extensions.data_analysis import loop_consumer\n",
    "from bci_framework.extensions import properties as prop\n",
    "\n",
    "import numpy as np\n",
    "\n",
    "class Stream(EEGStream):\n",
    "    \n",
    "    def __init__(self, *args, **kwargs):\n",
    "        super().__init__(*args, **kwargs)\n",
    "        DATAWIDTH = 1000\n",
    "\n",
    "        self.axis, self.time, self.lines = self.create_lines(time=-30, window=DATAWIDTH)\n",
    "        self.axis.set_title('Raw EEG')\n",
    "        self.axis.set_xlabel('Time')\n",
    "        self.axis.set_ylabel('Channels')\n",
    "        self.axis.grid(True)\n",
    "        self.axis.set_ylim(0, len(prop.CHANNELS) + 1)\n",
    "        self.axis.set_yticks(range(1, len(prop.CHANNELS) + 1))\n",
    "        self.axis.set_yticklabels(prop.CHANNELS.values())\n",
    "\n",
    "        self.create_buffer(30, resampling=DATAWIDTH)\n",
    "        self.stream()\n",
    "\n",
    "    @loop_consumer('eeg')\n",
    "    def stream(self):\n",
    "        eeg = self.buffer_eeg_resampled\n",
    "        for i, line in enumerate(self.lines):\n",
    "            line.set_data(self.time, eeg[i] + 1 + i)\n",
    "        self.feed()\n",
    "\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    Stream()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Plot a reverse buffer\n",
    "\n",
    "This option will optimize the image rendering, although the buffer will be transformed too."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<center>\n",
    "<img src='images/visualization-8ch_r.gif'></img>\n",
    "</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This feature can be activated with 2 simple changes, set the `time` positive:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "self.create_lines(time=30, window=1000)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And call `self.reverse_buffer` with the generated `axis`: "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "self.reverse_buffer(self.axis)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Here the full code:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from bci_framework.extensions.visualizations import EEGStream\n",
    "from bci_framework.extensions.data_analysis import loop_consumer\n",
    "from bci_framework.extensions import properties as prop\n",
    "\n",
    "import numpy as np\n",
    "\n",
    "class Stream(EEGStream):\n",
    "    \n",
    "    def __init__(self, *args, **kwargs):\n",
    "        \"\"\"\"\"\"\n",
    "        super().__init__(*args, **kwargs)\n",
    "        DATAWIDTH = 1000\n",
    "\n",
    "        self.axis, self.time, self.lines = self.create_lines(time=30, window=DATAWIDTH)\n",
    "        self.axis.set_title('Raw EEG')\n",
    "        self.axis.set_xlabel('Time')\n",
    "        self.axis.set_ylabel('Channels')\n",
    "        self.axis.grid(True)\n",
    "        self.axis.set_ylim(0, len(prop.CHANNELS) + 1)\n",
    "        self.axis.set_yticks(range(1, len(prop.CHANNELS) + 1))\n",
    "        self.axis.set_yticklabels(prop.CHANNELS.values())\n",
    "\n",
    "        self.create_buffer(30, resampling=DATAWIDTH, fill=np.nan)\n",
    "        self.reverse_buffer(self.axis)\n",
    "        self.stream()\n",
    "\n",
    "    @loop_consumer('eeg')\n",
    "    def stream(self):\n",
    "        eeg = self.buffer_eeg_resampled\n",
    "        for i, line in enumerate(self.lines):\n",
    "            line.set_data(self.time, eeg[i] + 1 + i)\n",
    "        self.feed()\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    Stream()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Topoplot\n",
    "\n",
    "Following the same instructions is possible to draw topoplots by setting the explicit axes."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from bci_framework.extensions.visualizations import EEGStream, loop_consumer\n",
    "import mne\n",
    "\n",
    "class Stream(EEGStream):\n",
    "\n",
    "    def __init__(self, *args, **kwargs):\n",
    "        super().__init__(*args, **kwargs)\n",
    "        self.axis = self.add_subplot(1, 1, 1)\n",
    "        self.tight_layout()\n",
    "        self.info = self.get_mne_info()\n",
    "        self.stream()\n",
    "\n",
    "    @loop_consumer('eeg')\n",
    "    def stream(self, data):\n",
    "        eeg, _ = data.value['data']\n",
    "        self.axis.clear()\n",
    "        mne.viz.plot_topomap(eeg.mean(axis=1) - eeg.mean(), self.info, axes=self.axis, show=False, outlines='skirt', cmap='cool')\n",
    "        self.feed()\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    Stream()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<center>\n",
    "<img src='images/visualization-topoplot.gif'></img>\n",
    "</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Skip frames\n",
    "\n",
    "Sometimes when the data processing is slow than acquisition, is necessary just to drop some data packages, this will prevent the buffer saturation and improve the visualization. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "@fake_loop_consumer('eeg')\n",
    "def stream(self, data, frame):\n",
    "    if (frame % 5) == 0:  # use only 1 of 5 packages\n",
    "        eeg, _ = data.value['data']\n",
    "        self.axis.clear()\n",
    "        mne.viz.plot_topomap(eeg.mean(axis=1) - eeg.mean(), self.info, axes=self.axis, show=False, outlines='skirt', cmap='cool')\n",
    "\n",
    "        self.feed()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<center>\n",
    "<img src='images/visualization-topoplot_slow.gif'></img>\n",
    "</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Framework integration\n",
    "\n",
    "In this interface is possible to load multiple visualizations at the same time (just keep an eye on your machine resources)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<center>\n",
    "<img src='images/visualization-integration.gif'></img>\n",
    "</center>"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
