Типы шаблонов
Обзор
Существует четыре основных типа шаблонов, которые определяются сигнатурой макроса @recipe
.
Пользовательские шаблоны
@recipe function f(custom_arg_1::T, custom_arg_2::S, ...; ...)
Теперь можно строить графики с использованием следующих шаблонов.
|
Шаблоны типов
@recipe function f(::Type{T}, val::T) where T
Совместимость: 1.0
В шаблонах типов RecipesBase 1.0 учитывается текущая ось (
При этом задаются направляющие только для осей с параметром |
Совместимость: 1.0
В RecipesBase 1.0 также поддерживаются шаблоны типов вида
для |
Шаблоны пользователей и шаблоны типов должны возвращать либо — функцию Допустимый тип — это либо точка данных Plots, либо тип, который может быть обработан другим пользовательским шаблоном или шаблоном типа. Точки данных Plots являются подтипами Если возвращаются две функции, первая должна указать Plots, как преобразовать |
Шаблоны рядов
@recipe function f(::Type{Val{:myseriesrecipename}}, x, y, z; ...)
Макрос
Это позволяет строить графики с использованием следующих функций:
|
Шаблоны графиков и шаблоны рядов должны задавать атрибут |
Пользовательские шаблоны
Пользовательские шаблоны вызываются на ранних этапах конвейера обработки и позволяют создавать пользовательские визуализации.
@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
Это связано с тем, что график и подграфики уже были построены до применения шаблона ряда.
Для всего, что изменяет атрибуты в масштабе графика, необходимо использовать шаблоны графиков, в противном случае рекомендуется использовать шаблоны рядов. |