Interactions
Animations in Bonito are done via Observables.jl, much like it's the case for Makie.jl, so the same docs apply:
But lets quickly get started with a Bonito specific example:
App() do session
s = Slider(1:3)
value = map(s.value) do x
return x ^ 2
end
# Record states is an experimental feature to record all states generated in the Julia session and allow the slider to stay interactive in the statically hosted docs!
return Bonito.record_states(session, DOM.div(s, value))
end
The s.value
is an Observable
which can be mapp'ed
to take on new values, and one can insert observables as an input to DOM.tag
or as any attribute. The value of the observable
will be rendered via jssrender(session, observable[])
, and then updated whenever the value changes. So anything that supports being inserted into the DOM
can be inside an observable, and the fallback is to use the display system (so plots etc. work as well). This way, one can also return DOM
elements as the result of an observable:
App() do session
s = Slider(1:3)
# use map!(result_observable, ...)
# To use any as the result type, otherwise you can't return
# different types from the map callback
value = map!(Observable{Any}(), s.value) do x
if x == 1
return DOM.h1("hello from slider: $(x)")
elseif x == 2
return DOM.img(src="https://docs.makie.org/stable/logo.svg", width="200px")
else
return x^2
end
end
return Bonito.record_states(session, DOM.div(s, value))
end
hello from slider: 1
In other words, the whole app can just be one big observable:
import Bonito.TailwindDashboard as D
App() do session
s = D.Slider("Slider: ", 1:3)
checkbox = D.Checkbox("Chose:", true)
menu = D.Dropdown("Menu: ", [sin, tan, cos])
app = map(checkbox.widget.value, s.widget.value, menu.widget.value) do checkboxval, sliderval, menuval
DOM.div(checkboxval, sliderval, menuval)
end
return Bonito.record_states(session, D.FlexRow(
D.Card(D.FlexCol(checkbox, s, menu)),
D.Card(app)
))
end
Chose:
Slider:
1
Menu:
sin (generic function with 14 methods)
Likes this one create interactive examples like this:
import Bonito.TailwindDashboard as D
function create_svg(sl_nsamples, sl_sample_step, sl_phase, sl_radii, color)
width, height = 900, 300
cxs_unscaled = [i*sl_sample_step + sl_phase for i in 1:sl_nsamples]
cys = sin.(cxs_unscaled) .* height/3 .+ height/2
cxs = cxs_unscaled .* width/4pi
rr = sl_radii
# DOM.div/svg/etc is just a convenience in Bonito for using Hyperscript, but circle isn't wrapped like that yet
geom = [SVG.circle(cx=cxs[i], cy=cys[i], r=rr, fill=color(i)) for i in 1:sl_nsamples[]]
return SVG.svg(SVG.g(geom...);
width=width, height=height
)
end
app = App() do session
colors = ["black", "gray", "silver", "maroon", "red", "olive", "yellow", "green", "lime", "teal", "aqua", "navy", "blue", "purple", "fuchsia"]
color(i) = colors[i%length(colors)+1]
sl_nsamples = D.Slider("nsamples", 1:200, value=100)
sl_sample_step = D.Slider("sample step", 0.01:0.01:1.0, value=0.1)
sl_phase = D.Slider("phase", 0.0:0.1:6.0, value=0.0)
sl_radii = D.Slider("radii", 0.1:0.1:60, value=10.0)
svg = map(create_svg, sl_nsamples.value, sl_sample_step.value, sl_phase.value, sl_radii.value, color)
return DOM.div(D.FlexRow(D.FlexCol(sl_nsamples, sl_sample_step, sl_phase, sl_radii), svg))
end
nsamples
100
sample step
0.1
phase
0.0
radii
10.0
As you notice, when exporting this example to the docs which get statically hosted, all interactions requiring Julia cease to exist. One way to create interactive examples that stay active is to move the parts that need Julia to Javascript:
app = App() do session
colors = ["black", "gray", "silver", "maroon", "red", "olive", "yellow", "green", "lime", "teal", "aqua", "navy", "blue", "purple", "fuchsia"]
nsamples = D.Slider("nsamples", 1:200, value=100)
nsamples.widget[] = 100
sample_step = D.Slider("sample step", 0.01:0.01:1.0, value=0.1)
sample_step.widget[] = 0.1
phase = D.Slider("phase", 0.0:0.1:6.0, value=0.0)
radii = D.Slider("radii", 0.1:0.1:60, value=10.0)
radii.widget[] = 10
svg = DOM.div()
evaljs(session, js"""
const [width, height] = [900, 300]
const colors = $(colors)
const observables = $([nsamples.value, sample_step.value, phase.value, radii.value])
function update_svg(args) {
const [nsamples, sample_step, phase, radii] = args;
const svg = (tag, attr) => {
const el = document.createElementNS('http://www.w3.org/2000/svg', tag);
for (const key in attr) {
el.setAttributeNS(null, key, attr[key]);
}
return el
}
const color = (i) => colors[i % colors.length]
const svg_node = svg('svg', {width: width, height: height});
for (let i=0; i<nsamples; i++) {
const cxs_unscaled = (i + 1) * sample_step + phase;
const cys = Math.sin(cxs_unscaled) * (height / 3.0) + (height / 2.0);
const cxs = cxs_unscaled * width / (4 * Math.PI);
const circle = svg('circle', {cx: cxs, cy: cys, r: radii, fill: color(i)});
svg_node.appendChild(circle);
}
$(svg).replaceChildren(svg_node);
}
Bonito.onany(observables, update_svg)
update_svg(observables.map(x=> x.value))
""")
return DOM.div(D.FlexRow(D.FlexCol(nsamples, sample_step, phase, radii), svg))
end
nsamples
100
sample step
0.1
phase
0.0
radii
10.0
This works, because the Javascript side of Bonito, will still update the observables in Javascript (which are mirrored from Julia), and therefore keep working without a running Julia process. You can use js_observable.on(value=> ....)
and Bonito.onany(array_of_js_observables, values=> ...)
to create interactions, pretty similar to how you would work with Observables in Julia.