def test_js_on_change_executes(self, single_plot_page): source = ColumnDataSource(dict(x=[1, 2], y=[1, 1])) 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)) text_input = TextInput(css_classes=['foo']) text_input.js_on_change('value', CustomJS(code=RECORD("value", "cb_obj.value"))) page = single_plot_page(column(text_input, plot)) el = page.driver.find_element_by_class_name('foo') enter_text_in_element(page.driver, el, "val1") results = page.results assert results['value'] == 'val1' # double click to highlight and overwrite old text enter_text_in_element(page.driver, el, "val2", click=2) results = page.results assert results['value'] == 'val2' # Check clicking outside input also triggers enter_text_in_element(page.driver, el, "val3", click=2, enter=False) page.click_canvas_at_position(10, 10) results = page.results assert results['value'] == 'val3' assert page.has_no_console_errors()
def test_js_on_change_executes(self, single_plot_page): source = ColumnDataSource(dict(x=[1, 2], y=[1, 1])) 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)) text_input = TextInput(css_classes=['foo']) text_input.js_on_change('value', CustomJS(code=RECORD("value", "cb_obj.value"))) page = single_plot_page(column(text_input, plot)) el = page.driver.find_element_by_css_selector('.foo input') enter_text_in_element(page.driver, el, "val1") results = page.results assert results['value'] == 'val1' # double click to highlight and overwrite old text enter_text_in_element(page.driver, el, "val2", click=2) results = page.results assert results['value'] == 'val2' # Check clicking outside input also triggers enter_text_in_element(page.driver, el, "val3", click=2, enter=False) page.click_canvas_at_position(10, 10) results = page.results assert results['value'] == 'val3' assert page.has_no_console_errors()
def test_multi_row_copy(self, bokeh_model_page) -> None: data = {'x': [1,2,3,4], 'y': [0,1,2,3], 'd': ['foo', 'bar', 'baz', 'quux']} source = ColumnDataSource(data) table = DataTable(columns=[ TableColumn(field="x", title="x"), TableColumn(field="y", title="y"), TableColumn(field="d", title="d"), ], source=source) text_input = TextInput(css_classes=["foo"]) text_input.js_on_change('value', CustomJS(code=RECORD("value", "cb_obj.value"))) page = bokeh_model_page(column(table, text_input)) row = get_table_row(page.driver, 1) row.click() row = get_table_row(page.driver, 3) shift_click(page.driver, row) enter_text_in_element(page.driver, row, Keys.INSERT, mod=Keys.CONTROL, click=0, enter=False) input_el = page.driver.find_element_by_css_selector('.foo') enter_text_in_element(page.driver, input_el, Keys.INSERT, mod=Keys.SHIFT, enter=False) enter_text_in_element(page.driver, input_el, "") results = page.results # XXX (bev) these should be newlines with a TextAreaInput but TextAreaInput # is not working in tests for some reason presently assert results['value'] == '0\t1\t0\tfoo 1\t2\t1\tbar 2\t3\t2\tbaz' assert page.has_no_console_errors()
def test_single_row_copy_with_zero(self, bokeh_model_page) -> None: data = {'x': [1,2,3,4], 'y': [0,0,0,0], 'd': ['foo', 'bar', 'baz', 'quux']} source = ColumnDataSource(data) table = DataTable(columns=[ TableColumn(field="x", title="x"), TableColumn(field="y", title="y"), TableColumn(field="d", title="d"), ], source=source) text_input = TextInput(css_classes=["foo"]) text_input.js_on_change('value', CustomJS(code=RECORD("value", "cb_obj.value"))) page = bokeh_model_page(column(table, text_input)) row = get_table_row(page.driver, 2) row.click() enter_text_in_element(page.driver, row, Keys.INSERT, mod=Keys.CONTROL, click=0, enter=False) input_el = page.driver.find_element_by_css_selector('.foo') enter_text_in_element(page.driver, input_el, Keys.INSERT, mod=Keys.SHIFT, enter=False) enter_text_in_element(page.driver, input_el, "") sleep(0.5) results = page.results assert results['value'] == '1\t2\t0\tbar' assert page.has_no_console_errors()
from bokeh.io import show from bokeh.models import CustomJS, TextInput text_input = TextInput(value="default", title="Label:") text_input.js_on_change("value", CustomJS(code=""" console.log('text_input: value=' + this.value, this.toString()) """)) show(text_input)
def generate_bokeh_umap(self, media_type): output_notebook() topics = [] labels = [] for key, value in self.top_words_map.items(): topics.append(value) labels.append(key) if len(labels) >= 20000: reducer = umap.UMAP(n_neighbors=100, metric='hellinger') if len(labels) >= 5000: reducer = umap.UMAP(n_neighbors=50, metric='hellinger') else: reducer = umap.UMAP(metric='hellinger') X = self.vectorized_out.copy() X_embedded = reducer.fit_transform(X) # tsne = TSNE(verbose=1, perplexity=100, random_state=42) # X = self.vectorized_out # X_embedded = tsne.fit_transform(X.toarray()) df_tmp = pd.DataFrame(self.doc_topic_dists) df_tmp['topic'] = df_tmp.idxmax(axis=1) y_labels = df_tmp['topic'].values y_labels_new = [] for i in y_labels: y_labels_new.append(labels[i]) df = self.original_df.copy() # data sources if media_type == 'videos': source = ColumnDataSource( data=dict(x=X_embedded[:, 0], y=X_embedded[:, 1], x_backup=X_embedded[:, 0], y_backup=X_embedded[:, 1], desc=y_labels, ids=df['id'], titles=df['title'], published_times=df['first_airing'], text=df['text'], publication_end_times=df['publication_end_time'], media_availables=df['media_available'], duration_minutes=df['duration_minutes'], finnpanel_genres=df['finnpanel_genre'], labels=["Topic " + str(x) for x in y_labels_new], links=df['link'])) # hover over information hover = HoverTool( tooltips=[ ("Id", "@ids{safe}"), ("Title", "@titles{safe}"), ("Published", "@published_times{safe}"), # ("Text", "@texts{safe}"), ("Publication ends", "@publication_end_times{safe}"), ("Currently available", "@media_availables{safe}"), ("Duration (minutes)", "@duration_minutes{safe}"), ("Finnpanel genres", "@finnpanel_genres{safe}"), ("Link", "@links") ], point_policy="follow_mouse") elif media_type == 'articles': source = ColumnDataSource( data=dict(x=X_embedded[:, 0], y=X_embedded[:, 1], x_backup=X_embedded[:, 0], y_backup=X_embedded[:, 1], desc=y_labels, ids=df.index, titles=df['title'], published_times=df['published_time'].dt.strftime( '%Y-%m-%d %H:%M'), text=df['text'], labels=["Topic " + str(x) for x in y_labels_new], links=df['link'])) # hover over information hover = HoverTool( tooltips=[ ("Id", "@ids{safe}"), ("Title", "@titles{safe}"), ("Published", "@published_times{safe}"), # ("Text", "@texts{safe}"), ("Link", "@links") ], point_policy="follow_mouse") # map colors mapper = linear_cmap(field_name='desc', palette=Category20[20], low=min(y_labels), high=max(y_labels)) # prepare the figure plot = figure(plot_width=1200, plot_height=850, tools=[ hover, 'pan', 'wheel_zoom', 'box_zoom', 'reset', 'save', 'tap' ], title="Clustering of the content with UMAP and NMF", toolbar_location="above") # plot settings plot.scatter('x', 'y', size=5, source=source, fill_color=mapper, line_alpha=0.3, line_color="black", legend='labels') plot.legend.background_fill_alpha = 0.6 # Keywords text_banner = Paragraph( text='Keywords: Slide to specific cluster to see the keywords.', height=45) input_callback_1 = input_callback(plot, source, text_banner, topics, self.nr_of_topics) # currently selected article div_curr = Div( text="""Click on a plot to see the link to the article.""", height=150) if media_type == 'videos': callback_selected = CustomJS(args=dict(source=source, current_selection=div_curr), code=selected_code_videos()) elif media_type == 'articles': callback_selected = CustomJS(args=dict(source=source, current_selection=div_curr), code=selected_code_articles()) taptool = plot.select(type=TapTool) taptool.callback = callback_selected # WIDGETS slider = Slider( start=0, end=self.nr_of_topics, value=self.nr_of_topics, step=1, title="Topic #") #, js_event_callbacks=input_callback_1) slider.js_on_change("value", input_callback_1) keyword = TextInput( title="Search:") #, js_event_callbacks=input_callback_1) keyword.js_on_change("value", input_callback_1) # pass call back arguments input_callback_1.args["text"] = keyword input_callback_1.args["slider"] = slider # STYLE slider.sizing_mode = "stretch_width" slider.margin = 15 keyword.sizing_mode = "scale_both" keyword.margin = 15 div_curr.style = { 'color': '#BF0A30', 'font-family': 'Helvetica Neue, Helvetica, Arial, sans-serif;', 'font-size': '1.1em' } div_curr.sizing_mode = "scale_both" div_curr.margin = 20 text_banner.style = { 'color': '#0269A4', 'font-family': 'Helvetica Neue, Helvetica, Arial, sans-serif;', 'font-size': '1.1em' } text_banner.sizing_mode = "scale_both" text_banner.margin = 20 plot.sizing_mode = "scale_both" plot.margin = 5 r = row(div_curr, text_banner) r.sizing_mode = "stretch_width" # LAYOUT OF THE PAGE l = layout([ [slider, keyword], [text_banner], [div_curr], [plot], ]) l.sizing_mode = "scale_both" # show output_file('t-sne_interactive_streamlit.html') show(l) return (l)
class Carte_tronçons(Fond_de_carte): """Carte de tronçons de lignes. Arguments: tronçons: GeoDataFrame, table des tronçons Attributs: tron: table des tronçons avec habillage tron_idx_name: le nom de l'index des tronçons transformé en colonne dans `tron` """ titre_ref = "carte_tronçons" titre_tron = "Tronçons" # coordonnées arbitraires de la carte pour créer des objets de la légende x_legende, y_legende = [277409, 277409], [6248780, 6248780] tab_width = 500 def __init__(self, tronçons: DataFrame, **kwargs) -> None: self.tron = tronçons self.tron["line_width"] = self.line_width self.tron["line_color"] = self.line_color self.tron.reset_index(inplace=True) self.tron_idx_name = self.tron.columns[0] self.cols = self.tron.columns.drop( ["geometry", "line_width", "line_color"], errors="ignore") super().__init__(**kwargs) @property def cols_tron(self) -> list[TableColumn]: return [TableColumn(field=c, title=c) for c in self.cols] @property def line_width(self) -> Series: """Épaisseur de l'affichage des tronçons. Doit renvoyer une série alignable avec l'argument tronçons et peut y faire référence via self.tron. """ return 2 @property def line_color(self) -> Series: """Couleur de l'affichage des tronçons. Doit renvoyer une série alignable avec l'argument tronçons et peut y faire référence via self.tron. """ return "blue" @property def tooltips(self) -> list[tuple[str, str]]: return [(c, f"@{c}") for c in self.cols] @property def texte_aide(self) -> str: return """<b>Mode d'emploi</b> <p>La sélection d'une ou plusieurs lignes est possible directement sur la carte (shift + clic) ou dans la table (shift/ctrl + clic).</p> <p>On peut aussi sélectionner une ligne en indiquant son numéro de ligne et de rang dans l'entrée texte située en haut à droite. Le bouton "Affiche les extrémités" permet de rendre visibles ou non les extrémités des lignes sélectionnées.</p> <p>En cas de problème, utiliser l'outil reset sur la droite de la carte.</p> """ def ajoute_table_tron(self) -> None: view = CDSView(source=self.source_lines, filters=[self.filter]) self.table_tron = DataTable( source=self.source_lines, view=view, columns=self.cols_tron, autosize_mode="none", sizing_mode="stretch_height", width=self.tab_width, height=200, ) def ajoute_toggle_extrémités(self) -> None: size = 4 fill_color = "DarkSlateGray" self.g = (self.tron.set_index( self.tron_idx_name).geometry.boundary.dropna().explode().droplevel( 1).rename("geometry").reset_index().reset_index()) idx_g = self.g.columns[0] # colonne qui contient le numéro de ligne self.src_extr = GeoJSONDataSource(geojson=self.g.to_json()) self.filter_extr = IndexFilter(list(range(self.g.shape[0]))) self.index_extrémités_par_tron = ( self.tron.reset_index() # numéro de ligne dans la colonne idx_g .merge( self.g, on=self.tron_idx_name ) # inner join donc tous les tronçons non localisés n'y sont pas .groupby(f"{idx_g}_x").apply( lambda s: list(s[f"{idx_g}_y"])).to_dict()) view = CDSView(source=self.src_extr, filters=[self.filter_extr]) self.extr_renderer = self.p.circle( x="x", y="y", size=size, fill_color=fill_color, line_color=fill_color, source=self.src_extr, visible=False, view=view, ) self.toggle_extr = Toggle(label="Affiche les extrémités", button_type="success", width=100) self.toggle_extr.js_link("active", self.extr_renderer, "visible") def ajoute_lignes(self) -> None: self.p.multi_line( xs="xs", ys="ys", line_color="line_color", line_width="line_width", source=self.source_lines, name="tronçons", ) @property def callback_selected(self) -> CustomJS: return CustomJS( args=dict( src_lines=self.source_lines, src_extr=self.src_extr, filter=self.filter, filter_extr=self.filter_extr, index_extr_dict=self.index_extrémités_par_tron, ), code="""var sel = src_lines.selected.indices; if (sel.length == 0) { sel = [...Array(src_lines.length).keys()]; var sel2 = [...Array(src_extr.length).keys()]; } else { var sel2 = sel.flatMap(el => index_extr_dict[el]); } filter.indices = sel; filter_extr.indices = sel2; src_lines.change.emit(); src_extr.change.emit(); """, ) def ajoute_input_num(self, title: str = None, groupby: str = None, max_width=80) -> None: """Sélection du tronçon par une colonne de la table.""" if groupby is None: groupby = self.tron_idx_name if title is None: title = self.tron_idx_name index_par_tron = DataFrame(self.tron).groupby(groupby).apply( lambda s: list(s.index)).to_dict() self.input_num = TextInput(value="", title=title, max_width=max_width) callback_text = CustomJS( args=dict( text=self.input_num, src_lines=self.source_lines, index_par_tron=index_par_tron, ), code="""if (text.value in index_par_tron) { src_lines.selected.indices = index_par_tron[text.value] src_lines.change.emit(); } """, ) self.input_num.js_on_change("value", callback_text) def ajoute_légende(self) -> None: pass @property def titre_tron_div(self) -> Div: return Div(text=f"<b>{self.titre_tron}</b>") def init_layout(self) -> None: super().init_layout() geojson = self.tron.to_json().replace( "null", '{"type":"Point","coordinates":[]}') self.source_lines = GeoJSONDataSource(geojson=geojson) self.filter = IndexFilter(list(range(self.tron.shape[0]))) self.ajoute_lignes() self.ajoute_toggle_extrémités() self.source_lines.selected.js_on_change("indices", self.callback_selected) self.ajoute_table_tron() self.ajoute_input_num() self.hover_tool = self.p.select(type=HoverTool) self.hover_tool.names = ["tronçons"] self.ajoute_légende() self.première_ligne = row(self.input_num, self.toggle_extr) def cstr_layout(self) -> Box: return row( self.p, column( self.première_ligne, self.titre_tron_div, self.table_tron, self.questions, self.aide, ), )
value=1, title='Allele Frequency Cutoff') slider_af.js_on_change( 'value', sliderCallback(source_sample, depth_sample, slider, slider_af, syngroup, unique_longitudinal_muts)) slider.js_on_change( 'value', sliderCallback(source_sample, depth_sample, slider, slider_af, syngroup, unique_longitudinal_muts)) syngroup.js_on_change( 'active', sliderCallback(source_sample, depth_sample, slider, slider_af, syngroup, unique_longitudinal_muts)) ose.js_on_change( 'value', sliderCallback2(source_sample, depth_sample, slider, slider_af, syngroup, ose, unique_longitudinal_muts)) # When mousing over Bokeh plot, allele frequency updated to user input. if (user_af != -123): slider_af.value = user_af ## g.js_on_event(events.PlotEvent, sliderCallback(source_sample, depth_sample, slider, slider_af, syngroup)) genome_plot.js_on_event( events.MouseEnter, sliderCallback(source_sample, depth_sample, slider, slider_af, syngroup, unique_longitudinal_muts)) # Creates labels with read information reads_info = (reads.loc[reads['Sample'] == name.strip()]) div = Div(text="""<font size="2" color="gray"><b>Total reads: </b>""" + str(reads_info.iloc[0]["Total"]) +
return ClosingPricesDataFrame '''''' DataFrame = GetStockDataFrame(IbmUrl) source = ColumnDataSource(DataFrame) InputSymbol = "IBM" '''''' '''This callback does not currently work :( ''' Callback = CustomJS(args=dict(source=source), code=""" // JavaScript code goes here // the model that triggered the callback is cb_obj: // models passed as args are automagically available var InputSymbol = cb_obj.value var NewData = GetStockDataFrame(GetSubmitUrl(InputSymbol)) var NewDataSource = ColumnDataSource(NewData) var x = NewDataSource.Dates var y = NewDataSource.Prices source.change.emit(); """) '''''' PageGraph = figure(title="Last Month's Closing Prices", plot_width=600, plot_height=400, x_axis_type='datetime') PageGraph.line(x=DataFrame["Dates"], y=DataFrame["Prices"], line_width=2) TextInputObject = TextInput(value="IBM", title=WelcomeMessage) TextInputObject.js_on_change('value', Callback) Page = Column(TextInputObject, PageGraph) ShowIo(Page)
def home(): #Open file and create sources dictionary = open_file_into_dictionary('SampleCSV2.csv') keys = list(key.title() for key in dictionary.keys()) values = [value for value in dictionary.values()] xy_source = ColumnDataSource(data=dict(xs=[values[0]], ys=[values[1]], labels = [keys[1]], colors = ['red', 'green', 'blue', 'purple', 'brown', 'aqua'])) variables_source = ColumnDataSource(data = dict(keys = keys, values = values)) #Create general plot plot = figure(plot_width=800, plot_height=600, toolbar_location = 'left') plot.title.text_font= 'helvetica' plot.title.text_font_size = '24pt' plot.title.align = 'center' plot.title.text_font_style = 'normal' plot.multi_line(xs = 'xs', ys = 'ys', legend = 'labels', line_color = 'colors', source = xy_source) #Define callbacks x_axis_callback = CustomJS(args=dict(xy_source = xy_source, variables_source = variables_source, axis = plot.xaxis[0]), code=""" var xy_data = xy_source.data; var variables_data = variables_source.data; var index = cb_obj.active; var values = variables_data.values; var y_length = xy_data['ys'].length; if (y_length == 0){ y_length = 1} var new_list = []; for (i = 0; i < y_length; i++) { new_list.push(values[index])} xy_data['xs'] = new_list; xy_source.change.emit(); var keys = variables_data.keys; var label = keys[index]; axis.axis_label = label; """) y_axis_callback = CustomJS(args=dict(xy_source = xy_source, variables_source = variables_source, axis = plot.yaxis[0]), code=""" var xy_data = xy_source.data; var variables_data = variables_source.data; var index_list = cb_obj.active; var values = variables_data.values; var index_length = index_list.length; var keys = variables_data.keys; var new_ys = []; var new_labels = []; for (i = 0; i < index_length; i++) { new_ys.push(values[index_list[i]]); new_labels.push(keys[index_list[i]])} xy_data['labels'] = new_labels; xy_data['ys'] = new_ys; if (index_length > 0){ var x_variable = xy_data['xs'][0]; var new_x = []; for (i = 0; i < index_length; i++) { new_x.push(x_variable)} xy_data['xs'] = new_x;} xy_source.change.emit(); var y_axis_name = keys[[index_list[0]]]; for (i = 1; i < index_length; i++) { y_axis_name += ", " + keys[[index_list[i]]];} axis.axis_label = y_axis_name; """) title_callback = CustomJS(args= dict(title = plot.title), code=""" var title_text = cb_obj.value; title.text = title_text; """) x_name_callback = CustomJS(args=dict(axis = plot.xaxis[0]), code=""" var label_text = cb_obj.value; axis.axis_label = label_text; """) y_name_callback = CustomJS(args=dict(axis = plot.yaxis[0]), code=""" var label_text = cb_obj.value; axis.axis_label = label_text; """) #Create toolbox label_x = Div(text="""X-Axis""", width=200) x_axis = RadioButtonGroup(labels=keys, active=0, callback = x_axis_callback) label_y = Div(text="""Y-Axis""", width=200) y_axis = CheckboxButtonGroup(labels=keys, active=[1], callback = y_axis_callback) label_axes = Div(text="""<br />Modify Labels""", width=200) title_name = TextInput(title="Title", value='Default Title') plot.title.text = title_name.value title_name.js_on_change('value', title_callback) x_name = TextInput(title="X-Axis", value='Default X Label') plot.xaxis.axis_label = keys[0] x_name.js_on_change('value', x_name_callback) y_name = TextInput(title="Y-Axis", value='Default Y Label') plot.yaxis.axis_label = keys[1] y_name.js_on_change('value', y_name_callback) toolbox = widgetbox(label_x, x_axis, label_y, y_axis, label_axes, title_name, x_name, y_name) #Integrate with html parts = dict(toolbox = toolbox, plot = plot) script, div = components(parts, INLINE) return render_template('plotpage.html', script=script, toolbox_div=div['toolbox'], plot_div=div['plot'], js_resources=INLINE.render_js(), css_resources=INLINE.render_css())
def create_html(distances, text_list, file_path, num_similar_shown): source = ColumnDataSource(data=dict( ids=range(len(text_list)), distances=distances.tolist(), text=text_list, display_text=text_list, display_ids=range(len(text_list)), )) display_source = ColumnDataSource(data=dict( closest_text=[""] * num_similar_shown, closest_dist=[0] * num_similar_shown, )) columns = [ TableColumn(field="display_text", title="Text"), ] closest_columns = [ TableColumn(field="closest_text", title="Closest examples", width=510, editor=TextEditor()), TableColumn(field="closest_dist", title="Distance", width=10, editor=TextEditor()), ] str_search_input = TextInput(value="", title="Search feedback") callback = CustomJS(args=dict(source=source, display_source=display_source, search_text=str_search_input), code=""" const data = source.data; // ################## // First search // ################## const search_text_str = search_text.value.toLowerCase(); const display_texts = []; const display_ids = []; data['text'].map(function(e, i) { const text_val = data['text'][i]; const text_id = data['ids'][i]; if (text_val.toLowerCase().includes(search_text_str)){ display_texts.push(text_val); display_ids.push(text_id); } }); data['display_text'] = display_texts; data['display_ids'] = display_ids; source.change.emit(); // ################## // Then show selected // ################## const num_similar_shown = data['num_similar']; if(source.selected.indices.length >= 1){ const selected_table_idx = source.selected.indices[0]; if (selected_table_idx >= data['display_ids'].length){ console.log("Empty cell selected") }else{ const selected_idx = data['display_ids'][selected_table_idx]; console.log(selected_idx) const texts = data['text']; const list_of_dist = data['distances']; const selected_dist = list_of_dist[selected_idx]; function indexOfNMin(arr, n) { if (arr.length < n) { return [arr, [...Array(arr.length).keys()]]; } var min_arr = arr.slice(0, n); var min_idxs = [...Array(n).keys()]; for (var i = n; i < arr.length; i++) { const max_selected = Math.max(...min_arr); if (arr[i] < max_selected) { var idx_max = min_arr.indexOf(max_selected); min_arr[idx_max] = arr[i]; min_idxs[idx_max] = i; } } return [min_arr, min_idxs]; } const closest_dist_values = indexOfNMin(selected_dist, """ + str(num_similar_shown) + """); const closest_dist = [].slice.call(closest_dist_values[0]); const closest_dist_idx = closest_dist_values[1]; function sortWithIndices(inputArray) { const toSort = inputArray.slice(); for (var i = 0; i < toSort.length; i++) { toSort[i] = [toSort[i], i]; } toSort.sort(function(left, right) { return left[0] < right[0] ? -1 : 1; }); var sortIndices = []; for (var j = 0; j < toSort.length; j++) { sortIndices.push(toSort[j][1]); } return sortIndices; } const sorted_closest_dist_idx_idx = sortWithIndices(closest_dist); const sorted_closest_dist_idx = sorted_closest_dist_idx_idx.map(i => closest_dist_idx[i]); const closest_texts = sorted_closest_dist_idx.map(i => texts[i]); const display_data = display_source.data; display_data['closest_text'] = closest_texts; display_data['closest_dist'] = closest_dist.sort(function(a, b){return a - b}).map(i => i.toFixed(3)); display_source.change.emit(); } } """) source.selected.js_on_change('indices', callback) str_search_input.js_on_change('value', callback) data_table = DataTable(source=source, columns=columns, width=600, height=420, selectable=True) closest_data_table = DataTable(source=display_source, columns=closest_columns, fit_columns=False, height=800, editable=True) title = Div(text="""<b>Feedback Finder</b><br><br> The left hand side will allow you to look at ALL feedback for this given app.<br><br> Click on a row to see the closest matches to this row (and the embedding distance of each match) on the right side.<br><br> Try using the search bar to narrow down feedback that you want to find. <br>For example, if you are looking for performance related bug reports, then try typing 'lag' into the search bar, and hitting enter.<br> Then click on one of the results on the left to see other related bits of feedback that do not explicitly mention the word 'lag' on the right.<br><br>""", width=1000, height=180) layout = column( title, row(column(str_search_input, data_table), column(closest_data_table))) # output to static HTML file output_file(f"{file_path}.html") save(layout)
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
def plot_bokeh( points, labels=None, hover_data=None, width=800, height=800, color_key_cmap="Spectral", point_size=None, alpha=None, interactive_text_search=False, ): data = pd.DataFrame(points, columns=("x", "y")) data["label"] = labels unique_labels = np.unique(labels) num_labels = unique_labels.shape[0] color_key = _to_hex( plt.get_cmap(color_key_cmap)(np.linspace(0, 1, num_labels)) ) if isinstance(color_key, dict): data["color"] = pd.Series(labels).map(color_key) else: unique_labels = np.unique(labels) if len(color_key) < unique_labels.shape[0]: raise ValueError( "Color key must have enough colors for the number of labels" ) new_color_key = {k: color_key[i] for i, k in enumerate(unique_labels)} data["color"] = pd.Series(labels).map(new_color_key) colors = "color" if hover_data is not None: tooltip_dict = {} for col_name in hover_data: data[col_name] = hover_data[col_name] tooltip_dict[col_name] = "@{" + col_name + "}" tooltips = list(tooltip_dict.items()) else: tooltips = None if alpha is not None: data["alpha"] = alpha else: data["alpha"] = 1 data_source = bpl.ColumnDataSource(data) plot = bpl.figure( width=width, height=height, tooltips=tooltips ) plot.circle( x="x", y="y", source=data_source, color=colors, size=point_size, alpha="alpha", legend_field='label' ) plot.grid.visible = False plot.axis.visible = False if interactive_text_search: text_input = TextInput(value="", title="Search:") interactive_text_search_columns = [] if hover_data is not None: interactive_text_search_columns.extend(hover_data.columns) if labels is not None: interactive_text_search_columns.append("label") callback = CustomJS( args=dict( source=data_source, matching_alpha=0.95, non_matching_alpha=1 - 0.95, search_columns=interactive_text_search_columns, ), code=""" var data = source.data; var text_search = cb_obj.value; var search_columns_dict = {} for (var col in search_columns){ search_columns_dict[col] = search_columns[col] } // Loop over columns and values // If there is no match for any column for a given row, change the alpha value var string_match = false; for (var i = 0; i < data.x.length; i++) { string_match = false for (var j in search_columns_dict) { if (String(data[search_columns_dict[j]][i]).includes(text_search) ) { string_match = true } } if (string_match){ data['alpha'][i] = matching_alpha }else{ data['alpha'][i] = non_matching_alpha } } source.change.emit(); """, ) text_input.js_on_change("value", callback) plot = column(text_input, plot) return plot
}, code=""" //console.log('select: value=' + this.value, this.toString()) mpoly.glyph.fill_color.field = this.value mpoly.data_source.change.emit() """)) selectors_map.append(select) # Range setting for map map_range_widgets = [] text_input = TextInput(value=str(color_mapper.high), title="High Color") text_input.js_on_change( "value", CustomJS(args={ 'ext_datafiles': ext_datafiles, 'color_mapper': color_mapper, }, code=""" color_mapper.high = Number(this.value) """)) map_range_widgets.append(text_input) text_input = TextInput(value=str(color_mapper.low), title="Low Color") text_input.js_on_change( "value", CustomJS(args={ 'ext_datafiles': ext_datafiles, 'color_mapper': color_mapper, }, code=""" color_mapper.low = Number(this.value) """))
def upload(): """ This function waits until the user uploads/crops an image, grabs the color palette and color codes, and loads the page with the image and palettes displayed. :return: rendered template of image page (known as 'image.html') with the image files and color codes passed in """ if request.method == 'POST' or request.method == 'GET': if "csv" in request.files: filename = photos.save(request.files["csv"]) fullname = os.path.join(app.config['UPLOADED_PHOTOS_DEST'], filename) dictionary = open_file_into_dictionary(fullname) keys = list(key.title() for key in dictionary.keys()) values = [value for value in dictionary.values()] color_keys = list(key.title() for key in color_palettes.keys()) color_values = [value for value in color_palettes.values()] xy_source = ColumnDataSource(data=dict(xs=[values[0]], ys=[values[1]], labels = [keys[1]], colors = color_palettes['default'])) variables_source = ColumnDataSource(data = dict(keys = keys, values = values)) colors_source = ColumnDataSource(data = dict(color_keys = color_keys, color_values = color_values)) #Create general plot plot = figure(plot_width=800, plot_height=600, toolbar_location = 'above') plot.title.text_font= 'helvetica' plot.title.text_font_size = '18pt' plot.title.align = 'center' plot.title.text_font_style = 'normal' plot.multi_line(xs = 'xs', ys = 'ys', legend = 'labels', line_color = 'colors', source = xy_source) plot.min_border = 40 #Define callbacks x_axis_callback = CustomJS(args=dict(xy_source = xy_source, variables_source = variables_source, axis = plot.xaxis[0]), code=""" var xy_data = xy_source.data; var variables_data = variables_source.data; var string = cb_obj.value; var keys = variables_data.keys; var index = keys.indexOf(string); var values = variables_data.values; var y_length = xy_data['ys'].length; if (y_length == 0){ y_length = 1} var new_list = []; for (i = 0; i < y_length; i++) { new_list.push(values[index])} xy_data['xs'] = new_list; xy_source.change.emit(); var label = keys[index]; axis.axis_label = label; """) y_axis_callback = CustomJS(args=dict(xy_source = xy_source, variables_source = variables_source, axis = plot.yaxis[0]), code=""" var xy_data = xy_source.data; var variables_data = variables_source.data; var index_list = cb_obj.active; var values = variables_data.values; var index_length = index_list.length; var keys = variables_data.keys; var new_ys = []; var new_labels = []; for (i = 0; i < index_length; i++) { new_ys.push(values[index_list[i]]); new_labels.push(keys[index_list[i]])} xy_data['labels'] = new_labels; xy_data['ys'] = new_ys; if (index_length > 0){ var x_variable = xy_data['xs'][0]; var new_x = []; for (i = 0; i < index_length; i++) { new_x.push(x_variable)} xy_data['xs'] = new_x;} xy_source.change.emit(); var y_axis_name = keys[[index_list[0]]]; for (i = 1; i < index_length; i++) { y_axis_name += ", " + keys[[index_list[i]]];} axis.axis_label = y_axis_name; """) title_callback = CustomJS(args= dict(title = plot.title), code=""" var title_text = cb_obj.value; title.text = title_text; """) x_name_callback = CustomJS(args=dict(axis = plot.xaxis[0]), code=""" var label_text = cb_obj.value; axis.axis_label = label_text; """) y_name_callback = CustomJS(args=dict(axis = plot.yaxis[0]), code=""" var label_text = cb_obj.value; axis.axis_label = label_text; """) #Create toolbox label_x = Div(text="""<h1>X-Axis</h1>""") x_axis = Select(title= 'Click to change:', options=keys, value = keys[0] + ' (Click to change.)', callback = x_axis_callback) label_y = Div(text="""<br /> <h1>Y-Axis</h1>""") y_axis = CheckboxGroup(labels=keys, active=[1], callback = y_axis_callback) label_axes = Div(text="""<br /><h1>Modify Labels</h1>""") title_name = TextInput(title="Title", value='Default Title') plot.title.text = title_name.value title_name.js_on_change('value', title_callback) x_name = TextInput(title="X-Axis", value='Default X Label') plot.xaxis.axis_label = keys[0] x_name.js_on_change('value', x_name_callback) y_name = TextInput(title="Y-Axis", value='Default Y Label') plot.yaxis.axis_label = keys[1] y_name.js_on_change('value', y_name_callback) toolbox = widgetbox(label_x, x_axis, label_y, y_axis, label_axes, title_name, x_name, y_name) #Fine-tuning toolbox callbacks legend_labels_callback = CustomJS(args=dict(xy_source = xy_source), code=""" var xy_data = xy_source.data; var labels = cb_obj.value; var new_labels = labels.split(", "); xy_data['labels'] = new_labels; xy_source.change.emit(); """) color_picker_callback = CustomJS(args=dict(xy_source = xy_source, colors_source = colors_source), code=""" var xy_data = xy_source.data; var colors_data = colors_source.data; var index = cb_obj.active; var palette = colors_data['color_values'][index]; xy_data['colors'] = palette; xy_source.change.emit(); """) #Toolbox colors_label = Div(text="""<h3>Change Color Scheme</h3>""", sizing_mode = 'scale_width') color_picker = RadioButtonGroup(labels=color_keys, active=0, callback = color_picker_callback) labels_label = Div(text="""<br><h3>Edit Legend Labels</h3>""", sizing_mode = 'scale_width') legend_labels = TextInput(title = 'Warning: Changing the variables will reset the legend labels.', value="Label 1, Label 2, Label 3...", sizing_mode = 'scale_width') legend_labels.js_on_change('value', legend_labels_callback) fine_toolbox = widgetbox(colors_label, color_picker, labels_label, legend_labels, sizing_mode = 'scale_width') #Integrate with html parts = dict(toolbox = toolbox, plot = plot, fine_toolbox = fine_toolbox) script, div = components(parts, INLINE) return render_template('plotpage.html', script=script, toolbox_div=div['toolbox'], plot_div=div['plot'], fine_toolbox_div = div['fine_toolbox'], js_resources=INLINE.render_js(), css_resources=INLINE.render_css()) else: for infile in glob.glob('static/uploaded_csv/*'): os.remove(infile) return render_template('index.html')
def bokeh_plot(node, link, name='NetworkMap'): from bokeh.plotting import figure, from_networkx, save from bokeh.models import ColumnDataSource, HoverTool from bokeh.io import export_png from bokeh.models import CustomJS, TextInput, CustomJSFilter, CDSView, TapTool from bokeh.layouts import column from bokeh.plotting import output_file, show from bokeh.tile_providers import get_provider, Vendors from bokeh.models import Circle, MultiLine, LabelSet, Toggle, CheckboxGroup from bokeh.models.graphs import NodesAndLinkedEdges text_input = TextInput(value="", title="Filter Nodes:") wgs84_to_web_mercator(node) node_source_data = ColumnDataSource( data=dict(x=node['MX'], y=node['MY'], desc=node['id'])) # link G = nx.from_pandas_edgelist(link, source='id', target='anode') nx.set_node_attributes(G, dict(zip(link.id, link.id)), 'desc') n_loc = {k: (x, y) for k, x, y in zip(node['id'], node['MX'], node['MY'])} nx.set_node_attributes(G, n_loc, 'pos') n_color = {k: 'orange' if 'C' in k else 'green' for k in node['id']} nx.set_node_attributes(G, n_color, 'color') n_alpha = {k: 1 if 'C' in k else 0 for k in node['id']} nx.set_node_attributes(G, n_alpha, 'alpha') e_color = {(s, t): 'red' if 'C' in s else 'black' for s, t in zip(link['id'], link['anode'])} nx.set_edge_attributes(G, e_color, 'color') e_line_type = {(s, t): 'dashed' if 'C' in s else 'solid' for s, t in zip(link['id'], link['anode'])} nx.set_edge_attributes(G, e_line_type, 'line_type') tile_provider = get_provider(Vendors.CARTODBPOSITRON) bokeh_plot = figure(title="%s network map" % name.split('/')[-1].split('.')[0], sizing_mode="scale_height", plot_width=1300, x_range=(min(node['MX']), max(node['MX'])), tools='pan,wheel_zoom', active_drag="pan", active_scroll="wheel_zoom") bokeh_plot.add_tile(tile_provider) # This callback is crucial, otherwise the filter will not be triggered when the slider changes callback = CustomJS(args=dict(source=node_source_data), code=""" source.change.emit(); """) text_input.js_on_change('value', callback) # Define the custom filter to return the indices from 0 to the desired percentage of total data rows. You could # also compare against values in source.data js_filter = CustomJSFilter(args=dict(text_input=text_input), code=f""" const z = source.data['desc']; var indices = ((() => {{ var result = []; for (let i = 0, end = source.get_length(), asc = 0 <= end; asc ? i < end : i > end; asc ? i++ : i--) {{ if (z[i].includes(text_input.value.toString(10))) {{ result.push(i); }} }} return result; }})()); return indices;""") # Use the filter in a view view = CDSView(source=node_source_data, filters=[js_filter]) callback2 = CustomJS(args=dict(x_range=bokeh_plot.x_range, y_range=bokeh_plot.y_range, text_input=text_input, source=node_source_data), code=f""" const z = source.data['desc']; const x = source.data['x']; const y = source.data['y']; var result = []; for (let i = 0, end = source.get_length(), asc = 0 <= end; asc ? i < end : i > end; asc ? i++ : i--) {{ if (z[i].includes(text_input.value.toString(10))) {{ result.push(i); }} }} var indices = result[0]; var Xstart = x[indices]; var Ystart = y[indices]; y_range.setv({{"start": Ystart-280, "end": Ystart+280}}); x_range.setv({{"start": Xstart-500, "end": Xstart+500}}); x_range.change.emit(); y_range.change.emit(); """) text_input.js_on_change('value', callback2) graph = from_networkx(G, nx.get_node_attributes(G, 'pos'), scale=2, center=(0, 0)) graph.node_renderer.glyph = Circle(radius=15, fill_color='color', fill_alpha='alpha') graph.node_renderer.hover_glyph = Circle(radius=15, fill_color='red') graph.edge_renderer.glyph = MultiLine( line_alpha=1, line_color='color', line_width=1, line_dash='line_type') # zero line alpha graph.edge_renderer.hover_glyph = MultiLine(line_color='#abdda4', line_width=5) graph.inspection_policy = NodesAndLinkedEdges() bokeh_plot.circle('x', 'y', source=node_source_data, radius=10, color='green', alpha=0.7, view=view) labels = LabelSet(x='x', y='y', text='desc', text_font_size="8pt", text_color='black', x_offset=5, y_offset=5, source=node_source_data, render_mode='canvas') code = '''\ if (toggle.active) { box.text_alpha = 0.0; console.log('enabling box'); } else { box.text_alpha = 1.0; console.log('disabling box'); } ''' callback3 = CustomJS(code=code, args={}) toggle = Toggle(label="Annotation", button_type="success") toggle.js_on_click(callback3) callback3.args = {'toggle': toggle, 'box': labels} bokeh_plot.add_tools(HoverTool(tooltips=[("id", "@desc")]), TapTool()) # Output filepath bokeh_plot.renderers.append(graph) bokeh_plot.add_layout(labels) layout = column(toggle, text_input, bokeh_plot) # export_png(p, filename="plot.png") output_file(name) show(layout)