From 882b9ea6d25b8d85fdb49d9af8b951fef0969826 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Sun, 18 Sep 2022 21:58:40 +0200 Subject: [PATCH 01/10] progress towards removing to_widget --- src/plopp/interactive.py | 57 +++++++++++++++++++++++--------------- src/plopp/plot.py | 22 ++++----------- src/plopp/toolbar.py | 20 +++++++------ src/plopp/view.py | 4 +-- src/plopp/widgets/slice.py | 51 ++++++++++++++++++++++------------ 5 files changed, 86 insertions(+), 68 deletions(-) diff --git a/src/plopp/interactive.py b/src/plopp/interactive.py index ce0a977c..5795aec7 100644 --- a/src/plopp/interactive.py +++ b/src/plopp/interactive.py @@ -5,26 +5,37 @@ from .figure import Figure from .toolbar import Toolbar -import ipywidgets as ipw +# import ipywidgets as ipw +from ipywidgets import VBox, HBox +# class SideBar(Displayable): +# def __init__(self, *args, **kwargs): +# self._children = list(*args, **kwargs) +# super().__init__(self._children) -class SideBar(list, Displayable): +# # def to_widget(self): +# # return VBox([child.to_widget() for child in self]) - def to_widget(self): - return ipw.VBox([child.to_widget() for child in self]) +class InteractiveFig(Figure, VBox): -class InteractiveFig(Figure, Displayable): + def __init__(self, *args, **kwargs): + + Figure.__init__(self, *args, **kwargs) + VBox.__init__(self, [ + self.top_bar, + HBox([self.left_bar, self._fig.canvas, self.right_bar]), self.bottom_bar + ]) def _post_init(self): self._fig.canvas.toolbar_visible = False self._fig.canvas.header_visible = False - self.left_bar = SideBar() - self.right_bar = SideBar() - self.bottom_bar = SideBar() - self.top_bar = SideBar() + self.left_bar = VBox() + self.right_bar = VBox() + self.bottom_bar = HBox() + self.top_bar = HBox() self.toolbar = Toolbar( tools={ @@ -37,20 +48,20 @@ def _post_init(self): }) self._fig.canvas.toolbar_visible = False self._fig.canvas.header_visible = False - self.left_bar.append(self.toolbar) - - def to_widget(self) -> ipw.Widget: - """ - Convert the Matplotlib figure to a widget. - """ - return ipw.VBox([ - self.top_bar.to_widget(), - ipw.HBox([ - self.left_bar.to_widget(), self._fig.canvas, - self.right_bar.to_widget() - ]), - self.bottom_bar.to_widget() - ]) + self.left_bar.children = tuple([self.toolbar]) + + # def to_widget(self) -> Widget: + # """ + # Convert the Matplotlib figure to a widget. + # """ + # return VBox([ + # self.top_bar.to_widget(), + # HBox([ + # self.left_bar.to_widget(), self._fig.canvas, + # self.right_bar.to_widget() + # ]), + # self.bottom_bar.to_widget() + # ]) def home(self): self._autoscale() diff --git a/src/plopp/plot.py b/src/plopp/plot.py index 0cdbd80d..d4b5bb9a 100644 --- a/src/plopp/plot.py +++ b/src/plopp/plot.py @@ -1,26 +1,14 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2022 Scipp contributors (https://github.com/scipp) -from .displayable import Displayable +from ipywidgets import VBox, HBox -def _maybe_to_widget(view): - return view.to_widget() if hasattr(view, "to_widget") else view - - -class Plot(Displayable): +class Plot(VBox): def __init__(self, views): self.views = views - - def to_widget(self): - """ - """ - import ipywidgets as ipw - out = [] + children = [] for view in self.views: - if isinstance(view, (list, tuple)): - out.append(ipw.HBox([_maybe_to_widget(v) for v in view])) - else: - out.append(_maybe_to_widget(view)) - return ipw.VBox(out) + children.append(HBox(view) if isinstance(view, (list, tuple)) else view) + return super().__init__(children) diff --git a/src/plopp/toolbar.py b/src/plopp/toolbar.py index f984ebd1..4b8810d6 100644 --- a/src/plopp/toolbar.py +++ b/src/plopp/toolbar.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2022 Scipp contributors (https://github.com/scipp) +from ipywidgets import Button, VBox from functools import partial from typing import Callable @@ -33,7 +34,7 @@ def __init__(self, callback: Callable, value: bool = False, **kwargs): cases, we need to toggle the button color without triggering the callback function. """ - from ipywidgets import Button + # from ipywidgets import Button self.widget = Button(**{**LAYOUT_STYLE, **kwargs}) self._callback = callback self.widget.on_click(self) @@ -70,7 +71,7 @@ def _toggle(self): } -class Toolbar(Displayable): +class Toolbar(VBox): """ Custom toolbar with additional buttons for controlling log scales and normalization, and with back/forward buttons removed. @@ -82,10 +83,11 @@ def __init__(self, tools=None): tool = TOOL_LIBRARY[key](callback=callback) setattr(self, key, tool) self._widgets[key] = tool.widget - - def to_widget(self): - """ - Return the VBox container - """ - from ipywidgets import VBox - return VBox(tuple(self._widgets.values())) + super().__init__(tuple(self._widgets.values())) + + # def to_widget(self): + # """ + # Return the VBox container + # """ + # from ipywidgets import VBox + # return VBox(tuple(self._widgets.values())) diff --git a/src/plopp/view.py b/src/plopp/view.py index 25865d63..c69e3ec2 100644 --- a/src/plopp/view.py +++ b/src/plopp/view.py @@ -1,11 +1,11 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2022 Scipp contributors (https://github.com/scipp) -from abc import ABC, abstractmethod +from abc import abstractmethod import uuid -class View(ABC): +class View: def __init__(self, *nodes): self.id = str(uuid.uuid1()) diff --git a/src/plopp/widgets/slice.py b/src/plopp/widgets/slice.py index d3a1fb60..2d9aac16 100644 --- a/src/plopp/widgets/slice.py +++ b/src/plopp/widgets/slice.py @@ -38,7 +38,7 @@ def render(self): self._update(new_coords=new_values.meta) -class SliceWidget(Displayable): +class SliceWidget(ipw.VBox): """ Widgets containing a slider for each of the input's dimensions, as well as buttons to modify the currently displayed axes. @@ -50,6 +50,7 @@ def __init__(self, data_array, dims: list): self._slider_dims = dims self.controls = {} self.view = None + children = [] for dim in dims: slider = ipw.IntSlider(step=1, @@ -63,22 +64,38 @@ def __init__(self, data_array, dims: list): description="Continuous update", indent=False, layout={"width": "20px"}) + label = ipw.Label() ipw.jslink((continuous_update, 'value'), (slider, 'continuous_update')) - self.controls[dim] = {'continuous': continuous_update, 'slider': slider} - - for dim in self._slider_dims: - row = list(self.controls[dim].values()) - self._container.append(ipw.HBox(row)) - - def to_widget(self) -> ipw.Widget: - """ - Gather all widgets in a single container box. - """ - out = ipw.VBox(self._container) - if self.view is not None: - out = ipw.HBox([out, self.view.to_widget()]) - return out + self.controls[dim] = { + 'continuous': continuous_update, + 'slider': slider, + 'label': label, + 'coord': data_array.meta[dim] + } + # slider.observe(self._update_label, names='value') + children.append(ipw.HBox([continuous_update, slider, label])) + + super().__init__(children) + + # for dim in self._slider_dims: + # row = list(self.controls[dim].values()) + # self._container.append(ipw.HBox(row)) + + # def to_widget(self) -> ipw.Widget: + # """ + # Gather all widgets in a single container box. + # """ + # out = ipw.VBox(self._container) + # if self.view is not None: + # out = ipw.HBox([out, self.view.to_widget()]) + # return out + + def _update_label(self, change): + dim = change['owner'].description + coord = self.controls[dim]['coord'] + self.controls[dim]['label'].value = value_to_string( + coord[dim, change['new']].values) + str(coord.unit) def observe(self, callback: Callable, **kwargs): for dim in self.controls: @@ -88,8 +105,8 @@ def observe(self, callback: Callable, **kwargs): def value(self) -> dict: return {dim: self.controls[dim]['slider'].value for dim in self._slider_dims} - def make_view(self, *nodes): - self.view = SliceView(self._slider_dims, *nodes) + # def make_view(self, *nodes): + # self.view = SliceView(self._slider_dims, *nodes) @node From e45a99098ed95206596fef6973c818c643427a7e Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Sun, 18 Sep 2022 23:01:29 +0200 Subject: [PATCH 02/10] stuck on slice widget creation --- src/plopp/widgets/slice.py | 46 +++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/plopp/widgets/slice.py b/src/plopp/widgets/slice.py index 2d9aac16..50202cbf 100644 --- a/src/plopp/widgets/slice.py +++ b/src/plopp/widgets/slice.py @@ -10,32 +10,31 @@ import ipywidgets as ipw from typing import Callable +# class SliceView(View): -class SliceView(View): +# def __init__(self, dims, *nodes): +# super().__init__(*nodes) +# self._labels = {dim: ipw.Label() for dim in dims} - def __init__(self, dims, *nodes): - super().__init__(*nodes) - self._labels = {dim: ipw.Label() for dim in dims} +# def to_widget(self) -> ipw.Widget: +# self.render() +# return ipw.VBox(list(self._labels.values())) - def to_widget(self) -> ipw.Widget: - self.render() - return ipw.VBox(list(self._labels.values())) +# def _update(self, new_coords): +# for dim, lab in self._labels.items(): +# if dim in new_coords: +# lab.value = value_to_string(new_coords[dim].values) + str( +# new_coords[dim].unit) - def _update(self, new_coords): - for dim, lab in self._labels.items(): - if dim in new_coords: - lab.value = value_to_string(new_coords[dim].values) + str( - new_coords[dim].unit) +# def notify_view(self, message): +# node_id = message["node_id"] +# new_values = self._graph_nodes[node_id].request_data() +# self._update(new_values.meta) - def notify_view(self, message): - node_id = message["node_id"] - new_values = self._graph_nodes[node_id].request_data() - self._update(new_values.meta) - - def render(self): - for n in self._graph_nodes.values(): - new_values = n.request_data() - self._update(new_coords=new_values.meta) +# def render(self): +# for n in self._graph_nodes.values(): +# new_values = n.request_data() +# self._update(new_coords=new_values.meta) class SliceWidget(ipw.VBox): @@ -45,6 +44,7 @@ class SliceWidget(ipw.VBox): """ def __init__(self, data_array, dims: list): + print("INIIIIT") self._container = [] self._slider_dims = dims @@ -73,7 +73,7 @@ def __init__(self, data_array, dims: list): 'label': label, 'coord': data_array.meta[dim] } - # slider.observe(self._update_label, names='value') + slider.observe(self._update_label, names='value') children.append(ipw.HBox([continuous_update, slider, label])) super().__init__(children) @@ -97,7 +97,7 @@ def _update_label(self, change): self.controls[dim]['label'].value = value_to_string( coord[dim, change['new']].values) + str(coord.unit) - def observe(self, callback: Callable, **kwargs): + def observe(self, callback: Callable, ignored, **kwargs): for dim in self.controls: self.controls[dim]['slider'].observe(callback, **kwargs) From 543a15f4e2ea3d8c0a731c2bc1d189d75aea7b99 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Mon, 19 Sep 2022 11:36:40 +0200 Subject: [PATCH 03/10] make slider coord update local to slider --- src/plopp/widgets/slice.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/plopp/widgets/slice.py b/src/plopp/widgets/slice.py index 50202cbf..ed146bd6 100644 --- a/src/plopp/widgets/slice.py +++ b/src/plopp/widgets/slice.py @@ -37,6 +37,13 @@ # self._update(new_coords=new_values.meta) +def _coord_to_string(coord): + out = value_to_string(coord.values) + if coord.unit is not None: + out += f" [{coord.unit}]" + return out + + class SliceWidget(ipw.VBox): """ Widgets containing a slider for each of the input's dimensions, as well as @@ -44,7 +51,7 @@ class SliceWidget(ipw.VBox): """ def __init__(self, data_array, dims: list): - print("INIIIIT") + # print("INIIIIT") self._container = [] self._slider_dims = dims @@ -53,25 +60,26 @@ def __init__(self, data_array, dims: list): children = [] for dim in dims: + coord = data_array.meta[dim] slider = ipw.IntSlider(step=1, description=dim, min=0, max=data_array.sizes[dim], continuous_update=True, - readout=True, - layout={"width": "400px"}) + readout=False, + layout={"width": "200px"}) continuous_update = ipw.Checkbox(value=True, description="Continuous update", indent=False, layout={"width": "20px"}) - label = ipw.Label() + label = ipw.Label(value=_coord_to_string(coord[dim, 0])) ipw.jslink((continuous_update, 'value'), (slider, 'continuous_update')) self.controls[dim] = { 'continuous': continuous_update, 'slider': slider, 'label': label, - 'coord': data_array.meta[dim] + 'coord': coord } slider.observe(self._update_label, names='value') children.append(ipw.HBox([continuous_update, slider, label])) @@ -93,13 +101,12 @@ def __init__(self, data_array, dims: list): def _update_label(self, change): dim = change['owner'].description - coord = self.controls[dim]['coord'] - self.controls[dim]['label'].value = value_to_string( - coord[dim, change['new']].values) + str(coord.unit) + coord = self.controls[dim]['coord'][dim, change['new']] + self.controls[dim]['label'].value = _coord_to_string(coord) - def observe(self, callback: Callable, ignored, **kwargs): - for dim in self.controls: - self.controls[dim]['slider'].observe(callback, **kwargs) + # def observe(self, callback: Callable, ignored, **kwargs): + # for dim in self.controls: + # self.controls[dim]['slider'].observe(callback, **kwargs) @property def value(self) -> dict: From 6bf2e3e1d28960feec7ea624c4e3bac09a794064 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Mon, 19 Sep 2022 12:47:46 +0200 Subject: [PATCH 04/10] make mask checkboxes a widget --- src/plopp/__init__.py | 2 +- src/plopp/model.py | 11 +++++++ src/plopp/widgets/__init__.py | 1 - src/plopp/widgets/checkboxes.py | 57 +++++++++------------------------ src/plopp/widgets/slice.py | 56 +++++--------------------------- src/plopp/widgets/widgetnode.py | 10 ------ tests/graph_nodes_test.py | 16 ++++----- 7 files changed, 43 insertions(+), 110 deletions(-) delete mode 100644 src/plopp/widgets/widgetnode.py diff --git a/src/plopp/__init__.py b/src/plopp/__init__.py index 394a3119..45dc97e6 100644 --- a/src/plopp/__init__.py +++ b/src/plopp/__init__.py @@ -15,7 +15,7 @@ from .graph import show_graph from .plot import Plot -from .model import Node, node, input_node +from .model import Node, node, input_node, widget_node from .wrappers import plot, figure from . import data diff --git a/src/plopp/model.py b/src/plopp/model.py index eeb8a6ce..5dbebfe1 100644 --- a/src/plopp/model.py +++ b/src/plopp/model.py @@ -52,3 +52,14 @@ def make_node(*args, **kwargs): def input_node(obj): return Node(lambda: obj) + + +def widget_node(widget): + n = Node(func=lambda: widget.value) + # TODO: Our custom widgets have a 'watch' method instead of 'observe' because + # inheriting from VBox causes errors when overriding the 'observe' method + # (see https://bit.ly/3SggPVS). + # We need to be careful that widgets don't get a method named 'watch' in the future. + func = widget.watch if hasattr(widget, 'watch') else widget.observe + func(n.notify_children, names="value") + return n diff --git a/src/plopp/widgets/__init__.py b/src/plopp/widgets/__init__.py index 1230455a..1b8c87b4 100644 --- a/src/plopp/widgets/__init__.py +++ b/src/plopp/widgets/__init__.py @@ -6,4 +6,3 @@ # from .masks import MaskWidget, hide_masks from .checkboxes import Checkboxes from .slice import SliceWidget, slice_dims -from .widgetnode import widget_node diff --git a/src/plopp/widgets/checkboxes.py b/src/plopp/widgets/checkboxes.py index 2f8b5a14..274c4096 100644 --- a/src/plopp/widgets/checkboxes.py +++ b/src/plopp/widgets/checkboxes.py @@ -1,24 +1,20 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2022 Scipp contributors (https://github.com/scipp) -from functools import partial from html import escape import ipywidgets as ipw from typing import Callable -from ..displayable import Displayable -class Checkboxes(Displayable): +class Checkboxes(ipw.HBox): """ Widget providing a list of checkboxes, along with a button to toggle them all. """ def __init__(self, entries: list, description=""): - - self._callback = None - self.checkboxes = {} self._lock = False + self._description = ipw.Label(value=description) for key in entries: self.checkboxes[key] = ipw.Checkbox(value=True, @@ -26,54 +22,31 @@ def __init__(self, entries: list, description=""): indent=False, layout={"width": "initial"}) - if len(self.checkboxes): - self._description = ipw.Label(value=description) + to_hbox = [ + self._description, + ipw.HBox(list(self.checkboxes.values()), + layout=ipw.Layout(flex_flow='row wrap')) + ] + if len(self.checkboxes): # Add a master button to control all masks in one go self.toggle_all_button = ipw.ToggleButton(value=True, description="De-select all", disabled=False, button_style="", - layout={"width": "100px"}) + layout={"width": "initial"}) + for cbox in self.checkboxes.values(): + ipw.jsdlink((self.toggle_all_button, 'value'), (cbox, 'value')) + to_hbox.insert(1, self.toggle_all_button) - def to_widget(self) -> ipw.Widget: - """ - Gather all widgets in a single container box. - """ - out = ipw.HBox() - if len(self.checkboxes): - out.children = [ - self._description, self.toggle_all_button, - ipw.HBox(list(self.checkboxes.values())) - ] - return out + super().__init__(to_hbox) - def observe(self, callback: Callable, **kwargs): + def watch(self, callback: Callable, **kwargs): for chbx in self.checkboxes.values(): - chbx.observe(self._toggle, **kwargs) - if len(self.checkboxes): - self.toggle_all_button.observe(self._toggle_all, **kwargs) - self._callback = partial(callback, None) + chbx.observe(callback, **kwargs) @property def value(self) -> dict: """ """ return {key: chbx.value for key, chbx in self.checkboxes.items()} - - def _toggle(self, _): - if self._lock: - return - self._callback() - - def _toggle_all(self, change: dict): - """ - A main button to hide or show all masks at once. - """ - self._lock = True - for key in self.checkboxes: - self.checkboxes[key].value = change["new"] - change["owner"].description = ("De-select all" - if change["new"] else "Select all") - self._lock = False - self._callback() diff --git a/src/plopp/widgets/slice.py b/src/plopp/widgets/slice.py index ed146bd6..74e612ee 100644 --- a/src/plopp/widgets/slice.py +++ b/src/plopp/widgets/slice.py @@ -5,37 +5,10 @@ from ..tools import value_to_string from ..view import View from ..model import node -from ..displayable import Displayable import ipywidgets as ipw from typing import Callable -# class SliceView(View): - -# def __init__(self, dims, *nodes): -# super().__init__(*nodes) -# self._labels = {dim: ipw.Label() for dim in dims} - -# def to_widget(self) -> ipw.Widget: -# self.render() -# return ipw.VBox(list(self._labels.values())) - -# def _update(self, new_coords): -# for dim, lab in self._labels.items(): -# if dim in new_coords: -# lab.value = value_to_string(new_coords[dim].values) + str( -# new_coords[dim].unit) - -# def notify_view(self, message): -# node_id = message["node_id"] -# new_values = self._graph_nodes[node_id].request_data() -# self._update(new_values.meta) - -# def render(self): -# for n in self._graph_nodes.values(): -# new_values = n.request_data() -# self._update(new_coords=new_values.meta) - def _coord_to_string(coord): out = value_to_string(coord.values) @@ -51,8 +24,6 @@ class SliceWidget(ipw.VBox): """ def __init__(self, data_array, dims: list): - # print("INIIIIT") - self._container = [] self._slider_dims = dims self.controls = {} @@ -86,35 +57,24 @@ def __init__(self, data_array, dims: list): super().__init__(children) - # for dim in self._slider_dims: - # row = list(self.controls[dim].values()) - # self._container.append(ipw.HBox(row)) - - # def to_widget(self) -> ipw.Widget: - # """ - # Gather all widgets in a single container box. - # """ - # out = ipw.VBox(self._container) - # if self.view is not None: - # out = ipw.HBox([out, self.view.to_widget()]) - # return out - def _update_label(self, change): dim = change['owner'].description coord = self.controls[dim]['coord'][dim, change['new']] self.controls[dim]['label'].value = _coord_to_string(coord) - # def observe(self, callback: Callable, ignored, **kwargs): - # for dim in self.controls: - # self.controls[dim]['slider'].observe(callback, **kwargs) + def watch(self, callback: Callable, **kwargs): + """ + TODO: Cannot name this method 'observe' when inheriting from HBox, so we name + it 'watch' instead (see https://bit.ly/3SggPVS). We need to be careful that + widgets don't get a method named 'watch' in the future. + """ + for dim in self.controls: + self.controls[dim]['slider'].observe(callback, **kwargs) @property def value(self) -> dict: return {dim: self.controls[dim]['slider'].value for dim in self._slider_dims} - # def make_view(self, *nodes): - # self.view = SliceView(self._slider_dims, *nodes) - @node def slice_dims(data_array: DataArray, slices: dict) -> DataArray: diff --git a/src/plopp/widgets/widgetnode.py b/src/plopp/widgets/widgetnode.py deleted file mode 100644 index af507247..00000000 --- a/src/plopp/widgets/widgetnode.py +++ /dev/null @@ -1,10 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright (c) 2022 Scipp contributors (https://github.com/scipp) - -from ..model import Node - - -def widget_node(widget): - n = Node(func=lambda: widget.value) - widget.observe(n.notify_children, names="value") - return n diff --git a/tests/graph_nodes_test.py b/tests/graph_nodes_test.py index b56ae81d..6719ac0f 100644 --- a/tests/graph_nodes_test.py +++ b/tests/graph_nodes_test.py @@ -2,8 +2,8 @@ # Copyright (c) 2022 Scipp contributors (https://github.com/scipp) import scipp as sc -from plopp import Plot, figure, input_node, node -from plopp.widgets import widget_node, Checkboxes, SliceWidget, slice_dims +from plopp import Plot, figure, node, input_node, widget_node +from plopp.widgets import Checkboxes, SliceWidget, slice_dims from plopp.data import dense_data_array, dense_dataset import ipywidgets as ipw @@ -60,7 +60,7 @@ def test_2d_image_smoothing_slider(): smooth_node = node(gaussian_filter)(a, sigma=sigma_node) fig = figure(smooth_node) - Plot([fig, sl]) + Plot([fig.to_widget(), sl]) sl.value = 5 @@ -76,7 +76,7 @@ def test_2d_image_with_masks(): masks_node = hide_masks(a, w) fig = figure(masks_node) - Plot([fig, widget]) + Plot([fig.to_widget(), widget]) widget.toggle_all_button.value = False @@ -95,7 +95,7 @@ def test_two_1d_lines_with_masks(): node_masks_a = hide_masks(a, w) node_masks_b = hide_masks(b, w) fig = figure(node_masks_a, node_masks_b) - Plot([fig, widget]) + Plot([fig.to_widget(), widget]) widget.toggle_all_button.value = False @@ -107,7 +107,7 @@ def test_node_sum_data_along_y(): fig1 = figure(a) fig2 = figure(s) - Plot([[fig1, fig2]]) + Plot([[fig1.to_widget(), fig2.to_widget()]]) def test_slice_3d_cube(): @@ -120,7 +120,7 @@ def test_slice_3d_cube(): sl.make_view(slice_node) fig = figure(slice_node) - Plot([fig, sl]) + Plot([fig.to_widget(), sl]) sl.controls["zz"]["slider"].value = 10 @@ -139,5 +139,5 @@ def test_3d_image_slicer_with_connected_side_histograms(): fx = figure(histx) fy = figure(histy) - Plot([[fx, fy], fig, sl]) + Plot([[fx.to_widget(), fy.to_widget()], fig.to_widget(), sl]) sl.controls["zz"]["slider"].value = 10 From 36b82d614cb4ef363a10397e206596dbae3a7b29 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Mon, 19 Sep 2022 12:55:14 +0200 Subject: [PATCH 05/10] cleanup --- src/plopp/displayable.py | 25 ------------------------- src/plopp/interactive.py | 22 ---------------------- src/plopp/toolbar.py | 9 --------- src/plopp/view.py | 4 ---- 4 files changed, 60 deletions(-) delete mode 100644 src/plopp/displayable.py diff --git a/src/plopp/displayable.py b/src/plopp/displayable.py deleted file mode 100644 index 1268a378..00000000 --- a/src/plopp/displayable.py +++ /dev/null @@ -1,25 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright (c) 2022 Scipp contributors (https://github.com/scipp) - -try: - from ipywidgets import Widget -except ImportError: - Widget = None - - -class Displayable: - - if hasattr(Widget, '_repr_mimebundle_'): - - def _repr_mimebundle_(self, include=None, exclude=None): - """ - Mimebundle display representation for jupyter notebooks. - """ - return self.to_widget()._repr_mimebundle_(include=include, exclude=exclude) - else: - - def _ipython_display_(self): - """ - IPython display representation for Jupyter notebooks. - """ - return self.to_widget()._ipython_display_() diff --git a/src/plopp/interactive.py b/src/plopp/interactive.py index 5795aec7..fd418b88 100644 --- a/src/plopp/interactive.py +++ b/src/plopp/interactive.py @@ -5,17 +5,8 @@ from .figure import Figure from .toolbar import Toolbar -# import ipywidgets as ipw from ipywidgets import VBox, HBox -# class SideBar(Displayable): -# def __init__(self, *args, **kwargs): -# self._children = list(*args, **kwargs) -# super().__init__(self._children) - -# # def to_widget(self): -# # return VBox([child.to_widget() for child in self]) - class InteractiveFig(Figure, VBox): @@ -50,19 +41,6 @@ def _post_init(self): self._fig.canvas.header_visible = False self.left_bar.children = tuple([self.toolbar]) - # def to_widget(self) -> Widget: - # """ - # Convert the Matplotlib figure to a widget. - # """ - # return VBox([ - # self.top_bar.to_widget(), - # HBox([ - # self.left_bar.to_widget(), self._fig.canvas, - # self.right_bar.to_widget() - # ]), - # self.bottom_bar.to_widget() - # ]) - def home(self): self._autoscale() self.crop(**self._crop) diff --git a/src/plopp/toolbar.py b/src/plopp/toolbar.py index 4b8810d6..a8d45fdb 100644 --- a/src/plopp/toolbar.py +++ b/src/plopp/toolbar.py @@ -16,7 +16,6 @@ def __init__(self, callback: Callable = None, **kwargs): """ Create a new button with a callback that is called when the button is clicked. """ - from ipywidgets import Button self.widget = Button(**{**LAYOUT_STYLE, **kwargs}) self._callback = callback self.widget.on_click(self) @@ -34,7 +33,6 @@ def __init__(self, callback: Callable, value: bool = False, **kwargs): cases, we need to toggle the button color without triggering the callback function. """ - # from ipywidgets import Button self.widget = Button(**{**LAYOUT_STYLE, **kwargs}) self._callback = callback self.widget.on_click(self) @@ -84,10 +82,3 @@ def __init__(self, tools=None): setattr(self, key, tool) self._widgets[key] = tool.widget super().__init__(tuple(self._widgets.values())) - - # def to_widget(self): - # """ - # Return the VBox container - # """ - # from ipywidgets import VBox - # return VBox(tuple(self._widgets.values())) diff --git a/src/plopp/view.py b/src/plopp/view.py index c69e3ec2..5048070d 100644 --- a/src/plopp/view.py +++ b/src/plopp/view.py @@ -17,7 +17,3 @@ def __init__(self, *nodes): @abstractmethod def notify_view(self, _): return - - @abstractmethod - def to_widget(self): - return From f2a25882be95eb326d52a9ef477e5e8f93804484 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Mon, 19 Sep 2022 12:59:40 +0200 Subject: [PATCH 06/10] flake8 --- src/plopp/interactive.py | 1 - src/plopp/toolbar.py | 2 -- src/plopp/widgets/slice.py | 1 - 3 files changed, 4 deletions(-) diff --git a/src/plopp/interactive.py b/src/plopp/interactive.py index fd418b88..6680ea62 100644 --- a/src/plopp/interactive.py +++ b/src/plopp/interactive.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2022 Scipp contributors (https://github.com/scipp) -from .displayable import Displayable from .figure import Figure from .toolbar import Toolbar diff --git a/src/plopp/toolbar.py b/src/plopp/toolbar.py index a8d45fdb..410ec6a3 100644 --- a/src/plopp/toolbar.py +++ b/src/plopp/toolbar.py @@ -5,8 +5,6 @@ from functools import partial from typing import Callable -from .displayable import Displayable - LAYOUT_STYLE = {"layout": {"width": "34px", "padding": "0px 0px 0px 0px"}} diff --git a/src/plopp/widgets/slice.py b/src/plopp/widgets/slice.py index 74e612ee..e4417b18 100644 --- a/src/plopp/widgets/slice.py +++ b/src/plopp/widgets/slice.py @@ -3,7 +3,6 @@ from scipp import DataArray from ..tools import value_to_string -from ..view import View from ..model import node import ipywidgets as ipw From 4b4d44106b52c71c3eb5de7587980f3fe5ce0ae5 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Mon, 19 Sep 2022 13:37:36 +0200 Subject: [PATCH 07/10] watch -> _plopp_observe --- src/plopp/model.py | 8 ++++---- src/plopp/widgets/checkboxes.py | 6 +++++- src/plopp/widgets/slice.py | 5 ++--- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/plopp/model.py b/src/plopp/model.py index 5dbebfe1..c585c101 100644 --- a/src/plopp/model.py +++ b/src/plopp/model.py @@ -56,10 +56,10 @@ def input_node(obj): def widget_node(widget): n = Node(func=lambda: widget.value) - # TODO: Our custom widgets have a 'watch' method instead of 'observe' because - # inheriting from VBox causes errors when overriding the 'observe' method + # TODO: Our custom widgets have a '_plopp_observe' method instead of 'observe' + # because inheriting from VBox causes errors when overriding the 'observe' method # (see https://bit.ly/3SggPVS). - # We need to be careful that widgets don't get a method named 'watch' in the future. - func = widget.watch if hasattr(widget, 'watch') else widget.observe + func = widget._plopp_observe if hasattr(widget, + '_plopp_observe') else widget.observe func(n.notify_children, names="value") return n diff --git a/src/plopp/widgets/checkboxes.py b/src/plopp/widgets/checkboxes.py index 274c4096..3658ca3b 100644 --- a/src/plopp/widgets/checkboxes.py +++ b/src/plopp/widgets/checkboxes.py @@ -41,7 +41,11 @@ def __init__(self, entries: list, description=""): super().__init__(to_hbox) - def watch(self, callback: Callable, **kwargs): + def _plopp_observe(self, callback: Callable, **kwargs): + """ + TODO: Cannot name this method 'observe' when inheriting from HBox, so we name + it '_plopp_observe' instead (see https://bit.ly/3SggPVS). + """ for chbx in self.checkboxes.values(): chbx.observe(callback, **kwargs) diff --git a/src/plopp/widgets/slice.py b/src/plopp/widgets/slice.py index e4417b18..5c439607 100644 --- a/src/plopp/widgets/slice.py +++ b/src/plopp/widgets/slice.py @@ -61,11 +61,10 @@ def _update_label(self, change): coord = self.controls[dim]['coord'][dim, change['new']] self.controls[dim]['label'].value = _coord_to_string(coord) - def watch(self, callback: Callable, **kwargs): + def _plopp_observe(self, callback: Callable, **kwargs): """ TODO: Cannot name this method 'observe' when inheriting from HBox, so we name - it 'watch' instead (see https://bit.ly/3SggPVS). We need to be careful that - widgets don't get a method named 'watch' in the future. + it '_plopp_observe' instead (see https://bit.ly/3SggPVS). """ for dim in self.controls: self.controls[dim]['slider'].observe(callback, **kwargs) From 99fcd006b87b880aa75899f2bc12804aa962ef79 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Mon, 19 Sep 2022 15:31:28 +0200 Subject: [PATCH 08/10] add extra underscore at the end of _plopp_observe --- src/plopp/model.py | 4 ++-- src/plopp/widgets/checkboxes.py | 6 +----- src/plopp/widgets/slice.py | 6 +----- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/plopp/model.py b/src/plopp/model.py index c585c101..a7aeff29 100644 --- a/src/plopp/model.py +++ b/src/plopp/model.py @@ -59,7 +59,7 @@ def widget_node(widget): # TODO: Our custom widgets have a '_plopp_observe' method instead of 'observe' # because inheriting from VBox causes errors when overriding the 'observe' method # (see https://bit.ly/3SggPVS). - func = widget._plopp_observe if hasattr(widget, - '_plopp_observe') else widget.observe + func = widget._plopp_observe_ if hasattr(widget, + '_plopp_observe_') else widget.observe func(n.notify_children, names="value") return n diff --git a/src/plopp/widgets/checkboxes.py b/src/plopp/widgets/checkboxes.py index 3658ca3b..d9cd5b8a 100644 --- a/src/plopp/widgets/checkboxes.py +++ b/src/plopp/widgets/checkboxes.py @@ -41,11 +41,7 @@ def __init__(self, entries: list, description=""): super().__init__(to_hbox) - def _plopp_observe(self, callback: Callable, **kwargs): - """ - TODO: Cannot name this method 'observe' when inheriting from HBox, so we name - it '_plopp_observe' instead (see https://bit.ly/3SggPVS). - """ + def _plopp_observe_(self, callback: Callable, **kwargs): for chbx in self.checkboxes.values(): chbx.observe(callback, **kwargs) diff --git a/src/plopp/widgets/slice.py b/src/plopp/widgets/slice.py index 0151b63d..f9a5119b 100644 --- a/src/plopp/widgets/slice.py +++ b/src/plopp/widgets/slice.py @@ -62,11 +62,7 @@ def _update_label(self, change): coord = self.controls[dim]['coord'][dim, change['new']] self.controls[dim]['label'].value = _coord_to_string(coord) - def _plopp_observe(self, callback: Callable, **kwargs): - """ - TODO: Cannot name this method 'observe' when inheriting from HBox, so we name - it '_plopp_observe' instead (see https://bit.ly/3SggPVS). - """ + def _plopp_observe_(self, callback: Callable, **kwargs): for dim in self.controls: self.controls[dim]['slider'].observe(callback, **kwargs) From c99ef5bf25f0b10a731f2eee74b5c40ae46dcffa Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Mon, 19 Sep 2022 15:33:43 +0200 Subject: [PATCH 09/10] fix python tests --- tests/graph_nodes_test.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/graph_nodes_test.py b/tests/graph_nodes_test.py index 6719ac0f..24db98d7 100644 --- a/tests/graph_nodes_test.py +++ b/tests/graph_nodes_test.py @@ -117,7 +117,6 @@ def test_slice_3d_cube(): w = widget_node(sl) slice_node = slice_dims(a, w) - sl.make_view(slice_node) fig = figure(slice_node) Plot([fig.to_widget(), sl]) @@ -131,7 +130,6 @@ def test_3d_image_slicer_with_connected_side_histograms(): w = widget_node(sl) sliced = slice_dims(a, w) - sl.make_view(sliced) fig = figure(sliced) histx = node(sc.sum, dim='xx')(sliced) From 9a161abad71d674ea8823b93f8f43abf49e6c952 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Mon, 19 Sep 2022 15:58:45 +0200 Subject: [PATCH 10/10] remove hard dependency on ipywidgets --- src/plopp/__init__.py | 2 -- src/plopp/plot.py | 14 -------------- src/plopp/widgets/__init__.py | 1 + src/plopp/widgets/box.py | 20 ++++++++++++++++++++ src/plopp/wrappers.py | 9 ++++----- tests/graph_nodes_test.py | 16 ++++++++-------- 6 files changed, 33 insertions(+), 29 deletions(-) delete mode 100644 src/plopp/plot.py create mode 100644 src/plopp/widgets/box.py diff --git a/src/plopp/__init__.py b/src/plopp/__init__.py index 131f1841..c7a3bd27 100644 --- a/src/plopp/__init__.py +++ b/src/plopp/__init__.py @@ -14,12 +14,10 @@ plt.ioff() from .graph import show_graph -from .plot import Plot from .model import Node, node, input_node, widget_node from .wrappers import plot, figure, slicer from . import data -from . import widgets def patch_scipp(): diff --git a/src/plopp/plot.py b/src/plopp/plot.py deleted file mode 100644 index d4b5bb9a..00000000 --- a/src/plopp/plot.py +++ /dev/null @@ -1,14 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright (c) 2022 Scipp contributors (https://github.com/scipp) - -from ipywidgets import VBox, HBox - - -class Plot(VBox): - - def __init__(self, views): - self.views = views - children = [] - for view in self.views: - children.append(HBox(view) if isinstance(view, (list, tuple)) else view) - return super().__init__(children) diff --git a/src/plopp/widgets/__init__.py b/src/plopp/widgets/__init__.py index 1b8c87b4..c616efd2 100644 --- a/src/plopp/widgets/__init__.py +++ b/src/plopp/widgets/__init__.py @@ -4,5 +4,6 @@ # flake8: noqa: F401 # from .masks import MaskWidget, hide_masks +from .box import Box from .checkboxes import Checkboxes from .slice import SliceWidget, slice_dims diff --git a/src/plopp/widgets/box.py b/src/plopp/widgets/box.py new file mode 100644 index 00000000..edc4e071 --- /dev/null +++ b/src/plopp/widgets/box.py @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2022 Scipp contributors (https://github.com/scipp) + +from ipywidgets import VBox, HBox + + +class Box(VBox): + """ + Container widget that accepts a list of items. For each item in the list, if the + item is itself a list, it will be bade into a horizontal row of the underlying + items, if not, the item will span then entire row. + Finally, all the rows will be placed inside a vertical box container. + """ + + def __init__(self, widgets): + self.widgets = widgets + children = [] + for view in self.widgets: + children.append(HBox(view) if isinstance(view, (list, tuple)) else view) + return super().__init__(children) diff --git a/src/plopp/wrappers.py b/src/plopp/wrappers.py index 6bee7427..488beb7f 100644 --- a/src/plopp/wrappers.py +++ b/src/plopp/wrappers.py @@ -3,7 +3,6 @@ from .figure import Figure from .model import input_node, widget_node -from .plot import Plot from .prep import preprocess from scipp import Variable, Dataset @@ -120,7 +119,7 @@ def slicer(obj: Union[VariableLike, ndarray], keep: List[str] = None, *, crop: Dict[str, Dict[str, Variable]] = None, - **kwargs) -> Plot: + **kwargs): """ Plot a multi-dimensional object by slicing one or more of the dimensions. This will produce one slider per sliced dimension, below the figure. @@ -145,13 +144,13 @@ def slicer(obj: Union[VariableLike, ndarray], Returns ------- : - A :class:`Plot` which will contain a :class:`Figure` and slider widgets. + A :class:`Box` which will contain a :class:`Figure` and slider widgets. """ if not _is_interactive_backend(): raise RuntimeError("The slicer can only be used with the interactive widget " "backend. Use `%matplotlib widget` at the start of your " "notebook.") - from plopp.widgets import SliceWidget, slice_dims + from plopp.widgets import SliceWidget, slice_dims, Box da = preprocess(obj, crop=crop, ignore_size=True) a = input_node(da) @@ -161,4 +160,4 @@ def slicer(obj: Union[VariableLike, ndarray], w = widget_node(sl) slice_node = slice_dims(a, w) fig = figure(slice_node, **{**{'crop': crop}, **kwargs}) - return Plot([fig, sl]) + return Box([fig, sl]) diff --git a/tests/graph_nodes_test.py b/tests/graph_nodes_test.py index 24db98d7..59fd047e 100644 --- a/tests/graph_nodes_test.py +++ b/tests/graph_nodes_test.py @@ -2,8 +2,8 @@ # Copyright (c) 2022 Scipp contributors (https://github.com/scipp) import scipp as sc -from plopp import Plot, figure, node, input_node, widget_node -from plopp.widgets import Checkboxes, SliceWidget, slice_dims +from plopp import figure, node, input_node, widget_node +from plopp.widgets import Checkboxes, SliceWidget, slice_dims, Box from plopp.data import dense_data_array, dense_dataset import ipywidgets as ipw @@ -60,7 +60,7 @@ def test_2d_image_smoothing_slider(): smooth_node = node(gaussian_filter)(a, sigma=sigma_node) fig = figure(smooth_node) - Plot([fig.to_widget(), sl]) + Box([fig.to_widget(), sl]) sl.value = 5 @@ -76,7 +76,7 @@ def test_2d_image_with_masks(): masks_node = hide_masks(a, w) fig = figure(masks_node) - Plot([fig.to_widget(), widget]) + Box([fig.to_widget(), widget]) widget.toggle_all_button.value = False @@ -95,7 +95,7 @@ def test_two_1d_lines_with_masks(): node_masks_a = hide_masks(a, w) node_masks_b = hide_masks(b, w) fig = figure(node_masks_a, node_masks_b) - Plot([fig.to_widget(), widget]) + Box([fig.to_widget(), widget]) widget.toggle_all_button.value = False @@ -107,7 +107,7 @@ def test_node_sum_data_along_y(): fig1 = figure(a) fig2 = figure(s) - Plot([[fig1.to_widget(), fig2.to_widget()]]) + Box([[fig1.to_widget(), fig2.to_widget()]]) def test_slice_3d_cube(): @@ -119,7 +119,7 @@ def test_slice_3d_cube(): slice_node = slice_dims(a, w) fig = figure(slice_node) - Plot([fig.to_widget(), sl]) + Box([fig.to_widget(), sl]) sl.controls["zz"]["slider"].value = 10 @@ -137,5 +137,5 @@ def test_3d_image_slicer_with_connected_side_histograms(): fx = figure(histx) fy = figure(histy) - Plot([[fx.to_widget(), fy.to_widget()], fig.to_widget(), sl]) + Box([[fx.to_widget(), fy.to_widget()], fig.to_widget(), sl]) sl.controls["zz"]["slider"].value = 10