Run the Ion range example provided inside Django View function

This is the error I’m getting when i try to take this example and try to embed it into django using the provided example : Adding a custom widget — Bokeh 2.4.2 Documentation

Uncaught Error: Model ‘IonRangeSlider’ does not exist. This could be due to a widget
or a custom model not being registered before first usage.

i put the Ion.py class inside a views.py file and the ts file as well but none of it works any idea what’s going on ?
i can get the example to run as is on my machine so i know the system is setup its when i try to port this code to django to run inside a view class/function it fails ?

This is the code setup

i placed the class IonRangeSlider inside django views.py file without changing any line of code it is copied pasted as written exactly inside the link above.

This is how I call it

callback_ion = CustomJS(args=dict(source=month_plot.x_range), code="""
                                       """)
ion_range_slider = IonRangeSlider(start=0.01, end=0.99, step=0.01,
            title='Ion Range Slider - Range', callback_policy='continuous')

ion_range_slider.js_on_change('value', callback_ion)

ODN_plot_l = layout([month_plot, ion_range_slider], 
                                 sizing_mode='stretch_width')
# create json item of graphs
json_ODN_plot = json_item(ODN_plot_l,'myplot11' )
jsplot_ajcall = {'N': json_ODN_plot  }
return JsonResponse(jsplot_ajcall)

I’m returning back a JsonResponse object as I’m doing a partial page update using ajax for the graph as well. But I get the error above ?

Note the ts file is not altered or affected or touched in any way either. It is copied and used exactly the same way

When you define a custom extension, you are creating a new Bokeh model that is not built-in to BokehJS. That new implementation code has to be registered with BokehJS before it can be used. THe message you see is BokehJS telling you that it does not know anything about the custom extension model you are trying to use, i.e. it has not been registered.

Although it has not occurred to me until just now, json_items is not compatible with using custom extensions. It’s possible work could be done so that json_items and custom extensions could be used together. If not, this limitation is something we should document better. Either way, I’d encourage you to make an issue on GitHub about this.

For now, the only option is to use the bokeh.embed.components function instead. This embed function does transmit custom extensions and makes sure they get registered. You might also want to look at the reference docs for components, since there are some options that might be useful in your partial loading scenario (i.e. generate raw JS code and not an actual <script> tag).

Just to elaborate for anyone curious about the details. Consider the following custom extension:

In [5]: from bokeh.core.properties import String, Instance
   ...: from bokeh.models import HTMLBox, Slider
   ...:
   ...: class Custom(HTMLBox):
   ...:
   ...:     __implementation__ = "console.log()"
   ...:
   ...:     text = String(default="Custom text")
   ...:
   ...:     slider = Instance(Slider)
   ...:

If you call json_item on this, you can see the implementation code is nowhere to be found:

In [6]: json_item(Custom())
Out[6]:
{'target_id': None,
 'root_id': '1068',
 'doc': {'roots': {'references': [{'attributes': {'slider': None},
     'id': '1068',
     'type': 'Custom'}],
   'root_ids': ['1068']},
  'title': '',
  'version': '1.2.1dev1-30-gb564a8709'}}

This is why json_item is not compatible with custom extensions. By contrast, the output of components does contain the implementation inside its output, as well as code to register the model with BokehJS:

In [10]: print(components(Custom())[0])

<script type="text/javascript">

  ...

  ({
    "custom/main": function(require, module, exports) {
      var models = {
        "Custom": require("custom/custom").Custom
      };
      require("base").register_models(models);
      module.exports = models;
    },
    "custom/custom": function(require, module, exports) {
  "use strict";
  console.log();

  }
  }, "custom/main");
  ;
  });

 ...

</script>

Ahh but with custom extensions will it be an ajax call i can’t have a page refresh for this it has to be a partial page update like an ajax call.

Essentially the user clicks a button to open a popup modal. Inside this popup modal is where the bokeh graph exists and i need to slider to filter/zoom or span the xrange axis, the same thing the ion range does. there are also secondary buttons which will and can then change the graph type and data as well but this will only refresh the popup not the page.

So I want to know if I can use components with ajax calls without a full page refresh ?

Because this is potentially a huge problem for me now for the product I am building possibly

I’ve never tried it, but AFAIK that was largely the point of the advanced parameters I mentioned listed in the reference guide. Those can be used to return raw JS that you can eval or create your own script tag for, and raw metadata that you could use to create the necessary div with yourself. (As opposed to the default behavior of getting actual HTML snippets you would put in an template)

Okay. Since I am still confused may you please post the links of hte documentation you are talking about specifically the piece of code you are referring too ?

Thank you

It’s the same link I provided above:

c.f. the wrap_script and wrap_plot_info parameters.

So this is what I’ve done so far

I use the layout function to make the layout

ODN_plot_l = layout([month_plot], sizing_mode='stretch_width')

#create json item of graphs 
json_ODN_plot = json_item(ODN_plot_l,'myplot11' )
jsplot_ajcall = {'N': json_ODN_plot  }
return JsonResponse(jsplot_ajcall)

Now this is done in an ajax call so keeping it succint this is how i get the graph out

modalimg = jsplot_ajcall.N;
console.log('N call ', modalimg);
Bokeh.embed.embed_item(modalimg);

Now this works and my graph is generated. Now my question is where is the information for the graph specifically the raw data or the x and y min and max ranges.

another alternative is I do everything myself client side. I can make the ionrange slider client side and use a jquery function to extract the data I need and change the graph client side on the html file myself without having to call the server. So to that strategy where in the javascript json object is my data stored ?

I can make the ionrange slider client side and use a jquery function to extract the data I need and change the graph client side

@amitsandhel If you are committed to using json_item then in fact his is a route I would probably recommend. Please see the topic User interaction without bokeh widgets? - #5 by Bryan for examples and information about locating Bokeh objects from non-Bokeh JS code.

Ok so using that link I still am having a bit of trouble. Using the browser console I can actually get to the data exactly like the link you posted using json_item. However if I try to do it programmatically using jquery I get undefined as a result.

This is the code I wrote

this is the object id of the graph/plot object in this case its 5879. As you can se its called myplot11 as the target_id
1. Object {target_id: “myplot11”, doc: Object, root_id: “5879”}
1. doc:Object
2. root_id:“5879”
3. target_id:“myplot11”
4. proto:Object

Now when I do 
//convert the root_id number to an int
var dataId = parseInt(jsplot_ajcall.rootid);  //where rootid is the root_id number 5879
var source = Bokeh.index[dataId];

Now source will give me undefined. Going to models.select_one won’t work as it can’t even find the object from Bokeh.index it seems for some odd reason.

However if I run this same number in the chrome console logger it will work for me see below

soure = Bokeh.index[5879].model.select_one("month23")
e {_subtype: undefined, document: t, destroyed: e, change: e, transformchange: e…}
source.data

soure.data
Object {value: Array(289), date: Array(289), valueFormatted: Array(289), dateHover: Array(289), valueHover: Array(289)…}

And this is correct as this graph has 289 data points of value and date and I see what I need what am I doing wrong thanks ?

There are a few possibilities:

  • your callback is running before the Bokeh content is initialized (i.e. Bokeh.index is not populated yet)

  • The ID you are using is not actually the correct one for some reason

As a first step to investigate either of these, I would suggest putting a debugger statement in your callback, or some console.log statements to interrogate Bokeh.index at that instant.

The ID is correct but your first point may be the problem the callback is running before the Bokeh content is initalized and somehow Bokeh.index is not populated with the most recent data but contains only old data for some reason.

Any idea how to fix the callback one so Bokeh.index is correctly populated ?

I was playing around using Object.keys() for some reason the key of the current graph can’t be found. It is displayed in the console log if you click on the triangle but it isn’t actually being detected by the code for some reason. So as your saying Bokeh.index isn’t being populated for some reason

I solved that problem with the Bokeh.index and how to make that callback work.

But I am confused about this statement you posted

> " you can update it by re-assigning to source.data and the plot will update (you have to make a real assignment, if you update the data arrays “in place” then BokehJS won’t be able to pick up the change automatically).

May you please give me an exampple of what you mean by re-assigning to source.data

In the python CustomJS side I could do this: 
callbacktest = CustomJS(args={ 'x_range': ODNplot.x_range},
						        code="""
				    x_range.start = cb_obj.value[0];
    				x_range.end = cb_obj.value[1];
			""")

what would be the equivalent of this say in client side ?

thanks

That statement referred to this:

source.data = new_dict  # Assignment to .data can be automatically detected

source.data['foo'] = new_list      # Changing .data in-place cannot be
source.data['foo'][10] = new_val   # Not this either

In the last two cases you have to give BokehJS an explicit nudge to know something has changed:

source.change.emit()

But these comments really only apply to complicated objects like CDS, and changing their sub-parts “in place”. For most simple properties like x_range.start, that are only simple scalar values to begin with, just setting them as you do above should be fine. That’s true whether the references to the Bokeh objects come from args and CustomJS, or whether you they came from looking through the views in Bokeh.index and finding the models you care about. (It’s the same literal JS objects in both cases.)

For anyone interested here is how one can control the slider from the front end and change the x axis range of the graph to display updated content using an Ajax call.

Here is the views.py code and the resulting javascript code. I removed some pieces that are sensitive. I am passing the plot as a json object as discussed above and I apologise for the bad names and variables I used and I hope people can still understand it.

If there is any way to improve this code to make it better please do post and let me know

Note that the slider i’m using is the ionrange sider and a moment.js library for datetime

https://momentjs.com/

Views.py

#use bokeh to create the layout
ODN_plot_l = layout([hourly_plot], sizing_mode='stretch_width')
#create json item of graphs not the div container where the plot will be embedded is 
#called myplot11
json_ODN_plot = json_item(ODN_plot_l,'myplot11' )
#extract the references information this is a huge dictionary
ans = json_ODN_plot['doc']['roots']['references']
#iterate over for loop to extract the start and end times from the columndatasource
#alternatively one can extract it from the XMaxRange and XMinRange but that didn't work.
for item in ans:
	#print (item)
	if (item['type']=='ColumnDataSource'):
		v = item
#save the information
s_start = v['attributes']['data']['date'][0]
s_end = v['attributes']['data']['date'][-1]
#create a json object which is passed by django the front end. N contains the plot itself xid is the root id which we don't need and start and end contain the plot start and end float times which will be something like 15023043ms 
jsplot_ajcall = {'N': json_ODN_plot, 'xid':json_ODN_plot['root_id'], 'start': s_start, 'end': s_end }

HTML CODE DESIGNED TO INITALIZE THE SLIDER we are using the ION RANGE SLIDER

<div id="sliderDiv">
<input type="text" class="js-range-slider2" name="my_range2" value=""/>
</div>

JAVASCRIPT CODE
This code is to initalize the jquery slider in the ajax call

$.ajax({
    url: "{%url 'modaldataaj' %}", //call the view function as a ajax call
    type: 'GET',
    datatype: 'json',
    data: {'horizon':$( "#horizon" ).val() },
    success: function(jsplot_ajcall) {
            //empty the div container first 
            $('#'+jsplot_ajcall.N.target_id).empty();
            var modalimg;
            modalimg = jsplot_ajcall.N;
            Bokeh.embed.embed_item(modalimg);
            
            if  ($( "#horizon" ).val() == 'Monthly') {
                var custom_valuesMonth = getTsArray(jsplot_ajcall.start, jsplot_ajcall.end, 'M');
                var to_Month = custom_valuesMonth.length;       
                var $range2 = $(".js-range-slider2");
                let range_instance2 = $range2.data("ionRangeSlider");
                range_instance2.update({
                    from: 0, //jsplot_ajcall.start, 
                    to: to_Month, //jsplot_ajcall.end,
                    values: custom_valuesMonth,
                    type: "double",
                    grid:true,
                    drag_interval:true,
                    prettify_enabled: true,
                });			
            }
    })

Now if someone wants to adjust the ionrange slider to make it pretty and human readable based on there documenation the following jquery code will need to be used which i am also doing

The function to creat ea new moment array. You need the moment.js library which works with datetimes

var getTsArray = function (start, end, duration) {
	var arr = new Array();
	var ts = start;
	var endTs = end;

	while (ts <= endTs) {
		ts = moment(ts).add(1, duration);
		arr.push(ts);
	}
	return arr;
}

This jquery function is the magic of the ionrange slider. When you adjust the sliders the data will change accordingly of the bokeh plot

$(document).ready(function(){
	var output;
	var lang = "en-US";
	
	function datexxx(num){
         //this function is to prettify the values and make it human readable
		if  ($( "#horizon" ).val() == 'Monthly' ){
			var d = new Date(num);
			output = d.toLocaleDateString(lang, {
                    year: 'numeric',
                    month: 'short'
                })
		}
		return output; 
	};
	
	
	//function to make adjust the slider and graph x axis
	$(".js-range-slider2").ionRangeSlider({
		prettify: datexxx, 	
		
		onChange: function (value) {
			console.log('ran on change');
			//console.log(Bokeh.index);
			//find the length of the bokeh index and take last value as this
			//is most recent data point value always
			varlen = Object.keys(Bokeh.index).length;
			varnum = Object.keys(Bokeh.index)[varlen-1];
			//console.log('start end from to', value.from, value.to, value.values, varlen, varnum, value.from_value, value.to_value );
			
			//initalize the bokeh source
			source = Bokeh.index[varnum].model; 
			//override the x range start and end variables using values from slider input
			Bokeh.index[varnum].model.children[0].x_range.start = value.from_value;
			Bokeh.index[varnum].model.children[0].x_range.end = value.to_value;
			
			source.change.emit();
		},
		
	
	});

The result should be something like this:

1 Like

Thanks for sharing @amitsandhel I’m hoping that in the not to distant future we can make it simpler to for users to package up things like this as custom extensions that can be easily shared!