Преобразования размеров
|
Страница в процессе перевода. |
Начиная с версии Makie 0.21, добавлена поддержка таких типов, как единицы измерения, категориальные значения и даты. Они преобразуются в представление, которое может быть построено, с помощью конвертаций размеров, которые также учитывают деления осей. В следующих разделах мы объясним, как они используются и как расширить интерфейс собственными типами.
Примеры
В простейшем случае использования достаточно заменить числа любым поддерживаемым типом, например Dates.Second.
using CairoMakie
using CairoMakie, Makie.Dates, Makie.Unitful
f, ax, pl = scatter(rand(Second(1):Second(60):Second(20*60), 10))
После того как для размера оси задается определенная единица измерения, график по этой оси необходимо строить в совместимых единицах измерения. Так, например, часы поддерживаются, поскольку они совместимы с конвертацией с единицами измерения.
scatter!(ax, rand(Hour(1):Hour(1):Hour(20), 10))
# Единицы измерения также поддерживаются
scatter!(ax, LinRange(0u"yr", 0.1u"yr", 5))
f
Обратите внимание, что единицы измерения, отображаемые в делениях, будут соответствовать заданному диапазону значений.
Попытка вернуться просто к числам приведет к ошибке, поскольку ось теперь имеет единицы измерения.
try
scatter!(ax, 1:4)
catch e
return e
end
Аналогичным образом попытка отложить значения с единицами измерения по оси без единиц измерения также приведет к ошибке, поскольку в противном случае смысл ранее построенных на графике значений изменился бы.
try
scatter!(ax, LinRange(0u"yr", 0.1u"yr", 10), rand(Hour(1):Hour(1):Hour(20), 10))
catch e
return e
end
Получить доступ к конвертации можно посредством ax.dim1_conversion и ax.dim2_conversion.
(ax.dim1_conversion[], ax.dim2_conversion[])
Задаются они аналогичным образом:
f = Figure()
ax = Axis(f[1, 1]; dim1_conversion=Makie.CategoricalConversion())
Ограничения
-
В настоящее время конвертации размеров применимы только к векторам с поддерживаемыми типами аргументов x и y стандартной двухмерной оси. Планируется распространить их и на другие типы осей, но полная интеграция пока не реализована.
-
Именованные аргументы, такие как
direction=:yдля столбчатого графика, не будут правильно распространяться на ось, так как первым аргументом в настоящее время всегда является x, а вторым — y. Мы все еще пытаемся найти способ решить эту проблему правильно. -
Категориальные значения необходимо заключать в
Categorical, поскольку сложно найти хороший тип, который не будет неоднозначным при использовании по умолчанию категориальной конвертации. Обходное решение см. в документации. -
Для делений типа «дата и время» просто используется функция
PlotUtils.optimize_datetime_ticks, которая также применяется в Plots.jl. В настоящее время она генерирует неоптимальные визуально деления и может создавать наложения и быстро выходить за пределы осей. Для генерирования удобочитаемых делений по умолчанию потребуется доработка. -
Для правильного применения конвертаций размеров только тогда, когда они возможны, необходимо использовать новый недокументированный макрос
@recipeи определить целевой тип конвертации. Это означает, что пользовательские шаблоны работают только в том случае, если они передаются посредством аргументов в любой базовый тип графика без конвертации.
Текущие конвертации в Makie
#
Makie.CategoricalConversion — Type
CategoricalConversion(; sortby=identity)
Категориальная конвертация. В настоящее время выбирается автоматически только для Categorical(array_of_objects). Однако категории работают с любым сортируемым значением, поэтому всегда можно использовать Axis(fig; dim1_conversion=CategoricalConversion()) для применения к другим категориям. Можно использовать CategoricalConversion(sortby=func), чтобы изменить сортировку или сделать несортируемые объекты сортируемыми.
Примеры
# Деления автоматически выбираются как категориальные
scatter(1:4, Categorical(["a", "b", "c", "a"]))
# Явно задайте их для других типов:
struct Named
value
end
Base.show(io::IO, s::SomeStruct) = println(io, "[$(s.value)]")
conversion = Makie.CategoricalConversion(sortby=x->x.value)
barplot(Named.([:a, :b, :c]), 1:3, axis=(dim1_conversion=conversion,))
#
Makie.UnitfulConversion — Type
UnitfulConversion(unit=automatic; units_in_label=false)
Позволяет строить графики на оси на основе массивов объектов с единицами измерения.
Аргументы
-
unit=automatic: задает целевую единицу измерения для конвертации. Если оставить значение automatic, для всех графиков и значений, откладываемых по оси, будет выбрана оптимальная единица измерения (например, годы для длительных периодов, километры для больших расстояний или наносекунды для коротких промежутков времени). -
units_in_label=true: определяет, отображаются ли графики в label_prefix меток осей или в метках делений.
Примеры
using Unitful, CairoMakie
# UnitfulConversion выбирается автоматически:
scatter(1:4, [1u"ns", 2u"ns", 3u"ns", 4u"ns"])
В качестве единицы измерения всегда используются метры; единицы отображаются в xlabel:
uc = Makie.UnitfulConversion(u"m"; units_in_label=false)
scatter(1:4, [0.01u"km", 0.02u"km", 0.03u"km", 0.04u"km"]; axis=(dim2_conversion=uc, xlabel="x (km)"))
#
Makie.DateTimeConversion — Type
DateTimeConversion(type=Automatic; k_min=automatic, k_max=automatic, k_ideal=automatic)
Создает конвертации для Date, DateTime и Time. Для других единиц времени следует использовать тип UnitfulConversion, который работает, например, с секундами.
Для объектов DateTime функция PlotUtils.optimize_datetime_ticks используется для получения конвертации. В противном случае axis.(x/y)ticks используется для целочисленного представления даты.
Аргументы
-
type=automatic: при значении automatic тип определяется по первому графику на оси. Можно также задать типTime,DateилиDateTime.
Примеры
date_time = DateTime("2021-10-27T11:11:55.914")
date_time_range = range(date_time, step=Week(5), length=10)
# В качестве xticks автоматически выбрано DateTeimeTicks:
scatter(date_time_range, 1:10)
# явно выбран объект DateTimeConversion, который используется для построения графика значений с единицами измерения и их отображения в формате `Time`:
using Makie.Unitful
conversion = Makie.DateTimeConversion(Time)
scatter(1:4, (1:4) .* u"s", axis=(dim2_conversion=conversion,))
Документация разработчика
Вы можете перегрузить API, чтобы определить собственные преобразования размеров. Для этого перегрузите следующие функции.
struct MyDimConversion <: Makie.AbstractDimConversion end
# Целевой тип конвертации размера
struct MyUnit
value::Float64
end
# В настоящее время это необходимо, поскольку `expand_dimensions` может быть ограниченно определено в Makie только для `Vector{<:Real}`.
# Поэтому, чтобы вызов `plot(some_y_values)` работал для ваших собственных типов, вам нужно определить этот метод:
Makie.expand_dimensions(::PointBased, y::AbstractVector{<:MyUnit}) = (keys(y.values), y)
function Makie.needs_tick_update_observable(conversion::MyDimConversion)
# Возвращает наблюдаемый объект, указывающий, когда необходимо обновлять деления, например в случае изменения единицы измерения или добавления новых категорий.
# Для простой конвертации единиц это не требуется, поэтому возвращается nothing.
return nothing
end
# Указывает, что данный тип следует конвертировать с помощью MyDimConversion
# Тип извлекается посредством `Makie.get_element_type(plot_argument_for_dim_n)`,
# поэтому, например, `plot(1:10, ["a", "b", "c"])` вызывает `Makie.get_element_type(["a", "b", "c"])` и возвращает `String` для размера оси 2.
Makie.create_dim_conversion(::Type{MyUnit}) = MyDimConversion()
# Эту функцию также необходимо перегрузить, хотя в некотором смысле это избыточно в отношении предыдущей перегрузки.
# Мы не хотели использовать `hasmethod(MakieCore.should_dim_convert, (MyDimTypes,))`, потому что это может быть медленным и приводить к ошибкам.
Makie.MakieCore.should_dim_convert(::Type{MyUnit}) = true
# Версия фактической функции конвертации без наблюдаемого объекта
# Требуется для конвертации пределов осей и должно быть чистой версией приведенной ниже функции `convert_dim_observable`
function Makie.convert_dim_value(::MyDimConversion, values)
return [v.value for v in values]
end
function Makie.convert_dim_observable(conversion::MyDimConversion, values_obs::Observable, deregister)
# Здесь выполняется собственно конвертация
# Большинство сложных конвертаций размеров должно производиться с наблюдаемым объектом (например, для создания словаря всех используемых категорий), поэтому одной функции `convert_dim_value` недостаточно.
result = Observable(Float64[])
f = on(values_obs; update=true) do values
result[] = Makie.convert_dim_value(conversion, values)
end
# Любая операция с наблюдаемым объектом, такая как `on` или `map`, должна отправляться в `deregister` для правильной очистки состояния, например при уничтожении графика.
# для `result = map(func, values_obs)` можно использовать `append!(deregister, result.inputs)`
push!(deregister, f)
return result
end
function Makie.get_ticks(::MyDimConversion, user_set_ticks, user_dim_scale, user_formatter, limits_min, limits_max)
# В этом примере ничего особенного с делениями делать не нужно, просто добавьте `myunit` к меткам и предоставьте все остальное обычным методам поиска делений из Makie.
ticknumbers, ticklabels = Makie.get_ticks(user_set_ticks, user_dim_scale, user_formatter, limits_min,
limits_max)
return ticknumbers, ticklabels .* "myunit"
end
barplot([MyUnit(1), MyUnit(2), MyUnit(3)], 1:3)
Более сложные примеры можно найти в реализации в Makie/src/dim-converts.
Конвертации применяются в функции Makie.conversion_pipeline в Makie/src/interfaces.jl.