From 1e28c9125cbbcfe6f2f6a1b36c5e5afcd8873843 Mon Sep 17 00:00:00 2001 From: rafaqz Date: Sun, 11 Feb 2024 16:13:29 +0100 Subject: [PATCH] distinguish points and intervals in plots --- .gitignore | 7 +- docs/crash/course/test_makie_plots.jl | 26 +++ ext/DimensionalDataMakie.jl | 300 +++++++++++++++++++++----- src/Dimensions/predicates.jl | 2 +- test/plotrecipes.jl | 19 ++ 5 files changed, 296 insertions(+), 58 deletions(-) create mode 100644 docs/crash/course/test_makie_plots.jl diff --git a/.gitignore b/.gitignore index 58fe77e9d..8507418e4 100644 --- a/.gitignore +++ b/.gitignore @@ -18,8 +18,13 @@ docs/build docs/var deps/build.jl Manifest.toml +<<<<<<< HEAD docs/.vitepress/dist docs/.vitepress/cache docs/src/.vitepress/dist docs/src/.vitepress/cache -node_modules \ No newline at end of file +node_modules +======= +node_modules +docs_site +>>>>>>> ea9b0786 (distinguish points and intervals in plots) diff --git a/docs/crash/course/test_makie_plots.jl b/docs/crash/course/test_makie_plots.jl new file mode 100644 index 000000000..eb7ab5224 --- /dev/null +++ b/docs/crash/course/test_makie_plots.jl @@ -0,0 +1,26 @@ +using DimensionalData +using DimensionalData: Metadata, NoMetadata, ForwardOrdered, ReverseOrdered, Unordered, + Sampled, Categorical, NoLookup, Transformed, + Regular, Irregular, Explicit, Points, Intervals, Start, Center, End + +using GLMakie: GLMakie as Mke + +A1intervals = rand(X(1.0:10.0; sampling=Intervals(Start())); name=:test) + +Mke.plot(set(A1intervals, X=>Points())) +Mke.plot(A1intervals) + +A2intervals1 = rand(X(10:10:100; sampling=Intervals(Start())), Z(1:3)) +Mke.plot(A2intervals1) +A2intervals2 = rand(X(10:10:100; sampling=Intervals(Start())), Z(1:3; sampling=Intervals(Start()))) +Mke.plot(A2intervals2) + +A3intervals1 = rand(X(10:1:15; sampling=Intervals(Start())), Y(1:3), Dim{:C}(10:15)) +Mke.plot(A3intervals1; z=:C) +# broken +A3intervals2 = rand(X(10:1:15; sampling=Intervals(Start())), Y(1:3), Z(10:15; sampling=Intervals(Start()))) +Mke.plot(A3intervals2) +A3intervals2a = rand(X(10:1:10; sampling=Intervals(Start())), Y(1:1; sampling=Intervals(Start())), Z(10:20)) +Mke.plot(A3intervals2a) + +a = rand(2,2,2) \ No newline at end of file diff --git a/ext/DimensionalDataMakie.jl b/ext/DimensionalDataMakie.jl index e9dda35a7..90a686d47 100644 --- a/ext/DimensionalDataMakie.jl +++ b/ext/DimensionalDataMakie.jl @@ -10,6 +10,74 @@ const SurfaceLikeCompat = isdefined(Makie, :SurfaceLike) ? Makie.SurfaceLike : U _paired(args...) = map(x -> x isa Pair ? x : x => x, args) +struct DimLines{T,N,D<:DD.DimTuple,O} <: DD.AbstractDimIndices{T,N} + dims::D + order::O +end +function DimLines(dims::DD.DimTuple; order=dims) + @assert count(isintervals(dims)) == 1 + order = map(d -> basetypeof(d)(), order) + T = typeof(_getline(dims, map(firstindex, dims))) + N = length(dims) + dims = N > 0 ? DD._format(dims) : dims + DimLines{T,N,typeof(dims),typeof(order)}(dims, order) +end + +Base.getindex(dp::DimLines, i1::Int, i2::Int, Is::Int...) = _getline(dims(dp), (i1, i2, Is...)) +Base.getindex(di::DimLines{<:Any,1}, i::Int) = (dims(di, 1)[i],) +Base.getindex(di::DimLines, i::Int) = di[Tuple(CartesianIndices(di)[i])...] + +function _getline(ds, I::Tuple) + D = map(rebuild, ds, I) + # Get dim-wrapped point values at i1, I... + intervaldim = only(dims(ds, isintervals)) + ods = otherdims(ds, intervaldim) + bounds = intervalbounds(lookup(intervaldim), val(dims(D, intervaldim))...) + return map(bounds) do b + (b, map(getindex, dims(ds, ods), map(val, dims(D, ods)))...) + end +end + +struct DimPolygons{T,N,D<:DD.DimTuple,O} <: DD.AbstractDimIndices{T,N} + dims::D + order::O +end +function DimPolygons(dims::DD.DimTuple; order=dims) + @assert count(isintervals(dims)) == 2 "num intervaldims $(count(isintervals(dims))) is not 2" + order = map(d -> basetypeof(d)(), order) + T = typeof(_getpolygon(dims, map(firstindex, dims))) + N = length(dims) + dims = N > 0 ? DD._format(dims) : dims + DimPolygons{T,N,typeof(dims),typeof(order)}(dims, order) +end + +Base.getindex(dp::DimPolygons, i1::Int, i2::Int, Is::Int...) = _getpolygon(dims(dp), (i1, i2, Is...)) +Base.getindex(di::DimPolygons{<:Any,1}, i::Int) = (dims(di, 1)[i],) +Base.getindex(di::DimPolygons, i::Int) = di[Tuple(CartesianIndices(di)[i])...] + +function _getpolygon(ds::DD.DimTuple, I::Tuple) + D = map(rebuild, ds, I) + # Get dim-wrapped point values at i1, I... + intervaldims = dims(ds, isintervals) + ods = otherdims(ds, intervaldims) + bvs = map(intervaldims) do id + rebuild(id, intervalbounds(lookup(id), val(dims(D, id))...)) + end + ovs = map(rebuild, ods, map(getindex, ods, val(dims(D, ods)))) + points = [ + _polypoint(ds, bvs, ovs, 1, 1), + _polypoint(ds, bvs, ovs, 1, 2), + _polypoint(ds, bvs, ovs, 2, 2), + _polypoint(ds, bvs, ovs, 2, 1), + _polypoint(ds, bvs, ovs, 1, 1), + ] + return Makie.Polygon(Makie.LineString(points)) +end +function _polypoint(order, (bs1, bs2), ovs, i, j) + vs = (rebuild(bs1, val(bs1)[i]), rebuild(bs2, val(bs2)[j]), ovs...) + Point(map(val, dims(vs, order))) +end + # Shared docstrings: keep things consistent. @@ -59,34 +127,63 @@ end # 1d PointBased # 1d plots are scatter by default -for (f1, f2) in _paired(:plot => :scatter, :scatter, :lines, :scatterlines, :stairs, :stem, :barplot, :waterfall) - f1!, f2! = Symbol(f1, '!'), Symbol(f2, '!') +for f in (:scatter, :lines, :scatterlines, :stairs, :stem, :barplot, :waterfall) + f! = Symbol(f, '!') docstring = """ - $f1(A::AbstractDimArray{<:Any,1}; attributes...) + $f(A::AbstractDimVector; attributes...) - Plot a 1-dimensional `AbstractDimArray` with `Makie.$f2`. + Plot a 1-dimensional `AbstractDimArray` with `Makie`. The X axis will be labelled with the dimension name and and use ticks from its lookup. - $(_keyword_heading_doc(f1)) + $(_keyword_heading_doc(f)) $AXISLEGENDKW_DOC """ @eval begin @doc $docstring - function Makie.$f1(A::AbstractDimArray{<:Any,1}; axislegendkw=(;), attributes...) - args, merged_attributes = _pointbased1(A, attributes) - p = Makie.$f2(args...; merged_attributes...) + function Makie.$f(A::AbstractDimVector; axislegendkw=(;), attributes...) + args, merged_attributes = _pointbased1(A; attributes...) + p = Makie.$f(args...; merged_attributes...) axislegend(p.axis; merge=false, unique=false, axislegendkw...) return p end - function Makie.$f1!(axis, A::AbstractDimArray{<:Any,1}; axislegendkw=(;), attributes...) - args, merged_attributes = _pointbased1(A, attributes; set_axis_attributes=false) - return Makie.$f2!(axis, args...; merged_attributes...) + function Makie.$f!(axis, A::AbstractDimVector; axislegendkw=(;), attributes...) + args, merged_attributes = _pointbased1(A; attributes..., _set_axis_attributes=false) + return Makie.$f!(axis, args...; merged_attributes...) end end end -function _pointbased1(A, attributes; set_axis_attributes=true) + +function Makie.linesegments(A::AbstractDimVector; axislegendkw=(;), attributes...) + args, merged_attributes = _pointbased1(A; attributes...) + lines = _interval_lines(A) + p = Makie.linesegments(lines; merged_attributes...) + axislegend(p.axis; merge=false, unique=false, axislegendkw...) + return p +end + +function Makie.linesegments!(x, A::AbstractDimVector; attributes...) + args, merged_attributes = _pointbased1(A; attributes...) + lines = _interval_lines(A) + return Makie.linesegments(x, lines; merged_attributes...) +end + +function _interval_lines(A::AbstractDimVector) + map(intervalbounds(only(dims(A))), A) do bounds, val + T = promote_type(typeof(first(bounds)), typeof(last(bounds)), typeof(val)) + (T(bounds[1]), T(val)), (T(bounds[2]), T(val)) + end +end + +function Makie.plot(A::AbstractDimVector; kw...) + only(isintervals(A)) ? Makie.linesegments(A; kw...) : Makie.scatter(A; kw...) +end +function Makie.plot!(ax, A::AbstractDimVector; kw...) + only(isintervals(A)) ? Makie.linesegments!(ax, A; kw...) : Makie.scatter(axis, A; kw...) +end + +function _pointbased1(A; _set_axis_attributes=true, attributes...) # Array/Dimension manipulation A1 = _prepare_for_makie(A) lookup_attributes, newdims = _split_attributes(A1) @@ -94,7 +191,7 @@ function _pointbased1(A, attributes; set_axis_attributes=true) args = Makie.convert_arguments(Makie.PointBased(), A2) # Plot attribute generation user_attributes = Makie.Attributes(; attributes...) - axis_attributes = if set_axis_attributes + axis_attributes = if _set_axis_attributes Attributes(; axis=(; xlabel=string(name(dims(A, 1))), @@ -109,7 +206,7 @@ function _pointbased1(A, attributes; set_axis_attributes=true) label=DD.label(A), ) merged_attributes = merge(user_attributes, axis_attributes, plot_attributes, lookup_attributes) - if !set_axis_attributes + if !_set_axis_attributes delete!(merged_attributes, :axis) end return args, merged_attributes @@ -118,55 +215,82 @@ end # 2d SurfaceLike -for (f1, f2) in _paired(:plot => :heatmap, :heatmap, :image, :contour, :contourf, :spy, :surface) - f1!, f2! = Symbol(f1, '!'), Symbol(f2, '!') +for f in (:heatmap, :image, :contour, :contourf, :spy, :surface) + f! = Symbol(f, '!') docstring = """ - $f1(A::AbstractDimArray{<:Any,2}; attributes...) + $f(A::AbstractDimMatrix; attributes...) - Plot a 2-dimensional `AbstractDimArray` with `Makie.$f2`. + Plot a 2-dimensional `AbstractDimArray` with `Makie.$f`. - $(_keyword_heading_doc(f1)) - $(_xy(f1)) - $(_maybe_colorbar_doc(f1)) + $(_keyword_heading_doc(f)) + $(_xy(f)) + $(_maybe_colorbar_doc(f)) """ @eval begin @doc $docstring - function Makie.$f1(A::AbstractDimArray{T,2}; - x=nothing, y=nothing, colorbarkw=(;), attributes... - ) where T - replacements = _keywords2dimpairs(x, y) - A1, A2, args, merged_attributes = _surface2(A, attributes, replacements) - p = if $(f1 == :surface) + function Makie.$f(A::AbstractDimMatrix{T}; colorbarkw=(;), attributes...) where T + A1, A2, args, merged_attributes = _surface2(A; attributes...) + p = if $(f == :surface) # surface is an LScene so we cant pass attributes - p = Makie.$f2(args...; attributes...) + p = Makie.$f(args...; attributes...) # And instead set axisnames manually p.axis.scene[OldAxis][:names, :axisnames] = map(DD.label, DD.dims(A2)) p else - Makie.$f2(args...; merged_attributes...) + Makie.$f(args...; merged_attributes...) end # Add a Colorbar for heatmaps and contourf - if T isa Real && $(f1 in (:plot, :heatmap, :contourf)) + if T isa Real && $(f in (:plot, :heatmap, :contourf)) Colorbar(p.figure[1, 2], p.plot; label=DD.label(A), colorbarkw... ) end return p end - function Makie.$f1!(axis, A::AbstractDimArray{<:Any,2}; - x=nothing, y=nothing, colorbarkw=(;), attributes... - ) - replacements = _keywords2dimpairs(x, y) - _, _, args, _ = _surface2(A, attributes, replacements) + function Makie.$f!(axis, A::AbstractDimMatrix; colorbarkw=(;), attributes...) + _, _, args, _ = _surface2(A, attributes) # No ColourBar in the ! in-place versions - return Makie.$f2!(axis, args...; attributes...) + return Makie.$f!(axis, args...; attributes...) end end end -function _surface2(A, attributes, replacements) +function Makie.plot(A::AbstractDimMatrix; kw...) + if all(isintervals(dims(A))) + Makie.heatmap(A; kw...) + elseif any(isintervals(A)) + Makie.linesegments(A; kw...) + else + Makie.scatter(A; kw...) + end +end +function Makie.plot!(ax, A::AbstractDimMatrix; kw...) + if all(isintervals(A)) + Makie.heatmap!(ax, A; kw...) + elseif any(isintervals(A)) + Makie.linesegments!(ax, A; kw...) + else + Makie.scatter!(ax, A; kw...) + end +end + +function Makie.linesegments(A::AbstractDimMatrix; attributes...) + A1, A2, args, merged_attributes = _surface2(A; attributes...) + lines = vec(collect(DimLines(A))) + p = Makie.linesegments(lines; color=vec(A), merged_attributes...) + return p +end + +function Makie.linesegments!(x, A::AbstractDimMatrix; attributes...) + A1, A2, args, merged_attributes = _surface2(A; attributes...) + lines = collect(vec(DimLines(A))) + return Makie.linesegments(x, lines; colors=vec(A), merged_attributes...) +end + +function _surface2(A; x=nothing, y=nothing, attributes...) + replacements = _keywords2dimpairs(x, y) # Array/Dimension manipulation - A1 = _prepare_for_makie(A, replacements) + A1 = _prepare_for_makie(A; replacements) lookup_attributes, newdims = _split_attributes(A1) A2 = _restore_dim_names(set(A1, map(Pair, newdims, newdims)...), A, replacements) args = Makie.convert_arguments(Makie.ContinuousSurface(), A2) @@ -188,37 +312,101 @@ end # 3d VolumeLike -for (f1, f2) in _paired(:plot => :volume, :volume, :volumeslices) - f1!, f2! = Symbol(f1, '!'), Symbol(f2, '!') +for f in (:volume, :volumeslices) + f! = Symbol(f, '!') docstring = """ - $f1(A::AbstractDimArray{<:Any,3}; attributes...) + $f(A::AbstractDimArray{<:Any,3}; attributes...) - Plot a 3-dimensional `AbstractDimArray` with `Makie.$f2`. + Plot a 3-dimensional `AbstractDimArray` with `Makie.$f`. - $(_keyword_heading_doc(f1)) - $(_xy(f1)) - $(_z(f1)) + $(_keyword_heading_doc(f)) + $(_xy(f)) + $(_z(f)) """ @eval begin @doc $docstring - function Makie.$f1(A::AbstractDimArray{<:Any,3}; x=nothing, y=nothing, z=nothing, attributes...) - replacements = _keywords2dimpairs(x, y, z) - A1, A2, args, merged_attributes = _volume3(A, attributes, replacements) - p = Makie.$f2(args...; merged_attributes...) + function Makie.$f(A::AbstractDimArray{<:Any,3}; attributes...) + A1, A2, args, merged_attributes = _volume3(A; attributes...) + p = Makie.$f(args...; merged_attributes...) p.axis.scene[OldAxis][:names, :axisnames] = map(DD.label, DD.dims(A2)) return p end - function Makie.$f1!(axis, A::AbstractDimArray{<:Any,3}; x=nothing, y=nothing, z=nothing, attributes...) - replacements = _keywords2dimpairs(x, y, z) - _, _, args, _ = _volume3(A, attributes, replacements) - return Makie.$f2!(axis, args...; attributes...) + function Makie.$f!(axis, A::AbstractDimArray{<:Any,3}; attributes...) + _, _, args, _ = _volume3(A; attributes...) + return Makie.$f!(axis, args...; attributes...) end end end -function _volume3(A, attributes, replacements) +function Makie.plot(A::AbstractDimArray{<:Any,3}; kw...) + if all(isintervals(dims(A))) + Makie.volume(A; interpolate=false, kw...) + elseif count(isintervals(A)) == 2 + Makie.poly(A; kw...) + elseif count(isintervals(A)) == 1 + Makie.linesegments(A; kw...) + else + Makie.scatter(A; kw...) + end +end +function Makie.plot!(ax, A::AbstractDimArray{<:Any,3}; kw...) + if all(isintervals(A)) + Makie.volume!(ax, A; interpolate=false, kw...) + elseif count(isintervals(A)) == 2 + Makie.poly!(ax, A; kw...) + elseif count(isintervals(A)) == 1 + Makie.linesegments!(ax, A; kw...) + else + Makie.scatter!(ax, A; kw...) + end +end + +function Makie.poly(A::AbstractDimArray{<:Any,3}; attributes...) + A1, A2, args, merged_attributes = _volume3(A; attributes...) + dps = DimPolygons(A) + polys = vec(collect(dps)) + # TODO this doesn't plot properly? + # All polygons plat on the same plane + p = Makie.poly(polys; color=vec(A2), merged_attributes...) + p.axis.scene[OldAxis][:names, :axisnames] = map(DD.label, DD.dims(A2)) + return p +end + +function Makie.poly!(ax, A::AbstractDimArray{<:Any,3}; attributes...) + A1, A2, args, merged_attributes = _volume3(A; attributes...) + polys = vec(collect(DimPolygons(A2))) + return Makie.poly(ax, polys; color=vec(A), merged_attributes...) +end + +function Makie.linesegments(A::AbstractDimArray{<:Any,3}; attributes...) + A1, A2, args, merged_attributes = _volume3(A; attributes...) + lines = vec(collect(DimLines(A))) + p = Makie.linesegments(lines; color=vec(A), merged_attributes...) + p.axis.scene[OldAxis][:names, :axisnames] = map(DD.label, DD.dims(A2)) + return p +end + +function Makie.linesegments!(ax, A::AbstractDimArray{<:Any,3}; attributes...) + A1, A2, args, merged_attributes = _surface2(A; attributes...) + lines = collect(vec(DimLines(A))) + p = Makie.linesegments!(ax, lines; colors=vec(A), merged_attributes...) + p.axis.scene[OldAxis][:names, :axisnames] = map(DD.label, DD.dims(A2)) + return p +end + +function Makie.scatter(A::AbstractDimArray{<:Any,3}; attributes...) + A1, A2, args, merged_attributes = _volume3(A; attributes...) + points = vec(collect(DimPoints(A2))) + p = Makie.scatter(points; color=vec(A), merged_attributes...) + p.axis.scene[OldAxis][:names, :axisnames] = map(DD.label, DD.dims(A2)) + return p +end + + +function _volume3(A; x=nothing, y=nothing, z=nothing, attributes...) + replacements = _keywords2dimpairs(x, y, z) # Array/Dimension manipulation - A1 = _prepare_for_makie(A, replacements) + A1 = _prepare_for_makie(A; replacements) _, newdims = _split_attributes(A1) A2 = _restore_dim_names(set(A1, map(Pair, newdims, newdims)...), A, replacements) args = Makie.convert_arguments(Makie.VolumeLike(), A2) @@ -437,7 +625,7 @@ function _split_attributes(dim::Dimension) return attributes, dims[1] end -function _prepare_for_makie(A, replacements=()) +function _prepare_for_makie(A; replacements=()) _permute_xyz(maybeshiftlocus(Center(), A; dims=(XDim, YDim)), replacements) |> _reorder end diff --git a/src/Dimensions/predicates.jl b/src/Dimensions/predicates.jl index a9bab9d94..e7a6c8448 100644 --- a/src/Dimensions/predicates.jl +++ b/src/Dimensions/predicates.jl @@ -16,7 +16,7 @@ for f in ( @eval begin LookupArrays.$f(x::Dimension) = $f(val(x)) LookupArrays.$f(::Nothing) = false - LookupArrays.$f(xs::DimTuple) = all(map($f, xs)) + LookupArrays.$f(xs::DimTuple) = map($f, xs) LookupArrays.$f(x::Any) = $f(dims(x)) LookupArrays.$f(x::Any, ds) = $f(dims(x, ds)) end diff --git a/test/plotrecipes.jl b/test/plotrecipes.jl index 396886b12..94def61d5 100644 --- a/test/plotrecipes.jl +++ b/test/plotrecipes.jl @@ -158,6 +158,7 @@ nothing # da_im2 = DimArray(im2, (X(10:10:100), Y(10:10:100)), "Image") # da_im2 |> plot +using DimensionalData using CairoMakie: CairoMakie as M using ColorTypes @testset "Makie" begin @@ -201,6 +202,11 @@ using ColorTypes M.waterfall!(ax, A1) fig, ax, _ = M.waterfall(A1m) M.waterfall!(ax, A1m) + + A1intervals = rand(X(1.0:10.0; sampling=Intervals(Start())); name=:test) + A1intervals + M.plot(set(A1intervals, X=>Points())) + M.plot(A1intervals) # 2d A2 = rand(X(10:10:100), Y(['a', 'b', 'c'])) A2r = rand(Y(10:10:100), X(['a', 'b', 'c'])) @@ -252,6 +258,10 @@ using ColorTypes M.series!(ax, A2m) @test_throws ArgumentError M.plot(A2; y=:c) @test_throws ArgumentError M.plot!(ax, A2; y=:c) + A2intervals1 = rand(X(10:10:100; sampling=Intervals(Start())), Z(1:3)) + M.plot(A2intervals1) + A2intervals2 = rand(X(10:10:100; sampling=Intervals(Start())), Z(1:3; sampling=Intervals(Start()))) + M.plot(A2intervals2) # x/y can be specified A2ab = DimArray(rand(6, 10), (:a, :b); name=:stuff) @@ -282,6 +292,7 @@ using ColorTypes # 3d A3 = rand(X(7), Z(10), Y(5)) + M.plot(A3) A3m = rand([missing, (1:7)...], X(7), Z(10), Y(5)) A3m[3] = missing A3rgb = rand(RGB, X(7), Z(10), Y(5)) @@ -305,4 +316,12 @@ using ColorTypes fig, ax, _ = M.volumeslices(A3abc; x=:c) fig, ax, _ = M.volumeslices(A3abc; z=:a) M.volumeslices!(ax, A3abc;z=:a) + + A3intervals1 = rand(X(10:1:15; sampling=Intervals(Start())), Y(1:3), Dim{:C}(10:15)) + M.plot(A3intervals1; z=:C) + # Broken from here + A3intervals2 = rand(X(10:1:15; sampling=Intervals(Start())), Y(1:3), Z(10:15; sampling=Intervals(Start()))) + M.plot(A3intervals2) + A3intervals2a = rand(X(10:1:10; sampling=Intervals(Start())), Y(1:1; sampling=Intervals(Start())), Z(10:20)) + M.plot(A3intervals2a) end