raindrops = {
const { range, linspace, repeat } = Bokeh.LinAlg;
const { figure, show } = Bokeh.Plotting;
const { Label, Row, Column, CustomJS } = Bokeh;
const { Slider, Button } = Bokeh.Widgets;
const BACKEND = "canvas";
let x = [];
let y = [];
let size = [];
let maxSize = [];
let alpha = [];
let lastTick = null;
let fps = null;
const p = figure({frame_height: 500, frame_width: 500, x_range: [0, 1], y_range: [0, 1],
output_backend: BACKEND
});
const r = p.circle({ x, y, size, alpha, fill_color: null, color: "grey" });
const labelFPS = new Label({x: 500, y: 500, x_units: "screen", y_units: "screen", x_offset: -10,
y_offset: -10, background_fill_color: "lightgrey",
text_baseline: "top", text_align: "right"});
p.add_layout(labelFPS);
const sliderCount = new Slider({start: 1, end: 1000, value: 30, step: 1, title: "Count", width: 200});
const sliderSpeed = new Slider({start: 0.0, end: 6, value: 2, step: 0.001, title: "Speed", width: 200});
const sliderRadius = new Slider({start: 10, end: 500, value: 100, step: 1, title: "Radius", width: 200});
const buttonStartStop = new Button({label: "Stop", width: 95, button_type: "danger"});
const buttonReset = new Button({label: "Reset", width: 95, button_type: "default"});
buttonStartStop.js_event_callbacks["button_click"] = [
new CustomJS({
code: `
if (window.animationId === null || window.animationId === undefined) {
this.origin.button_type = "danger";
this.origin.label = "Stop";
window.animationId = requestAnimationFrame(window.animate);
} else {
this.origin.button_type = "success";
this.origin.label = "Start";
cancelAnimationFrame(window.animationId);
window.animationId = null;
}
`
})
];
buttonReset.js_event_callbacks["button_click"] = [
new CustomJS({args: { sliderCount, sliderSpeed, sliderRadius, p }, code: `
sliderCount.value = 30;
sliderSpeed.value = 2;
sliderRadius.value = 100;
p.reset.emit();`
})
];
const layout = new Row({
children: [
new Column({children: [sliderCount, sliderSpeed, sliderRadius,
new Row({ children: [buttonReset, buttonStartStop] })
]}), p]
});
const div = html`<div></div>`;
yield div;
const plotView = await show(layout, div);
const animate = (window.animate = (tick) => {
const n = sliderCount.value;
const currentN = x.length;
if (lastTick === null) {
lastTick = tick;
}
const dt = tick - lastTick;
lastTick = tick;
// update FPS label
if (dt > 0) {
if (fps === null) {
fps = 1000 / dt;
} else {
const smoothing = 0.9;
fps = fps * smoothing + (1000 / dt) * (1 - smoothing);
}
labelFPS.text = `${Math.round(fps)} FPS`;
}
// add/remove raindrops if count slider changed
if (n > currentN) {
const addCount = n - currentN;
for (let i = 0; i < addCount; i++) {
x.push(Math.random());
y.push(Math.random());
size.push(Math.random() * sliderRadius.value);
maxSize.push((Math.random() + 0.5) * sliderRadius.value);
alpha.push(1 - size[size.length - 1] / maxSize[maxSize.length - 1]);
}
} else if (n < currentN) {
x.splice(n);
y.splice(n);
size.splice(n);
maxSize.splice(n);
alpha.splice(n);
}
// update radius & alpha for each raindrop
for (let i = 0; i < sliderCount.value; i++) {
// increase raindrop radius
size[i] += sliderSpeed.value;
// if reached max size, then create a new raindrop
if (size[i] > maxSize[i]) {
size[i] = 0;
maxSize[i] = (Math.random() + 0.5) * sliderRadius.value;
x[i] = Math.random();
y[i] = Math.random();
alpha[i] = 1;
} else {
alpha[i] = 1 - size[i] / maxSize[i];
}
}
r.data_source.change.emit();
window.animationId = requestAnimationFrame(animate);
});
window.animationId = requestAnimationFrame(animate);
// Bokeh keeps references to all created views and documents in global variables
// Bokeh.documents (for documents) and Bokeh.index (for views), and after
// DOM element (on cell reevaluation) is destroyed we need to make sure
// those objects are disposed.
function cleanupBokehPlot(plotView) {
for (let i = 0; i < Bokeh.documents.length; i++) {
const doc = Bokeh.documents[i];
if (doc === plotView.model.document) {
doc.clear();
Bokeh.documents.splice(i, 1);
break;
}
}
}
invalidation.then(() => {
cancelAnimationFrame(window.animationId);
window.animationId = null;
cleanupBokehPlot(plotView);
});
}