Skip to content

[Draft] Add CF conventions in YAXArray to use in plots #501

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/src/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const navTemp = {
{ text: 'Create', link: '/UserGuide/create' },
{ text: 'Select', link: '/UserGuide/select' },
{ text: 'Compute', link: '/UserGuide/compute' },
{ text: 'Plot', link: '/UserGuide/plot' },
{ text: 'Chunk', link: '/UserGuide/chunk' },
{ text: 'Cache', link: '/UserGuide/cache' },
{ text: 'Group', link: '/UserGuide/group' },
Expand Down
72 changes: 72 additions & 0 deletions docs/src/UserGuide/plot.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Plot YAXArrays

This section describes how to visualize YAXArrays.
See also the [Plotting maps tutorial](/tutorials/plottingmaps.html) to plot geospatial data.
All [plotting capabilities](https://rafaqz.github.io/DimisensionalData.jl/dev/plots) of `AbstractDimArray` apply to a `YAXArrays` as well, because every `YAXArray` is also an `AbstractDimArray`.

## Plot a YAXArrray

Create a simple YAXArray:

```@example plot
using CairoMakie
using YAXArrays
using DimensionalData

data = collect(reshape(1:20, 4, 5))
axlist = (X(1:4), Y(1:5))
a = YAXArray(axlist, data)
```

Plot the entire array:

```@example plot
plot(a)
```

This will plot a heatmap, because the array is a matrix.

Plot the first column:

```@example plot
plot(a[Y=1])
```

This results in a scatter plot, because the subarray is a vector.

## Plot a YAXArrray with CF conventions

[Climate and Forecast Metadata Conventions](https://cfconventions.org/) are used to generate appropriate labels for the plot whenever possible.
This requires the YAXArray to have metadata properties like `standard_name` and `units`.

Get a `Dataset` with CF meta data:

```@example plot
using NetCDF
using Downloads: download

path = download("https://www.unidata.ucar.edu/software/netcdf/examples/tos_O1_2001-2002.nc", "example.nc")
ds = open_dataset(path)
```

Plot the first time step of the sea surface temperature with CF metadata:

```@example plot
plot(ds.tos[time=1])
```

Time in [Climate and Forecasting (CF) conventions](https://cfconventions.org/Data/cf-conventions/cf-conventions-1.12/cf-conventions.html#time-coordinate-units) requires conversion before plotting, e.g., to plot the sea surface temperature over time at a given location (e.g. the null island):

```@example plot
a = ds.tos[lon = Near(0), lat = Near(0)]
times = Ti(map(x -> DateTime(string(x)), a.time.val))
a = YAXArray((times,), collect(a.data), a.properties)

plot(a)
```

Or as as an explicit line plot:

```@example plot
lines(a)
```
144 changes: 81 additions & 63 deletions src/Cubes/Cubes.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
using DocStringExtensions
using Tables: istable, schema, columns
using DimensionalData: DimensionalData as DD, AbstractDimArray, NoName
import DimensionalData: name
import DimensionalData: name, label

export concatenatecubes, caxes, subsetcube, readcubedata, renameaxis!, YAXArray, setchunks, cache

Expand Down Expand Up @@ -91,7 +91,7 @@

$(FIELDS)
"""
struct YAXArray{T,N,A<:AbstractArray{T,N}, D, Me} <: AbstractDimArray{T,N,D,A}
struct YAXArray{T,N,A<:AbstractArray{T,N},D,Me} <: AbstractDimArray{T,N,D,A}
"`Tuple` of Dimensions containing the Axes of the Cube"
axes::D
"length(axes)-dimensional array which holds the data, this can be a lazy DiskArray"
Expand Down Expand Up @@ -120,7 +120,7 @@
throw(ArgumentError("Can not construct YAXArray, supplied chunk dimension is $(ndims(chunks)) while the number of dims is $(length(axes))"))
else
axes = DD.format(axes, data)
return new{eltype(data),ndims(data),typeof(data),typeof(axes), typeof(properties)}(
return new{eltype(data),ndims(data),typeof(data),typeof(axes),typeof(properties)}(
axes,
data,
properties,
Expand All @@ -131,16 +131,34 @@
end
end

name(::YAXArray) = NoName()

YAXArray(axes, data, properties = Dict{String,Any}(); cleaner = CleanMe[], chunks = eachchunk(data)) =
"Name of an YAXArray using CF conventions. Searches first for metadata `long_name`, followed by `standard_name` and finally `name`"
function name(a::YAXArray)
# as implemented in python xarray
isempty(a.properties) && return NoName()
haskey(a.properties, "long_name") && return a.properties["long_name"] |> Symbol
haskey(a.properties, "standard_name") && return a.properties["standard_name"] |> Symbol
haskey(a.properties, "name") && return a.properties["name"] |> Symbol
return NoName()
end

"Label of an YAXArrray using CF conventions. Includes the name and the unit, if present. Searches first for metadata `units` followed by `unit``"
function label(a::YAXArray)
# as implemented in python xarray
isempty(a.properties) && return ""
haskey(a.properties, "units") && return "$(name(a)) [$(a.properties["units"])]"
haskey(a.properties, "unit") && return "$(name(a)) [$(a.properties["unit"])]"
return string(name(a))
end

YAXArray(axes, data, properties=Dict{String,Any}(); cleaner=CleanMe[], chunks=eachchunk(data)) =
YAXArray(axes, data, properties, chunks, cleaner)
YAXArray(axes,data,properties,cleaner) = YAXArray(axes,data,properties,eachchunk(data),cleaner)
YAXArray(axes, data, properties, cleaner) = YAXArray(axes, data, properties, eachchunk(data), cleaner)

Check warning on line 156 in src/Cubes/Cubes.jl

View check run for this annotation

Codecov / codecov/patch

src/Cubes/Cubes.jl#L156

Added line #L156 was not covered by tests
function YAXArray(x::AbstractArray)
ax = caxes(x)
props = getattributes(x)
chunks = eachchunk(x)
YAXArray(ax, x, props,chunks=chunks)
YAXArray(ax, x, props, chunks=chunks)
end


Expand Down Expand Up @@ -172,20 +190,20 @@
Base.Generator(f, A::YAXArray) = Base.Generator(f, parent(A))
Base.ndims(a::YAXArray{<:Any,N}) where {N} = N
Base.eltype(a::YAXArray{T}) where {T} = T
function Base.permutedims(c::YAXArray, p)
function Base.permutedims(c::YAXArray, p)
newdims = DD.sortdims(DD.dims(c), Tuple(p))
dimnums = map(d -> DD.dimnum(c, d), p)
newdata = permutedims(getdata(c), dimnums)
newchunks = DiskArrays.GridChunks(eachchunk(c).chunks[collect(dimnums)])
YAXArray(newdims, newdata, c.properties, newchunks, c.cleaner)
end
DiskArrays.cache(a::YAXArray;maxsize=1000) = DD.rebuild(a,cache(a.data;maxsize))
DiskArrays.cache(a::YAXArray; maxsize=1000) = DD.rebuild(a, cache(a.data; maxsize))

# DimensionalData overloads

DD.dims(x::YAXArray) = getfield(x,:axes)
DD.dims(x::YAXArray) = getfield(x, :axes)
DD.refdims(::YAXArray) = ()
DD.metadata(x::YAXArray) = getfield(x,:properties)
DD.metadata(x::YAXArray) = getfield(x, :properties)

function DD.rebuild(A::YAXArray, data::AbstractArray, dims::Tuple, refdims::Tuple, name, metadata)
#chunks = map(dims, eachchunk(data).chunks) do d, chunk
Expand All @@ -205,14 +223,14 @@
end

function caxes(x)
#@show x
#@show typeof(x)
dims = map(enumerate(dimnames(x))) do a
index, symbol = a
values = YAXArrayBase.dimvals(x, index)
DD.Dim{symbol}(values)
end
(dims... ,)
#@show x
#@show typeof(x)
dims = map(enumerate(dimnames(x))) do a
index, symbol = a
values = YAXArrayBase.dimvals(x, index)
DD.Dim{symbol}(values)
end
(dims...,)
end

caxes(x::DD.AbstractDimArray) = collect(DD.dims(x))
Expand All @@ -238,18 +256,18 @@
YAXArray(caxes(x), getindex_all(x), getattributes(x))
end

interpret_cubechunks(cs::NTuple{N,Int},cube) where N = DiskArrays.GridChunks(getdata(cube),cs)
interpret_cubechunks(cs::DiskArrays.GridChunks,_) = cs
interpret_dimchunk(cs::Integer,s) = DiskArrays.RegularChunks(cs,0,s)
interpret_cubechunks(cs::NTuple{N,Int}, cube) where {N} = DiskArrays.GridChunks(getdata(cube), cs)
interpret_cubechunks(cs::DiskArrays.GridChunks, _) = cs

Check warning on line 260 in src/Cubes/Cubes.jl

View check run for this annotation

Codecov / codecov/patch

src/Cubes/Cubes.jl#L260

Added line #L260 was not covered by tests
interpret_dimchunk(cs::Integer, s) = DiskArrays.RegularChunks(cs, 0, s)
interpret_dimchunk(cs::DiskArrays.ChunkVector, _) = cs

function interpret_cubechunks(cs,cube)
function interpret_cubechunks(cs, cube)
oldchunks = DiskArrays.eachchunk(cube).chunks
for k in keys(cs)
i = findAxis(k,cube)
i = findAxis(k, cube)
if i !== nothing
dimchunk = interpret_dimchunk(cs[k],size(cube.data,i))
oldchunks = Base.setindex(oldchunks,dimchunk,i)
dimchunk = interpret_dimchunk(cs[k], size(cube.data, i))
oldchunks = Base.setindex(oldchunks, dimchunk, i)
end
end
GridChunks(oldchunks)
Expand All @@ -267,7 +285,7 @@
- an AbstractDict or NamedTuple mapping one or more axis names to chunk sizes

"""
setchunks(c::YAXArray,chunks) = YAXArray(c.axes,c.data,c.properties,interpret_cubechunks(chunks,c),c.cleaner)
setchunks(c::YAXArray, chunks) = YAXArray(c.axes, c.data, c.properties, interpret_cubechunks(chunks, c), c.cleaner)
cubechunks(c) = approx_chunksize(eachchunk(c))
DiskArrays.eachchunk(c::YAXArray) = c.chunks
getindex_all(a) = getindex(a, ntuple(_ -> Colon(), ndims(a))...).data
Expand All @@ -283,45 +301,45 @@
=#


function batchextract(x,i)
function batchextract(x, i)

Check warning on line 304 in src/Cubes/Cubes.jl

View check run for this annotation

Codecov / codecov/patch

src/Cubes/Cubes.jl#L304

Added line #L304 was not covered by tests
# This function should be documented and moved to DimensionalData
sch = schema(i)
axinds = map(sch.names) do n
findAxis(n,x)
findAxis(n, x)

Check warning on line 308 in src/Cubes/Cubes.jl

View check run for this annotation

Codecov / codecov/patch

src/Cubes/Cubes.jl#L308

Added line #L308 was not covered by tests
end
tcols = columns(i)
#Try to find a column denoting new axis name and values
newaxcol = nothing
if any(isnothing,axinds)
allnothings = findall(isnothing,axinds)

if any(isnothing, axinds)
allnothings = findall(isnothing, axinds)

Check warning on line 315 in src/Cubes/Cubes.jl

View check run for this annotation

Codecov / codecov/patch

src/Cubes/Cubes.jl#L314-L315

Added lines #L314 - L315 were not covered by tests
if length(allnothings) == 1
newaxcol = allnothings[1]
end
tcols = (;[p[1:2] for p in zip(keys(tcols), values(tcols), axinds) if !isnothing(last(p))]...)
axinds = filter(!isnothing,axinds)
tcols = (; [p[1:2] for p in zip(keys(tcols), values(tcols), axinds) if !isnothing(last(p))]...)
axinds = filter(!isnothing, axinds)

Check warning on line 320 in src/Cubes/Cubes.jl

View check run for this annotation

Codecov / codecov/patch

src/Cubes/Cubes.jl#L319-L320

Added lines #L319 - L320 were not covered by tests
end

allax = 1:ndims(x)
axrem = setdiff(allax,axinds)
axrem = setdiff(allax, axinds)

Check warning on line 324 in src/Cubes/Cubes.jl

View check run for this annotation

Codecov / codecov/patch

src/Cubes/Cubes.jl#L324

Added line #L324 was not covered by tests
ai1, ai2 = extrema(axinds)
if !all(diff(sort(collect(axinds))).==1)

if !all(diff(sort(collect(axinds))) .== 1)

Check warning on line 327 in src/Cubes/Cubes.jl

View check run for this annotation

Codecov / codecov/patch

src/Cubes/Cubes.jl#L327

Added line #L327 was not covered by tests
#Axes to be extracted from are not consecutive in cube -> permute
p = [1:(ai1-1);collect(axinds);filter(!in(axinds),ai1:ai2);(ai2+1:ndims(x))]
x_perm = permutedims(x,p)
return batchextract(x_perm,i)
p = [1:(ai1-1); collect(axinds); filter(!in(axinds), ai1:ai2); (ai2+1:ndims(x))]
x_perm = permutedims(x, p)
return batchextract(x_perm, i)

Check warning on line 331 in src/Cubes/Cubes.jl

View check run for this annotation

Codecov / codecov/patch

src/Cubes/Cubes.jl#L329-L331

Added lines #L329 - L331 were not covered by tests
end

cartinds = map(axinds,tcols) do iax,col
cartinds = map(axinds, tcols) do iax, col

Check warning on line 334 in src/Cubes/Cubes.jl

View check run for this annotation

Codecov / codecov/patch

src/Cubes/Cubes.jl#L334

Added line #L334 was not covered by tests
axcur = caxes(x)[iax]
map(col) do val
axVal2Index(axcur,val)
axVal2Index(axcur, val)

Check warning on line 337 in src/Cubes/Cubes.jl

View check run for this annotation

Codecov / codecov/patch

src/Cubes/Cubes.jl#L337

Added line #L337 was not covered by tests
end
end
before = ntuple(_->Colon(),ai1-1)
after = ntuple(_->Colon(),ndims(x)-ai2)

before = ntuple(_ -> Colon(), ai1 - 1)
after = ntuple(_ -> Colon(), ndims(x) - ai2)

Check warning on line 342 in src/Cubes/Cubes.jl

View check run for this annotation

Codecov / codecov/patch

src/Cubes/Cubes.jl#L341-L342

Added lines #L341 - L342 were not covered by tests
sp = issorted(axinds) ? nothing : sortperm(collect(axinds))
function makeindex(sp, inds...)
if sp === nothing
Expand All @@ -330,37 +348,37 @@
CartesianIndex(inds[sp]...)
end
end
indlist = makeindex.(Ref(sp),cartinds...)
d = getdata(x)[before...,indlist,after...]
indlist = makeindex.(Ref(sp), cartinds...)
d = getdata(x)[before..., indlist, after...]

Check warning on line 352 in src/Cubes/Cubes.jl

View check run for this annotation

Codecov / codecov/patch

src/Cubes/Cubes.jl#L351-L352

Added lines #L351 - L352 were not covered by tests
cax = caxes(x)
newax = if newaxcol == nothing
outaxis_from_data(cax,axinds,indlist)
outaxis_from_data(cax, axinds, indlist)

Check warning on line 355 in src/Cubes/Cubes.jl

View check run for this annotation

Codecov / codecov/patch

src/Cubes/Cubes.jl#L355

Added line #L355 was not covered by tests
else
outaxis_from_column(i,newaxcol)
outaxis_from_column(i, newaxcol)

Check warning on line 357 in src/Cubes/Cubes.jl

View check run for this annotation

Codecov / codecov/patch

src/Cubes/Cubes.jl#L357

Added line #L357 was not covered by tests
end
outax = Tuple([axcopy(a) for a in cax][axrem]...)
insert!(outax,minimum(axinds),newax)
YAXArray(outax,d,x.properties)
insert!(outax, minimum(axinds), newax)
YAXArray(outax, d, x.properties)

Check warning on line 361 in src/Cubes/Cubes.jl

View check run for this annotation

Codecov / codecov/patch

src/Cubes/Cubes.jl#L360-L361

Added lines #L360 - L361 were not covered by tests
end

function outaxis_from_column(tab,icol)
function outaxis_from_column(tab, icol)

Check warning on line 364 in src/Cubes/Cubes.jl

View check run for this annotation

Codecov / codecov/patch

src/Cubes/Cubes.jl#L364

Added line #L364 was not covered by tests
axdata = columns(tab)[icol]
axname = schema(tab).names[icol]
if eltype(axdata) <: AbstractString ||
(!issorted(axdata) && !issorted(axdata, rev = true))
(!issorted(axdata) && !issorted(axdata, rev=true))
DD.rebuild(DD.name2dim(Symbol(axname)), axdata)
else
DD.rebuild(DD.name2dim(Symbol(axname)), axdata)
end
end

function outaxis_from_data(cax,axinds,indlist)
mergeaxes = getindex.(Ref(cax),axinds)
function outaxis_from_data(cax, axinds, indlist)
mergeaxes = getindex.(Ref(cax), axinds)

Check warning on line 376 in src/Cubes/Cubes.jl

View check run for this annotation

Codecov / codecov/patch

src/Cubes/Cubes.jl#L375-L376

Added lines #L375 - L376 were not covered by tests
mergenames = axname.(mergeaxes)
newname = join(mergenames,'_')
newname = join(mergenames, '_')

Check warning on line 378 in src/Cubes/Cubes.jl

View check run for this annotation

Codecov / codecov/patch

src/Cubes/Cubes.jl#L378

Added line #L378 was not covered by tests
minai = minimum(axinds)
mergevals = map(indlist) do i
broadcast(mergeaxes,axinds) do ax,ai
broadcast(mergeaxes, axinds) do ax, ai

Check warning on line 381 in src/Cubes/Cubes.jl

View check run for this annotation

Codecov / codecov/patch

src/Cubes/Cubes.jl#L381

Added line #L381 was not covered by tests
ax.values[i[ai-minai+1]]
end
end
Expand Down Expand Up @@ -471,14 +489,14 @@
end


function Base.getindex(a::YAXArray, args::DD.Dimension...; kwargs...)
function Base.getindex(a::YAXArray, args::DD.Dimension...; kwargs...)
kwargsdict = Dict{Any,Any}(kwargs...)
for ext in YAXDefaults.subsetextensions
ext(kwargsdict)
end
d2 = Dict()
for (k,v) in kwargsdict
d = getAxis(k,a)
for (k, v) in kwargsdict
d = getAxis(k, a)
if d !== nothing
d2[DD.name(d)] = v
else
Expand All @@ -491,7 +509,7 @@
Base.read(d::YAXArray) = getindex_all(d)

function formatbytes(x)
exts = ["bytes", "KB", "MB", "GB", "TB","PB"]
exts = ["bytes", "KB", "MB", "GB", "TB", "PB"]
i = 1
while x >= 1024
i = i + 1
Expand All @@ -515,7 +533,7 @@

# ? sizeof : Check if the element type is a bitstype or a union of bitstypes
if (isconcretetype(eltype(c)) && isbitstype(eltype(c))) ||
(eltype(c) isa Union && all(isbitstype, Base.uniontypes(eltype(c))))
(eltype(c) isa Union && all(isbitstype, Base.uniontypes(eltype(c))))

println(io, "\n data size: ", formatbytes(cubesize(c)))
else # fallback
Expand Down
Loading
Loading