from __future__ import annotations
import ipywidgets
import IPython.display
import threading
import time
import datetime
import bqplot
import numpy as np
from abc import abstractmethod
from typing import Dict, Tuple, Callable, List, Union
from cohesivm.experiment import ExperimentState, Experiment
from cohesivm.analysis import Analysis
from cohesivm.data_stream import DataStream
from cohesivm.plots import Plot
from cohesivm.database import Database
[docs]
class DataStreamPlot(DataStream, Plot):
"""A :class:`multiprocessing.Queue` object which is used to stream data from a
:class:`~cohesivm.measurements.Measurement` to a :class:`bqplot.Figure` object where the streamed data
is put into. A child class implements the methods for updating the data and the figure. Its intended use is within
the :class:`~cohesivm.gui.ExperimentGUI`."""
def __init__(self) -> None:
DataStream.__init__(self)
Plot.__init__(self)
[docs]
@abstractmethod
def update_plot(self) -> None:
"""Fetches the data from the :attr:`~cohesivm.data_stream.DataStream.data_stream` and puts it in the figure."""
pass
[docs]
class XYDataStreamPlot(DataStreamPlot):
"""Generates and updates a two-dimensional x-y-plot with the data which is put in the ``data_stream`` queue.
:param x_label: Label of the x-axis.
:param y_label: Label of the y-axis.
:param figsize: Size of the figure in inch (see :class:`matplotlib.figure.Figure`).
"""
_data_types = (np.floating, np.floating)
def __init__(self, x_label: str, y_label: str, figsize: Tuple[float, float] = (7, 5.5)) -> None:
DataStreamPlot.__init__(self)
self.x_label = x_label
self.y_label = y_label
self.figsize = figsize
self._line = None
self._x_sc = None
self._y_sc = None
@property
def figure(self) -> Union[bqplot.Figure, None]:
"""The :class:`bqplot.Figure` object which is populated with the data."""
return self._figure
[docs]
def make_plot(self) -> None:
self._x_sc, self._y_sc = bqplot.LinearScale(min=-1, max=1), bqplot.LinearScale(min=-1, max=1)
x_ax = bqplot.Axis(scale=self._x_sc, label=self.x_label, tick_format='.1f', grid_lines='solid',
label_offset='30')
y_ax = bqplot.Axis(scale=self._y_sc, label=self.y_label, tick_format='.1e', grid_lines='solid',
label_offset='-50', orientation='vertical')
x_ax.num_ticks = 7
self._line = bqplot.Lines(x=[], y=[], scales={'x': self._x_sc, 'y': self._y_sc})
self._figure = bqplot.Figure(marks=[self._line], axes=[x_ax, y_ax],
figsize=self.figsize, padding_x=0, padding_y=0,
fig_margin={'top': 10, 'bottom': 45, 'left': 65, 'right': 30})
[docs]
def update_plot(self) -> None:
while not self.data_stream.empty():
data = self.data_stream.get()
if len(data) == 2:
self._line.x = list(self._line.x) + [data[0]]
self._line.y = list(self._line.y) + [data[1]]
if len(self._line.x) > 1:
self._x_sc.min = None
self._x_sc.max = None
self._y_sc.min = None
self._y_sc.max = None
[docs]
def clear_plot(self) -> None:
self.update_plot()
self._x_sc.min = -1
self._x_sc.max = 1
self._y_sc.min = -1
self._y_sc.max = 1
self._line.x = []
self._line.y = []
class _InterfacePlotGUI:
def __init__(self):
self.plot = None
self.interface_frame = None
self.plot_frame = None
def _make_interface_frame(self, contact_positions: Dict[str, Tuple[float, float]], height: int
) -> ipywidgets.Output:
interface_frame = ipywidgets.Output(layout=ipywidgets.Layout(height=f'{height}px'))
interface_frame.add_class('interface-frame')
x_coords = []
y_coords = []
for coords in contact_positions.values():
x_coords.append(coords[0])
y_coords.append(coords[1])
x_lim, y_lim = [(min(coords), max(coords)) for coords in [x_coords, y_coords]]
x_len, y_len = [lim[1] - lim[0] for lim in [x_lim, y_lim]]
if x_len < y_len:
x_center = x_lim[0] + x_len/2
x_lim = (x_center - y_len/2, x_center + y_len/2)
elif x_len > y_len:
y_center = y_lim[0] + y_len/2
y_lim = (y_center - x_len/2, y_center + x_len/2)
x_scale = bqplot.LinearScale(min=x_lim[0]-x_len*.1, max=x_lim[1]+x_len*.1)
y_scale = bqplot.LinearScale(min=y_lim[0]-y_len*.1, max=y_lim[1]+y_len*.1)
self.interface_scatter = bqplot.Scatter(x=x_coords, y=y_coords, names=list(contact_positions.keys()),
colors=[state_colors['INITIAL']], default_size=100,
scales={'x': x_scale, 'y': y_scale})
ax_x = bqplot.Axis(scale=x_scale, grid_lines='none', visible=False)
ax_y = bqplot.Axis(scale=y_scale, grid_lines='none', visible=False)
fm = 0
self.interface_fig = bqplot.Figure(marks=[self.interface_scatter], axes=[ax_x, ax_y],
figsize=(5, 5), padding_x=0, padding_y=0,
fig_margin={'top': fm, 'bottom': fm, 'left': fm, 'right': fm})
return interface_frame
def _update_interface_frame(self, *args, **kwargs) -> None:
pass
def _display_interface_frame(self) -> None:
self._update_interface_frame()
self.interface_frame.clear_output(wait=True)
with self.interface_frame:
IPython.display.display(self.interface_fig)
def _make_plot_frame(self, height: int, make_plot: bool = True) -> ipywidgets.Output:
plot_frame = ipywidgets.Output(layout=ipywidgets.Layout(height=f'{height}px'))
plot_frame.add_class('plot-frame')
if make_plot:
self.plot.make_plot()
return plot_frame
def _update_plot_frame(self, *args, **kwargs) -> None:
pass
def _display_plot_frame(self) -> None:
self._update_plot_frame()
self.plot_frame.clear_output(wait=True)
with self.plot_frame:
IPython.display.display(self.plot.figure)
[docs]
class ExperimentGUI(_InterfacePlotGUI):
"""
A graphical user interface for monitoring and controlling an experiment within a Jupyter Notebook.
:param experiment: The :class:`~cohesivm.experiment.Experiment` which should be monitored.
:param plot: A :class:`~cohesivm.plots.DataStreamPlot` object which is compatible with the ``experiment``.
"""
instance = None
""":meta private:"""
def __new__(cls, *args, **kwargs) -> ExperimentGUI:
if cls.instance is None:
cls.instance = super(ExperimentGUI, cls).__new__(cls)
return cls.instance
def __init__(self, experiment: Experiment, plot: DataStreamPlot) -> None:
_InterfacePlotGUI.__init__(self)
plot.check_compatibility(experiment.measurement.output_type)
experiment.data_stream = plot.data_stream
self.experiment = experiment
self.current_contact = None
self.preview_contact = None
self.plot = plot
self.statusbar = self._make_statusbar()
self.buttons = self._make_buttons()
self.interface_frame = self._make_interface_frame(experiment.interface.contact_positions, 400)
self.interface_scatter.on_element_click(self._preview_click)
self.plot_frame = self._make_plot_frame(438)
self.widget = self._make_widget()
self.update_worker = None
self.stop_update = True
@staticmethod
def _make_statusbar() -> tuple[ipywidgets.HTML, ipywidgets.HTML]:
statusbar = (ipywidgets.HTML(), ipywidgets.HTML())
statusbar[0].style = {
'font_size': '18px',
}
statusbar[1].style = {
'font_size': '18px'
}
return statusbar
def _update_statusbar(self) -> None:
self.statusbar[0].value = \
f'State: <span style="font-weight: bold; color: {state_colors[self.experiment.state.name]}">' \
f'{self.experiment.state.name}</span>'
contact_status = ''
if self.experiment.state in [ExperimentState.RUNNING, ExperimentState.ABORTED]:
if self.current_contact is None:
contact_status = f'[PREVIEW: {self.preview_contact}]'
else:
contact_status = f'[{self.current_contact}]'
self.statusbar[1].value = f'{self.experiment.measurement.name}: {self.experiment.sample_id} {contact_status}'
def _make_buttons(self) -> List[ipywidgets.Button]:
setup_button = ipywidgets.Button(description='Setup', disabled=True, icon='cogs')
setup_button.add_class('setup-button')
start_button = ipywidgets.Button(description='Start', disabled=True, icon='play')
start_button.add_class('start-button')
abort_button = ipywidgets.Button(description='Abort', disabled=True, icon='stop')
abort_button.add_class('abort-button')
setup_button.on_click(self._setup_button_click)
start_button.on_click(self._start_button_click)
abort_button.on_click(self._abort_button_click)
return [setup_button, start_button, abort_button]
def _setup_button_click(self, button) -> None:
self.experiment.setup()
self.preview_contact = None
def _start_button_click(self, button) -> None:
self.experiment.start()
self.preview_contact = None
def _abort_button_click(self, button) -> None:
self.experiment.abort()
def _update_buttons(self) -> None:
if self.experiment.state in [ExperimentState.INITIAL, ExperimentState.FINISHED, ExperimentState.ABORTED]:
self.buttons[0].disabled = False
else:
self.buttons[0].disabled = True
if self.experiment.state == ExperimentState.READY:
self.buttons[1].disabled = False
else:
self.buttons[1].disabled = True
if self.experiment.state in [ExperimentState.READY, ExperimentState.RUNNING]:
self.buttons[2].disabled = False
else:
self.buttons[2].disabled = True
def _preview_click(self, event, data) -> None:
if self.experiment.state == ExperimentState.RUNNING:
return
contact_id = data['data']['name']
self.preview_contact = contact_id
self.plot.clear_plot()
self.experiment.preview(contact_id)
def _update_interface_frame(self) -> None:
colors = []
for contact_id in self.experiment.interface.contact_ids:
if contact_id == self.preview_contact and self.experiment.state == ExperimentState.RUNNING:
colors.append(state_colors['RUNNING'])
elif contact_id == self.preview_contact and self.experiment.state == ExperimentState.ABORTED:
colors.append(state_colors['ABORTED'])
elif contact_id not in self.experiment.selected_contacts:
colors.append('black')
elif self.experiment.current_contact_idx == -2:
colors.append(state_colors['INITIAL'])
elif self.experiment.current_contact_idx < self.experiment.selected_contacts.index(contact_id):
colors.append(state_colors['READY'])
elif self.experiment.current_contact_idx > self.experiment.selected_contacts.index(contact_id):
colors.append(state_colors['FINISHED'])
elif contact_id == self.experiment.selected_contacts[self.experiment.current_contact_idx]:
if self.experiment.state == ExperimentState.RUNNING:
colors.append(state_colors['RUNNING'])
if self.experiment.state == ExperimentState.ABORTED:
colors.append(state_colors['ABORTED'])
self.interface_scatter.colors = colors
if self.experiment.state == ExperimentState.RUNNING:
self.interface_frame.remove_class('interactive-interface')
else:
self.interface_frame.add_class('interactive-interface')
def _update_plot_frame(self) -> None:
if self.experiment.state is ExperimentState.ABORTED:
return
if self.experiment.state is not ExperimentState.RUNNING:
self.plot.clear_plot()
return
if self.experiment.current_contact_idx < 0:
pass
elif self.current_contact != self.experiment.selected_contacts[self.experiment.current_contact_idx]:
self.current_contact = self.experiment.selected_contacts[self.experiment.current_contact_idx]
self.plot.clear_plot()
self.plot.update_plot()
def _make_widget(self) -> ipywidgets.HBox:
frame_layout = ipywidgets.Layout(border='2px #dddddd solid', border_top='none',
justify_content='center', margin='2px')
left_body = ipywidgets.VBox([
ipywidgets.HBox([self.statusbar[0]],
layout=ipywidgets.Layout(justify_content='center')),
self.interface_frame,
ipywidgets.HBox(self.buttons,
layout=ipywidgets.Layout(justify_content='center'))
], layout=frame_layout)
right_body = ipywidgets.VBox([
ipywidgets.HBox([self.statusbar[1]],
layout=ipywidgets.Layout(justify_content='center')),
self.plot_frame
], layout=frame_layout)
left_column = ipywidgets.VBox([
ipywidgets.HTML('Control').add_class('column-heading'),
left_body
], layout=ipywidgets.Layout(width='40%'))
right_column = ipywidgets.VBox([
ipywidgets.HTML('Plot').add_class('column-heading'),
right_body
], layout=ipywidgets.Layout(width='60%'))
return ipywidgets.HBox([
ipywidgets.HTML(f"<style>{style}</style>",
layout=ipywidgets.Layout(display='none')),
left_column,
right_column
])
def _update_widget(self) -> None:
self.stop_update = False
while not self.stop_update:
self._update_statusbar()
self._update_buttons()
self._update_interface_frame()
self._update_plot_frame()
[docs]
def display(self) -> None:
"""Displays the ExperimentGUI widget and starts the update loop in a separate thread."""
self.stop_update = True
time.sleep(1)
IPython.display.display(self.widget, clear=True)
self._display_interface_frame()
self._display_plot_frame()
self.update_worker = threading.Thread(target=self._update_widget)
self.update_worker.start()
[docs]
class AnalysisGUI(_InterfacePlotGUI):
"""
A graphical user interface for plotting measurement data and displaying analysis results within a Jupyter Notebook.
:param analysis: An :class:`~cohesivm.analysis.Analysis` object which should be displayed.
"""
def __init__(self, analysis: Analysis) -> None:
_InterfacePlotGUI.__init__(self)
self.analysis = analysis
self.plot = None
self.current_contact = None
self.current_plot = 0
self.available_plots = list(self.analysis.plots.keys())
self.interface_frame = self._make_interface_frame(analysis.contact_position_dict, 420)
self.interface_frame.add_class('interactive-interface')
self.interface_scatter.on_element_click(self._select_contact)
self.plot_heading = ipywidgets.HTML('Plot')
self.switch_plot_buttons = self._make_switch_plot_buttons()
self.plot_frame = self._make_plot_frame(420, False)
self.results_frame = ipywidgets.HTML()
self.widget = self._make_widget()
def _select_contact(self, event, data) -> None:
contact_id = data['data']['name']
self.current_contact = contact_id
self._update_plot_frame()
self._update_results_frame()
self._update_interface_frame()
def _update_interface_frame(self) -> None:
colors = []
for contact_id in self.analysis.contact_position_dict.keys():
if contact_id not in self.analysis.data.keys():
colors.append('black')
elif contact_id == self.current_contact:
colors.append(state_colors['FINISHED'])
else:
colors.append(state_colors['INITIAL'])
self.interface_scatter.colors = colors
def _make_switch_plot_buttons(self) -> ipywidgets.HBox:
left_button = ipywidgets.Button(icon='caret-left', disabled=True)
left_button.value = 'left'
left_button.on_click(self._switch_plot)
right_button = ipywidgets.Button(icon='caret-right', disabled=True)
right_button.value = 'right'
right_button.on_click(self._switch_plot)
return ipywidgets.HBox([left_button, right_button])
def _switch_plot(self, button) -> None:
if button.value == 'left':
self.current_plot -= 1
else:
self.current_plot += 1
self._update_switch_plot_buttons()
self._update_plot_frame()
def _update_switch_plot_buttons(self) -> None:
left_button, right_button = self.switch_plot_buttons.children
if self.current_plot == 0:
left_button.disabled = True
elif self.current_plot > 0:
left_button.disabled = False
if self.current_plot < len(self.available_plots) - 1:
right_button.disabled = False
else:
right_button.disabled = True
def _update_plot_frame(self) -> None:
self.plot_heading.value = f'Plot: {self.available_plots[self.current_plot]}'
if self.current_contact is None:
return
figure = self.analysis.plots[self.available_plots[self.current_plot]](self.current_contact)
self.plot_frame.clear_output(wait=True)
with self.plot_frame:
IPython.display.display(figure)
def _update_results_frame(self) -> None:
results = []
for func_name, func in self.analysis.functions.items():
try:
result = func(self.current_contact)
except Exception as exc:
result = type(exc).__name__
results.append(f'<tr><td style="font-weight:bold;">{func_name}</td><td>{result}</td></tr>')
self.results_frame.value = f'<table class="results-table">' \
f'<thead><tr><th>Function</th><th>Result</th></tr></thead>' \
f'<tbody>{"".join(results)}</tbody>' \
f'</table>'
def _make_widget(self) -> ipywidgets.VBox:
frame_layout = ipywidgets.Layout(border='2px #dddddd solid', border_top='none',
justify_content='center', margin='2px')
left_column = ipywidgets.VBox([
ipywidgets.HTML('Interface').add_class('column-heading'),
ipywidgets.VBox([self.interface_frame], layout=frame_layout)
], layout=ipywidgets.Layout(width='40%'))
right_column = ipywidgets.VBox([
ipywidgets.HBox([
self.plot_heading,
self.switch_plot_buttons
]).add_class('column-heading'),
ipywidgets.VBox([self.plot_frame], layout=frame_layout)
], layout=ipywidgets.Layout(width='60%'))
widget_top = ipywidgets.HBox([
ipywidgets.HTML(f"<style>{style}</style>",
layout=ipywidgets.Layout(display='none')),
left_column,
right_column
])
widget = ipywidgets.VBox([
widget_top,
self.results_frame
], layout=ipywidgets.Layout(width='100%'))
widget.add_class('analysis-gui')
return widget
[docs]
def display(self) -> None:
"""Displays the AnalysisGUI widget."""
IPython.display.display(self.widget, clear=True)
self._display_interface_frame()
self._update_switch_plot_buttons()
[docs]
class DatabaseGUI:
"""
A graphical user interface for displaying and filtering the contents of a database file within a Jupyter Notebook.
:param database: The :class:`~cohesivm.database.Database` object which should be displayed.
"""
def __init__(self, database: Database) -> None:
self.database = database
self.toggle = self._make_toggle()
self.results_count = ipywidgets.HTML()
self.results_count.add_class('results-count')
self.buttons_frame = ipywidgets.HBox()
self.buttons_frame.add_class('buttons-frame')
self.filters_frame = ipywidgets.VBox()
self.filters_frame.add_class('filters-frame')
self.filter_values = {}
self.filter_widgets = {}
self.results_frame = ipywidgets.HTML()
self.results_frame.add_class('results-frame')
self.widget = self._make_widget()
self.timezone = datetime.datetime.now(datetime.timezone.utc).astimezone().tzinfo
def _make_toggle(self) -> ipywidgets.ToggleButtons:
toggle = ipywidgets.ToggleButtons(value=None, options=['Measurements', 'Samples'], icons=['folder']*2)
toggle.add_class('data-toggle')
toggle.observe(self._toggle_click, 'value')
return toggle
def _toggle_click(self, change) -> None:
self._update_results_frame()
self.filters_frame.children = []
icons = ['folder'] * 2
if change['new'] == 'Measurements':
self._measurements_toggle()
icons[0] = 'folder-open'
else:
self._samples_toggle()
icons[1] = 'folder-open'
self.toggle.icons = icons
def _enable_buttons(self) -> None:
for button in self.buttons_frame.children:
button.disabled = False
def _measurements_toggle(self) -> None:
measurement_buttons = [ipywidgets.Button(description=measurement)
for measurement in self.database.get_measurements()]
for button in measurement_buttons:
button.add_class('measurement-button')
button.on_click(self._measurement_button_click)
self.buttons_frame.children = measurement_buttons
def _measurement_button_click(self, button) -> None:
self._enable_buttons()
button.disabled = True
measurement = button.description
self.filter_values = self.database.get_filters(measurement)
self.filter_widgets = {}
measurement_filters = []
checkbox_layout = ipywidgets.Layout(width='30px')
label_layout = ipywidgets.Layout(padding='8px 10px', min_width='200px')
row_layout = ipywidgets.Layout(padding='6px 0px', align_items='center')
for setting, values in self.filter_values.items():
if len(values) == 1:
widget = ipywidgets.HTML(f'{list(values)[0]}')
widget.disabled = True
elif all([np.issubdtype(type(value), bool)
for value in values]):
widget = ipywidgets.SelectMultiple(value=(True, False), options=[True, False], rows=2, disabled=True)
elif all([np.issubdtype(type(value), np.integer)
for value in values]):
widget = ipywidgets.IntRangeSlider(value=[min(values), max(values)], min=min(values), max=max(values),
continous_update=False, disabled=True)
elif all([np.issubdtype(type(value), np.integer) or np.issubdtype(type(value), np.floating)
for value in values]):
widget = ipywidgets.FloatRangeSlider(value=[min(values), max(values)], min=min(values), max=max(values),
continous_update=False, disabled=True)
else:
widget = ipywidgets.SelectMultiple(value=tuple(values), options=values, disabled=True)
self.filter_widgets[setting] = widget
widget.observe(self._change_filter(measurement), 'value')
checkbox = ipywidgets.Checkbox(value=False, layout=checkbox_layout)
checkbox.observe(self._toggle_filter(measurement, setting), 'value')
measurement_filters.append(ipywidgets.HBox([
checkbox,
ipywidgets.HTML(f'{setting}', layout=label_layout),
widget
], layout=row_layout))
self.filters_frame.children = measurement_filters
def _toggle_filter(self, measurement: str, setting: str) -> Callable:
def func(change):
self.filter_widgets[setting].disabled = not change['new']
self._apply_filters(measurement)
return func
def _change_filter(self, measurement: str) -> Callable:
def func(change):
self._apply_filters(measurement)
return func
def _apply_filters(self, measurement: str) -> None:
filters = {}
for setting, widget in self.filter_widgets.items():
if widget.disabled:
continue
if type(widget) == ipywidgets.HTML:
filters[setting] = list(self.filter_values[setting])
elif type(widget) == ipywidgets.SelectMultiple:
filters[setting] = list(widget.value)
else:
filters[setting] = [value for value in self.filter_values[setting]
if (value >= widget.value[0]) and (value <= widget.value[1])]
if len(filters) == 0:
self._update_results_frame()
return
results = self.database.filter_by_settings_batch(measurement, filters)
self._update_results_frame(results)
def _samples_toggle(self) -> None:
sample_buttons = [ipywidgets.Button(description=sample) for sample in self.database.get_sample_ids()]
for button in sample_buttons:
button.add_class('sample-button')
button.on_click(self._sample_button_click)
self.buttons_frame.children = sample_buttons
def _sample_button_click(self, button) -> None:
self._enable_buttons()
button.disabled = True
results = self.database.filter_by_sample_id(button.description)
self._update_results_frame(results)
def _update_results_frame(self, results=None) -> None:
if results is None:
self.results_frame.value = ''
self.results_count.value = ''
else:
result_rows = []
for result in results:
row = []
_, measurement, _, datetime_sample = result.split('/')
measurement_datetime = datetime_sample[:26]
row.append(datetime.datetime.fromisoformat(measurement_datetime + '+00:00').astimezone(self.timezone))
row.append(measurement)
row.append(', '.join([str(v) for v in self.database.get_measurement_settings(result).values()]))
row.append(datetime_sample[27:])
row.append(self.database.get_dataset_length(result))
row.append(f'<a href="#" onclick="'
f'const old_icon = document.querySelector(\'i.fa-check\'); '
f'if (old_icon != null) {{ '
f'old_icon.classList.toggle(\'fa-clone\'); old_icon.classList.toggle(\'fa-check\'); }}'
f'const new_icon = this.querySelector(\'i\'); '
f'new_icon.classList.toggle(\'fa-clone\'); new_icon.classList.toggle(\'fa-check\'); '
f'navigator.clipboard.writeText(\'{result}\')">'
f'<i class="fa fa-clone"></i> Copy'
f'</a>')
result_rows.append(row)
result_rows.sort(key=lambda x: x[0], reverse=True)
result_rows = [f'<tr>'
f'<td>{row[0].strftime("%d.%m.%y")}</td>'
f'<td>{row[0].strftime("%H:%M:%S")}</td>'
f'<td>{row[1]}</td>'
f'<td>{row[2]}</td>'
f'<td>{row[3]}</td>'
f'<td>{row[4]}</td>'
f'<td>{row[5]}</td>'
f'</tr>' for row in result_rows]
results_table = f'<table class="results-table">' \
f'<thead><tr>' \
f'<th>Date</th>' \
f'<th>Time</th>' \
f'<th>Measurement</th>' \
f'<th>Settings</th>' \
f'<th>Sample ID</th>' \
f'<th>Entries</th>' \
f'<th>Path</th>' \
f'</tr></thead>' \
f'<tbody>{"".join(result_rows)}</tbody>' \
f'</table>'
self.results_frame.value = results_table
self.results_count.value = f'{len(result_rows)} results'
def _make_widget(self) -> ipywidgets.VBox:
widget = ipywidgets.VBox([
ipywidgets.HTML(f'<style>{style}</style>', layout=ipywidgets.Layout(display='none')),
ipywidgets.HBox([self.toggle, self.results_count],
layout=ipywidgets.Layout(justify_content='space-between', align_items='center')),
self.buttons_frame,
self.filters_frame,
self.results_frame
])
widget.add_class('database-gui')
return widget
[docs]
def display(self) -> None:
"""Displays the DatabaseGUI widget."""
IPython.display.display(self.widget, clear=True)
state_colors = {
'INITIAL': '#6c757d',
'READY': '#ffc107',
'RUNNING': '#28a745',
'FINISHED': '#007bff',
'ABORTED': '#dc3545'
}
style = f"""
.column-heading {{
background-color: {state_colors['FINISHED']};
padding: 12px 15px;
margin: 2px;
margin-bottom: -2px;
justify-content: space-between;
}}
.column-heading .widget-html {{
font-size: inherit;
margin: 0;
}}
.column-heading, .column-heading .widget-html-content {{
color: #ffffff;
font-size: 120%;
font-weight: bold;
}}
.column-heading button {{
width: auto;
margin: 0;
font-size: 150%;
background: none!important;
outline: none!important;
box-shadow: none!important;
color: #ffffff;
}}
.interface-frame .output, .interface-frame .output_area, .interface-frame .output_subarea, .interface-frame .bqplot,
.interface-frame .jp-OutputArea, .interface-frame .jp-OutputArea-child, .interface-frame .jp-OutputArea-output,
.plot-frame .output, .plot-frame .output_area, .plot-frame .output_subarea,
.plot-frame .jp-OutputArea, .plot-frame .jp-OutputArea-child, .plot-frame .jp-OutputArea-output {{
height: 100%;
overflow: hidden;
}}
.plot-frame img, .plot-frame .bqplot {{
height: 100%!important;
width: 100%!important;
}}
.interactive-interface .dot.element {{
cursor: pointer;
}}
.interactive-interface .dot.element:hover {{
transform: scale(1.3);
}}
.output_area + .output_area,
.jp-OutputArea + .jp-OutputArea,
.jp-OutputArea-child + .jp-OutputArea-child,
.jp-OutputArea-output + .jp-OutputArea-output {{
display: none;
}}
.widget-container {{
overflow: hidden;
}}
.widget-button, .dataset-toggle button {{
margin: 5px;
font-size: 110%;
font-weight: bold;
cursor: pointer;
}}
.dataset-toggle button {{
width: 170px;
}}
.widget-button .icon, .widget-toggle-button i {{
margin-left: 5px;
margin-right: 5px;
}}
.dataset-toggle i {{
margin-left: 5px;
}}
.widget-button:disabled {{
pointer-events: none;
}}
.setup-button, .start-button, .abort-button {{
width: 115px;
color: #fff;
}}
.setup-button {{
background-color: {state_colors['READY']};
}}
.start-button {{
background-color: {state_colors['RUNNING']};
}}
.abort-button {{
background-color: {state_colors['ABORTED']};
}}
.results-count {{
padding-right: 12px;
font-size: 120%;
}}
.buttons-frame {{
margin-left: 2px;
margin-right: 2px;
margin-top: 0px;
margin-bottom: 2px;
flex-wrap: wrap;
}}
.measurement-button, .sample-button {{
font-weight: normal;
width: auto;
}}
.measurement-button:disabled, .sample-button:disabled {{
font-weight: bold;
background-color: {state_colors['FINISHED']};
color: #fff;
}}
.filters-frame {{
margin: 10px 2px;
margin-top: 0;
}}
.filters-frame:empty {{
margin: 0;
}}
.filters-frame .widget-hbox {{
border: 2px #dddddd solid;
margin-bottom: -2px;
}}
.filters-frame .widget-hbox:last-child {{
border-bottom: 4px #dddddd solid;
}}
.filters-frame .widget-hbox:nth-of-type(even) {{
background-color: #f3f3f3;
}}
.filters-frame .widget-hbox:hover {{
border-left: 2px {state_colors['FINISHED']} solid;
border-right: 2px {state_colors['FINISHED']} solid;
}}
.filters-frame .widget-checkbox .widget-label {{
width: 0;
margin-top: 3px;
}}
.filters-frame .widget-select-multiple select {{
padding: 3px;
}}
.filters-frame :disabled div {{
pointer-events: none;
color: #888888;
}}
.results-frame {{
margin-top: 5px;
}}
.results-frame:empty {{
margin: 0;
}}
.results-table {{
border-collapse: collapse;
border: 2px #dddddd solid;
width: 100%;
font-family: sans-serif;
}}
.results-table thead tr {{
border: 2px {state_colors['FINISHED']} solid;
background-color: {state_colors['FINISHED']};
color: #ffffff;
text-align: left;
font-size: 120%;
}}
.results-table th,
.results-table td {{
padding: 12px 15px;
line-height: 1.1;
}}
.analysis-gui .results-table th,
.analysis-gui .results-table td {{
width: 50%;
}}
.results-table tbody tr {{
border-bottom: 2px solid #dddddd;
}}
.results-table tbody tr:hover {{
border-left: 2px {state_colors['FINISHED']} solid;
border-right: 2px {state_colors['FINISHED']} solid;
}}
.results-table tbody tr:nth-of-type(even) {{
background-color: #f3f3f3;
}}
.results-table tbody tr:last-of-type {{
border-bottom: 3px solid {state_colors['FINISHED']};
}}
"""