{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "import panel as pn\n",
    "import pandas as pd\n",
    "import numpy as np\n",
    "import random\n",
    "from datetime import datetime, timedelta\n",
    "\n",
    "pn.extension('perspective')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The `Perspective` pane provides a powerful visualization component for large, real-time datasets built on the [Perspective Project](https://perspective.finos.org/). **`Perspective` brings *excel-like* capabilities to your data app**. Check out the [Perspective Examples Gallery](https://perspective.finos.org/examples/) for inspiration.\n",
    "\n",
    "The `Perspective` pane is a very good alternative to the [`Tabulator`](../widgets/Tabulator.ipynb) widget.\n",
    "\n",
    "## Parameters:\n",
    "\n",
    "For details on other options for customizing the component see the [layout](../../how_to/layout/index.md) and [styling](../../how_to/styling/index.md) how-to guides.\n",
    "\n",
    "* **`aggregates`** (dict): Aggregation spec, e.g. {x: \"distinct count\"}\n",
    "* **`columns`** (list or dict): List of column names to display or a dictionary with column configuration options.\n",
    "* **`columns_config`** (dict): Column configuration allowing specification of formatters, coloring and a variety of other attributes for each column.\n",
    "* **`editable`** (bool, default=True): Whether items are editable.\n",
    "* **`expressions`** (list): List of expressions, e.g. `['\"x\"+\"y\"']`\n",
    "* **`filters`** (list): A list of filters, e.g. `[[\"x\", \"<\", 3], [\"y\", \"contains\", \"abc\"]]`.\n",
    "* **`group_by`** (list): List of columns to group by, e.g. `[\"x\", \"y\"]`\n",
    "* **`object`** (dict or pd.DataFrame): The plot data declared as a dictionary of arrays or a DataFrame.\n",
    "* **`plugin_config`** (dict): Configuration for the PerspectiveViewerPlugin \n",
    "* **`plugin`** (str): The name of a plugin to display the data. For example `'datagrid'` or `'d3_xy_scatter'`.\n",
    "* **`selectable`** (bool, default=True): Whether rows are selectable\n",
    "* **`settings`** (bool, default=True): Whether to show the settings panel.\n",
    "* **`sort`** (list): List of sorting specs, e.g. `[[\"x\", \"desc\"]]`\n",
    "* **`split_by`** (list):  A list of columns to pivot by. e.g. `[\"x\", \"y\"]`\n",
    "* **`theme`** (str): The theme of the viewer, available options include `'pro'`, `'pro-dark'`, `'monokai'`, `'solarized'`, `'solarized-dark'` and `'vaporwave'`\n",
    "* **`title`** (str): Title for the Perspective Viewer.\n",
    "\n",
    "##### Callbacks\n",
    "\n",
    "* **`on_click`**: Allows registering callbacks which are given `PerspectiveClickEvent` objects containing the `config`, `column_names` and `row` of the clicked item.\n",
    "\n",
    "___\n",
    "\n",
    "The `Perspective` pane renders columns of data specified as a dictionary of lists or arrays and pandas DataFrames:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "tags": []
   },
   "source": [
    "## Basic Examples"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "data = {\n",
    "    'int': [random.randint(-10, 10) for _ in range(9)],\n",
    "    'float': [random.uniform(-10, 10) for _ in range(9)],\n",
    "    'date': [(datetime.now() + timedelta(days=i)).date() for i in range(9)],\n",
    "    'datetime': [(datetime.now() + timedelta(hours=i)) for i in range(9)],\n",
    "    'category': ['Category A', 'Category B', 'Category C', 'Category A', 'Category B',\n",
    "             'Category C', 'Category A', 'Category B', 'Category C',],\n",
    "    'link': ['https://panel.holoviz.org/', 'https://discourse.holoviz.org/', 'https://github.com/holoviz/panel']*3,\n",
    "}\n",
    "df = pd.DataFrame(data)\n",
    "\n",
    "pn.pane.Perspective(df, width=1000)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "tags": []
   },
   "source": [
    "Try interacting with the `Perspective` pane.\n",
    "\n",
    "- The three vertical dots in the upper, left corner will toggle the *configuration menu*\n",
    "- The three vertical lines at the top of each column will toggle a *column configuration menu*.\n",
    "- The top menu will provide you with options to change the *plugin* as well as *group*, *split*, *order* and *filter* your data\n",
    "- The bottom menu will provide you with options to *reset*, *download* and *copy* as well as *change the theme*"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "tags": []
   },
   "source": [
    "The `index` is shown by default. If you want to not display it by default, you can provide the list of `columns` to display."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "pn.pane.Perspective(df, columns=list(df.columns), width=1000)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "tags": []
   },
   "source": [
    "You might also hide the *config menu* via the `settings` parameter."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "pn.pane.Perspective(df, columns=[\"float\"], group_by=[\"category\"], plugin='d3_y_bar', settings=False, width=1000, height=300)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Try interacting with the `Perspective` pane by toggling the *config menu* via the 3 vertical dots in the upper, left corner."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Plugin Configuration\n",
    "\n",
    "You can configure the active *plugin* manually as shown for the *Datagrid* below\n",
    "\n",
    "<img src=\"../../assets/perspective_edit.png\" style=\"margin-left: auto; margin-right: auto; display: block;\"></img>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "tags": []
   },
   "source": [
    "You can also configure the *columns* configuration programmatically via the `columns_config` parameter"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "pn.pane.Perspective(df, columns=list(df.columns), width=1000, columns_config={\n",
    "    'int': {'number_fg_mode': 'color', 'neg_fg_color': '#880808', 'pos_fg_color': '#008000', \"fixed\": 0},\n",
    "    'float': {'number_fg_mode': \"bar\", 'neg_fg_color': '#880808', 'pos_fg_color': '#008000', 'fg_gradient': 7.93,  },\n",
    "    'category': {'string_color_mode': 'series', 'format': 'italics'},\n",
    "    'date': {\"dateStyle\": \"short\", \"datetime_color_mode\": \"foreground\", \"color\": \"#008000\"},\n",
    "    'datetime': {\"timeZone\": \"Asia/Shanghai\", \"dateStyle\": \"full\", \"timeStyle\": \"full\", \"datetime_color_mode\": \"background\", \"color\": \"#880808\"},\n",
    "    'link': {'format': 'link', 'string_color_mode': 'foreground', 'color': '#008000'},\n",
    "})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "tags": []
   },
   "source": [
    "Please note\n",
    "\n",
    "- You can also use *named* colors like 'green' when you provide the `plugin_config`. But if you do, then they will not be set in the *color picker* in the *column config menu*.\n",
    "\n",
    "For more detail about the available options see the [Column Configuration Options Section](#column-configuration-options) below."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Timezone Handling\n",
    "\n",
    "The underlying Perspective Viewer assumes *non-timezone* aware datetimes are in UTC time. And it displays them in your local time zone by default.\n",
    "\n",
    "If your data is not time-zone aware you can make them. My servers time zone is 'cet' and I can make them aware as shown below."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "df_aware = df.copy(deep=True)\n",
    "df_aware['datetime'] = df_aware['datetime'].dt.tz_localize(\"CET\")\n",
    "pn.Column(df_aware.head(3), pn.pane.Perspective(df_aware, columns=list(df.columns), width=1000))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "tags": []
   },
   "source": [
    "As shown in the section above you can then force the datetimes to be shown in a specific timezone."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "pn.pane.Perspective(df_aware, width=1000, columns=list(df.columns), plugin_config={'columns': {'datetime': {\"timeZone\": \"Europe/London\", \"timeStyle\": \"full\"}}})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Streaming and Patching"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "tags": []
   },
   "source": [
    "The `Perspective` pane also supports `stream` and `patch` methods allowing us to efficiently update the data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "df_stream = pd.DataFrame(np.random.randn(400, 4), columns=list('ABCD')).cumsum()\n",
    "\n",
    "stream_perspective = pn.pane.Perspective(\n",
    "    df_stream, plugin='d3_y_line', columns=['A', 'B', 'C', 'D'], theme='pro-dark',\n",
    "    sizing_mode='stretch_width', height=500, margin=0\n",
    ")\n",
    "\n",
    "stream_perspective"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Lets start the streaming.\n",
    "\n",
    "The amount of data to keep in the streaming buffer can be controlled via the `rollover` option:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "rollover = pn.widgets.IntInput(label='Rollover', value=500)\n",
    "\n",
    "def stream():\n",
    "    data = df_stream.iloc[-1] + np.random.randn(4)\n",
    "    stream_perspective.stream(data, rollover.value)\n",
    "\n",
    "cb = pn.state.add_periodic_callback(stream, 50)\n",
    "\n",
    "pn.Row(cb.param.period, rollover)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Alternatively we can also `patch` the data:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "mixed_df = pd.DataFrame({'A': np.arange(10), 'B': np.random.rand(10), 'C': [f'foo{i}' for i in range(10)]})\n",
    "\n",
    "perspective = pn.pane.Perspective(mixed_df, columns=list(mixed_df), height=500)\n",
    "\n",
    "perspective"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The easiest way to patch the data is by supplying a dictionary as the patch value. The dictionary should have the following structure:\n",
    "\n",
    "```python\n",
    "{\n",
    "    column: [\n",
    "        (index: int or slice, value),\n",
    "        ...\n",
    "    ],\n",
    "    ...\n",
    "}\n",
    "```\n",
    "    \n",
    "As an example, below we will patch the 'A' and 'C' columns. On the `'A'` column we will replace the 0th row and on the `'C'` column we replace the first two rows:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "perspective.patch({'A': [(0, 3)], 'C': [(slice(0, 1), 'bar')]})\n",
    "\n",
    "perspective"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Deleting rows can be achieved by streaming the data you want to become visible and setting rollover equal to the row count of new data. Effectively, deleting old rows. Removing specific rows by index in a similar manner as patching is currently not supported."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "data = {'A': np.arange(10)}\n",
    "\n",
    "perspective = pn.pane.Perspective(data, height=500)\n",
    "\n",
    "perspective"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "smaller_data = {'A': np.arange(5)}\n",
    "\n",
    "perspective.stream(smaller_data, rollover=5)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Controls\n",
    "\n",
    "The `Perspective` pane exposes a number of options which can be changed from both Python and Javascript.\n",
    "\n",
    "<div class=\"alert alert-block alert-info\"> <b>NOTE</b> You can use the controls widget below to effectively explore parameters including the `plugin` and `plugin_config` parameter! Use this to avoid a time-consuming trial and error workflows!</div>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "component = pn.pane.Perspective(df, columns=list(df.columns), width=1000)\n",
    "pn.Row(component.controls(), component, height=800)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Plugin and Column Configuration Options\n",
    "\n",
    "The *plugin and column configurations options* of the underlying FinOS Perspective viewer are not well documented. The best way to find them is\n",
    "\n",
    "- Exploring interactively via a *Controls* widget like the one above.\n",
    "- Exploring the [Perspective Examples Gallery](https://perspective.finos.org/examples/).\n",
    "- Reverse engineering them from the [Perspective GitHub repository](https://github.com/finos/perspective).\n",
    "  - For example for the `Datagrid` plugin, the options for configuring a *number* column are defined in [number_column_style.rs](https://github.com/finos/perspective/blob/master/rust/perspective-viewer/src/rust/config/number_column_style.rs).\n",
    "\n",
    "\n",
    "\n",
    "Below we list some of the most useful options we have been able to find and seen working.\n",
    "\n",
    "<div class=\"alert alert-block alert-info\"> <b>NOTE</b> Some of the options below are <em>camel cased<em> like `timeZone`. We expect this to be a bug and be fixed by FinOS Perspective some day. If a <em>camel cased</em> option stops working try <em>snake casing</em> it. For example to `time_zone`. Please report changes on <a href=\"https://github.com/holoviz/panel/issues\" target=\"_blank\">Github</a></div>\n",
    "\n",
    "### `columns_config`\n",
    "\n",
    "The Perspective API documentation does provide [types for most configuration options](https://perspective.finos.org/docs/obj/perspective-viewer/#perspectivecolumnconfigvalue):\n",
    "\n",
    "```\n",
    "bg_gradient?\tnumber\n",
    "color?\tstring\n",
    "date_format?\tDateFormat\n",
    "datetime_color_mode?\t\"foreground\" | \"background\"\n",
    "fg_gradient?\tnumber\n",
    "fixed?\tnumber\n",
    "format?\t\"link\" | \"image\" | \"bold\" | \"italics\" | \"custom\"\n",
    "neg_bg_color?\tstring\n",
    "neg_fg_color?\tstring\n",
    "number_bg_mode?\t\"color\" | \"gradient\" | \"pulse\" | \"disabled\"\n",
    "number_fg_mode?\t\"color\" | \"bar\" | \"disabled\"\n",
    "number_format?\tNumberFormat\n",
    "pos_bg_color?\tstring\n",
    "pos_fg_color?\tstring\n",
    "string_color_mode?\t\"foreground\" | \"background\" | \"series\"\n",
    "symbols?\tRecord<string, string>\n",
    "```\n",
    "\n",
    "### Datagrid\n",
    "\n",
    "#### Datetime Column ([source](https://github.com/finos/perspective/blob/master/rust/perspective-viewer/src/rust/config/datetime_column_style.rs))\n",
    "\n",
    "- `timeZone`: (A valid time zone like 'Africa/Tunis', 'Asia/Shanghai', 'Europe/Berlin' etc.)\n",
    "- `dateStyle`: ('full', 'long', 'medium', 'short', 'disabled')\n",
    "- `timeStyle`: ('full', 'long', 'medium', 'short', 'disabled')\n",
    "- `datetime_color_mode` ('foreground', 'background')\n",
    "  - `color` (str)\n",
    "  \n",
    "#### Number Column ([Source](https://github.com/finos/perspective/blob/master/rust/perspective-viewer/src/rust/config/number_column_style.rs))\n",
    "\n",
    "- Precision:\n",
    "  - `fixed` (int)\n",
    "- Foreground:\n",
    "  - `number_fg_mode` ('disabled', 'color', 'bar')\n",
    "  - `pos_fg_color` (hex string), `neg_fg_color` (hex string), `fg_gradient` (float)\n",
    "- Background:\n",
    "  - `number_bg_mode` ('disabled', 'color', 'gradient', 'pulse')\n",
    "  - `pos_bg_color` (str), `neg_bg_color` (str), `bg_gradient` (float)\n",
    "  \n",
    "#### String Column ([source](https://github.com/finos/perspective/blob/master/rust/perspective-viewer/src/rust/config/string_column_style.rs))\n",
    "\n",
    "- `string_color_mode`: ('background', 'foreground', 'series')\n",
    "  - `color` (str)\n",
    "- `format`: ('bold', 'link', 'italics')\n",
    "  - The [source](https://github.com/finos/perspective/blob/master/rust/perspective-viewer/src/rust/config/string_column_style.rs) code also exposes an 'image' option. But I've not been able to get it working. If you can please [let us know](https://github.com/holoviz/panel/issues)."
   ]
  }
 ],
 "metadata": {
  "language_info": {
   "name": "python",
   "pygments_lexer": "ipython3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
