{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import panel as pn\n",
    "\n",
    "pn.extension()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Bokeh's property system defines the valid properties for all the different Bokeh models. Using ``jslink`` we can easily tie a widget value to Bokeh properties on another widget or plot. This example defines functions that generate a property editor for the most common Bokeh properties. First, we define two functions that generate a set of widgets linked to a plot:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from bokeh.core.enums import LineDash, LineCap, MarkerType, NamedColor\n",
    "from bokeh.models.plots import Model, _list_attr_splat"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def meta_widgets(model, width=500):\n",
    "    tabs = pn.Tabs(width=width)\n",
    "    widgets = get_widgets(model, width=width-25)\n",
    "    if widgets:\n",
    "        tabs.append((type(model).__name__, widgets))\n",
    "    for p, v in model.properties_with_values().items():\n",
    "        if isinstance(v, _list_attr_splat):\n",
    "            v = v[0]\n",
    "        if isinstance(v, Model):\n",
    "            subtabs = meta_widgets(v)\n",
    "            if subtabs is not None:\n",
    "                tabs.append((p.title(), subtabs))\n",
    "                \n",
    "    if hasattr(model, 'renderers'):\n",
    "        for r in model.renderers:\n",
    "            tabs.append((type(r).__name__, meta_widgets(r)))\n",
    "    if hasattr(model, 'axis') and isinstance(model.axis, list):\n",
    "        for pre, axis in zip('XY', model.axis):\n",
    "            tabs.append(('%s-Axis' % pre, meta_widgets(axis)))\n",
    "    if hasattr(model, 'grid'):\n",
    "        for pre, grid in zip('XY', model.grid):\n",
    "            tabs.append(('%s-Grid' % pre, meta_widgets(grid)))\n",
    "    if not widgets and not len(tabs) > 1:\n",
    "        return None\n",
    "    elif not len(tabs) > 1:\n",
    "        return tabs[0]\n",
    "    return tabs"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_widgets(model, skip_none=True, **kwargs):\n",
    "    widgets = []\n",
    "    for p, v in model.properties_with_values().items():\n",
    "        if isinstance(v, dict):\n",
    "            if 'value' in v:\n",
    "                v = v.get('value')\n",
    "            else:\n",
    "                continue\n",
    "        if v is None and skip_none:\n",
    "            continue\n",
    "                \n",
    "        ps = dict(name=p, value=v, **kwargs)\n",
    "        if 'alpha' in p:\n",
    "            w = pn.widgets.FloatSlider(start=0, end=1, **ps)\n",
    "        elif 'color' in p:\n",
    "            if v in list(NamedColor):\n",
    "                w = pn.widgets.Select(options=list(NamedColor), **ps)\n",
    "            else:\n",
    "                w = pn.widgets.ColorPicker(**ps)\n",
    "        elif p==\"width\":\n",
    "            w = pn.widgets.IntSlider(start=400, end=800, **ps)\n",
    "        elif p in [\"inner_width\", \"outer_width\"]:\n",
    "            w = pn.widgets.IntSlider(start=0, end=20, **ps)\n",
    "        elif p.endswith('width'):\n",
    "            w = pn.widgets.FloatSlider(start=0, end=20, **ps)\n",
    "        elif 'marker' in p:\n",
    "            w = pn.widgets.Select(options=list(MarkerType), **ps)\n",
    "        elif p.endswith('cap'):\n",
    "            w = pn.widgets.Select(options=list(LineCap), **ps)\n",
    "        elif p == 'size':\n",
    "            w = pn.widgets.FloatSlider(start=0, end=20, **ps)\n",
    "        elif p.endswith('text') or p.endswith('label'):\n",
    "            w = pn.widgets.TextInput(**ps)\n",
    "        elif p.endswith('dash'):\n",
    "            patterns = list(LineDash)\n",
    "            w = pn.widgets.Select(options=patterns, value=v or patterns[0], **kwargs)\n",
    "        else:\n",
    "            continue\n",
    "        w.jslink(model, value=p)\n",
    "        widgets.append(w)\n",
    "    return pn.Column(*sorted(widgets, key=lambda w: w.name))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Having defined these helper functions we can now declare a plot and use the ``meta_widgets`` function to generate the GUI:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from bokeh.plotting import figure\n",
    "\n",
    "p = figure(title='This is a title', x_axis_label='x-axis', y_axis_label='y-axis')\n",
    "xs = np.linspace(0, 10)\n",
    "r = p.scatter(xs, np.sin(xs))\n",
    "\n",
    "editor=pn.Row(meta_widgets(p), p)\n",
    "editor"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## App\n",
    "\n",
    "Lets wrap it into nice template that can be served via `panel serve bokeh_property_editor.ipynb`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pn.template.FastListTemplate(\n",
    "    site=\"Panel\", title=\"Bokeh Property Editor\", \n",
    "    main=[pn.pane.Markdown(\"The Bokeh Property Editor enables you to fine tune the Bokeh plot.\", sizing_mode=\"stretch_width\"), editor]\n",
    ").servable();"
   ]
  }
 ],
 "metadata": {
  "language_info": {
   "name": "python",
   "pygments_lexer": "ipython3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
