예제 #1
0
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}
예제 #2
0
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
예제 #3
0
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
            )
        )
예제 #4
0
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
            )
        )