{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "opposed-review",
   "metadata": {},
   "source": [
    "# Developing Custom Models\n",
    "\n",
    "Panel ships with a number of custom Bokeh models, which have both Python and Javascript components. When developing Panel these custom models have to be compiled. This happens automatically with `pip install -e .` or `python setup.py develop`, however when runnning actively developing you can rebuild the extension with `panel build panel`. The build command is just an alias for `bokeh build`; see the [Bokeh developer guide](https://docs.bokeh.org/en/latest/docs/dev_guide/setup.html) for more information about developing bokeh models or the [Awesome Panel - Bokeh Extensions Guide](https://awesome-panel.readthedocs.io/en/latest/guides/awesome-panel-extensions-guide/bokeh-extensions.html)\n",
    "\n",
    "Just like any other Javascript (or Typescript) library Panel defines a `package.json` and `package-lock.json` files. When adding, updating or removing a dependency in the `package.json` file ensure you commit the changes to the `package-lock.json` after running npm install."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "raising-serum",
   "metadata": {},
   "source": [
    "## Adding a new Custom Model\n",
    "\n",
    "This example will guide you through adding a new model. \n",
    "\n",
    "We will use the the `ChartJS` model as an example. But you should replace `ChartJS` and similar with the name of your model. \n",
    "\n",
    "Here we will add a simple Button model to start with. But we call it `ChartJS`. \n",
    "\n",
    "My experience is that you should start small with a working example and the continue in small, incremental steps. For me it did not work trying to copy a large, complex example and refactoring it when I started out learning about Custom Models.\n",
    "\n",
    "1. Create a new branch `chartjs`.\n",
    "2. Add the files and code for a *minimum working model*. This includes\n",
    "    - A Panel Python model\n",
    "    - A Bokeh Python and TypeScript model\n",
    "\n",
    "#### Add the Panel Python Model\n",
    "\n",
    "Add the file *panel/pane/chartjs.py* and the code\n",
    "    \n",
    "```python\n",
    "import param\n",
    "\n",
    "from panel.widgets.base import Widget\n",
    "\n",
    "from ..models import ChartJS as _BkChartJS\n",
    "\n",
    "\n",
    "class ChartJS(Widget):\n",
    "    # Set the Bokeh model to use\n",
    "    _widget_type = _BkChartJS\n",
    "\n",
    "    # Rename Panel Parameters -> Bokeh Model properties\n",
    "    # Parameters like title that does not exist on the Bokeh model should be renamed to None\n",
    "    _rename = {\n",
    "        \"title\": None,\n",
    "    }\n",
    "\n",
    "    # Parameters to be mapped to Bokeh model properties\n",
    "    object = param.String(default=\"Click Me!\")\n",
    "    clicks = param.Integer(default=0)\n",
    "```\n",
    "\n",
    "Add the Panel model to `panel/pane/__init__.py`\n",
    "\n",
    "```python\n",
    "from .chartjs import ChartJS\n",
    "```\n",
    "\n",
    "#### Add the Bokeh Python Model\n",
    "\n",
    "Add the file *panel/models/chartjs.py* and the code\n",
    "\n",
    "```python\n",
    "from bokeh.core.properties import Int, String\n",
    "from bokeh.models import HTMLBox\n",
    "\n",
    "class ChartJS(HTMLBox):\n",
    "    \"\"\"Custom ChartJS Model\"\"\"\n",
    "\n",
    "    object = String()\n",
    "    clicks = Int()\n",
    "```\n",
    "\n",
    "Add the Bokeh model to `panel/models/__init__.py` file\n",
    "\n",
    "```python\n",
    "from .chartjs import ChartJS\n",
    "```\n",
    "\n",
    "#### Add the Bokeh TypeScript Model\n",
    "\n",
    "Add the file *panel/models/chartjs.ts* and the code\n",
    "\n",
    "```typescript\n",
    "// See https://docs.bokeh.org/en/latest/docs/reference/models/layouts.html\n",
    "import { HTMLBox, HTMLBoxView } from \"@bokehjs/models/layouts/html_box\"\n",
    "\n",
    "// See https://docs.bokeh.org/en/latest/docs/reference/core/properties.html\n",
    "import * as p from \"@bokehjs/core/properties\"\n",
    "\n",
    "// The view of the Bokeh extension/ HTML element\n",
    "// Here you can define how to render the model as well as react to model changes or View events.\n",
    "export class ChartJSView extends HTMLBoxView {\n",
    "    model: ChartJS\n",
    "    objectElement: any // Element\n",
    "\n",
    "    connect_signals(): void {\n",
    "        super.connect_signals()\n",
    "\n",
    "        this.connect(this.model.properties.object.change, () => {\n",
    "            this.render();\n",
    "        })\n",
    "    }\n",
    "\n",
    "    render(): void {\n",
    "        super.render()\n",
    "        this.el.innerHTML = `<button type=\"button\">${this.model.object}</button>`\n",
    "        this.objectElement = this.el.firstElementChild\n",
    "\n",
    "        this.objectElement.addEventListener(\"click\", () => {this.model.clicks+=1;}, false)\n",
    "    }\n",
    "}\n",
    "\n",
    "export namespace ChartJS {\n",
    "    export type Attrs = p.AttrsOf<Props>\n",
    "    export type Props = HTMLBox.Props & {\n",
    "        object: p.Property<string>,\n",
    "        clicks: p.Property<number>,\n",
    "    }\n",
    "}\n",
    "\n",
    "export interface ChartJS extends ChartJS.Attrs { }\n",
    "\n",
    "// The Bokeh .ts model corresponding to the Bokeh .py model\n",
    "export class ChartJS extends HTMLBox {\n",
    "    properties: ChartJS.Props\n",
    "\n",
    "    constructor(attrs?: Partial<ChartJS.Attrs>) {\n",
    "        super(attrs)\n",
    "    }\n",
    "\n",
    "    static __module__ = \"panel.models.chartjs\"\n",
    "\n",
    "    static init_ChartJS(): void {\n",
    "        this.prototype.default_view = ChartJSView;\n",
    "\n",
    "        this.define<ChartJS.Props>(({Int, String}) => ({\n",
    "            object: [String, \"Click Me!\"],\n",
    "            clicks: [Int, 0],\n",
    "        }))\n",
    "    }\n",
    "}\n",
    "```\n",
    "\n",
    "Add the `ChartJS` typescript model to *panel/models/index.ts*\n",
    "\n",
    "```typescript\n",
    "export {ChartJS} from \"./chartjs\"\n",
    "```\n",
    "\n",
    "#### Build the Model\n",
    "\n",
    "You can now build the model using `panel build panel`. It should look similar to\n",
    "\n",
    "```bash\n",
    "(base) root@475bb36209a9:/workspaces/panel# panel build panel\n",
    "Working directory: /workspaces/panel/panel\n",
    "Using /workspaces/panel/panel/tsconfig.json\n",
    "Compiling styles\n",
    "Compiling TypeScript (45 files)\n",
    "Linking modules\n",
    "Output written to /workspaces/panel/panel/dist\n",
    "All done.\n",
    "```\n",
    "\n",
    "#### Test the Model\n",
    "\n",
    "Add the file *panel/tests/pane/test_chartjs.py* and the code\n",
    "\n",
    "```python\n",
    "import panel as pn\n",
    "\n",
    "\n",
    "def test_constructor():\n",
    "    chartjs = pn.pane.ChartJS(object=\"Click Me Now!\")\n",
    "\n",
    "def get_app():\n",
    "    chartjs = pn.pane.ChartJS(object=\"Click Me Now!\")\n",
    "    return pn.Column(\n",
    "        chartjs, pn.Param(chartjs, parameters=[\"object\", \"clicks\"])\n",
    "    )\n",
    "\n",
    "if __name__.startswith(\"bokeh\"):\n",
    "    get_app().servable()\n",
    "```\n",
    "\n",
    "Run `pytest panel/tests/pane/test_chartjs.py` and make sure it passes.\n",
    "\n",
    "Serve the app with `panel serve panel/tests/pane/test_chartjs.py --auto --show`\n",
    "\n",
    "You have to *hard refresh* your browser to reload the new panel `.js` files with your `ChartJS` model. In Chrome I press `CTRL+F5`. See [How to hard refresh in Chrome, Firefox and IE](https://www.namecheap.com/support/knowledgebase/article.aspx/10078/2194/how-to-do-a-hard-refresh-in-chrome-firefox-and-ie/) for other browsers.\n",
    "\n",
    "Now you can manually test your model\n",
    "\n",
    "![Chart JS Button](../assets/chartjs-button.gif)\n",
    "\n",
    "#### Save your new Model\n",
    "\n",
    "Finally you should save your changes via `git add .` and maybe even commit them `git commit -m \"First iteration on ChartJS model\"`"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "blessed-railway",
   "metadata": {},
   "source": [
    "## Build a small HTML Example\n",
    "\n",
    "In the beginning of your journey into Custom Models there will be things that break and difficulties figuring out why. When you combine several new things it can be really difficult to figure out why. Is the problem Panel, Bokeh, Python, Javascript, Node or ....? \n",
    "\n",
    "So I suggest creating a small, working example in plain HTML/ JS before you start combining with Panel and Bokeh Models.\n",
    "\n",
    "Please note the below example works out of the box. It is not always that easy importing javascript libraries in a Notebook. So it can be a good idea to work in a `.html` file first."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "funded-traffic",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<script src=\"https://cdn.jsdelivr.net/npm/chart.js@2.8.0\"></script>\n",
       "<div class=\"chart-container\" style=\"position: relative; height:400px; width:100%\">\n",
       "    <canvas id=\"myChart\"></canvas>\n",
       "</div>\n",
       "<script>\n",
       "var ctx = document.getElementById('myChart').getContext('2d');\n",
       "var chart = new Chart(ctx, {\n",
       "    // The type of chart we want to create\n",
       "    type: 'line',\n",
       "\n",
       "    // The data for our dataset\n",
       "    data: {\n",
       "        labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],\n",
       "        datasets: [{\n",
       "            label: 'My First dataset',\n",
       "            backgroundColor: 'rgb(255, 99, 132)',\n",
       "            borderColor: 'rgb(255, 99, 132)',\n",
       "            data: [0, 10, 5, 2, 20, 30, 45]\n",
       "        }]\n",
       "    },\n",
       "\n",
       "    // Configuration options go here\n",
       "    options: {\n",
       "        responsive: true,\n",
       "        maintainAspectRatio: false,\n",
       "    }\n",
       "});\n",
       "</script>\n"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "%%HTML\n",
    "<script src=\"https://cdn.jsdelivr.net/npm/chart.js@2.8.0\"></script>\n",
    "<div class=\"chart-container\" style=\"position: relative; height:400px; width:100%\">\n",
    "    <canvas id=\"myChart\"></canvas>\n",
    "</div>\n",
    "<script>\n",
    "var ctx = document.getElementById('myChart').getContext('2d');\n",
    "var chart = new Chart(ctx, {\n",
    "    // The type of chart we want to create\n",
    "    type: 'line',\n",
    "\n",
    "    // The data for our dataset\n",
    "    data: {\n",
    "        labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],\n",
    "        datasets: [{\n",
    "            label: 'My First dataset',\n",
    "            backgroundColor: 'rgb(255, 99, 132)',\n",
    "            borderColor: 'rgb(255, 99, 132)',\n",
    "            data: [0, 10, 5, 2, 20, 30, 45]\n",
    "        }]\n",
    "    },\n",
    "\n",
    "    // Configuration options go here\n",
    "    options: {\n",
    "        responsive: true,\n",
    "        maintainAspectRatio: false,\n",
    "    }\n",
    "});\n",
    "</script>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "raising-burner",
   "metadata": {},
   "source": [
    "## Using the Javascript Model\n",
    "\n",
    "Getting something shown using the `ChartJS` `js` library would be the next step. It might require a bit of experimentation, looking at other examples, google or support from the community.\n",
    "\n",
    "Here I found that a good step where the following changes\n",
    "\n",
    "#### Import the Javascript Library\n",
    "\n",
    "Update *test_chartjs.py* tp\n",
    "\n",
    "```python\n",
    "import panel as pn\n",
    "\n",
    "\n",
    "def test_constructor():\n",
    "    chartjs = pn.pane.ChartJS(object=\"Click Me Now!\")\n",
    "\n",
    "def get_app():\n",
    "    chartjs = pn.pane.ChartJS(object=\"Click Me Now!\")\n",
    "    return pn.Column(\n",
    "        chartjs, pn.Param(chartjs, parameters=[\"object\", \"clicks\"])\n",
    "    )\n",
    "\n",
    "if __name__.startswith(\"bokeh\"):\n",
    "    pn.config.js_files[\"chartjs\"]=\"https://cdn.jsdelivr.net/npm/chart.js@2.8.0\"\n",
    "    get_app().servable()\n",
    "```\n",
    "\n",
    "#### Render the Plot\n",
    "\n",
    "In the *chartjs.ts* file add `import { canvas, div } from \"@bokehjs/core/dom\";` at the top and change the `render` function to \n",
    "\n",
    "```typescript\n",
    "render(): void {\n",
    "    super.render()\n",
    "    var object = {\n",
    "        type: 'line',\n",
    "        data: {\n",
    "            labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],\n",
    "            datasets: [{\n",
    "                label: 'My First dataset',\n",
    "                backgroundColor: 'rgb(255, 99, 132)',\n",
    "                borderColor: 'rgb(255, 99, 132)',\n",
    "                data: [0, 10, 5, 2, 20, 30, 45]\n",
    "            }]\n",
    "        },\n",
    "        options: {\n",
    "            responsive: true,\n",
    "            maintainAspectRatio: false,\n",
    "        }\n",
    "    }\n",
    "\n",
    "    var chartContainer = div({class: \"chartjs-container\", style: \"position: relative; height:400px; width:100%\"})\n",
    "    var chartCanvas = canvas({class: \"chartjs\"})\n",
    "    chartContainer.appendChild(chartCanvas)\n",
    "    var ctx: any = chartCanvas.getContext('2d');\n",
    "    new (window as any).Chart(ctx, object);\n",
    "\n",
    "    this.el.appendChild(chartContainer)\n",
    "}\n",
    "```\n",
    "\n",
    "#### Build and Test\n",
    "\n",
    "Run `panel build panel` and hard refresh your browser. You should see\n",
    "\n",
    "![ChartJS Hello World](../assets/chartjs-hello-world.png)\n",
    "\n",
    "#### Save Your Model\n",
    "\n",
    "Remember to stage and/ or commit your working changes."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "compound-optics",
   "metadata": {},
   "source": [
    "## Next Steps\n",
    "\n",
    "- Enable setting the Python `ChartJS.object` parameter to any ChartJS dictionary.\n",
    "- Checkout support for different sizing modes, responsiveness and window maximize.\n",
    "- Configure the javascript, css, .. dependencies in the Bokeh Python File.\n",
    "- ....."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "middle-nelson",
   "metadata": {},
   "source": [
    "## Check List\n",
    "\n",
    "When you develop and test your model eventually you should consider implementing and testing\n",
    "\n",
    "- Dynamic updates to the `object` parameter and any other parameters added.\n",
    "- Resizing\n",
    "    - Does it resize when `width` is changed dynamically?\n",
    "    - Does it resize when `height` is changed dynamically?\n",
    "    - Does it work with `sizing_mode=\"stretch_width\"` etc.\n",
    "- Themes (Light, Dark)\n",
    "- Window Resizing, Window Maximizing, Window Minimizing.\n",
    "- Streaming of Data. Is it efficient?\n",
    "- Events (Click, Hover etc.)\n",
    "- Consider supporting the Python Wrapper (ECharts -> PyECharts, ChartJS -> [PyChart.JS](https://pypi.org/project/pyChart.JS/))\n",
    "- Tests\n",
    "- Reference Notebook\n",
    "- Communication also to for example ChartJS community and developers."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "invisible-physics",
   "metadata": {},
   "source": [
    "## Tips and Tricks\n",
    "\n",
    "- Work in small increments and stage your changes when they work\n",
    "- Remember to `panel build panel` and hard refresh before you test.\n",
    "- Add [console.log](https://www.w3schools.com/jsref/met_console_log.asp) statements to your `.ts` code for debugging.\n",
    "- Use the [*Developer Tools*](https://developers.google.com/web/tools/chrome-devtools) *console* to see the `console.log` output and identify errors. In my browsers I toggle the Developer Tools using `CTRL+SHIFT+I`.\n",
    "- Find inspiration for next steps in the [existing Panel Custom Models](https://github.com/holoviz/panel/tree/master/panel/models). For `ChartJS` one of the most relevant custom models would be `Echarts`. See Panel [echarts.py](https://github.com/holoviz/panel/blob/master/panel/pane/echarts.py), Bokeh [echarts.py](https://github.com/holoviz/panel/blob/master/panel/models/echarts.py) and [echarts.ts](https://github.com/holoviz/panel/blob/master/panel/models/echarts.ts).\n",
    "- Use the existing documentation\n",
    "    - [Panel - Developer Guide](https://panel.holoviz.org/developer_guide/index.html)\n",
    "    - [Bokeh - Extending Bokeh](https://docs.bokeh.org/en/latest/docs/user_guide/extensions.html)\n",
    "    - [Awesome Panel - Bokeh Extensions Guide](https://awesome-panel.readthedocs.io/en/latest/guides/awesome-panel-extensions-guide/bokeh-extensions.html)\n",
    "- Use Google Search. You don't have to be an expert javascript or typescript developer. It's a very small subset of those languages that is used when developing Custom Models.\n",
    "- Ask for help in [PyViz Gitter](https://gitter.im/pyviz/pyviz), [HoloViz Discourse](https://discourse.holoviz.org/) and [Bokeh Discourse](https://discourse.bokeh.org/) forums."
   ]
  }
 ],
 "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.8.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
