class QuadSelection(cytoflow.views.ScatterplotView): """Plots, and lets the user interact with, a quadrant gate. Attributes ---------- op : Instance(Range2DOp) The instance of Range2DOp that we're viewing / editing huefacet : Str The conditioning variable to plot multiple colors subset : Str The string passed to `Experiment.query()` to subset the data before plotting interactive : Bool is this view interactive? Ie, can the user set the threshold with a mouse click? Notes ----- We inherit `xfacet` and `yfacet` from `cytoflow.views.ScatterplotView`, but they must both be unset! Examples -------- In an IPython notebook with `%matplotlib notebook` >>> q = flow.QuadOp(name = "Quad", ... xchannel = "V2-A", ... ychannel = "Y2-A")) >>> qv = q.default_view() >>> qv.interactive = True >>> qv.plot(ex2) """ id = Constant('edu.mit.synbio.cytoflow.views.quad') friendly_id = Constant("Quadrant Selection") op = Instance(IOperation) name = DelegatesTo('op') xchannel = DelegatesTo('op') ychannel = DelegatesTo('op') interactive = Bool(False, transient = True) # internal state. _ax = Any(transient = True) _hline = Instance(Line2D, transient = True) _vline = Instance(Line2D, transient = True) _cursor = Instance(Cursor, transient = True) def plot(self, experiment, **kwargs): """Plot the underlying scatterplot and then plot the selection on top of it.""" if not experiment: raise util.CytoflowOpError("No experiment specified") if not experiment: raise util.CytoflowViewError("No experiment specified") if self.xfacet: raise util.CytoflowViewError("RangeSelection.xfacet must be empty or `Undefined`") if self.yfacet: raise util.CytoflowViewError("RangeSelection.yfacet must be empty or `Undefined`") super(QuadSelection, self).plot(experiment, **kwargs) self._ax = plt.gca() self._draw_lines() self._interactive() @on_trait_change('op.xthreshold, op.ythreshold', post_init = True) def _draw_lines(self): if not self._ax: return if self._hline and self._hline in self._ax.lines: self._hline.remove() if self._vline and self._vline in self._ax.lines: self._vline.remove() if self.op.xthreshold and self.op.ythreshold: self._hline = plt.axhline(self.op.ythreshold, linewidth = 3, color = 'blue') self._vline = plt.axvline(self.op.xthreshold, linewidth = 3, color = 'blue') plt.draw_if_interactive() @on_trait_change('interactive', post_init = True) def _interactive(self): if self._ax and self.interactive: self._cursor = Cursor(self._ax, horizOn = True, vertOn = True, color = 'blue') self._cursor.connect_event('button_press_event', self._onclick) elif self._cursor: self._cursor.disconnect_events() self._cursor = None def _onclick(self, event): """Update the threshold location""" self.op.xthreshold = event.xdata self.op.ythreshold = event.ydata
class CrossSection(object): """ Class to manage the axes, artists and properties associated with showing a 2D image, a cross-hair cursor and two parasite axes which provide horizontal and vertical cross sections of image. You will likely need to call `CrossSection.init_artists(init_image)` after creating this object. Parameters ---------- fig : matplotlib.figure.Figure The figure object to build the class on, will clear current contents cmap : str, colormap, or None color map to use. Defaults to gray norm : Normalize or None Normalization function to use limit_func : callable, optional function that takes in the image and returns clim values auto_redraw : bool, optional interpolation : str, optional Interpolation method to use. List of valid options can be found in CrossSection2DView.interpolation """ def __init__(self, fig, cmap=None, norm=None, limit_func=None, auto_redraw=True, interpolation=None): self._cursor_position_cbs = [] self._interpolation = interpolation # used to determine if setting properties should force a re-draw self._auto_redraw = auto_redraw # clean defaults if limit_func is None: limit_func = lambda image: (image.min(), image.max()) if cmap is None: cmap = 'gray' # stash the color map self._cmap = cmap # let norm pass through as None, mpl defaults to linear which is fine if norm is None: norm = Normalize() self._norm = norm # save a copy of the limit function, we will need it later self._limit_func = limit_func # this is used by the widget logic self._active = True self._dirty = True self._cb_dirty = True # work on setting up the mpl axes self._fig = fig # blow away what ever is currently on the figure fig.clf() # Configure the figure in our own image # # +----------------------+ # | H cross section | # +----------------------+ # +---+ +----------------------+ # | V | | | # | | | | # | x | | | # | s | | Main Axes | # | e | | | # | c | | | # | t | | | # | i | | | # | o | | | # | n | | | # +---+ +----------------------+ # make the main axes self._im_ax = fig.add_subplot(1, 1, 1) self._im_ax.set_aspect('equal') self._im_ax.xaxis.set_major_locator(NullLocator()) self._im_ax.yaxis.set_major_locator(NullLocator()) self._imdata = None self._im = self._im_ax.imshow([[]], cmap=self._cmap, norm=self._norm, interpolation=self._interpolation, aspect='equal', vmin=0, vmax=1) # make it dividable divider = make_axes_locatable(self._im_ax) # set up all the other axes # (set up the horizontal and vertical cuts) self._ax_h = divider.append_axes('top', .5, pad=0.1, sharex=self._im_ax) self._ax_h.yaxis.set_major_locator(LinearLocator(numticks=2)) self._ax_v = divider.append_axes('left', .5, pad=0.1, sharey=self._im_ax) self._ax_v.xaxis.set_major_locator(LinearLocator(numticks=2)) self._ax_cb = divider.append_axes('right', .2, pad=.5) # add the color bar self._cb = fig.colorbar(self._im, cax=self._ax_cb) # add the cursor place holder self._cur = None # turn off auto-scale for the horizontal cut self._ax_h.autoscale(enable=False) # turn off auto-scale scale for the vertical cut self._ax_v.autoscale(enable=False) # create line artists self._ln_v, = self._ax_v.plot([], [], 'k-', animated=True, visible=False) self._ln_h, = self._ax_h.plot([], [], 'k-', animated=True, visible=False) # backgrounds for blitting self._ax_v_bk = None self._ax_h_bk = None # stash last-drawn row/col to skip if possible self._row = None self._col = None # make attributes for callback ids self._move_cid = None self._click_cid = None self._clear_cid = None def add_cursor_position_cb(self, callback): """ Add a callback for the cursor position in the main axes Parameters ---------- callback : callable(cc, rr) Function that gets called when the cursor position moves to a new row or column on main axes """ self._cursor_position_cbs.append(callback) # set up the call back for the updating the side axes def _move_cb(self, event): if not self._active: return if event is None: x = self._col y = self._row self._col = None self._row = None else: # short circuit on other axes if event.inaxes is not self._im_ax: return x, y = event.xdata, event.ydata numrows, numcols = self._imdata.shape if x is not None and y is not None: self._ln_h.set_visible(True) self._ln_v.set_visible(True) col = int(x + 0.5) row = int(y + 0.5) if row != self._row or col != self._col: if 0 <= col < numcols and 0 <= row < numrows: self._col = col self._row = row for cb in self._cursor_position_cbs: cb(col, row) for data, ax, bkg, art, set_fun in zip( (self._imdata[row, :], self._imdata[:, col]), (self._ax_h, self._ax_v), (self._ax_h_bk, self._ax_v_bk), (self._ln_h, self._ln_v), (self._ln_h.set_ydata, self._ln_v.set_xdata)): self._fig.canvas.restore_region(bkg) set_fun(data) ax.draw_artist(art) self._fig.canvas.blit(ax.bbox) def _click_cb(self, event): if event.inaxes is not self._im_ax: return self.active = not self.active if self.active: self._cur.onmove(event) self._move_cb(event) @auto_redraw def _connect_callbacks(self): """ Connects all of the callbacks for the motion and click events """ self._disconnect_callbacks() self._cur = Cursor(self._im_ax, useblit=True, color='red', linewidth=2) self._move_cid = self._fig.canvas.mpl_connect('motion_notify_event', self._move_cb) self._click_cid = self._fig.canvas.mpl_connect('button_press_event', self._click_cb) self._clear_cid = self._fig.canvas.mpl_connect('draw_event', self._clear) self._fig.tight_layout() self._fig.canvas.draw_idle() def _disconnect_callbacks(self): """ Disconnects all of the callbacks """ if self._fig.canvas is None: # no canvas -> can't do anything about the call backs which # should not exist self._move_cid = None self._clear_cid = None self._click_cid = None return for atr in ('_move_cid', '_clear_cid', '_click_cid'): cid = getattr(self, atr, None) if cid is not None: self._fig.canvas.mpl_disconnect(cid) setattr(self, atr, None) # clean up the cursor if self._cur is not None: self._cur.disconnect_events() del self._cur self._cur = None @auto_redraw def _init_artists(self, init_image): """ Update the CrossSection with a new base-image. This function takes care of setting up all of the details about the image size in the limits/artist extent of the image and the secondary data in the cross-section parasite plots. Parameters ---------- init_image : ndarray An image to serve as the new 'base' image. """ im_shape = init_image.shape # first deal with the image axis # update the image, `update_artists` takes care of # updating the actual artist self._imdata = init_image # update the extent of the image artist self._im.set_extent([-0.5, im_shape[1] + .5, im_shape[0] + .5, -0.5]) # update the limits of the image axes to match the exent self._im_ax.set_xlim([-.05, im_shape[1] + .5]) self._im_ax.set_ylim([im_shape[0] + .5, -0.5]) # update the format coords printer numrows, numcols = im_shape # note, this is a closure over numrows and numcols def format_coord(x, y): # adjust xy -> col, row col = int(x + 0.5) row = int(y + 0.5) # make sure the point falls in the array if col >= 0 and col < numcols and row >= 0 and row < numrows: # if it does, grab the value z = self._imdata[row, col] return "X: {x:d} Y: {y:d} I: {i:.2f}".format(x=col, y=row, i=z) else: return "X: {x:d} Y: {y:d}".format(x=col, y=row) # replace the current format_coord function self._im_ax.format_coord = format_coord # net deal with the parasite axes and artist self._ln_v.set_data(np.zeros(im_shape[0]), np.arange(im_shape[0])) self._ax_v.set_ylim([0, im_shape[0]]) self._ln_h.set_data(np.arange(im_shape[1]), np.zeros(im_shape[1])) self._ax_h.set_xlim([0, im_shape[1]]) # if we have a cavas, then connect/set up junk if self._fig.canvas is not None: self._connect_callbacks() # mark as dirty self._dirty = True def _clear(self, event): self._ax_v_bk = self._fig.canvas.copy_from_bbox(self._ax_v.bbox) self._ax_h_bk = self._fig.canvas.copy_from_bbox(self._ax_h.bbox) self._ln_h.set_visible(False) self._ln_v.set_visible(False) # this involves reaching in and touching the guts of the # cursor widget. The problem is that the mpl widget # skips updating it's saved background if the widget is inactive if self._cur: self._cur.background = self._cur.canvas.copy_from_bbox( self._cur.canvas.figure.bbox) @property def interpolation(self): return self._interpolation @property def active(self): return self._active @active.setter def active(self, val): self._active = val self._cur.active = val @auto_redraw def update_interpolation(self, interpolation): """ Set the interpolation method """ self._dirty = True self._im.set_interpolation(interpolation) @auto_redraw def update_cmap(self, cmap): """ Set the color map used """ # TODO: this should stash new value, not apply it self._cmap = cmap self._dirty = True @auto_redraw def update_image(self, image): """ Set the image data The input data does not necessarily have to be the same shape as the original image """ image = np.asarray(image) if self._imdata is None or self._imdata.shape != image.shape: self._init_artists(image) self._imdata = image self._move_cb(None) self._dirty = True @auto_redraw def update_norm(self, norm): """ Update the way that matplotlib normalizes the image """ self._norm = norm self._dirty = True self._cb_dirty = True @auto_redraw def update_limit_func(self, limit_func): """ Set the function to use to determine the color scale """ # set the new function to use for computing the color limits self._limit_func = limit_func self._dirty = True def _update_artists(self): """ Updates the figure by re-drawing """ # if the figure is not dirty, short-circuit if not (self._dirty or self._cb_dirty): return # this is a tuple which is the max/min used in the color mapping. # these values are also used to set the limits on the value # axes of the parasite axes # value_limits vlim = self._limit_func(self._imdata) # set the color bar limits self._im.set_clim(vlim) self._norm.vmin, self._norm.vmax = vlim # set the cross section axes limits self._ax_v.set_xlim(*vlim[::-1]) self._ax_h.set_ylim(*vlim) # set the imshow data self._im.set_cmap(self._cmap) self._im.set_norm(self._norm) if self._imdata is None: return self._im.set_data(self._imdata) # TODO if cb_dirty, remake the colorbar, I think this is # why changing the norm does not play well self._dirty = False self._cb_dirty = False def _draw(self): self._fig.canvas.draw_idle() @auto_redraw def autoscale_horizontal(self, enable): self._ax_h.autoscale(enable=enable) @auto_redraw def autoscale_vertical(self, enable): self._ax_v.autoscale(enable=False)
class ThresholdSelection(cytoflow.views.HistogramView): """ Plots, and lets the user interact with, a threshold on the X axis. TODO - beautify! Attributes ---------- op : Instance(ThresholdOp) the ThresholdOp we're working on. huefacet : Str The conditioning variable to show multiple colors on this plot subset : Str the string passed to Experiment.subset() defining the subset we plot interactive : Bool is this view interactive? Notes ----- We inherit `xfacet` and `yfacet` from `cytoflow.views.HistogramView`, but they must both be unset! Examples -------- In an IPython notebook with `%matplotlib notebook` >>> t = flow.ThresholdOp(name = "Threshold", ... channel = "Y2-A") >>> tv = t.default_view() >>> tv.plot(ex2) >>> tv.interactive = True >>> # .... draw a threshold on the plot >>> ex3 = thresh.apply(ex2) """ id = Constant('edu.mit.synbio.cytoflow.views.threshold') friendly_id = Constant("Threshold Selection") op = Instance(IOperation) name = DelegatesTo('op') channel = DelegatesTo('op') interactive = Bool(False, transient = True) # internal state _ax = Any(transient = True) _line = Instance(Line2D, transient = True) _cursor = Instance(Cursor, transient = True) def plot(self, experiment, **kwargs): """Plot the histogram and then plot the threshold on top of it.""" if not experiment: raise util.CytoflowViewError("No experiment specified") if self.xfacet: raise util.CytoflowViewError("ThresholdSelection.xfacet must be empty") if self.yfacet: raise util.CytoflowViewError("ThresholdSelection.yfacet must be empty") super(ThresholdSelection, self).plot(experiment, **kwargs) self._ax = plt.gca() self._draw_threshold() self._interactive() @on_trait_change('op.threshold', post_init = True) def _draw_threshold(self): if not self._ax or not self.op.threshold: return if self._line: # when used in the GUI, _draw_threshold gets called *twice* without # the plot being updated inbetween: and then the line can't be # removed from the plot, because it was never added. so check # explicitly first. this is likely to be an issue in other # interactive plots, too. if self._line and self._line in self._ax.lines: self._line.remove() self._line = None if self.op.threshold: self._line = plt.axvline(self.op.threshold, linewidth=3, color='blue') plt.draw_if_interactive() @on_trait_change('interactive', post_init = True) def _interactive(self): if self._ax and self.interactive: self._cursor = Cursor(self._ax, horizOn=False, vertOn=True, color='blue') self._cursor.connect_event('button_press_event', self._onclick) elif self._cursor: self._cursor.disconnect_events() self._cursor = None def _onclick(self, event): """Update the threshold location""" self.op.threshold = event.xdata
class PolygonSelection(HasStrictTraits): """Plots, and lets the user interact with, a 2D selection. Attributes ---------- polygon : Instance(numpy.ndarray) The polygon vertices view : Instance(IView) the IView that this view is wrapping. I suggest that if it's another ISelectionView, that its `interactive` property remain False. >.> interactive : Bool is this view interactive? Ie, can the user set the polygon verticies with mouse clicks? """ id = "edu.mit.synbio.cytoflow.views.polygon" friendly_id = "Polygon Selection" view = Instance(IView, transient = True) interactive = Bool(False, transient = True) vertices = List((Float, Float)) # internal state. _cursor = Instance(Cursor, transient = True) _path = Instance(mpl.path.Path, transient = True) _patch = Instance(mpl.patches.PathPatch, transient = True) _line = Instance(mpl.lines.Line2D, transient = True) _drawing = Bool(transient = True) def plot(self, experiment, **kwargs): """Plot self.view, and then plot the selection on top of it.""" self.view.plot(experiment, **kwargs) self._draw_poly() def is_valid(self, experiment): """If the decorated view is valid, we are too.""" return self.view.is_valid(experiment) @on_trait_change('vertices') def _draw_poly(self): ca = plt.gca() if self._patch and self._patch in ca.patches: self._patch.remove() if self._drawing or not self.vertices or len(self.vertices) < 3 \ or any([len(x) != 2 for x in self.vertices]): return patch_vert = np.concatenate((np.array(self.vertices), np.array((0,0), ndmin = 2))) self._patch = \ mpl.patches.PathPatch(mpl.path.Path(patch_vert, closed = True), edgecolor="black", linewidth = 1.5, fill = False) ca.add_patch(self._patch) plt.gcf().canvas.draw() @on_trait_change('interactive') def _interactive(self): if self.interactive: ax = plt.gca() self._cursor = Cursor(ax, horizOn = False, vertOn = False) self._cursor.connect_event('button_press_event', self._onclick) self._cursor.connect_event('motion_notify_event', self._onmove) else: self._cursor.disconnect_events() self._cursor = None def _onclick(self, event): """Update selection traits""" if(self._cursor.ignore(event)): return if event.dblclick: self._drawing = False self.vertices = map(tuple, self._path.vertices) self._path = None return ca = plt.gca() self._drawing = True if self._patch and self._patch in ca.patches: self._patch.remove() if self._path: vertices = np.concatenate((self._path.vertices, np.array((event.xdata, event.ydata), ndmin = 2))) else: vertices = np.array((event.xdata, event.ydata), ndmin = 2) self._path = mpl.path.Path(vertices, closed = False) self._patch = mpl.patches.PathPatch(self._path, edgecolor = "black", fill = False) ca.add_patch(self._patch) plt.gcf().canvas.draw() def _onmove(self, event): if(self._cursor.ignore(event) or not self._drawing or not self._path or self._path.vertices.shape[0] == 0 or not event.xdata or not event.ydata): return ca = plt.gca() if not ca: return if self._line and self._line in ca.lines: self._line.remove() xdata = [self._path.vertices[-1, 0], event.xdata] ydata = [self._path.vertices[-1, 1], event.ydata] self._line = mpl.lines.Line2D(xdata, ydata, linewidth = 1, color = "black") ca.add_line(self._line) plt.gcf().canvas.draw()
class MaskEditor(ToolWindow, DoubleFileChooserDialog): def __init__(self, *args, **kwargs): self.mask = None self._undo_stack = [] self._im = None self._selector = None self._cursor = None self.exposureloader = None self.plot2d = None ToolWindow.__init__(self, *args, **kwargs) DoubleFileChooserDialog.__init__( self, self.widget, 'Open mask file...', 'Save mask file...', [('Mask files', '*.mat'), ('All files', '*')], self.instrument.config['path']['directories']['mask'], os.path.abspath(self.instrument.config['path']['directories']['mask']), ) def init_gui(self, *args, **kwargs): self.exposureloader = ExposureLoader(self.instrument) self.builder.get_object('loadexposure_expander').add(self.exposureloader) self.exposureloader.connect('open', self.on_loadexposure) self.plot2d = PlotImageWidget() self.builder.get_object('plotbox').pack_start(self.plot2d.widget, True, True, 0) self.builder.get_object('toolbar').set_sensitive(False) def on_loadexposure(self, exposureloader: ExposureLoader, im: Exposure): if self.mask is None: self.mask = im.mask self._im = im self.plot2d.set_image(im.intensity) self.plot2d.set_mask(self.mask) self.builder.get_object('toolbar').set_sensitive(True) def on_new(self, button): if self._im is None or self.mask is None: return False self.mask = np.ones_like(self.mask) self.plot2d.set_mask(self.mask) self.set_last_filename(None) def on_open(self, button): filename = self.get_open_filename() if filename is not None: mask = loadmat(filename) self.mask = mask[[k for k in mask.keys() if not k.startswith('__')][0]] self.plot2d.set_mask(self.mask) def on_save(self, button): filename = self.get_last_filename() if filename is None: return self.on_saveas(button) maskname = os.path.splitext(os.path.split(filename)[1])[0] savemat(filename, {maskname: self.mask}) def on_saveas(self, button): filename = self.get_save_filename(None) if filename is not None: self.on_save(button) def suggest_filename(self): return 'mask_dist_{0.year:d}{0.month:02d}{0.day:02d}.mat'.format(datetime.date.today()) def on_selectcircle_toggled(self, button): if button.get_active(): self.set_sensitive(False, 'Ellipse selection not ready', ['new_button', 'save_button', 'saveas_button', 'open_button', 'undo_button', 'selectrectangle_button', 'selectpolygon_button', 'pixelhunting_button', 'loadexposure_expander', 'close_button', self.plot2d.toolbar, self.plot2d.settings_expander]) while self.plot2d.toolbar.mode != '': # turn off zoom, pan, etc. modes. self.plot2d.toolbar.zoom() self._selector = EllipseSelector(self.plot2d.axis, self.on_ellipse_selected, rectprops={'facecolor': 'white', 'edgecolor': 'none', 'alpha': 0.7, 'fill': True, 'zorder': 10}, button=[1, ], interactive=False, lineprops={'zorder': 10}) self._selector.state.add('square') self._selector.state.add('center') else: assert isinstance(self._selector, EllipseSelector) self._selector.set_active(False) self._selector.set_visible(False) self._selector = None self.plot2d.replot(keepzoom=False) self.set_sensitive(True) def on_ellipse_selected(self, pos1, pos2): # pos1 and pos2 are mouse button press and release events, with xdata and ydata carrying # the two opposite corners of the bounding box of the circle. These are NOT the exact # button presses and releases! row = np.arange(self.mask.shape[0])[:, np.newaxis] column = np.arange(self.mask.shape[1])[np.newaxis, :] row0 = 0.5 * (pos1.ydata + pos2.ydata) col0 = 0.5 * (pos1.xdata + pos2.xdata) r2 = ((pos2.xdata - pos1.xdata) ** 2 + (pos2.ydata - pos1.ydata) ** 2) / 8 tobemasked = (row - row0) ** 2 + (column - col0) ** 2 <= r2 self._undo_stack.append(self.mask) if self.builder.get_object('mask_button').get_active(): self.mask &= ~tobemasked elif self.builder.get_object('unmask_button').get_active(): self.mask |= tobemasked elif self.builder.get_object('invertmask_button').get_active(): self.mask[tobemasked] = ~self.mask[tobemasked] else: pass self.builder.get_object('selectcircle_button').set_active(False) self.plot2d.set_mask(self.mask) def on_selectrectangle_toggled(self, button): if button.get_active(): self.set_sensitive(False, 'Rectangle selection not ready', ['new_button', 'save_button', 'saveas_button', 'open_button', 'undo_button', 'selectcircle_button', 'selectpolygon_button', 'pixelhunting_button', 'loadexposure_expander', 'close_button', self.plot2d.toolbar, self.plot2d.settings_expander]) while self.plot2d.toolbar.mode != '': # turn off zoom, pan, etc. modes. self.plot2d.toolbar.zoom() self._selector = RectangleSelector(self.plot2d.axis, self.on_rectangle_selected, rectprops={'facecolor': 'white', 'edgecolor': 'none', 'alpha': 0.7, 'fill': True, 'zorder': 10}, button=[1, ], interactive=False, lineprops={'zorder': 10}) else: self._selector.set_active(False) self._selector.set_visible(False) self._selector = None self.plot2d.replot(keepzoom=False) self.set_sensitive(True) def on_rectangle_selected(self, pos1, pos2): # pos1 and pos2 are mouse button press and release events, with xdata and ydata # carrying the two opposite corners of the bounding box of the rectangle. These # are NOT the exact button presses and releases! row = np.arange(self.mask.shape[0])[:, np.newaxis] column = np.arange(self.mask.shape[1])[np.newaxis, :] tobemasked = ((row >= min(pos1.ydata, pos2.ydata)) & (row <= max(pos1.ydata, pos2.ydata)) & (column >= min(pos1.xdata, pos2.xdata)) & (column <= max(pos1.xdata, pos2.xdata))) self._undo_stack.append(self.mask) if self.builder.get_object('mask_button').get_active(): self.mask = self.mask & (~tobemasked) elif self.builder.get_object('unmask_button').get_active(): self.mask = self.mask | tobemasked elif self.builder.get_object('invertmask_button').get_active(): self.mask[tobemasked] = ~self.mask[tobemasked] else: pass self.builder.get_object('selectrectangle_button').set_active(False) self.plot2d.set_mask(self.mask) def on_selectpolygon_toggled(self, button): if button.get_active(): self.set_sensitive(False, 'Polygon selection not ready', ['new_button', 'save_button', 'saveas_button', 'open_button', 'undo_button', 'selectrectangle_button', 'selectcircle_button', 'pixelhunting_button', 'loadexposure_expander', 'close_button', self.plot2d.toolbar, self.plot2d.settings_expander]) while self.plot2d.toolbar.mode != '': # turn off zoom, pan, etc. modes. self.plot2d.toolbar.zoom() self._selector = LassoSelector(self.plot2d.axis, self.on_polygon_selected, lineprops={'color': 'white', 'zorder': 10}, button=[1, ], ) else: self._selector.set_active(False) self._selector.set_visible(False) self._selector = None self.plot2d.replot(keepzoom=False) self.set_sensitive(True) def on_polygon_selected(self, vertices): path = Path(vertices) col, row = np.meshgrid(np.arange(self.mask.shape[1]), np.arange(self.mask.shape[0])) points = np.vstack((col.flatten(), row.flatten())).T tobemasked = path.contains_points(points).reshape(self.mask.shape) self._undo_stack.append(self.mask) if self.builder.get_object('mask_button').get_active(): self.mask = self.mask & (~tobemasked) elif self.builder.get_object('unmask_button').get_active(): self.mask = self.mask | tobemasked elif self.builder.get_object('invertmask_button').get_active(): self.mask[tobemasked] = ~self.mask[tobemasked] else: pass self.plot2d.set_mask(self.mask) self.builder.get_object('selectpolygon_button').set_active(False) def on_mask_toggled(self, button): pass def on_unmask_toggled(self, button): pass def on_invertmask_toggled(self, button): pass def on_pixelhunting_toggled(self, button): if button.get_active(): self._cursor = Cursor(self.plot2d.axis, useblit=False, color='white', lw=1) self._cursor.connect_event('button_press_event', self.on_cursorclick) while self.plot2d.toolbar.mode != '': # turn off zoom, pan, etc. modes. self.plot2d.toolbar.zoom() else: self._cursor.disconnect_events() self._cursor = None self._undo_stack.append(self.mask) self.plot2d.replot(keepzoom=False) def on_cursorclick(self, event): if (event.inaxes == self.plot2d.axis) and (self.plot2d.toolbar.mode == ''): self.mask[round(event.ydata), round(event.xdata)] ^= True self._cursor.disconnect_events() self._cursor = None self.plot2d.replot(keepzoom=True) self.on_pixelhunting_toggled(self.builder.get_object('pixelhunting_button')) def cleanup(self): super().cleanup() self._undo_stack = [] def on_undo(self, button): try: self.mask = self._undo_stack.pop() except IndexError: return self.plot2d.set_mask(self.mask)
class ThresholdSelection(cytoflow.views.HistogramView): """ Plots, and lets the user interact with, a threshold on the X axis. TODO - beautify! Attributes ---------- op : Instance(ThresholdOp) the ThresholdOp we're working on. huefacet : Str The conditioning variable to show multiple colors on this plot subset : Str the string passed to Experiment.subset() defining the subset we plot interactive : Bool is this view interactive? Notes ----- We inherit `xfacet` and `yfacet` from `cytoflow.views.HistogramView`, but they must both be unset! Examples -------- In an IPython notebook with `%matplotlib notebook` >>> t = flow.ThresholdOp(name = "Threshold", ... channel = "Y2-A") >>> tv = t.default_view() >>> tv.plot(ex2) >>> tv.interactive = True >>> # .... draw a threshold on the plot >>> ex3 = thresh.apply(ex2) """ id = Constant('edu.mit.synbio.cytoflow.views.threshold') friendly_id = Constant("Threshold Selection") op = Instance(IOperation) name = DelegatesTo('op') channel = DelegatesTo('op') threshold = DelegatesTo('op') interactive = Bool(False, transient=True) # internal state _ax = Any(transient=True) _line = Instance(Line2D, transient=True) _cursor = Instance(Cursor, transient=True) def plot(self, experiment, **kwargs): """Plot the histogram and then plot the threshold on top of it.""" if not experiment: raise util.CytoflowViewError("No experiment specified") if self.xfacet: raise util.CytoflowViewError( "ThresholdSelection.xfacet must be empty") if self.yfacet: raise util.CytoflowViewError( "ThresholdSelection.yfacet must be empty") super(ThresholdSelection, self).plot(experiment, **kwargs) self._ax = plt.gca() self._draw_threshold() self._interactive() @on_trait_change('threshold', post_init=True) def _draw_threshold(self): if not self._ax or not self.threshold: return if self._line: # when used in the GUI, _draw_threshold gets called *twice* without # the plot being updated inbetween: and then the line can't be # removed from the plot, because it was never added. so check # explicitly first. this is likely to be an issue in other # interactive plots, too. if self._line and self._line in self._ax.lines: self._line.remove() self._line = None if self.threshold: self._line = plt.axvline(self.threshold, linewidth=3, color='blue') plt.draw() @on_trait_change('interactive', post_init=True) def _interactive(self): if self._ax and self.interactive: self._cursor = Cursor(self._ax, horizOn=False, vertOn=True, color='blue', useblit=True) self._cursor.connect_event('button_press_event', self._onclick) elif self._cursor: self._cursor.disconnect_events() self._cursor = None def _onclick(self, event): """Update the threshold location""" # sometimes the axes aren't set up and we don't get xdata (??) if event.xdata: self.threshold = event.xdata
class CrossSection(object): """ Class to manage the axes, artists and properties associated with showing a 2D image, a cross-hair cursor and two parasite axes which provide horizontal and vertical cross sections of image. You will likely need to call `CrossSection.init_artists(init_image)` after creating this object. Parameters ---------- fig : matplotlib.figure.Figure The figure object to build the class on, will clear current contents init_image : 2d ndarray The initial image cmap : str, colormap, or None color map to use. Defaults to gray norm : Normalize or None Normalization function to use limit_func : callable, optional function that takes in the image and returns clim values auto_redraw : bool, optional interpolation : str, optional Interpolation method to use. List of valid options can be found in CrossSection2DView.interpolation Properties ---------- interpolation : str The stringly-typed pixel interpolation. See _INTERPOLATION attribute of this cross_section_2d module cmap : str The colormap to use for rendering the image """ def __init__(self, fig, cmap=None, norm=None, limit_func=None, auto_redraw=True, interpolation=None): self._cursor_position_cbs = [] if interpolation is None: interpolation = _INTERPOLATION[0] self._interpolation = interpolation # used to determine if setting properties should force a re-draw self._auto_redraw = auto_redraw # clean defaults if limit_func is None: limit_func = fullrange_limit_factory() if cmap is None: cmap = 'gray' # stash the color map self._cmap = cmap # let norm pass through as None, mpl defaults to linear which is fine if norm is None: norm = Normalize() self._norm = norm # save a copy of the limit function, we will need it later self._limit_func = limit_func # this is used by the widget logic self._active = True self._dirty = True self._cb_dirty = True # work on setting up the mpl axes self._fig = fig # blow away what ever is currently on the figure fig.clf() # Configure the figure in our own image # # +----------------------+ # | H cross section | # +----------------------+ # +---+ +----------------------+ # | V | | | # | | | | # | x | | | # | s | | Main Axes | # | e | | | # | c | | | # | t | | | # | i | | | # | o | | | # | n | | | # +---+ +----------------------+ # make the main axes self._im_ax = fig.add_subplot(1, 1, 1) self._im_ax.set_aspect('equal') self._im_ax.xaxis.set_major_locator(NullLocator()) self._im_ax.yaxis.set_major_locator(NullLocator()) self._imdata = None self._im = self._im_ax.imshow([[]], cmap=self._cmap, norm=self._norm, interpolation=self._interpolation, aspect='equal', vmin=0, vmax=1) # make it dividable divider = make_axes_locatable(self._im_ax) # set up all the other axes # (set up the horizontal and vertical cuts) self._ax_h = divider.append_axes('top', .5, pad=0.1, sharex=self._im_ax) self._ax_h.yaxis.set_major_locator(LinearLocator(numticks=2)) self._ax_v = divider.append_axes('left', .5, pad=0.1, sharey=self._im_ax) self._ax_v.xaxis.set_major_locator(LinearLocator(numticks=2)) self._ax_cb = divider.append_axes('right', .2, pad=.5) # add the color bar self._cb = fig.colorbar(self._im, cax=self._ax_cb) # add the cursor place holder self._cur = None # turn off auto-scale for the horizontal cut self._ax_h.autoscale(enable=False) # turn off auto-scale scale for the vertical cut self._ax_v.autoscale(enable=False) # create line artists self._ln_v, = self._ax_v.plot([], [], 'k-', animated=True, visible=False) self._ln_h, = self._ax_h.plot([], [], 'k-', animated=True, visible=False) # backgrounds for blitting self._ax_v_bk = None self._ax_h_bk = None # stash last-drawn row/col to skip if possible self._row = None self._col = None # make attributes for callback ids self._move_cid = None self._click_cid = None self._clear_cid = None def add_cursor_position_cb(self, callback): """ Add a callback for the cursor position in the main axes Parameters ---------- callback : callable(cc, rr) Function that gets called when the cursor position moves to a new row or column on main axes """ self._cursor_position_cbs.append(fun) # set up the call back for the updating the side axes def _move_cb(self, event): if not self._active: return if event is None: x = self._col y = self._row self._col = None self._row = None else: # short circuit on other axes if event.inaxes is not self._im_ax: return x, y = event.xdata, event.ydata numrows, numcols = self._imdata.shape if x is not None and y is not None: self._ln_h.set_visible(True) self._ln_v.set_visible(True) col = int(x + 0.5) row = int(y + 0.5) if row != self._row or col != self._col: if 0 <= col < numcols and 0 <= row < numrows: self._col = col self._row = row for cb in self._cursor_position_cbs: cb(col, row) for data, ax, bkg, art, set_fun in zip( (self._imdata[row, :], self._imdata[:, col]), (self._ax_h, self._ax_v), (self._ax_h_bk, self._ax_v_bk), (self._ln_h, self._ln_v), (self._ln_h.set_ydata, self._ln_v.set_xdata)): self._fig.canvas.restore_region(bkg) set_fun(data) ax.draw_artist(art) self._fig.canvas.blit(ax.bbox) def _click_cb(self, event): if event.inaxes is not self._im_ax: return self.active = not self.active if self.active: self._cur.onmove(event) self._move_cb(event) @auto_redraw def _connect_callbacks(self): """ Connects all of the callbacks for the motion and click events """ self._disconnect_callbacks() self._cur = Cursor(self._im_ax, useblit=True, color='red', linewidth=2) self._move_cid = self._fig.canvas.mpl_connect('motion_notify_event', self._move_cb) self._click_cid = self._fig.canvas.mpl_connect('button_press_event', self._click_cb) self._clear_cid = self._fig.canvas.mpl_connect('draw_event', self._clear) self._fig.tight_layout() self._fig.canvas.draw() def _disconnect_callbacks(self): """ Disconnects all of the callbacks """ if self._fig.canvas is None: # no canvas -> can't do anything about the call backs which # should not exist self._move_cid = None self._clear_cid = None self._click_cid = None return for atr in ('_move_cid', '_clear_cid', '_click_cid'): cid = getattr(self, atr, None) if cid is not None: self._fig.canvas.mpl_disconnect(cid) setattr(self, atr, None) # clean up the cursor if self._cur is not None: self._cur.disconnect_events() del self._cur self._cur = None @auto_redraw def _init_artists(self, init_image): """ Update the CrossSection with a new base-image. This function takes care of setting up all of the details about the image size in the limits/artist extent of the image and the secondary data in the cross-section parasite plots. Parameters ---------- init_image : ndarray An image to serve as the new 'base' image. """ im_shape = init_image.shape # first deal with the image axis # update the image, `update_artists` takes care of # updating the actual artist self._imdata = init_image # update the extent of the image artist self._im.set_extent([-0.5, im_shape[1] + .5, im_shape[0] + .5, -0.5]) # update the limits of the image axes to match the exent self._im_ax.set_xlim([-.05, im_shape[1] + .5]) self._im_ax.set_ylim([im_shape[0] + .5, -0.5]) # update the format coords printer numrows, numcols = im_shape # note, this is a closure over numrows and numcols def format_coord(x, y): # adjust xy -> col, row col = int(x + 0.5) row = int(y + 0.5) # make sure the point falls in the array if col >= 0 and col < numcols and row >= 0 and row < numrows: # if it does, grab the value z = self._imdata[row, col] return "X: {x:d} Y: {y:d} I: {i:.2f}".format(x=col, y=row, i=z) else: return "X: {x:d} Y: {y:d}".format(x=col, y=row) # replace the current format_coord function self._im_ax.format_coord = format_coord # net deal with the parasite axes and artist self._ln_v.set_data(np.zeros(im_shape[0]), np.arange(im_shape[0])) self._ax_v.set_ylim([0, im_shape[0]]) self._ln_h.set_data(np.arange(im_shape[1]), np.zeros(im_shape[1])) self._ax_h.set_xlim([0, im_shape[1]]) # if we have a cavas, then connect/set up junk if self._fig.canvas is not None: self._connect_callbacks() # mark as dirty self._dirty = True def _clear(self, event): self._ax_v_bk = self._fig.canvas.copy_from_bbox(self._ax_v.bbox) self._ax_h_bk = self._fig.canvas.copy_from_bbox(self._ax_h.bbox) self._ln_h.set_visible(False) self._ln_v.set_visible(False) # this involves reaching in and touching the guts of the # cursor widget. The problem is that the mpl widget # skips updating it's saved background if the widget is inactive if self._cur: self._cur.background = self._cur.canvas.copy_from_bbox( self._cur.canvas.figure.bbox) @property def interpolation(self): return self._interpolation @property def active(self): return self._active @active.setter def active(self, val): self._active = val self._cur.active = val @auto_redraw def update_interpolation(self, interpolation): """ Set the interpolation method """ self._dirty = True self._im.set_interpolation(interpolation) @auto_redraw def update_cmap(self, cmap): """ Set the color map used """ # TODO: this should stash new value, not apply it self._cmap = cmap self._dirty = True @auto_redraw def update_image(self, image): """ Set the image data The input data does not necessarily have to be the same shape as the original image """ if self._imdata is None or self._imdata.shape != image.shape: self._init_artists(image) self._imdata = image self._move_cb(None) self._dirty = True @auto_redraw def update_norm(self, norm): """ Update the way that matplotlib normalizes the image """ self._norm = norm self._dirty = True self._cb_dirty = True @auto_redraw def update_limit_func(self, limit_func): """ Set the function to use to determine the color scale """ # set the new function to use for computing the color limits self._limit_func = limit_func self._dirty = True def _update_artists(self): """ Updates the figure by re-drawing """ # if the figure is not dirty, short-circuit if not (self._dirty or self._cb_dirty): return # this is a tuple which is the max/min used in the color mapping. # these values are also used to set the limits on the value # axes of the parasite axes # value_limits vlim = self._limit_func(self._imdata) # set the color bar limits self._im.set_clim(vlim) self._norm.vmin, self._norm.vmax = vlim # set the cross section axes limits self._ax_v.set_xlim(*vlim[::-1]) self._ax_h.set_ylim(*vlim) # set the imshow data self._im.set_data(self._imdata) self._im.set_cmap(self._cmap) self._im.set_norm(self._norm) # TODO if cb_dirty, remake the colorbar, I think this is # why changing the norm does not play well self._dirty = False self._cb_dirty = False def _draw(self): self._fig.canvas.draw() @auto_redraw def autoscale_horizontal(self, enable): self._ax_h.autoscale(enable=enable) @auto_redraw def autoscale_vertical(self, enable): self._ax_v.autoscale(enable=False)
class PolygonSelection(cytoflow.views.ScatterplotView): """Plots, and lets the user interact with, a 2D polygon selection. Attributes ---------- op : Instance(PolygonOp) The operation on which this selection view is operating huefacet : Str The conditioning variable to show multiple colors on this plot subset : Str The string for subsetting the plot interactive : Bool is this view interactive? Ie, can the user set the polygon verticies with mouse clicks? Notes ----- We inherit `xfacet` and `yfacet` from `cytoflow.views.ScatterPlotView`, but they must both be unset! Examples -------- In an IPython notebook with `%matplotlib notebook` >>> s = flow.ScatterplotView(xchannel = "V2-A", ... ychannel = "Y2-A") >>> poly = s.default_view() >>> poly.plot(ex2) >>> poly.interactive = True """ id = Constant('edu.mit.synbio.cytoflow.views.polygon') friendly_id = Constant("Polygon Selection") op = Instance(IOperation) name = DelegatesTo('op') xchannel = DelegatesTo('op') ychannel = DelegatesTo('op') interactive = Bool(False, transient=True) # internal state. _ax = Any(transient=True) _cursor = Instance(Cursor, transient=True) _path = Instance(mpl.path.Path, transient=True) _patch = Instance(mpl.patches.PathPatch, transient=True) _line = Instance(mpl.lines.Line2D, transient=True) _drawing = Bool(transient=True) _last_draw_time = Float(0.0, transient=True) _last_click_time = Float(0.0, transient=True) def plot(self, experiment, **kwargs): """Plot self.view, and then plot the selection on top of it.""" if not experiment: raise util.CytoflowViewError("No experiment specified") if self.xfacet: raise util.CytoflowViewError( "RangeSelection.xfacet must be empty or `Undefined`") if self.yfacet: raise util.CytoflowViewError( "RangeSelection.yfacet must be empty or `Undefined`") super(PolygonSelection, self).plot(experiment, **kwargs) self._ax = plt.gca() self._draw_poly() self._interactive() @on_trait_change('op.vertices', post_init=True) def _draw_poly(self): if not self._ax: return if self._patch and self._patch in self._ax.patches: self._patch.remove() if self._drawing or not self.op.vertices or len(self.op.vertices) < 3 \ or any([len(x) != 2 for x in self.op.vertices]): return patch_vert = np.concatenate( (np.array(self.op.vertices), np.array((0, 0), ndmin=2))) self._patch = \ mpl.patches.PathPatch(mpl.path.Path(patch_vert, closed = True), edgecolor="black", linewidth = 1.5, fill = False) self._ax.add_patch(self._patch) plt.draw_if_interactive() @on_trait_change('interactive', post_init=True) def _interactive(self): if self._ax and self.interactive: self._cursor = Cursor(self._ax, horizOn=False, vertOn=False) self._cursor.connect_event('button_press_event', self._onclick) self._cursor.connect_event('motion_notify_event', self._onmove) elif self._cursor: self._cursor.disconnect_events() self._cursor = None def _onclick(self, event): """Update selection traits""" if not self._ax: return if (self._cursor.ignore(event)): return # we have to check the wall clock time because the IPython notebook # doesn't seem to register double-clicks if event.dblclick or (time.clock() - self._last_click_time < 0.5): self._drawing = False self.op.vertices = map(tuple, self._path.vertices) self.op._xscale = plt.gca().get_xscale() self.op._yscale = plt.gca().get_yscale() self._path = None return self._last_click_time = time.clock() self._drawing = True if self._patch and self._patch in self._ax.patches: self._patch.remove() if self._path: vertices = np.concatenate((self._path.vertices, np.array((event.xdata, event.ydata), ndmin=2))) else: vertices = np.array((event.xdata, event.ydata), ndmin=2) self._path = mpl.path.Path(vertices, closed=False) self._patch = mpl.patches.PathPatch(self._path, edgecolor="black", fill=False) self._ax.add_patch(self._patch) plt.draw_if_interactive() def _onmove(self, event): if not self._ax: return if (self._cursor.ignore(event) or not self._drawing or not self._path or self._path.vertices.shape[0] == 0 or not event.xdata or not event.ydata): return # only draw 5 times/sec if (time.clock() - self._last_draw_time < 0.2): return self._last_draw_time = time.clock() if self._line and self._line in self._ax.lines: self._line.remove() xdata = [self._path.vertices[-1, 0], event.xdata] ydata = [self._path.vertices[-1, 1], event.ydata] self._line = mpl.lines.Line2D(xdata, ydata, linewidth=1, color="black") self._ax.add_line(self._line) plt.gcf().canvas.draw()
class QuadSelection(Op2DView, ScatterplotView): """Plots, and lets the user interact with, a quadrant gate. Attributes ---------- interactive : Bool is this view interactive? Ie, can the user set the threshold with a mouse click? Notes ----- We inherit :attr:`xfacet` and :attr:`yfacet` from :class:`cytoflow.views.ScatterplotView`, but they must both be unset! Examples -------- In an Jupyter notebook with `%matplotlib notebook` >>> q = flow.QuadOp(name = "Quad", ... xchannel = "V2-A", ... ychannel = "Y2-A")) >>> qv = q.default_view() >>> qv.interactive = True >>> qv.plot(ex2) """ id = Constant('edu.mit.synbio.cytoflow.views.quad') friendly_id = Constant("Quadrant Selection") xfacet = Constant(None) yfacet = Constant(None) # override the Op2DView xscale = util.ScaleEnum yscale = util.ScaleEnum xthreshold = DelegatesTo('op') ythreshold = DelegatesTo('op') interactive = Bool(False, transient=True) # internal state. _ax = Any(transient=True) _hline = Instance(Line2D, transient=True) _vline = Instance(Line2D, transient=True) _cursor = Instance(Cursor, transient=True) def plot(self, experiment, **kwargs): """ Plot the underlying scatterplot and then plot the selection on top of it. Parameters ---------- """ if experiment is None: raise util.CytoflowViewError('experiment', "No experiment specified") super().plot(experiment, **kwargs) self._ax = plt.gca() self._draw_lines() self._interactive() @on_trait_change('xthreshold, ythreshold', post_init=True) def _draw_lines(self): if not self._ax: return if self._hline and self._hline in self._ax.lines: self._hline.remove() if self._vline and self._vline in self._ax.lines: self._vline.remove() if self.xthreshold and self.ythreshold: self._hline = plt.axhline(self.ythreshold, linewidth=3, color='blue') self._vline = plt.axvline(self.xthreshold, linewidth=3, color='blue') plt.draw() @on_trait_change('interactive', post_init=True) def _interactive(self): if self._ax and self.interactive: self._cursor = Cursor(self._ax, horizOn=True, vertOn=True, color='blue', useblit=True) self._cursor.connect_event('button_press_event', self._onclick) elif self._cursor: self._cursor.disconnect_events() self._cursor = None def _onclick(self, event): """Update the threshold location""" self.xthreshold = event.xdata self.ythreshold = event.ydata
class PolygonSelection(cytoflow.views.ScatterplotView): """Plots, and lets the user interact with, a 2D polygon selection. Attributes ---------- op : Instance(PolygonOp) The operation on which this selection view is operating huefacet : Str The conditioning variable to show multiple colors on this plot subset : Str The string for subsetting the plot interactive : Bool is this view interactive? Ie, can the user set the polygon verticies with mouse clicks? Notes ----- We inherit `xfacet` and `yfacet` from `cytoflow.views.ScatterPlotView`, but they must both be unset! Examples -------- In an IPython notebook with `%matplotlib notebook` >>> s = flow.ScatterplotView(xchannel = "V2-A", ... ychannel = "Y2-A") >>> poly = s.default_view() >>> poly.plot(ex2) >>> poly.interactive = True """ id = Constant('edu.mit.synbio.cytoflow.views.polygon') friendly_id = Constant("Polygon Selection") op = Instance(IOperation) name = DelegatesTo('op') xchannel = DelegatesTo('op') ychannel = DelegatesTo('op') interactive = Bool(False, transient = True) # internal state. _ax = Any(transient = True) _cursor = Instance(Cursor, transient = True) _path = Instance(mpl.path.Path, transient = True) _patch = Instance(mpl.patches.PathPatch, transient = True) _line = Instance(mpl.lines.Line2D, transient = True) _drawing = Bool(transient = True) _last_draw_time = Float(0.0, transient = True) _last_click_time = Float(0.0, transient = True) def plot(self, experiment, **kwargs): """Plot self.view, and then plot the selection on top of it.""" if not experiment: raise util.CytoflowViewError("No experiment specified") if self.xfacet: raise util.CytoflowViewError("RangeSelection.xfacet must be empty or `Undefined`") if self.yfacet: raise util.CytoflowViewError("RangeSelection.yfacet must be empty or `Undefined`") super(PolygonSelection, self).plot(experiment, **kwargs) self._ax = plt.gca() self._draw_poly() self._interactive() @on_trait_change('op.vertices', post_init = True) def _draw_poly(self): if not self._ax: return if self._patch and self._patch in self._ax.patches: self._patch.remove() if self._drawing or not self.op.vertices or len(self.op.vertices) < 3 \ or any([len(x) != 2 for x in self.op.vertices]): return patch_vert = np.concatenate((np.array(self.op.vertices), np.array((0,0), ndmin = 2))) self._patch = \ mpl.patches.PathPatch(mpl.path.Path(patch_vert, closed = True), edgecolor="black", linewidth = 1.5, fill = False) self._ax.add_patch(self._patch) plt.draw_if_interactive() @on_trait_change('interactive', post_init = True) def _interactive(self): if self._ax and self.interactive: self._cursor = Cursor(self._ax, horizOn = False, vertOn = False) self._cursor.connect_event('button_press_event', self._onclick) self._cursor.connect_event('motion_notify_event', self._onmove) elif self._cursor: self._cursor.disconnect_events() self._cursor = None def _onclick(self, event): """Update selection traits""" if not self._ax: return if(self._cursor.ignore(event)): return # we have to check the wall clock time because the IPython notebook # doesn't seem to register double-clicks if event.dblclick or (time.clock() - self._last_click_time < 0.5): self._drawing = False self.op.vertices = map(tuple, self._path.vertices) self.op._xscale = plt.gca().get_xscale() self.op._yscale = plt.gca().get_yscale() self._path = None return self._last_click_time = time.clock() self._drawing = True if self._patch and self._patch in self._ax.patches: self._patch.remove() if self._path: vertices = np.concatenate((self._path.vertices, np.array((event.xdata, event.ydata), ndmin = 2))) else: vertices = np.array((event.xdata, event.ydata), ndmin = 2) self._path = mpl.path.Path(vertices, closed = False) self._patch = mpl.patches.PathPatch(self._path, edgecolor = "black", fill = False) self._ax.add_patch(self._patch) plt.draw_if_interactive() def _onmove(self, event): if not self._ax: return if(self._cursor.ignore(event) or not self._drawing or not self._path or self._path.vertices.shape[0] == 0 or not event.xdata or not event.ydata): return # only draw 5 times/sec if(time.clock() - self._last_draw_time < 0.2): return self._last_draw_time = time.clock() if self._line and self._line in self._ax.lines: self._line.remove() xdata = [self._path.vertices[-1, 0], event.xdata] ydata = [self._path.vertices[-1, 1], event.ydata] self._line = mpl.lines.Line2D(xdata, ydata, linewidth = 1, color = "black") self._ax.add_line(self._line) plt.gcf().canvas.draw()
class QuadSelection(cytoflow.views.ScatterplotView): """Plots, and lets the user interact with, a quadrant gate. Attributes ---------- op : Instance(Range2DOp) The instance of Range2DOp that we're viewing / editing huefacet : Str The conditioning variable to plot multiple colors subset : Str The string passed to `Experiment.query()` to subset the data before plotting interactive : Bool is this view interactive? Ie, can the user set the threshold with a mouse click? Notes ----- We inherit `xfacet` and `yfacet` from `cytoflow.views.ScatterplotView`, but they must both be unset! Examples -------- In an IPython notebook with `%matplotlib notebook` >>> q = flow.QuadOp(name = "Quad", ... xchannel = "V2-A", ... ychannel = "Y2-A")) >>> qv = q.default_view() >>> qv.interactive = True >>> qv.plot(ex2) """ id = Constant('edu.mit.synbio.cytoflow.views.quad') friendly_id = Constant("Quadrant Selection") op = Instance(IOperation) name = DelegatesTo('op') xchannel = DelegatesTo('op') ychannel = DelegatesTo('op') interactive = Bool(False, transient=True) # internal state. _ax = Any(transient=True) _hline = Instance(Line2D, transient=True) _vline = Instance(Line2D, transient=True) _cursor = Instance(Cursor, transient=True) def plot(self, experiment, **kwargs): """Plot the underlying scatterplot and then plot the selection on top of it.""" if not experiment: raise util.CytoflowOpError("No experiment specified") if not experiment: raise util.CytoflowViewError("No experiment specified") if self.xfacet: raise util.CytoflowViewError( "RangeSelection.xfacet must be empty or `Undefined`") if self.yfacet: raise util.CytoflowViewError( "RangeSelection.yfacet must be empty or `Undefined`") super(QuadSelection, self).plot(experiment, **kwargs) self._ax = plt.gca() self._draw_lines() self._interactive() @on_trait_change('op.xthreshold, op.ythreshold', post_init=True) def _draw_lines(self): if not self._ax: return if self._hline and self._hline in self._ax.lines: self._hline.remove() if self._vline and self._vline in self._ax.lines: self._vline.remove() if self.op.xthreshold and self.op.ythreshold: self._hline = plt.axhline(self.op.ythreshold, linewidth=3, color='blue') self._vline = plt.axvline(self.op.xthreshold, linewidth=3, color='blue') plt.draw_if_interactive() @on_trait_change('interactive', post_init=True) def _interactive(self): if self._ax and self.interactive: self._cursor = Cursor(self._ax, horizOn=True, vertOn=True, color='blue') self._cursor.connect_event('button_press_event', self._onclick) elif self._cursor: self._cursor.disconnect_events() self._cursor = None def _onclick(self, event): """Update the threshold location""" self.op.xthreshold = event.xdata self.op.ythreshold = event.ydata