def on_mouse_click(self, e): """Select a cluster by clicking on a spike.""" if 'Control' in e.modifiers: # Get mouse position in NDC. box_id, _ = self.canvas.stacked.box_map(e.pos) channel_id = np.nonzero(self.channel_y_ranks == box_id)[0] # Find the spike and cluster closest to the mouse. db = self.data_bounds # Get the information about the displayed spikes. wt = [(t, s, c, ch) for t, s, c, ch in self._waveform_times if channel_id in ch] if not wt: return # Get the time coordinate of the mouse position. mouse_pos = self.canvas.panzoom.window_to_ndc(e.pos) mouse_time = Range(NDC, db).apply(mouse_pos)[0][0] # Get the closest spike id. times, spike_ids, spike_clusters, channel_ids = zip(*wt) i = np.argmin(np.abs(np.array(times) - mouse_time)) # Raise the select_spike event. spike_id = spike_ids[i] cluster_id = spike_clusters[i] emit('select_spike', self, channel_id=channel_id, spike_id=spike_id, cluster_id=cluster_id) if 'Shift' in e.modifiers: # Get mouse position in NDC. box_id, _ = self.canvas.stacked.box_map(e.pos) channel_id = int(np.nonzero(self.channel_y_ranks == box_id)[0][0]) emit('select_channel', self, channel_id=channel_id, button=e.button)
def attach(self, gui): """Attach the view to the GUI. Perform the following: - Add the view to the GUI. - Update the view's attribute from the GUI state - Add the default view actions (auto_update, screenshot) - Bind the on_select() method to the select event raised by the supervisor. """ # Add shortcuts only for the first view of any given type. shortcuts = self.shortcuts if not gui.list_views(self.__class__) else None gui.add_view(self, position=self._default_position) self.gui = gui # Set the view state. self.set_state(gui.state.get_view_state(self)) self.actions = Actions( gui, name=self.name, view=self, default_shortcuts=shortcuts, default_snippets=self.default_snippets) # Freeze and unfreeze the view when selecting clusters. self.actions.add( self.toggle_auto_update, checkable=True, checked=self.auto_update, show_shortcut=False) self.actions.add(self.screenshot, show_shortcut=False) self.actions.add(self.close, show_shortcut=False) self.actions.separator() on_select = partial(self.on_select_threaded, gui=gui) connect(on_select, event='select') # Save the view state in the GUI state. @connect def on_close_view(view_, gui): if view_ != self: return logger.debug("Close view %s.", self.name) self._closed = True gui.remove_menu(self.name) unconnect(on_select) gui.state.update_view_state(self, self.state) self.canvas.close() gc.collect(0) @connect(sender=gui) def on_close(sender): gui.state.update_view_state(self, self.state) # HACK: Fix bug on macOS where docked OpenGL widgets were not displayed at startup. self._set_floating = AsyncCaller(delay=5) @self._set_floating.set def _set_floating(): self.dock.setFloating(False) emit('view_attached', self, gui)
def test_probe_view(qtbot, gui): n = 50 positions = staggered_positions(n) positions = positions.astype(np.int32) best_channels = lambda cluster_id: range(1, 9, 2) v = ProbeView(positions=positions, best_channels=best_channels, dead_channels=(3, 7, 12)) v.do_show_labels = False v.show() qtbot.waitForWindowShown(v.canvas) v.attach(gui) class Supervisor(object): pass v.toggle_show_labels(True) v.on_select(cluster_ids=[]) v.on_select(cluster_ids=[0]) v.on_select(cluster_ids=[0, 2, 3]) emit('select', Supervisor(), cluster_ids=[0, 2]) v.toggle_show_labels(False) _stop_and_close(qtbot, v)
def on_mouse_click(self, e): """Add a polygon point with ctrl+click.""" if 'Control' in e.modifiers: if e.button == 'Left': layout = getattr(self.canvas, 'layout', None) if hasattr(layout, 'box_map'): box, pos = layout.box_map(e.pos) # Only update the box for the first click, so that the box containing # the lasso is determined by the first click only. if self.box is None: self.box = box # Avoid clicks outside the active box (box of the first click). if box != self.box: return else: # pragma: no cover pos = self.canvas.window_to_ndc(e.pos) # Force the active box to be the box of the first click, not the box of the # current click. if layout: layout.active_box = self.box self.add(pos) # call update_lasso_visual emit("lasso_updated", self.canvas, self.polygon) else: self.clear() self.box = None
def on_mouse_click(self, e): """Select a feature dimension by clicking on a box in the feature view.""" b = e.button if 'Alt' in e.modifiers: # Get mouse position in NDC. (i, j), _ = self.canvas.grid.box_map(e.pos) dim = self.grid_dim[i][j] dim_x, dim_y = dim.split(',') dim = dim_x if b == 'Left' else dim_y other_dim = dim_y if b == 'Left' else dim_x if dim not in self.attributes: # When a regular (channel, PC) dimension is selected. channel_pc = self._get_channel_and_pc(dim) if channel_pc is None: return channel_id, pc = channel_pc logger.debug("Click on feature dim %s, channel id %s, PC %s.", dim, channel_id, pc) else: # When the selected dimension is an attribute, e.g. "time". pc = None # Take the channel id in the other dimension. channel_pc = self._get_channel_and_pc(other_dim) channel_id = channel_pc[0] if channel_pc is not None else None logger.debug("Click on feature dim %s.", dim) emit('select_feature', self, dim=dim, channel_id=channel_id, pc=pc)
def _clusters_selected(self, sender, obj, **kwargs): """When clusters are selected in the cluster view, register the action in the history stack, update the similarity view, and emit the global supervisor.select event unless update_views is False.""" if sender != self.cluster_view: return cluster_ids = obj['selected'] next_cluster = obj['next'] kwargs = obj.get('kwargs', {}) logger.debug("Clusters selected: %s (%s)", cluster_ids, next_cluster) self.task_logger.log(self.cluster_view, 'select', cluster_ids, output=obj) # Update the similarity view when the cluster view selection changes. self.similarity_view.reset(cluster_ids) self.similarity_view.set_selected_index_offset( len(self.selected_clusters)) # Emit supervisor.select event unless update_views is False. This happens after # a merge event, where the views should not be updated after the first cluster_view.select # event, but instead after the second similarity_view.select event. if kwargs.pop('update_views', True): emit('select', self, self.selected, **kwargs) if cluster_ids: self.cluster_view.scroll_to(cluster_ids[-1]) self.cluster_view.dock.set_status('clusters: %s' % ', '.join(map(str, cluster_ids)))
def test_trace_view(self): self.trace_view.actions.go_to_next_spike() self.trace_view.actions.go_to_previous_spike() self.trace_view.actions.toggle_highlighted_spikes(True) mouse_click(self.qtbot, self.trace_view.canvas, (100, 100), modifiers=('Control',)) mouse_click(self.qtbot, self.trace_view.canvas, (150, 100), modifiers=('Shift',)) emit('select_time', self, 0) self.trace_view.actions.next_color_scheme()
def on_cluster(sender, up): # NOTE: update the cluster meta of new clusters, depending on the values of the # ancestor clusters. In case of a conflict between the values of the old clusters, # the largest cluster wins and its value is set to its descendants. if up.added: self.cluster_meta.set_from_descendants( up.descendants, largest_old_cluster=up.largest_old_cluster) emit('cluster', self, up)
def on_lasso_updated(sender, polygon): if len(polygon) < 3: return pos = range_transform([self.data_bounds], [NDC], self.marker_positions) ind = self.canvas.lasso.in_polygon(pos) cluster_ids = self.all_cluster_ids[ind] emit("request_select", self, list(cluster_ids))
def _init_table(self, columns=None, value_names=None, data=None, sort=None): """Build the table.""" columns = columns or ['id'] value_names = value_names or columns data = data or [] b = self.builder b.set_body_src('index.html') if is_high_dpi(): # pragma: no cover b.add_style(''' /* This is for high-dpi displays. */ body { transform: scale(2); transform-origin: 0 0; overflow-y: scroll; /*overflow-x: hidden;*/ } input.filter { width: 50% !important; } ''') b.add_style(_color_styles()) self.data = data self.columns = columns self.value_names = value_names emit('pre_build', self) data_json = dumps(self.data) columns_json = dumps(self.columns) value_names_json = dumps(self.value_names) sort_json = dumps(sort) b.body += ''' <script> var data = %s; var options = { valueNames: %s, columns: %s, sort: %s, }; var table = new Table('table', options, data); </script> ''' % (data_json, value_names_json, columns_json, sort_json) self.build(lambda html: emit('ready', self)) connect(event='select', sender=self, func=lambda *args: self.update(), last=True) connect(event='ready', sender=self, func=lambda *args: self._set_ready())
def add_visual(self, visual, **kwargs): """Add a visual to the canvas and build its OpenGL program using the attached interacts. We can't build the visual's program before, because we need the canvas' transforms first. Parameters ---------- visual : Visual clearable : True Whether the visual should be deleted when calling `canvas.clear()`. exclude_origins : list-like List of interact instances that should not apply to that visual. For example, use to add a visual outside of the subplots, or with no support for pan and zoom. key : str An optional key to identify a visual """ if self.has_visual(visual): logger.log(5, "This visual has already been added.") return visual.canvas = self # This is the list of origins (mostly, interacts and layouts) that should be ignored # when adding this visual. For example, an AxesVisual would keep the PanZoom interact, # but not the Grid layout. exclude_origins = kwargs.pop('exclude_origins', ()) # Retrieve the visual's GLSL inserter. v_inserter = visual.inserter # Add the canvas' GPU transforms. v_inserter.add_gpu_transforms(self.gpu_transforms) # Also, add the canvas' inserter. The snippets that should be ignored will be excluded # in insert_into_shaders() below. v_inserter += self.inserter # Now, we insert the transforms GLSL into the shaders. vs, fs = visual.vertex_shader, visual.fragment_shader vs, fs = v_inserter.insert_into_shaders( vs, fs, exclude_origins=exclude_origins) # Geometry shader, if there is one. gs = getattr(visual, 'geometry_shader', None) if gs: gs = gloo.GeometryShader(gs, visual.geometry_count, visual.geometry_in, visual.geometry_out) # Finally, we create the visual's program. visual.program = LazyProgram(vs, fs, gs) logger.log(5, "Vertex shader: %s", vs) logger.log(5, "Fragment shader: %s", fs) # Initialize the size. visual.on_resize(self.size().width(), self.size().height()) # Register the visual in the list of visuals in the canvas. self.visuals.append(Bunch(visual=visual, **kwargs)) emit('visual_added', self, visual) return visual
def on_mouse_click(self, e): """Select a cluster by clicking in the raster plot.""" b = e.button if 'Control' in e.modifiers: # Get mouse position in NDC. cluster_idx, _ = self.canvas.stacked.box_map(e.pos) cluster_id = self.all_cluster_ids[cluster_idx] logger.debug("Click on cluster %d with button %s.", cluster_id, b) emit('cluster_click', self, cluster_id, button=b)
def emitJS(self, name, arg_json): logger.log(5, "Emit from Python %s %s.", name, arg_json) args = str(name), self._parent, json.loads(str(arg_json)) # NOTE: debounce some events but not other events coming from JS. # This is typically used for select events of table widgets. if name in self._debounce_events: self._debouncer.submit(emit, *args) else: emit(*args)
def on_mouse_click(self, e): """Select a cluster by clicking on its template waveform.""" b = e.button if 'Control' in e.modifiers or 'Shift' in e.modifiers: # Get mouse position in NDC. (channel_idx, cluster_rel), _ = self.canvas.grid.box_map(e.pos) cluster_id = self.all_cluster_ids[cluster_rel] logger.debug("Click on cluster %d with button %s.", cluster_id, b) emit('cluster_click', self, cluster_id, button=b, modifiers=e.modifiers)
def attach(self, gui): """Attach to the GUI.""" # Make sure the selected field in cluster and similarity views are saved in the local # supervisor state, as this information is dataset-dependent. gui.state.add_local_keys(['ClusterView.selected']) # Create the cluster view and similarity view. self._create_views(gui=gui, sort=gui.state.get('ClusterView', {}).get('current_sort', None)) # Create the TaskLogger. self.task_logger = TaskLogger( cluster_view=self.cluster_view, similarity_view=self.similarity_view, supervisor=self, ) connect(self._save_gui_state, event='close', sender=gui) gui.add_view(self.cluster_view, position='left', closable=False) gui.add_view(self.similarity_view, position='left', closable=False) # Create all supervisor actions (edit and view menu). self.action_creator.attach(gui) self.actions = self.action_creator.edit_actions # clustering actions self.select_actions = self.action_creator.select_actions self.view_actions = gui.view_actions emit('attach_gui', self) # Call supervisor.save() when the save/ctrl+s action is triggered in the GUI. @connect(sender=gui) def on_request_save(sender): self.save() # Set the debouncer. self._busy = {} self._is_busy = False # Collect all busy events from the views, and sets the GUI as busy # if at least one view is busy. @connect def on_is_busy(sender, is_busy): self._busy[sender] = is_busy self._set_busy(any(self._busy.values())) @connect(sender=gui) def on_close(e): unconnect(on_is_busy, self) @connect(sender=self.cluster_view) def on_ready(sender): """Select the clusters from the cluster view state.""" selected = gui.state.get('ClusterView', {}).get('selected', []) if selected: # pragma: no cover self.cluster_view.select(selected)
def pan(self, value): """Pan translation.""" assert len(value) == 2 old = tuple(self.pan) self._pan[:] = value self._constrain_pan() new = tuple(self.pan) if new != old: emit('pan', self, new) self.update()
def add_view(self, view, position=None, closable=True, floatable=True, floating=None): """Add a dock widget to the main window. Parameters ---------- view : View position : str Relative position where to add the view (left, right, top, bottom). closable : boolean Whether the view can be closed by the user. floatable : boolean Whether the view can be detached from the main GUI. floating : boolean Whether the view should be added in floating mode or not. """ logger.debug("Add view %s to GUI.", view.__class__.__name__) name = self._set_view_name(view) self._views.append(view) self._view_class_indices[view.__class__] += 1 # Get the Qt canvas for matplotlib/OpenGL views. widget = _try_get_matplotlib_canvas(view) widget = _try_get_opengl_canvas(widget) dock_widget = _create_dock_widget(widget, name, closable=closable, floatable=floatable) self.addDockWidget(_get_dock_position(position), dock_widget, Qt.Horizontal) if floating is not None: dock_widget.setFloating(floating) dock_widget.view = view view.dock_widget = dock_widget # Emit the close_view event when the dock widget is closed. @connect(sender=dock_widget) def on_close_dock_widget(sender): self._views.remove(view) emit('close_view', self, view) dock_widget.show() emit('add_view', self, view) logger.log(5, "Add %s to GUI.", name) return dock_widget
def on_mouse_click(self, e): """Select a cluster by clicking in the raster plot.""" if 'Control' not in e.modifiers: return b = e.button # Get mouse position in NDC. cluster_idx, _ = self.canvas.stacked.box_map(e.pos) cluster_id = self.all_cluster_ids[cluster_idx] logger.debug("Click on cluster %d with button %s.", cluster_id, b) if 'Shift' in e.modifiers: emit('select_more', self, [cluster_id]) else: emit('request_select', self, [cluster_id])
def on_mouse_click(self, e): """Select a cluster by clicking on its template waveform.""" if 'Control' not in e.modifiers: return b = e.button # Get mouse position in NDC. (channel_idx, cluster_rel), _ = self.canvas.grid.box_map(e.pos) cluster_id = self.all_cluster_ids[cluster_rel] logger.debug("Click on cluster %d with button %s.", cluster_id, b) if 'Shift' in e.modifiers: emit('select_more', self, [cluster_id]) else: emit('request_select', self, [cluster_id])
def finished(): # When the task has finished in the thread pool, we recover all program # updates of the view, and we execute them on the GPU. if isinstance(self.canvas, PlotCanvas): self.canvas.set_lazy(False) # We go through all collected OpenGL updates. for program, name, data in self.canvas.iter_update_queue(): # We update data buffers in OpenGL programs. program[name] = data # Finally, we update the canvas. self.canvas.update() emit('is_busy', self, False) self._lock = None
def _similar_selected(self, sender, obj): """When clusters are selected in the similarity view, register the action in the history stack, and emit the global supervisor.select event.""" if sender != self.similarity_view: return similar = obj['selected'] next_similar = obj['next'] kwargs = obj.get('kwargs', {}) logger.debug("Similar clusters selected: %s (%s)", similar, next_similar) self.task_logger.log(self.similarity_view, 'select', similar, output=obj) emit('select', self, self.selected, **kwargs) if similar: self.similarity_view.scroll_to(similar[-1])
def on_mouse_click(self, e): """Select a cluster by clicking on its template waveform.""" if 'Control' in e.modifiers: return b = e.button pos = self.canvas.window_to_ndc(e.pos) marker_pos = range_transform([self.data_bounds], [NDC], self.marker_positions) cluster_rel = np.argmin(((marker_pos - pos)**2).sum(axis=1)) cluster_id = self.all_cluster_ids[cluster_rel] logger.debug("Click on cluster %d with button %s.", cluster_id, b) if 'Shift' in e.modifiers: emit('select_more', self, [cluster_id]) else: emit('request_select', self, [cluster_id])
def on_mouse_click(self, e): """Select a channel by clicking on a box in the waveform view.""" b = e.button nums = tuple('%d' % i for i in range(10)) if 'Control' in e.modifiers or e.key in nums: key = int(e.key) if e.key in nums else None # Get mouse position in NDC. channel_idx, _ = self.canvas.boxed.box_map(e.pos) channel_id = self.channel_ids[channel_idx] logger.debug("Click on channel_id %d with key %s and button %s.", channel_id, key, b) emit('select_channel', self, channel_id=channel_id, key=key, button=b)
def test_manual_clustering_view_2(qtbot, gui): v = MyView() v.canvas.show() # qtbot.addWidget(v.canvas) v.attach(gui) class Supervisor(object): pass emit('select', Supervisor(), cluster_ids=[0, 1]) qtbot.wait(200) # qtbot.stop() v.canvas.close() v.dock_widget.close() qtbot.wait(100)
def zoom(self, value): """Zoom level.""" if isinstance(value, (int, float)): value = (value, value) assert len(value) == 2 old = tuple(self.zoom) self._zoom = np.clip(value, self._zmin, self._zmax) # Constrain bounding box. self._constrain_pan() self._constrain_zoom() new = tuple(self.zoom) if new != old: emit('zoom', self, new) self.update()
def set(self, field, clusters, value, add_to_stack=True): """Set the value of one of several clusters. Parameters ---------- field : str The field to set. clusters : list The list of cluster ids to change. value : str The new metadata value for the given clusters. add_to_stack : boolean Whether this metadata change should be recorded in the undo stack. Returns ------- up : UpdateInfo instance """ # Add the field if it doesn't exist. if field not in self._fields: self.add_field(field) assert field in self._fields clusters = _as_list(clusters) for cluster in clusters: if cluster not in self._data: self._data[cluster] = {} self._data[cluster][field] = value up = UpdateInfo( description='metadata_' + field, metadata_changed=clusters, metadata_value=value, ) undo_state = emit('request_undo_state', self, up) if add_to_stack: self._undo_stack.add((clusters, field, value, up, undo_state)) emit('cluster', self, up) return up
def save(self): """Save the manual clustering back to disk. This method emits the `save_clustering(spike_clusters, groups, *labels)` event. It is up to the caller to react to this event and save the data to disk. """ spike_clusters = self.clustering.spike_clusters groups = { c: self.cluster_meta.get('group', c) or 'unsorted' for c in self.clustering.cluster_ids} # List of tuples (field_name, dictionary). labels = [ (field, self.get_labels(field)) for field in self.cluster_meta.fields if field not in ('next_cluster')] emit('save_clustering', self, spike_clusters, groups, *labels) # Cache the spikes_per_cluster array. self._save_spikes_per_cluster() self._is_dirty = False
def _reset_table(self, data=None, columns=(), sort=None): """Recreate the table with specified columns, data, and sort.""" emit(self._view_name + '_init', self) # Ensure 'id' is the first column. if 'id' in columns: columns.remove('id') columns = ['id'] + list(columns) # Add required columns if needed. for col in self._required_columns: if col not in columns: columns += [col] assert col in columns assert columns[0] == 'id' # Allow to have <tr data_group="good"> etc. which allows for CSS styling. value_names = columns + [{'data': ['group']}] # Default sort. sort = sort or ('n_spikes', 'desc') self._init_table(columns=columns, value_names=value_names, data=data, sort=sort)
def test_gui_dock_widget_1(qtbot, gui): gui.show() v = _create_canvas() gui.add_view(v) def callback(checked): pass # Add 2 buttons. v.dock.add_button(name='b1', text='hello world', callback=callback) @v.dock.add_button(name='b2', checkable=True, checked=True, icon='f15c', event='button_clicked') def callback_1(checked): pass # Add a checkbox. @v.dock.add_checkbox(name='c1', text='checkbox', checked=True) def callback_2(checked): pass # Make sure the second button reacts to events. b2 = v.dock.get_widget('b2') assert b2.isChecked() emit('button_clicked', v, False) assert not b2.isChecked() # Set and check the title bar status text. v.dock.set_status("this is a status") assert v.dock.status == 'this is a status' # Set and check the title bar status text. v.dock.set_status("---very long---" + "------" * 10) assert len(v.dock.status) <= v.dock.max_status_length + 5 b2.click() v.dock.get_widget('b1').click() v.dock.get_widget('c1').click()
def redo(self): """Redo the next metadata change. Returns ------- up : UpdateInfo instance """ args = self._undo_stack.forward() if args is None: return clusters, field, value, up, undo_state = args self.set(field, clusters, value, add_to_stack=False) # Return the UpdateInfo instance of the redo action. up.history = 'redo' emit('cluster', self, up) return up