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

Типы шаблонов

Обзор

Существует четыре основных типа шаблонов, которые определяются сигнатурой макроса @recipe.

Пользовательские шаблоны

@recipe function f(custom_arg_1::T, custom_arg_2::S, ...; ...)

@userplot предоставляет удобный способ создания пользовательского типа для диспетчеризации и определяет пользовательские функции построения графиков.

@userplot MyPlot
@recipe function f(mp::MyPlot; ...)
    ...
end

Теперь можно строить графики с использованием следующих шаблонов.

myplot(args...; kw...)
myplot!(args...; kw...)

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

@recipe function f(::Type{T}, val::T) where T
Совместимость: 1.0

В шаблонах типов RecipesBase 1.0 учитывается текущая ось (:x, :y, :z).

@recipe function f(::Type{MyType}, val::MyType)
    guide --> "My Guide"
    ...
end

При этом задаются направляющие только для осей с параметром MyType. Для более сложных шаблонов типов буква текущей оси может быть доступна в [@recipe](api.md#RecipesBase.@recipe-Tuple{Expr}) с помощью plotattributes[:letter].

Совместимость: 1.0

В RecipesBase 1.0 также поддерживаются шаблоны типов вида

@recipe function f(::Type{T}, val::T) where T <: AbstractArray{MyType}

для AbstractArray пользовательских типов.

Шаблоны пользователей и шаблоны типов должны возвращать либо — функцию AbstractArray{<:V}, где V — допустимый тип, — две функции, либо — ничего.

Допустимый тип — это либо точка данных Plots, либо тип, который может быть обработан другим пользовательским шаблоном или шаблоном типа. Точки данных Plots являются подтипами Union{AbstractString, Missing} и Union{Number, Missing}.

Если возвращаются две функции, первая должна указать Plots, как преобразовать T в точку данных, а вторая — как преобразовать точку данных в строку для форматирования метки делений.

Шаблоны графиков

@recipe function f(::Type{Val{:myplotrecipename}}, plt::AbstractPlot; ...)

Шаблоны рядов

@recipe function f(::Type{Val{:myseriesrecipename}}, x, y, z; ...)

Макрос @shorthands удобно использовать для определения функций построения графиков для пользовательских шаблонов графиков или шаблонов рядов.

@shorthands myseriestype
@recipe function f(::Type{Val{:myseriestype}}, x, y, z; ...)
    ...
end

Это позволяет строить графики с использованием следующих функций:

myseriestype(args...; kw...)
myseriestype!(args...; kw...)

Шаблоны графиков и шаблоны рядов должны задавать атрибут seriestype.

Пользовательские шаблоны

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

@recipe function f(custom_arg_1::T, custom_arg_2::S, ...; ...)

Пример пользовательского шаблона уже рассматривался выше в разделе «Синтаксис». Пользовательские шаблоны также можно использовать для определения пользовательской визуализации без необходимости построения пользовательского типа. Для этого мы можем создать тип, по которому будет осуществляться диспетчеризация. Будем использовать макрос @userplot.

@userplot MyPlot

расширяется до

mutable struct MyPlot
    args
end
export myplot, myplot!
myplot(args...; kw...) = plot(MyPlot(args); kw...)
myplot!(args...; kw...) = plot!(MyPlot(args); kw...)

Для проверки типа args определите структуру с параметрами типа.

@userplot struct MyPlot{T<:Tuple{AbstractVector}}
    args::T
end

Ее можно использовать для определения пользовательского шаблона для круговой диаграммы.

# определяет изменяемую структуру `UserPie` и задает сокращения `userpie` и `userpie!`
@userplot UserPie
@recipe function f(up::UserPie)
    y = up.args[end] # извлекает y из аргументов
    # если передано два аргумента, первый из них используется в качестве надписей
    labels = length(up.args) == 2 ? up.args[1] : eachindex(y)
    framestyle --> :none
    aspect_ratio --> true
    s = sum(y)
    θ = 0
    # добавляет фигуру для каждой части круговой диаграммы
    for i in 1:length(y)
        # определяет угол, пока не будет сделана остановка
        θ_new = θ + 2π * y[i] / s
        # вычисляет координаты
        coords = [(0.0, 0.0); Plots.partialcircle(θ, θ_new, 50)]
        @series begin
            seriestype := :shape
            label --> string(labels[i])
            coords
        end
        θ = θ_new
    end
    # мы уже добавили все фигуры в @series, поэтому не хотим возвращать ряд
    # здесь. (Технически мы возвращаем пустой ряд, который не добавляется в
    # условные обозначения.)
    primary := false
    ()
end

Теперь можно просто использовать шаблон в таком виде:

userpie('A':'D', rand(4))

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

Шаблоны типов определяют взаимно-однозначные сопоставления пользовательских типов с тем, что поддерживает Plots.

@recipe function f(::Type{T}, val::T) where T

Предположим, что у нас есть пользовательская оболочка для векторов.

struct MyWrapper
    v::Vector
end

В шаблоне типа можем указать Plots использовать для построения только заключенный в оболочку вектор.

@recipe f(::Type{MyWrapper}, mw::MyWrapper) = mw.v

Теперь Plots знает, что делать, когда видит MyWrapper.

mw = MyWrapper(cumsum(rand(10)))
plot(mw)

Благодаря рекурсивному применению шаблонов типов они даже компонуются автоматически.

struct MyOtherWrapper
    w
end

@recipe f(::Type{MyOtherWrapper}, mow::MyOtherWrapper) = mow.w

mow = MyOtherWrapper(mw)
plot(mow)

Если необходимо поэлементное преобразование пользовательских типов, можно определить функцию преобразования к типу, поддерживаемому Plots (Real, AbstractString), и средство форматирования для надписей делений. Рассмотрим следующий простой тип времени.

struct MyTime
    h::Int
    m::Int
end

# отображает, например, `MyTime(1, 30)` как "01:30"
time_string(mt) = join((lpad(string(c), 2, "0") for c in (mt.h, mt.m)), ":")
# сопоставляет объект `MyTime` с количеством минут, прошедших с полуночи.
# это фактическая дата, которую будет использовать Plots.
minutes_since_midnight(mt) = 60 * mt.h + mt.m
# преобразует минуты, прошедшие с полуночи, в красивую строку, отображающую `MyTime`
formatter(n) = time_string(MyTime(divrem(n, 60)...))

# определяет шаблон (он должен возвращать две функции)
@recipe f(::Type{MyTime}, mt::MyTime) = (minutes_since_midnight, formatter)

Теперь мы можем строить векторы MyTime автоматически с правильными надписями делений. С помощью такого шаблона типа в Plots реализованы, например, DateTime и Char.

times = MyTime.(0:23, rand(0:59, 24))
vals = log.(1:24)

plot(times, vals)

И снова все хорошо компонуется.

plot(MyWrapper(vals), MyOtherWrapper(times))

Шаблоны графиков

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

@recipe function f(::Type{Val{:myplotrecipename}}, plt::AbstractPlot; ...)

Шаблоны графиков определяют новый тип ряда. Они применяются после шаблонов типов. Следовательно, для входных данных :x, :y и :z в plotattributes могут быть приняты стандартные типы Plots. Шаблоны графиков могут получать доступ к атрибутам графиков и подграфиков до их обработки, например для построения макетов. И шаблоны графиков, и шаблоны рядов должны изменять тип ряда. В противном случае будет выведено предупреждение о возможной ошибке StackOverflow.

Мы можем определить тип ряда :yscaleplot, который автоматически отображает данные с линейной шкалой y на одном подграфике и с логарифмической шкалой y на другом.

@recipe function f(::Type{Val{:yscaleplot}}, plt::AbstractPlot)
    x, y = plotattributes[:x], plotattributes[:y]
    layout := (1, 2)
    for (i, scale) in enumerate((:linear, :log))
        @series begin
            title --> string(scale, " scale")
            seriestype := :path
            subplot := i
            yscale := scale
        end
    end
end

Его можно вызвать с помощью plot(...; ..., seriestype = :yscaleplot) либо можно определить сокращение с помощью макроса @shorthands.

@shorthands myseries

расширяется до

export myseries, myseries!
myseries(args...; kw...) = plot(args...; kw..., seriestype = :myseries)
myseries!(args...; kw...) = plot!(args...; kw..., seriestype = :myseries)

Итак, попробуем шаблон графика yscaleplot.

@shorthands yscaleplot

yscaleplot((1:10).^2)

Волшебным образом композиция с использованием шаблонов типов снова работает.

yscaleplot(MyWrapper(times), MyOtherWrapper((1:24).^2))

Шаблоны рядов

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

@recipe function f(::Type{Val{:myseriesrecipename}}, x, y, z; ...)

Если потребуется вызвать шаблон userpie с пользовательским типом, возникнут ошибки.

userpie(MyWrapper(rand(4)))
ERROR: MethodError: no method matching keys(::MyWrapper)
Stacktrace:
 [1] eachindex(::MyWrapper) at ./abstractarray.jl:209

Более того, если мы хотим показать несколько круговых диаграмм на разных подграфиках, мы также не получим того, чего ожидаем.

userpie(rand(4, 2), layout = 2)

С этими проблемами можно справиться, реализовав необходимые методы AbstractArray для MyWrapper (вместо шаблона типа) и уделив больше внимания различным рядам в шаблоне userpie. Однако более простым подходом является написание шаблона круговой диаграммы в виде шаблона рядов и использование конвейера обработки Plots.

@recipe function f(::Type{Val{:seriespie}}, x, y, z)
    framestyle --> :none
    aspect_ratio --> true
    s = sum(y)
    θ = 0
    for i in eachindex(y)
        θ_new = θ + 2π * y[i] / s
        coords = [(0.0, 0.0); Plots.partialcircle(θ, θ_new, 50)]
        @series begin
            seriestype := :shape
            label --> string(x[i])
            x := first.(coords)
            y := last.(coords)
        end
        θ = θ_new
    end
end
@shorthands seriespie
seriespie! (generic function with 1 method)

Здесь мы используем уже обработанные значения x и y для вычисления координат фигуры для каждой части диаграммы, обновляем x и y в соответствии с этими координатами и задаем тип ряда :shape.

seriespie(rand(4))

Это автоматически работает для шаблонов типов …​

seriespie(MyWrapper(rand(4)))

или макетов.

seriespie(rand(4, 2), layout = 2)

Замечания

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

@recipe function f(::Type{Val{:plotpie}}, plt::AbstractPlot)
    y = plotattributes[:y]
    labels = plotattributes[:x]
    framestyle --> :none
    aspect_ratio --> true
    s = sum(y)
    θ = 0
    for i in 1:length(y)
        θ_new = θ + 2π * y[i] / s
        coords = [(0.0, 0.0); Plots.partialcircle(θ, θ_new, 50)]
        @series begin
            seriestype := :shape
            label --> string(labels[i])
            x := first.(coords)
            y := last.(coords)
        end
        θ = θ_new
    end
end
@shorthands plotpie

plotpie(rand(4, 2), layout = (1, 2))

Синтаксис шаблонов рядов в этом случае просто немного красивее.

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

Давайте попробуем сделать наоборот и реализуем шаблон yscaleplot как шаблон ряда.

@recipe function f(::Type{Val{:yscaleseries}}, x, y, z)
    layout := (1, 2)
    for (i, scale) in enumerate((:linear, :log))
        @series begin
            title --> string(scale, " scale")
            seriestype := :path
            subplot := i
            yscale := scale
        end
    end
end
@shorthands yscaleseries
yscaleseries! (generic function with 1 method)

Выглядит немного симпатичнее, чем вариант шаблона графика. Давайте попробуем построить график.

yscaleseries((1:10).^2)
MethodError: Cannot `convert` an object of type Int64 to an object of type Plots.Subplot{Plots.GRBackend}
Closest candidates are:
  convert(::Type{T}, !Matched::T) where T at essentials.jl:168
  Plots.Subplot{Plots.GRBackend}(::Any, !Matched::Any, !Matched::Any, !Matched::Any, !Matched::Any, !Matched::Any, !Matched::Any, !Matched::Any) where T<:RecipesBase.AbstractBackend at /home/daniel/.julia/packages/Plots/rNwM4/src/types.jl:88

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

Для всего, что изменяет атрибуты в масштабе графика, необходимо использовать шаблоны графиков, в противном случае рекомендуется использовать шаблоны рядов.