def modify_doc(doc): source = ColumnDataSource(dict(x=[1, 2], y=[1, 1], val=["a", "b"])) plot = Plot(plot_height=400, plot_width=400, x_range=Range1d(0, 1), y_range=Range1d(0, 1), min_border=0) plot.add_glyph(source, Circle(x='x', y='y', size=20)) plot.add_tools(CustomAction(callback=CustomJS(args=dict(s=source), code=RECORD("data", "s.data")))) group = CheckboxButtonGroup(labels=LABELS, css_classes=["foo"]) def cb(active): source.data['val'] = (active + [0, 0])[:2] # keep col length at 2, padded with zero group.on_click(cb) doc.add_root(column(group, plot))
class DriftWidget: "Allows removing the drifts" __widget: CheckboxButtonGroup def __init__(self, ctrl, tasks: CyclesModelAccess) -> None: self.__theme = ctrl.theme.add(DriftWidgetTheme()) self.__tasks = tasks def addtodoc(self, mainview, ctrl) -> List[Widget]: "creates the widget" self.__widget = CheckboxButtonGroup(labels=self.__theme.labels, name='Cycles:DriftWidget', width=self.__theme.width, height=self.__theme.height, css_classes=[self.__theme.title], **self.__data()) self.__widget.on_click(mainview.actionifactive(ctrl)(self._onclick_cb)) return [self.__widget] def _onclick_cb(self, value): "action to be performed when buttons are clicked" for ind, name in enumerate(('driftperbead', 'driftpercycle')): attr = getattr(self.__tasks, name) task = attr.task if (ind not in value) != (task is None): getattr(self.__tasks, name).update(disabled=ind not in value) def reset(self, resets: CACHE_TYPE): "updates the widget" resets[self.__widget].update(**self.__data()) def __data(self) -> dict: value: List[int] = [] if self.__tasks.driftperbead.task is not None: value = [0] if self.__tasks.driftpercycle.task is not None: value += [1] return dict(active=value)
def add_item(): item_price_input = TextInput(value="[Preis]", width=140) item_name_input = TextInput(value="[Produkt]", width=140) toggle_as_coupon_button = CheckboxButtonGroup(labels=['G'], active=[], width_policy="min") remove_item_button = Button(label='X', width_policy='min', button_type='warning') this_layout = row(item_price_input, item_name_input, toggle_as_coupon_button, remove_item_button) def remove_this_item(*args): item_data_column_layout.children.remove(this_layout) if len(item_data_column_layout.children) == 0: add_item() #avoid empty layout else: update_coupon_and_endsum() remove_item_button.on_click(remove_this_item) toggle_as_coupon_button.on_click(update_coupon_and_endsum) item_price_input.on_change('value', update_coupon_and_endsum_wrapper) item_data_column_layout.children.append(this_layout) update_coupon_and_endsum()
def check_btn_callback(attr): if 1 in attr: print("SYMBOLS ON") labels.visible = True else: print("SYMBOLS OFF") labels.visible = False if 0 in attr: print("LEGEND ON") plot.legend.visible = True else: print("LEGEND OFF") plot.legend.visible = False checkbox_button_group.on_click(check_btn_callback) date_picker = DatePicker(value="2019-01-22", min_date="2019-01-22", max_date="2020-11-22") as_slider = Slider(start=1, end=10, value=5, step=.1, title="animation speed") l_slider = RangeSlider(start=-1.00, end=1.00, value=(0.75, 1.0), step=0.01, title="show links for") spacer = Spacer(width=240, height=700) controls = [ spacer, date_picker, af_button, ab_button, as_slider, l_slider, checkbox_button_group
class BokehBaseExplorer(Loggable, ABC): """ ???+ note "Base class for visually exploring data with `Bokeh`." Assumes: - in supplied dataframes - (always) xy coordinates in `x` and `y` columns; - (always) an index for the rows; - (always) classification label (or ABSTAIN) in a `label` column. Does not assume: - a specific form of data; - what the map serves to do. """ SUBSET_GLYPH_KWARGS = {} MANDATORY_COLUMNS = ["label", "x", "y"] TOOLTIP_KWARGS = {"label": True, "coords": True, "index": True} def __init__(self, df_dict, **kwargs): """ ???+ note "Constructor shared by all child classes." | Param | Type | Description | | :---------- | :----- | :--------------------------- | | `df_dict` | `dict` | `str` -> `DataFrame` mapping | | `**kwargs` | | forwarded to `bokeh.plotting.figure` | 1. settle the figure settings by using child class defaults & kwargs overrides 2. settle the glyph settings by using child class defaults 3. create widgets that child classes can override 4. create data sources the correspond to class-specific data subsets. 5. activate builtin search callbacks depending on the child class. 6. initialize a figure under the settings above """ self.figure_kwargs = { "tools": STANDARD_PLOT_TOOLS, "tooltips": self._build_tooltip(kwargs.pop("tooltips", "")), # bokeh recommends webgl for scalability "output_backend": "webgl", } self.figure_kwargs.update(kwargs) self.figure = figure(**self.figure_kwargs) self.glyph_kwargs = { _key: _dict["constant"].copy() for _key, _dict in self.__class__.SUBSET_GLYPH_KWARGS.items() } self._setup_dfs(df_dict) self._setup_sources() self._setup_widgets() self._activate_search_builtin() @classmethod def from_dataset(cls, dataset, subset_mapping, *args, **kwargs): """ ???+ note "Alternative constructor from a `SupervisableDataset`." | Param | Type | Description | | :--------------- | :----- | :--------------------------- | | `dataset` | `SupervisableDataset` | dataset with `DataFrame`s | | `subset_mapping` | `dict` | `dataset` -> `explorer` subset mapping | | `*args` | | forwarded to the constructor | | `**kwargs` | | forwarded to the constructor | """ # local import to avoid import cycles from hover.core.dataset import SupervisableDataset assert isinstance(dataset, SupervisableDataset) df_dict = {_v: dataset.dfs[_k] for _k, _v in subset_mapping.items()} return cls(df_dict, *args, **kwargs) def view(self): """ ???+ note "Define the high-level visual layout of the whole explorer." """ from bokeh.layouts import column return column(self._layout_widgets(), self.figure) def _build_tooltip(self, extra): """ ???+ note "Define a windowed tooltip which shows inspection details." | Param | Type | Description | | :--------------- | :----- | :--------------------------- | | `extra` | `str` | user-supplied extra HTML | Note that this is a method rather than a class attribute because child classes may involve instance attributes in the tooltip. """ standard = bokeh_hover_tooltip(**self.__class__.TOOLTIP_KWARGS) return f"{standard}\n{extra}" def _setup_widgets(self): """ ???+ note "High-level function creating widgets for interactive functionality." """ self._info("Setting up widgets") self._dynamic_widgets = OrderedDict() self._dynamic_callbacks = OrderedDict() self._setup_search_highlight() self._setup_selection_option() self._setup_subset_toggle() @abstractmethod def _layout_widgets(self): """ ???+ note "Define the low-level layout of widgets." """ pass @abstractmethod def _setup_search_highlight(self): """ ???+ note "Define how to search and highlight data points." Left to child classes that have a specific feature format. """ pass def _setup_selection_option(self): """ ???+ note "Create a group of checkbox(es) for advanced selection options." """ from bokeh.models import CheckboxGroup self.selection_option_box = CheckboxGroup( labels=["cumulative selection"], active=[] ) def _setup_subset_toggle(self): """ ???+ note "Create a group of buttons for toggling which data subsets to show." """ from bokeh.models import CheckboxButtonGroup data_keys = list(self.__class__.SUBSET_GLYPH_KWARGS.keys()) self.data_key_button_group = CheckboxButtonGroup( labels=data_keys, active=list(range(len(data_keys))) ) def update_data_key_display(active): visible_keys = {self.data_key_button_group.labels[idx] for idx in active} for _renderer in self.figure.renderers: # if the renderer has a name "on the list", update its visibility if _renderer.name in self.__class__.SUBSET_GLYPH_KWARGS.keys(): _renderer.visible = _renderer.name in visible_keys # store the callback (useful, for example, during automated tests) and link it self._callback_subset_display = lambda: update_data_key_display( self.data_key_button_group.active ) self.data_key_button_group.on_click(update_data_key_display) def value_patch(self, col_original, col_patch, **kwargs): """ ???+ note "Allow source values to be dynamically patched through a slider." | Param | Type | Description | | :--------------- | :----- | :--------------------------- | | `col_original` | `str` | column of values before the patch | | `col_patch` | `str` | column of list of values to use as patches | | `**kwargs` | | forwarded to the slider | [Reference](https://github.com/bokeh/bokeh/blob/2.3.0/examples/howto/patch_app.py) """ # add a patch slider to widgets, if none exist if "patch_slider" not in self._dynamic_widgets: slider = Slider(start=0, end=1, value=0, step=1, **kwargs) slider.disabled = True self._dynamic_widgets["patch_slider"] = slider else: slider = self._dynamic_widgets["patch_slider"] # create a slider-adjusting callback exposed to the outside def adjust_slider(): """ Infer slider length from the number of patch values. """ num_patches = None for _key, _df in self.dfs.items(): assert ( col_patch in _df.columns ), f"Subset {_key} expecting column {col_patch} among columns, got {_df.columns}" # find all array lengths; note that the data subset can be empty _num_patches_seen = _df[col_patch].apply(len).values assert ( len(set(_num_patches_seen)) <= 1 ), f"Expecting consistent number of patches, got {_num_patches_seen}" _num_patches = _num_patches_seen[0] if _df.shape[0] > 0 else None # if a previous subset has implied the number of patches, run a consistency check if num_patches is None: num_patches = _num_patches else: assert ( num_patches == _num_patches ), f"Conflicting number of patches: {num_patches} vs {_num_patches}" assert num_patches >= 2, f"Expecting at least 2 patches, got {num_patches}" slider.end = num_patches - 1 slider.disabled = False self._dynamic_callbacks["adjust_patch_slider"] = adjust_slider # create the callback for patching values def update_patch(attr, old, new): for _key, _df in self.dfs.items(): # calculate the patch corresponding to slider value _value = [_arr[new] for _arr in _df[col_patch].values] _slice = slice(_df.shape[0]) _patch = {col_original: [(_slice, _value)]} self.sources[_key].patch(_patch) slider.on_change("value", update_patch) self._good(f"Patching {col_original} using {col_patch}") def _setup_dfs(self, df_dict, copy=False): """ ???+ note "Check and store DataFrames **by reference by default**." Intended to be extended in child classes for pre/post processing. | Param | Type | Description | | :---------- | :----- | :--------------------------- | | `df_dict` | `dict` | `str` -> `DataFrame` mapping | | `copy` | `bool` | whether to copy `DataFrame`s | """ self._info("Setting up DataFrames") supplied_keys = set(df_dict.keys()) expected_keys = set(self.__class__.SUBSET_GLYPH_KWARGS.keys()) # perform high-level df key checks supplied_not_expected = supplied_keys.difference(expected_keys) expected_not_supplied = expected_keys.difference(supplied_keys) for _key in supplied_not_expected: self._warn( f"{self.__class__.__name__}.__init__(): got unexpected df key {_key}" ) for _key in expected_not_supplied: self._warn( f"{self.__class__.__name__}.__init__(): missing expected df key {_key}" ) # create df with column checks self.dfs = dict() for _key, _df in df_dict.items(): if _key in expected_keys: for _col in self.__class__.MANDATORY_COLUMNS: if _col not in _df.columns: # edge case: DataFrame has zero rows assert ( _df.shape[0] == 0 ), f"Missing column '{_col}' from non-empty {_key} DataFrame: found {list(_df.columns)}" _df[_col] = None self.dfs[_key] = _df.copy() if copy else _df def _setup_sources(self): """ ???+ note "Create, **(not update)**, `ColumnDataSource` objects." Intended to be extended in child classes for pre/post processing. """ self._info("Setting up sources") self.sources = {_key: ColumnDataSource(_df) for _key, _df in self.dfs.items()} self._postprocess_sources() # initialize attributes that couple with sources # extra columns for dynamic plotting self._extra_source_cols = defaultdict(dict) self._setup_selection_tools() def _setup_selection_tools(self): """ ???+ note "Create data structures and callbacks for dynamic selections." Useful for linking and filtering selections across explorers. """ from bokeh.events import SelectionGeometry # store the last manual selections self._last_selections = { _key: RootUnionFind(set()) for _key in self.sources.keys() } # store commutative, idempotent index filters self._selection_filters = { _key: RootUnionFind(set()) for _key in self.sources.keys() } def cumulative_selection_flag(): """ Determine whether cumulative selection is enabled. """ return bool(0 in self.selection_option_box.active) def store_selection(): """ Keep track of the last manual selection. Useful for applying cumulation / filters dynamically. """ # store selection indices for _key, _source in self.sources.items(): _selected = _source.selected.indices # use clear() and update() instead of assignment to keep clean references if not cumulative_selection_flag(): self._last_selections[_key].data.clear() self._last_selections[_key].data.update(_selected) else: self._last_selections[_key].data.update(_selected) _source.selected.indices = list(self._last_selections[_key].data) def trigger_selection_filters(subsets=None): """ Filter selection indices on specified subsets. """ if subsets is None: subsets = self.sources.keys() else: assert set(subsets).issubset( self.sources.keys() ), f"Expected subsets from {self.sources.keys()}" for _key in subsets: _selected = self._last_selections[_key].data for _func in self._selection_filters[_key].data: _selected = _func(_selected, _key) self.sources[_key].selected.indices = list(_selected) # keep reference to trigger_store_selection() for testing only self._store_selection = store_selection self.figure.on_event( SelectionGeometry, lambda event: self._store_selection() if event.final else None, ) # keep reference to trigger_selection_filter() for further access # for example, toggling filters should call the trigger self._trigger_selection_filters = trigger_selection_filters self.figure.on_event( SelectionGeometry, lambda event: self._trigger_selection_filters() if event.final else None, ) def _update_sources(self): """ ???+ note "Update the sources with the corresponding dfs." Note that the shapes and fields of sources are overriden. Thus supplementary fields (those that do not exist in the dfs), such as dynamic plotting kwargs, need to be re-assigned. """ for _key in self.dfs.keys(): self.sources[_key].data = self.dfs[_key] self._postprocess_sources() # self._activate_search_builtin(verbose=False) # reset attribute values that couple with sources for _key in self.sources.keys(): _num_points = len(self.sources[_key].data["label"]) # add extra columns for _col, _fill_value in self._extra_source_cols[_key].items(): self.sources[_key].add([_fill_value] * _num_points, _col) # clear last selection but keep the set object self._last_selections[_key].data.clear() # DON'T DO: self._last_selections = {_key: set() for _key in self.sources.keys()} def _postprocess_sources(self): """ ???+ note "Infer source attributes from the dfs, without altering the dfs." Useful for assigning dynamic glyph attributes, similarly to `activate_search()`. """ pass def _activate_search_builtin(self, verbose=True): """ ???+ note "Assign Highlighting callbacks to search results in a manner built into the class." Typically called once during initialization. Note that this is a template method which heavily depends on class attributes. | Param | Type | Description | | :---------- | :----- | :--------------------------- | | `verbose` | `bool` | whether to log verbosely | """ for _key, _dict in self.__class__.SUBSET_GLYPH_KWARGS.items(): if _key in self.sources.keys(): # determine responding attributes _responding = list(_dict["search"].keys()) # create a field that holds search results that could be used elsewhere _num_points = len(self.sources[_key].data["label"]) self._extra_source_cols[_key][SEARCH_SCORE_FIELD] = 0 self.sources[_key].add([0] * _num_points, SEARCH_SCORE_FIELD) # make attributes respond to search for _flag, _params in _dict["search"].items(): self.glyph_kwargs[_key] = self.activate_search( _key, self.glyph_kwargs[_key], altered_param=_params, ) if verbose: self._info( f"Activated {_responding} on subset {_key} to respond to the search widgets." ) @abstractmethod def activate_search(self, subset, kwargs, altered_param=("size", 10, 5, 7)): """ ???+ note "Left to child classes that have a specific feature format." | Param | Type | Description | | :-------------- | :------ | :--------------------------- | | `subset` | `str` | the subset to activate search on | | `kwargs` | `bool` | kwargs for the plot to add to | | `altered_param` | `tuple` | (attribute, positive, negative, default) | """ pass def _prelink_check(self, other): """ ???+ note "Sanity check before linking two explorers." | Param | Type | Description | | :------ | :------ | :----------------------------- | | `other` | `BokehBaseExplorer` | the other explorer | """ assert other is not self, "Self-loops are fordidden" assert isinstance(other, BokehBaseExplorer), "Must link to BokehBaseExplorer" def link_selection(self, key, other, other_key): """ ???+ note "Synchronize the selected indices between specified sources." | Param | Type | Description | | :------ | :------ | :----------------------------- | | `key` | `str` | the key of the subset to link | | `other` | `BokehBaseExplorer` | the other explorer | | `other_key` | `str` | the key of the other subset | """ self._prelink_check(other) # link selection in a bidirectional manner sl, sr = self.sources[key], other.sources[other_key] def left_to_right(attr, old, new): sr.selected.indices = sl.selected.indices[:] def right_to_left(attr, old, new): sl.selected.indices = sr.selected.indices[:] sl.selected.on_change("indices", left_to_right) sr.selected.on_change("indices", right_to_left) # link last manual selections (pointing to the same set) self._last_selections[key].union(other._last_selections[other_key]) # link selection filter functions (pointing to the same set) self._selection_filters[key].data.update( other._selection_filters[other_key].data ) self._selection_filters[key].union(other._selection_filters[other_key]) def link_selection_options(self, other): """ ???+ note "Synchronize the selection option values between explorers." | Param | Type | Description | | :------ | :------ | :----------------------------- | | `other` | `BokehBaseExplorer` | the other explorer | """ def left_to_right(attr, old, new): other.selection_option_box.active = self.selection_option_box.active[:] def right_to_left(attr, old, new): self.selection_option_box.active = other.selection_option_box.active[:] self.selection_option_box.on_change("active", left_to_right) other.selection_option_box.on_change("active", right_to_left) def link_xy_range(self, other): """ ???+ note "Synchronize plotting ranges on the xy-plane." | Param | Type | Description | | :------ | :------ | :----------------------------- | | `other` | `BokehBaseExplorer` | the other explorer | """ self._prelink_check(other) # link coordinate ranges in a bidirectional manner for _attr in ["start", "end"]: self.figure.x_range.js_link(_attr, other.figure.x_range, _attr) self.figure.y_range.js_link(_attr, other.figure.y_range, _attr) other.figure.x_range.js_link(_attr, self.figure.x_range, _attr) other.figure.y_range.js_link(_attr, self.figure.y_range, _attr) @abstractmethod def plot(self, *args, **kwargs): """ ???+ note "Plot something onto the figure." Implemented in child classes based on their functionalities. | Param | Type | Description | | :--------- | :---- | :-------------------- | | `*args` | | left to child classes | | `**kwargs` | | left to child classes | """ pass def auto_color_mapping(self): """ ???+ note "Find all labels and an appropriate color for each." """ from hover.utils.bokeh_helper import auto_label_color labels = set() for _key in self.dfs.keys(): labels = labels.union(set(self.dfs[_key]["label"].values)) return auto_label_color(labels)
for c in plotted.copy(): if c not in new: print('-', checkbox.labels[c]) # del_dateline(checkbox.labels[c]) plotted.remove(c) layout.children[1].children=[plot_t(), plot_c()] def clearcountries(): checkbox.active=[] def plottype_handler(new): layout.children[1].children[0]=plot_t() def c_period_handler(attr, old, new): calc_lastP(int(new)) for country, pop in population.items(): if country+'_lastP' in plottimeline: del plottimeline[country+'_lastP'] layout.children[1].children[1]=plot_c() checkbox.on_click(update_country) btn_clear.on_click(clearcountries) btng_main.on_click(plottype_handler) slide_c_period.on_change('value', c_period_handler) # Set up layouts and add to document inputs = column(btng_main, slide_c_period, btn_clear, checkbox) outputs = column(plot_t(),plot_c()) layout = row(inputs,outputs) curdoc().add_root(layout) curdoc().title = "Plot"
class Widgets: def __init__(self, renderer): self.file_input = Dropdown(label="Select dataset", menu=[]) for dataset_name in SQLDBInformer(DbConn({})).get_all_schemas(): if dataset_name in set(["information_schema", "performance_schema", "sys", "defaultDB", "mysql"]): continue if dataset_name[:3] == "pg_": continue self.file_input.menu.append(dataset_name) self.run_id_dropdown = Dropdown(label="select run id here", menu=[]) self.ground_truth_id_dropdown = Dropdown(label="select ground truth id here", menu=[]) self.score_slider = RangeSlider(start=0, end=1, value=(0, 1), step=.1, callback_policy='mouseup', title="score range") self.max_elements_slider = Slider(start=1000, end=100000, value=10000, step=1000, callback_policy='mouseup', title="max render") self.range_link_radio = RadioGroup(labels=["Link read plot to x-range", "Link read plot to y-range"], active=0, orientation="horizontal") self.full_render_button = Button(label="render without limit") self.render_mems_button = Button(label="render MEMs") self.delete_button = Button(label="Delete Dataset") self.force_read_id = TextInput(value="", title="Render reads with ids (comma seperated list):") self.file_input.on_change("value", lambda x,y,z: self.file_input_change(renderer)) self.run_id_dropdown.on_change("value", lambda x,y,z: self.run_id_change(renderer)) self.ground_truth_id_dropdown.on_change("value", lambda x,y,z: self.ground_id_change(renderer)) self.score_slider.on_change("value_throttled", lambda x,y,z: self.slider_change(renderer)) self.max_elements_slider.on_change("value_throttled", lambda x,y,z: self.slider_change(renderer)) self.full_render_button.on_event(ButtonClick, lambda x: self.full_render(renderer)) self.render_mems_button.on_event(ButtonClick, lambda x: self.render_mems_button_event(renderer)) self.delete_button.on_event(ButtonClick, lambda x: self.delete_button_event(renderer)) self.force_read_id.on_change("value", lambda x,y,z: self.forced_read_ids_change(renderer)) self.spinner_div = Div(text=html_file("spinner"), sizing_mode="scale_both", visible=False) self.condition = threading.Condition() self.subset_buttons = CheckboxButtonGroup(labels=["Render false-positives", "Render false-negatives", "Render true-positives", "Compute Stats"], active=[0, 1, 2]) self.subset_buttons.on_click(lambda x: self.forced_read_ids_change(renderer)) self.blur_slider = Slider(start=0, end=500, value=100, step=1, callback_policy='mouseup', title="Blur") self.blur_slider.on_change("value_throttled", lambda x,y,z: self.slider_change(renderer)) def show_spinner(self, renderer): self.spinner_div.visible = True def hide_spinner(self, renderer): self.spinner_div.visible = False def file_input_change(self, renderer): with self.condition: self.file_input.label = "Selected dataset: " + self.file_input.value renderer.setup() def run_id_change(self, renderer): with self.condition: renderer.read_plot.recalc_stat = True print("new run_id:", self.run_id_dropdown.value) renderer.cached_global_overview = None run_table = SvCallerRunTable(renderer.db_conn) self.run_id_dropdown.label = "Selected run: " + run_table.getName(int(self.run_id_dropdown.value)) + \ " - " + self.run_id_dropdown.value call_table = SvCallTable(renderer.db_conn) self.score_slider.end = 0 if call_table.num_calls(int(self.run_id_dropdown.value), 0) > 0: self.score_slider.end = call_table.max_score(int(self.run_id_dropdown.value)) + 1 self.score_slider.value = (0, self.score_slider.end) renderer.render(ignorable=False) def ground_id_change(self, renderer): with self.condition: renderer.read_plot.recalc_stat = True run_table = SvCallerRunTable(renderer.db_conn) self.ground_truth_id_dropdown.label = "Selected ground truth: " + \ run_table.getName(int(self.ground_truth_id_dropdown.value)) + \ " - " + self.ground_truth_id_dropdown.value renderer.render(ignorable=False) def slider_change(self, renderer): renderer.read_plot.recalc_stat = True renderer.render(ignorable=False) def forced_read_ids_change(self, renderer): renderer.render(ignorable=False) def full_render(self, renderer): renderer.render(render_all=True, ignorable=False) def get_blur(self): return self.blur_slider.value def get_render_f_p(self): return 0 in self.subset_buttons.active def get_render_f_n(self): return 1 in self.subset_buttons.active def get_render_t_p(self): return 2 in self.subset_buttons.active def compute_stats(self): return 3 in self.subset_buttons.active def get_forced_read_ids(self, renderer): if len(self.force_read_id.value) == 0: return [] read_table = ReadTable(renderer.db_conn) ret = [] for id_n_name in self.force_read_id.value.split(";"): split = id_n_name.split(":") if not len(split) == 2: continue seq_id, name = split idx = read_table.get_read_id(int(seq_id), name) if idx == -1: print(name, "does not exist in DB") continue ret.append(idx) return ret def render_mems_button_event(self, renderer): if not renderer.selected_read_id is None: read = ReadTable(renderer.db_conn).get_read(renderer.selected_read_id) seed_plot_y_s = int(max(renderer.read_plot.plot.y_range.start, 0)) seed_plot_y_e = int(min(renderer.read_plot.plot.y_range.end, len(read))) seed_plot_x_s = int(max(renderer.read_plot.plot.x_range.start, 0)) seed_plot_x_e = int(min(renderer.read_plot.plot.x_range.end, renderer.pack.unpacked_size_single_strand)) filtered_mems = compute_seeds_area(renderer.params, seed_plot_x_s, seed_plot_y_s, seed_plot_x_e-seed_plot_x_s, seed_plot_y_e-seed_plot_y_s, read, renderer.pack) seed_data_new = dict((key, []) for key in renderer.read_plot.seeds.data.keys()) if len(filtered_mems) > 0: max_seed_size = max( seed.size for seed in filtered_mems ) for idx in range(len(filtered_mems)): add_seed(filtered_mems[idx], seed_data_new, max_seed_size, [], [], 0, False, -1, renderer.selected_read_id, idx, read.name) renderer.read_plot.seeds.data = seed_data_new def delete_button_event(self, renderer): print("unimplemented at the moment...") #renderer.sv_db.delete_run(renderer.get_run_id()) renderer.setup()
None elif HPC and ('Nuclear with HPC' not in showcols): showcols.append('Nuclear with HPC') if 'All other' not in showcols: try: showcols.remove('All other - including lagoon') except: None elif lagoon and ('All other - including lagoon' not in showcols): showcols.append('All other - including lagoon') showLines(showcols) checkbuttons.on_click(checkboxCallback) HPCButton = Toggle(label='Add Hinkley Point C', button_type='default') def addHPC(clicked): global HPC HPC = clicked if HPC and ('Nuclear' not in showcols): showcols.append('Nuclear') if HPC: showcols.append('Nuclear with HPC') HPCButton.button_type = 'success' elif 'Nuclear with HPC' in showcols: showcols.remove('Nuclear with HPC') if not HPC:
# TODO: enable widgets that support multi-selection # Elements selection widget from a periodic table code = Select(title='Code', value=codes[0], options=codes) code.on_change('value', lambda attr, old, new: CF.update_code()) exchange = Select(title='ExchangeCorrelation', value=exchanges[0], options=exchanges) exchange.on_change('value', lambda attr, old, new: CF.update_exchange()) struct = Select(title='Structure', value=structures[0], options=structures) struct.on_change('value', lambda attr, old, new: CF.update_struct()) element = CheckboxButtonGroup(labels=_elements, active=[1]) element.on_click(CF.update_element) prop = Select(title='Property', value=properties[0], options=properties) prop.on_change('value', lambda attr, old, new: CF.update_prop()) apply_crossfilter = Button(label='CrossFilter and Plot') apply_crossfilter.on_click(CF.update_crossfilter) clean_crossfilter = Button(label='Clear') clean_crossfilter.on_click(CF.clear_crossfilter) x_select.on_change('value', lambda attr, old, new: CF.update_x()) y_select.on_change('value', lambda attr, old, new: CF.update_y()) analyse_crossfilt = Button(label='PadeAnalysis')
# define the selection widgets for code, exchange, # TODO: enable widgets that support multi-selection # Elements selection widget from a periodic table code = Select(title='Code', value=codes[0], options=codes) code.on_change('value', lambda attr, old, new: CF.update_code()) exchange = Select(title='ExchangeCorrelation', value=exchanges[0], options=exchanges) exchange.on_change('value', lambda attr, old, new: CF.update_exchange()) struct = Select(title='Structure', value=structures[0], options=structures) struct.on_change('value', lambda attr, old, new: CF.update_struct()) element = CheckboxButtonGroup(labels=_elements, active=[1]) element.on_click(CF.update_element) prop = Select(title='Property', value=properties[0], options=properties) prop.on_change('value', lambda attr, old, new: CF.update_prop()) apply_crossfilter = Button(label='CrossFilter and Plot') apply_crossfilter.on_click(CF.update_crossfilter) clean_crossfilter = Button(label='Clear') clean_crossfilter.on_click(CF.clear_crossfilter) x_select.on_change('value', lambda attr, old, new: CF.update_x()) y_select.on_change('value', lambda attr, old, new: CF.update_y()) analyse_crossfilt = Button(label='PadeAnalysis')
class BokehForLabeledText(Loggable, ABC): """ Base class that keeps template explorer settings. Assumes: - in supplied dataframes - (always) text data in a `text` column - (always) xy coordinates in `x` and `y` columns - (always) an index for the rows - (likely) classification label in a `label` column Does not assume: - what the explorer serves to do. """ DEFAULT_FIGURE_KWARGS = { "tools": [ # change the scope "pan", "wheel_zoom", # make selections "tap", "poly_select", "lasso_select", # make inspections "hover", # navigate changes "undo", "redo", ], # inspection details "tooltips": bokeh_hover_tooltip(label=True, text=True, image=False, coords=True, index=True), # bokeh recommends webgl for scalability "output_backend": "webgl", } DATA_KEY_TO_KWARGS = {} MANDATORY_COLUMNS = ["text", "label", "x", "y"] def __init__(self, df_dict, **kwargs): """ Operations shared by all child classes. - settle the figure settings by using child class defaults & kwargs overrides - settle the glyph settings by using child class defaults - create widgets that child classes can override - create data sources the correspond to class-specific data subsets. - activate builtin search callbacks depending on the child class. - create a (typically) blank figure under such settings """ self.figure_kwargs = self.__class__.DEFAULT_FIGURE_KWARGS.copy() self.figure_kwargs.update(kwargs) self.glyph_kwargs = { _key: _dict["constant"].copy() for _key, _dict in self.__class__.DATA_KEY_TO_KWARGS.items() } self._setup_widgets() self._setup_dfs(df_dict) self._setup_sources() self._activate_search_builtin() self.figure = figure(**self.figure_kwargs) self.reset_figure() @classmethod def from_dataset(cls, dataset, subset_mapping, *args, **kwargs): """ Construct from a SupervisableDataset. """ # local import to avoid import cycles from hover.core.dataset import SupervisableDataset assert isinstance(dataset, SupervisableDataset) df_dict = {_v: dataset.dfs[_k] for _k, _v in subset_mapping.items()} return cls(df_dict, *args, **kwargs) def reset_figure(self): """Start over on the figure.""" self._info("Resetting figure") self.figure.renderers.clear() def _setup_widgets(self): """ Prepare widgets for interactive functionality. Create positive/negative text search boxes. """ from bokeh.models import TextInput, CheckboxButtonGroup # set up text search widgets, without assigning callbacks yet # to provide more flexibility with callbacks self._info("Setting up widgets") self.search_pos = TextInput( title="Text contains (plain text, or /pattern/flag for regex):", width_policy="fit", height_policy="fit", ) self.search_neg = TextInput(title="Text does not contain:", width_policy="fit", height_policy="fit") # set up subset display toggles which do have clearly defined callbacks data_keys = list(self.__class__.DATA_KEY_TO_KWARGS.keys()) self.data_key_button_group = CheckboxButtonGroup( labels=data_keys, active=list(range(len(data_keys)))) def update_data_key_display(active): visible_keys = { self.data_key_button_group.labels[idx] for idx in active } for _renderer in self.figure.renderers: # if the renderer has a name "on the list", update its visibility if _renderer.name in self.__class__.DATA_KEY_TO_KWARGS.keys(): _renderer.visible = _renderer.name in visible_keys # store the callback (useful, for example, during automated tests) and link it self.update_data_key_display = update_data_key_display self.data_key_button_group.on_click(self.update_data_key_display) def _layout_widgets(self): """Define the layout of widgets.""" return column(self.search_pos, self.search_neg, self.data_key_button_group) def view(self): """Define the layout of the whole explorer.""" return column(self._layout_widgets(), self.figure) def _setup_dfs(self, df_dict, copy=False): """ Check and store DataFrames BY REFERENCE BY DEFAULT. Intended to be extended in child classes for pre/post processing. """ self._info("Setting up DataFrames") supplied_keys = set(df_dict.keys()) expected_keys = set(self.__class__.DATA_KEY_TO_KWARGS.keys()) # perform high-level df key checks supplied_not_expected = supplied_keys.difference(expected_keys) expected_not_supplied = expected_keys.difference(supplied_keys) for _key in supplied_not_expected: self._warn( f"{self.__class__.__name__}.__init__(): got unexpected df key {_key}" ) for _key in expected_not_supplied: self._warn( f"{self.__class__.__name__}.__init__(): missing expected df key {_key}" ) # create df with column checks self.dfs = dict() for _key, _df in df_dict.items(): if _key in expected_keys: for _col in self.__class__.MANDATORY_COLUMNS: if _col not in _df.columns: # edge case: DataFrame has zero rows assert ( _df.shape[0] == 0 ), f"Missing column '{_col}' from non-empty {_key} DataFrame: found {list(_df.columns)}" _df[_col] = None self.dfs[_key] = _df.copy() if copy else _df def _setup_sources(self): """ Create (NOT UPDATE) ColumnDataSource objects. Intended to be extended in child classes for pre/post processing. """ self._info("Setting up sources") self.sources = { _key: ColumnDataSource(_df) for _key, _df in self.dfs.items() } def _update_sources(self): """ Update the sources with the corresponding dfs. Note that it seems mandatory to re-activate the search widgets. This is because the source loses plotting kwargs. """ for _key in self.dfs.keys(): self.sources[_key].data = self.dfs[_key] self._activate_search_builtin(verbose=False) def _activate_search_builtin(self, verbose=True): """ Typically called once during initialization. Highlight positive search results and mute negative search results. Note that this is a template method which heavily depends on class attributes. """ for _key, _dict in self.__class__.DATA_KEY_TO_KWARGS.items(): if _key in self.sources.keys(): _responding = list(_dict["search"].keys()) for _flag, _params in _dict["search"].items(): self.glyph_kwargs[_key] = self.activate_search( self.sources[_key], self.glyph_kwargs[_key], altered_param=_params, ) if verbose: self._info( f"Activated {_responding} on subset {_key} to respond to the search widgets." ) def activate_search(self, source, kwargs, altered_param=("size", 10, 5, 7)): """ Enables string/regex search-and-highlight mechanism. Modifies the plotting source in-place. """ assert isinstance(source, ColumnDataSource) assert isinstance(kwargs, dict) updated_kwargs = kwargs.copy() param_key, param_pos, param_neg, param_default = altered_param num_points = len(source.data["text"]) default_param_list = [param_default] * num_points source.add(default_param_list, f"{param_key}") updated_kwargs[param_key] = param_key search_callback = CustomJS( args={ "source": source, "key_pos": self.search_pos, "key_neg": self.search_neg, "param_pos": param_pos, "param_neg": param_neg, "param_default": param_default, }, code=f""" const data = source.data; const text = data['text']; var arr = data['{param_key}']; """ + """ var search_pos = key_pos.value; var search_neg = key_neg.value; var valid_pos = (search_pos.length > 0); var valid_neg = (search_neg.length > 0); function determineAttr(candidate) { var score = 0; if (valid_pos) { if (candidate.search(search_pos) >= 0) { score += 1; } else { score -= 2; } }; if (valid_neg) { if (candidate.search(search_neg) < 0) { score += 1; } else { score -= 2; } }; if (score > 0) { return param_pos; } else if (score < 0) { return param_neg; } else {return param_default;} } function toRegex(search_key) { var match = search_key.match(new RegExp('^/(.*?)/([gimy]*)$')); if (match) { return new RegExp(match[1], match[2]); } else { return search_key; } } if (valid_pos) {search_pos = toRegex(search_pos);} if (valid_neg) {search_neg = toRegex(search_neg);} for (var i = 0; i < arr.length; i++) { arr[i] = determineAttr(text[i]); } source.change.emit() """, ) self.search_pos.js_on_change("value", search_callback) self.search_neg.js_on_change("value", search_callback) return updated_kwargs def _prelink_check(self, other): """ Sanity check before linking two explorers. """ assert other is not self, "Self-loops are fordidden" assert isinstance( other, BokehForLabeledText), "Must link to BokehForLabelText" def link_selection(self, key, other, other_key): """ Sync the selected indices between specified sources. """ self._prelink_check(other) # link selection in a bidirectional manner sl, sr = self.sources[key], other.sources[other_key] sl.selected.js_link("indices", sr.selected, "indices") sr.selected.js_link("indices", sl.selected, "indices") def link_xy_range(self, other): """ Sync plotting ranges on the xy-plane. """ self._prelink_check(other) # link coordinate ranges in a bidirectional manner for _attr in ["start", "end"]: self.figure.x_range.js_link(_attr, other.figure.x_range, _attr) self.figure.y_range.js_link(_attr, other.figure.y_range, _attr) other.figure.x_range.js_link(_attr, self.figure.x_range, _attr) other.figure.y_range.js_link(_attr, self.figure.y_range, _attr) @abstractmethod def plot(self, *args, **kwargs): """ Plot something onto the figure. """ pass def auto_labels_cmap(self): """ Find all labels and an appropriate color map. """ labels = set() for _key in self.dfs.keys(): labels = labels.union(set(self.dfs[_key]["label"].values)) labels.discard(module_config.ABSTAIN_DECODED) labels = sorted(labels, reverse=True) assert len(labels) <= 20, "Too many labels to support (max at 20)" cmap = "Category10_10" if len(labels) <= 10 else "Category20_20" return labels, cmap def auto_legend_correction(self): """ Find legend items and deduplicate by label. """ if not hasattr(self.figure, "legend"): self._fail( "Attempting auto_legend_correction when there is no legend") return # extract all items and start over items = self.figure.legend.items[:] self.figure.legend.items.clear() # use one item to hold all renderers matching its label label_to_item = OrderedDict() for _item in items: _label = _item.label.get("value", "") if _label not in label_to_item.keys(): label_to_item[_label] = _item else: label_to_item[_label].renderers.extend(_item.renderers) # assign deduplicated items back to the legend self.figure.legend.items = list(label_to_item.values()) return
renderer['lines'] = p2.line(x="xs", y="ys", line_width=2, source=d2) def callback_glyphs(new): print("Active Checkboxes:", new) renderer['circles'].visible = False renderer['lines'].visible = False if 0 in new: renderer['circles'].visible = True if 1 in new: renderer['lines'].visible = True btn_glyphs = CheckboxButtonGroup(labels=["Circles", "Lines"], active=[0, 1]) btn_glyphs.on_click(callback_glyphs) """ ======================= Two Axes ======================= """ d_axis_1 = ColumnDataSource(dict(xs=np.arange(0, 10), ys=np.random.rand(10))) d_axis_2 = ColumnDataSource( dict(xs=np.arange(0, 10), ys=np.random.rand(10) * 100)) p_axis = figure(plot_width=400, plot_height=400, y_range=(0.0, 1.0)) p_axis.line(x="xs", y="ys", line_width=2, source=d_axis_1)