Is the Select dropdown Interactive widgets a Post/server request or not?

Hi I am interested in using a select widget to filter my data set but it can’t be a post request or call the server to filter the data. It must do so from the browser.

Now I wonder if the select built in widget and python code is a POST request or an ajax front end only html call.

I will be passing in all my data. For example assume I have 24 temperature data points (one temperature for each hour of the day ) for the whole month of April. The columndatasource will contain all the data ie the temperature data of each hour for each of the 30 days of april. However on page load it will only display the today’s data, The user can use a select dropdown to pick and see the 24 hour temperature data for a different day. Is that built in bokeh going to do a post request on the select widget to update the chart or is it a front end javascript which will not do a server call and can filter the data in the browser

This is the link to the docs. I’m using BOkeh 1.4 but I assume 2.0 for this shouldn’t be too different for what I need

Thanks

Amit

It’s neither, Bokeh server uses a websocket protocol exclusively. However, I don’t understand if you are using the Bokeh server or not, this statement is confusing:

I am interested in using a select widget to filter my data set but it can’t be a post request or call the server to filter the data. It must do so from the browser.

Is there server mentioned here a Bokeh server? Some other server? It’s not clear what your configuration is.

Hi Bryan

I’m using Django as my web framework. So infrastructure wise i have a microsoft database which I call get the data write some python code and pass the chart as a string object and then use some javascript to embed the chart into some div element in my html box.

In that case your option is to add a CustomJS callback to the select widget:

select.js_on_change('value', callback)

The CustomJS can execute whatever Javascript code you need to update your plot data (e.g. make a an Ajax all, whatever). General information about JS callbacks is here:

hmm I see let me try that and see how that works.
In my case I don’t need an ajax call or call to any external server. It has to be a pure frontend solution. I just need the select widget to display and filter the data for eg instead of showing the temperature for todays date if the user selects yesterdays Apr 29 date it will show the hourly temperature of april 29. Now granted my columnDatasource has the hourl temperature for the whole month of april so i just want to do a splice/filter operation to show parts of the chart data

Hopefully that makes it more clearer

Sure, if you are able to send all fo the data you will ever need into the Bokeh document up front, then you can filter the data in the CustomJS callback. You could do this by hand, but depending on specifics you might be able to make use of a CDSView to help with the filtering. @p-himik has helped a few other folks here with very similar tasks so you might use the search feature to look for recent posts about filtering in JS callbacks.

thanks i’ll read and study this a bit more.
I decided to ask first rather then implement a full low level custom javascript implementation instead.

Another question is the CustomJS callback tied to only bokeh’s in built controls or can they be used or attached to say external widgets like the ionden slider. So the CustomJS callback say for bokeh’s built in slider or dropdown can it be used with an ionden slider.

We’re using this third party slider for one of the charts

I’m wondering if I can use the slider CustomJS callback with this slider

If you create a Bokeh custom extension to wrap some third party widget (e.g. ionRangeSlider) then the custom extension could be written to accept and use a CustomJS just like the built-in slider.

If you are just using a bare ionRangeSlider in an HTML template or something, then not really. Technically speaking there is nothing to stop you from getting ahold of the Bokeh Document somehow, and digging though it to find the CustomJS object that you want, then calling its execute method. But that’s obviously a pain.

Later this year I’d really like to introduce a new concept of “virtual widget interfaces”. These would be lightweight Bokeh models that only deal with data updates, and don’t participate in any Bokeh layout at all. The idea is to make it easy take external external widgets in HTML templates, and wire them up to Bokeh data sources and callbacks in a standard way that was much less work than creating a custom extension. I don’t have any timeframe for this yet, though (collaborators would be welcome).

Hello

Can you give me an example of which Bokeh Document key you are talking about or how to access it from the front end html using pure javascript please as that is the option we are using. Using a third party IonDen slider in html and making it filter the chart displayed using the Bokeh.embed.embed_item(datax) option.
Do you need a piece of example code that could help you ?
Thanks

At present there is not a great method. There is a global Bokeh.index that has a map of ids to Bokeh views and from the views you can get to Bokeh models via their .model property. So you would just have to root around that object graph to find whatever Bokeh model you wanted to update. In the future, embed_item will return the embedded model so you can just keep a reference to it if you want.

Ok below find the code in Django 2 and bokeh I’m using 1.4 on how to use a ionden range slider to filter the chart of a data. I’m going to be posting my code so others can follow through. The way I did it doesn’t need typescript and you can write some javascript code to do the change on the front end. If there is any modification or code enhancement please let me know.

urls.py
#module where all the urls ares tored in our case it will be dashboard/ionden url link
#http://127.0.0.1:8000/dashboard/ionden
#temporary test page to see if its possible for CDSVIew to work with django errors popped up in prototype using bokeh 1,0

        path('ionden', views.IonDenView.as_view(), name='ionden'),

views.py

views.py
this is going to be a class object that generates some pseudo data and saves it as a string to then display the chart into the html document similar to Django and flask. Note that this chart has two yaxis and the xtimestamps are a bit off but thats okay. I created some static arrays and saved the data.
The trick is to save the data twice into two columndatasources. One columndatasource contains the full data set that is called full_source. The second columndatasource called curr_source only contains the slice/filtered piece you wish to display.
In the javascript code you will see the curr_source data being updated by taking the array slice of the full_source the fulll data set.

from bokeh.models import ColumnDataSource,FactorRange, InputWidget, CustomJS, Slider, CDSView
from bokeh.models import IndexFilter, GroupFilter, CustomJSFilter, Select, DatetimeTickFormatter
from bokeh.embed import components
from bokeh.plotting import Figure, curdoc
from bokeh.models import Range1d, LinearAxis
from bokeh.layouts import column, row, grid, layout
import json
from bokeh.embed import json_item
from bokeh.layouts import column, row, grid
from datetime import datetime
import time
from datetime import timedelta
class IonDenView(View):
    def __init__(self):
        #class to generate some data 
        self.template_name = "dashboard/ionden.html"
    def get(self, request):
        context = {}
        #third archtiecture a list of all combined this is a list of datetimestamps
        T1 = datetime.strptime("2020-04-30 00:00:00", "%Y-%m-%d %H:%M:%S")
        T2 = datetime.strptime("2020-04-30 01:00:00", "%Y-%m-%d %H:%M:%S")
        T3 = datetime.strptime("2020-04-30 02:00:00", "%Y-%m-%d %H:%M:%S")
        T4 = datetime.strptime("2020-04-30 03:00:00", "%Y-%m-%d %H:%M:%S")
        T5 = datetime.strptime("2020-04-30 04:00:00", "%Y-%m-%d %H:%M:%S")
        T6 = datetime.strptime("2020-04-30 05:00:00", "%Y-%m-%d %H:%M:%S")
        T7 = datetime.strptime("2020-04-30 06:00:00", "%Y-%m-%d %H:%M:%S")
        T8 = datetime.strptime("2020-04-30 07:00:00", "%Y-%m-%d %H:%M:%S")
        T9 = datetime.strptime("2020-04-30 08:00:00", "%Y-%m-%d %H:%M:%S")
        T10 = datetime.strptime("2020-04-30 09:00:00", "%Y-%m-%d %H:%M:%S")
        xValues = [T1,T2,T3,T4,T5,T6,T7,T8,T9,T10]

        #third archtiecture a list of all combined
        #second data construction idea
        full_datax2 = {'x': [T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,
                        T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,
                        T1,T2,T3,T4,T5,T6,T7,T8,T9,T10], 
                    'y': [11019.3927693927, 10776.5364335049, 10665.6387722406,11019.3927693927,10776.5364335049,10665.6387722406,11019.392769392, 10819.1664372857, 10666.4884808127,13545.6409013634,
                            11019.392769392, 10819.1664372857, 10666.4884808127,13545.6409013634,14101.8252422713,14175.5187203708,11019.392769392, 10819.1664372857, 10666.4884808127,13545.6409013634,
                            10755.2114866876, 11315.4174414679, 10697.3738545654,11315.4174414679,12488.8314193305,13553.5572757138,11019.392769392, 10819.1664372857, 10666.4884808127,13545.6409013634], 
                    'y1': [10900.3524214508, 10700.1149649245, 10602.7809338582,10920.2050639338,11510.2752077477,12280.3819850462,11019.392769392, 10819.1664372857, 10666.4884808127,13545.6409013634,
                            10944.5877993602, 10700.6384037727, 10602.8669280333,10920.2441548973,11510.3023680626,12280.4111885769,11019.392769392, 10819.1664372857, 10666.4884808127,13545.6409013634,
                            10723.3167123311, 11041.6511776374, 10606.1508040811,11041.6511776374,11678.5417945967,12453.5836012828,11019.392769392, 10819.1664372857, 10666.4884808127,13545.6409013634,],
                    'temp': [12, 11, 11,10,11,11,12, 11, 11,10, 
                            11, 11, 11,12,13,14,12, 11, 11,10,
                            11, 15, 16,16,15,11,12, 11, 11,10],
                'z': ['1', '1', '1','1', '1', '1','1', '1', '1','1',
                        '2', '2', '2', '2', '2', '2','2', '2', '2', '2',
                        '3', '3', '3','3', '3', '3','3', '3', '3','3']}
                
        curr_datax3 = {'x': xValues,
                    'y': [10802.402023372, 11317.53093475, 12490.8960277275,13389.1294707906, 13494.4806046587, 13710.6162250893,10729.3107884848, 11041.769217588, 11678.6265949395, 13693.9409210058], 
                    'y1': [10729.3107884848, 11041.769217588, 11678.6265949395, 13693.9409210058, 13774.3895579185, 13957.9496061794,11678.6265949395, 13693.9409210058, 13774.3895579185, 13957.9496061794],
                    'temp': [10, 10, 10, 15, 15, 16,10, 15, 15, 16],
                'z': ['10', '10', '10','10', '10', '10','10', '10', '10','10']}

        curr_source = ColumnDataSource(data=curr_datax3, name="curr_source")
        full_source = ColumnDataSource(data=full_datax2, name="full_source")

        plot = Figure(plot_width=600, plot_height=400, y_range=[0,25])
        plot.xaxis.formatter=DatetimeTickFormatter(
                        hours=["%a %b %d %Y %H"],
                        days=["%a %b %d %Y %H"],
                        months=["%a %b %d %Y %H"],
                        years=["%a %b %d %Y %H"],
                    )
        plot.xaxis.major_label_orientation = "vertical"

        #you need to duplicate the sources the second source will always exist hte first source is the one that 
        #will change based on the slider or dropdown widget we select and use
        #this is empty but needed so that the ionden slider can actually filter the chart
        custom_filter = CustomJSFilter(args=dict(curr_source=curr_source, full_source=full_source), code = '''
        ''')
       
        #properties, js_event_callbacks
        #use CDS to filter the view
        view1 = CDSView(source = curr_source, filters = [custom_filter])

        #second y axis 
        plot.extra_y_ranges['aqew2'] = Range1d(0, 30000)
        ax2 = LinearAxis(y_range_name="aqew2", axis_label="aqew2")

        plot.vbar(x='x', top='temp', source=curr_source, width=600000, legend_label='temp',view=view1 )
        plot.line('x', 'y', source=curr_source, line_width=3, line_alpha=0.6, legend_label='aqew1',y_range_name = "aqew2",view=view1 )
        plot.line('x', 'y1', source=curr_source, line_width=3, line_color='darkmagenta', line_alpha=0.6,legend_label='aqew2', 
            y_range_name = "aqew2",view=view1 )

        plot.add_layout(ax2, 'right')
        
        l =layout([plot])

        #turn the layout into a json string
        item_text = json_item(l,'myplot')
        json_dataxxx = json.dumps(item_text)
        context.update({"item_graph":json_dataxxx})
        
        return render(request, self.template_name, context )

html file

ionden.html
This html file contains the chart the django class makes along with the ionden slider. It


<!DOCTYPE html>
<html>

<head>
<title> DailyDemandDaily </title>
<link rel="stylesheet" href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
<!--Plugin CSS file with desired skin-->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/ion-rangeslider/2.3.1/css/ion.rangeSlider.min.css"/>
<link rel="stylesheet" href="https://cdn.pydata.org/bokeh/release/bokeh-1.4.0.min.css" type="text/css" />
<!--bootstrap4 CDN links good to use CDN most sites use a cdn which is memory cached making this fast -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"/>
</head>


<script type="text/javascript" src="https://cdn.pydata.org/bokeh/release/bokeh-1.4.0.min.js"></script>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"></script>
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<!--jQuery-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>  
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<!--Plugin JavaScript file-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/ion-rangeslider/2.3.1/js/ion.rangeSlider.min.js"></script>



<script type="text/javascript">
	
	
	$(document).ready(function(){
		//embed the graph into the html document
		datax = {{item_graph|safe}};
		Bokeh.embed.embed_item(datax);

		//slider code initalize the slider
		var $range = $(".js-range-slider");

		$(".js-range-slider").ionRangeSlider({
			type: "single",
			min: 1,
			max: 3,
			from: 3,
			onStart: function(data) {
			},
			onChange: function(data,value) {
				//function that will update the chart based on slider changes
	 			varlen = Object.keys(Bokeh.index).length;
	            varnum = Object.keys(Bokeh.index)[varlen-1];
	            source = Bokeh.index[varnum].model; 
	            console.log(Bokeh, Object.keys(Bokeh), Bokeh.index);
	            //console.log('index num source ', varlen, varnum, source)

	            //here you are using the bokeh index to get the two columndatasources you need that you actually 
	            //named it is important you have them named to make this work and make your life easy
	            s2 = Bokeh.index[varnum].model.select_one("full_source");
	            source = Bokeh.index[varnum].model.select_one("curr_source");
	            //console.log('var1 ', source, s2);
				//console.log('onchange ran ', data.from);
				//extract each of the index values
				var zindex = s2.data['z'];
				//for the curr_source which I badly called source reinitalize the values as empty lists
			    source.data['x'] = [];
			    source.data['y'] = [];
			    source.data['y1'] = [];
			    source.data['z'] = [];
			    source.data['temp'] = [];
			    
			    for (i=0; i < zindex.length; i++) {
			    	//loop over the index if the z index value matches the slider value which is selected it will push and 
			    	//populate the x,y,y1,z,temp values from the full_source to the curr_source array. Only the matching datapoints 
			    	//will be pushed
					console.log(zindex[i]);
					if (zindex[i] == data.from) {
						console.log(i, zindex[i],s2.data['x'][i],s2.data['y'][i]);
						source.data['x'].push(s2.data['x'][i]);
						source.data['y'].push(s2.data['y'][i]);
						source.data['z'].push(s2.data['z'][i]);
						source.data['y1'].push(s2.data['y1'][i]);
						source.data['temp'].push(s2.data['temp'][i]);
					}
				}
				console.log(source.data);
				//as Brydev says this is used to tell Bokeh something changed and this will update your chart
			    source.change.emit();
			},
			onUpdate: function(data, value) {
				console.log('update ran');
			},

		});

		let range_instance2 = $range.data("ionRangeSlider");
		range_instance2.update({
		});
		

	});
</script>


<style>
	/*slider css values*/
/*https://github.com/IonDen/ion.rangeSlider/blob/master/css/ion.rangeSlider.css*/
#sliderDiv {
	padding-bottom: 10px !important;
	margin-bottom: 10px !important;
}

.range-slider {
    position: relative;
    height: 80px;
}
</style>

<div class="row gutter-10 mb-1">
	<div class = "header_title">Meter Data</div>	
</div>

<div class="row  gutter-10">
	<div class="col-12">
		<div class="radialbox gutter-10 firstrowgraph">
			<div class="myplot" id="myplot"></div>
			<div class="range-slider">
				<input type="text" class="js-range-slider" name="range-slider" value="" />
			</div>
		</div>
	</div>
</div>
</body>
</html>

This is the output of what you will get and should see and happen
you will see the slider start at the 3rd hour as that is the default current place we wish to use
so at the url http://127.0.0.1:8000/dashboard/ionden will show the output of the following gif

Hope this helps people and the code is complete enough for people to start using the ionden slider this way and duplicate for themselves.
If there are any recommendations or optimisations people recommend please advise

1 Like