Ejemplo n.º 1
0
    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()
Ejemplo n.º 2
0
    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()
Ejemplo n.º 3
0
Archivo: gui.py Proyecto: YDnepr/asda
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
Ejemplo n.º 4
0
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
Ejemplo n.º 5
0
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  
Ejemplo n.º 6
0
Archivo: gui.py Proyecto: YDnepr/asda
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  
Ejemplo n.º 7
0
Archivo: gui.py Proyecto: YDnepr/asda
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 
Ejemplo n.º 8
0
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)