diff --git a/sans/CreateConvertPlotExample.ipynb b/sans/CreateConvertPlotExample.ipynb
new file mode 100644
index 0000000..6e245eb
--- /dev/null
+++ b/sans/CreateConvertPlotExample.ipynb
@@ -0,0 +1,165 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Data manpulation widgets toy examples"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "scrolled": true
+ },
+ "outputs": [],
+ "source": [
+ "from example_widgets import ProcessWidget, PlotWidget, fake_load, setup_code_hiding, allowed_dimensions, filepath_converter\n",
+ "import scipp as sc\n",
+ "import functools\n",
+ "\n",
+ "# Workaround for the fact that you can't raise directly in tertiary expressions.\n",
+ "def raise_(ex):\n",
+ " raise ex\n",
+ "\n",
+ "setup_code_hiding()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "plot_widget = PlotWidget(globals())"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Data Loading\n",
+ "Currently creates some example data rather than loading a file. The created file is a histogrammed time of flight data."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "data_creation = ProcessWidget(globals(), fake_load, 'Load', {'filepath': filepath_converter}, {'filepath': 'run number or file name'})\n",
+ "data_creation.subscribe(plot_widget)\n",
+ "data_creation"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Data conversion\n",
+ "This converts one physical dimension to another the current conversions support in scipp.neutron are:\n",
+ "* tof -> dspacing, wavelength, E\n",
+ "* d-spacing -> tof\n",
+ "* wavelength -> Q, tof\n",
+ "* E -> tof\n",
+ "* Q -> wavelength"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "scrolled": true
+ },
+ "outputs": [],
+ "source": [
+ "dim_converter = lambda dim : dim if dim in allowed_dimensions else raise_(ValueError(\n",
+ " f'{dim} not an allowed dimension. Supported dimensions are {list(dimesion_to_unit.keys())}'))\n",
+ "data_converter = lambda name : globals()[name] if (name in globals()) else raise_(ValueError(\n",
+ " f'{name} does not exist in notebook.'))\n",
+ "data_options = lambda : [\n",
+ " key for key, item in globals().items()\n",
+ " if isinstance(item, (sc.DataArray, sc.Dataset, sc.Variable))\n",
+ " ]\n",
+ "\n",
+ "data_conversion = ProcessWidget(globals(), \n",
+ " sc.neutron.convert, 'Convert', \n",
+ " {'data': data_converter,\n",
+ " 'from' : dim_converter, 'to': dim_converter},\n",
+ " options={'from' : allowed_dimensions,\n",
+ " 'to': allowed_dimensions,\n",
+ " 'data': data_options})\n",
+ "data_conversion.subscribe(plot_widget)\n",
+ "data_conversion"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Can also specify just some inputs of a function graphically."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Workaround because python doesn't like shadowing of from\n",
+ "kwargs = {'from':'tof'}\n",
+ "convert_tof = functools.partial(sc.neutron.convert, **kwargs)\n",
+ "data_conversion_tof = ProcessWidget(globals(), \n",
+ " convert_tof, 'Convert from tof', \n",
+ " {'data': data_converter,\n",
+ " 'to': dim_converter}, \n",
+ " options={'to': allowed_dimensions,\n",
+ " 'data': data_options})\n",
+ "data_conversion_tof.subscribe(plot_widget)\n",
+ "data_conversion_tof"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Data plotting\n",
+ "Plots the data aquired by evaluating the expression entered."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "scrolled": false
+ },
+ "outputs": [],
+ "source": [
+ "plot_widget"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "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.7.8"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/sans/example_widgets.py b/sans/example_widgets.py
new file mode 100644
index 0000000..698a9cd
--- /dev/null
+++ b/sans/example_widgets.py
@@ -0,0 +1,236 @@
+import ipywidgets as w
+import scipp as sc
+from scipp.plot import plot
+import numpy as np
+import IPython.display as disp
+import os
+
+allowed_dimensions = ['tof', 'd-spacing', 'wavelength', 'E', 'Q']
+
+
+def filepath_converter(filename):
+ if filename.isdigit():
+ # specified by run number
+ filename = 'LARMOR' + filename
+
+ # We will probably want to be a bit cleverer in how we hande file
+ # finding and directory specifying. In particular browsing to files
+ # or directories may be a requirment.
+ directory = '/path/to/data/directory'
+ filepath = os.path.join(directory, filename)
+ # Commenting out as would always throw when loading fake files.
+ # if not os.path.exists(filepath):
+ # raise ValueError(f'File {}')
+ return filepath
+
+
+class ProcessWidget(w.Box):
+ def __init__(self,
+ scope,
+ callable,
+ name,
+ inputs,
+ descriptions={},
+ options={}):
+ super().__init__()
+ self.scope = scope
+ self.callable = callable
+
+ self.input_widgets = []
+ self.inputs = inputs
+ self.setup_input_widgets(descriptions, options)
+
+ self.output = w.Text(placeholder='output name',
+ value='',
+ continuous_update=False)
+
+ self.button = w.Button(description=name)
+ self.button.on_click(self._on_button_click)
+
+ self.children = [
+ w.HBox(self.input_widgets + [self.output, self.button])
+ ]
+
+ self.subscribers = []
+
+ def setup_input_widgets(self, descriptions, options):
+ for name in self.inputs.keys():
+ placeholder = descriptions[name] if name in descriptions else name
+ option = options[name] if name in options else []
+ option = option() if callable(option) else option
+ self.input_widgets.append(
+ w.Combobox(placeholder=placeholder,
+ continuous_update=False,
+ options=option))
+
+ def subscribe(self, observer):
+ self.subscribers.append(observer)
+
+ def notify(self):
+ for observer in self.subscribers:
+ observer.update()
+
+ def _on_button_click(self, b):
+ self.process()
+ self.notify()
+
+ def _retrive_kwargs(self):
+ kwargs = {
+ name: converter(item.value)
+ for name, converter, item in zip(
+ self.inputs.keys(), self.inputs.values(), self.input_widgets)
+ }
+ return kwargs
+
+ def process(self):
+ try:
+ kwargs = self._retrive_kwargs()
+ except ValueError as e:
+ print(f'Invalid inputs: {e}')
+ return
+
+ if self.output.value:
+ output_name = self.output.value
+ else:
+ print(f'Invalid inputs: No output name specified')
+ return
+
+ self.scope[output_name] = self.callable(**kwargs)
+
+
+class PlotWidget(w.Box):
+ def __init__(self, scope):
+ super().__init__()
+ self.scope = scope
+ options = [
+ key for key, item in globals().items()
+ if isinstance(item, (sc.DataArray, sc.Dataset))
+ ]
+ self._data_selector = w.Combobox(placeholder='Data to plot',
+ options=options)
+ self._button = w.Button(description='Plot')
+ self._button.on_click(self._on_button_clicked)
+ self.plot_options = w.Output()
+ self.update_button = w.Button(description='Manual Update')
+ self.update_button.on_click(self.update)
+ self.output = w.Output(width='100%', height='100%')
+ self.children = [
+ w.VBox([
+ w.HBox([self.plot_options, self.update_button]),
+ w.HBox([self._data_selector, self._button]), self.output
+ ])
+ ]
+ self.update()
+
+ def _repr_html_(self, input_scope=None):
+ import inspect
+ # Is there a better way to get the scope? The `7` is hard-coded for the
+ # current IPython stack when calling _repr_html_ so this is bound to break.
+ scope = input_scope if input_scope else inspect.stack()[7][0].f_globals
+ from IPython import get_ipython
+ ipython = get_ipython()
+ out = ''
+ for category in ['Variable', 'DataArray', 'Dataset']:
+ names = ipython.magic(f"who_ls {category}")
+ out += f"{category}s:"\
+ f"({len(names)})
"
+ for name in names:
+ html = sc.table_html.make_html(eval(name, scope))
+ out += f""\
+ f"{name}
{html} "
+ out += " "
+ from IPython.core.display import display, HTML
+ display(HTML(out))
+
+ def _on_button_clicked(self, b):
+ self.output.clear_output()
+ with self.output:
+ disp.display(plot(eval(self._data_selector.value, self.scope)))
+
+ def update(self, b=None):
+ options = [
+ key for key, item in self.scope.items()
+ if isinstance(item, (sc.DataArray, sc.Dataset, sc.Variable))
+ ]
+ self._data_selector.options = options
+ self.plot_options.clear_output()
+ with self.plot_options:
+ self._repr_html_(self.scope)
+
+
+def fake_load(filepath):
+ dim = 'tof'
+ num_spectra = 10
+ return sc.Dataset(
+ {
+ 'sample':
+ sc.Variable(['spectrum', dim],
+ values=np.random.rand(num_spectra, 10),
+ variances=0.1 * np.random.rand(num_spectra, 10)),
+ 'background':
+ sc.Variable(['spectrum', dim],
+ values=np.arange(0.0, num_spectra, 0.1).reshape(
+ num_spectra, 10),
+ variances=0.1 * np.random.rand(num_spectra, 10))
+ },
+ coords={
+ dim:
+ sc.Variable([dim], values=np.arange(11.0), unit=sc.units.us),
+ 'spectrum':
+ sc.Variable(['spectrum'],
+ values=np.arange(num_spectra),
+ unit=sc.units.one),
+ 'source-position':
+ sc.Variable(value=np.array([1., 1., 10.]),
+ dtype=sc.dtype.vector_3_float64,
+ unit=sc.units.m),
+ 'sample-position':
+ sc.Variable(value=np.array([1., 1., 60.]),
+ dtype=sc.dtype.vector_3_float64,
+ unit=sc.units.m),
+ 'position':
+ sc.Variable(['spectrum'],
+ values=np.arange(3 * num_spectra).reshape(
+ num_spectra, 3),
+ unit=sc.units.m,
+ dtype=sc.dtype.vector_3_float64)
+ })
+
+
+#Method to hide code blocks taken from
+#https://stackoverflow.com/questions/27934885/how-to-hide-code-from-cells-in-ipython-notebook-visualized-with-nbviewer
+javascript_functions = {False: "hide()", True: "show()"}
+button_descriptions = {False: "Show code", True: "Hide code"}
+
+
+def toggle_code(state):
+ """
+ Toggles the JavaScript show()/hide() function on the div.input element.
+ """
+
+ output_string = ""
+ output_args = (javascript_functions[state], )
+ output = output_string.format(*output_args)
+
+ disp.display(disp.HTML(output))
+
+
+def button_action(value):
+ """
+ Calls the toggle_code function and updates the button description.
+ """
+
+ state = value.new
+
+ toggle_code(state)
+
+ value.owner.description = button_descriptions[state]
+
+
+def setup_code_hiding():
+ state = False
+ toggle_code(state)
+
+ button = w.ToggleButton(state, description=button_descriptions[state])
+ button.observe(button_action, "value")
+ return button