{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "3278098f-2378-4a4b-9805-51c0f3db0f05",
   "metadata": {},
   "outputs": [],
   "source": [
    "from seeq import spy\n",
    "import pandas as pd\n",
    "import plotly.express as px\n",
    "import plotly.graph_objects as go\n",
    "import solara\n",
    "import os\n",
    "import urllib.parse as urlparse\n",
    "from urllib.parse import parse_qs\n",
    "from requests.models import PreparedRequest"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "c81cb316-55fa-42a4-8c83-3e640ef6cdae",
   "metadata": {},
   "outputs": [],
   "source": [
    "def generate_chart(df=None, signal_map=None, jitter=1, whiskerwidth=1, show_box_points=False, title=None, height=500, width=1200):\n",
    "    \"\"\"\n",
    "    Generate a box plot chart using Plotly.\n",
    "\n",
    "    Args:\n",
    "        df (pandas.DataFrame): A DataFrame containing the data to be plotted.\n",
    "        signal_map (dict): A dictionary mapping column names to dictionaries containing asset, name, and color information.\n",
    "        jitter (float, optional): The amount of jitter to apply to the box points. Defaults to 1.\n",
    "        whiskerwidth (float, optional): The width of the whiskers. Defaults to 1.\n",
    "        show_box_points (bool, optional): Whether to show individual box points. Defaults to False.\n",
    "        title (str, optional): The title of the chart. Defaults to None.\n",
    "        height (int, optional): The height of the chart in pixels. Defaults to 500.\n",
    "\n",
    "    Returns:\n",
    "        plotly.graph_objects.Figure: A Plotly figure object containing the box plot chart.\n",
    "    \"\"\"\n",
    "    \n",
    "    # Create a figure object once and update it\n",
    "    fig = go.Figure()\n",
    "\n",
    "    # Use a list comprehension to create traces efficiently\n",
    "    traces = [\n",
    "        go.Box(\n",
    "            y=series,\n",
    "            name=f\"{signal_map[name]['Asset']} >> {signal_map[name]['Name']}\",\n",
    "            marker_color=signal_map[name][\"Color\"],\n",
    "            boxmean=\"sd\",\n",
    "            boxpoints='all' if show_box_points else None,\n",
    "            jitter=jitter,\n",
    "            whiskerwidth=whiskerwidth\n",
    "        )\n",
    "        for name, series in df.items()\n",
    "    ]\n",
    "\n",
    "    # Add traces to the figure object\n",
    "    fig.add_traces(traces)\n",
    "\n",
    "    # Update layout properties\n",
    "    fig.update_layout(\n",
    "        title=title,\n",
    "        height=height,\n",
    "        width=width,\n",
    "        # autosize=True,\n",
    "        showlegend=False,\n",
    "    )\n",
    "    fig.update_annotations(\n",
    "        font=dict(family=\"Helvetica\", size=12),\n",
    "        xanchor='left',\n",
    "        x=0.0\n",
    "                          )\n",
    "\n",
    "    return fig"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "6689b6cb",
   "metadata": {},
   "outputs": [],
   "source": [
    "def parse_jupyter_notebook_url(jupyter_notebook_url, backup_workbench):\n",
    "    \"\"\"\n",
    "    Parses the Jupyter Notebook URL and returns the workbook ID and worksheet ID.\n",
    "\n",
    "    Args:\n",
    "        jupyter_notebook_url (str): The URL of the Jupyter Notebook.\n",
    "        backup_workbench (str): The URL of a backup Seeq Worksheet with signals in the details pane.\n",
    "\n",
    "    Returns:\n",
    "        tuple: A tuple containing the workbook ID and worksheet ID.\n",
    "    \"\"\"\n",
    "    parsed_url = urlparse.urlparse(jupyter_notebook_url)\n",
    "    query_params = parse_qs(parsed_url.query)\n",
    "\n",
    "    if not query_params:\n",
    "        # Assuming it's not being run as an Add-on and there are no query parameters\n",
    "        # Manually including defaults from the backup_workbench\n",
    "        url = spy.utils.get_data_lab_project_url()\n",
    "        workbook_id = spy.utils.get_workbook_id_from_url(backup_workbench)\n",
    "        worksheet_id = spy.utils.get_worksheet_id_from_url(backup_workbench)\n",
    "        params = {'workbookId': workbook_id, 'worksheetId': worksheet_id}\n",
    "        req = PreparedRequest()\n",
    "        req.prepare_url(url, params)\n",
    "        parsed_url = urlparse.urlparse(req.url)\n",
    "        query_params = parse_qs(parsed_url.query)\n",
    "\n",
    "    workbook_id = query_params['workbookId'][0]\n",
    "    worksheet_id = query_params['worksheetId'][0]\n",
    "\n",
    "    return workbook_id, worksheet_id\n",
    "\n",
    "def create_df(workbook_id, worksheet_id):\n",
    "    \"\"\"\n",
    "    Fetches worksheet data from a Seeq server and returns a DataFrame.\n",
    "\n",
    "    Args:\n",
    "        workbook_id (str): The ID of the workbook.\n",
    "        worksheet_id (str): The ID of the worksheet.\n",
    "\n",
    "    Returns:\n",
    "        pandas.DataFrame: The fetched data DataFrame.\n",
    "    \"\"\"\n",
    "    host = os.environ.get('SEEQ_HOST', spy.client.host[:-3])\n",
    "    url = f\"{host}workbook/{workbook_id}/worksheet/{worksheet_id}\"\n",
    "\n",
    "    # Fetch worksheet items and filter out string units of measure\n",
    "    worksheet_items = spy.search(url, quiet=True)\n",
    "    worksheet_items = worksheet_items[worksheet_items['Value Unit Of Measure'] != 'string']\n",
    "\n",
    "    # Pull the workbook and specific worksheet\n",
    "    workbook = spy.workbooks.pull(workbook_id,\n",
    "                                  include_inventory=False,\n",
    "                                  include_annotations=False,\n",
    "                                  include_referenced_workbooks=False,\n",
    "                                  include_images=False,\n",
    "                                  include_rendered_content=False,\n",
    "                                  specific_worksheet_ids=[worksheet_id],\n",
    "                                  quiet=True)[0]\n",
    "\n",
    "    worksheet = workbook.worksheets[0]\n",
    "\n",
    "    # Update items to include the axis color\n",
    "    display_items = worksheet.display_items[['ID', 'Color']]\n",
    "    worksheet_items = pd.merge(worksheet_items, display_items, on='ID')\n",
    "\n",
    "    # Get Display Range\n",
    "    display_range = worksheet.display_range\n",
    "\n",
    "    # Fetch data for the selected worksheet items\n",
    "    df = spy.pull(worksheet_items,\n",
    "                  start=display_range['Start'],\n",
    "                  end=display_range['End'],\n",
    "                  header=\"ID\",\n",
    "                  grid=None,\n",
    "                  quiet=True)\n",
    "\n",
    "    return df, worksheet_items\n",
    "\n",
    "def signal_info_to_dict(worksheet_items):\n",
    "    \"\"\"\n",
    "    Creates a signal map dictionary from the worksheet items.\n",
    "\n",
    "    Args:\n",
    "        worksheet_items (pandas.DataFrame): The worksheet items DataFrame.\n",
    "\n",
    "    Returns:\n",
    "        dict: The signal map dictionary.\n",
    "    \"\"\"\n",
    "    signal_map = worksheet_items.set_index('ID').apply(lambda x: x.to_dict(), axis=1).to_dict()\n",
    "    return signal_map\n",
    "\n",
    "def get_worksheet_data(jupyter_notebook_url, backup_workbench):\n",
    "    \"\"\"\n",
    "    Fetches worksheet data and signal map from a Seeq server based on the provided Jupyter Notebook URL.\n",
    "\n",
    "    Args:\n",
    "        jupyter_notebook_url (str): The URL of the Jupyter Notebook.\n",
    "        backup_workbench (str): The URL of a backup Seeq Worksheet with signals in the details pane.\n",
    "\n",
    "    Returns:\n",
    "        tuple: A tuple containing the fetched data DataFrame and the signal map dictionary.\n",
    "    \"\"\"\n",
    "    workbook_id, worksheet_id = parse_jupyter_notebook_url(jupyter_notebook_url, backup_workbench)\n",
    "    df , worksheet_items= create_df(workbook_id, worksheet_id)\n",
    "    signal_info = signal_info_to_dict(worksheet_items)\n",
    "\n",
    "    return df, signal_info\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "a27c01c5-eed2-4634-bdc5-21603a42de7e",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div style=\"background-color: #FFFFCC; color:black;\">request_origin_label argument was not specified, which means that it will be more difficult to track data consumption back to this script. Please supply a descriptive label (such as request_origin_label=\"My Cool Script\") to help your friendly Seeq administrators.</div><div style=\"background-color: #EEFFEE;color:black; text-align: left;\">Logged in to <strong>https://develop.seeq.dev</strong> as <strong>John.Garramone@seeq.com (John Garramone [Admin])</strong>.<br>Seeq Server Version: <strong>R66.0.0-v202408090123-CD</strong><br>Seeq SDK Module Version: <strong>66.0.0</strong> @ /Users/jgarramone/Documents/sandbox/crabBranches/PROJECTS/exampleAddOnTool/.venv/lib/python3.11/site-packages/seeq/sdk<br>Seeq SPy Module Version: <strong>192.33</strong> @ /Users/jgarramone/Documents/sandbox/crabBranches/PROJECTS/exampleAddOnTool/.venv/lib/python3.11/site-packages/seeq/spy</div>"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "b36794a0a20a4bcf91b42b6d635d2d5e",
       "version_major": 2,
       "version_minor": 0
      },
      "text/html": [
       "Cannot show widget. You probably want to rerun the code cell above (<i>Click in the code cell, and press Shift+Enter <kbd>⇧</kbd>+<kbd>↩</kbd></i>)."
      ],
      "text/plain": [
       "Cannot show ipywidgets in text"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "@solara.component\n",
    "def BoxPlotComponent():\n",
    "    # State variables\n",
    "    whisker_width, set_whisker_width = solara.use_state(1)\n",
    "    show_data_points, set_show_data_points = solara.use_state(False)\n",
    "    selected_jitter, set_jitter = solara.use_state(1)  \n",
    "    error_message, set_error_message = solara.use_state(None)\n",
    "\n",
    "    def validate_whisker_width(value):\n",
    "        if value > 1:\n",
    "            set_error_message(\"Whisker width cannot be greater than 1\")\n",
    "        else:\n",
    "            set_error_message(None)\n",
    "            set_whisker_width(value)\n",
    "\n",
    "    def render_controls():\n",
    "        with solara.HBox() as controls:\n",
    "            solara.Select(\n",
    "                label=\"Show Samples\",\n",
    "                values=[True, False],\n",
    "                value=show_data_points,\n",
    "                on_value=set_show_data_points,\n",
    "            )\n",
    "\n",
    "            if show_data_points:\n",
    "                solara.Select(\n",
    "                    label=\"Jitter\",\n",
    "                    values=[0, 0.1, 0.5, 1],\n",
    "                    value=selected_jitter,\n",
    "                    on_value=set_jitter,\n",
    "                )\n",
    "\n",
    "            solara.InputFloat(\n",
    "                label=\"Whisker Width\",\n",
    "                value=whisker_width,\n",
    "                on_value=validate_whisker_width,\n",
    "            )\n",
    "\n",
    "        return controls\n",
    "\n",
    "    def render_chart():\n",
    "        fig = generate_chart(\n",
    "            df=df,\n",
    "            jitter=selected_jitter,\n",
    "            show_box_points=show_data_points,\n",
    "            signal_map=signal_info,\n",
    "            whiskerwidth=whisker_width,\n",
    "            height=800,\n",
    "            width=1200,\n",
    "        )\n",
    "        solara.FigurePlotly(fig, dependencies=[selected_jitter, show_data_points, whisker_width])\n",
    "\n",
    "    with solara.Div() as main:\n",
    "        with solara.AppBarTitle():\n",
    "            solara.Text(\"Box Plot\")\n",
    "\n",
    "        if error_message:\n",
    "            solara.Error(\n",
    "                f\"{error_message}\",\n",
    "                text=False,\n",
    "                dense=True,\n",
    "                outlined=False,\n",
    "                icon=True,\n",
    "            )\n",
    "\n",
    "        render_controls()\n",
    "        render_chart()\n",
    "\n",
    "    return main\n",
    "\n",
    "\n",
    "BoxPlotComponent()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "72d173f3-f21e-4bad-99c6-6d2c412fe296",
   "metadata": {},
   "outputs": [],
   "source": [
    "df, signal_info = get_worksheet_data(jupyter_notebook_url=jupyter_notebook_url)\n",
    "\n",
    "# The following line is required when running the code in a Jupyter notebook:\n",
    "BoxPlotComponent()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "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.11.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
