Extending view components
If you want to use our Bokeh CartoApp framework, we provide a way to customize the GUI. The custom UI components are put at the right panel of the view, which are handled by the method CartoApp.install_right_panel_views().
Import UI components
CartoApp.install_right_panel_views() use three sources and collect then into a list in order:
ProbeDesp.extra_controls()returns probe-specific components.user config file. It not set, use
['blueprint', 'atlas'].command-line options
--view
Elements in that list should be recognised by init_view(), there are:
None: skipa
ViewBasesub-class instance or sub-typeIf it is a
ExtensionView, also check whether the probe is supportted.
a
ImageHandlerinstance or sub-type (with zero args__init__). It will be wrapped withImageView.a str
'file': useFileImageView(experimental feature)a str
'atlas': useAtlasBrainViewa str
'blueprint': useBlueprintViewa str
'script': useBlueprintScriptView(experimental feature)a path str
image_path: useImageViewa str
[ROOT:]module_path:view_name: dynamic load the corresponding component, and apply above rule again.
and a special rule:
a str
'-': remove source 2, 3 before it.
Debuging a view component.
With the UI-importing rules, you can prepare a file my_view.py and put under folder extend (it doesn’t in the PYTHONPATH).
[ ]:
# some imports
class MyView(ViewBase):
def __init__(self, config: CartoConfig):
super().__init__(config, logger='neurocarto.view.my_view')
... # other abstract methods implemtation
if __name__ == '__main__':
import sys
from neurocarto.config import parse_cli
from neurocarto.main_app import main
main(parse_cli([
*sys.argv[1:],
'--debug',
'--view=-',
'--view=extend:my_view:MyView',
]))
Then you can run this file direct and test your custom component.
python extend/my_view.py
Import Probe specific UI components
A probe implementation can provide their specific components by extra_controls(). For example, NpxProbeDesp has one probe-specific UI component:
[ ]:
class NpxProbeDesp:
def extra_controls(self, config: CartoConfig):
from .views import NpxReferenceControl
return [NpxReferenceControl]
A probe implementation can provide some probe-specific functions which are required by some UI components. We use Protocol to declare the require methods.
Customize UI components
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.
Implement ViewBase
All UI components should be a subclass of neurocarto.views.base.ViewBase. ViewBase provides a layout framework.
[ ]:
class MyView(ViewBase):
def __init__(self, config: CartoConfig):
super().__init__(config, logger='neurocarto.view.my_view')
@property
def name(self) -> str:
return 'Title of my view' # show in <div>
@property
def description(self) -> str | None: # optional
return "description of my view" # show in help button
def _setup_render(self, f: Figure, **kwargs): # optional
... # if you have something renders and want to plot them in the figure.
def _setup_title(self, **kwargs) -> list[UIElement]: # optional
... # if you have some UI elements and want to put them in the title row.
def _setup_content(self, **kwargs) -> UIElement | list[UIElement] | None: # optional
... # if you have some UI elements and want to put them in the content row.
def start(self):
pass
Please note that all bokeh-related UI components should be initialized during setup(), which invokes _setup_render(), _setup_title() and _setup_content(). Otherwise, the ID of bokeh components will be used by other HTML documents (such as when you refresh the web page), and cause server error.
Extend ViewBase
We have following mixin classes to extend behaviors of ViewBase.
InvisibleView
Once MyView inherits from InvisibleView, then MyView becomes invisible.
Extra UI elements:
attribute
visible_btn(Switch) shown at the first place in the title row.
and visible state of the following things will be controlled:
content row (all things returned from
_setup_content()).all attributes
render_*with type hinted GlyphRenderer
StateView
Once MyView inherits from StateView, then MyView can read/restore the state from *.config.json with a correspond channelmap file.
DynamicView
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.
BoundView
Once MyView inherits from BoundView, it indicates MyView will plot something that has a boundary in the figure.
Extra UI elements:
a
tool_boundaryBoxEditTool in the figure toolbars.(optional) scaling controls
Renders in figure
a boundary rectangle
render_boundary(Rect) controlled bydata_boundary.
A help function:
transform_image_data(image, boundary): to fit image data into the boundary.
You must overwrite the method _setup_render() to provide your image-like render, and the method on_boundary_transform() to receive the updated transformation.
Extend ViewBase (advance)
The following minxin classes are the special class that the methods declared are decorated by @final, because they are used to communicate with the CartoApp. In details, the methods are replaced by CartoApp with the actual content during the setup.
ControllerView
It is used to control other components in CartoApp.
GlobalStateView
As same as StateView but it can to store/restore the config into/from user config file.
EditorView
It is used to change the channelmap and the blueprint. A method update_probe() is given to notify the updated channelmap or blueprint.
RecordView
(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.
ExtensionView
It makes a component only enable itself when the probe is supported something.
Utility functions
Besides mixin classes, we have some utility functions.
as_callback
wrap a callback into bokeh event callback.
[ ]:
from bokeh.models import Slider
from neurocarto.util.bokeh_util import as_callback
class MyView(ViewBase):
def setup(self):
slider = Slider(...)
slider.on_change('value', self._without_as_callback)
slider.on_change('value', as_callback(self._with_as_callback))
# only allow this signature by Bokeh
def _without_as_callback(self, prop:str, old_value, new_value): ...
# allow following all signatures by as_callback
def _with_as_callback(self): ...
def _with_as_callback(self, new_value): ...
def _with_as_callback(self, old_value, new_value): ...
def _with_as_callback(self, prop:str, old_value, new_value): ...
recursive_call_barrier
A method decorator to detect recursive calling stack for an event processing.
[ ]:
from neurocarto.util.bokeh_util import recursive_call_barrier
class MyView(ViewBase):
@recursive_call_barrier
def on_change(self, value): # as UI component event callback
self.set_value(value)
def set_value(self, value): # may call by other UI components
... # set value to UI component, invoking on_change(value)
UI factory
A factory class to produce UI controls with the same styling. So far, we have provided ButtonFactory and SliderFactory.
PathAutocompleteInput
A class extend AutocompleteInput to support file input with path completion.
[ ]:
from bokeh.layouts import row
from neurocarto.util.bokeh_util import PathAutocompleteInput
def on_image_selected(path:Path):
pass
pai = PathAutocompleteInput(
Path('.'),
on_image_selected,
mode='file',
accept=['image/*'], # file suffix '.png' or mime type 'image/png'
width=300,
)
row(pai.input);
DataView
DataView handle probe-related data, either static (like experimental data) or dynamic (like real-time experimental data) data.
It required to implement an abstract method data(), which returns a dictionary that used for updating the ColumnDataSource.
It has subclasses:
Data1DView
If you data is one-dimension data along the probe, multi_line is used, and required data dictionary should like dict(x=[[...]], y=[[...]]).
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.
Examples
neurocarto.views.data_density.ElectrodeDensityDataView
FileDataView
Providing an extra PathAutocompleteInput to get filepath from GUI. load_data(file) will be invoked.
ImageView
ImageView handle the image render. It require a ImageHandler to provide the image and its information.
It has a sub-class:
FileImageView
Providing an extra PathAutocompleteInput to get a image filepath from GUI. use ImageHandler.from_file() to create a ImageHandler for the image.
ImageHandler
A ImageHandler holds a image (2D and 3D image) and provide the related informations. It used by ImageView.
It has sub-classes:
NumpyImageHandler
It holds a image sotred in a numpy array. It is a static handler that the content of image does not update when probe is updated. It is usually created by ImageHandler.from_file(), ImageHandler.from_numpy() and ImageHandler.from_tiff() (not tested yet).
PltImageHandler
It holds a image generated by matplotlib. 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.
This class provide a context function plot_figure() to hold a Axes for plotting. After exiting the context, the image will be shown.
[ ]:
# from tests/main_image_plt_plot_channelmap.py
class PlotChannelMap(PltImageHandler):
def on_probe_update(self, probe, chmap, e):
if chmap is not None:
self.plot_channelmap(chmap)
else:
self.set_image(None)
def plot_channelmap(self, m):
from neurocarto.probe_npx import plot
with self.plot_figure() as ax:
plot.plot_channelmap_block(ax, chmap=m)
plot.plot_probe_shape(ax, m, color='k')
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. Therefore, user doesn’t need to change its position and its resolution to align the boken figure.