diff --git a/REQUIRE b/REQUIRE index 9a07200..36105a0 100644 --- a/REQUIRE +++ b/REQUIRE @@ -2,4 +2,5 @@ julia 0.7 IntervalSets 0.1 IterTools RangeArrays +RecipesBase Compat 0.61.0 diff --git a/docs/src/index.md b/docs/src/index.md index f9e1d80..2ca272e 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -78,8 +78,21 @@ And data, a 2400001×2 Array{Float64,2}: ``` -AxisArrays behave like regular arrays, but they additionally use the axis -information to enable all sorts of fancy behaviors. For example, we can specify +AxisArrays behave like regular arrays, but they carry extra information about +their axes along with them: + +```jldoctest +julia> A.time +0.0 s:2.5e-5 s:60.0 s + +julia> A.chan +2-element Array{Symbol,1}: + :c1 + :c2 + +``` + +This enables all sorts of fancy indexing behaviors. For example, we can specify indices in *any* order, just so long as we annotate them with the axis name: ```jldoctest diff --git a/src/AxisArrays.jl b/src/AxisArrays.jl index f90db54..2d0d8bc 100644 --- a/src/AxisArrays.jl +++ b/src/AxisArrays.jl @@ -9,7 +9,7 @@ using IterTools using Compat using Dates -function axes end +@deprecate axes _axes export AxisArray, Axis, axisnames, axisvalues, axisdim, axes, atindex, atvalue, collapse @@ -23,5 +23,6 @@ include("indexing.jl") include("sortedvector.jl") include("categoricalvector.jl") include("combine.jl") +include("recipes.jl") end diff --git a/src/core.jl b/src/core.jl index 324b458..81cb88d 100644 --- a/src/core.jl +++ b/src/core.jl @@ -59,32 +59,40 @@ A[Axis{2}(2:5)] # grabs the second through 5th columns ``` """ -struct Axis{name,T} - val::T +struct Axis{name,A,T,N} <: AbstractArray{T,N} + val::A end # Constructed exclusively through Axis{:symbol}(...) or Axis{1}(...) -Axis{name}(I::T=()) where {name,T} = Axis{name,T}(I) +Axis{name}(I::AbstractArray{T,N}) where {name,T,N} = Axis{name,typeof(I),T,N}(I) +# Constructing with a scalar gives a 0-dimensional Axis +Axis{name}(I::T=()) where {name,T} = Axis{name,T,T,0}(I) + +const ArrayAxis = Axis{<:Any,<:AbstractArray} + Base.:(==)(A::Axis{name}, B::Axis{name}) where {name} = A.val == B.val Base.hash(A::Axis{name}, hx::UInt) where {name} = hash(A.val, hash(name, hx)) axistype(::Axis{name,T}) where {name,T} = T axistype(::Type{Axis{name,T}}) where {name,T} = T -# Pass indexing and related functions straight through to the wrapped value -# TODO: should Axis be an AbstractArray? AbstractArray{T,0} for scalar T? -Base.getindex(A::Axis, i...) = A.val[i...] -Base.eltype(::Type{Axis{name,T}}) where {name,T} = eltype(T) -Base.size(A::Axis) = size(A.val) -VERSION < v"0.7.0" && (Base.endof(A::Axis) = length(A)) -Base.lastindex(A::Axis) = length(A) -Base.axes(A::Axis) = Base.axes(A.val) -Base.axes(A::Axis, d) = Base.axes(A.val, d) -Base.length(A::Axis) = length(A.val) +Base.getindex(ax::ArrayAxis, i) = ax.val[i] +Base.size(ax::ArrayAxis) = size(ax.val) +Base.getindex(ax::Axis) = ax.val +Base.size(ax::Axis) = () (A::Axis{name})(i) where {name} = Axis{name}(i) Base.convert(::Type{Axis{name,T}}, ax::Axis{name,T}) where {name,T} = ax Base.convert(::Type{Axis{name,T}}, ax::Axis{name}) where {name,T} = Axis{name}(convert(T, ax.val)) -Base.iterate(a::Axis) = (a, nothing) -Base.iterate(::Axis, ::Any) = nothing -Base.iterate(::Type{T}) where {T<:Axis} = (T, nothing) -Base.iterate(::Type{T}, ::Any) where {T<:Axis} = nothing +Base.show(io::IO, ax::Axis{name}) where {name} = print(io, "Axis{", name, "}(", ax.val, ")") + +struct AxisUnitRange{T,R<:AbstractUnitRange{T},A<:Axis} <: AbstractUnitRange{T} + range::R + axis::A +end +Base.getindex(r::AxisUnitRange, i::Integer) = getindex(r.range, i) +Base.length(r::AxisUnitRange) = length(r.range) +Base.first(r::AxisUnitRange) = first(r.range) +Base.last(r::AxisUnitRange) = last(r.range) +Base.iterate(r::AxisUnitRange,s...) = iterate(r.range, s...) + +Base.show(io::IO, r::AxisUnitRange) = print(io, "AxisUnitRange(", r.range, ", ", r.axis, ")") """ An AxisArray is an AbstractArray that wraps another AbstractArray and @@ -234,9 +242,9 @@ end AxisArray(A::AxisArray) = A AxisArray(A::AxisArray, ax::Vararg{Axis, N}) where N = - AxisArray(A.data, ax..., last(Base.IteratorsMD.split(axes(A), Val(N)))...) + AxisArray(A.data, ax..., last(Base.IteratorsMD.split(_axes(A), Val(N)))...) AxisArray(A::AxisArray, ax::NTuple{N, Axis}) where N = - AxisArray(A.data, ax..., last(Base.IteratorsMD.split(axes(A), Val(N)))...) + AxisArray(A.data, ax..., last(Base.IteratorsMD.split(_axes(A), Val(N)))...) # Traits struct HasAxes{B} end @@ -285,11 +293,25 @@ function axisdim(::Type{AxisArray{T,N,D,Ax}}, ::Type{<:Axis{name,S} where S}) wh idx end +# Access to the values of axes by name +function Base.getproperty(A::AxisArray, name::Symbol) + if name === :data || name === :axes + getfield(A, name) + else + # Other things are axis names + getfield(A, :axes)[axisdim(A, Axis{name})] + end +end +function Base.propertynames(A::AxisArray, private=false) + ns = axisnames(A) + private ? (ns..., :data, :axes) : ns +end + # Base definitions that aren't provided by AbstractArray @inline Base.size(A::AxisArray) = size(A.data) @inline Base.size(A::AxisArray, Ax::Axis) = size(A.data, axisdim(A, Ax)) @inline Base.size(A::AxisArray, ::Type{Ax}) where {Ax<:Axis} = size(A.data, axisdim(A, Ax)) -@inline Base.axes(A::AxisArray) = Base.axes(A.data) +@inline Base.axes(A::AxisArray) = map((range,ax)->AxisUnitRange(range,ax), Base.axes(A.data), A.axes) @inline Base.axes(A::AxisArray, Ax::Axis) = Base.axes(A.data, axisdim(A, Ax)) @inline Base.axes(A::AxisArray, ::Type{Ax}) where {Ax<:Axis} = Base.axes(A.data, axisdim(A, Ax)) Base.convert(::Type{Array{T,N}}, A::AxisArray{T,N}) where {T,N} = convert(Array{T,N}, A.data) @@ -309,7 +331,7 @@ Base.similar(A::AxisArray, ::Type{S}, ax1::Axis, axs::Axis...) where {S} = simil ax = Expr(:tuple) for d=1:N push!(inds.args, :(Base.axes(A, Axis{$d}))) - push!(ax.args, :(axes(A, Axis{$d}))) + push!(ax.args, :(_axes(A, Axis{$d}))) end to_delete = Int[] for i=1:length(axs.parameters) @@ -327,16 +349,16 @@ Base.similar(A::AxisArray, ::Type{S}, ax1::Axis, axs::Axis...) where {S} = simil AxisArray(d, $ax) end end -const AxisUnitRange{T,N,D<:AbstractUnitRange,Ax} = AxisArray{T,N,D,Ax} -Base.similar(A::AxisArray{T}, ax1::AxisUnitRange, axs::AxisUnitRange...) where {T} = similar(A, T, (ax1, axs...)) -Base.similar(A::AxisArray, ::Type{S}, ax1::AxisUnitRange, axs::AxisUnitRange...) where {S} = similar(A, S, (ax1, axs...)) -Base.similar(A::AxisArray, ::Type{S}, axs::Tuple{AxisUnitRange,Vararg{AxisUnitRange}}) where {S} = similar(A, S, map(x->x.axes[1], axs)) +const AxisUnitRangeLike = Union{AxisArray{<:Any,<:Any,<:AbstractUnitRange,<:Any}, AxisUnitRange} +Base.similar(A::AxisArray{T}, ax1::AxisUnitRangeLike, axs::AxisUnitRangeLike...) where {T} = similar(A, T, (ax1, axs...)) +Base.similar(A::AxisArray, ::Type{S}, ax1::AxisUnitRangeLike, axs::AxisUnitRangeLike...) where {S} = similar(A, S, (ax1, axs...)) +Base.similar(A::AxisArray, ::Type{S}, axs::Tuple{AxisUnitRangeLike,Vararg{AxisUnitRangeLike}}) where {S} = similar(A, S, map(x->x.axes[1], axs)) # These methods allow us to preserve the AxisArray under reductions # Note that we only extend the following two methods, and then have it # dispatch to package-local `reduced_indices` and `reduced_indices0` # methods. This avoids a whole slew of ambiguities. -Base.reduced_indices(A::AxisArray, region) = map(ax->AxisArray(Base.axes(ax.val, 1), ax), reduced_indices(axes(A), region)) -Base.reduced_indices0(A::AxisArray, region) = map(ax->AxisArray(Base.axes(ax.val, 1), ax), reduced_indices0(axes(A), region)) +Base.reduced_indices(A::AxisArray, region) = map(ax->AxisArray(Base.axes(ax.val, 1), ax), reduced_indices(_axes(A), region)) +Base.reduced_indices0(A::AxisArray, region) = map(ax->AxisArray(Base.axes(ax.val, 1), ax), reduced_indices0(_axes(A), region)) reduced_indices(axs::Tuple{Vararg{Axis}}, ::Tuple{}) = axs reduced_indices0(axs::Tuple{Vararg{Axis}}, ::Tuple{}) = axs @@ -394,7 +416,7 @@ reduced_axis0(ax) = ax(length(ax.val) == 0 ? Base.OneTo(0) : Base.OneTo(1)) function Base.permutedims(A::AxisArray, perm) p = permutation(perm, axisnames(A)) - AxisArray(permutedims(A.data, p), axes(A)[[p...]]) + AxisArray(permutedims(A.data, p), _axes(A)[[p...]]) end Base.transpose(A::AxisArray{T,2}) where {T} = AxisArray(transpose(A.data), A.axes[2], A.axes[1]) @@ -459,12 +481,12 @@ end function _dropdims(A::AxisArray, dims) keepdims = setdiff(1:ndims(A), dims) - AxisArray(dropdims(A.data; dims=dims), axes(A)[keepdims]) + AxisArray(dropdims(A.data; dims=dims), _axes(A)[keepdims]) end # This version attempts to be type-stable function _dropdims(A::AxisArray, ::Type{Ax}) where {Ax<:Axis} dim = axisdim(A, Ax) - AxisArray(dropdims(A.data; dims=dim), dropax(Ax, axes(A)...)) + AxisArray(dropdims(A.data; dims=dim), dropax(Ax, _axes(A)...)) end @inline dropax(ax, ax1, axs...) = (ax1, dropax(ax, axs...)...) @@ -476,7 +498,7 @@ dropax(ax) = () # A simple display method to include axis information. It might be nice to # eventually display the axis labels alongside the data array, but that is # much more difficult. -function Base.summary(io::IO, A::AxisArray) +function Base.summary(io::IO, A::AxisArray, inds) _summary(io, A) for (name, val) in zip(axisnames(A), axisvalues(A)) print(io, " :$name, ") @@ -499,27 +521,30 @@ axisvalues() = () axisvalues(ax::Axis, axs::Axis...) = tuple(ax.val, axisvalues(axs...)...) """ - axes(A::AxisArray) -> (Axis...) - axes(A::AxisArray, ax::Axis) -> Axis - axes(A::AxisArray, dim::Int) -> Axis + _axes(A::AxisArray) -> (Axis...) + _axes(A::AxisArray, ax::Axis) -> Axis + _axes(A::AxisArray, dim::Int) -> Axis Returns the tuple of axis vectors for an AxisArray. If an specific `Axis` is specified, then only that axis vector is returned. Note that when extracting a -single axis vector, `axes(A, Axis{1})`) is type-stable and will perform better -than `axes(A)[1]`. +single axis vector, `_axes(A, Axis{1})`) is type-stable and will perform better +than `_axes(A)[1]`. + +For an AbstractArray without `Axis` information, `_axes` returns the +default _axes, i.e., those that would be produced by `AxisArray(A)`. -For an AbstractArray without `Axis` information, `axes` returns the -default axes, i.e., those that would be produced by `AxisArray(A)`. +Note that this is deprecated because AxisArrays is transitioning to a function +of the same name, now exported by `Base`. """ -axes(A::AxisArray) = A.axes -axes(A::AxisArray, dim::Int) = A.axes[dim] -axes(A::AxisArray, ax::Axis) = axes(A, typeof(ax)) -@generated function axes(A::AxisArray, ax::Type{T}) where T<:Axis +_axes(A::AxisArray) = A.axes +_axes(A::AxisArray, dim::Int) = A.axes[dim] +_axes(A::AxisArray, ax::Axis) = _axes(A, typeof(ax)) +@generated function _axes(A::AxisArray, ax::Type{T}) where T<:Axis dim = axisdim(A, T) :(A.axes[$dim]) end -axes(A::AbstractArray) = default_axes(A) -axes(A::AbstractArray, dim::Int) = default_axes(A)[dim] +_axes(A::AbstractArray) = default_axes(A) +_axes(A::AbstractArray, dim::Int) = default_axes(A)[dim] """ axisparams(::AxisArray) -> Vararg{::Type{Axis}} diff --git a/src/indexing.jl b/src/indexing.jl index 0db70b6..c930ffe 100644 --- a/src/indexing.jl +++ b/src/indexing.jl @@ -48,7 +48,7 @@ Base.eachindex(A::AxisArray) = eachindex(A.data) This internal function determines the new set of axes that are constructed upon indexing with I. """ -reaxis(A::AxisArray, I::Idx...) = _reaxis(make_axes_match(axes(A), I), I) +reaxis(A::AxisArray, I::Idx...) = _reaxis(make_axes_match(_axes(A), I), I) # Linear indexing reaxis(A::AxisArray{<:Any,1}, I::AbstractArray{Int}) = _new_axes(A.axes[1], I) reaxis(A::AxisArray, I::AbstractArray{Int}) = default_axes(I) @@ -142,7 +142,7 @@ end end function Base.reshape(A::AxisArray, ::Val{N}) where N - axN, _ = Base.IteratorsMD.split(axes(A), Val(N)) + axN, _ = Base.IteratorsMD.split(_axes(A), Val(N)) AxisArray(reshape(A.data, Val(N)), Base.front(axN)) end @@ -355,7 +355,7 @@ end end ## Extracting the full axis (name + values) from the Axis{:name} type -@inline Base.getindex(A::AxisArray, ::Type{Ax}) where {Ax<:Axis} = getaxis(Ax, axes(A)...) +@inline Base.getindex(A::AxisArray, ::Type{Ax}) where {Ax<:Axis} = getaxis(Ax, _axes(A)...) @inline getaxis(::Type{Ax}, ax::Ax, axs...) where {Ax<:Axis} = ax @inline getaxis(::Type{Ax}, ax::Axis, axs...) where {Ax<:Axis} = getaxis(Ax, axs...) @noinline getaxis(::Type{Ax}) where {Ax<:Axis} = throw(ArgumentError("no axis of type $Ax was found")) diff --git a/src/recipes.jl b/src/recipes.jl new file mode 100644 index 0000000..541ca09 --- /dev/null +++ b/src/recipes.jl @@ -0,0 +1,26 @@ +using RecipesBase + +@recipe function plot(a::AxisArray) where {name} + ax1 = _axes(a,1) + xlabel --> axisname(ax1) + if ndims(a) == 1 + ax1.val, a.data + else + ax2 = _axes(a,2) + # Categorical axes print as a set of labelled series + if axistrait(ax2) === Categorical + for i in eachindex(ax2.val) + @series begin + label --> "$(axisname(ax2)) $(ax2.val[i])" + ax1.val, a.data[:,i] + end + end + else + # Other axes as a 2D array + ylabel --> axisname(ax2) + seriestype --> :heatmap + ax1.val, ax2.val, a.data + end + end +end + diff --git a/test/core.jl b/test/core.jl index 01d1b05..874ea5a 100644 --- a/test/core.jl +++ b/test/core.jl @@ -167,6 +167,18 @@ A = @inferred(AxisArray(reshape(1:24, 2,3,4), @test axisdim(A, Axis{:x}) == axisdim(A, Axis{:x}()) == 1 @test axisdim(A, Axis{:y}) == axisdim(A, Axis{:y}()) == 2 @test axisdim(A, Axis{:z}) == axisdim(A, Axis{:z}()) == 3 +# Test that getproperty is fully inferred when a const name is supplied +let getx(A) = A.x, + getz(A) = A.z, + getdata(A) = A.data + @test @inferred(getx(A)) == A.axes[1].val + @test @inferred(getz(A)) == A.axes[3].val + @test @inferred(AxisArrays.axes(A)) === A.axes + @test @inferred(getdata(A)) === A.data +end +@test propertynames(A) == (:x, :y, :z) +@test propertynames(A, true) == (:x, :y, :z, :data, :axes) + # Test axes @test @inferred(AxisArrays.axes(A)) == (Axis{:x}(.1:.1:.2), Axis{:y}(1//10:1//10:3//10), Axis{:z}(["a", "b", "c", "d"])) @test @inferred(AxisArrays.axes(A, Axis{:x})) == @inferred(AxisArrays.axes(A, Axis{:x}())) == Axis{:x}(.1:.1:.2)