diff --git a/Project.toml b/Project.toml index 11f46b0ee..10a187050 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "DimensionalData" uuid = "0703355e-b756-11e9-17c0-8b28908087d0" authors = ["Rafael Schouten "] -version = "0.27.9" +version = "0.27.10" [deps] Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" @@ -57,7 +57,7 @@ InvertedIndices = "1" IteratorInterfaceExtensions = "1" JLArrays = "0.1" LinearAlgebra = "1" -Makie = "0.19, 0.20, 0.21" +Makie = "0.20, 0.21" OffsetArrays = "1" Plots = "1" PrecompileTools = "1" diff --git a/README.md b/README.md index 0c65b09f7..7acf43c56 100644 --- a/README.md +++ b/README.md @@ -15,21 +15,22 @@ DimensionalData.jl provides tools and abstractions for working with datasets tha DimensionalData is a pluggable, generalised version of [AxisArrays.jl](https://github.com/JuliaArrays/AxisArrays.jl) with a cleaner syntax, and additional functionality found in NamedDims.jl. It has similar goals to pythons [xarray](http://xarray.pydata.org/en/stable/), and is primarily written for use with spatial data in [Rasters.jl](https://github.com/rafaqz/Rasters.jl). -> [!IMPORTANT] -> INSTALLATION +## Installation ```shell julia>] pkg> add DimensionalData ``` +## Quick start + Start using the package: ```julia using DimensionalData ``` -The basic syntax is: +The basic syntax to create a dimensional array (`DimArray`) is: ```julia A = DimArray(rand(50, 31), (X(), Y(10.0:40.0))); @@ -76,7 +77,7 @@ A[Y=1:10, X=1] 19.0 0.605331 ``` -One can also subset by lookup, using a `Selector`, lets try `At`: +One can also subset by lookup, using a `Selector`, let's try `At`: ```julia A[Y(At(25))] @@ -125,8 +126,7 @@ using DimensionalData using DimensionalData.Lookup, DimensionalData.Dimensions ``` -> [!IMPORTANT] -> Alternative Packages +## Alternative packages There are a lot of similar Julia packages in this space. AxisArrays.jl, NamedDims.jl, NamedArrays.jl are registered alternative that each cover some of the functionality provided by DimensionalData.jl. DimensionalData.jl should be able to replicate most of their syntax and functionality. diff --git a/ext/DimensionalDataMakie.jl b/ext/DimensionalDataMakie.jl index 64c5d283d..ffbdc7238 100644 --- a/ext/DimensionalDataMakie.jl +++ b/ext/DimensionalDataMakie.jl @@ -7,12 +7,8 @@ using DimensionalData.Dimensions, DimensionalData.LookupArrays const DD = DimensionalData -# Handle changes between Makie 0.19 and 0.20 -const SurfaceLikeCompat = isdefined(Makie, :SurfaceLike) ? Makie.SurfaceLike : Union{Makie.VertexGrid,Makie.CellGrid,Makie.ImageLike} - _paired(args...) = map(x -> x isa Pair ? x : x => x, args) - # Shared docstrings: keep things consistent. const AXISLEGENDKW_DOC = """ @@ -138,23 +134,31 @@ for (f1, f2) in _paired(:plot => :heatmap, :heatmap, :image, :contour, :contourf ) where T replacements = _keywords2dimpairs(x, y) A1, A2, args, merged_attributes = _surface2(A, $f2, attributes, replacements) - p = if $(f1 == :surface) + axis_type = if haskey(merged_attributes, :axis) && haskey(merged_attributes.axis, :type) + to_value(type) + else + Makie.args_preferred_axis(Makie.Plot{$f2}, args...) + end + + p = if axis_type isa Type && axis_type <: Union{LScene, Makie.PolarAxis} # surface is an LScene so we cant pass attributes p = Makie.$f2(args...; attributes...) # And instead set axisnames manually - if !isnothing(p.axis.scene[OldAxis]) + if p.axis isa LScene && !isnothing(p.axis.scene[OldAxis]) p.axis.scene[OldAxis][:names, :axisnames] = map(DD.label, DD.dims(A2)) end p - else + else # axis_type isa Nothing, axis_type isa Makie.Axis or GeoAxis or similar Makie.$f2(args...; merged_attributes...) end # Add a Colorbar for heatmaps and contourf + # TODO: why not surface too? if T isa Real && $(f1 in (:plot, :heatmap, :contourf)) Colorbar(p.figure[1, 2], p.plot; label=DD.label(A), colorbarkw... ) end + p return p end function Makie.$f1!(axis, A::AbstractDimMatrix; @@ -162,7 +166,7 @@ for (f1, f2) in _paired(:plot => :heatmap, :heatmap, :image, :contour, :contourf ) replacements = _keywords2dimpairs(x, y) _, _, args, _ = _surface2(A, $f2, attributes, replacements) - # No ColourBar in the ! in-place versions + # No Colorbar in the ! in-place versions return Makie.$f2!(axis, args...; attributes...) end function Makie.$f1!(axis, A::Observable{<:AbstractDimMatrix}; @@ -182,9 +186,10 @@ function _surface2(A, plotfunc, attributes, replacements) lookup_attributes, newdims = _split_attributes(A1) A2 = _restore_dim_names(set(A1, map(Pair, newdims, newdims)...), A, replacements) P = Plot{plotfunc} - args = Makie.convert_arguments(P, A2) - # PTrait = Makie.conversion_trait(P, A2) - # status = Makie.got_converted(P, PTrait, converted) + PTrait = Makie.conversion_trait(P, A2) + # We define conversions by trait for all of the explicitly overridden functions, + # so we can just use the trait here. + args = Makie.convert_arguments(PTrait, A2) # if status === true # args = converted @@ -371,6 +376,7 @@ Makie.plottype(::AbstractDimArray{<:Any,3}) = Makie.Volume # Makie.expand_dimensions(::Makie.PointBased, y::IntervalSets.AbstractInterval) = (keys(y), y) # Conversions +# Generic conversion for arbitrary recipes that don't define a conversion trait function Makie.convert_arguments(t::Type{<:Makie.AbstractPlot}, A::AbstractDimMatrix) A1 = _prepare_for_makie(A) tr = Makie.conversion_trait(t, A) @@ -381,6 +387,7 @@ function Makie.convert_arguments(t::Type{<:Makie.AbstractPlot}, A::AbstractDimMa end return xs, ys, last(Makie.convert_arguments(t, parent(A1))) end +# PointBased conversions (scatter, lines, poly, etc) function Makie.convert_arguments(t::Makie.PointBased, A::AbstractDimVector) A1 = _prepare_for_makie(A) xs = parent(lookup(A, 1)) @@ -389,18 +396,31 @@ end function Makie.convert_arguments(t::Makie.PointBased, A::AbstractDimMatrix) return Makie.convert_arguments(t, parent(A)) end -function Makie.convert_arguments(t::SurfaceLikeCompat, A::AbstractDimMatrix) +# Grid based conversions (surface, image, heatmap, contour, meshimage, etc) + +# VertexGrid is for e.g. contour and surface, it uses a position per vertex. +function Makie.convert_arguments(t::Makie.VertexGrid, A::AbstractDimMatrix) A1 = _prepare_for_makie(A) - xs, ys = map(_lookup_to_vector, lookup(A1)) - # the following will not work for irregular spacings, we'll need to add a check for this. + # If the lookup is intervals, use the midpoint of each interval + # as the sampling point. + # If the lookup is points, just use the points. + xs, ys = map(_lookup_to_vertex_vector, lookup(A1)) return xs, ys, last(Makie.convert_arguments(t, parent(A1))) end +# ImageLike is for e.g. image, meshimage, etc. It uses an interval based sampling method so requires regular spacing. function Makie.convert_arguments(t::Makie.ImageLike, A::AbstractDimMatrix) A1 = _prepare_for_makie(A) - xs, ys = map(_lookup_to_interval, lookup(A)) - # the following will not work for irregular spacings, we'll need to add a check for this. + xlookup, ylookup, = lookup(A1) # take the first two dimensions only + # We need to make sure the lookups are regular intervals. + _check_regular_or_categorical_sampling(xlookup; axis = :x) + _check_regular_or_categorical_sampling(ylookup; axis = :y) + # Convert the lookups to intervals (<: Makie.EndPoints). + xs, ys = map(_lookup_to_interval, (xlookup, ylookup)) return xs, ys, last(Makie.convert_arguments(t, parent(A1))) end + +# CellGrid is for e.g. heatmap, contourf, etc. It uses vertices as corners of cells, so +# there have to be n+1 vertices for n cells on an axis. function Makie.convert_arguments( t::Makie.CellGrid, A::AbstractDimMatrix ) @@ -408,10 +428,15 @@ function Makie.convert_arguments( xs, ys = map(_lookup_to_vector, lookup(A1)) return xs, ys, last(Makie.convert_arguments(t, parent(A1))) end + +# VolumeLike is for e.g. volume, volumeslices, etc. It uses a regular grid. function Makie.convert_arguments(t::Makie.VolumeLike, A::AbstractDimArray{<:Any,3}) A1 = _prepare_for_makie(A) - xs, ys, zs = map(_lookup_to_interval, lookup(A1)) - # the following will not work for irregular spacings + xl, yl, zl = lookup(A1) + _check_regular_or_categorical_sampling(xl; axis = :x) + _check_regular_or_categorical_sampling(yl; axis = :y) + _check_regular_or_categorical_sampling(zl; axis = :z) + xs, ys, zs = map(_lookup_to_interval, (xl, yl, zl)) return xs, ys, zs, last(Makie.convert_arguments(t, parent(A1))) end @@ -423,7 +448,7 @@ function Makie.convert_arguments(t::Type{Plot{Makie.volumeslices}}, A::AbstractD 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" + @warn "Conversion trait $t not implemented for `AbstractDimArray` with $N dims, falling back to parent array type" return Makie.convert_arguments(t, parent(A)) end @@ -432,9 +457,11 @@ end # These can just forward to the relevant converts. Makie.expand_dimensions(t::Makie.PointBased, A::AbstractDimVector) = Makie.convert_arguments(t, A) Makie.expand_dimensions(t::Makie.PointBased, A::AbstractDimMatrix) = Makie.convert_arguments(t, A) - Makie.expand_dimensions(t::SurfaceLikeCompat, A::AbstractDimMatrix) = Makie.convert_arguments(t, A) + Makie.expand_dimensions(t::Makie.VertexGrid, A::AbstractDimMatrix) = Makie.convert_arguments(t, A) + Makie.expand_dimensions(t::Makie.ImageLike, A::AbstractDimMatrix) = Makie.convert_arguments(t, A) Makie.expand_dimensions(t::Makie.CellGrid, A::AbstractDimMatrix) = Makie.convert_arguments(t, A) - Makie.expand_dimensions(t::Makie.VolumeLike, A::AbstractDimArray{<:Any,3}) = Makie.convert_arguments(t, A) + Makie.expand_dimensions(t::Makie.VolumeLike, A::AbstractDimArray{<:Any,3}) = Makie.convert_arguments(t, A) + Makie.expand_dimensions(t::Type{Plot{Makie.volumeslices}}, A::AbstractDimArray{<:Any,3}) = Makie.convert_arguments(t, A) end # Utility methods @@ -464,6 +491,19 @@ function _categorical_or_dependent(A, ::Nothing) end end +# Check for regular sampling on a lookup, throw an error if not. +# Here, we assume +function _check_regular_or_categorical_sampling(l; axis = nothing) + if !(DD.isregular(l) || DD.iscategorical(l)) + throw(ArgumentError(""" + DimensionalDataMakie: The $(isnothing(axis) ? "" : "$axis-axis ")lookup is not regularly spaced, which is required for image-like plot types in Makie. + The lookup was: + $l + + You can solve this by resampling your raster, or by using a more permissive plot type like `heatmap`, `surface`, `contour`, or `contourf`. + """)) + end +end # Simplify dimension lookups and move information to axis attributes _split_attributes(A) = _split_attributes(dims(A)) @@ -561,7 +601,18 @@ function _lookup_to_vector(l) end end +function _lookup_to_vertex_vector(l) + if isintervals(l) + bs = intervalbounds(l) + return @. first(bs) + (last(bs) - first(bs)) / 2 + else # ispoints + return collect(parent(l)) + end +end + function _lookup_to_interval(l) + # TODO: warn or error if not regular sampling. + # Maybe use Preferences.jl to determine if we should error or warn. l1 = if isnolookup(l) Sampled(parent(l); order=ForwardOrdered(), sampling=Intervals(Center()), span=Regular(1)) elseif ispoints(l)