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

SpecApi

Страница в процессе перевода.

Warning SpecApi все еще находится в стадии активной разработки, и в него могут часто вноситься критические изменения. Кроме того, анимация с ним выполняется медленнее, чем при использовании обычного API Makie, так как необходимо часто повторно создавать графики и проходить по всему дереву графиков для нахождения различных значений. Хотя производительность всегда будет ниже, чем при прямом использовании наблюдаемых объектов для обновления атрибутов, этот API все еще недостаточно оптимизирован, поэтому мы рассчитываем повысить производительность в будущем. Также следует ожидать ошибок, поскольку API совсем новый, однако он предлагает множество новых сложных функциональных возможностей. Не стесняйтесь создавать проблемы, если вы столкнулись с неожиданным поведением. Запросы на вытягивание также приветствуются: код на самом деле не такой сложный, и в нем должно быть легко разобраться (src/basic_recipes/specapi.jl).

Что такое SpecApi

Начиная с версии 0.20, Makie поддерживает создание графиков и рисунков с помощью объектов спецификаций (spec). Эти объекты являются декларативными версиями таких знакомых объектов, как Axis, Colorbar, Scatter или Heatmap. Декларативность означает, что эти объекты не реализуют никаких сложных внутренних механизмов, необходимых для интерактивного построения графиков с отслеживанием состояния. Это просто описания, которые Makie автоматически конвертирует в полноценные объекты.

Полезность

Объекты спецификаций компактны и легко объединяются в более крупные структуры без необходимости их пошагового построения. Это позволяет соединять подрисунки, возвращаемые различными функциями, вместо построения в одном существующем родительском объекте Figure с помощью изменяющих функций. Вы также можете строить наблюдаемые объекты спецификаций в заданном объекте Figure, а когда вы меняете описание, весь подрисунок автоматически обновляется или перестраивается с максимальным сохранением существующих структур, где это возможно, благодаря использованию сравнения. Это может значительно упростить создание информационных панелей или небольших интерактивных приложений, так как пользователю приходится отслеживать меньшее количество состояний. С другой стороны, сравнение приводит к снижению производительности и в общем случае замедляет выполнение по сравнению с использованием изменяющего API Makie напрямую.

Создавать объекты спецификаций можно с помощью объекта Makie.SpecApi. Есть два основных типа спецификаций, PlotSpec и BlockSpec, которые соответствуют объектам графиков, таким как Scatter или Heatmap, и объектам Block, таким как Axis или Colorbar.

Предполагается, что API должен быть похож на обычный API, только декларативный. Сложные спецификации создаются путем вложения друг в друга более простых. При создании объектов спецификаций принято всегда использовать префикс S, чтобы их нельзя было спутать с аналогичными объектами из стандартного API.

import Makie.SpecApi as S

scatterspec = S.Scatter(1:4) # PlotSpec, описывающий график рассеяния
axspec = S.Axis(plots=[scatterspec]) # BlockSpec, описывающий ось с графиком рассеяния
layout_spec = S.GridLayout(axspec) # Макет, описывающий рисунок с осью с графиком рассеяния

# Теперь мы можем воплотить спецификацию в полноценный рисунок.
# Обратите внимание, что в настоящее время типом вывода из `plot` является
# тип FigureAxisPlot, что немного сбивает с толку, поскольку `pl` не является обычным графиком,
# а на рисунке может быть ноль или несколько осей.
# Это будет изменено в будущих версиях.
f, _, pl = plot(layout_spec)

# Обновляя входной наблюдаемый объект `pl`, то есть объект графика, мы можем
# изменить все содержимое рисунка на новое. В этом случае
# мы просто меняем тип графика на оси со Scatter на Lines, а
# название оси — на «Lines».
pl[1] = S.GridLayout(S.Axis(; title="Lines", plots=[S.Lines(1:4)]))

С помощью plot можно строить не только спецификации, описывающие весь макет, но и спецификации, описывающие блоки (Block) или отдельные графики.

s = Makie.PlotSpec(:Scatter, 1:4; color=:red)
axis = Makie.BlockSpec(:Axis; title="Axis at layout position (1, 1)")

Построение макетов по спецификациям

Для быстрого построения макетов можно передавать в S.GridLayout векторы столбцов, векторы строк или матрицы спецификаций блоков. Если требуется больше контроля над макетом, можно напрямую указывать размеры строк и столбцов, а также промежутки.

using GLMakie
using DelimitedFiles
using Makie.FileIO
import Makie.SpecApi as S
using Random

Random.seed!(123)

volcano = readdlm(Makie.assetpath("volcano.csv"), ',', Float64)
brain = load(assetpath("brain.stl"))
r = LinRange(-1, 1, 100)
cube = [(x .^ 2 + y .^ 2 + z .^ 2) for x = r, y = r, z = r]

density_plots = map(x -> S.Density(x * randn(200) .+ 3x, color=:y), 1:5)
brain_mesh = S.Mesh(brain, colormap=:Spectral, color=[tri[1][2] for tri in brain for i in 1:3])
volcano_contour = S.Contourf(volcano; colormap=:inferno)
cube_contour = S.Contour(cube, alpha=0.5)

ax_densities = S.Axis(; plots=density_plots, yautolimitmargin = (0, 0.1))
ax_volcano = S.Axis(; plots=[volcano_contour])
ax_brain = S.Axis3(; plots=[brain_mesh], protrusions = (50, 20, 10, 0))
ax_cube = S.Axis3(; plots=[cube_contour], protrusions = (50, 20, 10, 0))

spec_column_vector = S.GridLayout([ax_densities, ax_volcano, ax_brain]);
spec_matrix = S.GridLayout([ax_densities ax_volcano; ax_brain ax_cube]);
spec_row = S.GridLayout([spec_column_vector spec_matrix], colsizes = [Auto(), Auto(4)])

f, ax, pl = plot(S.GridLayout(spec_row); figure = (; fontsize = 10))
e163500

Расширенное построение макетов по спецификациям

Если требуется еще больше контроля, можно напрямую передавать позицию каждого объекта в макете в S.GridLayout. Эти позиции задаются в виде кортежа (rows, columns [, side]), где side по умолчанию равно Inside(). Для rows и columns можно использовать целые числа, например 2, диапазоны, например 1:3, или оператор-двоеточие :, который охватывает все строки или столбцы, указанные для других элементов. По умолчанию строки и столбцы начинаются с 1, но при необходимости можно использовать числа меньше 1.

using CairoMakie
import Makie.SpecApi as S

plot(
    S.GridLayout([
        (1, 1) => S.Axis(),
        (1, 2) => S.Axis(),
        (2, :) => S.Axis(),
        (2, 2, Right()) => S.Box(),
        (2, 2, Right()) => S.Label(
            text = "Label",
            rotation = pi/2,
            padding = (10, 10, 10, 10)
        ),
    ])
)
285755c

Кроме того, заданные вручную позиции можно использовать с вложенными GridLayout.

using CairoMakie
import Makie.SpecApi as S

plot(S.GridLayout([
    (1, 1) => S.Axis(),
    (1, 2) => S.Axis(),
    (2, :) => S.GridLayout(fill(S.Axis(), 1, 3)),
]))
d7eb03c

Ниже приведены все именованные аргументы, которые принимает S.GridLayout.

S.GridLayout([...],
    colsizes = [Auto(), Auto(), 300],
    rowsizes = [Relative(0.4), Relative(0.6)],
    colgaps,
    rowgaps,
    alignmode,
    halign,
    valign,
    tellheight,
    tellwidth,
)

Ссылки на оси также поддерживаются, но они являются частью не оси, а окружающего макета GridLayout, так как при построении оси обычно еще нет других осей, с которыми ее нужно связать.

using CairoMakie
import Makie.SpecApi as S
axis_matrix = broadcast(1:2, (1:2)') do x, y
    S.Axis(; title="$x, $y")
end
layout = S.GridLayout(
    axis_matrix;
    xaxislinks=vec(axis_matrix[1:2, 1]),
    yaxislinks=vec(axis_matrix[1:2, 2])
)
f, _, pl = plot(layout)
# Изменим пределы, чтобы увидеть ссылки в действии
for ax in f.content[[1, 3]]
    limits!(ax, 2, 3, 2, 3)
end
f
464e506

Использование спецификаций в convert_arguments

Warning Пока не решено, как будет удобнее и быстрее перенаправлять именованные аргументы из plots(...; kw...) в convert_arguments для SpecApi. Пока следует помечать атрибуты, которые вы хотите использовать в convert_arguments, с помощью Makie.used_attributes, однако при изменении любого атрибута вся спецификация будет полностью перерисовываться. В будущих версиях также может потребоваться перегрузить другую функцию.

Вы можете перегрузить convert_arguments и получить массив PlotSpecs или GridLayoutSpec. Основное различие в том, что возвращаемый массив PlotSpecs может быть построен по осям как любой шаблон, в то время как перегрузки, возвращающие GridLayoutSpec, не допускают этого.

convert_arguments для GridLayoutSpec

В этом примере мы перегружаем convert_arguments для пользовательского типа, чтобы можно было легко создавать фасетные сетки.

using CairoMakie
import Makie.SpecApi as S

# Пользовательский тип, для которого мы хотим написать метод конвертации
struct PlotGrid
    nplots::Tuple{Int,Int}
end

# Если мы хотим использовать атрибут `color` в конвертации, его нужно
# пометить с помощью `used_attributes`
Makie.used_attributes(::PlotGrid) = (:color,)

# Метод конвертации создает сетку объектов `Axis` с графиком `Lines` внутри
# Мы ограничиваем возможные варианты только типом Plot{plot}, чтобы работал только вызов `plot(PlotGrid(...))`, но не, например, `scatter(PlotGrid(...))`.
function Makie.convert_arguments(::Type{Plot{plot}}, obj::PlotGrid; color=:black)
    axes = [
        S.Axis(plots=[S.Lines(cumsum(randn(1000)); color=color)])
            for i in 1:obj.nplots[1],
                j in 1:obj.nplots[2]
    ]
    return S.GridLayout(axes)
end

# Теперь при построении `PlotGrid` мы получаем полный фасетный макет
plot(PlotGrid((3, 4)))
5caf50d

Также можно выполнять построение в существующих объектах Figure с помощью нового метода plot:

f = Figure()
plot(f[1, 1], PlotGrid((2, 2)); color=Cycled(1))
plot(f[1, 2], PlotGrid((3, 2)); color=Cycled(2))
f
77fbd07

convert_arguments для PlotSpec

Мы можем вернуть вектор объектов PlotSpec из convert_arguments, что позволяет динамически выбирать объекты графиков, которые нужно добавить, учитывая входные данные. Хотя с помощью старого API шаблонов можно было выбирать типы графиков на основе входных данных, это было не так просто в случае с обновлениями наблюдаемых объектов, которые изменяли эти типы графиков в существующем рисунке. Пользователям приходилось выполнять утомительную ручную работу по учету, которая теперь абстрагирована.

Обратите внимание, что в настоящее время этот метод не позволяет перенаправлять именованные аргументы из команды plot в convert_arguments, поэтому в следующем примере мы помещаем аргументы графика в объект LineScatter:

using CairoMakie
import Makie.SpecApi as S
using Random

Random.seed!(123)

# определяем структуру для `convert_arguments`
struct CustomMatrix
    data::Matrix{Float32}
    style::Symbol
    kw::Dict{Symbol,Any}
end
CustomMatrix(data; style, kw...) = CustomMatrix(data, style, Dict{Symbol,Any}(kw))

function Makie.convert_arguments(::Type{<:AbstractPlot}, obj::CustomMatrix)
    plots = PlotSpec[]
    if obj.style === :heatmap
        push!(plots, S.Heatmap(obj.data; obj.kw...))
    elseif obj.style === :contourf
        push!(plots, S.Contourf(obj.data; obj.kw...))
    end
    max_position = Tuple(argmax(obj.data))
    push!(plots, S.Scatter(max_position; markersize = 30, strokecolor = :white, color = :transparent, strokewidth = 4))
    return plots
end

data = randn(30, 30)

f = Figure()
ax = Axis(f[1, 1])
# Можно либо построить график на существующей оси,
plot!(ax, CustomMatrix(data, style = :heatmap, colormap = :Blues))
# либо создать новую автоматически, как мы привыкли делать в стандартном API
plot(f[1, 2], CustomMatrix(data, style = :contourf, colormap = :inferno))
f
298c033

Интерактивный пример

SpecApi ориентирован на создание информационных панелей и интерактивное построение сложных графиков. Вот пример использования Slider и Menu для визуализации фиктивной симуляции.

using GLMakie
using Random
import Makie.SpecApi as S

struct MySimulation
    plottype::Symbol
    arguments::AbstractVector
end

function Makie.convert_arguments(::Type{<:AbstractPlot}, sim::MySimulation)
    return map(enumerate(sim.arguments)) do (i, data)
        return PlotSpec(sim.plottype, data)
    end
end
f = Figure()
s = Slider(f[1, 1], range=1:10)
m = Menu(f[1, 2], options=[:Scatter, :Lines, :BarPlot])
sim = lift(s.value, m.selection) do n_plots, p
    Random.seed!(123)
    args = [cumsum(randn(100)) for i in 1:n_plots]
    return MySimulation(p, args)
end
ax, pl = plot(f[2, :], sim)
tight_ticklabel_spacing!(ax)
# более низкий приоритет для гарантированного выполнения обратного вызова в последнюю очередь
on(sim; priority=-1) do x
    autolimits!(ax)
end

record(f, "interactive_specapi.mp4", framerate=1) do io
    pause = 0.1
    m.i_selected[] = 1
    for i in 1:4
        set_close_to!(s, i)
        sleep(pause)
        recordframe!(io)
    end
    m.i_selected[] = 2
    sleep(pause)
    recordframe!(io)
    for i in 5:7
        set_close_to!(s, i)
        sleep(pause)
        recordframe!(io)
    end
    m.i_selected[] = 3
    sleep(pause)
    recordframe!(io)
    for i in 7:10
        set_close_to!(s, i)
        sleep(pause)
        recordframe!(io)
    end
end

Доступ к созданным блокам

Доступ к созданным блокам можно получить посредством синтаксиса then(f).

import Makie.SpecApi as S
ax =  S.Axis(...)
ax.then() do actual_axis_object
    return on(events(actual_axis_object).mouseposition) do mp
        println("mouse: $(mp)")
    end
end

Обратите внимание, что обратный вызов должен быть чистым, так как объекты будут использоваться повторно и обратный вызов будет выполнен снова. Чтобы можно было использовать on или onany, можно вернуть массив ObserverFunctions или одну такую функцию, как в примере выше.

ax.then() do ax
    obs1 = on(f1, events(ax).keyboardbutton)
    obs2 = on(f2, events(ax).mousebutton)
    obs_array = onany(f3, some_obs1, some_obs2)
    return [obs1, obs2, obs_array...]
end
This allows the SpecApi to clean up the callbacks on reuse.
Note that things like `hidedecorations!(axis)` is not yet supported, since we will need some better book keeping of what got mutated by that call.
One of the few functions that's already supported is `linkaxes!`:

julia axes_1 = [S.Axis(title="Axis (1): i ") for i in 1:3] axes_2 = [S.Axis(title="Axis (2): i ") for i in 1:3] for ax1 in axes_1 for ax2 in axes_2 if ax1 != ax2 ax1.then() do iax ax2.then() do jax linkaxes!(iax, jax) return end return end end end end `