-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
291 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from .panel import PanelWidgets # noqa: F401 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |