def test_callback_property_executes(self, bokeh_model_page): button = Toggle(css_classes=['foo']) button.callback = CustomJS(code=RECORD("value", "cb_obj.active")) page = bokeh_model_page(button) button = page.driver.find_element_by_class_name('foo') button.click() results = page.results assert results == {'value': True} button = page.driver.find_element_by_class_name('foo') button.click() results = page.results assert results == {'value': False} button = page.driver.find_element_by_class_name('foo') button.click() results = page.results assert results == {'value': True} assert page.has_no_console_errors()
def test_callback_property_executes(self, bokeh_model_page): button = Toggle(css_classes=['foo']) button.callback = CustomJS(code=RECORD("value", "cb_obj.active")) page = bokeh_model_page(button) button = page.driver.find_element_by_css_selector('.foo .bk-btn') button.click() results = page.results assert results == {'value': True} button = page.driver.find_element_by_css_selector('.foo .bk-btn') button.click() results = page.results assert results == {'value': False} button = page.driver.find_element_by_css_selector('.foo .bk-btn') button.click() results = page.results assert results == {'value': True} assert page.has_no_console_errors()
def plotPairs(fileName, bokehPlaceholderId='bokehContent'): source = ColumnDataSource(data={'count':[], 'index':[]}) minCount = TextInput(value="20", title="Minimum number of trips per pair:") checkbox_group = CheckboxGroup(labels=["Include trips from a station to itself."]) ShowButton = Toggle(label="Show", type="success") model = dict(source=source, checkbox = checkbox_group, minCount = minCount) plot = Figure(title="Accumulative Number Of Trips Over Top Station Pairs", x_axis_label='Top Station Pairs in Decreasing Order', y_axis_label='Accumulative Number of Trips', plot_width=1200, plot_height=400) plot.ray(x = [0], y=[90], line_color='red', angle=0.0, name='90%', length = 0, line_width=2) plot.ray(x = [0], y=[90], line_color='red', angle=180.0, angle_units = 'deg', length = 0, line_width=2) plot.line(x = 'index', y ='count', source=source, line_width=2, line_alpha=0.6) callback = CustomJS(args=model, code=""" var selectSelf = checkbox.get('active').length; var minCountVal = minCount.get('value'); var xmlhttp; xmlhttp = new XMLHttpRequest(); xmlhttp.onreadystatechange = function() { if (xmlhttp.readyState == XMLHttpRequest.DONE ) { if(xmlhttp.status == 200){ var data = source.get('data'); var result = JSON.parse(xmlhttp.responseText); data['count'] = result.count; data['index'] = result.index; source.trigger('change'); alert('triggered'); } else if(xmlhttp.status == 400) { alert(400); } else { alert(xmlhttp.status); } } }; var params = {self:selectSelf, min:minCountVal}; url = "/pairsdist?" + jQuery.param( params ); xmlhttp.open("GET", url, true); xmlhttp.send(); """) ShowButton.callback = callback layout = vform(checkbox_group, minCount, ShowButton, plot) script, div = components(layout) html = readHtmlFile(fileName) html = insertScriptIntoHeader(html, script) html = appendElementContent(html, div, "div", "bokehContent") return html
def plotHistogram(fileName, initData, stations, dateRange, bokehPlaceholderId='bokehContent'): data = { 'xs': [initData['bins']], 'ys': [initData['values']], 'ss': [1, 2], 'es': [3, 4] } #ss and es are for test purposes we'll add other values of the controlles e.g. age, usertype, Gender coming fetshed from initdata source = ColumnDataSource(data=data) stations.insert(0, "All") selectSS = Select(title="Start Station:", value="All", options=stations) selectES = Select(title="End Station:", value="All", options=stations) selectUT = Select(title="User Type:", value="All", options=["All", "Subscriber", "Customer"]) selectGender = Select(title="Gender:", value="All", options=["All", "Male", "Female"]) sliderAge = Slider(start=8, end=100, value=30, step=5, title="Age") startDP = DatePicker(title="Start Date:", min_date=dateRange[0], max_date=dateRange[1], value=dateRange[0]) endDP = DatePicker(title="End Date:", min_date=dateRange[0], max_date=dateRange[1], value=dateRange[1]) binSize = TextInput(value="15", title="Bin Size (Days):") AddButton = Toggle(label="Add", type="success") DeleteButton = Toggle(label="delete", type="success") columns = [ TableColumn(field="ss", title="Start Station"), TableColumn(field="es", title="End Station") ] # add other columns contains values of other controllers data_table = DataTable(source=source, columns=columns, width=650, height=300) model = dict(source=source, selectSS=selectSS, selectES=selectES, startDP=startDP, endDP=endDP, binSize=binSize, selectUT=selectUT, selectGender=selectGender, sliderAge=sliderAge) plot = Figure(plot_width=650, plot_height=400, x_axis_type="datetime") plot.multi_line('xs', 'ys', source=source, line_width='width', line_alpha=0.6, line_color='color') callback = CustomJS(args=model, code=""" //alert("callback"); var startStation = selectSS.get('value'); var endStation = selectES.get('value'); var startDate = startDP.get('value'); if ( typeof(startDate) !== "number") startDate = startDate.getTime(); var endDate = endDP.get('value'); if ( typeof(endDate) !== "number") endDate = endDate.getTime(); var binSize = binSize.get('value'); //alert(startStation + " " + endStation + " " + startDate + " " + endDate + " " + binSize); var xmlhttp; xmlhttp = new XMLHttpRequest(); xmlhttp.onreadystatechange = function() { if (xmlhttp.readyState == XMLHttpRequest.DONE ) { if(xmlhttp.status == 200){ var data = source.get('data'); var result = JSON.parse(xmlhttp.responseText); var temp=[]; for(var date in result.x) { temp.push(new Date(result.x[date])); } data['xs'].push(temp); data['ys'].push(result.y); source.trigger('change'); } else if(xmlhttp.status == 400) { alert(400); } else { alert(xmlhttp.status); } } }; var params = {ss:startStation, es:endStation, sd:startDate, ed:endDate, bs: binSize}; url = "/histogram?" + jQuery.param( params ); xmlhttp.open("GET", url, true); xmlhttp.send(); """) AddButton.callback = callback #DeleteButton.on_click(callback1) layout1 = vform(startDP, endDP, binSize) layout2 = vform(plot, DeleteButton, data_table) layout3 = vform(selectSS, selectES, selectUT, selectGender, sliderAge, AddButton) layout = hplot(layout1, layout2, layout3) script, div = components(layout) html = readHtmlFile(fileName) html = insertScriptIntoHeader(html, script) html = appendElementContent(html, div, "div", "bokehContent") return html
def plotHistogram(fileName, initData, stations, dateRange, bokehPlaceholderId='bokehContent'): data = {'xs':[initData['bins']], 'ys':[initData['values']],'ss':[1,2], 'es':[3,4] }#ss and es are for test purposes we'll add other values of the controlles e.g. age, usertype, Gender coming fetshed from initdata source = ColumnDataSource(data=data) stations.insert(0, "All") selectSS = Select(title="Start Station:", value="All", options=stations) selectES = Select(title="End Station:", value="All", options=stations) selectUT = Select(title="User Type:", value="All", options=["All", "Subscriber", "Customer"]) selectGender = Select(title="Gender:", value="All", options=["All", "Male", "Female"]) sliderAge = Slider(start=8, end=100, value=30, step=5, title="Age") startDP = DatePicker(title="Start Date:", min_date=dateRange[0] ,max_date=dateRange[1], value=dateRange[0]) endDP = DatePicker(title="End Date:", min_date=dateRange[0] ,max_date=dateRange[1], value=dateRange[1]) binSize = TextInput(value="15", title="Bin Size (Days):") AddButton = Toggle(label="Add", type="success") DeleteButton = Toggle(label="delete", type="success") columns = [TableColumn(field="ss", title="Start Station"),TableColumn(field="es", title="End Station")]# add other columns contains values of other controllers data_table = DataTable(source=source, columns=columns, width=650, height=300) model = dict(source=source, selectSS = selectSS, selectES = selectES, startDP = startDP, endDP = endDP, binSize = binSize,selectUT=selectUT,selectGender=selectGender,sliderAge=sliderAge) plot = Figure(plot_width=650, plot_height=400, x_axis_type="datetime") plot.multi_line('xs', 'ys', source=source, line_width='width', line_alpha=0.6, line_color='color') callback = CustomJS(args=model, code=""" //alert("callback"); var startStation = selectSS.get('value'); var endStation = selectES.get('value'); var startDate = startDP.get('value'); if ( typeof(startDate) !== "number") startDate = startDate.getTime(); var endDate = endDP.get('value'); if ( typeof(endDate) !== "number") endDate = endDate.getTime(); var binSize = binSize.get('value'); //alert(startStation + " " + endStation + " " + startDate + " " + endDate + " " + binSize); var xmlhttp; xmlhttp = new XMLHttpRequest(); xmlhttp.onreadystatechange = function() { if (xmlhttp.readyState == XMLHttpRequest.DONE ) { if(xmlhttp.status == 200){ var data = source.get('data'); var result = JSON.parse(xmlhttp.responseText); var temp=[]; for(var date in result.x) { temp.push(new Date(result.x[date])); } data['xs'].push(temp); data['ys'].push(result.y); source.trigger('change'); } else if(xmlhttp.status == 400) { alert(400); } else { alert(xmlhttp.status); } } }; var params = {ss:startStation, es:endStation, sd:startDate, ed:endDate, bs: binSize}; url = "/histogram?" + jQuery.param( params ); xmlhttp.open("GET", url, true); xmlhttp.send(); """) AddButton.callback = callback #DeleteButton.on_click(callback1) layout1 = vform (startDP,endDP,binSize) layout2 = vform(plot,DeleteButton,data_table) layout3 = vform(selectSS, selectES,selectUT,selectGender,sliderAge,AddButton) layout = hplot(layout1,layout2,layout3) script, div = components(layout) html = readHtmlFile(fileName) html = insertScriptIntoHeader(html, script) html = appendElementContent(html, div, "div", "bokehContent") return html
def plotHistogram(fileName, initData, stations, dateRange, bokehPlaceholderId='bokehContent'): MAX_AGE = 150 #The initial data data = { 'xs':[initData['bins']], 'ys':[initData['values']], 'ss':["All"], 'es':["All"], 'ut':["All"], 'g':["All"], 'a':[MAX_AGE], 'color':["blue"], 'line_width':[2] } source = ColumnDataSource(data=data) stations.insert(0, "All") selectSS = Select(title="Start Station:", value="All", options=stations) selectES = Select(title="End Station:", value="All", options=stations) selectUT = Select(title="User Type:", value="All", options=["All", "Subscriber", "Customer"]) selectGender = Select(title="Gender:", value="All", options=["All", "Male", "Female", "Unknown"]) sliderAge = Slider(start=0, end=MAX_AGE, value=0, step=1, title="Age LEQ") startDP = DatePicker(title="Start Date:", min_date=dateRange[0] ,max_date=dateRange[1], value=dateRange[0]) endDP = DatePicker(title="End Date:", min_date=dateRange[0] ,max_date=dateRange[1], value=dateRange[1]) binSize = TextInput(value="15", title="Bin Size (Days):") AddButton = Toggle(label="Add", type="success") DeleteButton = Toggle(label="Delete Selected", type="success") columns = [ TableColumn(field="ss", title="SS"), TableColumn(field="es", title="ES"), TableColumn(field="ut", title="User Type"), TableColumn(field="a", title="Age LEQ"), TableColumn(field="g", title="Sex") ] data_table = DataTable(source=source, columns=columns, width=400, row_headers=False, selectable='checkbox') model = dict(source=source, selectSS = selectSS, selectES = selectES, startDP = startDP, endDP = endDP, binSize = binSize,selectUT=selectUT,selectGender=selectGender,sliderAge=sliderAge, dt = data_table) addCallback = CustomJS(args=model, code=""" //alert("callback"); var startStation = selectSS.get('value'); var endStation = selectES.get('value'); var startDate = startDP.get('value'); if ( typeof(startDate) !== "number") startDate = startDate.getTime(); var endDate = endDP.get('value'); if ( typeof(endDate) !== "number") endDate = endDate.getTime(); var binSize = binSize.get('value'); var gender = selectGender.get('value'); var userType = selectUT.get('value'); var age = sliderAge.get('value'); //alert(age); //alert(startStation + " " + endStation + " " + startDate + " " + endDate + " " + binSize); var xmlhttp; xmlhttp = new XMLHttpRequest(); xmlhttp.onreadystatechange = function() { if (xmlhttp.readyState == XMLHttpRequest.DONE ) { if(xmlhttp.status == 200){ var data = source.get('data'); var result = JSON.parse(xmlhttp.responseText); var temp=[]; for(var date in result.x) { temp.push(new Date(result.x[date])); } data['xs'].push(temp); data['ys'].push(result.y); data['ss'].push(startStation); data['es'].push(endStation); data['ut'].push(userType); data['g'].push(gender); data['a'].push(age); data['color'].push('blue'); data['line_width'].push(2); source.trigger('change'); dt.trigger('change'); } else if(xmlhttp.status == 400) { alert(400); } else { alert(xmlhttp.status); } } }; var params = {ss:startStation, es:endStation, sd:startDate, ed:endDate, bs: binSize, g:gender, ut:userType, age:age}; url = "/histogram?" + jQuery.param( params ); xmlhttp.open("GET", url, true); xmlhttp.send(); """) deleteCallBack = CustomJS(args=dict(source=source, dt= data_table), code=""" var indices = source.get('selected')['1d'].indices; if(indices.length != 0){ indices.sort(); var data = source.get('data'); var counter = 0; var i = 0; var key = 0; var index = 0; for(i in indices) { index = indices[i]; index -= counter; for(key in data) { data[key].splice(index, 1); } counter += 1; } source.trigger('change'); dt.trigger('change'); } """) AddButton.callback = addCallback DeleteButton.callback = deleteCallBack; plot = Figure(title="Number Of Trips Over Time", x_axis_label='Time', y_axis_label='Number of trips', plot_width=750, plot_height=400, x_axis_type="datetime") plot.multi_line('xs', 'ys', source=source, line_width='line_width', line_alpha=0.9, line_color='color') l2 = vform(plot, hplot(startDP, endDP), binSize) l3 = vform(selectSS, selectES,selectUT,selectGender,sliderAge, hplot(AddButton, DeleteButton), data_table) layout = hplot(l2, l3) script, div = components(layout) html = readHtmlFile(fileName) html = insertScriptIntoHeader(html, script) html = appendElementContent(html, div, "div", "bokehContent") return html
def plotDistanceDurationRelation(fileName, dateRange, bokehPlaceholderId='bokehContent'): MAX_AGE = 150 #The initial data data = { 'x':[], 'y':[], 'ss':[], 'ss_lat':[], 'ss_long':[], 'es':[], 'es_lat':[], 'es_long':[], 'distance':[], 'avg_duration':[], 'count':[] } source = ColumnDataSource(data=data) ## Input Widgets ## textInputTopK = TextInput(value="1000", title="Number of Top Results Selected:") textInputMinDistance = TextInput(value="0.3", title="Minimum Distance Between Stations (km):") textInputMinDuration = TextInput(value="1", title="Minimum Trip Duration (min):") selectUT = Select(title="User Type:", value="All", options=["All", "Subscriber", "Customer"]) selectGender = Select(title="Gender:", value="All", options=["All", "Male", "Female", "Unknown"]) sliderAgeGeq = Slider(start=0, end=MAX_AGE, value=0, step=1, title="Age GEQ") sliderAgeLeq = Slider(start=0, end=MAX_AGE, value=0, step=1, title="Age LEQ") startDP = DatePicker(title="Start Date:", min_date=dateRange[0] ,max_date=dateRange[1], value=dateRange[0]) endDP = DatePicker(title="End Date:", min_date=dateRange[0] ,max_date=dateRange[1], value=dateRange[1]) ## Action Widget ## showButton = Toggle(label="Show", type="success") ## Output Widget ## columns = [ TableColumn(field="ss", title="SS"), TableColumn(field="es", title="ES"), TableColumn(field="distance", title="Distance (km)"), TableColumn(field="avg_duration", title="Avg Duration (min)"), TableColumn(field="count", title="#Trips") ] data_table = DataTable(source=source, columns=columns, width=850, row_headers=False, selectable='checkbox') ## Callbacks ## model = dict(source=source,textInputTopK=textInputTopK, textInputMinDuration=textInputMinDuration, textInputMinDistance=textInputMinDistance, startDP = startDP, endDP = endDP, selectUT=selectUT,selectGender=selectGender,sliderAgeLeq=sliderAgeLeq, sliderAgeGeq=sliderAgeGeq, dt = data_table) showCallback = CustomJS(args=model, code=""" //alert("callback"); var topK = textInputTopK.get('value'); var minDuration = textInputMinDuration.get('value'); var minDistance = textInputMinDistance.get('value'); var gender = selectGender.get('value'); var userType = selectUT.get('value'); var startAge = sliderAgeGeq.get('value'); var endAge = sliderAgeLeq.get('value'); var startDate = startDP.get('value'); if ( typeof(startDate) !== "number") startDate = startDate.getTime(); var endDate = endDP.get('value'); if ( typeof(endDate) !== "number") endDate = endDate.getTime(); var xmlhttp; xmlhttp = new XMLHttpRequest(); xmlhttp.onreadystatechange = function() { if (xmlhttp.readyState == XMLHttpRequest.DONE ) { if(xmlhttp.status == 200){ var data = source.get('data'); var result = JSON.parse(xmlhttp.responseText); data['ss']=result.ss; data['ss_lat']=result.ss_lat; data['ss_long']=result.ss_long; data['es']=result.es; data['es_lat']=result.es_lat; data['es_long']=result.es_long; data['distance']=result.distance; data['avg_duration']=result.avg_duration; data['count']=result.count; source.trigger('change'); dt.trigger('change'); } else if(xmlhttp.status == 400) { alert(400); } else { alert(xmlhttp.status); } } }; var params = {topK:topK, minDuration:minDuration, minDistance:minDistance, gender:gender, userType:userType, startAge:startAge, endAge:endAge, startDate:startDate, endDate:endDate}; url = "/pairsDistanceOverDuration?" + jQuery.param( params ); xmlhttp.open("GET", url, true); xmlhttp.send(); """) showButton.callback = showCallback TOOLS = "box_zoom,reset,pan,wheel_zoom,save, tap, poly_select" plot = Figure(title="Station Pair Distance/Trip Duration Relation", x_axis_label='Distance Between Stations (km)', y_axis_label='Average Trip Duration (min)', plot_width=850, plot_height=400, tools=TOOLS) plot.circle('distance', 'avg_duration', source=source, color="navy", size = 5, alpha = 0.7) l1 = vform(plot, data_table) tab1 = Panel(child = vform(textInputTopK, textInputMinDistance), title="Station Pairs Filters") tab2 = Panel(child = vform(textInputMinDuration, selectUT, selectGender,sliderAgeLeq, sliderAgeGeq, startDP, endDP), title="Trips Filters") tabs = Tabs(tabs=[tab1, tab2]) layout = hplot(vplot(tabs, showButton), l1) script, div = components(layout) html = readHtmlFile(fileName) html = insertScriptIntoHeader(html, script) html = appendElementContent(html, div, "div", "bokehContent") return html
def choropleth_map( data, geo, bins=[0, 2, 5, 10, 15, 20, 25, 30, 100], bin_labels="%", size=(1100, 550), hover_color='#3bdd9d', nodata_color='#d9d9d9', output_name="obesity-trends.html", output_title="Obesity trends", notebook=False, value_title="Obesity rate (%)", value_axis_label="% Obesity (BMI ≥ 30)", map_title="Share of adults who are obese in {}", default_map_year=1975, map_palette='YlOrRd', chart_title="Evolution of obesity", default_chart_country="France", country_palette='gist_ncar', footer_text=textwrap.dedent("""\ Data: World Health Organization - Global Health Observatory</br > Author: <a href="https://cbouy.github.io">Cédric Bouysset</a>"""), ): ''' data: Pandas DataFrame containing the numeric value to display, and the following columns: ['Country', 'Code', 'Year'] geo: GeoPandas DataFrame containing the info to plot each country. Columns must be name ['Country', 'Code', 'geometry'] bins: list of bins to label the data. Must contain the min and max possible values. bin_labels: labels shown on the graph. Can be a str which will be appended to every bin, or a list of custom labels. map_palette: color palette assigned to the bins and displayed on the map. Can be the name of a brewer palette from Bokeh or a list of hex color codes of length len(bins)-1. country_palette: color palette used for the chart. Can be the name of a matplotlib colormap or a list of hex color codes. ''' # assign nan values when a year is missing for a country df = data[["Year", "Code"]].copy() mux = pd.MultiIndex.from_product( [df["Year"].unique(), df["Code"].unique()], names=["Year", "Code"]) df = df.reindex(mux).drop(columns=["Year", "Code"]).reset_index() data = pd.merge(df, data, how="left", on=["Year", "Code"]) # create stylish labels if isinstance(bin_labels, str): bin_labels = [f'≤{bins[1]}{bin_labels}'] + [ f'{bins[i]}-{bins[i+1]}{bin_labels}' for i in range(1, len(bins) - 2) ] + [f'>{bins[-2]}{bin_labels}'] # assign each row to a bin data['bin'] = pd.cut( data['Value'], bins=bins, right=True, include_lowest=True, precision=0, labels=bin_labels, ).astype(str) # Merge the geographic data with obesity data df = geo.merge(data, on='Code', how='left') df = df.drop(columns="Country_y").rename(columns={"Country_x": "Country"}) # Add a 'No data' bin for countries without data on the endpoint df.loc[df['Value'].isna(), 'bin'] = 'No data' df.fillna('No data', inplace=True) if isinstance(map_palette, str): map_palette = brewer[map_palette][len(bins) - 1] # Assign obesity prevalence to a color def val_to_color(value, nan_color=nodata_color): if isinstance(value, str): return nan_color for i in range(1, len(bins)): if value <= bins[i]: return map_palette[i - 1] df['color'] = df['Value'].apply(val_to_color) # assign x coordinates def bin_to_cbar_x(value): if value == 'No data': return -2 for i, b in enumerate(bin_labels): if value == b: return 5 * (i + 1) df['cbar_x'] = df['bin'].apply(bin_to_cbar_x) # assign width df['cbar_w'] = df['Value'].apply(lambda x: 5 if x == 'No data' else 4.7) # create color palette for the graph countries = sorted(df[df["bin"] != "No data"]["Country"].unique()) n_country = len(countries) if isinstance(country_palette, str): cmap = plt.get_cmap(country_palette, n_country) country_palette = [rgb2hex(cmap(i)[:3]) for i in range(cmap.N)] # define the output reset_output() if notebook: output_notebook(hide_banner=True) else: output_file(output_name, title=output_title, mode="inline") # Input sources df.sort_values(by=["Country", "Year"], inplace=True) # source that will contain all necessary data for the map geosource = GeoJSONDataSource(geojson=df.to_json()) # source that contains the data that is actually shown on the map (for a given year) displayed_src = GeoJSONDataSource( geojson=df[df['Year'].isin(['No data', default_map_year])].to_json()) # source that will be used for the graph (we don't need the countries shapes for this) country_source = ColumnDataSource( df[df['Country'] == default_chart_country].drop(columns=["geometry"])) # Tools # slider to select the year temp = pd.to_numeric(df["Year"], errors="coerce") min_year = int(temp.min()) max_year = int(temp.max()) del temp slider = Slider(title='Year', start=min_year, end=max_year, step=1, value=default_map_year) # hover tool for the map map_hover = HoverTool( tooltips=[('Country', '@Country (@Code)'), (value_title, '@Value')]) # hover tool for the graph graph_hover = HoverTool( tooltips=[('Country', '@Country (@Code)'), (value_title, '@Value'), ('Year', '@Year')]) # button for the animation anim_button = Toggle(label="▶ Play", button_type="success", width=50, active=False) # create map figure p = figure( title=map_title.format(default_map_year), plot_width=size[0], plot_height=size[1], toolbar_location="right", tools="tap,pan,wheel_zoom,box_zoom,save,reset", toolbar_sticky=False, active_scroll="wheel_zoom", ) p.title.text_font_size = '16pt' p.xgrid.grid_line_color = None p.ygrid.grid_line_color = None p.axis.visible = False # Add hover tool p.add_tools(map_hover) # Add patches (countries) to the figure patches = p.patches( 'xs', 'ys', source=displayed_src, fill_color='color', line_color='black', line_width=0.25, fill_alpha=1, hover_fill_color='color', ) # outline when we hover over a country patches.hover_glyph.line_color = hover_color patches.hover_glyph.line_width = 3 patches.nonselection_glyph = None # create the interactive colorbar p_bar = figure(title=None, plot_height=80, plot_width=int(size[0] * 0.6), tools="tap", toolbar_location=None) p_bar.xgrid.grid_line_color = None p_bar.ygrid.grid_line_color = None p_bar.outline_line_color = None p_bar.yaxis.visible = False # set the title and ticks of the colorbar p_bar.xaxis.axis_label = value_axis_label p_bar.xaxis.ticker = sorted(df['cbar_x'].unique()) p_bar.xaxis.major_label_overrides = dict([ (i[0], i[1]) for i in df.groupby(['cbar_x', 'bin']).describe().index ]) p_bar.xaxis.axis_label_text_font_size = "12pt" p_bar.xaxis.major_label_text_font_size = "10pt" # activate the hover but hide tooltips hover_bar = HoverTool(tooltips=None) p_bar.add_tools(hover_bar) # plot the rectangles for the colorbar cbar = p_bar.rect(x='cbar_x', y=0, width='cbar_w', height=1, color='color', source=displayed_src, hover_line_color=hover_color, hover_fill_color='color') # outline when we hover over the colorbar legend cbar.hover_glyph.line_width = 4 cbar.nonselection_glyph = None # create the graph figure p_country = figure( title=chart_title, plot_width=size[0], plot_height=int(size[1] * 1.3), tools="pan,wheel_zoom,save", active_scroll="wheel_zoom", toolbar_location="right", ) p_country.title.text_font_size = '14pt' p_country.xaxis.axis_label = "Year" p_country.yaxis.axis_label = value_axis_label p_country.axis.major_label_text_font_size = "12pt" p_country.axis.axis_label_text_font_size = "14pt" # plot data on the figure line_plots = {} legend_items = {} for i, country in enumerate(countries): # get subset of data corresponding to a country country_source = ColumnDataSource( df[df['Country'] == country].drop(columns=["geometry"])) # plot line = p_country.line("Year", "Value", legend=False, source=country_source, color=country_palette[i], line_width=2) circle = p_country.circle("Year", "Value", legend=False, source=country_source, line_color="darkgrey", fill_color=country_palette[i], size=8) # used later in the interactive callbacks line_plots[country] = [line, circle] legend_items[country] = LegendItem(label=country, renderers=[line, circle]) # only display France at first if country != default_chart_country: line.visible = False circle.visible = False default_legend = [ (default_chart_country, line_plots[default_chart_country]), ] legend = Legend(items=default_legend, location="top_center") legend.click_policy = "hide" p_country.add_layout(legend, 'right') # Add hover tool p_country.add_tools(graph_hover) # JS callbacks # Update the map on slider change slider_callback = CustomJS(args=dict(slider=slider, source=geosource, displayed_src=displayed_src), code=""" var year = slider.value; var show = [year, 'No data']; var data = {}; columns = Object.keys(source.data); columns.forEach(function(key) { data[key] = []; }); for (var i = 0; i < source.get_length(); i++){ if (show.includes(source.data['Year'][i])){ columns.forEach(function(key) { data[key].push(source.data[key][i]) }); } } displayed_src.data = data; displayed_src.change.emit(); """) slider.js_on_change('value', slider_callback) # Update figure title from slider change callback_title = CustomJS(args=dict(slider=slider, figure=p, title=map_title), code=""" var year = slider.value; figure.title.text = title.replace("{}", year); """) slider.js_on_change('value', callback_title) # Add callback on country click plot_callback = CustomJS(args=dict(csource=country_source, source=geosource, displayed_src=displayed_src, line_plots=line_plots, legend=legend, legend_items=legend_items), code=""" // only continue if a country was selected var ixs = displayed_src.selected.indices; if (ixs.length == 0) { return; } // init var data = {}; var items = []; countries = []; columns = Object.keys(source.data); columns.forEach(function(key) { data[key] = []; }); // hide all plots for (var country in line_plots) { var line = line_plots[country][0]; var circle = line_plots[country][1]; line.visible = false; circle.visible = false; } // loop over the selected countries ixs.forEach(function(ix) { // identify corresponding country country = displayed_src.data["Country"][ix]; countries.push(country); }); // sort them in order countries.sort() // display the corresponding glyphs and legend countries.forEach(function(country) { line = line_plots[country][0]; circle = line_plots[country][1]; line.visible = true; circle.visible = true; items.push(legend_items[country]); for (var i = 0; i < source.get_length(); i++){ if (source.data['Country'][i] == country) { columns.forEach(function(key) { data[key].push(source.data[key][i]) }); } } }); legend.items = items; csource.data = data; csource.change.emit(); """) displayed_src.selected.js_on_change('indices', plot_callback) # add animation update_interval = 500 # in ms anim_callback = CustomJS(args=dict(slider=slider, update_interval=update_interval, max_year=max_year, min_year=min_year), code=""" var button = cb_obj; if (button.active == true){ button.label = "◼ Stop"; button.button_type = "danger"; mytimer = setInterval(update_year, update_interval); } else { button.label = "▶ Play"; button.button_type = "success"; clearInterval(mytimer); } function update_year() { year = slider.value; if (year < max_year) { slider.value += 1; } else { slider.value = min_year; } } """) anim_button.callback = anim_callback # arrange display with tabs tab_map = Panel( title="Map", child=column( p, # map p_bar, # colorbar row(widgetbox(anim_button), Spacer(width=10), widgetbox(slider)) # animation button and slider )) tab_chart = Panel(title="Chart", child=column(p_country)) tabs = Tabs(tabs=[tab_map, tab_chart]) # save the document and display it ! footer = Div(text=footer_text) layout = column(tabs, footer) show(layout)