{
 "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."
   ]
  },
  {
   "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', 'tabulator', sizing_mode=\"stretch_width\")"
   ]
  },
  {
   "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(), label=\"Generate Data\", doc=\"\"\"\n",
    "      An action callback which will update the sample_df.\"\"\")\n",
    "    \n",
    "    file_name = param.String(default=\"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(\n",
    "            name=\"Download Data\", filename=self.file_name,\n",
    "            callback=self._download_callback, button_type=\"primary\"\n",
    "        )\n",
    "        self.table = pn.widgets.Tabulator(\n",
    "            self.sample_df.head(10), layout='fit_data_stretch', theme='site', height=360\n",
    "        )\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",
    "    @pn.depends(\"sample_df\", watch=True)\n",
    "    def _update_table(self):\n",
    "        if hasattr(self, \"table\"):\n",
    "            self.table.value = self.sample_df.head(10)\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",
    "            \"## Generate and Download Data\",\n",
    "            pn.Row(\n",
    "                pn.Param(self, parameters=['samples', 'voltage_bounds', 'time_bounds', 'generate_sample_df'], show_name=False, widgets={\"generate_sample_df\": {\"button_type\": \"primary\"}}),\n",
    "                pn.Column(self.param.file_name, self.download, align='end', margin=(10,5,5,5)),\n",
    "            ),\n",
    "            \"**Sample data (10 Rows)**\",\n",
    "            self.table,\n",
    "        )\n",
    "\n",
    "sample_data_app = SampleDataApp()\n",
    "sample_data_app_view = sample_data_app.view()\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",
    "        super().__init__(file_input=pn.widgets.FileInput(), **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",
    "        if value:\n",
    "            string_io = io.StringIO(value.decode(\"utf8\"))\n",
    "            self.data = pd.read_csv(string_io, parse_dates=[\"Time\"])\n",
    "        else:\n",
    "            print(\"error\")\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",
    "            \"## Upload and Plot Data\",\n",
    "            self.file_input,\n",
    "            self.plotly_pane,\n",
    "        )\n",
    "    \n",
    "voltage_app = VoltageApp()\n",
    "\n",
    "voltage_app_view = voltage_app.view()\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": [
    "description = \"\"\"\n",
    "This application demonstrates the ability to **download** a file using the `FileDownload` widget \n",
    "and **upload** a file using the `FileInput` widget.\n",
    "</br></br>\n",
    "Try filtering the data, 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",
    "component = pn.Column(\n",
    "    description,\n",
    "    sample_data_app_view,\n",
    "    voltage_app_view,\n",
    "    sizing_mode='stretch_both'\n",
    ")\n",
    "component"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## App\n",
    "\n",
    "Lets wrap it into nice template that can be served via `panel serve download_upload_csv.ipynb`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pn.template.FastListTemplate(site=\"Panel\", title=\"Download and Upload CSV File\", main=[ description, sample_data_app_view, voltage_app_view,]).servable();"
   ]
  }
 ],
 "metadata": {
  "language_info": {
   "name": "python",
   "pygments_lexer": "ipython3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
