Skip to content

Commit

Permalink
Simple panel app (#27)
Browse files Browse the repository at this point in the history
  • Loading branch information
hoxbro committed Sep 20, 2023
1 parent 11f9842 commit 2325406
Show file tree
Hide file tree
Showing 4 changed files with 291 additions and 0 deletions.
130 changes: 130 additions & 0 deletions examples/app_example.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "e9254fc3-0447-440e-825e-68e02dcb9eb7",
"metadata": {},
"outputs": [],
"source": [
"import panel as pn\n",
"import numpy as np\n",
"import pandas as pd\n",
"import hvplot.pandas\n",
"import numpy as np\n",
"import holoviews as hv\n",
"\n",
"from holonote.annotate import SQLiteDB, Annotator\n",
"from holonote.app import PanelWidgets\n",
"\n",
"pn.extension()"
]
},
{
"cell_type": "markdown",
"id": "4867a649-3a32-4004-8d81-f47090eacf98",
"metadata": {},
"source": [
"# Single figure"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6273665b-b850-451b-a0c8-0635bcdfe765",
"metadata": {},
"outputs": [],
"source": [
"curve = pd.read_parquet(\"assets/example.parquet\").hvplot(x=\"TIME\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "74340a51-d1b0-4cd7-954f-923f45c50941",
"metadata": {},
"outputs": [],
"source": [
"connector = SQLiteDB(table_name=\"test_app\")\n",
"fields = [\"Stoppage\", \"Reason\", \"Category\"]\n",
"annotator = Annotator({\"TIME\": np.datetime64}, fields=fields, connector=connector)\n",
"annotator_element = annotator * curve"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b65b2e68-057c-490c-9061-4ee4ea69ca0e",
"metadata": {},
"outputs": [],
"source": [
"fields_values = {\n",
" \"Stoppage\": [\"Yes\", \"No\"],\n",
" \"Category\": [\"Mechanical\", \"Electrical\", \"Process\", \"Other\"],\n",
"}\n",
"\n",
"w = PanelWidgets(annotator, field_values=fields_values)\n",
"pn.Row(w, annotator_element).servable()"
]
},
{
"cell_type": "markdown",
"id": "cc26d77d-4c47-4037-a2a4-1b389988ff44",
"metadata": {},
"source": [
"# Multiple figures"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "e5a8c340-0211-436b-9f51-fe2240bc2a73",
"metadata": {},
"outputs": [],
"source": [
"xvals = np.linspace(-4, 0, 202)\n",
"yvals = np.linspace(4, 0, 202)\n",
"xs, ys = np.meshgrid(xvals, yvals)\n",
"alpha, beta = 1, 0\n",
"ab_data = np.sin(((ys / alpha) ** alpha + beta) * xs)\n",
"\n",
"image = hv.Image(ab_data, kdims=[\"A\", \"B\"]).opts(cmap=\"greens\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "de2c967f-067b-47da-9fe7-201ee7cd2df4",
"metadata": {},
"outputs": [],
"source": [
"connector = SQLiteDB(table_name=\"test_multi_app\")\n",
"fields = [\"Stoppage\", \"Reason\", \"Category\"]\n",
"multi_annotator = Annotator(\n",
" {\"TIME\": np.datetime64, \"A\": float, \"B\": float}, fields=fields, connector=connector\n",
")\n",
"\n",
"multi_annotator_element = multi_annotator * curve + multi_annotator * image"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2896d0a4-54bc-4d49-97ab-59cb77a031f7",
"metadata": {},
"outputs": [],
"source": [
"mutli_w = PanelWidgets(multi_annotator, field_values=fields_values)\n",
"pn.Row(mutli_w, multi_annotator_element).servable()"
]
}
],
"metadata": {
"language_info": {
"name": "python",
"pygments_lexer": "ipython3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
1 change: 1 addition & 0 deletions holonote/app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .panel import PanelWidgets # noqa: F401
151 changes: 151 additions & 0 deletions holonote/app/panel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
from __future__ import annotations

import datetime as dt
from typing import TYPE_CHECKING, Any

import panel as pn
import param

if TYPE_CHECKING:
from holonote.annotate import Annotator


class PanelWidgets:
mapping = {
str: pn.widgets.TextInput,
bool: pn.widgets.Checkbox,
dt.datetime: pn.widgets.DatePicker,
dt.date: pn.widgets.DatePicker,
int: pn.widgets.IntSlider,
float: pn.widgets.FloatSlider,
}

def __init__(self, annotator: Annotator, field_values: dict[str, Any] | None=None):
self.annotator = annotator
self.annotator.snapshot()
self._widget_mode_group = pn.widgets.RadioButtonGroup(
name="Mode", options=["+", "-", "✏"], width=90
)
self._widget_apply_button = pn.widgets.Button(name="✓", width=20)
self._widget_revert_button = pn.widgets.Button(name="↺", width=20)
self._widget_commit_button = pn.widgets.Button(name="▲", width=20)

if field_values is None:
self._fields_values = {k: "" for k in self.annotator.fields}
else:
self._fields_values = {
k: field_values.get(k, "") for k in self.annotator.fields
}
self._fields_widgets = self._create_fields_widgets(self._fields_values)

self._set_standard_callbacks()

@property
def tool_widgets(self):
return pn.Row(
self._widget_apply_button,
pn.Spacer(width=10),
self._widget_mode_group,
pn.Spacer(width=10),
self._widget_revert_button,
self._widget_commit_button,
)

def _create_fields_widgets(self, fields_values):
fields_widgets = {}
for widget_name, default in fields_values.items():
if isinstance(default, param.Parameter):
parameterized = type(
"widgets", (param.Parameterized,), {widget_name: default}
)
pane = pn.Param(parameterized)
fields_widgets[widget_name] = pane.layout[1]
elif isinstance(default, list):
fields_widgets[widget_name] = pn.widgets.Select(
value=default[0], options=default, name=widget_name
)
else:
widget_type = self.mapping[type(default)]
if issubclass(widget_type, pn.widgets.TextInput):
fields_widgets[widget_name] = widget_type(
value=default, placeholder=widget_name, name=widget_name
)
else:
fields_widgets[widget_name] = widget_type(
value=default, name=widget_name
)
return fields_widgets

@property
def fields_widgets(self):
accordion = False # Experimental
widgets = pn.Column(*self._fields_widgets.values())
if accordion:
return pn.Accordion(("fields", widgets))
else:
return widgets

def _reset_fields_widgets(self):
for widget_name, default in self._fields_values.items():
if isinstance(default, param.Parameter):
default = default.default
try:
self._fields_widgets[widget_name].value = default
except Exception:
pass # TODO: Fix when lists (for categories, not the same as the default!)

def _callback_apply(self, event):
selected_ind = (
self.annotator.selected_indices[0]
if len(self.annotator.selected_indices) == 1
else None
)
self.annotator.select_by_index()

if self._widget_mode_group.value in ["+", "✏"]:
fields_values = {k: v.value for k, v in self._fields_widgets.items()}
if self._widget_mode_group.value == "+":
self.annotator.add_annotation(**fields_values)
self._reset_fields_widgets()
elif (self._widget_mode_group.value == "✏") and (selected_ind is not None):
self.annotator.update_annotation_fields(
selected_ind, **fields_values
) # TODO: Handle only changed
elif self._widget_mode_group.value == "-":
if selected_ind is not None:
self.annotator.delete_annotation(selected_ind)

def _callback_commit(self, event):
self.annotator.commit()

def _watcher_selected_indices(self, event):
if len(event.new) != 1:
return
selected_index = event.new[0]
# if self._widget_mode_group.value == '✏':
for name, widget in self._fields_widgets.items():
value = self.annotator.annotation_table._field_df.loc[selected_index][name]
widget.value = value

def _watcher_mode_group(self, event):
if event.new in ["-", "✏"]:
self.annotator.selection_enabled = True
self.annotator.select_by_index()
self.annotator.editable_enabled = False
elif event.new == "+":
self.annotator.editable_enabled = True
self.annotator.select_by_index()
self.annotator.selection_enabled = False

for widget in self._fields_widgets.values():
widget.disabled = event.new == "-"

def _set_standard_callbacks(self):
self._widget_apply_button.on_click(self._callback_apply)
self._widget_revert_button.on_click(lambda event: self.annotator.revert_to_snapshot())
self._widget_commit_button.on_click(self._callback_commit)
self.annotator.param.watch(self._watcher_selected_indices, "selected_indices")
self._widget_mode_group.param.watch(self._watcher_mode_group, "value")

def __panel__(self):
return pn.Column(self.fields_widgets, self.tool_widgets)
9 changes: 9 additions & 0 deletions holonote/tests/test_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import panel as pn

from holonote.app import PanelWidgets


def test_panel_app(annotator_range1d):
w = PanelWidgets(annotator_range1d)
assert isinstance(w.fields_widgets, pn.Column)
assert isinstance(w.tool_widgets, pn.Row)

0 comments on commit 2325406

Please sign in to comment.