def test_js_on_change_executes(self, bokeh_model_page: BokehModelPage) -> None: group = CheckboxGroup(labels=LABELS) group.js_on_change('active', CustomJS(code=RECORD("active", "cb_obj.active"))) page = bokeh_model_page(group) el = find_element_for(page.driver, group, 'input[value="2"]') el.click() results = page.results assert results['active'] == [2] el = find_element_for(page.driver, group, 'input[value="0"]') el.click() results = page.results assert results['active'] == [0, 2] el = find_element_for(page.driver, group, 'input[value="2"]') el.click() results = page.results assert results['active'] == [0] assert page.has_no_console_errors()
def get_checkboxes_with_filter(labels: List[str], column_label: str, source, select_all_btn=True, clear_all_btn=True): """ :param labels: names for checkbox labels as list (dataframe must contain it as value to filter) :param column_label: name of column in dataframe :param source: dataframe :return: checkboxes and filter for graph """ # routes checkboxes checkboxes = CheckboxGroup(labels=labels, active=list(range(len(labels)))) filter = CustomJSFilter(code=''' var selected = checkboxes.active.map(i=>checkboxes.labels[i]); var indices = []; var column = source.data[column_label]; // iterate through rows of data source and see if each satisfies some constraint for (var i = 0; i < column.length; i++){ if(selected.includes(column[i])){ indices.push(true); } else { indices.push(false); } } console.log("filter completed"); return indices; ''', args=dict(checkboxes=checkboxes, column_label=column_label)) checkboxes.js_on_change( "active", CustomJS(code="source.change.emit();", args=dict(source=source))) widgets = [checkboxes] if select_all_btn: select_all = Button(label="выбрать все", width=65, height=30) select_all.js_on_click( CustomJS(args=dict(checkboxes=checkboxes, all_active=list(range(len(labels)))), code=""" checkboxes.active = all_active """)) widgets.append(select_all) if clear_all_btn: clear_all = Button(label="отчистить все", width=65, height=30) clear_all.js_on_click( CustomJS(args=dict(checkboxes=checkboxes), code=""" checkboxes.active = [] """)) widgets.append(clear_all) return Column(*widgets), filter
from bokeh.plotting import figure output_file("line_on_off.html", title="line_on_off.py example") p = figure() props = dict(line_width=4, line_alpha=0.7) x = np.linspace(0, 4 * np.pi, 100) l0 = p.line(x, np.sin(x), color=Viridis3[0], legend_label="Line 0", **props) l1 = p.line(x, 4 * np.cos(x), color=Viridis3[1], legend_label="Line 1", **props) l2 = p.line(x, np.tan(x), color=Viridis3[2], legend_label="Line 2", **props) checkbox = CheckboxGroup(labels=["Line 0", "Line 1", "Line 2"], active=[0, 1, 2], width=100) callback = CustomJS(args=dict(l0=l0, l1=l1, l2=l2, checkbox=checkbox), code=""" l0.visible = 0 in checkbox.active; l1.visible = 1 in checkbox.active; l2.visible = 2 in checkbox.active; """) checkbox.js_on_change('active', callback) layout = row(checkbox, p) show(layout)
def plot(tables, output_filename): ''' This is the plot function that uses Bokeh functions and widgets to make an interactive hexagon plot. This function recieves: - tables: dictionary with tables used to create arrays of repeated x, y coordinates (depending on the counts) for the hexagon plot. - output_filename: filename of .html output in the plots folder The coordinate arrays are used to create a pandas dataframe with Bokeh functions. This dataframe contains the q, r coordinates and counts used to plot the hexagons. To this dataframe, extra information is added (e.g. most common chemicals), which is displayed in the hover tooltip. Gaussian blur is added to copies of this dataframe and given as input to the Bokeh slider widget. Other widgets are added as well, for saturation, normalisation etc. Bokeh allows to customize these widges with javascript code. The hexagon plot is saved as a .html file and also shown in the browser. ''' file_name = 'plots/' + str(output_filename) + '.html' output_file(file_name) # Blur and saturation values BLUR_MAX = 3 BLUR_STEP_SIZE = 1 SATURATION_MAX = 5 SATURATION_STEP_SIZE = 0.25 # First, create array for plot properties ( ratio, size of hexagons etc.) default_term = list(tables.keys())[0] x, y, ids = create_array(tables[default_term]['table'], normalisation=False) # Hexagon plot properties length = len(x) orientation = 'flattop' ratio = ((max(y) - min(y)) / (max(x) - min(x))) size = 10 / ratio h = sqrt(3) * size h = h * ratio title = 'Hexbin plot for ' + str( length) + ' annotated chemicals with query ' + str(default_term) # make figure p = figure(title=title, x_range=[min(x) - 0.5, max(x) + 0.5], y_range=[0 - (h / 2), max(y) + 100], tools="wheel_zoom,reset,save", background_fill_color='#440154') p.grid.visible = False p.xaxis.axis_label = "log(P)" p.yaxis.axis_label = "mass in Da" p.xaxis.axis_label_text_font_style = 'normal' p.yaxis.axis_label_text_font_style = 'normal' # source for plot term_to_source, term_to_metadata, options = make_plot_sources( tables, size, ratio, orientation, BLUR_MAX, BLUR_STEP_SIZE) # start source for plot, this is the source that is first displayed in the hexagon figure x, y, ids = create_array(tables[default_term]['table'], normalisation=False) df = hexbin(x, y, ids, size, aspect_scale=ratio, orientation=orientation) df = add_counts(df, tables[default_term]['table']) source = ColumnDataSource(df) metadata = term_to_metadata[default_term] metadata = return_html(metadata) # color mapper mapper = linear_cmap('scaling', 'Viridis256', 0, max(source.data['scaling'])) # plot hex = p.hex_tile(q="q", r="r", size=size, line_color=None, source=source, aspect_scale=ratio, orientation=orientation, fill_color=mapper) # HOVER TOOLTIPS = return_tooltip() code_callback_hover = return_code('hover') callback_hover = CustomJS(code=code_callback_hover) hover = HoverTool(tooltips=TOOLTIPS, callback=callback_hover, show_arrow=False) p.add_tools(hover) # WIDGETS slider1 = Slider(start=1, end=SATURATION_MAX, value=1, step=SATURATION_STEP_SIZE, title="Saturation", width=100) slider2 = Slider(start=0, end=BLUR_MAX, value=0, step=BLUR_STEP_SIZE, title="Blur", width=100) checkbox = CheckboxGroup(labels=["TFIDF"], active=[]) radio_button_group = RadioGroup(labels=["Viridis256", "Greys256"], active=0) button = Button(label="Metadata", button_type="default", width=100) multi_select = MultiSelect(title=output_filename, value=[default_term], options=options, width=100, height=300) # WIDGETS CODE FOR CALLBACK code_callback_slider1 = return_code('slider1') code_callback_slider2 = return_code('slider2') code_callback_checkbox = return_code('checkbox') code_callback_rbg = return_code('rbg') code_callback_button = return_code('button') code_callback_ms = return_code('multi_select') # WIDGETS CALLBACK callback_slider1 = CustomJS(args={ 'source': source, 'mapper': mapper }, code=code_callback_slider1) callback_slider2 = CustomJS(args={ 'source': source, 'mapper': mapper, 'slider1': slider1, 'multi_select': multi_select, 'checkbox': checkbox, 'term_to_source': term_to_source, 'step_size': BLUR_STEP_SIZE }, code=code_callback_slider2) callback_checkbox = CustomJS(args={ 'source': source, 'term_to_source': term_to_source, 'multi_select': multi_select, 'step_size': BLUR_STEP_SIZE, 'slider1': slider1, 'slider2': slider2, 'mapper': mapper }, code=code_callback_checkbox) callback_radio_button_group = CustomJS(args={ 'p': p, 'mapper': mapper, 'Viridis256': Viridis256, 'Greys256': Greys256 }, code=code_callback_rbg) callback_button = CustomJS(args={ 'term_to_metadata': term_to_metadata, 'multi_select': multi_select }, code=code_callback_button) callback_ms = CustomJS(args={ 'source': source, 'term_to_source': term_to_source, 'checkbox': checkbox, 'metadata': metadata, 'step_size': BLUR_STEP_SIZE, 'slider2': slider2, 'slider1': slider1, 'p': p, 'mapper': mapper }, code=code_callback_ms) # # WIDGETS INTERACTION slider1.js_on_change('value', callback_slider1) slider2.js_on_change('value', callback_slider2) checkbox.js_on_change('active', callback_checkbox) radio_button_group.js_on_change('active', callback_radio_button_group) button.js_on_event(events.ButtonClick, callback_button) multi_select.js_on_change("value", callback_ms) # LAYOUT layout = row( multi_select, p, column(slider1, slider2, checkbox, radio_button_group, button)) show(layout)
def _show_graph(self, data_frame): #create the plot panels for the Agent_Tasks panels = self._create_task_panels(data_frame) top_left_x_range = panels[list(panels.keys())[0]]['panel1'].x_range #Altitude over ground pAltitude = figure(plot_width=800, plot_height=300, x_range=top_left_x_range) alti_legend = [] # Setting the second y axis range name and range pAltitude.extra_y_ranges = {"speed": Range1d(50, 120)} # Adding the second axis to the plot. pAltitude.add_layout( LinearAxis(y_range_name="speed", axis_label="IAS, TAS [Knots]"), 'right') altitudeLine = pAltitude.line(data_frame.index * self.step_time, data_frame['position_h_sl_ft'], line_width=2, color=Viridis4[2]) alti_legend.append(("Altitude [ftsl]", [altitudeLine])) kiasLine = pAltitude.line(data_frame.index * self.step_time, data_frame['velocities_vc_kts'], line_width=2, y_range_name="speed", color=Viridis4[1]) alti_legend.append(("Indicated Airspeed [KIAS]", [kiasLine])) tasLine = pAltitude.line(data_frame.index * self.step_time, data_frame['velocities_vtrue_kts'], line_width=2, y_range_name="speed", color=Viridis4[0]) alti_legend.append(("True Airspeed [KAS]", [tasLine])) pAltitude.extra_y_ranges.renderers = [ kiasLine, tasLine ] #this does not quite work: https://stackoverflow.com/questions/48631530/bokeh-twin-axes-with-datarange1d-not-well-scaling pAltitude.y_range.renderers = [altitudeLine] lg_alti = Legend(items=alti_legend, location=(0, 10), glyph_width=25, label_width=190) lg_alti.click_policy = "hide" pAltitude.add_layout(lg_alti, 'right') tAlti = Title() tAlti.text = 'Altitude and Speed [IAS, TAS] over Timesteps' pAltitude.title = tAlti pAltitude.xaxis.axis_label = 'timestep [s]' pAltitude.yaxis[0].axis_label = 'Altitude [ftsl]' pAltitude.legend.location = "center_right" pSideslip = figure(plot_width=800, plot_height=300, x_range=top_left_x_range) slip_legend = [] slip_skid_line = pSideslip.line(data_frame.index * self.step_time, data_frame['aero_beta_deg'], line_width=2, color=Viridis4[2]) slip_legend.append(("Sideslip", [slip_skid_line])) pSideslip.y_range.renderers = [slip_skid_line] lg_slip = Legend(items=slip_legend, location=(0, 10), glyph_width=25, label_width=190) lg_slip.click_policy = "hide" pSideslip.add_layout(lg_slip, 'right') tSlip = Title() tSlip.text = 'Sideslip' pSideslip.title = tSlip pSideslip.xaxis.axis_label = 'timestep [s]' pSideslip.yaxis[0].axis_label = 'Sideslip [deg]' pSideslip.legend.location = "center_right" #activate the zooming on all plots #this is not nice, but this not either: https://stackoverflow.com/questions/49282688/how-do-i-set-default-active-tools-for-a-bokeh-gridplot pAltitude.toolbar.active_scroll = pAltitude.toolbar.tools[ 1] #this selects the WheelZoomTool instance pSideslip.toolbar.active_scroll = pSideslip.toolbar.tools[ 1] #this selects the WheelZoomTool instance reset_output() # if self.env.meta_dict['model_type'] == 'trained': # discriminator = self.env.meta_dict['model_base_name']+"_%+.2f" % (data_frame['reward'].sum()) # self.env.meta_dict['model_discriminator'] = discriminator # else: # discriminator = self.env.meta_dict['model_discriminator'] ts = time.time() overshoot_frames_per_task = self._analyze_overshoot(data_frame) overshoot_divs = [ Div(text=ovs_fr.round(3).to_html(), width=600) for ovs_fr in overshoot_frames_per_task ] print("Overshoot analysis done in %.2f sec" % (time.time() - ts)) ts = time.time() settlement_times_per_task = self._analyze_settle_times(data_frame) settlement_divs = [ Div(text=settle_fr.round(3).to_html(), width=600) for settle_fr in settlement_times_per_task ] print("Settlement analysis done in %.2f sec" % (time.time() - ts)) panel_grid = [] panel_grid.append([ Div(text='<h3>' + t.name + '</h3>', id='div_' + t.name) for t in self.env.task_list ]) # to switch on and off the statistics panels, this is unfortuntely the best, I could achive # https://stackoverflow.com/a/52416676/2682209 cols = [] checkbox = CheckboxGroup( labels=["show stats"], active=[], width=100) #checkbox is added to header_col later on for i, t in enumerate(self.env.task_list): # overshoot_stat = overshoot_divs[i] c = column(Div()) #empty for the beginning cols.append(c) callback = CustomJS(args=dict(overshoot_divs=overshoot_divs, settlement_divs=settlement_divs, cols=cols, checkbox=checkbox), code=""" for (var j = 0; j < cols.length; j++) { console.log('col', j) const children = [] for (const i of checkbox.active) { console.log('active', i) children.push(overshoot_divs[j]) children.push(settlement_divs[j]) } console.log('children', children) cols[j].children = children } """) checkbox.js_on_change('active', callback) # show_stats_btn = [Div(text=""" # <button onclick="display_event(%s)">Try it</button> # """ %t.name for t in self.env.task_list] # for t, b in zip(self.env.task_list, show_stats_btn): # b.tags = ['id', 'btn_'+t.name] # panel_grid.append(show_stats_btn) # [b.js_on_event(events.ButtonClick, display_event(b, t.name)) for t, b in zip(self.env.task_list, show_stats_btn)] # panel_grid.append(chkbxs) panel_grid.append(cols) panel_grid_t = [[ panels[name]['panel1'], panels[name]['panel2'], panels[name]['panel3'] ] for name in self.task_names] [panel_grid.append(fig) for fig in list(zip(*panel_grid_t))] # add the additional plots panel_grid.append([pAltitude, pSideslip]) panel_grid_plot = gridplot(panel_grid, toolbar_location='right', sizing_mode='stretch_width') #for string formatting look here: https://pyformat.info/ titleString = '' if 'experiment_name' in self.env.meta_dict: titleString += "{}: ".format(self.env.meta_dict['experiment_name']) titleString += "Run Date: {}; ".format( datetime.datetime.now().strftime("%c")) if 'train_step' in self.env.meta_dict: titleString += "Training Step: {}; ".format( self.env.meta_dict['train_step']) if 'episode_number' in self.env.meta_dict: titleString += "Episode: {}; ".format( self.env.meta_dict['episode_number']) if 'csv_line_nr' in self.env.meta_dict: titleString += "Env in CSV line: {}; ".format( self.env.meta_dict['csv_line_nr']) # titleString += "Total Reward: {:.2f}; ".format(data_frame['reward'].sum()) # titleString += "Model Discriminator: {};".format(self.env.meta_dict['model_discriminator']) header_col = column( Div(text="<h1>" + self.env.unwrapped.spec.id + (" - " + self.env.meta_dict['env_info']) if 'env_info' in self.meta_dict else "" + "</h1>"), row(Div(text="<h2>" + titleString + "</h2>", width=1200), checkbox)) webpage = gridplot([[header_col], [panel_grid_plot]], toolbar_location=None, sizing_mode='stretch_width') base_filename = 'Run_' + '_'.join([tsk.name for tsk in self.task_list]) html_output_name = os.path.join(self.save_path, 'plots', base_filename + '_latest.html') os.makedirs(os.path.dirname(html_output_name), exist_ok=True) if self.showNextPlotFlag: output_file( html_output_name, mode='inline' ) #mode='absolute') #use mode='absolute' to make it work offline with the js and css installed in the bokeh package locally if self.firstRun: show(webpage) #opens up a new browser window self.firstRun = False else: save( webpage ) #just updates the HTML; Manual F5 in browser required :-(, (There must be a way to push...) if self.exportNextPlotFlag and self.save_path: #build the filename including the individual rewards task_rewards = [ self.reward_variables[i].get_legal_name() for i in range(len(self.task_list)) ] task_names_with_rewards = [ t.name + '_' + f'{data_frame[task_rewards[i]].sum():.2f}' for i, t in enumerate(self.task_list) ] name_with_rewards = 'Run_' + '_'.join( task_names_with_rewards) + 'time_{}'.format( datetime.datetime.now().strftime("%H-%M-%S")) base_filename = os.path.join( self.save_path, 'plots', name_with_rewards ) # 'glideAngle_Elevator_Reward_{:.2f}_time_{}'.format(data_frame['reward'].sum(), datetime.datetime.now().strftime("%H-%M-%S"))) if self.showNextPlotFlag: #we keep the html as well for easy exploration shutil.copyfile(html_output_name, base_filename + '.html') def export(webpage): png_filename = base_filename + '.png' webpage.width = 1800 #set the width of the page instead of passing a width parameter to the export; https://stackoverflow.com/a/61563173/2682209 export_png( webpage, filename=png_filename ) #TODO: the width parameter is ignored in bokeh/io/export.py get_layout_html() as webpage isn't a Plot export( gridplot([[header_col], [panel_grid_plot]], toolbar_location=None)) self.showNextPlotFlag = False #only show the plot once and then reset self.exportNextPlotFlag = False print("Output Plot generated: " + titleString)
def plot(tables, output_filename, xmin, xmax, ymin, ymax, superterm): ''' This is the plot function that uses Bokeh functions and widgets to make an interactive hexagon plot. This function recieves: - tables: dictionary with tables used to create arrays of repeated x, y coordinates (depending on the counts) for the hexagon plot. - output_filename: filename of .html output in the plots folder The coordinate arrays are used to create a pandas dataframe with Bokeh functions. This dataframe contains the q, r coordinates and counts used to plot the hexagons. To this dataframe, extra information is added (e.g. most common chemicals), which is displayed in the hover tooltip. Gaussian blur is added to copies of this dataframe and given as input to the Bokeh slider widget. Other widgets are added as well, for saturation, normalization etc. Bokeh allows to customize these widges with javascript code. The hexagon plot is saved as a .html file and also shown in the browser. ''' file_name = 'plots/' + str(output_filename) + '.html' output_file(file_name) # Blur and saturation values BLUR_MAX = 4 BLUR_STEP_SIZE = 0.25 SATURATION_MAX = 5 SATURATION_STEP_SIZE = 0.25 # Hexagon plot properties SIZE_HEXAGONS = 10 orientation = 'flattop' #bokeh alows 2 different hexagon orientations which also influences hexagon size calculations, but we currently have only calculated blur distances for this orientation ratio = ((ymax - ymin) / (xmax - xmin)) size = SIZE_HEXAGONS / ratio hexagon_height = sqrt(3) * size hexagon_height = hexagon_height * ratio # make figure p = figure(x_range=[xmin, xmax], y_range=[ymin - (hexagon_height / 2), ymax], tools="wheel_zoom,reset,save", background_fill_color='#440154') p.grid.visible = False p.xaxis.axis_label = "log(P)" p.yaxis.axis_label = "mass in Da" p.xaxis.axis_label_text_font_style = 'normal' p.yaxis.axis_label_text_font_style = 'normal' # term_to_source, term_to_metadata, options = make_plot_sources(tables, size, ratio, orientation, BLUR_MAX, BLUR_STEP_SIZE) # source for widgets term_to_source = dict() term_to_class = dict() term_to_metadata = dict() options = [] for term in tables.keys(): options.append((term, term)) table = tables[term]['table'] if superterm: source = create_class_source(table, term, size, ratio, orientation, superterm) term_to_class[term] = {} term_to_class[term]['show_class'] = True term_to_class[term]['source'] = source else: term_to_class[term] = {'show_class': False} source, title = create_data_source(table, term, size, ratio, orientation, BLUR_MAX, BLUR_STEP_SIZE) metadata = return_html(tables[term]['metadata']) term_to_source[term] = {'source': source, 'title': title} term_to_metadata[term] = metadata # hex = p.hex_tile(q='q', r="r", size=size, line_color=None, source=source, aspect_scale=ratio,orientation=orientation, # fill_color='pink' ) # show(p) # make default souce for plot, this is the first source shown in the plot, and also works like a container. Old data is thrown out and new data is thrown in. default_term = list(tables.keys())[0] # pick the first one metadata = tables[default_term]['metadata'] metadata = return_html(metadata) table = tables[default_term]['table'] source, title = create_data_source(table, default_term, size, ratio, orientation, BLUR_MAX, BLUR_STEP_SIZE) p.title.text = title # color mapper mapper = linear_cmap('scaling', 'Viridis256', 0, max(source.data['scaling'])) # plot hex = p.hex_tile(q="q", r="r", size=size, line_color=None, source=source, aspect_scale=ratio, orientation=orientation, fill_color=mapper) if superterm: source_class = term_to_class[default_term]['source'] class_hex = p.hex_tile(q='q', r="r", size=size, line_color=None, source=source_class, aspect_scale=ratio, orientation=orientation, fill_color='pink', fill_alpha=0.7) class_hex.visible = False # HOVER TOOLTIPS = return_JS_code('tooltips') TOOLTIPS_tfidf = return_JS_code('tooltips_tfidf') code_callback_hover = return_JS_code('hover') callback_hover = CustomJS(code=code_callback_hover) hover = HoverTool(tooltips=TOOLTIPS, callback=callback_hover, show_arrow=False) p.add_tools(hover) # WIDGETS slider1 = Slider(start=1, end=SATURATION_MAX, value=1, step=SATURATION_STEP_SIZE, title="Saturation", width=100) slider2 = Slider(start=0, end=BLUR_MAX, value=0, step=BLUR_STEP_SIZE, title="Blur", width=100) checkbox = CheckboxGroup(labels=["TFIDF"], active=[]) radio_button_group = RadioGroup(labels=["Viridis256", "Greys256"], active=0) button = Button(label="Metadata", button_type="default", width=100) multi_select = MultiSelect(title=output_filename, value=[default_term], options=options, width=100, height=300) if superterm: label = "Show " + str(superterm) checkbox_class = CheckboxGroup(labels=[label], active=[]) # WIDGETS CODE FOR CALLBACK code_callback_slider1 = return_JS_code('slider1') code_callback_slider2 = return_JS_code('slider2') code_callback_checkbox = return_JS_code('checkbox') code_callback_rbg = return_JS_code('rbg') code_callback_button = return_JS_code('button') code_callback_ms = return_JS_code('multi_select') if superterm: code_callback_class = return_JS_code('class') # WIDGETS CALLBACK callback_slider1 = CustomJS(args={ 'source': source, 'mapper': mapper, 'slider2': slider2, 'checkbox': checkbox }, code=code_callback_slider1) callback_slider2 = CustomJS(args={ 'source': source, 'mapper': mapper, 'slider1': slider1, 'checkbox': checkbox }, code=code_callback_slider2) callback_checkbox = CustomJS(args={ 'source': source, 'slider1': slider1, 'slider2': slider2, 'mapper': mapper, 'hover': hover, 'tooltips': TOOLTIPS, 'tooltips_tfidf': TOOLTIPS_tfidf }, code=code_callback_checkbox) callback_radio_button_group = CustomJS(args={ 'p': p, 'multi_select': multi_select, 'mapper': mapper, 'term_to_class': term_to_class, 'Viridis256': Viridis256, 'Greys256': Greys256 }, code=code_callback_rbg) callback_button = CustomJS(args={ 'term_to_metadata': term_to_metadata, 'multi_select': multi_select }, code=code_callback_button) callback_ms = CustomJS(args={ 'source': source, 'term_to_source': term_to_source, 'term_to_class': term_to_class, 'checkbox': checkbox, 'slider2': slider2, 'slider1': slider1, 'p': p, 'mapper': mapper }, code=code_callback_ms) if superterm: callback_radio_button_group = CustomJS(args={ 'p': p, 'multi_select': multi_select, 'class_hex': class_hex, 'term_to_class': term_to_class, 'mapper': mapper, 'Viridis256': Viridis256, 'Greys256': Greys256 }, code=code_callback_rbg) callback_class = CustomJS(args={ 'multi_select': multi_select, 'term_to_class': term_to_class, 'class_hex': class_hex }, code=code_callback_class) callback_ms = CustomJS(args={ 'source': source, 'term_to_source': term_to_source, 'checkbox': checkbox, 'slider2': slider2, 'slider1': slider1, 'p': p, 'mapper': mapper, 'checkbox_class': checkbox_class, 'class_hex': class_hex, 'term_to_class': term_to_class }, code=code_callback_ms) # WIDGETS INTERACTION slider1.js_on_change('value', callback_slider1) slider2.js_on_change('value', callback_slider2) checkbox.js_on_change('active', callback_checkbox) radio_button_group.js_on_change('active', callback_radio_button_group) button.js_on_event(events.ButtonClick, callback_button) multi_select.js_on_change("value", callback_ms) if superterm: checkbox_class.js_on_change('active', callback_class) # LAYOUT if superterm: layout = row( multi_select, p, column(slider1, slider2, checkbox, checkbox_class, radio_button_group, button)) else: layout = row( multi_select, p, column(slider1, slider2, checkbox, radio_button_group, button)) show(layout)
def make_plot(emperor, df, column=None, value=None): df = df[df['Emperor'] == emperor] source = ColumnDataSource(df) select_den = Select(title="Denomination", value="All", options=["All", "Aureus", "Denarius"]) select_grd = Select(title="Grade", value="All", options=[ 'All', 'Fine', 'Good Fine', 'Near VF', 'VF', 'Good VF', 'Near EF', 'EF', 'Superb EF', 'Choice EF', 'FDC' ]) check_attractive = CheckboxGroup(labels=["Attractively Toned"], active=[]) check_cabinet = CheckboxGroup(labels=["Cabinet Toning"], active=[]) check_lusterous = CheckboxGroup(labels=["Lusterous"], active=[]) check_centered = CheckboxGroup(labels=["Well Centered"], active=[]) check_portrait = CheckboxGroup(labels=["Artistic Portrait"], active=[]) # This callback is crucial, otherwise the filter will # not be triggered when the select changes callback = CustomJS(args=dict(source=source), code=""" source.change.emit(); """) select_den.js_on_change('value', callback) select_grd.js_on_change('value', callback) check_attractive.js_on_change('active', callback) check_cabinet.js_on_change('active', callback) check_lusterous.js_on_change('active', callback) check_centered.js_on_change('active', callback) check_portrait.js_on_change('active', callback) custom_filter_grd = CustomJSFilter(args=dict(select=select_grd, source=source), code=''' var indices = []; console.log(select.value); // no select cuts applied if (select.value == 'All') { for (var i = 0; i < source.get_length(); i++){ indices.push(true); } return indices; } // iterate through rows of data source and see if each satisfies some constraint for (var i = 0; i < source.get_length(); i++){ if (source.data['Grade'][i] == select.value){ indices.push(true); } else { indices.push(false) } } return indices; ''') custom_filter_den = CustomJSFilter(args=dict(select=select_den, source=source), code=''' var indices = []; console.log(select.value); // no select cuts applied if (select.value == 'All') { for (var i = 0; i < source.get_length(); i++){ indices.push(true); } return indices; } // iterate through rows of data source and see if each satisfies some constraint for (var i = 0; i < source.get_length(); i++){ if (source.data['Denomination'][i] == select.value){ indices.push(true); } else { indices.push(false) } } //console.log(indices) return indices; ''') custom_filter_attractive = CustomJSFilter(args=dict( checkbox=check_attractive, source=source), code=''' var indices = []; //console.log(checkbox.active); if (checkbox.active.includes(0)) { //console.log('0 on') //console.log(checkbox.active.includes(0)); for (var i = 0; i < source.get_length(); i++) { if (source.data['Attractively Toned'][i] == 1) { indices.push(true); } else { indices.push(false) } } } else { //console.log('0 off') for (var i = 0; i < source.get_length(); i++) { indices.push(true); } } return indices; ''') custom_filter_cabinet = CustomJSFilter(args=dict(checkbox=check_cabinet, source=source), code=''' var indices = []; //console.log(checkbox.active); if (checkbox.active.includes(0)) { //console.log('0 on') //console.log(checkbox.active.includes(0)); for (var i = 0; i < source.get_length(); i++) { if (source.data['Cabinet Toning'][i] == 1) { indices.push(true); } else { indices.push(false) } } } else { //console.log('0 off') for (var i = 0; i < source.get_length(); i++) { indices.push(true); } } return indices; ''') custom_filter_lusterous = CustomJSFilter(args=dict( checkbox=check_lusterous, source=source), code=''' var indices = []; //console.log(checkbox.active); if (checkbox.active.includes(0)) { //console.log('0 on') //console.log(checkbox.active.includes(0)); for (var i = 0; i < source.get_length(); i++) { if (source.data['Lusterous'][i] == 1) { indices.push(true); } else { indices.push(false) } } } else { //console.log('0 off') for (var i = 0; i < source.get_length(); i++) { indices.push(true); } } return indices; ''') custom_filter_centered = CustomJSFilter(args=dict(checkbox=check_centered, source=source), code=''' var indices = []; //console.log(checkbox.active); if (checkbox.active.includes(0)) { //console.log('0 on') //console.log(checkbox.active.includes(0)); for (var i = 0; i < source.get_length(); i++) { if (source.data['Cabinet Toning'][i] == 1) { indices.push(true); } else { indices.push(false) } } } else { //console.log('0 off') for (var i = 0; i < source.get_length(); i++) { indices.push(true); } } return indices; ''') custom_filter_portrait = CustomJSFilter(args=dict(checkbox=check_portrait, source=source), code=''' var indices = []; //console.log(checkbox.active); if (checkbox.active.includes(0)) { //console.log('0 on') //console.log(checkbox.active.includes(0)); for (var i = 0; i < source.get_length(); i++) { if (source.data['Quality Portrait'][i] == 1) { indices.push(true); } else { indices.push(false) } } } else { //console.log('0 off') for (var i = 0; i < source.get_length(); i++) { indices.push(true); } } return indices; ''') view = CDSView(source=source, filters=[ custom_filter_grd, custom_filter_den, custom_filter_attractive, custom_filter_cabinet, custom_filter_lusterous, custom_filter_centered, custom_filter_portrait ]) TOOLS = ["pan, wheel_zoom, box_zoom, reset, save"] TOOLTIPS = [("Auction", "@AuctionID"), ("Lot", "@LotNumber"), ("Emperor", "@Emperor"), ("RIC Number", "@RIC"), ("Estimate [USD]", "@Estimate"), ("Sale [USD]", "@Sale")] plot = figure( title='CNG Auctions through 2019 for Coins of Emperor ' + emperor, plot_width=500, plot_height=300, tools=TOOLS, tooltips=TOOLTIPS, x_range=Range1d(start=20, end=80000, bounds=(None, None)), y_range=[ 'Fine', 'Good Fine', 'Near VF', 'VF', 'Good VF', 'Near EF', 'EF', 'Superb EF', 'Choice EF', 'FDC' ], x_axis_type='log') color_mapper = CategoricalColorMapper(factors=['Aureus', 'Denarius'], palette=['#FFD700', '#C0C0C0']) plot.circle(x='Sale', y=jitter('Grade', 0.4, range=plot.y_range), source=source, view=view, fill_alpha=0.8, size=5, legend='data', line_color=None, color={ 'field': 'Denomination', 'transform': color_mapper }, hover_color='red') plot.xaxis.axis_label = "Sale Price (USD)" plot.yaxis.axis_label = "Grade" plot.xaxis.formatter = BasicTickFormatter(use_scientific=False) plot.xaxis.formatter = NumeralTickFormatter(format='$0,0') plot.legend.visible = False return row( plot, widgetbox([ select_den, select_grd, check_attractive, check_cabinet, check_lusterous, check_centered, check_portrait ]))
p_answered.add_tools(hover_answered) p_nanswered.add_tools(hover_answered) # # Widgets # # data filters filter_answered = checkbox_categorical_filter(src_ans, src_ans_orig) filter_nanswered = checkbox_categorical_filter(src_nans, src_nans_orig) # checkbox object checkbox_group = CheckboxGroup(labels=LABELS, active=[i for i in range(len(LABELS))]) # event handlers checkbox_group.js_on_change('active', filter_answered) checkbox_group.js_on_change('active', filter_nanswered) # output_file("color_scatter.html", title="color_scatter.py example") # layout specificationns lay = layout( children = [ [p_answered, p_nanswered], [checkbox_group], ] ) tab = Panel(child=lay, title = 'Clusters') tabs = Tabs(tabs=[tab])
def get_dashboard(local_data=False): """ Assemble the dashboard. Define the layout, controls and its callbacks, as well as data sources. """ # Load data video_data = {} if local_data: data = pd.json_normalize( pd.read_json('data/channel/processed_data.json')['items']) with open('data/channel/processed_data.json', 'r') as f: data = json.load(f) video_data = data['items'] else: video_data = handle_channel_data.get_data_firebase()['items'] # X axis categories x_axis_map = { "Climber": "climber", "Zone": "zone", "Grade": "grade", } # Y axis categories y_axis_map = { "Count": "count", "Views": "viewCount", # "Favourites": "favoriteCount", "Likes": "likeCount", "Dislikes": "dislikeCount", "Comments": "commentCount" } # get ready to plot data barchart_data = prepare_barchart_data(video_data, x_axis_map) # html template to place the plots desc = Div(text=open(join(dirname(__file__), "templates/stats.html")).read(), sizing_mode="stretch_width") # initial data source fill data_to_plot = barchart_data['grade']['raw'] od = collections.OrderedDict( sorted(data_to_plot.items(), key=lambda x: x[0])) x_to_plot = np.array([key for key, _ in od.items()]) y_to_plot = np.array([val['count'] for _, val in od.items()]) source = ColumnDataSource(data=dict(x=x_to_plot, y=y_to_plot)) # initial data x_init = x_to_plot[0:NUM_RESULTS] y_init = y_to_plot[0:NUM_RESULTS] # Create Input controls checkbox_limit_results = CheckboxGroup( labels=["Show only first 50 results"], active=[0]) label_slider = Slider(start=0, end=90, value=90, step=1, title="Label Angle") range_slider = RangeSlider(title="Value Range", start=0, end=max(y_to_plot), value=(0, max(y_to_plot)), step=1) min_year = Slider(title="From", start=2015, end=2020, value=2015, step=1) max_year = Slider(title="To", start=2015, end=2020, value=2020, step=1) sort_order = RadioButtonGroup( labels=["Alphabetically", "Decreasing", "Increasing"], active=0) x_axis = Select(title="X Axis", options=sorted(x_axis_map.keys()), value="Grade") y_axis = Select(title="Y Axis", options=sorted(y_axis_map.keys()), value="Count") checkbox = CheckboxGroup( labels=["Show ratio with respect to number of videos"], active=[]) # show number of categories x_count_source = ColumnDataSource( data=dict(x_count=[len(x_init)], category=[x_axis.value])) columns = [ TableColumn(field="category", title="Category"), TableColumn(field="x_count", title="Count"), ] x_count_data_table = DataTable(source=x_count_source, columns=columns, width=320, height=280) # Generate the actual plot # p = figure(x_range=x_to_plot, y_range=(0, max(y_to_plot)), plot_height=250, title="{} {}".format(x_axis.value, y_axis.value), # toolbar_location="above") p = figure(x_range=x_init, y_range=(0, max(y_init)), plot_height=250, title="{} {}".format(x_axis.value, y_axis.value), toolbar_location="above") # Fill it with data and format it p.vbar(x='x', top='y', width=0.9, source=source) p.xaxis.major_label_orientation = math.pi / 2 p.add_tools(HoverTool(tooltips=[("name", "@x"), ("count", "@y")])) # Controls controls = [ checkbox_limit_results, range_slider, min_year, max_year, sort_order, x_axis, y_axis, checkbox, label_slider, x_count_data_table ] # Callbacks for controls label_callback = CustomJS(args=dict(axis=p.xaxis[0]), code=""" axis.major_label_orientation = cb_obj.value * Math.PI / 180; """) label_slider.js_on_change('value', label_callback) # limit checkbox checkbox_limit_results_callback = CustomJS( args=dict(source=source, x_source=x_count_source, o_data=barchart_data, sort_order=sort_order, x_axis_map=x_axis_map, x_axis=x_axis, y_axis_map=y_axis_map, y_axis=y_axis, range_slider=range_slider, checkbox=checkbox, fig=p, title=p.title), code=SORT_FUNCTION + JS_NUM_RESULTS + """ var data = o_data[x_axis_map[x_axis.value]]; var x = data['x']; var y = data['y']; var apply_limit = cb_obj.active.length > 0; var is_ratio = checkbox.active.length > 0; title.text = x_axis.value.concat(" ", y_axis.value); // Sort data var sorted_data = sortData(data['raw'], sort_order.active, y_axis_map[y_axis.value], is_ratio); var new_y = []; var new_x = []; var final_x = []; var final_y = []; for (var i = 0; i < x.length; i++) { if (apply_limit) { if(sorted_data[i][1] >= range_slider.value[0] && sorted_data[i][1] <= range_slider.value[1]) { new_x.push(sorted_data[i][0]); new_y.push(sorted_data[i][1]); } } else { new_x.push(sorted_data[i][0]); new_y.push(sorted_data[i][1]); } } if (apply_limit) { final_x = new_x.slice(0, num_results); final_y = new_y.slice(0, num_results); window.should_update_range = false; } else { final_x = new_x; final_y = new_y; window.should_update_range = true; } x_source.data['x_count'] = [final_x.length]; x_source.data['category'] = [x_axis.value]; x_source.change.emit(); source.data['x'] = new_x; source.data['y'] = new_y; source.change.emit(); fig.x_range.factors = []; fig.x_range.factors = final_x; if (Array.isArray(new_y) && new_y.length) { // range init and end cannot have same value var range_end = Math.max.apply(Math, new_y); if (range_end == 0 || range_end == -Infinity) { range_end = 1; } range_slider.value = [0, Math.max.apply(Math, final_y)]; range_slider.end = range_end; fig.y_range.end = Math.max.apply(Math, final_y); fig.change.emit(); } """) checkbox_limit_results.js_on_change('active', checkbox_limit_results_callback) # ratio checkbox checkbox_callback = CustomJS(args=dict( source=source, x_source=x_count_source, o_data=barchart_data, sort_order=sort_order, x_axis_map=x_axis_map, x_axis=x_axis, y_axis_map=y_axis_map, y_axis=y_axis, range_slider=range_slider, checkbox_limit_results=checkbox_limit_results, fig=p, title=p.title), code=SORT_FUNCTION + JS_NUM_RESULTS + """ var data = o_data[x_axis_map[x_axis.value]]; var x = data['x']; var y = data['y']; var is_ratio = cb_obj.active.length > 0; title.text = x_axis.value.concat(" ", y_axis.value); if (is_ratio) { title.text = x_axis.value.concat(" ", y_axis.value, " per video"); } // Sort data var sorted_data = sortData(data['raw'], sort_order.active, y_axis_map[y_axis.value], is_ratio); var new_y = []; var new_x = []; var final_x = []; var final_y = []; for (var i = 0; i < x.length; i++) { if (checkbox_limit_results.active.length <= 0) { if(sorted_data[i][1] >= range_slider.value[0] && sorted_data[i][1] <= range_slider.value[1]) { new_x.push(sorted_data[i][0]); new_y.push(sorted_data[i][1]); } } else { new_x.push(sorted_data[i][0]); new_y.push(sorted_data[i][1]); } } if (checkbox_limit_results.active.length > 0) { final_x = new_x.slice(0, num_results); final_y = new_y.slice(0, num_results); window.should_update_range = false; } else { final_x = new_x; final_y = new_y; window.should_update_range = true; } x_source.data['x_count'] = [final_x.length]; x_source.data['category'] = [x_axis.value]; x_source.change.emit(); source.data['x'] = new_x; source.data['y'] = new_y; source.change.emit(); fig.x_range.factors = []; fig.x_range.factors = final_x; if (Array.isArray(new_y) && new_y.length) { // range init and end cannot have same value var range_end = Math.max.apply(Math, new_y); if (range_end == 0 || range_end == -Infinity) { range_end = 1; } range_slider.value = [0, Math.max.apply(Math, final_y)]; range_slider.end = range_end; fig.y_range.end = Math.max.apply(Math, final_y); fig.change.emit(); } """) checkbox.js_on_change('active', checkbox_callback) # range slider range_callback = CustomJS(args=dict( source=source, x_source=x_count_source, o_data=barchart_data, sort_order=sort_order, x_axis_map=x_axis_map, x_axis=x_axis, y_axis_map=y_axis_map, y_axis=y_axis, checkbox=checkbox, checkbox_limit_results=checkbox_limit_results, fig=p), code=SORT_FUNCTION + """ if (window.should_update_range == true) { var data = o_data[x_axis_map[x_axis.value]]; var x = data['x']; var y = data['y']; var is_ratio = checkbox.active.length > 0; // Sort data var sorted_data = sortData(data['raw'], sort_order.active, y_axis_map[y_axis.value], is_ratio); var new_y = []; var new_x = []; for (var i = 0; i < x.length; i++) { if (checkbox_limit_results.active.length <= 0) { if (sorted_data[i][1] >= cb_obj.value[0] && sorted_data[i][1] <= cb_obj.value[1]) { new_x.push(sorted_data[i][0]); new_y.push(sorted_data[i][1]); } } else { new_x.push(sorted_data[i][0]); new_y.push(sorted_data[i][1]); } } x_source.data['x_count'] = [new_x.length]; x_source.data['category'] = [x_axis.value]; x_source.change.emit(); source.data['x'] = new_x; source.data['y'] = new_y; source.change.emit(); fig.x_range.factors = []; fig.x_range.factors = new_x; if (Array.isArray(new_y) && new_y.length) { fig.y_range.end = Math.max.apply(Math, new_y); } } else { window.should_update_range = true; } """) range_slider.js_on_change('value', range_callback) # variable to group data x_axis_callback = CustomJS(args=dict( source=source, x_source=x_count_source, o_data=barchart_data, x_axis_map=x_axis_map, y_axis_map=y_axis_map, y_axis=y_axis, range_slider=range_slider, sort_order=sort_order, checkbox=checkbox, checkbox_limit_results=checkbox_limit_results, fig=p, title=p.title), code=SORT_FUNCTION + JS_NUM_RESULTS + """ title.text = cb_obj.value.concat(" ", y_axis.value); var data = o_data[x_axis_map[cb_obj.value]]; var x = data['x']; var y = data['y']; var is_ratio = checkbox.active.length > 0; if (is_ratio) { title.text = title.text.concat(" per video"); } var sorted_data = sortData(data['raw'], sort_order.active, y_axis_map[y_axis.value], is_ratio); var new_y = []; var new_x = []; var final_x = []; var final_y = []; for (var i = 0; i < x.length; i++) { new_x.push(sorted_data[i][0]); new_y.push(sorted_data[i][1]); } if (checkbox_limit_results.active.length > 0) { final_x = new_x.slice(0, num_results); final_y = new_y.slice(0, num_results); window.should_update_range = false; } else { final_x = new_x; final_y = new_y; window.should_update_range = true; } x_source.data['x_count'] = [final_x.length]; x_source.data['category'] = [cb_obj.value]; x_source.change.emit(); source.data['x'] = new_x; source.data['y'] = new_y; source.change.emit(); fig.x_range.factors = []; fig.x_range.factors = final_x; if (new_y && Array.isArray(new_y) && new_y.length) { // range init and end cannot have same value var range_end = Math.max.apply(Math, new_y); if (range_end == 0 || range_end == -Infinity) { range_end = 1; } range_slider.value = [0, Math.max.apply(Math, final_y)]; range_slider.end = range_end; fig.y_range.end = Math.max.apply(Math, final_y); fig.change.emit(); } """) x_axis.js_on_change('value', x_axis_callback) # variable to group data y_axis_callback = CustomJS(args=dict( source=source, x_source=x_count_source, o_data=barchart_data, x_axis_map=x_axis_map, x_axis=x_axis, y_axis_map=y_axis_map, range_slider=range_slider, sort_order=sort_order, checkbox=checkbox, checkbox_limit_results=checkbox_limit_results, fig=p, title=p.title), code=SORT_FUNCTION + JS_NUM_RESULTS + """ title.text = x_axis.value.concat(" ", cb_obj.value); var data = o_data[x_axis_map[x_axis.value]]; var x = data['x']; var y = data['y']; var is_ratio = checkbox.active.length > 0; if (is_ratio) { title.text = x_axis.value.concat(" ", cb_obj.value, " per video"); } var sorted_data = sortData(data['raw'], sort_order.active, y_axis_map[cb_obj.value], is_ratio); var new_y = []; var new_x = []; var final_x = []; var final_y = []; for (var i = 0; i < x.length; i++) { new_x.push(sorted_data[i][0]); new_y.push(sorted_data[i][1]); } if (checkbox_limit_results.active.length > 0) { final_x = new_x.slice(0, num_results); final_y = new_y.slice(0, num_results); window.should_update_range = false; } else { final_x = new_x; final_y = new_y; window.should_update_range = true; } x_source.data['x_count'] = [final_x.length]; x_source.data['category'] = [x_axis.value]; x_source.change.emit(); source.data['x'] = new_x; source.data['y'] = new_y; source.change.emit(); fig.x_range.factors = []; fig.x_range.factors = final_x; if (new_y && Array.isArray(new_y) && new_y.length) { // range init and end cannot have same value var range_end = Math.max.apply(Math, new_y); if (range_end == 0 || range_end == -Infinity) { range_end = 1; } range_slider.value = [0, Math.max.apply(Math, final_y)]; range_slider.end = range_end; fig.y_range.end = Math.max.apply(Math, final_y); } """) y_axis.js_on_change('value', y_axis_callback) # sort order control sort_order_callback = CustomJS(args=dict( source=source, x_source=x_count_source, o_data=barchart_data, x_axis_map=x_axis_map, x_axis=x_axis, y_axis_map=y_axis_map, y_axis=y_axis, range_slider=range_slider, checkbox=checkbox, checkbox_limit_results=checkbox_limit_results, fig=p), code=SORT_FUNCTION + JS_NUM_RESULTS + """ var data = o_data[x_axis_map[x_axis.value]]; var x = data['x']; var y = data['y']; // Sort data var is_ratio = checkbox.active.length > 0; var sorted_data = sortData(data['raw'], cb_obj.active, y_axis_map[y_axis.value], is_ratio); var new_y = []; var new_x = []; var final_x = []; var final_y = []; // push data if it lies inside range for (var i = 0; i < x.length; i++) { if (checkbox_limit_results.active.length <= 0) { if (sorted_data[i][1] >= cb_obj.value[0] && sorted_data[i][1] <= cb_obj.value[1]) { new_x.push(sorted_data[i][0]); new_y.push(sorted_data[i][1]); } } else { new_x.push(sorted_data[i][0]); new_y.push(sorted_data[i][1]); } } if (checkbox_limit_results.active.length > 0) { final_x = new_x.slice(0, 50); final_y = new_y.slice(0, 50); window.should_update_range = false; } else { final_x = new_x; final_y = new_y; window.should_update_range = true; } x_source.data['x_count'] = [final_x.length]; x_source.data['category'] = [x_axis.value]; x_source.change.emit(); source.data['x'] = new_x; source.data['y'] = new_y; source.change.emit(); fig.x_range.factors = []; fig.x_range.factors = final_x; if (new_y && Array.isArray(new_y) && new_y.length) { // range init and end cannot have same value var range_end = Math.max.apply(Math, new_y); if (range_end == 0 || range_end == -Infinity) { range_end = 1; } range_slider.value = [0, Math.max.apply(Math, final_y)]; range_slider.end = range_end; fig.y_range.end = Math.max.apply(Math, final_y); fig.change.emit(); } """) sort_order.js_on_change('active', sort_order_callback) # Define layout inputs = column(*controls, width=320, height=1000) inputs.sizing_mode = "fixed" l = layout([ [desc], [inputs, p], ], sizing_mode="scale_both") return l
def bokeh_spacetimepop( frm, geometry, title = '', preamble = '', varNames = None, varNotes = dict(), pw = 700, ph = 700, xZones = dict(), ): import numpy as np import pandas as pd df = pd.DataFrame idx = pd.IndexSlice import geopandas as gpd gdf = gpd.GeoDataFrame from bokeh.models import ColumnDataSource, HoverTool, Legend, LegendItem, CDSView, IndexFilter from bokeh.plotting import figure, show from bokeh.io import output_notebook # frm = frm.reset_index().pivot(index = frm.index.names[0], columns = frm.index.names[1]) frm = frm.copy() frm = frm.sort_index() # geometry = geometry.copy() from bokeh.models import Div title = f'<h1>{title}</h1>' title = Div( text = title, width = pw, ) preamble = Div( text = preamble, width = pw, ) if varNames is None: varNames = frm.columns.sort_values() varMetaName = varNames.name else: varMetaName = 'variable' varNames = list(varNames) seriesNames = frm.index.levels[1].sort_values() seriesMetaName = seriesNames.name seriesNames = list(seriesNames) dates = [str(int(round(i.to_numpy().astype(int) / 1e6))) for i in frm.index.levels[0]] frm.index = frm.index.set_levels(dates, level = 0) defaultVar = varNames[0] defaultDate = dates[-1] pivotFrm = frm.reset_index() \ .pivot(index = frm.index.names[0], columns = frm.index.names[1]) \ .sort_index() defaultVar = varNames[0] defaultDate = dates[-1] for key in varNames: if not key in varNotes: varNotes[key] = '' else: varNotes[key] = f'<i>{varNotes[key]}</i>' varNote = Div( text = varNotes[defaultVar], width = pw - 120, ) lineSources = { key: ColumnDataSource(pivotFrm[key]) for key in pivotFrm.columns.levels[0] } lineSource = ColumnDataSource(pivotFrm[defaultVar]) lineSource.name = defaultVar barSources = dict() for varName in varNames: for index, date in zip(sorted(pivotFrm.index), dates): series = pivotFrm.loc[index, varName] subFrm = df(dict( name = series.index, value = series.values, height = abs(series.values), offset = series.values / 2. )) barSources[varName + '_' + date] = ColumnDataSource(subFrm) barSource = ColumnDataSource(barSources[defaultVar + '_' + defaultDate].data) barSource.name = ', '.join([str(defaultVar), str(defaultDate)]) bounds = geometry.bounds minx = np.min(bounds['minx']) maxx = np.max(bounds['maxx']) miny = np.min(bounds['miny']) maxy = np.max(bounds['maxy']) aspect = (maxx - minx) / (maxy - miny) from shapely.geometry import Polygon import itertools corners = list(itertools.product(geometry.total_bounds[::2], geometry.total_bounds[1::2])) allPoly = Polygon([corners[0], corners[1], corners[3], corners[2]]) allPoly = allPoly.centroid.buffer(np.sqrt(allPoly.area) / 1e6) for name in frm.index.levels[1]: if not name in geometry.index: geometry[name] = allPoly geometry = geometry.simplify(np.sqrt(geometry.area).min() * 10. ** 3.5) geoFrm = frm.reset_index().pivot(index = frm.index.names[1], columns = frm.index.names[0]) geoFrm.columns = geoFrm.columns.map('_'.join).str.strip('_') geoFrm['geometry'] = geometry geoFrm = gdf(geoFrm) from bokeh.models import GeoJSONDataSource geoJSON = geoFrm.reset_index().to_json() geoSource = GeoJSONDataSource(geojson = geoJSON) mins = {n: frm[n].min() for n in varNames} maxs = {n: frm[n].max() for n in varNames} xName = frm.index.names[0] lineFig = figure( x_axis_type = 'datetime', y_range = (mins[defaultVar], maxs[defaultVar]), plot_height = int((ph - 100) * 1. / 3.), plot_width = pw, toolbar_location = 'left', tools = 'save, xpan, box_zoom, reset, xwheel_zoom', active_scroll = 'auto', # title = title, ) barFig = figure( x_range = seriesNames, plot_height = int((ph - 100) * 1. / 2.), plot_width = pw, # title = "Scores on my birthday", toolbar_location = None, tools = "" ) barFig.xgrid.grid_line_color = None barFig.xaxis.major_label_orientation = 'vertical' mapFig = figure( plot_width = pw - 20, plot_height = int(round((pw - 20) / aspect)), toolbar_location = 'right', tools = 'pan, wheel_zoom, reset', background_fill_color = "lightgrey" ) mapFig.xgrid.grid_line_color = None mapFig.ygrid.grid_line_color = None from matplotlib.pyplot import get_cmap from matplotlib.colors import rgb2hex cmap = get_cmap('nipy_spectral') cs = [rgb2hex(cmap(i / len(seriesNames), alpha = 0.5)) for i in range(len(seriesNames))] lines = [] for seriesName, colour in zip(seriesNames, cs): line = lineFig.line( xName, seriesName, source = lineSource, color = colour, alpha = 0.8, muted_color = 'gray', muted_alpha = 0.3, muted = True, line_width = 2, # legend_label = seriesName, ) from bokeh.models import HoverTool lineFig.add_tools(HoverTool( renderers = [ line, ], tooltips = [ (seriesMetaName.capitalize(), seriesName), (xName.capitalize(), f'@{xName}' + '{%Y-%m-%d}'), ('Value', f'@{{{seriesName}}}'), ], formatters = { f'@{xName}': 'datetime', seriesName: 'numeral', }, toggleable = False )) lines.append(line) bars = [] for i, (seriesName, colour) in enumerate(zip(seriesNames, cs)): view = CDSView(source = barSource, filters = [IndexFilter([i,]),]) bar = barFig.rect( source = barSource, view = view, x = 'name', y = 'offset', height = 'height', width = 0.9, color = colour, muted_color = 'gray', muted_alpha = 0.3, muted = True, ) bars.append(bar) from bokeh.palettes import Viridis256 from bokeh.models import LinearColorMapper, ColorBar palette = Viridis256 mapColourMapper = LinearColorMapper( palette = palette, low = frm.loc[idx[defaultDate, :], defaultVar].min(), high = frm.loc[idx[defaultDate, :], defaultVar].max(), ) mapColourBar = ColorBar( color_mapper = mapColourMapper, label_standoff = 8, width = 30, height = int(round(mapFig.plot_height * 0.9)), border_line_color = None, location = (0, 0), orientation = 'vertical', ) mapFig.add_layout(mapColourBar, 'left') patches = [] for i, seriesName in enumerate(seriesNames): view = CDSView(source = geoSource, filters = [IndexFilter([i,]),]) patch = mapFig.patches( 'xs', 'ys', source = geoSource, view = view, fill_color = dict( field = '_'.join([defaultVar, defaultDate]), transform = mapColourMapper, ), line_color = 'grey', line_width = 0.25, fill_alpha = 0., name = '_'.join([defaultVar, defaultDate]) ) patches.append(patch) from bokeh.models import HoverTool mapHover = HoverTool( renderers = patches, tooltips = [ (seriesMetaName.capitalize(), f'@{seriesMetaName}'), ('Value', '@$name'), ] ) mapFig.add_tools(mapHover) from bokeh.models import BoxAnnotation from bokeh.models import Label for name, zone in xZones.items(): convD = lambda x: int(round(pd.Timestamp(x).to_numpy().astype(int) / 1e6)) left, right = [None if val is None else convD(val) for val in zone] zone = BoxAnnotation( left = left, right = right, fill_alpha = 0.1, fill_color = 'gray', ) zoneLabel = Label( text = name + ' (end)' if left is None else name, text_font_size = '8pt', x = right if left is None else left, y = 10, x_units = 'data', y_units = 'screen', angle = -90 if left is None else 90, angle_units = 'deg', x_offset = -10 if left is None else 10, y_offset = 5 * (len(name) + 6) if left is None else 0 ) lineFig.add_layout(zone) lineFig.add_layout(zoneLabel) from bokeh.models import Span span = Span( location = int(defaultDate), dimension = 'height', line_color = 'red', # line_dash = 'dashed', line_width = 1 ) lineFig.add_layout(span) from bokeh.models.widgets import DateSlider slider = DateSlider( title = 'Date', start = int(dates[0]), end = int(dates[-1]), step = int(8.64 * 1e7), # days value = int(defaultDate), width = pw - 60, align = 'end' ) from bokeh.models.widgets import Select select = Select( title = "Choose data:", options = varNames, value = defaultVar, width = 100, ) from bokeh.models import CheckboxGroup checkboxes = CheckboxGroup( labels = seriesNames, active = [], ) checkboxAll = CheckboxGroup( labels = ['All',], active = [], ) from bokeh.models import CustomJS callback = CustomJS( args = dict( y_range = lineFig.y_range, lineSources = lineSources, lineSource = lineSource, barSources = barSources, barSource = barSource, bars = bars, lines = lines, patches = patches, select = select, slider = slider, span = span, checkboxes = checkboxes, varNote = varNote, varNotes = varNotes, geoSource = geoSource, mapColourMapper = mapColourMapper, mins = mins, maxs = maxs, ), code = """ lineSource.data = lineSources[select.value].data lineSource.name = select.value lineSource.change.emit() span.location = slider.value span.change.emit() y_range.setv({'start': mins[select.value], 'end': maxs[select.value]}) varNote.text = varNotes[select.value] varNote.change.emit() const barChoice = select.value + '_' + slider.value barSource.data = barSources[barChoice].data barSource.name = select.value.toString() + ', ' + slider.value.toString() barSource.change.emit() for (let i = 0; i < lines.length; i++){ let checked = checkboxes.active.includes(i) lines[i].muted = !(checked) bars[i].muted = !(checked) var alpha = checked ? 1 : 0; patches[i].glyph.fill_alpha = alpha } const newCol = select.value + '_' + slider.value for (let i = 0; i < lines.length; i++){ patches[i].glyph.fill_color['field'] = newCol patches[i].name = newCol } mapColourMapper.low = mins[select.value] mapColourMapper.high = maxs[select.value] geoSource.change.emit() """, ) allCheckCallback = CustomJS( args = dict( lines = lines, checkboxes = checkboxes, checkboxAll = checkboxAll, callback = callback ), code = """ checkboxes.active.length = 0 if (checkboxAll.active.length > 0) { let arr = [] for (let i = 0; i < lines.length; i++){ arr.push(i) } checkboxes.active.push(...arr) } checkboxes.change.emit() callback.execute() """ ) slider.js_on_change('value', callback) select.js_on_change('value', callback) checkboxes.js_on_change('active', callback) checkboxAll.js_on_change('active', allCheckCallback) from bokeh.layouts import column, row layout = column( title, preamble, row(select, varNote), row(column(lineFig, slider, barFig), column(checkboxes, checkboxAll)), mapFig ) return layout
menu = [("Item 1", "item_1_value"), ("Item 2", "item_2_value"), None, ("Item 3", "item_3_value")] dropdown = Dropdown(label="Dropdown button", button_type="warning", menu=menu) dropdown.js_on_event("button_click", CustomJS(code="console.log('dropdown: click ' + this.toString())")) dropdown.js_on_event("menu_item_click", CustomJS(code="console.log('dropdown: ' + this.item, this.toString())")) dropdown_disabled = Dropdown(label="Dropdown button (disabled)", button_type="warning", disabled=True, menu=menu) dropdown_disabled.js_on_event("button_click", CustomJS(code="console.log('dropdown(disabled): click ' + this.toString())")) dropdown_disabled.js_on_event("menu_item_click", CustomJS(code="console.log('dropdown(disabled): ' + this.item, this.toString())")) dropdown_split = Dropdown(label="Split button", split=True, button_type="danger", menu=menu) dropdown_split.js_on_event("button_click", CustomJS(code="console.log('dropdown(split): click ' + this.toString())")) dropdown_split.js_on_event("menu_item_click", CustomJS(code="console.log('dropdown(split): ' + this.item, this.toString())")) checkbox_group = CheckboxGroup(labels=["Option 1", "Option 2", "Option 3"], active=[0, 1]) checkbox_group.js_on_change('active', CustomJS(code="console.log('checkbox_group: active=' + this.active, this.toString())")) radio_group = RadioGroup(labels=["Option 1", "Option 2", "Option 3"], active=0) radio_group.js_on_change('active', CustomJS(code="console.log('radio_group: active=' + this.active, this.toString())")) checkbox_button_group = CheckboxButtonGroup(labels=["Option 1", "Option 2", "Option 3"], active=[0, 1]) checkbox_button_group.js_on_event("button_click", CustomJS(code="console.log('checkbox_button_group: active=' + this.origin.active, this.toString())")) radio_button_group = RadioButtonGroup(labels=["Option 1", "Option 2", "Option 3"], active=0) radio_button_group.js_on_event("button_click", CustomJS(code="console.log('radio_button_group: active=' + this.origin.active, this.toString())")) widget_box = Column(children=[ button, button_disabled, toggle_inactive, toggle_active, dropdown, dropdown_disabled, dropdown_split, checkbox_group, radio_group,
def index(): form = CategoriseButton() cnx = app.config.get('SQLALCHEMY_DATABASE_URI') if form.validate_on_submit(): rules = {} categories = [ x[0] for x in db.session.query(CategoryRule.category.distinct()).all() ] for cat in categories: cat_rules = CategoryRule.query.filter_by(category=cat).all() rules[cat] = [] for rule in cat_rules: rules[cat].append({ 'str': rule.string_match, 'date': rule.date_match, 'exact': rule.exact_rule, 'max_amt': rule.max_amount }) df_transactions = pd.read_sql_query( ''' SELECT * FROM "transaction"''', cnx) df_transactions['transaction_date'] = pd.to_datetime( df_transactions['transaction_date']) df_transactions['transaction_date'] = df_transactions[ 'transaction_date'].dt.date def categorise(name, date, amount, categories): for category in categories: for rule in categories[category]: if rule['exact'] == True: if (rule['max_amt'] == None) or (amount < rule['max_amt']): if rule['date'] == None and rule['str'] == name: return category if rule['date'] == date and rule['str'] == name: return category else: if (rule['max_amt'] == None) or (amount < rule['max_amt']): if rule['date'] == None and rule['str'] in name: return category if rule['date'] == date and rule['str'] in name: return category df_transactions['category'] = df_transactions.apply( lambda x: categorise(x['description'], x['transaction_date'], x[ 'debits'], rules), axis=1) transactions = Transaction.query.all() for i, t in enumerate(transactions): t.category = df_transactions['category'].iloc[i] db.session.commit() ############################################### # Create the plot ############################################### df_transactions = pd.read_sql_query( ''' SELECT * FROM "transaction" ORDER BY transaction_date DESC, description ASC''', cnx) df_transactions['transaction_date'] = pd.to_datetime( df_transactions['transaction_date']) df_transactions = df_transactions[ df_transactions['transaction_date'] >= '2019-01-01'] df_transactions[ 'MonthBegin'] = df_transactions['transaction_date'] - pd.Timedelta( '1d') * (df_transactions['transaction_date'].dt.day - 1) df = df_transactions.groupby(['MonthBegin', 'category']).agg({ 'debits': 'sum', 'credits': 'sum' }).reset_index() df['NetSpend'] = df['debits'] - df['credits'] df['left'] = df['MonthBegin'] + pd.Timedelta(days=8) df['right'] = df['MonthBegin'] + pd.Timedelta(days=22) df['top'] = df['NetSpend'] df['bottom'] = 0.0 categories = sorted(df['category'].unique().tolist()) for i, cat in enumerate(categories): idx = i while idx > 19: idx -= 20 df.loc[df['category'] == cat, 'colour'] = d3['Category20'][20][idx] df = df.set_index(['MonthBegin']) df = df.drop(['debits', 'credits'], axis=1) df.sort_index(inplace=True) source = ColumnDataSource(data=df) category = 'Groceries' df_filtered = df.copy() df_filtered.loc[df_filtered['category'] != category] = np.nan source_filtered = ColumnDataSource(data=df_filtered) plot = make_plot(source_filtered) callback = CustomJS(args=dict(source=source, source_filtered=source_filtered), code=""" var chosen_idxs = cb_obj.active; var labels = cb_obj.labels; var month_sum = {}; var category_bottom = {}; var chosen_categories = []; for (var i = 0; i < chosen_idxs.length; i++) { chosen_categories.push(labels[chosen_idxs[i]]); } for (var i = 0; i < chosen_categories.length; i++) { for (var j = 0; j < source.data['category'].length; j++) { if (source.data['category'][j]==chosen_categories[i]) { if (!month_sum.hasOwnProperty(source.data['MonthBegin'][j].toString())) { month_sum[source.data['MonthBegin'][j].toString()] = 0.0; } if (!category_bottom.hasOwnProperty(i)) { category_bottom[i] = {} } category_bottom[i][source.data['MonthBegin'][j].toString()] = JSON.parse(JSON.stringify(month_sum[source.data['MonthBegin'][j].toString()])); month_sum[source.data['MonthBegin'][j].toString()] += Math.abs(source.data['NetSpend'][j]); } } } for (var i = 0; i < source.data['category'].length; i++) { if (chosen_categories.includes(source.data['category'][i])) { source_filtered.data['category'][i] = source.data['category'][i]; source_filtered.data['MonthBegin'][i] = source.data['MonthBegin'][i]; source_filtered.data['NetSpend'][i] = source.data['NetSpend'][i]; source_filtered.data['left'][i] = source.data['left'][i]; source_filtered.data['right'][i] = source.data['right'][i]; if (category_bottom[chosen_categories.indexOf(source.data['category'][i])].hasOwnProperty(source.data['MonthBegin'][i].toString())) { source_filtered.data['bottom'][i] = category_bottom[chosen_categories.indexOf(source.data['category'][i])][source.data['MonthBegin'][i].toString()]; } else { source_filtered.data['bottom'][i] = 0.0; } source_filtered.data['top'][i] = source_filtered.data['bottom'][i] + Math.abs(source_filtered.data['NetSpend'][i]); if (source_filtered.data['NetSpend'][i] < 0.0) { source_filtered.data['colour'][i] = '#ffffff'; } else { source_filtered.data['colour'][i] = source.data['colour'][i]; } } else { source_filtered.data['category'][i] = undefined; source_filtered.data['MonthBegin'][i] = undefined; source_filtered.data['NetSpend'][i] = undefined; source_filtered.data['left'][i] = undefined; source_filtered.data['right'][i] = undefined; source_filtered.data['bottom'][i] = undefined; source_filtered.data['top'][i] = undefined; source_filtered.data['colour'][i] = undefined; } } source_filtered.change.emit(); """) category_select = CheckboxGroup(labels=categories, active=[categories.index(category)]) category_select.js_on_change('active', callback) plotgrid = grid([plot, category_select], ncols=2) ############################################### # Embed plot into HTML via Flask Render script, div = components(plotgrid) df_transactions = pd.read_sql_query( ''' SELECT * FROM "transaction" WHERE category IS NULL ORDER BY transaction_date DESC, description ASC''', cnx) return render_template('index.html', title='Home', table=df_transactions, form=form, script=script, div=div)