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

Преобразования размеров

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

Начиная с версии 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))
f226d1e

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

scatter!(ax, rand(Hour(1):Hour(1):Hour(20), 10))
# Единицы измерения также поддерживаются
scatter!(ax, LinRange(0u"yr", 0.1u"yr", 5))
f
f1d82f5

Обратите внимание, что единицы измерения, отображаемые в делениях, будут соответствовать заданному диапазону значений.

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

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

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,))
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)"))
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)
e86aa4c

Более сложные примеры можно найти в реализации в Makie/src/dim-converts.

Конвертации применяются в функции Makie.conversion_pipeline в Makie/src/interfaces.jl.