Skip to content

Commit

Permalink
add Makie extension
Browse files Browse the repository at this point in the history
  • Loading branch information
rafaqz committed Sep 3, 2023
1 parent 85a9da5 commit 4da0ee3
Show file tree
Hide file tree
Showing 5 changed files with 298 additions and 95 deletions.
9 changes: 7 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ IntervalSets = "8197267c-284f-5f27-9208-e0e47529a953"
InvertedIndices = "41ab1584-1d38-5bbf-9106-f11c6c58b48f"
IteratorInterfaceExtensions = "82899510-4779-5014-852e-03e436cf321d"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
MakieCore = "20f20a25-4f0e-4fdf-b5d1-57303727442b"
PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01"
Expand All @@ -30,13 +29,16 @@ Extents = "0.1"
IntervalSets = "0.5, 0.6, 0.7"
InvertedIndices = "1"
IteratorInterfaceExtensions = "1"
MakieCore = "0.6"
Makie = "0.19"
PrecompileTools = "1"
RecipesBase = "0.7, 0.8, 1"
TableTraits = "1"
Tables = "1"
julia = "1.6"

[extensions]
DimensionalDataMakie = "Makie"

[extras]
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
ArrayInterface = "4fba245c-0d91-5ea0-9b3e-6abc04ee57a9"
Expand All @@ -58,3 +60,6 @@ Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d"

[targets]
test = ["Aqua", "ArrayInterface", "BenchmarkTools", "Combinatorics", "CoordinateTransformations", "DataFrames", "Distributions", "Documenter", "ImageFiltering", "ImageTransformations", "OffsetArrays", "Plots", "Random", "SafeTestsets", "StatsPlots", "Test", "Unitful"]

[weakdeps]
Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a"
280 changes: 280 additions & 0 deletions ext/DimensionalDataMakie.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
module DimensionalDataMakie

using DimensionalData, Makie
using DimensionalData.Dimensions, DimensionalData.LookupArrays

const DD = DimensionalData

_paired(args...) = map(x -> x isa Pair ? x : x => x, args)

# 1d plots are scatter by default
for (f1, f2) in _paired(:plot => :scatter, :scatter, :lines, :stem)
docstring = """
$f1(::AbstractDimArray{<:Any,1})
Plot a 1-dimensional `AbstractDimArray` with `Makie.$f2`.
The X axis will be labelled with the dimension name and and use ticks from its lookup.
"""
@eval begin
@doc $docstring
function Makie.$f1(A::AbstractDimArray{<:Any,1}; attributes...)
A1 = _prepare_for_makie(A)
d = DD.dims(A1, 1)
user_attributes = Makie.Attributes(; attributes...)
plot_attributes = Makie.Attributes(;
axis=(;
xlabel=string(name(d)),
ylabel=DD.label(A),
),
)
# Convert categorical to Int
lookup_attributes, newdims = _split_attributes(A1)
A2 = _restore_dim_names(set(A1, d => newdims[1]), A)
args = Makie.convert_arguments(Makie.PointBased(), A2)
merged_attributes = merge(user_attributes, plot_attributes, lookup_attributes)
Makie.$f2(args...; merged_attributes...)
end
end
end

for (f1, f2) in _paired(:plot => :heatmap, :heatmap, :image, :contour, :contourf)
docstring = """
$f1(::AbstractDimArray{<:Any,2})
Plot a 2-dimensional `AbstractDimArray` with `Makie.$f2`.
# Keywords
Keywords for `$f1` work as usual.
- `dims`: A `Pair` or Tuple of Pair of `Dimension` or `Symbol`. Can be used to
specify dimensions that should be moved to the `X`, `Y` and `Z` dimensions
of the plot. For example `$f1(A, dims=:a => :X)` will use the `:a` dimension
as the `X` dimension in the plot.
"""
@eval begin
@doc $docstring
function Makie.$f1(A::AbstractDimArray{<:Any,2}; dims=(), attributes...)
A1 = _prepare_for_makie(A, dims)
lookup_attributes, newdims = _split_attributes(A1)
A2 = _restore_dim_names(set(A1, map(Pair, newdims, newdims)...), A, dims)

dx, dy = DD.dims(A2)
user_attributes = Makie.Attributes(; attributes...)
dd_attributes = Makie.Attributes(;
axis=(;
xlabel=DD.label(dx),
ylabel=DD.label(dy),
title=DD.label(A),
),
)
merged_attributes = merge(user_attributes, dd_attributes, lookup_attributes)
args = Makie.convert_arguments(Makie.ContinuousSurface(), A2)
merged_attributes = merge(user_attributes, dd_attributes, lookup_attributes)
Makie.$f2(args...; merged_attributes...)
end
end
end

for (f1, f2) in _paired(:plot => :volume, :volume, :volumeslices)
docstring = """
$f1(::AbstractDimArray{<:Any,2})
Plot a 2-dimensional `AbstractDimArray` with `Makie.$f2`.
# Keywords
Keywords for $f1 work as usual.
- `dims`: A `Pair` or Tuple of Pair of `Dimension` or `Symbol`. Can be used to
specify dimensions that should be moved to the `X`, `Y` and `Z` dimensions
of the plot. For example `$f1(A, dims=:a => :X)` will use the `:a` dimension
as the `X` dimension in the plot.
"""
@eval begin
@doc $docstring
function Makie.$f1(A::AbstractDimArray{<:Any,3}; dims=(), attributes...)
A1 = _prepare_for_makie(A, dims)
dx, dy, dz = DD.dims(A1)
user_attributes = Makie.Attributes(; attributes...)
dd_attributes = Makie.Attributes(;
# axis=(; cant actually set anything here for LScene)
)
_, newdims = _split_attributes(A1)
merged_attributes = merge(user_attributes, dd_attributes)
A2 = _restore_dim_names(set(A1, map(Pair, newdims, newdims)...), A, dims)
args = Makie.convert_arguments(Makie.VolumeLike(), A2)
Makie.$f2(args...; merged_attributes...)
end
end
end

for f in (:violin, :boxplot)
docstring = """
$f(::AbstractDimArray{<:Any,2})
Plot a 2-dimensional `AbstractDimArray` with `Makie.$f`.
At least one dimension should have a `Categorical` `Lookup`, which
will be used to assign categories to the values in the array.
"""
@eval begin
@doc $docstring
function Makie.$f(A::AbstractDimArray{<:Any,2}; attributes...)
categoricaldim = reduce(dims(A); init=nothing) do acc, x
lookup(acc) isa AbstractCategorical ? acc : x
end
isnothing(categoricaldim) && throw(ArgumentError("No dimensions have Categorical lookups"))
categoricallookup = parent(categoricaldim)
otherdim = only(otherdims(A, categoricaldim))
categories = broadcast_dims((_, c) -> c, A, DimArray(eachindex(categoricaldim), categoricaldim))

user_attributes = Makie.Attributes(; attributes...)
dd_attributes = Makie.Attributes(;
axis=(;
xlabel=DD.label(categoricaldim),
xticks=axes(categoricallookup, 1),
xtickformat=I -> map(string, categoricallookup[map(Int, I)]),
ylabel=DD.label(A),
),
)
merged_attributes = merge(user_attributes, dd_attributes)
Makie.$f(vec(categories), vec(A); merged_attributes...)
end
end
end

Makie.plottype(A::AbstractDimArray{<:Any,1}) = Makie.Scatter
Makie.plottype(A::AbstractDimArray{<:Any,2}) = Makie.Heatmap
Makie.plottype(A::AbstractDimArray{<:Any,3}) = Makie.Volume

# then, define how they are to be converted to plottable data
function Makie.convert_arguments(t::Makie.PointBased, A::AbstractDimArray{<:Any,1})
A = _prepare_for_makie(A)
xs = lookup(A, 1)
return Makie.convert_arguments(t, parent(xs), parent(A))
end
function Makie.convert_arguments(t::Makie.PointBased, A::AbstractDimArray{<:Number,2})
return Makie.convert_arguments(t, parent(A))
end
function Makie.convert_arguments(t::Makie.SurfaceLike, A::AbstractDimArray{<:Any,2})
A1 = _prepare_for_makie(A)
xs, ys = map(parent, lookup(A1))
return Makie.convert_arguments(t, xs, ys, parent(A1))
end
function Makie.convert_arguments(
t::Makie.DiscreteSurface, A::AbstractDimArray{<:Any,2}
)
A1 = _prepare_for_makie(A)
xs, ys = map(_lookup_edges, lookup(A1))
return Makie.convert_arguments(t, xs, ys, parent(A1))
end
function Makie.convert_arguments(t::Makie.VolumeLike, A::AbstractDimArray{<:Any,3})
A1 = _prepare_for_makie(A)
xs, ys, zs = map(parent, lookup(A1))
return xs, ys, zs, parent(A1)
end
# fallbacks with descriptive error messages
function Makie.convert_arguments(t::Makie.ConversionTrait, A::AbstractDimArray{<:Any,N}) where {N}
@warn "$t not implemented for `AbstractDimArray` with $N dims, falling back to parent array type"
return Makie.convert_arguments(t, parent(A))
end

# Calculate the edges
function _lookup_edges(l::LookupArray)
l = if l isa AbstractSampled
set(l, Intervals())
else
set(l, Sampled(; sampling=Intervals()))
end
if l == 1
return [bounds(l)...]
else
ib = intervalbounds(l)
if order(l) isa ForwardOrdered
edges = first.(ib)
push!(edges, last(last(ib)))
else
edges = last.(ib)
push!(edges, first(last(ib)))
end
return edges
end
end

_split_attributes(A) = _split_attributes(dims(A))
function _split_attributes(dims::DD.DimTuple)
reduce(dims; init=(Attributes(), ())) do (attr, ds), d
l = lookup(d)
if l isa AbstractCategorical
ticks = axes(l, 1)
int_dim = rebuild(d, NoLookup(ticks))
dim_attr = if d isa X
Attributes(; axis=(xticks=ticks, xtickformat=I -> map(string, parent(l)[map(Int, I)])))
elseif d isa Y
Attributes(; axis=(yticks=ticks, ytickformat=I -> map(string, parent(l)[map(Int, I)])))
else
Attributes(; axis=(zticks=ticks, ztickformat=I -> map(string, parent(l)[map(Int, I)])))
end
merge(attr, dim_attr), (ds..., int_dim)
else
attr, (ds..., d)
end
end
end
_split_attributes(dim::Dimension) = _split_attributes((dim,))

_prepare_for_makie(A, dims=()) = _permute(A, dims) |> _reorder |> _makie_eltype

_permute(A, replacements::Pair) = _permute(A, (replacements,))
_permute(A, replacements::Tuple{<:Pair,Vararg{<:Pair}}) =
_permute1(A, map(p -> basetypeof(key2dim(p[1]))(basetypeof(key2dim(p[2]))()), replacements))
function _permute(A::AbstractDimArray{<:Any,N}, replacements::Tuple) where N
xyz_dims = (X(), Y(), Z())[1:N]
all_replacements = _get_replacement_dims(A, replacements)
A_replaced_dims = set(A, all_replacements...)
# Permute to X/Y/Z order
permutedims(A_replaced_dims, xyz_dims)
end

_restore_dim_names(A2, A1, replacements::Pair) = _restore_dim_names(A2, A1, (replacements,))
_restore_dim_names(A2, A1, replacements::Tuple{<:Pair,Vararg{<:Pair}}) =
_restore_dim_names(A2, A1, map(p -> basetypeof(key2dim(p[1]))(basetypeof(key2dim(p[2]))()), replacements))
function _restore_dim_names(A2, A1, replacements::Tuple=())
all_replacements = _get_replacement_dims(A1, replacements)
# Invert our replacement dimensions - `set` sets the outer wrapper
# dimension to the inner/wrapped dimension
inverted_replacements = map(all_replacements) do r
basetypeof(val(r))(basetypeof(r)())
end
@show replacements inverted_replacements
# Set the dimensions back to the originals now they are in the right order
return set(A2, inverted_replacements...)
end

function _get_replacement_dims(A::AbstractDimArray{<:Any,N}, replacements::Tuple) where N
xyz_dims = (X(), Y(), Z())[1:N]
# Make sure destinations are X/Y/Z only
dest_dims = map(replacements) do d
d1 = basetypeof(val(d))()
d1 in xyz_dims || throw(ArgumentError("`dims` destinations must be in $(map(basetypeof, xyz_dims))"))
d1
end
replaced_dims = set(basedims(A), replacements...)
# Find remaining dims that are not X/Y/Z
other_source_dims = otherdims(replaced_dims, xyz_dims)
# Assign dest dims from whatever X/Y/Z remain
other_dest_dims = otherdims(xyz_dims, replaced_dims)
# Define the missing replacements
other_replacements = map(rebuild, other_source_dims, other_dest_dims)
return (replacements..., other_replacements...)
end

_reorder(A) = reorder(A, DD.ForwardOrdered)
_makie_eltype(A) = _missing_or_float32.(A)

_missing_or_float32(num::Number) = Float32(num)
_mirssing_or_float32(::Missing) = missing

end
1 change: 0 additions & 1 deletion src/DimensionalData.jl
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import Adapt,
Extents,
InvertedIndices,
IteratorInterfaceExtensions,
MakieCore,
RecipesBase,
PrecompileTools,
TableTraits,
Expand Down
1 change: 1 addition & 0 deletions src/Dimensions/set.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ _set(dims_::DimTuple, args::Dimension...; kw...) = _set(dims_, (args..., kwdims(
# Convert pairs to wrapped dims and set
_set(dims_::DimTuple, p::Pair, ps::Vararg{Pair}) = _set(dims_, (p, ps...))
_set(dims_::DimTuple, ps::Tuple{Vararg{Pair}}) = _set(dims_, pairdims(ps...))
_set(dims_::DimTuple, ::Tuple{}) = dims_
# Set dims with (possibly unsorted) wrapper vals
_set(dims::DimTuple, wrappers::DimTuple) = begin
# Check the dimension types match
Expand Down
Loading

0 comments on commit 4da0ee3

Please sign in to comment.