def test_index_trigger(self): select = Select(options=[1, 2, 3]) observations = [] def f(change): observations.append(change.new) select.observe(f, 'index') assert select.index == 0 select.options = [4, 5, 6] assert select.index == 0 assert select.value == 4 assert select.label == '4' assert observations == [0]
class Dashboard(VBox): """ Build the dashboard for Jupyter widgets. Requires running in a notebook/jupyterlab. """ def __init__(self, net, width="95%", height="550px", play_rate=0.5): self._ignore_layer_updates = False self.player = _Player(self, play_rate) self.player.start() self.net = net r = random.randint(1, 1000000) self.class_id = "picture-dashboard-%s-%s" % (self.net.name, r) self._width = width self._height = height ## Global widgets: style = {"description_width": "initial"} self.feature_columns = IntText(description="Detail columns:", value=self.net.config["dashboard.features.columns"], min=0, max=1024, style=style) self.feature_scale = FloatText(description="Detail scale:", value=self.net.config["dashboard.features.scale"], min=0.1, max=10, style=style) self.feature_columns.observe(self.regenerate, names='value') self.feature_scale.observe(self.regenerate, names='value') ## Hack to center SVG as justify-content is broken: self.net_svg = HTML(value="""<p style="text-align:center">%s</p>""" % ("",), layout=Layout( width=self._width, overflow_x='auto', overflow_y="auto", justify_content="center")) # Make controls first: self.output = Output() controls = self.make_controls() config = self.make_config() super().__init__([config, controls, self.net_svg, self.output]) def propagate(self, inputs): """ Propagate inputs through the dashboard view of the network. """ if dynamic_pictures_check(): return self.net.propagate(inputs, class_id=self.class_id, update_pictures=True) else: self.regenerate(inputs=input) def goto(self, position): if len(self.net.dataset.inputs) == 0 or len(self.net.dataset.targets) == 0: return if self.control_select.value == "Train": length = len(self.net.dataset.train_inputs) elif self.control_select.value == "Test": length = len(self.net.dataset.test_inputs) #### Position it: if position == "begin": self.control_slider.value = 0 elif position == "end": self.control_slider.value = length - 1 elif position == "prev": if self.control_slider.value - 1 < 0: self.control_slider.value = length - 1 # wrap around else: self.control_slider.value = max(self.control_slider.value - 1, 0) elif position == "next": if self.control_slider.value + 1 > length - 1: self.control_slider.value = 0 # wrap around else: self.control_slider.value = min(self.control_slider.value + 1, length - 1) self.position_text.value = self.control_slider.value def change_select(self, change=None): """ """ self.update_control_slider(change) self.regenerate() def update_control_slider(self, change=None): self.net.config["dashboard.dataset"] = self.control_select.value if len(self.net.dataset.inputs) == 0 or len(self.net.dataset.targets) == 0: self.total_text.value = "of 0" self.control_slider.value = 0 self.position_text.value = 0 self.control_slider.disabled = True self.position_text.disabled = True for child in self.control_buttons.children: if not hasattr(child, "icon") or child.icon != "refresh": child.disabled = True return if self.control_select.value == "Test": self.total_text.value = "of %s" % len(self.net.dataset.test_inputs) minmax = (0, max(len(self.net.dataset.test_inputs) - 1, 0)) if minmax[0] <= self.control_slider.value <= minmax[1]: pass # ok else: self.control_slider.value = 0 self.control_slider.min = minmax[0] self.control_slider.max = minmax[1] if len(self.net.dataset.test_inputs) == 0: disabled = True else: disabled = False elif self.control_select.value == "Train": self.total_text.value = "of %s" % len(self.net.dataset.train_inputs) minmax = (0, max(len(self.net.dataset.train_inputs) - 1, 0)) if minmax[0] <= self.control_slider.value <= minmax[1]: pass # ok else: self.control_slider.value = 0 self.control_slider.min = minmax[0] self.control_slider.max = minmax[1] if len(self.net.dataset.train_inputs) == 0: disabled = True else: disabled = False self.control_slider.disabled = disabled self.position_text.disbaled = disabled self.position_text.value = self.control_slider.value for child in self.control_buttons.children: if not hasattr(child, "icon") or child.icon != "refresh": child.disabled = disabled def update_zoom_slider(self, change): if change["name"] == "value": self.net.config["svg_scale"] = self.zoom_slider.value self.regenerate() def update_position_text(self, change): # {'name': 'value', 'old': 2, 'new': 3, 'owner': IntText(value=3, layout=Layout(width='100%')), 'type': 'change'} self.control_slider.value = change["new"] def get_current_input(self): if self.control_select.value == "Train" and len(self.net.dataset.train_targets) > 0: return self.net.dataset.train_inputs[self.control_slider.value] elif self.control_select.value == "Test" and len(self.net.dataset.test_targets) > 0: return self.net.dataset.test_inputs[self.control_slider.value] def get_current_targets(self): if self.control_select.value == "Train" and len(self.net.dataset.train_targets) > 0: return self.net.dataset.train_targets[self.control_slider.value] elif self.control_select.value == "Test" and len(self.net.dataset.test_targets) > 0: return self.net.dataset.test_targets[self.control_slider.value] def update_slider_control(self, change): if len(self.net.dataset.inputs) == 0 or len(self.net.dataset.targets) == 0: self.total_text.value = "of 0" return if change["name"] == "value": self.position_text.value = self.control_slider.value if self.control_select.value == "Train" and len(self.net.dataset.train_targets) > 0: self.total_text.value = "of %s" % len(self.net.dataset.train_inputs) if self.net.model is None: return if not dynamic_pictures_check(): self.regenerate(inputs=self.net.dataset.train_inputs[self.control_slider.value], targets=self.net.dataset.train_targets[self.control_slider.value]) return output = self.net.propagate(self.net.dataset.train_inputs[self.control_slider.value], class_id=self.class_id, update_pictures=True) if self.feature_bank.value in self.net.layer_dict.keys(): self.net.propagate_to_features(self.feature_bank.value, self.net.dataset.train_inputs[self.control_slider.value], cols=self.feature_columns.value, scale=self.feature_scale.value, html=False) if self.net.config["show_targets"]: if len(self.net.output_bank_order) == 1: ## FIXME: use minmax of output bank self.net.display_component([self.net.dataset.train_targets[self.control_slider.value]], "targets", class_id=self.class_id, minmax=(-1, 1)) else: self.net.display_component(self.net.dataset.train_targets[self.control_slider.value], "targets", class_id=self.class_id, minmax=(-1, 1)) if self.net.config["show_errors"]: ## minmax is error if len(self.net.output_bank_order) == 1: errors = np.array(output) - np.array(self.net.dataset.train_targets[self.control_slider.value]) self.net.display_component([errors.tolist()], "errors", class_id=self.class_id, minmax=(-1, 1)) else: errors = [] for bank in range(len(self.net.output_bank_order)): errors.append( np.array(output[bank]) - np.array(self.net.dataset.train_targets[self.control_slider.value][bank])) self.net.display_component(errors, "errors", class_id=self.class_id, minmax=(-1, 1)) elif self.control_select.value == "Test" and len(self.net.dataset.test_targets) > 0: self.total_text.value = "of %s" % len(self.net.dataset.test_inputs) if self.net.model is None: return if not dynamic_pictures_check(): self.regenerate(inputs=self.net.dataset.test_inputs[self.control_slider.value], targets=self.net.dataset.test_targets[self.control_slider.value]) return output = self.net.propagate(self.net.dataset.test_inputs[self.control_slider.value], class_id=self.class_id, update_pictures=True) if self.feature_bank.value in self.net.layer_dict.keys(): self.net.propagate_to_features(self.feature_bank.value, self.net.dataset.test_inputs[self.control_slider.value], cols=self.feature_columns.value, scale=self.feature_scale.value, html=False) if self.net.config["show_targets"]: ## FIXME: use minmax of output bank self.net.display_component([self.net.dataset.test_targets[self.control_slider.value]], "targets", class_id=self.class_id, minmax=(-1, 1)) if self.net.config["show_errors"]: ## minmax is error if len(self.net.output_bank_order) == 1: errors = np.array(output) - np.array(self.net.dataset.test_targets[self.control_slider.value]) self.net.display_component([errors.tolist()], "errors", class_id=self.class_id, minmax=(-1, 1)) else: errors = [] for bank in range(len(self.net.output_bank_order)): errors.append( np.array(output[bank]) - np.array(self.net.dataset.test_targets[self.control_slider.value][bank])) self.net.display_component(errors, "errors", class_id=self.class_id, minmax=(-1, 1)) def toggle_play(self, button): ## toggle if self.button_play.description == "Play": self.button_play.description = "Stop" self.button_play.icon = "pause" self.player.resume() else: self.button_play.description = "Play" self.button_play.icon = "play" self.player.pause() def prop_one(self, button=None): self.update_slider_control({"name": "value"}) def regenerate(self, button=None, inputs=None, targets=None): ## Protection when deleting object on shutdown: if isinstance(button, dict) and 'new' in button and button['new'] is None: return ## Update the config: self.net.config["dashboard.features.bank"] = self.feature_bank.value self.net.config["dashboard.features.columns"] = self.feature_columns.value self.net.config["dashboard.features.scale"] = self.feature_scale.value inputs = inputs if inputs is not None else self.get_current_input() targets = targets if targets is not None else self.get_current_targets() features = None if self.feature_bank.value in self.net.layer_dict.keys() and inputs is not None: if self.net.model is not None: features = self.net.propagate_to_features(self.feature_bank.value, inputs, cols=self.feature_columns.value, scale=self.feature_scale.value, display=False) svg = """<p style="text-align:center">%s</p>""" % (self.net.to_svg( inputs=inputs, targets=targets, class_id=self.class_id, highlights={self.feature_bank.value: { "border_color": "orange", "border_width": 30, }})) if inputs is not None and features is not None: html_horizontal = """ <table align="center" style="width: 100%%;"> <tr> <td valign="top" style="width: 50%%;">%s</td> <td valign="top" align="center" style="width: 50%%;"><p style="text-align:center"><b>%s</b></p>%s</td> </tr> </table>""" html_vertical = """ <table align="center" style="width: 100%%;"> <tr> <td valign="top">%s</td> </tr> <tr> <td valign="top" align="center"><p style="text-align:center"><b>%s</b></p>%s</td> </tr> </table>""" self.net_svg.value = (html_vertical if self.net.config["svg_rotate"] else html_horizontal) % ( svg, "%s details" % self.feature_bank.value, features) else: self.net_svg.value = svg def make_colormap_image(self, colormap_name): from .layers import Layer if not colormap_name: colormap_name = get_colormap() layer = Layer("Colormap", 100) minmax = layer.get_act_minmax() image = layer.make_image(np.arange(minmax[0], minmax[1], .01), colormap_name, {"pixels_per_unit": 1, "svg_rotate": self.net.config["svg_rotate"]}).resize((300, 25)) return image def set_attr(self, obj, attr, value): if value not in [{}, None]: ## value is None when shutting down if isinstance(value, dict): value = value["value"] if isinstance(obj, dict): obj[attr] = value else: setattr(obj, attr, value) ## was crashing on Widgets.__del__, if get_ipython() no longer existed self.regenerate() def make_controls(self): layout = Layout(width='100%', height="100%") button_begin = Button(icon="fast-backward", layout=layout) button_prev = Button(icon="backward", layout=layout) button_next = Button(icon="forward", layout=layout) button_end = Button(icon="fast-forward", layout=layout) #button_prop = Button(description="Propagate", layout=Layout(width='100%')) #button_train = Button(description="Train", layout=Layout(width='100%')) self.button_play = Button(icon="play", description="Play", layout=layout) step_down = Button(icon="sort-down", layout=Layout(width="95%", height="100%")) step_up = Button(icon="sort-up", layout=Layout(width="95%", height="100%")) up_down = HBox([step_down, step_up], layout=Layout(width="100%", height="100%")) refresh_button = Button(icon="refresh", layout=Layout(width="25%", height="100%")) self.position_text = IntText(value=0, layout=layout) self.control_buttons = HBox([ button_begin, button_prev, #button_train, self.position_text, button_next, button_end, self.button_play, up_down, refresh_button ], layout=Layout(width='100%', height="100%")) length = (len(self.net.dataset.train_inputs) - 1) if len(self.net.dataset.train_inputs) > 0 else 0 self.control_slider = IntSlider(description="Dataset index", continuous_update=False, min=0, max=max(length, 0), value=0, layout=Layout(width='100%')) if self.net.config["dashboard.dataset"] == "Train": length = len(self.net.dataset.train_inputs) else: length = len(self.net.dataset.test_inputs) self.total_text = Label(value="of %s" % length, layout=Layout(width="100px")) self.zoom_slider = FloatSlider(description="Zoom", continuous_update=False, min=0, max=1.0, style={"description_width": 'initial'}, layout=Layout(width="65%"), value=self.net.config["svg_scale"] if self.net.config["svg_scale"] is not None else 0.5) ## Hook them up: button_begin.on_click(lambda button: self.goto("begin")) button_end.on_click(lambda button: self.goto("end")) button_next.on_click(lambda button: self.goto("next")) button_prev.on_click(lambda button: self.goto("prev")) self.button_play.on_click(self.toggle_play) self.control_slider.observe(self.update_slider_control, names='value') refresh_button.on_click(lambda widget: (self.update_control_slider(), self.output.clear_output(), self.regenerate())) step_down.on_click(lambda widget: self.move_step("down")) step_up.on_click(lambda widget: self.move_step("up")) self.zoom_slider.observe(self.update_zoom_slider, names='value') self.position_text.observe(self.update_position_text, names='value') # Put them together: controls = VBox([HBox([self.control_slider, self.total_text], layout=Layout(height="40px")), self.control_buttons], layout=Layout(width='100%')) #net_page = VBox([control, self.net_svg], layout=Layout(width='95%')) controls.on_displayed(lambda widget: self.regenerate()) return controls def move_step(self, direction): """ Move the layer stepper up/down through network """ options = [""] + [layer.name for layer in self.net.layers] index = options.index(self.feature_bank.value) if direction == "up": new_index = (index + 1) % len(options) else: ## down new_index = (index - 1) % len(options) self.feature_bank.value = options[new_index] self.regenerate() def make_config(self): layout = Layout() style = {"description_width": "initial"} checkbox1 = Checkbox(description="Show Targets", value=self.net.config["show_targets"], layout=layout, style=style) checkbox1.observe(lambda change: self.set_attr(self.net.config, "show_targets", change["new"]), names='value') checkbox2 = Checkbox(description="Errors", value=self.net.config["show_errors"], layout=layout, style=style) checkbox2.observe(lambda change: self.set_attr(self.net.config, "show_errors", change["new"]), names='value') hspace = IntText(value=self.net.config["hspace"], description="Horizontal space between banks:", style=style, layout=layout) hspace.observe(lambda change: self.set_attr(self.net.config, "hspace", change["new"]), names='value') vspace = IntText(value=self.net.config["vspace"], description="Vertical space between layers:", style=style, layout=layout) vspace.observe(lambda change: self.set_attr(self.net.config, "vspace", change["new"]), names='value') self.feature_bank = Select(description="Details:", value=self.net.config["dashboard.features.bank"], options=[""] + [layer.name for layer in self.net.layers], rows=1) self.feature_bank.observe(self.regenerate, names='value') self.control_select = Select( options=['Test', 'Train'], value=self.net.config["dashboard.dataset"], description='Dataset:', rows=1 ) self.control_select.observe(self.change_select, names='value') column1 = [self.control_select, self.zoom_slider, hspace, vspace, HBox([checkbox1, checkbox2]), self.feature_bank, self.feature_columns, self.feature_scale ] ## Make layer selectable, and update-able: column2 = [] layer = self.net.layers[-1] self.layer_select = Select(description="Layer:", value=layer.name, options=[layer.name for layer in self.net.layers], rows=1) self.layer_select.observe(self.update_layer_selection, names='value') column2.append(self.layer_select) self.layer_visible_checkbox = Checkbox(description="Visible", value=layer.visible, layout=layout) self.layer_visible_checkbox.observe(self.update_layer, names='value') column2.append(self.layer_visible_checkbox) self.layer_colormap = Select(description="Colormap:", options=[""] + AVAILABLE_COLORMAPS, value=layer.colormap if layer.colormap is not None else "", layout=layout, rows=1) self.layer_colormap_image = HTML(value="""<img src="%s"/>""" % self.net._image_to_uri(self.make_colormap_image(layer.colormap))) self.layer_colormap.observe(self.update_layer, names='value') column2.append(self.layer_colormap) column2.append(self.layer_colormap_image) ## get dynamic minmax; if you change it it will set it in layer as override: minmax = layer.get_act_minmax() self.layer_mindim = FloatText(description="Leftmost color maps to:", value=minmax[0], style=style) self.layer_maxdim = FloatText(description="Rightmost color maps to:", value=minmax[1], style=style) self.layer_mindim.observe(self.update_layer, names='value') self.layer_maxdim.observe(self.update_layer, names='value') column2.append(self.layer_mindim) column2.append(self.layer_maxdim) output_shape = layer.get_output_shape() self.layer_feature = IntText(value=layer.feature, description="Feature to show:", style=style) self.svg_rotate = Checkbox(description="Rotate", value=layer.visible, layout=layout) self.layer_feature.observe(self.update_layer, names='value') column2.append(self.layer_feature) self.svg_rotate = Checkbox(description="Rotate network", value=self.net.config["svg_rotate"], style={"description_width": 'initial'}, layout=Layout(width="52%")) self.svg_rotate.observe(lambda change: self.set_attr(self.net.config, "svg_rotate", change["new"]), names='value') self.save_config_button = Button(icon="save", layout=Layout(width="10%")) self.save_config_button.on_click(self.save_config) column2.append(HBox([self.svg_rotate, self.save_config_button])) config_children = HBox([VBox(column1, layout=Layout(width="100%")), VBox(column2, layout=Layout(width="100%"))]) accordion = Accordion(children=[config_children]) accordion.set_title(0, self.net.name) accordion.selected_index = None return accordion def save_config(self, widget=None): self.net.save_config() def update_layer(self, change): """ Update the layer object, and redisplay. """ if self._ignore_layer_updates: return ## The rest indicates a change to a display variable. ## We need to save the value in the layer, and regenerate ## the display. # Get the layer: layer = self.net[self.layer_select.value] # Save the changed value in the layer: layer.feature = self.layer_feature.value layer.visible = self.layer_visible_checkbox.value ## These three, dealing with colors of activations, ## can be done with a prop_one(): if "color" in change["owner"].description.lower(): ## Matches: Colormap, lefmost color, rightmost color ## overriding dynamic minmax! layer.minmax = (self.layer_mindim.value, self.layer_maxdim.value) layer.minmax = (self.layer_mindim.value, self.layer_maxdim.value) layer.colormap = self.layer_colormap.value if self.layer_colormap.value else None self.layer_colormap_image.value = """<img src="%s"/>""" % self.net._image_to_uri(self.make_colormap_image(layer.colormap)) self.prop_one() else: self.regenerate() def update_layer_selection(self, change): """ Just update the widgets; don't redraw anything. """ ## No need to redisplay anything self._ignore_layer_updates = True ## First, get the new layer selected: layer = self.net[self.layer_select.value] ## Now, let's update all of the values without updating: self.layer_visible_checkbox.value = layer.visible self.layer_colormap.value = layer.colormap if layer.colormap != "" else "" self.layer_colormap_image.value = """<img src="%s"/>""" % self.net._image_to_uri(self.make_colormap_image(layer.colormap)) minmax = layer.get_act_minmax() self.layer_mindim.value = minmax[0] self.layer_maxdim.value = minmax[1] self.layer_feature.value = layer.feature self._ignore_layer_updates = False
class DatabaseExplorer(VBox): """ Combo widget based on a select box containing all experiments in specified database. """ session = None ee = None experiments = None keywords = None variables = None def __init__(self, session=None, de=None): if session is None: session = database.create_session() self.session = session if de is not None: warning.warn( "DatabaseExtension has been deprecated is no longer supported") self.experiments = querying.get_experiments(session=self.session, all=True) self.keywords = sorted(querying.get_keywords(self.session), key=str.casefold) self.variables = querying.get_variables(self.session, inferred=True) self._make_widgets() # Call super init and pass widgets as children super().__init__(children=[ self.header, self.selectors, self.expt_info, self.expt_explorer ]) # Show the experiment information: important for only one experiment, as # events will not trigger this otherwise self._show_experiment_information(self.expt_selector.value) self._set_handlers() def _make_widgets(self): style = "<style>.header p{ line-height: 1.4; margin-bottom: 10px }</style>" # Gui header self.header = HTML( value=style + """ <h3>Database Explorer</h3> <div class="header"> <p>Select an experiment to show more detailed information where available. With an experiment selected push 'Load Experiment' to open an Experiment Explorer gui.</p> <p>The list of experiments can be filtered by keywords and/or variables. Multiple keywords can be selected using alt/option/ctrl (system dependent) or the shift modifier when selecting. To filter by variables select a variable and add it to the "Filter variables" box using the ">>" button, and vice-versa to remove variables from the filter. Push the 'Filter' button to show only matching experiments.</p> <p>When the ExperimentExplorer element loads data it is accessible as the <tt>.data</tt> attribute of the DatabaseExplorer object</p> </div> """, description="", layout={"width": "60%"}, ) # Experiment selector box self.expt_selector = Select( options=sorted(set(self.experiments.experiment), key=str.casefold), rows=24, layout={ "padding": "0px 5px", "width": "auto" }, disabled=False, ) # Keyword filtering element is a Multiple selection box # checkboxes self.filter_widget = SelectMultiple( rows=15, options=sorted(self.keywords, key=str.casefold), layout={"flex": "0 0 100%"}, ) # Reset keywords button self.clear_keywords_button = Button( description="Clear", layout={ "width": "20%", "align": "center" }, tooltip="Click to clear selected keywords", ) self.keyword_box = VBox( [self.filter_widget, self.clear_keywords_button], layout={"flex": "0 0 40%"}) # Filtering button self.filter_button = Button( description="Filter", # layout={'width': '50%', 'align': 'center'}, tooltip="Click to filter experiments", ) # Variable filter selector combo widget self.var_filter = VariableSelectFilter(self.variables, layout={"flex": "0 0 40%"}) # Tab box to contain keyword and variable filters self.filter_tabs = Tab(title="Filter", children=[self.keyword_box, self.var_filter]) self.filter_tabs.set_title(0, "Keyword") self.filter_tabs.set_title(1, "Variable") self.load_button = Button( description="Load Experiment", disabled=False, layout={ "width": "50%", }, tooltip="Click to load experiment", ) # Experiment information panel self.expt_info = HTML( value="", description="", layout={ "width": "80%", "align": "center" }, ) # Experiment explorer box self.expt_explorer = HBox() # Some box layout nonsense to organise widgets in space self.selectors = HBox([ VBox( [ Label(value="Experiments:"), self.expt_selector, self.load_button, ], layout={ "padding": "0px 5px", "flex": "0 0 30%" }, ), VBox( [ Label(value="Filter by:"), self.filter_tabs, self.filter_button ], layout={ "padding": "0px 10px", "flex": "0 0 65%" }, ), ]) def _keyword_filter(self, keywords): """ Return a list of experiments matching *all* of the supplied keywords """ try: return querying.get_experiments(self.session, keywords=keywords).experiment except AttributeError: return [] def _variable_filter(self, variables): """ Return a set of experiments that contain all the defined variables """ return querying.get_experiments(self.session, variables=variables).experiment def _set_handlers(self): """ Define routines to handle button clicks and experiment selection """ self.expt_selector.observe(self._expt_eventhandler, names="value") self.load_button.on_click(self._load_experiment) self.filter_button.on_click(self._filter_experiments) self.clear_keywords_button.on_click(self._clear_keywords) def _filter_restart_eventhandler(self, selector): """ Re-populate variable list when checkboxes selected/de-selected """ self._filter_variables() def _clear_keywords(self, selector): """ Deselect all keywords """ self.filter_widget.value = () def _expt_eventhandler(self, selector): """ When experiment is selected populate the experiment information elements """ if selector.new is None: return self._show_experiment_information(selector.new) def _show_experiment_information(self, experiment_name): """ Populate box with experiment information """ expt = self.experiments[self.experiments.experiment == experiment_name] style = """ <style> .info { font: normal 90% Verdana, Arial, sans-serif; } .info a:hover { color: red; text-decoration: underline; } </style> """ self.expt_info.value = (style + """ <div class="info"> <table> <tr><td><b>Experiment:</b></td> <td>{experiment}</td></tr> <tr><td style="vertical-align:top;"><b>Description:</b></td> <td>{description}</td></tr> <tr><td style="vertical-align:top;"><b>Notes:</b></td> <td>{notes}</td></tr> <tr><td><b>Contact:</b></td> <td>{contact} <<a href="mailto:{email}" target="_blank">{email}</a>></td></tr> <tr><td><b>Control repo:</b></td> <td><a href="{url}" target="_blank">{url}</a></td></tr> <tr><td><b>No. files:</b></td> <td>{ncfiles}</td></tr> <tr><td><b>Created:</b></td> <td>{created}</td></tr> </table> </div> """.format(experiment=experiment_name, **{ field: return_value_or_empty(expt[field].values[0]) for field in [ "description", "notes", "contact", "email", "url", "ncfiles", "created", ] })) def _filter_experiments(self, b): """ Filter experiment list by keywords and variable """ options = set(self.experiments.experiment) kwds = self.filter_widget.value if len(kwds) > 0: options.intersection_update(self._keyword_filter(kwds)) variables = self.var_filter.selected_vars() if len(variables) > 0: options.intersection_update(self._variable_filter(variables)) self.expt_selector.options = sorted(options, key=str.casefold) def _load_experiment(self, b): """ Open an Experiment Explorer UI with selected experiment """ if self.expt_selector.value is not None: self.ee = ExperimentExplorer(session=self.session, experiment=self.expt_selector.value) self.expt_explorer.children = [self.ee] @property def data(self): """ Return xarray DataArray if one has been loaded in ExperimentExplorer """ if self.ee is None: print("Cannot return data if no experiment has been loaded") return None return self.ee.data
class VariableSelector(VBox): """ Combo widget based on a Select box with a search panel above to live filter variables to Select. When a variable is selected the long name attribute is displayed under the select box. There are also two checkboxes which hide coordinates and restart variables. Note that a dict is used to populate the Select widget, so the visible value is the variable name and is accessed via the label attribute, and the long name via the value attribute. """ variables = None def __init__(self, variables, rows=10, **kwargs): """ variables is a pandas dataframe. kwargs are passed through to child widgets which, theoretically, allows for layout information to be specified """ self._make_widgets(rows) super().__init__(children=[ self.model, self.search, self.selector, self.info, self.filter_coords, self.filter_restarts, ], **kwargs) self.set_variables(variables) self._set_info() self._set_observes() def _make_widgets(self, rows): """ Instantiate all widgets """ # Experiment selector element self.model = Dropdown( options=(), layout={ "padding": "0px 5px", "width": "initial" }, description="", ) # Variable search self.search = Text( placeholder="Search: start typing", layout={ "padding": "0px 5px", "width": "auto", "overflow-x": "scroll" }, ) # Variable selection box self.selector = Select( options=(), # sorted(self.variables.name, key=str.casefold), rows=rows, layout=self.search.layout, ) # Variable info self.info = HTML(layout=self.search.layout) # Variable filtering elements self.filter_coords = Checkbox( value=True, indent=False, description="Hide coordinates", ) self.filter_restarts = Checkbox( value=True, indent=False, description="Hide restarts", ) def _set_observes(self): """ Set event handlers """ self.filter_coords.observe(self._filter_eventhandler, names="value") self.filter_restarts.observe(self._filter_eventhandler, names="value") self.model.observe(self._model_eventhandler, names="value") self.search.observe(self._search_eventhandler, names="value") self.selector.observe(self._selector_eventhandler, names="value") def set_variables(self, variables): """ Change variables """ # Add a new column to keep track of visibility in widget self.variables = variables.assign(visible=True) # Set default filtering self._filter_variables() # Update selector self._update_selector(self.variables[self.variables.visible]) def _update_selector(self, variables): """ Update the variables in the selector. The variable are passed as an argument, so can differ from the internal variable list. This allows for easy filtering """ # Populate model selector. Note label and value differ options = {"All models": ""} for model in variables.model.cat.categories.values: if len(model) > 0 and model != "none": options["{} only".format(model.capitalize())] = model self.model.options = options options = dict() firstvar = None for vals in variables.sort_values(["name" ])[["name", "long_name", "units"]].values: var, name, units = map(str, vals) if firstvar is None: firstvar = var if name.lower() == "none" or name == "": name = var # Add units string if suitable value exists if (units.lower() == "none" or units.lower() == "nounits" or units.lower() == "no units" or units.lower() == "dimensionless" or units.lower() == "1" or units == ""): options[var] = "{}".format(name) else: options[var] = "{} ({})".format(name, units) # Populate variable selector self.selector.options = options # Highlight first value, otherwise accessors like .value are not # immediately accessible if firstvar is not None: self.selector.value = options[firstvar] def _reset_filters(self): """ Reset filters to default values """ self.filter_coords.value = True self.filter_restarts.value = True def _model_eventhandler(self, event=None): """ Filter by model """ model = self.model.value # Reset the coord and restart filters when a model changed self._reset_filters() self._filter_variables(model=model) def _filter_eventhandler(self, event=None): """ Called when filter button pushed """ self._filter_variables(self.filter_coords.value, self.filter_restarts.value, self.model.value) def _filter_variables(self, coords=True, restarts=True, model=""): """ Optionally hide some variables """ # Set up a mask with all true values mask = self.variables["name"] != "" # Filter for matching models if model != "": mask = mask & (self.variables["model"] == model) # Conditionally filter out restarts and coordinates if coords: mask = mask & ~self.variables["coordinate"] if restarts: mask = mask & ~self.variables["restart"] # Mask out hidden variables self.variables["visible"] = mask # Update the variable selector self._update_selector(self.variables[self.variables.visible]) # Reset the search self.search.value = "" self.selector.value = None def _search_eventhandler(self, event=None): """ Live search bar, updates the selector options dynamically, does not alter visible mask in variables """ search_term = self.search.value variables = self.variables[self.variables.visible] if search_term is not None or search_term != "": try: variables = variables[variables.name.str.contains( search_term, case=False, na=False) | variables.long_name.str.contains( search_term, case=False, na=False)] except: warnings.warn("Illegal character in search!", UserWarning) search_term = self.search.value self._update_selector(variables) def _selector_eventhandler(self, event=None): """ Update variable info when variable selected """ self._set_info(self.selector.value) def _set_info(self, long_name=None): """ Set long name info widget """ if long_name is None or long_name == "": long_name = " " style = "<style>.breakword { word-wrap: break-word; font-size: 90%; line-height: 1.1;}</style>" self.info.value = style + '<p class="breakword">{long_name}</p>'.format( long_name=long_name) def delete(self, variable_names=None): """ Remove variables """ # If no variable specified just delete the currently selected one if variable_names is None: if self.selector.label is None: return None else: variable_names = [ self.selector.label, ] if isinstance(variable_names, str): variable_names = [ variable_names, ] mask = self.variables["name"].isin(variable_names) deleted = self.variables[mask] # Delete variables self.variables = self.variables[~mask] # Update selector. Use search eventhandler so the selector preserves any # current search term. It is annoying to have that reset and type in again # if multiple variables are to be added self._search_eventhandler() return deleted def add(self, variables): """ Add variables """ # Concatenate existing and new variables self.variables = pd.concat([self.variables, variables]) # Need to recalculate the visible flag as new variables have been added self._filter_eventhandler(None) def get_selected(self): """ Return currently selected variable name """ return self.selector.label
class FileChooser(VBox, ValueWidget): """FileChooser class.""" _LBL_TEMPLATE = '<span style="margin-left:10px; color:{1};">{0}</span>' _LBL_NOFILE = 'No file selected' def __init__(self, path=os.getcwd(), filename='', title='', width=300, select_desc='Select', change_desc='Change', show_hidden=False, select_default=False, use_dir_icons=False, show_only_dirs=False, filter_pattern=None, **kwargs): """Initialize FileChooser object.""" self._default_path = path.rstrip(os.path.sep) self._default_filename = filename self._selected_path = None self._selected_filename = None self._show_hidden = show_hidden self._select_desc = select_desc self._change_desc = change_desc self._callback = None self._select_default = select_default self._use_dir_icons = use_dir_icons self._show_only_dirs = show_only_dirs self._filter_pattern = filter_pattern # Widgets self._pathlist = Dropdown(description="", layout=Layout(width='auto', grid_area='pathlist')) self._filename = Text( placeholder='output filename', layout=Layout(width='auto', grid_area='filename', display=(None, "none")[self._show_only_dirs]), disabled=self._show_only_dirs) self._dircontent = Select(rows=8, layout=Layout(width='auto', grid_area='dircontent')) self._cancel = Button(description='Cancel', layout=Layout(width='auto', display='none')) self._select = Button(description=self._select_desc, layout=Layout(width='auto')) self._title = HTML(value=title) if title == '': self._title.layout.display = 'none' # Widget observe handlers self._pathlist.observe(self._on_pathlist_select, names='value') self._dircontent.observe(self._on_dircontent_select, names='value') self._filename.observe(self._on_filename_change, names='value') self._select.on_click(self._on_select_click) self._cancel.on_click(self._on_cancel_click) # Selected file label self._label = HTML(value=self._LBL_TEMPLATE.format( self._LBL_NOFILE, 'black'), placeholder='', description='') # Layout self._gb = VBox( children=[self._pathlist, self._filename, self._dircontent], layout=Layout(width=f'{width}px')) # self._gb = GridBox( # children=[ # self._pathlist, # self._filename, # self._dircontent # ], # layout=Layout( # display='none', # width=f'{width}px', # grid_gap='0px 0px', # grid_template_rows='auto auto', # grid_template_columns='60% 40%', # grid_template_areas=''' # 'pathlist {}' # 'dircontent dircontent' # '''.format(('filename', 'pathlist')[self._show_only_dirs]) # ) # ) # buttonbar = HBox( # children=[ # self._select, # self._cancel, # self._label # ], # layout=Layout(width='auto') # ) buttonbar = VBox(children=[self._select, self._cancel, self._label], layout=Layout(width='auto')) # Call setter to set initial form values self._set_form_values(self._default_path, self._default_filename) # Use the defaults as the selected values if self._select_default: self._apply_selection() # Call VBox super class __init__ super().__init__(children=[ self._title, self._gb, buttonbar, ], layout=Layout(width='auto'), **kwargs) def _set_form_values(self, path, filename): """Set the form values.""" # Disable triggers to prevent selecting an entry in the Select # box from automatically triggering a new event. self._pathlist.unobserve(self._on_pathlist_select, names='value') self._dircontent.unobserve(self._on_dircontent_select, names='value') self._filename.unobserve(self._on_filename_change, names='value') # In folder only mode zero out the filename if self._show_only_dirs: filename = '' # Set form values self._pathlist.options = get_subpaths(path) self._pathlist.value = path self._filename.value = filename # file/folder real names dircontent_real_names = get_dir_contents( path, show_hidden=self._show_hidden, prepend_icons=False, show_only_dirs=self._show_only_dirs, filter_pattern=self._filter_pattern) # file/folder display names dircontent_display_names = get_dir_contents( path, show_hidden=self._show_hidden, prepend_icons=self._use_dir_icons, show_only_dirs=self._show_only_dirs, filter_pattern=self._filter_pattern) # Dict to map real names to display names self._map_name_to_disp = { real_name: disp_name for real_name, disp_name in zip(dircontent_real_names, dircontent_display_names) } # Dict to map display names to real names self._map_disp_to_name = dict( reversed(item) for item in self._map_name_to_disp.items()) # Set _dircontent form value to display names self._dircontent.options = dircontent_display_names # If the value in the filename Text box equals a value in the # Select box and the entry is a file then select the entry. if ((filename in dircontent_real_names) and os.path.isfile(os.path.join(path, filename))): self._dircontent.value = self._map_name_to_disp[filename] else: self._dircontent.value = None # Reenable triggers again self._pathlist.observe(self._on_pathlist_select, names='value') self._dircontent.observe(self._on_dircontent_select, names='value') self._filename.observe(self._on_filename_change, names='value') # Update the state of the select button if self._gb.layout.display is None: # Disable the select button if path and filename # - equal an existing folder in the current view # - equal the already selected values # - don't match the provided filter pattern(s) check1 = filename in dircontent_real_names check2 = os.path.isdir(os.path.join(path, filename)) check3 = False check4 = False # Only check selected if selected is set if ((self._selected_path is not None) and (self._selected_filename is not None)): selected = os.path.join(self._selected_path, self._selected_filename) check3 = os.path.join(path, filename) == selected # Ensure only allowed extensions are used if self._filter_pattern: check4 = not match_item(filename, self._filter_pattern) if (check1 and check2) or check3 or check4: self._select.disabled = True else: self._select.disabled = False def _on_pathlist_select(self, change): """Handle selecting a path entry.""" self._set_form_values(change['new'], self._filename.value) def _on_dircontent_select(self, change): """Handle selecting a folder entry.""" new_path = os.path.realpath( os.path.join(self._pathlist.value, self._map_disp_to_name[change['new']])) # Check if folder or file if os.path.isdir(new_path): path = new_path filename = self._filename.value elif os.path.isfile(new_path): path = self._pathlist.value filename = self._map_disp_to_name[change['new']] self._set_form_values(path, filename) def _on_filename_change(self, change): """Handle filename field changes.""" self._set_form_values(self._pathlist.value, change['new']) def _on_select_click(self, _b): """Handle select button clicks.""" if self._gb.layout.display == 'none': # If not shown, open the dialog self._show_dialog() else: # If shown, close the dialog and apply the selection self._apply_selection() # Execute callback function if self._callback is not None: try: self._callback(self) except TypeError: # Support previous behaviour of not passing self self._callback() def _show_dialog(self): """Show the dialog.""" # Show dialog and cancel button self._gb.layout.display = None self._cancel.layout.display = None # Show the form with the correct path and filename if ((self._selected_path is not None) and (self._selected_filename is not None)): path = self._selected_path filename = self._selected_filename else: path = self._default_path filename = self._default_filename self._set_form_values(path, filename) def _apply_selection(self): """Close the dialog and apply the selection.""" self._gb.layout.display = 'none' self._cancel.layout.display = 'none' self._select.description = self._change_desc self._selected_path = self._pathlist.value self._selected_filename = self._filename.value selected = os.path.join(self._selected_path, self._selected_filename) if os.path.isfile(selected): self._label.value = self._LBL_TEMPLATE.format(selected, 'orange') else: self._label.value = self._LBL_TEMPLATE.format(selected, 'green') def _on_cancel_click(self, _b): """Handle cancel button clicks.""" self._gb.layout.display = 'none' self._cancel.layout.display = 'none' self._select.disabled = False def reset(self, path=None, filename=None): """Reset the form to the default path and filename.""" self._selected_path = None self._selected_filename = None # Reset select button and label self._select.description = self._select_desc self._label.value = self._LBL_TEMPLATE.format(self._LBL_NOFILE, 'black') if path is not None: self._default_path = path.rstrip(os.path.sep) if filename is not None: self._default_filename = filename # Set a proper filename value if self._show_only_dirs: filename = '' else: filename = self._default_filename self._set_form_values(self._default_path, filename) # Use the defaults as the selected values if self._select_default: self._apply_selection() def refresh(self): """Re-render the form.""" self._set_form_values(self._pathlist.value, self._filename.value) @property def show_hidden(self): """Get _show_hidden value.""" return self._show_hidden @show_hidden.setter def show_hidden(self, hidden): """Set _show_hidden value.""" self._show_hidden = hidden self.refresh() @property def use_dir_icons(self): """Get _use_dir_icons value.""" return self._use_dir_icons @use_dir_icons.setter def use_dir_icons(self, dir_icons): """Set _use_dir_icons value.""" self._use_dir_icons = dir_icons self.refresh() @property def rows(self): """Get current number of rows.""" return self._dircontent.rows @rows.setter def rows(self, rows): """Set number of rows.""" self._dircontent.rows = rows @property def title(self): """Get the title.""" return self._title.value @title.setter def title(self, title): """Set the title.""" self._title.value = title if title == '': self._title.layout.display = 'none' else: self._title.layout.display = None @property def default(self): """Get the default value.""" return os.path.join(self._default_path, self._default_filename) @property def default_path(self): """Get the default_path value.""" return self._default_path @default_path.setter def default_path(self, path): """Set the default_path.""" self._default_path = path.rstrip(os.path.sep) self._set_form_values(self._default_path, self._filename.value) @property def default_filename(self): """Get the default_filename value.""" return self._default_filename @default_filename.setter def default_filename(self, filename): """Set the default_filename.""" self._default_filename = filename self._set_form_values(self._pathlist.value, self._default_filename) @property def show_only_dirs(self): """Get show_only_dirs property value.""" return self._show_only_dirs @show_only_dirs.setter def show_only_dirs(self, show_only_dirs): """Set show_only_dirs property value.""" self._show_only_dirs = show_only_dirs # Update widget layout self._filename.disabled = self._show_only_dirs self._filename.layout.display = (None, "none")[self._show_only_dirs] self._gb.layout.children = [self._pathlist, self._dircontent] if not self._show_only_dirs: self._gb.layout.children.insert(1, self._filename) self._gb.layout.grid_template_areas = ''' 'pathlist {}' 'dircontent dircontent' '''.format(('filename', 'pathlist')[self._show_only_dirs]) # Reset the dialog self.reset() @property def filter_pattern(self): """Get file name filter pattern.""" return self._filter_pattern @filter_pattern.setter def filter_pattern(self, filter_pattern): """Set file name filter pattern.""" self._filter_pattern = filter_pattern self.refresh() @property def selected(self): """Get selected value.""" try: return os.path.join(self._selected_path, self._selected_filename) except TypeError: return None @property def selected_path(self): """Get selected_path value.""" return self._selected_path @property def selected_filename(self): """Get the selected_filename.""" return self._selected_filename def __repr__(self): """Build string representation.""" str_ = ("FileChooser(" "path='{0}', " "filename='{1}', " "title='{2}', " "show_hidden='{3}', " "use_dir_icons='{4}', " "show_only_dirs='{5}', " "select_desc='{6}', " "change_desc='{7}')").format( self._default_path, self._default_filename, self._title, self._show_hidden, self._use_dir_icons, self._show_only_dirs, self._select_desc, self._change_desc) return str_ def register_callback(self, callback): """Register a callback function.""" self._callback = callback def get_interact_value(self): """Return the value which should be passed to interactive functions.""" return self.selected
class FileChooser(VBox): _LBL_TEMPLATE = '<span style="margin-left:10px; color:{1};">{0}</span>' _LBL_NOFILE = 'No file selected' def __init__(self, path=os.getcwd(), filename='', show_hidden=False, **kwargs): self._default_path = path.rstrip(os.path.sep) self._default_filename = filename self._selected_path = '' self._selected_filename = '' self._show_hidden = show_hidden # Widgets self._pathlist = Dropdown(description="", layout=Layout(width='auto', grid_area='pathlist')) self._filename = Text(placeholder='output filename', layout=Layout(width='auto', grid_area='filename')) self._dircontent = Select(rows=8, layout=Layout(width='auto', grid_area='dircontent')) self._cancel = Button(description='Cancel', layout=Layout(width='auto', display='none')) self._select = Button(description='Select', layout=Layout(width='auto')) # Widget observe handlers self._pathlist.observe(self._on_pathlist_select, names='value') self._dircontent.observe(self._on_dircontent_select, names='value') self._filename.observe(self._on_filename_change, names='value') self._select.on_click(self._on_select_click) self._cancel.on_click(self._on_cancel_click) # Selected file label self._label = HTML(value=self._LBL_TEMPLATE.format( self._LBL_NOFILE, 'black'), placeholder='', description='') # Layout self._gb = GridBox( children=[self._pathlist, self._filename, self._dircontent], layout=Layout(display='none', width='500px', grid_gap='0px 0px', grid_template_rows='auto auto', grid_template_columns='60% 40%', grid_template_areas=''' 'pathlist filename' 'dircontent dircontent' ''')) buttonbar = HBox(children=[self._select, self._cancel, self._label], layout=Layout(width='auto')) # Call setter to set initial form values self._set_form_values(self._default_path, self._default_filename) # Call VBox super class __init__ super().__init__(children=[ self._gb, buttonbar, ], layout=Layout(width='auto'), **kwargs) def _set_form_values(self, path, filename): '''Set the form values''' # Disable triggers to prevent selecting an entry in the Select # box from automatically triggering a new event. self._pathlist.unobserve(self._on_pathlist_select, names='value') self._dircontent.unobserve(self._on_dircontent_select, names='value') self._filename.unobserve(self._on_filename_change, names='value') # Set form values self._pathlist.options = get_subpaths(path) self._pathlist.value = path self._filename.value = filename self._dircontent.options = get_dir_contents(path, hidden=self._show_hidden) # If the value in the filename Text box equals a value in the # Select box and the entry is a file then select the entry. if ((filename in self._dircontent.options) and os.path.isfile(os.path.join(path, filename))): self._dircontent.value = filename else: self._dircontent.value = None # Reenable triggers again self._pathlist.observe(self._on_pathlist_select, names='value') self._dircontent.observe(self._on_dircontent_select, names='value') self._filename.observe(self._on_filename_change, names='value') # Set the state of the select Button if self._gb.layout.display is None: selected = os.path.join(self._selected_path, self._selected_filename) # filename value is empty or equals the selected value if (filename == '') or (os.path.join(path, filename) == selected): self._select.disabled = True else: self._select.disabled = False def _on_pathlist_select(self, change): '''Handler for when a new path is selected''' self._set_form_values(change['new'], self._filename.value) def _on_dircontent_select(self, change): '''Handler for when a folder entry is selected''' new_path = update_path(self._pathlist.value, change['new']) # Check if folder or file if os.path.isdir(new_path): path = new_path filename = self._filename.value elif os.path.isfile(new_path): path = self._pathlist.value filename = change['new'] self._set_form_values(path, filename) def _on_filename_change(self, change): '''Handler for when the filename field changes''' self._set_form_values(self._pathlist.value, change['new']) def _on_select_click(self, b): '''Handler for when the select button is clicked''' if self._gb.layout.display is 'none': self._gb.layout.display = None self._cancel.layout.display = None # Show the form with the correct path and filename if self._selected_path and self._selected_filename: path = self._selected_path filename = self._selected_filename else: path = self._default_path filename = self._default_filename self._set_form_values(path, filename) else: self._gb.layout.display = 'none' self._cancel.layout.display = 'none' self._select.description = 'Change' self._selected_path = self._pathlist.value self._selected_filename = self._filename.value # self._default_path = self._selected_path # self._default_filename = self._selected_filename selected = os.path.join(self._selected_path, self._selected_filename) if os.path.isfile(selected): self._label.value = self._LBL_TEMPLATE.format( selected, 'orange') else: self._label.value = self._LBL_TEMPLATE.format( selected, 'green') def _on_cancel_click(self, b): '''Handler for when the cancel button is clicked''' self._gb.layout.display = 'none' self._cancel.layout.display = 'none' self._select.disabled = False def reset(self, path=None, filename=None): '''Reset the form to the default path and filename''' self._selected_path = '' self._selected_filename = '' self._label.value = self._LBL_TEMPLATE.format(self._LBL_NOFILE, 'black') if path is not None: self._default_path = path.rstrip(os.path.sep) if filename is not None: self._default_filename = filename self._set_form_values(self._default_path, self._default_filename) def refresh(self): '''Re-render the form''' self._set_form_values(self._pathlist.value, self._filename.value) @property def show_hidden(self): '''Get current number of rows''' return self._show_hidden @show_hidden.setter def show_hidden(self, hidden): '''Set number of rows''' self._show_hidden = hidden self.refresh() @property def rows(self): '''Get current number of rows''' return self._dircontent.rows @rows.setter def rows(self, rows): '''Set number of rows''' self._dircontent.rows = rows @property def default(self): '''Get the default value''' return os.path.join(self._default_path, self._default_filename) @property def default_path(self): '''Get the default_path value''' return self._default_path @default_path.setter def default_path(self, path): '''Set the default_path''' self._default_path = path.rstrip(os.path.sep) self._default = os.path.join(self._default_path, self._filename.value) self._set_form_values(self._default_path, self._filename.value) @property def default_filename(self): '''Get the default_filename value''' return self._default_filename @default_filename.setter def default_filename(self, filename): '''Set the default_filename''' self._default_filename = filename self._default = os.path.join(self._pathlist.value, self._default_filename) self._set_form_values(self._pathlist.value, self._default_filename) @property def selected(self): '''Get selected value''' return os.path.join(self._selected_path, self._selected_filename) @property def selected_path(self): '''Get selected_path value''' return self._selected_path @property def selected_filename(self): '''Get the selected_filename''' return self._selected_filename def __repr__(self): str_ = ("FileChooser(" "path='{0}', " "filename='{1}', " "show_hidden='{2}')").format(self._default_path, self._default_filename, self._show_hidden) return str_