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

Шаблоны

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

Шаблоны позволяют расширять Makie собственными пользовательскими типами и командами построения графиков.

Note Если вы разработчик пакетов, вы можете добавлять шаблоны, не добавляя весь пакет Makie.jl в качестве зависимости. Вместо этого вы можете использовать облегченный пакет MakieCore, который предоставляет все необходимые элементы для создания шаблона, такие как макрос @recipe, функции convert_arguments и convert_attribute, а также некоторые базовые определения типов графиков.

Есть два типа шаблонов.

  • Шаблон типа определяет простое сопоставление пользовательского типа с существующим типом графика.

  • Полный шаблон определяет новую пользовательскую функцию построения графиков.

Шаблоны типов

Шаблоны типов в основном представляют собой просто конвертации из одного типа или набора типов входных аргументов, еще неизвестных Makie, в другой, с которым Makie уже работает.

Попытки конвертации в Makie осуществляются в следующей последовательности.

  • Диспетчеризация по convert_arguments(::PlotType, args...).

  • Если соответствующий метод не найден, определяется типаж конвертации с помощью conversion_trait(::PlotType).

  • Диспетчеризация по convert_arguments(::ConversionTrait, args...).

  • Если соответствующий метод не найден, выполняются попытки конвертировать каждый отдельный аргумент рекурсивно с помощью convert_single_argument до тех пор, пока тип не перестает меняться.

  • Диспетчеризация по convert_arguments(::PlotType, converted_args...).

  • Если метод не найден, происходит сбой.

Конвертация нескольких аргументов с помощью convert_arguments

Построение Circle, например, можно определить посредством конвертации в вектор точек для любого существующего типа графика:

Makie.convert_arguments(::Type{<: AbstractPlot}, x::Circle) = (decompose(Point2f, x),)

# или если вы выбрали простую систему шаблонов MakieCore в качестве зависимости
MakieCore.convert_arguments(::Type{<: AbstractPlot}, x::Circle) = (decompose(Point2f, x),)

Warning Функция convert_arguments всегда должна возвращать кортеж.

Определять конвертацию для каждого типа графика, скорее всего, не имеет смысла, поэтому можно ограничиться подмножеством типов, например только для графиков рассеяния:

Makie.convert_arguments(P::Type{<:Scatter}, ::MyType) = convert_arguments(P, rand(10))

Типажи конвертации облегчают определение поведения для группы типов графиков с одинаковым типажом. Например, PointBased применяется к Scatter, Lines и т. д. Предварительно определены следующие типажи: NoConversion, PointBased, CellGrid <: GridBased, VertexGrid <: GridBased, ImageLike, VolumeLike и SampleBased. Все они наследуются от ConversionTrait, иногда косвенно.

Makie.convert_arguments(::PointBased, ::MyType) = ...

Кроме того, есть возможность конвертировать сразу несколько аргументов.

Makie.convert_arguments(::Type{<:Scatter}, x::MyType, y::MyOtherType) = ...

При необходимости можно определить тип графика по умолчанию, чтобы при вызове plot(x::MyType) всегда строился, например, график поверхности:

plottype(::MyType) = Surface

Конвертация одного аргумента с помощью convert_single_argument

Некоторые типы, неизвестные Makie, можно конвертировать в другие типы, для которых доступны методы convert_arguments. Делается это с помощью convert_single_argument.

Например, массивы AbstractArrays со значениями типа Real и missing обычно можно безопасно конвертировать в массивы типа Float32 со значениями NaN вместо missing.

Разница между функциями convert_single_argument и convert_arguments с одним аргументом заключается в том, что первую можно применять к любому аргументу любой сигнатуры, тогда как последняя соответствует только сигнатурам с одним аргументом.

Полные шаблоны с макросом @recipe

Полный шаблон состоит из двух частей. Первая часть — это имя типа графика, например MyPlot, а также аргументы и определение темы, которые задаются с помощью макроса @recipe.

Вторая часть включает в себя по крайней мере один пользовательский метод plot! для MyPlot, который создает фактическую визуализацию с использованием других существующих функций построения графиков.

Покажем, как это работает, на примере.

@recipe(MyPlot, x, y, z) do scene
    Theme(
        plot_color = :red
    )
end

Этот макрос расширяется в несколько объектов. Во-первых, определение типа.

const MyPlot{ArgTypes} = Plot{myplot, ArgTypes}

Параметр типа Plot содержит функцию myplot, а не, например, символ MyPlot. Это делает сопоставление MyPlot с myplot более безопасным и простым. Для удобства использования MyPlot автоматически определяются следующие сигнатуры.

myplot(args...; kw_args...) = ...
myplot!(args...; kw_args...) = ...

Специализация argument_names выдается, если макросу шаблона предоставляется список аргументов (x,y,z).

argument_names(::Type{<: MyPlot}) = (:x, :y, :z)

Это необязательно, но, например, позволяет использовать plot_object.x для получения первого аргумента из вызова plot_object = myplot(rand(10), rand(10), rand(10)).

Вместо этого вы всегда можете получить i-й аргумент с помощью plot_object[i], а если аргументы (x,y,z) не указываются, версия argument_names по умолчанию предоставит plot_object.arg1 и т. д.

Тема, заданная в теле вызова @recipe, вставляется в специализацию default_theme, которая добавляет эту тему к всем сценам, на которых отрисовывается MyPlot.

function default_theme(scene, ::MyPlot)
    Theme(
        plot_color => :red
    )
end

Кроме того, вы можете контролировать тип оси, используемый для графика, определяя

Makie.args_preferred_axis(::Type{<: MyPlot}, x, y, z) =  Makie.LScene

или

Makie.preferred_axis_type(plot::MyPlot) = Makie.LScene # если вам нужен весь объект графика в качестве информации.

Обратите внимание, что в Makie предпочтительной осью по умолчанию является Makie.Axis.

Во второй части определения MyPlot следует реализовать фактический способ построения объекта MyPlot, специализировав plot!.

function Makie.plot!(myplot::MyPlot)
    # обычный код построения графика, основанный на ранее определенных шаблонах
    # или атомарные операции построения графиков, добавляемые к объединенной функции `myplot`:
    lines!(myplot, rand(10), color = myplot.plot_color)
    plot!(myplot, myplot.x, myplot.y, myplot.z)
    myplot
end

Здесь можно добавить специализации в зависимости от типов аргументов, передаваемых в myplot. Например, так можно специализировать поведение myplot(a), когда a — это трехмерный массив чисел с плавающей запятой:

const MyVolume = MyPlot{Tuple{<:AbstractArray{<: AbstractFloat, 3}}}
argument_names(::Type{<: MyVolume}) = (:volume,) # это также необязательно
function plot!(plot::MyVolume)
    # строим график объема с цветовой картой от полной прозрачности до plot_color
    volume!(plot, plot[:volume], colormap = :transparent => plot[:plot_color])
    plot
end

Пример: биржевая диаграмма

Допустим, нам нужно визуализировать стоимость акций с помощью классических комбинаций открытия/закрытия и минимума/максимума. В данном примере мы создадим специальный тип для хранения этой информации и шаблон для построения графика этого типа.

Сначала мы создаем структуру для хранения значений стоимости акций за определенный день:

using CairoMakie

struct StockValue{T<:Real}
    open::T
    close::T
    high::T
    low::T
end

Теперь создадим новый тип графика под названием StockChart. Замыкание do scene — это просто функция, которая возвращает атрибуты по умолчанию. В данном случае она окрашивает падающие в цене акции в красный цвет, а растущие в цене акции — в зеленый.

@recipe(StockChart) do scene
    Attributes(
        downcolor = :red,
        upcolor = :green,
    )
end

Затем мы переходим к сути шаблона, которая заключается в создании метода графика. Нам необходимо перегрузить определенный метод Makie.plot!, аргументом которого является подтип нашего нового типа графика StockChart. Его параметр типа представляет собой кортеж, описывающий типы аргументов, для которых этот метод должен работать.

Обратите внимание, что входные аргументы, которые мы получаем внутри метода plot! и можем извлечь, обращаясь по индексам к StockChart, автоматически конвертируются в наблюдаемые объекты в Makie.

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

function Makie.plot!(
        sc::StockChart{<:Tuple{AbstractVector{<:Real}, AbstractVector{<:StockValue}}})

    # первым аргументом является наблюдаемый объект параметрического типа AbstractVector{<:Real}
    times = sc[1]
    # вторым аргументом является наблюдаемый объект параметрического типа AbstractVector{<:StockValue}}
    stockvalues = sc[2]

    # мы предварительно определяем несколько наблюдаемых объектов для отрезков
    # и столбчатых графиков, которые нужно отрисовать
    # это необходимо по той причине, что в Makie каждый шаблон должен обновляться в интерактивном режиме,
    # а для этого необходимо подключить механизм наблюдаемых объектов
    linesegs = Observable(Point2f[])
    bar_froms = Observable(Float32[])
    bar_tos = Observable(Float32[])
    colors = Observable(Bool[])

    # эта вспомогательная функция будет обновлять наблюдаемые объекты
    # при каждом изменении `times` или `stockvalues`
    function update_plot(times, stockvalues)
        colors[]

        # очищаем векторы внутри наблюдаемых объектов,
        empty!(linesegs[])
        empty!(bar_froms[])
        empty!(bar_tos[])
        empty!(colors[])

        # затем заполняем их обновленными значениями
        for (t, s) in zip(times, stockvalues)
            push!(linesegs[], Point2f(t, s.low))
            push!(linesegs[], Point2f(t, s.high))
            push!(bar_froms[], s.open)
            push!(bar_tos[], s.close)
        end
        append!(colors[], [x.close > x.open for x in stockvalues])
        colors[] = colors[]
    end

    # подключаем функцию `update_plot`, чтобы она вызывалась при каждом
    # изменении `times` или `stockvalues`
    Makie.Observables.onany(update_plot, times, stockvalues)

    # затем вызываем ее один раз вручную с первоначальным содержимым `times` и `stockvalues`,
    # чтобы предварительно заполнить все наблюдаемые объекты правильными значениями
    update_plot(times[], stockvalues[])

    # для цветов мы просто используем вектор логических значений или 0 и 1, которые
    # выполняют окрашивание в соответствии с 2-элементной цветовой картой
    # мы строим эту цветовую карту на основе `downcolor` и `upcolor`
    # мы присваиваем наблюдаемому объекту тип `Any`, чтобы он не выдавал ошибку при изменении
    # типа цвета с символа, например :red, на другой, допустим RGBf(1, 0, 1)
    colormap = Observable{Any}()
    map!(colormap, sc.downcolor, sc.upcolor) do dc, uc
        [dc, uc]
    end

    # на последнем шаге мы создаем график в объекте `sc` StockChart, что означает,
    # что наш новый график состоит из двух более простых шаблонов, наложенных
    # друг на друга
    linesegments!(sc, linesegs, color = colors, colormap = colormap)
    barplot!(sc, times, bar_froms, fillto = bar_tos, color = colors, strokewidth = 0, colormap = colormap)

    # наконец, мы возвращаем новый график StockChart
    sc
end

Давайте попробуем построить график для нескольких акций:

timestamps = 1:100

# мы создаем несколько фиктивных значений стоимости так, чтобы они неплохо выглядели в дальнейшем
startvalue = StockValue(0.0, 0.0, 0.0, 0.0)
stockvalues = foldl(timestamps[2:end], init = [startvalue]) do values, t
    open = last(values).close + 0.3 * randn()
    close = open + randn()
    high = max(open, close) + rand()
    low = min(open, close) - rand()
    push!(values, StockValue(
        open, close, high, low
    ))
end

# теперь можно использовать наш новый шаблон
f = Figure()

stockchart(f[1, 1], timestamps, stockvalues)

# и давайте попробуем изменить атрибуты по умолчанию
stockchart(f[2, 1], timestamps, stockvalues,
    downcolor = :purple, upcolor = :orange)
f
9328abf

В последнем примере представим, что данные по акциям поступают динамически и на их основе нужно создать анимацию. Это легко сделать, если в качестве входных аргументов используются наблюдаемые объекты, которые будут обновляться покадрово:

timestamps = Observable(collect(1:100))
stocknode = Observable(stockvalues)

fig, ax, sc = stockchart(timestamps, stocknode)

record(fig, "stockchart_animation.mp4", 101:200,
        framerate = 30) do t
    # добавляем новую метку времени, не активируя наблюдаемый объект
    push!(timestamps[], t)

    # добавляем новое значение StockValue, не активируя наблюдаемый объект
    old = last(stocknode[])
    open = old.close + 0.3 * randn()
    close = open + randn()
    high = max(open, close) + rand()
    low = min(open, close) - rand()
    new = StockValue(open, close, high, low)
    push!(stocknode[], new)

    # теперь метки времени и stocknode синхронизированы
    # снова, и мы можем активировать один из этих объектов, присвоив его самому себе,
    # чтобы обновить весь биржевой график для нового кадра
    stocknode[] = stocknode[]
    # давайте также обновим пределы осей, поскольку график будет расти
    # вправо
    autolimits!(ax)
end

Расширение пакета Makie

Простой пример расширения пакета для Makie см. на странице https://github.com/jkrumbiegel/MakiePkgExtTest. В следующей документации объясняются основы реализации в связанном примере.

Настройте расширение пакета так, чтобы зависимостью был Makie, а не MakieCore или какой-либо из бэкендов Makie.

Вам потребуется определить и экспортировать функции полных шаблонов в основной пакет, например:

module SomePackage

export someplot
export someplot!

# функции без методов
function someplot end
function someplot! end

end # модуль

после чего ваше расширение пакета Makie добавит методы в someplot!.

module MakieExtension

using SomePackage
import SomePackage: someplot, someplot!

Makie.convert_single_argument(v::SomeVector) = v.v

@recipe(SomePlot) do scene
    Theme()
end

function Makie.plot!(p::SomePlot)
    lines!(p, p[1])
    scatter!(p, p[1])
    return p
end

end # модуль

В приведенном по ссылке выше примере представлены дополнительные функциональные возможности, такие как поддержка одновременно расширений и Requires.jl, предоставление подсказок об ошибках функций построения графиков, у которых еще нет методов, или настройка файла Project.toml.