{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Fileinput Exploration with Pandas and Plotly\n",
    "\n",
    "In Panel the FileDownload widget allows downloading a file generated on the server the app is running on while the `FileInput` widget allows uploading a file. In this example we demonstrate a pipeline of two little apps one which allows generating a sample data CSV file and one which allows uploading this file and displays it as a Plotly plot. \n",
    "\n",
    "For more details on how to use these components see  [`FileInput`](../../reference/widgets/FileInput.ipynb) and [`FileDownload`](../../reference/widgets/FileDownload.ipynb) reference guides.\n",
    "\n",
    "**Authors**: [Marc Skov Madsen](https://datamodelsanalytics.com/) and Philipp Rudiger"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import io\n",
    "import param\n",
    "import panel as pn\n",
    "import pandas as pd\n",
    "import random\n",
    "\n",
    "from datetime import datetime, timedelta\n",
    "\n",
    "import plotly.express as px\n",
    "\n",
    "pn.extension('plotly')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Lets start out by creating some sample data by defining some parameters which declare bounds on the values to generate along with a [`FileDownload`](../../reference/widgets/FileDownload.ipynb) widget which will allow the user to download the data onto their machine."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class SampleDataApp(param.Parameterized):\n",
    "    \n",
    "    samples = param.Integer(default=40, bounds=(0,100), doc=\"\"\"\n",
    "      Number of data samples to generate.\"\"\")\n",
    "    \n",
    "    voltage_bounds=param.Range(default=(0,100), bounds=(0,1000), doc=\"\"\"\n",
    "      The bounds of the voltage values to generate.\"\"\")\n",
    "\n",
    "    time_bounds=param.CalendarDateRange(\n",
    "        default=(datetime(2020, 2, 1), datetime(2020, 2, 26)),\n",
    "        bounds=(datetime(2020, 1, 1), datetime(2020, 3, 26)),\n",
    "        doc=\"The bounds of the time values to generate.\")\n",
    "    \n",
    "    fub_ids = param.ListSelector(\n",
    "        default=[\"a1\", \"b1\", \"b2\"], objects=[\"a1\", \"b1\", \"b2\"], doc=\"\"\"\n",
    "      The IDS to generate.\"\"\")\n",
    "    \n",
    "    sample_df = param.DataFrame(doc=\"\"\"\n",
    "      The current dataframe of samples.\"\"\")\n",
    "    \n",
    "    generate_sample_df = param.Action(lambda self: self.update_sample_df(), doc=\"\"\"\n",
    "      An action callback which will update the sample_df.\"\"\")\n",
    "    \n",
    "    file_name = param.String(default=\"sample_data.csv\", doc=\"\"\"\n",
    "      The filename to save to.\"\"\")\n",
    "        \n",
    "    def __init__(self, **params):\n",
    "        super().__init__(**params)\n",
    "        self.update_sample_df()\n",
    "        self.download = pn.widgets.FileDownload(filename=self.file_name,\n",
    "                                                callback=self._download_callback)\n",
    "\n",
    "    @pn.depends('file_name', watch=True)\n",
    "    def _update_filename(self):\n",
    "        self.download.filename = self.file_name\n",
    "\n",
    "    def _download_callback(self):\n",
    "        \"\"\"\n",
    "        A FileDownload callback will return a file-like object which can be serialized\n",
    "        and sent to the client.\n",
    "        \"\"\"\n",
    "        self.download.filename = self.file_name\n",
    "        sio = io.StringIO()\n",
    "        self.sample_df.to_csv(sio, index=False)\n",
    "        sio.seek(0)\n",
    "        return sio\n",
    "        \n",
    "    def update_sample_df(self, event=None):\n",
    "        start = self.time_bounds[0]\n",
    "        end = self.time_bounds[1]\n",
    "        days = (end-start).days\n",
    "        \n",
    "        sample_data = {\n",
    "            \"Time\": [start+timedelta(days=random.uniform(0,days)) for _ in range(0,self.samples)],\n",
    "            \"Voltage\": [random.uniform(*self.voltage_bounds) for _ in range(0,self.samples)],\n",
    "            \"FubId\": [random.choice(self.fub_ids) for _ in range(0,self.samples)],\n",
    "        }\n",
    "        self.sample_df = pd.DataFrame(sample_data) \n",
    "        \n",
    "    \n",
    "    def save_sample_data(self, event=None):\n",
    "        if not self.sample_df is None:\n",
    "            self.sample_df\n",
    "            \n",
    "    def view(self):\n",
    "        return pn.Column(\n",
    "            pn.Row(\n",
    "                pn.Param(self, parameters=['samples', 'voltage_bounds', 'time_bounds']),\n",
    "                pn.Column(self.param.generate_sample_df, self.param.file_name, self.download, align='end')\n",
    "            ),\n",
    "            self.param.sample_df,\n",
    "        )\n",
    "\n",
    "sample_data_app = SampleDataApp()\n",
    "sample_data_app.view()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Click the `Save sample df` button**\n",
    "\n",
    "This should save the dataframe to your default download folder. Now let us define the `VoltageApp` which will display the data we just generated."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class VoltageApp(param.Parameterized):\n",
    "    data = param.DataFrame()\n",
    "    \n",
    "    file_input = param.Parameter()\n",
    "    \n",
    "    def __init__(self, **params):\n",
    "        self.param.file_input.default = pn.widgets.FileInput()\n",
    "        super().__init__(**params)\n",
    "        self.plotly_pane = pn.pane.Plotly(height=400, sizing_mode=\"stretch_width\")\n",
    "\n",
    "    @pn.depends(\"file_input.value\", watch=True)\n",
    "    def _parse_file_input(self):\n",
    "        value = self.file_input.value\n",
    "        string_io = io.StringIO(value.decode(\"utf8\"))\n",
    "        self.data = pd.read_csv(string_io, parse_dates=[\"Time\"])\n",
    "\n",
    "    @pn.depends('data', watch=True)\n",
    "    def get_plot(self):\n",
    "        df = self.data\n",
    "        if df is None:\n",
    "            return\n",
    "        assert (\"Voltage\" in df.columns) and (\"Time\" in df.columns), \"no columns voltage and time\"\n",
    "        df = (df.loc[df['Voltage'] != 'Invalid/Calib']).copy(deep=True)\n",
    "        df['Voltage'] = df['Voltage'].astype(float)\n",
    "        if \"FubId\" in df.columns:\n",
    "            p = px.scatter(df, x=\"Time\", y=\"Voltage\", color=\"FubId\")\n",
    "        else:\n",
    "            p = px.scatter(df, x=\"Time\", y=\"Voltage\")\n",
    "        self.plotly_pane.object = p\n",
    "        \n",
    "    def view(self):\n",
    "        return pn.Column(\n",
    "            self.file_input,\n",
    "            self.plotly_pane,\n",
    "        )\n",
    "    \n",
    "voltage_app = VoltageApp()\n",
    "\n",
    "voltage_app.view()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now let us put these two components together into a servable app:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sample_data_app = SampleDataApp()\n",
    "voltage_app = VoltageApp()\n",
    "\n",
    "description = \"\"\"\n",
    "This application demonstrates the ability to download a file using the FileDownload widget \n",
    "and uploading a file using the FileInput widget.\n",
    "</br></br>\n",
    "Try filtering the data on the left, download the file by clicking on the Download button\n",
    "and then plot it on the right by uploading that same file.\n",
    "\"\"\"\n",
    "\n",
    "pn.Row(\n",
    "    sample_data_app.view(),\n",
    "    pn.Column(\n",
    "        description,\n",
    "        voltage_app.view(),\n",
    "    ),\n",
    "    sizing_mode='stretch_both'\n",
    ").servable()"
   ]
  }
 ],
 "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.7.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
