WGLMakie
|
Страница в процессе перевода. |
WGLMakie — это веб-бэкенд, который в настоящее время в основном реализован на Julia. WGLMakie использует Bonito с целью генерирования кода HTML и JavaScript для отображения графиков. Что касается JavaScript, то для отрисовки графиков используются ThreeJS и WebGL. В настоящее время целью является перенос большей части реализации на JavaScript, что улучшит API для JavaScript и расширит возможности взаимодействия без запущенного сервера Julia.
|
Warning WGLMakie можно считать экспериментальным бэкендом, так как API для JavaScript пока нестабилен и интеграция с записными книжками далека от идеала, но все типы графиков, а значит, и все шаблоны, должны работать, хотя и с оговорками. |
Поддержка браузеров
IJulia
-
Bonito теперь использует соединение IJulia и поэтому может применяться даже со сложной конфигурацией прокси-сервера без дополнительной настройки.
-
Перезагрузка страницы не поддерживается. При перезагрузке необходимо повторно выполнить все ячейки, причем вызов
Page()должен выполняться первым.
JupyterHub, Jupyterlab, Binder
-
WGLMakie в основном должен работать с подключением через WebSocket. Bonito пытается определить конфигурацию прокси-сервера, необходимую для подключения к процессу Julia. В локальных экземплярах Jupyterlab это должно происходить без проблем. В размещенных экземплярах, скорее всего, потребуется установить
jupyter-server-proxy, а затем выполнить вызов наподобиеPage(; listen_port=9091, proxy_url="<jhub-instance>.com/user/<username>/proxy/9091"). См. также:
Pluto
-
По-прежнему используется подключение Bonito через WebSocket, поэтому для удаленных серверов требуется дополнительная настройка.
-
Перезагрузка страницы не поддерживается. При перезагрузке необходимо повторно выполнить все ячейки, причем вызов
Page()должен выполняться первым. -
Экспорт статичного кода HTML пока работает не полностью.
JuliaHub
-
VSCode в браузере должен работать сразу.
-
У Pluto в JuliaHub по-прежнему есть проблема с подключением через WebSocket. График будет отображаться, но взаимодействие будет невозможно.
Поддержка браузеров
Некоторые браузеры могут поддерживать только WebGL 1.0 или требовать дополнительных действий для включения WebGL, но в целом все современные браузеры на мобильных устройствах и компьютерах должны поддерживать WebGL 2.0. Однако пользователям Safari может потребоваться включить WebGL. Если вам приходится использовать WebGL 1.0, в первую очередь у вас не будет функции volume и contour(volume).
Активация и настройка экрана
Чтобы активировать бэкенд, вызовите WGLMakie.activate!() со следующими параметрами.
#
WGLMakie.activate! — Function
WGLMakie.activate!(; screen_config...)
Устанавливает WGLMakie в качестве текущего активного бэкенда, а также позволяет быстро задать screen_config. Обратите внимание, что screen_config также можно задать на постоянной основе с помощью Makie.set_theme!(WGLMakie=(screen_config...,)).
Аргументы, которые можно передавать через screen_config:
-
framerate = 30: задает более высокую частоту кадров (в секунду) для более плавной анимации или менее высокую частоту для экономии ресурсов. -
resize_to = nothing: приresize_to=:parentразмер холста изменяется в соответствии с родительским элементом, а приresize_to = :body— в соответствии с основной областью. При значении по умолчаниюnothingразмер не изменяется. Допускается также кортеж с теми же значениями ширины и высоты.
Вывод
Вы можете использовать Bonito и WGLMakie в Pluto, IJulia, Webpages и Documenter для создания интерактивных приложений и информационных панелей, размещения их на динамических веб-страницах или экспорта в статические файлы HTML.
В этом руководстве рассматриваются различные режимы и возможные ограничения.
Страница
С помощью Page() можно сбросить состояние Bonito, что необходимо для многостраничного вывода, как в случае с Documenter или различными записными книжками (IJulia, Pluto и т. д.). Раньше в записных книжках всегда приходилось вставлять и отображать вызов Page, но теперь вызов Page() необязателен и не требует отображения. Он просто сбрасывает состояние для нового многостраничного вывода, что обычно происходит в случае с Documenter, когда создается несколько страниц в ходе одного сеанса Julia. Этот вызов также можно использовать для сброса состояния в записных книжках, например после перезагрузки страницы. Page(exportable=true, offline=true) можно использовать для принудительного встраивания всех зависимостей данных и JS, чтобы все необходимое можно было загрузить в одном объекте HTML без запуска процесса Julia. Соответствующие настройки по умолчанию уже должны быть выбраны, например для Documenter, поэтому этот вызов следует в основном использовать, например, для автономного экспорта Pluto (который в настоящее время поддерживается не полностью, но скоро должен быть реализован).
Вот пример использования во Franklin:
using WGLMakie
using Bonito, Markdown
Page() # для Franklin все равно требуется настройка
WGLMakie.activate!()
Makie.inline!(true) # Обязательно встраивайте графики в выходные данные Documenter.
scatter(1:4, color=1:4)
Как видите, выходные данные полностью статичные, так как нет работающего сервера Julia, как в случае, например, с Pluto. Чтобы сделать график интерактивным, необходимо реализовать большую часть функционала WGLMakie на JS, и работа над этим ведется. Как видите, интерактивность уже работает в трехмерных случаях.
N = 60
function xy_data(x, y)
r = sqrt(x^2 + y^2)
r == 0.0 ? 1f0 : (sin(r)/r)
end
l = range(-10, stop = 10, length = N)
z = Float32[xy_data(x, y) for x in l, y in l]
surface(
-1..1, -1..1, z,
colormap = :Spectral
)
Есть несколько способов продолжить взаимодействие с графиками при статическом экспорте.
Запись карты состояний
Bonito позволяет записывать карту состояний для всех виджетов, соответствующих следующему интерфейсу.
# должно быть true для нахождения внутри DOM
is_widget(x) = true
# Обновление виджета не зависит от какого-либо другого состояния (единственное, что поддерживается на данный момент)
is_independant(x) = true
# Значения, которые виджет может перебирать
function value_range end
# обновление виджета определенным значением (обычно наблюдаемым объектом)
function update_value!(x, value) end
В настоящее время только ползунки перегружают интерфейс.
using Observables
App() do session::Session
n = 10
index_slider = Slider(1:n)
volume = rand(n, n, n)
slice = map(index_slider) do idx
return volume[:, :, idx]
end
fig = Figure()
ax, cplot = contour(fig[1, 1], volume)
rectplot = linesegments!(ax, Rect(-1, -1, 12, 12), linewidth=2, color=:red)
on(index_slider) do idx
translate!(rectplot, 0,0,idx)
end
heatmap(fig[1, 2], slice)
slider = DOM.div("z-index: ", index_slider, index_slider.value)
return Bonito.record_states(session, DOM.div(slider, fig))
end
Выполнение кода JavaScript напрямую
Bonito позволяет легко создавать целые приложения на HTML и JS. Например, можно напрямую зарегистрировать функцию JavaScript, которая будет запускаться при изменении.
using Bonito
App() do session::Session
s1 = Slider(1:100)
slider_val = DOM.p(s1[]) # инициализируем текущим значением
# вызываем функцию `on_update` при каждом изменении s1.value в JS:
onjs(session, s1.value, js"""function on_update(new_value) {
//interpolating of DOM nodes and other Julia values work mostly as expected:
const p_element = $(slider_val)
p_element.innerText = new_value
}
""")
return DOM.div("slider 1: ", s1, slider_val)
end
Кроме того, можно интерполировать графики в JS и обновлять их через JS. Проблема в том, что пока нет удобного интерфейса. Возвращаемый объект представляет собой непосредственно объект THREE, и все атрибуты графика конвертированы в типы JavaScript. Хорошая новость заключается в том, что все атрибуты должны быть либо в three_scene.material.uniforms, либо в three_scene.geometry.attributes. В дальнейшем мы должны будем создать API в WGLMakie, который позволит делать это так же просто, как в Julia: plot.attribute = value. Пока же регистрация возвращаемого объекта позволяет довольно легко понять, что делать. Кстати, консоль JS в сочетании с регистрацией — это очень эффективный инструмент, с помощью которого очень легко экспериментировать с объектом после регистрации.
using Bonito: on_document_load
using WGLMakie
App() do session::Session
s1 = Slider(1:100)
slider_val = DOM.p(s1[]) # инициализируем текущим значением
fig, ax, splot = scatter(1:4)
# С помощью on_document_load можно запускать код JS после завершения загрузки.
# Это альтернатива функции `evaljs`, которую здесь нельзя использовать,
# так как она запускается сразу же, а значит, не сможет найти графики.
on_document_load(session, js"""
// you get a promise for an array of plots, when interpolating into JS:
$(splot).then(plots=>{
// just one plot for atomics like scatter, but for recipes it can be multiple plots
const scatter_plot = plots[0]
// open the console with ctr+shift+i, to inspect the values
// tip - you can right click on the log and store the actual variable as a global, and directly interact with it to change the plot.
console.log(scatter_plot)
console.log(scatter_plot.material.uniforms)
console.log(scatter_plot.geometry.attributes)
})
""")
# с учетом вышесказанного мы можем узнать, что позиции хранятся в `offset`
# (увы, связано это с тем, что атрибуты `position` в threejs — это особые случаи, поэтому их нельзя использовать)
# Теперь давайте изменим их с помощью ползунка :)
onjs(session, s1.value, js"""function on_update(new_value) {
$(splot).then(plots=>{
const scatter_plot = plots[0]
// change first point x + y value
scatter_plot.geometry.attributes.pos.array[0] = (new_value/100) * 4
scatter_plot.geometry.attributes.pos.array[1] = (new_value/100) * 4
// this always needs to be set of geometry attributes after an update
scatter_plot.geometry.attributes.pos.needsUpdate = true
})
}
""")
# и для полученных данных добавим ползунок для изменения цвета:
color_slider = Slider(LinRange(0, 1, 100))
onjs(session, color_slider.value, js"""function on_update(hue) {
$(splot).then(plots=>{
const scatter_plot = plots[0]
const color = new THREE.Color()
color.setHSL(hue, 1.0, 0.5)
scatter_plot.material.uniforms.color.value.x = color.r
scatter_plot.material.uniforms.color.value.y = color.g
scatter_plot.material.uniforms.color.value.z = color.b
})
}""")
markersize = Slider(1:100)
onjs(session, markersize.value, js"""function on_update(size) {
$(splot).then(plots=>{
const scatter_plot = plots[0]
scatter_plot.material.uniforms.markersize.value.x = size
scatter_plot.material.uniforms.markersize.value.y = size
})
}""")
return DOM.div(s1, color_slider, markersize, fig)
end
Так в настоящее время обстоят дела с интерактивностью в WGLMakie внутри статичных страниц.
Автономная подсказка
Makie.DataInspector отлично работает с WGLMakie, но для отображения и обновления подсказки требуется запущенный процесс Julia.
Существует также способ отобразить подсказку непосредственно в JavaScript. Он требует вставки в DOM HTML. Это означает, что нам нужно использовать Bonito.App, чтобы вернуть объект DOM:
App() do session
f, ax, pl = scatter(1:4, markersize=100, color=Float32[0.3, 0.4, 0.5, 0.6])
custom_info = ["a", "b", "c", "d"]
on_click_callback = js"""(plot, index) => {
// the plot object is currently just the raw THREEJS mesh
console.log(plot)
// Which can be used to extract e.g. position or color:
const {pos, color} = plot.geometry.attributes
console.log(pos)
console.log(color)
const x = pos.array[index*2] // everything is a flat array in JS
const y = pos.array[index*2+1]
const c = Math.round(color.array[index] * 10) / 10 // rounding to a digit in JS
const custom = $(custom_info)[index]
// return either a string, or an HTMLNode:
return "Point: <" + x + ", " + y + ">, value: " + c + " custom: " + custom
}
"""
# ToolTip(figurelike, js_callback; plots=plots_you_want_to_hover)
tooltip = WGLMakie.ToolTip(f, on_click_callback; plots=pl)
return DOM.div(f, tooltip)
end
Pluto и IJulia
Обратите внимание, что обычные интерактивные возможности Makie сохраняются при использовании WGLMakie, например, в Pluto, пока запущен сеанс Julia. Что подводит нас к настройке сеансов Pluto и IJulia. В локальной среде WGLMakie должен изначально работать с Pluto и IJulia, но при доступе к ноутбуку с другого ПК необходимо сделать нечто подобное:
begin
using Bonito
some_forwarded_port = 8080
Page(listen_url="0.0.0.0", listen_port=some_forwarded_port)
end
Либо укажите URL-адрес прокси-сервера, если у него более сложная конфигурация. Более сложные настройки см. в документации по ?Page и описании Bonito.configure_server!.
Стили
Bonito позволяет загружать произвольные таблицы CSS, а DOM.xxx инкапсулирует все имеющиеся HTML-теги. Таким образом, можно использовать любой CSS-файл, даже такие библиотеки, как Tailwind с Asset.
TailwindCSS = Bonito.Asset("/path/to/tailwind.min.css")
Bonito также предоставляет тип Styles, который позволяет определять целые таблицы стилей и присваивать их любому объекту DOM. Компоненты таблицы стилей создаются в Bonito следующим образом.
Rows(args...) = DOM.div(args..., style=Styles(
"display" => "grid",
"grid-template-rows" => "fr",
"grid-template-columns" => "repeat($(length(args)), fr)",
))
Этот объект стиля вставляется в DOM только один раз за сеанс, и при последующем использовании div получает тот же класс.
Обратите внимание, что в Bonito уже определено нечто наподобие приведенного выше объекта Rows.
using Colors
using Bonito
App() do session::Session
hue_slider = Slider(0:360)
color_swatch = DOM.div(class="h-6 w-6 p-2 m-2 rounded shadow")
onjs(session, hue_slider.value, js"""function (hue){
$(color_swatch).style.backgroundColor = "hsl(" + hue + ",60%,50%)"
}""")
return Row(hue_slider, color_swatch)
end
Bonito также предоставляет компонент карточки (Card) с настраиваемым стилем:
using Markdown
App() do session::Session
# Теперь его можно использовать где угодно.
fig = Figure(size=(300, 300))
contour(fig[1,1], rand(4,4))
card = Card(Grid(
Centered(DOM.h1("Hello"); style=Styles("grid-column" => "1 / 3")),
StylableSlider(1:100; style=Styles("grid-column" => "1 / 3")),
DOM.img(src="https://julialang.org/assets/infra/logo.svg"),
fig; columns="1fr 1fr", justify_items="stretch"
))
# Markdown также создает DOM, и туда можно вставлять
# произвольные элементы, поддерживающие jsrender.
return DOM.div(card)
end
Будем надеяться, что со временем появятся вспомогательные библиотеки с множеством элементов с настройкой стиля, подобных приведенным выше, для создания эффектных информационных панелей с помощью Bonito и WGLMakie.
Экспорт
Documenter просто отрисовывает графики и страницу как HTML, поэтому, если нужно встроить объекты WGLMakie/Bonito в собственную страницу, достаточно использовать код наподобие следующего.
using WGLMakie, Bonito, FileIO
WGLMakie.activate!()
open("index.html", "w") do io
println(io, """
<html>
<head>
</head>
<body>
""")
Page(exportable=true, offline=true)
# После этого можно просто встраивать графики или все, что угодно :)
# Конечно, было бы разумнее поместить это в отдельное приложение
app = App() do
C(x;kw...) = Card(x; height="fit-content", width="fit-content", kw...)
figure = (; size=(300, 300))
f1 = scatter(1:4; figure)
f2 = mesh(load(assetpath("brain.stl")); figure)
C(DOM.div(
Bonito.StylableSlider(1:100),
Row(C(f1), C(f2))
); padding="30px", margin="15px")
end
show(io, MIME"text/html"(), app)
# Bonito или какой-либо иной объект, который можно отобразить как HTML:
println(io, """
</body>
</html>
""")
end