class RampZMagResultsWidget: "Table containing discrete zmag values" __slider: Slider __src: ColumnDataSource __box: Optional[BoxAnnotation] def __init__(self, ctrl, model: RampPlotModel) -> None: self.__model = model self.__ctrl = ctrl self.__theme = ctrl.theme.add(RampZMagResultsTheme()) self.__box = None def addtodoc(self, mainview, ctrl) -> List[Widget]: "creates the widget" data = self.__data() self.__src = ColumnDataSource(data.pop("table")) table = DataTable(source=self.__src, columns=self.__columns(), editable=False, index_position=None, width=self.__theme.width, height=self.__theme.heights[1]) self.__slider = Slider(width=self.__theme.width, height=self.__theme.heights[0], **data) @mainview.actionifactive(ctrl) def _onchange_cb(attr, old, new): ctrl.theme.update(self.__theme, value=new) self.__slider.on_change("value", _onchange_cb) return [self.__slider, table] def observe(self, mainview, ctrl): "observe the controller" @ctrl.theme.observe(self.__theme) def _observe(**_): if not mainview.isactive(): return data = self.__data() self.__slider.update(title=data.pop('title')) self.__src.update(data=data.pop('table')) if self.__box is not None: zmag = self.__theme.value self.__box.update(left=zmag, right=zmag) def reset(self, resets): "resets the wiget when a new file is opened" data = self.__data() resets[self.__src].update(data=data.pop('table')) resets[self.__slider].update(**data) if self.__box is not None: zmag = self.__theme.value resets[self.__box].update(left=zmag, right=zmag) def addline(self, fig): "add a vertical line at the current zmag" zmag = self.__theme.value self.__box = BoxAnnotation(left=zmag, right=zmag, fill_alpha=0., line_alpha=self.__theme.alpha, line_color=self.__theme.color) fig.add_layout(self.__box) def __columns(self): return [ TableColumn( field=i[0], title=i[1], width=i[2], formatter=DpxNumberFormatter(format=i[3], text_align='right') if i[3] else StringFormatter()) for i in self.__theme.columns ] def __data(self): data = self.__model.getdisplay("consensus") zmag = self.__theme.value return { **self.__update_slider(data, zmag), **self.__update_table(data, zmag), } def __update_slider(self, data, zmag): itms = dict( start=-.6, end=-.3, step=self.__theme.step, title="", value=zmag, ) if data is not None: name = "normalized" if self.__model.theme.dataformat == "norm" else "consensus" cols = [(name, i) for i in range(3)] + [("zmag", "")] # type: ignore arr = data[cols] fcn = lambda *x: interp1d(arr["zmag", ""], arr[x], assume_sorted=True, fill_value=np.NaN, bounds_error=False)(zmag) tit = self.__theme.title unit = 1 if self.__model.theme.dataformat == "norm" else 0 itms.update(start=np.nanmin(arr["zmag", ""]), end=np.nanmax(arr["zmag", ""]), title=tit.format(bead=fcn(name, 1), zmag=zmag, err=(fcn(name, 2) - fcn(name, 0)) * .5, unit=self.__theme.units[unit][1:-1])) return itms def __update_table(self, data, zmag): table = { 'closing': [1. - i for i in self.__theme.ranges], **{ i: [0] * len(self.__theme.ranges) for i in ('count', 'percent', 'beads') } } if data is not None: beads = np.array([ *self.__model.display.status(self.__model.tasks.roottask, self.__ctrl).get('ok', []) ]) loss = (np.clip( np.array([ interp1d(data["zmag", ""], data[i, 1], assume_sorted=True, fill_value=0., bounds_error=False)(zmag) for i in beads ]), 0., 100.) / [data[i, 1].max() for i in beads]) notfound = loss >= -1 splits = [] for i in self.__theme.ranges: good = loss <= i splits.append(beads[good & notfound]) notfound = ~good table.update(count=[len(i) for i in splits], percent=[len(i) / len(beads) for i in splits], beads=[intlistsummary(i) for i in splits]) return {'table': table}
class QCHairpinSizeWidget: "Table containing discrete bead extensions" __table: DataTable __slider: Slider __src: ColumnDataSource def __init__(self, ctrl, model): self._model = model self._theme = ctrl.theme.add(QCHairpinSizeTheme(), False) def addtodoc(self, mainview, ctrl) -> List[Widget]: "creates the widget" self._theme, self.__src, self.__table = addtodoc( ctrl, self._theme, self.__tabledata() ) self.__slider = Slider( title = self._theme.title, step = self._theme.binstep, width = self._theme.width, height = self._theme.sliderheight, **self.__sliderdata(), ) @mainview.actionifactive(ctrl) def _onchange_cb(attr, old, new): ctrl.theme.update(self._theme, binsize = new) self.__slider.on_change("value", _onchange_cb) return [self.__slider, self.__table] def observe(self, mainview, ctrl): "observe the controller" @ctrl.theme.observe(self._theme) def _observe(**_): if mainview.isactive(): self.__slider.update(**self.__sliderdata()) self.__src.update(data = self.__tabledata()) def reset(self, resets): "resets the wiget when a new file is opened" resets[self.__src].update(data = self.__tabledata()) resets[self.__slider].update(**self.__sliderdata()) def _sliderdata(self) -> Dict[str, float]: return {'start': self._theme.binstart, "end": self._theme.binend} def _tabledata(self) -> Iterable[Tuple[int, float]]: track = self._model.track if track is None: return () return ((i, track.beadextension(i)) for i in self._model.status()["ok"]) def __sliderdata(self) -> Dict[str, float]: data = self._sliderdata() data["value"] = self._theme.binsize return data def __tabledata(self) -> Dict[str, np.ndarray]: out = {'z': np.empty(0), 'count': np.empty(0), 'percent': np.empty(0)} data = np.array(list(self._tabledata()), dtype = [("bead", "i4"), ("extent", "f4")]) if len(data) == 0: return out bsize = self._theme.binsize inds = np.round(data["extent"]/bsize).astype(int) izval = np.sort(np.unique(inds)) if len(izval): cnt = np.array([np.sum(inds == i) for i in izval]) out.update(z = [data["extent"][inds == i].mean() for i in izval], count = cnt, percent = cnt*100./cnt.sum(), beads = [intlistsummary(data["bead"][inds == i]) for i in izval]) if not self._theme.headers: out["percent"] = [f"{i:.0f} %" for i in out["percent"]] out["z"] = [f"{i:.2f} µm" for i in out["z"]] return out
class DatapointTracer(WebPlot): # sig_plot_options_changed = BokehCallbackSignal() # sig_frame_changed = BokehCallbackSignal() def __init__( self, parent: WebPlot, doc, project_path: Union[Path, str], tooltip_columns: List[str] = None, image_figure_params: dict = None, curve_figure_params: dict = None ): self.sig_plot_options_changed = BokehCallbackSignal() self.sig_frame_changed = BokehCallbackSignal() WebPlot.__init__(self) self.parent = parent self.doc = doc # self.parent_document: Document = parent_document self.project_path: Path = Path(project_path) self.frame: np.ndarray = np.empty(0) if image_figure_params is None: image_figure_params = dict() self.image_figure: Figure = figure( **{ **_default_image_figure_params, **image_figure_params, 'output_backend': "webgl" } ) # must initialize with some array else it won't work empty_img = np.zeros(shape=(100, 100), dtype=np.uint8) self.image_glyph: Image = self.image_figure.image( image=[empty_img], x=0, y=0, dw=10, dh=10, level="image" ) self.roi_patches_glyph: Patches = self.image_figure.patches( xs="xs", ys="ys", # color="colors", color="#ffffff", alpha=0.0, line_width=2, line_alpha=1.0, source={ "xs": [[]], "ys": [[]], # "colors": ["#ffffff"], }, ) self.image_figure.grid.grid_line_width = 0 self.curve_figure: Figure = None if curve_figure_params is None: curve_figure_params = dict() self.curve_figure_params = \ { **_default_curve_figure_params, **curve_figure_params } self.curve_glyph: MultiLine = None self.curve_plot_bands: List[BoxAnnotation] = [] self.tooltip_columns = tooltip_columns self.tooltips = None if self.tooltip_columns is not None: self.tooltips = [(col, f'@{col}') for col in tooltip_columns] # self.datatable: self.dataframe: pd.DataFrame = None self.sample_id: str = None self.img_uuid: UUID = None self.current_frame: int = -1 self.tif: tifffile.TiffFile = None self.color_mapper: LogColorMapper = None self.curve_color_selector = Select(title="Color based on:", value='', options=['']) self.curve_color_selector.on_change('value', self.sig_plot_options_changed.trigger) self.curve_data_selector = Select(title="Curve data:", value='', options=['']) self.curve_data_selector.on_change('value', self.sig_plot_options_changed.trigger) self.curve_plot_bands_selector = Select(title="Bands based on:", value='', options=['']) self.curve_plot_bands_selector.on_change('value', self.sig_plot_options_changed.trigger) ############################################################ # TEMPORARY ############################################################ # self.button_remove_selection = Button(label="Remove current selection") # self.button_remove_selection.on_click(self.remove_sample) ############################################################ ############################################################ ############################################################ self.sig_plot_options_changed.connect(self.set_curve) self.frame_slider = Slider(start=0, end=1000, value=1, step=10, title="Frame index:") self.frame_slider.on_change('value', self.sig_frame_changed.trigger) self.sig_frame_changed.connect(self._set_current_frame) self.label_filesize: TextInput = TextInput(value='', title='Filesize (GB):') self.label_sample_id: TextInput = TextInput(value='', title="SampleID:") # def remove_sample(self): # self.parent.dataframe = self.parent.dataframe[ # self.parent.dataframe['SampleID'] != self.sample_id # ] # # sid = self.parent.dataframe['SampleID'].unique()[0] # # self.set_sample( # self.parent.dataframe[self.parent.dataframe['SampleID'] == sid] # ) # # self.parent.update_glyph() def _check_sample(self, dataframe: pd.DataFrame): if len(dataframe['SampleID'].unique()) > 1: raise ValueError("Greater than one SampleID in the sub-dataframe") if len(dataframe['ImgUUID'].unique()) > 1: raise ValueError("Greater than one ImgUUID in the sub-dataframe") self.dataframe = dataframe.copy(deep=True) self.sample_id = dataframe['SampleID'].unique()[0] self.img_uuid = dataframe['ImgUUID'].unique()[0] @WebPlot.signal_blocker def set_sample(self, dataframe: pd.DataFrame): """ :param dataframe: dataframe with values pertaining to one sample :return: """ self._check_sample(dataframe) fname = f'{self.sample_id}-_-{self.img_uuid}.tiff' vid_path = self.project_path.joinpath('images', fname) self._set_video(vid_path) self._update_plot_options() self.set_curve() self.label_sample_id.update(value=self.sample_id) def _set_video(self, vid_path: Union[Path, str]): self.tif = tifffile.TiffFile(vid_path) self.current_frame = 0 self.frame = self.tif.asarray(key=self.current_frame) # this is basically used for vmin mvax self.color_mapper = LogColorMapper( palette=auto_colormap(256, 'gnuplot2', output='bokeh'), low=np.nanmin(self.frame), high=np.nanmax(self.frame) ) self.image_glyph.data_source.data['image'] = [self.frame] self.image_glyph.glyph.color_mapper = self.color_mapper # shows the file size in gigabytes self.label_filesize.update(value=str(os.path.getsize(vid_path) / 1024 / 1024 / 1024)) def _get_roi_coors(self, r: pd.Series): roi_type = r['roi_type'] if roi_type == 'ManualROI': pos = r['roi_graphics_object_state']['pos'] points = r['roi_graphics_object_state']['points'] return points + np.array(pos) # @WebPlot.signal_blocker def _update_plot_options(self): # categorical_columns = get_categorical_columns(self.dataframe) logger.debug("Updating plot opts") categorical_columns = self.tooltip_columns numerical_columns = get_numerical_columns(self.dataframe) self.curve_color_selector.update( value= \ self.curve_color_selector.value \ if self.curve_color_selector.value in categorical_columns \ else categorical_columns[0], options=categorical_columns ) self.curve_data_selector.update( value= \ self.curve_data_selector.value \ if self.curve_data_selector.value in numerical_columns \ else numerical_columns[0], options=numerical_columns ) if len(self.parent.transmission.STIM_DEFS) > 0: self.curve_plot_bands_selector.update( value= \ self.curve_plot_bands_selector.value \ if self.curve_plot_bands_selector.value in self.parent.transmission.STIM_DEFS \ else self.parent.transmission.STIM_DEFS[0], options=self.parent.transmission.STIM_DEFS ) else: self.curve_plot_bands_selector.update(value='', options=['']) def _set_current_frame(self, i: int): self.current_frame = i frame = self.tif.asarray(key=self.current_frame, maxworkers=20) self.image_glyph.data_source.data['image'] = [frame] def _get_trimmed_dataframe(self) -> pd.DataFrame: """ Get dataframe for tooltips, JSON serializable. """ return self.dataframe.drop( columns=[c for c in self.dataframe.columns if c not in self.tooltip_columns] ).copy(deep=True) @WebPlot.signal_blocker def set_curve(self, *args): logger.debug('updating curve') logger.debug(self.dataframe) data_column = self.curve_data_selector.value ys = self.dataframe[data_column].values xs = [np.arange(0, v.size) for v in ys] self.frame_slider.update(start=0, end=ys[0].size - 1, value=0) df = self._get_trimmed_dataframe() colors_column = self.curve_color_selector.value ncolors = df[colors_column].unique().size if ncolors < 11: cmap = 'tab10' elif 10 < ncolors < 21: cmap = 'tab20' else: cmap = 'hsv' src = ColumnDataSource( { **df, 'xs': xs, 'ys': ys, 'colors': map_labels_to_colors(df[colors_column], cmap, output='bokeh') } ) if self.curve_figure is not None: self.doc.remove_root(self.curve_figure) del self.curve_figure # New figure has to be created each time self.curve_figure = figure( tooltips=self.tooltips, **self.curve_figure_params, ) stim_option = self.curve_plot_bands_selector.value stim_df = self.dataframe['stim_maps'].iloc[0][0][0].get(stim_option, None) if stim_df is not None: for ix, stim_period in stim_df.iterrows(): self.curve_figure.add_layout( BoxAnnotation( left=stim_period['start'], right=stim_period['end'], fill_color=list(stim_period['color'][:-1]), fill_alpha=0.1 ) ) self.curve_glyph = self.curve_figure.multi_line( xs='xs', ys='ys', legend=colors_column, line_color='colors', line_width=2, source=src ) # TODO: ROIs # # set the ROIs # p = pickle.load( # open( # os.path.join( # self.parent.transmission.get_proj_path(), # self.dataframe['ImgInfoPath'].iloc[0] # ), # 'rb' # ) # ) # # roi_coors = self.dataframe['ROI_State'].apply(self._get_roi_coors).values # # xs = [a[:, 0].tolist() for a in roi_coors] # ys = [a[:, 1].tolist() for a in roi_coors] # # self.roi_patches_glyph.data_source.data['xs'] = xs # self.roi_patches_glyph.data_source.data['ys'] = ys # colors_list = self.curve_glyph.data_source.data['colors'] # if len(xs) != len(colors_list): # colors_list = ["#ffffff"] * len(xs) # else: # self.roi_patches_glyph.data_source.data['colors'] = colors_list self.image_glyph.glyph.dw = self.frame.shape[0] self.image_glyph.glyph.dh = self.frame.shape[1] self.image_glyph.glyph.x = 0 self.image_glyph.glyph.y = 0 # add the new curve plot to the doc root self.doc.add_root(self.curve_figure) logger.debug(">>>> DATAFRAME IS <<<<<<") logger.debug(self.dataframe) def set_dashboard(self, figures: List[Figure]): logger.info('setting dashboard, this might take a few minutes') self.doc.add_root( column( row(*(f for f in figures), self.image_figure), row( self.curve_data_selector, self.curve_color_selector, self.curve_plot_bands_selector ), row( self.label_sample_id, self.label_filesize, ), self.frame_slider ) )
class DatapointTracer(WebPlot): # sig_plot_options_changed = BokehCallbackSignal() # sig_frame_changed = BokehCallbackSignal() def __init__( self, doc, project_path: Union[Path, str], tooltip_columns: List[str] = None, image_figure_params: dict = None, curve_figure_params: dict = None ): self.sig_plot_options_changed = BokehCallbackSignal() self.sig_frame_changed = BokehCallbackSignal() WebPlot.__init__(self) self.doc = doc # self.parent_document: Document = parent_document self.project_path: Path = Path(project_path) if image_figure_params is None: image_figure_params = dict() self.image_figure: Figure = figure( **{ **_default_image_figure_params, **image_figure_params } ) # must initialize with some array else it won't work empty_img = np.zeros(shape=(100, 100), dtype=np.uint8) self.image_glyph: Image = self.image_figure.image( image=[empty_img], x=0, y=0, dw=10, dh=10, level="image" ) self.image_figure.grid.grid_line_width = 0 self.curve_figure: Figure = None if curve_figure_params is None: curve_figure_params = dict() self.curve_figure_params = \ { **_default_curve_figure_params, **curve_figure_params } self.curve_glyph: MultiLine = None self.tooltip_columns = tooltip_columns self.tooltips = None if self.tooltip_columns is not None: self.tooltips = [(col, f'@{col}') for col in tooltip_columns] self.dataframe: pd.DataFrame = None self.sample_id: str = None self.img_uuid: UUID = None self.current_frame: int = -1 self.tif: tifffile.TiffFile = None self.color_mapper: LogColorMapper = None self.curve_color_selector = Select(title="Color based on:", value='', options=['']) self.curve_color_selector.on_change('value', self.sig_plot_options_changed.trigger) self.curve_data_selector = Select(title="Curve data:", value='', options=['']) self.curve_data_selector.on_change('value', self.sig_plot_options_changed.trigger) self.sig_plot_options_changed.connect(self.set_curve) self.frame_slider = Slider(start=0, end=1000, value=1, step=10, title="Frame index:") self.frame_slider.on_change('value', self.sig_frame_changed.trigger) self.sig_frame_changed.connect(self._set_current_frame) self.label_filesize: TextInput = TextInput(value='', title='Filesize (GB):') def _check_sample(self, dataframe: pd.DataFrame): if len(dataframe['SampleID'].unique()) > 1: raise ValueError("Greater than one SampleID in the sub-dataframe") if len(dataframe['ImgUUID'].unique()) > 1: raise ValueError("Greater than one ImgUUID in the sub-dataframe") self.dataframe = dataframe.copy(deep=True) self.sample_id = dataframe['SampleID'].unique()[0] self.img_uuid = dataframe['ImgUUID'].unique()[0] @WebPlot.signal_blocker def set_sample(self, dataframe: pd.DataFrame): """ :param dataframe: dataframe with values pertaining to one sample :return: """ self._check_sample(dataframe) fname = f'{self.sample_id}-_-{self.img_uuid}.tiff' vid_path = self.project_path.joinpath('images', fname) self._set_video(vid_path) self._update_plot_options() self.set_curve() def _set_video(self, vid_path: Union[Path, str]): self.tif = tifffile.TiffFile(vid_path) self.current_frame = 0 frame = self.tif.asarray(key=self.current_frame) # this is basically used for vmin mvax self.color_mapper = LogColorMapper( palette=auto_colormap(256, 'gnuplot2', output='bokeh'), low=np.nanmin(frame), high=np.nanmax(frame) ) self.image_glyph.data_source.data['image'] = [frame] self.image_glyph.glyph.color_mapper = self.color_mapper # shows the file size in gigabytes self.label_filesize.update(value=str(os.path.getsize(vid_path) / 1024 / 1024 / 1024)) # @WebPlot.signal_blocker def _update_plot_options(self): # categorical_columns = get_categorical_columns(self.dataframe) print("Updating plot opts") categorical_columns = self.tooltip_columns numerical_columns = get_numerical_columns(self.dataframe) self.curve_color_selector.update( value= \ self.curve_color_selector.value \ if self.curve_color_selector.value in categorical_columns \ else categorical_columns[0], options=categorical_columns ) self.curve_data_selector.update( value= \ self.curve_data_selector.value \ if self.curve_data_selector.value in numerical_columns \ else numerical_columns[0], options=numerical_columns ) def _set_current_frame(self, i: int): self.current_frame = i frame = self.tif.asarray(key=self.current_frame) self.image_glyph.data_source.data['image'] = [frame] def set_curve(self): print('updating curve') print(self.dataframe) data_column = self.curve_data_selector.value ys = self.dataframe[data_column].values xs = [np.arange(0, v.size) for v in ys] self.frame_slider.update(start=0, end=ys[0].size - 1, value=0) df = self.dataframe.drop( columns=[c for c in self.dataframe.columns if c not in self.tooltip_columns] ).copy(deep=True) colors_column = self.curve_color_selector.value ncolors = df[colors_column].unique().size if ncolors < 11: cmap = 'tab10' elif 10 < ncolors < 21: cmap = 'tab20' else: cmap = 'hsv' src = ColumnDataSource( { **df, 'xs': xs, 'ys': ys, 'colors': map_labels_to_colors(df[colors_column], cmap, output='bokeh') } ) if self.curve_figure is not None: self.doc.remove_root(self.curve_figure) del self.curve_figure # New figure has to be created each time self.curve_figure = figure( tooltips=self.tooltips, **self.curve_figure_params, ) self.curve_glyph = self.curve_figure.multi_line( xs='xs', ys='ys', legend=colors_column, line_color='colors', line_width=2, source=src ) # add the new curve plot to the doc root self.doc.add_root(self.curve_figure) print(">>>> DATAFRAME IS <<<<<<") print(self.dataframe) def set_dashboard(self, figures: List[Figure]): print('setting dashboard') self.doc.add_root( column( row(*(f for f in figures), self.image_figure), row(self.curve_data_selector, self.curve_color_selector), self.label_filesize, self.frame_slider ) )