{ "cells": [ { "cell_type": "markdown", "id": "e4ae7020", "metadata": {}, "source": [ "# Extending view components\n", "\n", "If you want to use our Bokeh `CartoApp` framework, we provide a way to customize the GUI. \n", "The custom UI components are put at the right panel of the view, which are handled by the method `CartoApp.install_right_panel_views()`." ] }, { "cell_type": "markdown", "id": "94244caf", "metadata": {}, "source": [ "## Import UI components\n", "\n", "`CartoApp.install_right_panel_views()` use three sources and collect then into a list in order:\n", "\n", "1. `ProbeDesp.extra_controls()` returns probe-specific components.\n", "2. user config file. It not set, use `['blueprint', 'atlas']`.\n", "3. command-line options `--view`\n", "\n", "Elements in that list should be recognised by `init_view()`, there are:\n", "\n", "* `None`: skip\n", "* a `ViewBase` sub-class instance or sub-type\n", "\n", " * If it is a `ExtensionView`, also check whether the probe is supportted.\n", "\n", "* a `ImageHandler` instance or sub-type (with zero args `__init__`). It will be wrapped with `ImageView`.\n", "* a str `'file'`: use `FileImageView` (experimental feature)\n", "* a str `'atlas'`: use `AtlasBrainView`\n", "* a str `'blueprint'`: use `BlueprintView`\n", "* a str `'script'`: use `BlueprintScriptView` (experimental feature)\n", "* a path str `image_path`: use `ImageView`\n", "* a str `[ROOT:]module_path:view_name`: dynamic load the corresponding component, and apply above rule again.\n", "\n", "and a special rule:\n", "\n", "* a str `'-'`: remove source 2, 3 before it." ] }, { "cell_type": "markdown", "id": "272f1184", "metadata": {}, "source": [ "### Debuging a view component.\n", "\n", "With the UI-importing rules, you can prepare a file `my_view.py` and put under folder `extend` (it doesn't in the `PYTHONPATH`)." ] }, { "cell_type": "code", "execution_count": null, "id": "7b235008", "metadata": {}, "outputs": [], "source": [ "# some imports\n", "\n", "class MyView(ViewBase):\n", " def __init__(self, config: CartoConfig):\n", " super().__init__(config, logger='neurocarto.view.my_view')\n", " ... # other abstract methods implemtation\n", " \n", "if __name__ == '__main__':\n", " import sys\n", " from neurocarto.config import parse_cli\n", " from neurocarto.main_app import main\n", "\n", " main(parse_cli([\n", " *sys.argv[1:],\n", " '--debug',\n", " '--view=-',\n", " '--view=extend:my_view:MyView',\n", " ]))" ] }, { "cell_type": "markdown", "id": "57de3909", "metadata": {}, "source": [ "Then you can run this file direct and test your custom component.\n", "\n", " python extend/my_view.py" ] }, { "cell_type": "markdown", "id": "c1bb238f", "metadata": {}, "source": [ "### Import Probe specific UI components\n", "\n", "A probe implementation can provide their specific components by `extra_controls()`. For example, `NpxProbeDesp` has one probe-specific UI component:" ] }, { "cell_type": "code", "execution_count": null, "id": "6ab9fd49", "metadata": {}, "outputs": [], "source": [ "class NpxProbeDesp:\n", " def extra_controls(self, config: CartoConfig):\n", " from .views import NpxReferenceControl\n", " return [NpxReferenceControl]" ] }, { "cell_type": "markdown", "id": "394a0bf7", "metadata": {}, "source": [ "A probe implementation can provide some probe-specific functions which are required by some UI components. We use Protocol to declare the require methods." ] }, { "cell_type": "markdown", "id": "ae3eec84", "metadata": {}, "source": [ "## Customize UI components\n", "\n", "We provide a framework, a base view component `ViewBase` to interact with `CartoApp`. Based on the base, we built several classes and tools to support different kinds of visualizing." ] }, { "cell_type": "markdown", "id": "69bb490f", "metadata": {}, "source": [ "### Implement ViewBase\n", "\n", "All UI components should be a subclass of `neurocarto.views.base.ViewBase`. \n", "`ViewBase` provides a layout framework." ] }, { "cell_type": "code", "execution_count": null, "id": "70c7e8ba", "metadata": {}, "outputs": [], "source": [ "class MyView(ViewBase):\n", " def __init__(self, config: CartoConfig):\n", " super().__init__(config, logger='neurocarto.view.my_view')\n", " @property\n", " def name(self) -> str:\n", " return 'Title of my view' # show in
\n", " @property \n", " def description(self) -> str | None: # optional\n", " return \"description of my view\" # show in help button\n", " def _setup_render(self, f: Figure, **kwargs): # optional\n", " ... # if you have something renders and want to plot them in the figure.\n", " def _setup_title(self, **kwargs) -> list[UIElement]: # optional\n", " ... # if you have some UI elements and want to put them in the title row.\n", " def _setup_content(self, **kwargs) -> UIElement | list[UIElement] | None: # optional\n", " ... # if you have some UI elements and want to put them in the content row.\n", " def start(self):\n", " pass" ] }, { "cell_type": "markdown", "id": "3164424e", "metadata": {}, "source": [ "Please note that all bokeh-related UI components should be initialized during `setup()`, \n", "which invokes `_setup_render()`, `_setup_title()` and `_setup_content()`. \n", "Otherwise, the ID of bokeh components will be used by other HTML documents (such as when you refresh the web page), \n", "and cause server error." ] }, { "cell_type": "markdown", "id": "4a643370", "metadata": {}, "source": [ "### Extend ViewBase\n", "\n", "We have following mixin classes to extend behaviors of `ViewBase`." ] }, { "cell_type": "markdown", "id": "9f1fb27c", "metadata": {}, "source": [ "#### InvisibleView\n", "\n", "Once `MyView` inherits from `InvisibleView`, then `MyView` becomes invisible. \n", "\n", "Extra UI elements:\n", "\n", "* attribute `visible_btn` ([Switch](https://docs.bokeh.org/en/latest/docs/reference/models/widgets/inputs.html#bokeh.models.Switch))\n", " shown at the first place in the title row.\n", "\n", "and visible state of the following things will be controlled:\n", "\n", "* content row (all things returned from `_setup_content()`).\n", "* all attributes `render_*` with type hinted [GlyphRenderer](https://docs.bokeh.org/en/latest/docs/reference/models/renderers.html#bokeh.models.GlyphRenderer)" ] }, { "cell_type": "markdown", "id": "4c97f60f", "metadata": {}, "source": [ "#### StateView\n", "\n", "Once `MyView` inherits from `StateView`, then `MyView` can read/restore the state from `*.config.json` with a correspond channelmap file." ] }, { "cell_type": "markdown", "id": "5052bbbf", "metadata": {}, "source": [ "#### DynamicView\n", "\n", "Once `MyView` inherits from `DynamicView`, it can receive the changes of the channelmap and blueprint from the GUI. `MyView` can also use `DynamicView` to recognise its sub-custom components and pass events." ] }, { "cell_type": "markdown", "id": "11e6db4f", "metadata": {}, "source": [ "#### BoundView\n", "\n", "Once `MyView` inherits from `BoundView`, it indicates `MyView` will plot something that has a boundary in the figure.\n", "\n", "Extra UI elements:\n", "\n", "* a `tool_boundary` [BoxEditTool](https://docs.bokeh.org/en/latest/docs/reference/models/tools.html#bokeh.models.BoxEditTool) in the figure toolbars.\n", "* (optional) rotating controls (a reset [Button](https://docs.bokeh.org/en/latest/docs/reference/models/widgets/buttons.html#bokeh.models.Button) and a [Slider](https://docs.bokeh.org/en/latest/docs/reference/models/widgets/sliders.html#bokeh.models.Slider))\n", "* (optional) scaling controls\n", "\n", "Renders in figure\n", "\n", "* a boundary rectangle `render_boundary` ([Rect](https://docs.bokeh.org/en/latest/docs/reference/models/glyphs/rect.html#bokeh.models.Rect)) controlled by `data_boundary`.\n", "\n", "A help function:\n", "\n", "* `transform_image_data(image, boundary)`: to fit image data into the boundary.\n", "\n", "You must overwrite the method `_setup_render()` to provide your image-like render, and the method `on_boundary_transform()` to receive the updated transformation.\n", "\n" ] }, { "cell_type": "markdown", "id": "63b77910", "metadata": {}, "source": [ "### Extend ViewBase (advance)\n", "\n", "The following minxin classes are the special class that the methods declared are decorated by `@final`, \n", "because they are used to communicate with the `CartoApp`. \n", "In details, the methods are replaced by `CartoApp` with the actual content during the setup." ] }, { "cell_type": "markdown", "id": "63dc0d34", "metadata": {}, "source": [ "#### ControllerView\n", "\n", "It is used to control other components in `CartoApp`.\n" ] }, { "cell_type": "markdown", "id": "61e11c4b", "metadata": {}, "source": [ "#### GlobalStateView\n", "\n", "As same as `StateView` but it can to store/restore the config into/from user config file." ] }, { "cell_type": "markdown", "id": "3245d9f9", "metadata": {}, "source": [ "#### EditorView\n", "\n", "It is used to change the channelmap and the blueprint. A method `update_probe()` is given to notify the updated channelmap or blueprint." ] }, { "cell_type": "markdown", "id": "a6d522d8-5cbf-451d-97c2-c37a5807110c", "metadata": {}, "source": [ "#### RecordView\n", "\n", "(on branch `record-steps`). It is used to record each GUI operating step. The steps history can be stored/loaded as well as manipulated/replayed." ] }, { "cell_type": "markdown", "id": "75c45b0e", "metadata": {}, "source": [ "#### ExtensionView\n", "\n", "It makes a component only enable itself when the probe is supported something." ] }, { "cell_type": "markdown", "id": "0021732f", "metadata": {}, "source": [ "### Utility functions\n", "\n", "Besides mixin classes, we have some utility functions." ] }, { "cell_type": "markdown", "id": "547620a7", "metadata": {}, "source": [ "#### as_callback\n", "\n", "wrap a callback into bokeh event callback." ] }, { "cell_type": "code", "execution_count": null, "id": "74f1cd79", "metadata": {}, "outputs": [], "source": [ "from bokeh.models import Slider\n", "from neurocarto.util.bokeh_util import as_callback\n", "\n", "class MyView(ViewBase):\n", " def setup(self):\n", " slider = Slider(...)\n", " slider.on_change('value', self._without_as_callback)\n", " slider.on_change('value', as_callback(self._with_as_callback))\n", "\n", " # only allow this signature by Bokeh\n", " def _without_as_callback(self, prop:str, old_value, new_value): ...\n", " # allow following all signatures by as_callback\n", " def _with_as_callback(self): ...\n", " def _with_as_callback(self, new_value): ...\n", " def _with_as_callback(self, old_value, new_value): ...\n", " def _with_as_callback(self, prop:str, old_value, new_value): ...\n", " " ] }, { "cell_type": "markdown", "id": "8db2bcc4", "metadata": {}, "source": [ "#### recursive_call_barrier\n", "\n", "A method decorator to detect recursive calling stack for an event processing.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "4bd166fd", "metadata": {}, "outputs": [], "source": [ "from neurocarto.util.bokeh_util import recursive_call_barrier\n", "\n", "class MyView(ViewBase):\n", " @recursive_call_barrier\n", " def on_change(self, value): # as UI component event callback\n", " self.set_value(value)\n", " \n", " def set_value(self, value): # may call by other UI components\n", " ... # set value to UI component, invoking on_change(value)" ] }, { "cell_type": "markdown", "id": "f5e53faa", "metadata": {}, "source": [ "#### UI factory\n", "\n", "A factory class to produce UI controls with the same styling. So far, we have provided `ButtonFactory` and `SliderFactory`." ] }, { "cell_type": "markdown", "id": "6187a833", "metadata": {}, "source": [ "#### PathAutocompleteInput\n", "\n", "A class extend [AutocompleteInput](https://docs.bokeh.org/en/latest/docs/reference/models/widgets/inputs.html#bokeh.models.AutocompleteInput) \n", "to support file input with path completion." ] }, { "cell_type": "code", "execution_count": null, "id": "1aecb52c", "metadata": {}, "outputs": [], "source": [ "from bokeh.layouts import row\n", "from neurocarto.util.bokeh_util import PathAutocompleteInput\n", "\n", "def on_image_selected(path:Path):\n", " pass\n", "\n", "pai = PathAutocompleteInput(\n", " Path('.'),\n", " on_image_selected,\n", " mode='file',\n", " accept=['image/*'], # file suffix '.png' or mime type 'image/png'\n", " width=300,\n", ")\n", "\n", "\n", "row(pai.input);" ] }, { "cell_type": "markdown", "id": "f98cbb5c", "metadata": {}, "source": [ "### new_help_button\n", "\n", "create a small [HelpButton](https://docs.bokeh.org/en/latest/docs/reference/models/widgets/buttons.html#bokeh.models.HelpButton)." ] }, { "cell_type": "markdown", "id": "b5cb0984", "metadata": {}, "source": [ "## DataView\n", "\n", "`DataView` handle probe-related data, either static (like experimental data) or dynamic (like real-time experimental data) data.\n", "\n", "It required to implement an abstract method `data()`, which returns a dictionary that used for updating the [ColumnDataSource](https://docs.bokeh.org/en/latest/docs/reference/models/sources.html#bokeh.models.ColumnDataSource).\n", "\n", "It has subclasses:" ] }, { "cell_type": "markdown", "id": "8fb3074f", "metadata": {}, "source": [ "### Data1DView\n", "\n", "If you data is one-dimension data along the probe, [multi_line](https://docs.bokeh.org/en/latest/docs/reference/plotting/figure.html#bokeh.plotting.figure.multi_line) is used, and required data dictionary should like `dict(x=[[...]], y=[[...]])`.\n", "\n", "You have a helper classmethod `arr_to_dict(data)` to convert a numpy array (wich shpe `Array[float, [S,], N, (x, y)]`, means `(N, 2)` or `(S, N, 2)` floating array) to correct data dictionary.\n", "\n", "#### Examples\n", "\n", "* `neurocarto.views.data_density.ElectrodeDensityDataView`" ] }, { "cell_type": "markdown", "id": "67fc8d0e", "metadata": {}, "source": [ "### FileDataView\n", "\n", "Providing an extra `PathAutocompleteInput` to get filepath from GUI. `load_data(file)` will be invoked." ] }, { "cell_type": "markdown", "id": "179bc9ad", "metadata": {}, "source": [ "## ImageView\n", "\n", "`ImageView` handle the image render. It require a `ImageHandler` to provide the image and its information.\n", "\n", "It has a sub-class:" ] }, { "cell_type": "markdown", "id": "2461f5b4", "metadata": {}, "source": [ "### FileImageView\n", "\n", "Providing an extra `PathAutocompleteInput` to get a image filepath from GUI. use `ImageHandler.from_file()` to create a `ImageHandler` for the image." ] }, { "cell_type": "markdown", "id": "5848b618", "metadata": {}, "source": [ "### ImageHandler\n", "\n", "A `ImageHandler` holds a image (2D and 3D image) and provide the related informations. \n", "It used by `ImageView`.\n", "\n", "It has sub-classes:" ] }, { "cell_type": "markdown", "id": "a561d3e4", "metadata": {}, "source": [ "#### NumpyImageHandler\n", "\n", "It holds a image sotred in a numpy array. \n", "It is a static handler that the content of image does not update when probe is updated.\n", "It is usually created by `ImageHandler.from_file()`, `ImageHandler.from_numpy()` and `ImageHandler.from_tiff()` (not tested yet)." ] }, { "cell_type": "markdown", "id": "6621af5e", "metadata": {}, "source": [ "#### PltImageHandler\n", "\n", "It holds a image generated by `matplotlib`.\n", "It is a dynamic handler that the content of image could follow the chanes of probes, by using `on_probe_update()` to revice the updates.\n", "\n", "This class provide a context function `plot_figure()` to hold a `Axes` for plotting. After exiting the context, the image will be shown." ] }, { "cell_type": "code", "execution_count": null, "id": "962b4ee8", "metadata": {}, "outputs": [], "source": [ "# from tests/main_image_plt_plot_channelmap.py\n", "\n", "class PlotChannelMap(PltImageHandler):\n", " def on_probe_update(self, probe, chmap, e):\n", " if chmap is not None:\n", " self.plot_channelmap(chmap)\n", " else:\n", " self.set_image(None)\n", "\n", " def plot_channelmap(self, m):\n", " from neurocarto.probe_npx import plot\n", "\n", " with self.plot_figure() as ax:\n", " plot.plot_channelmap_block(ax, chmap=m)\n", " plot.plot_probe_shape(ax, m, color='k')" ] }, { "cell_type": "markdown", "id": "91a4cea9", "metadata": {}, "source": [ "The context function `plot_figure()` use the rc setting from `image_plt.matplotlibrc`, which purpose to plot probe-align-able figure. After existing the context, `PltImageHandler` will try to align the image on the boken figure.\n", "Therefore, user doesn't need to change its position and its resolution to align the boken figure." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "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.10.13" } }, "nbformat": 4, "nbformat_minor": 5 }