Skip to content

Commit

Permalink
SelectionWidget now uses browser side caching
Browse files Browse the repository at this point in the history
This is a major refactor of the SelectionWidget javascript
code, which now caches all figures inside the DOM. This avoids
flicker for almost all figure types and allows caching even
live widget output in the browser.
  • Loading branch information
philippjfr committed Jun 4, 2015
1 parent 70081ac commit fc7dd2b
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 59 deletions.
133 changes: 84 additions & 49 deletions holoviews/ipython/jsslider.jinja
@@ -1,48 +1,98 @@
<script language="javascript">
/* Define the NDSlider class */
function NDSlider(frames, id, slider_ids, keyMap, dim_vals, notFound, load_json, mpld3, nbagg, cached){
function NDSlider(frames, id, slider_ids, keyMap, dim_vals, notFound, load_json, mpld3_enabled, nbagg, cached){
this.frames = frames;
this.fig_id = "fig_" + id;
this.img_id = "_anim_img" + id;
this.id = id;
this.slider_ids = slider_ids;
this.keyMap = keyMap
this.current_frame = 0;
this.previous_frame = 0;
this.current_vals = dim_vals;
this.load_json = load_json;
this.mpld3 = mpld3;
this.mpld3 = mpld3_enabled;
this.nbagg = nbagg;
this.notFound = notFound;
this.cached = cached;
this.update();
if(this.nbagg) {
this.cache = {};
if(this.cached) {
this.update_cache();
this.update(0, 0);
} else if(this.nbagg) {
this.update_cache();
this.update(0, 0);
this.set_frame(this.current_vals[0], 0);
} else {
this.dynamic_update(0, 0);
}
}
NDSlider.prototype.update = function(){
if(this.current_frame == undefined) {
$("#" + this.img_id).html(this.notFound);
} else if(this.load_json) {
var data_url = "{{ server }}/" + this.fig_id + "/" + this.current_frame;
if(this.mpld3) {
d3.select("#"+this.img_id).selectAll("*").remove();
$.getJSON(data_url, (function(img_id) {
return function(data) {
mpld3.draw_figure(img_id, data);
};
}(this.img_id)));
} else {
$("#" + this.img_id).load("{{ server }}/" + this.fig_id + "/" + this.current_frame);
}
NDSlider.prototype.update_cache = function(){
for (var i=0; i<_.size(this.frames); i++) {
i = Object.keys(this.frames)[i];
if(!(i in this.cache)) {
this.cache[i] = $('<div />').appendTo("#" + this.img_id);
if(this.mpld3) {
var cache_id = this.img_id+"_"+i;
this.cache[i].attr("id", cache_id);
}
if(this.load_json) {
var data_url = "{{ server }}/" + this.fig_id + "/" + i;
if(this.mpld3) {
$.getJSON(data_url, (function(cache_id) {
return function(data) {
mpld3.draw_figure(cache_id, data);
};
}(cache_id)));
} else {
this.cache[i].load(data_url);
}
} else {
if(this.mpld3) {
mpld3.draw_figure(cache_id, this.frames[i]);
} else {
this.cache[i].html(this.frames[i]);
}
}
this.cache[i].hide();
}
}
}
NDSlider.prototype.dynamic_update = function(prev, current){
var kernel = IPython.notebook.kernel;
if(this.nbagg) {
function callback(msg) { }
} else {
if(this.mpld3) {
d3.select("#" + this.img_id).selectAll("*").remove();
mpld3.draw_figure(this.img_id, this.frames[this.current_frame]);
} else {
$("#" + this.img_id).html(this.frames[this.current_frame]);
}
}
function callback(msg){
/* This callback receives data from Python as a string
in order to parse it correctly quotes are sliced off*/
var data = msg.content.data['text/plain'].slice(1, -1);
if(this.mpld3){
data = JSON.parse(data)[0];
}
this.frames[current] = data;
this.update_cache();
this.update(prev, current);
}
}
if(this.nbagg || !(current in this.cache)) {
callbacks = {iopub: {output: $.proxy(callback, this)}};
var cmd = "holoviews.ipython.widgets.SelectionWidget.widgets['" + this.id + "'].update(" + current + ")";
kernel.execute("import holoviews;" + cmd, callbacks, {silent : false});
} else {
this.update(prev, current);
}
}
NDSlider.prototype.update = function(prev, current){
if(prev in this.cache) {
this.cache[prev].hide();
}
if(current in this.cache) {
this.cache[current].show();
}
}
NDSlider.prototype.set_frame = function(dim_val, dim_idx){
Expand All @@ -62,30 +112,14 @@
else if(this.slider_ids.length == 1) { key += ',';}
}
key += ")";
var prev = this.current_frame;
var current = this.keyMap[key];
this.previous_frame = prev;
this.current_frame = current;
if(this.cached) {
this.current_frame = this.keyMap[key];
this.update()
this.update(prev, current)
} else {
var kernel = IPython.notebook.kernel;
if(this.nbagg) {
function callback(msg) { }
} else {
function callback(msg){
/* This callback receives data from Python as a string
in order to parse it correctly quotes are sliced off*/
var data = msg.content.data['text/plain'].slice(1, -1);
if(this.mpld3){
data = JSON.parse(data);
} else {
data = {0: data};
}
this.frames = data;
this.update()
}
}
callbacks = {iopub: {output: $.proxy(callback, this)}};
var cmd = "holoviews.ipython.widgets.SelectionWidget.widgets['" + this.id + "'].update(" + this.keyMap[key] + ")";
kernel.execute("import holoviews;" + cmd, callbacks, {silent : false});
this.dynamic_update(prev, current)
}
}
</script>
Expand All @@ -94,6 +128,7 @@
div.hololayout {
display: flex;
align-items: center;
margin: 0;
}
form.holoform {
Expand Down Expand Up @@ -204,7 +239,7 @@ div.hologroup {
var dim_val = valMap{{ id }}_{{ widget_data['dim'] }}[ui.value];
$('#textInput{{ id }}_{{ widget_data['dim'] }}').val(dim_val);
anim{{ id }}.set_frame(dim_val, {{ widget_data['dim_idx'] }});
}, 50)
}, 100)
});
$('#_anim_widget{{ id }}_{{ widget_data['dim'] }}').keypress(function(event) {
if (event.which == 80 || event.which == 112) {
Expand Down
16 changes: 6 additions & 10 deletions holoviews/ipython/widgets.py
Expand Up @@ -390,9 +390,6 @@ class SelectionWidget(NdWidget):
Whether to embed all plots in the Javascript, generating
a static widget not dependent on the IPython server.""")

cache_size = param.Integer(default=100, doc="""
Size of dynamic cache if frames are not embedded.""")

template = param.String('jsslider.jinja', doc="""
The jinja2 template used to generate the html output.""")

Expand All @@ -412,7 +409,10 @@ def __init__(self, plot, **params):
frames = {idx: self._plot_figure(idx)
for idx in range(len(self.keys))}
self.frames = self.encode_frames(frames)
elif self.nbagg:
else:
SelectionWidget.widgets[self.id] = self

if self.nbagg:
fig = self.plot[0]
self.manager = new_figure_manager_given_figure(OutputMagic.nbagg_counter, fig)
# Need to call mouse_init on each 3D axis to enable rotation support
Expand All @@ -421,7 +421,6 @@ def __init__(self, plot, **params):
ax.mouse_init()
OutputMagic.nbagg_counter += 1
self.comm = CustomCommSocket(self.manager)
SelectionWidget.widgets[self.id] = self


def get_widgets(self):
Expand Down Expand Up @@ -500,13 +499,10 @@ def update(self, n):
fig = self.plot[n]
fig.canvas.draw_idle()
return
if n not in self.frames:
if len(self.frames) >= self.cache_size:
self.frames.popitem(last=False)
else:
frame = self._plot_figure(n)
if self.mpld3: frame = self.encode_frames({0: frame})
self.frames[n] = frame
return self.frames[n]
return frame



Expand Down

0 comments on commit fc7dd2b

Please sign in to comment.