Документация Engee

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!(; 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