diff --git a/.codecov.yml b/.codecov.yml index 69cb76019a4..1854ef2db43 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -1 +1,4 @@ comment: false +coverage: + status: + project: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a245094cb7..44f70a552a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,17 @@ # Changelog -## [0.20.9] - 2024-03-29 +## [Unreleased] - Improved thread safety of rendering with CairoMakie (independent `Scene`s only) by locking FreeType handles [#3777](https://github.com/MakieOrg/Makie.jl/pull/3777). ## [0.21.0] - 2024-03-0X -- Add `voxels` plot [#3527](https://github.com/MakieOrg/Makie.jl/pull/3527) +- Add `voxels` plot [#3527](https://github.com/MakieOrg/Makie.jl/pull/3527). - Added supported markers hint to unsupported marker warn message [#3666](https://github.com/MakieOrg/Makie.jl/pull/3666). - Fixed bug in CairoMakie line drawing when multiple successive points had the same color [#3712](https://github.com/MakieOrg/Makie.jl/pull/3712). - Remove StableHashTraits in favor of calculating hashes directly with CRC32c [#3667](https://github.com/MakieOrg/Makie.jl/pull/3667). -- **Potentially breaking** Added a new `@recipe` variant with a `begin end` block. This variant is considered **internal** to Makie for now and could be refactored in patch releases. If you want to use it already on an experimental basis, do so only with Makie pinned to a patch version to avoid breakage. The new syntax allows documenting attributes directly in the `@recipe` definition and validating that all user-passed attributes are known whenever a plot is created. All of Makie's recipes have been ported over to this new syntax and will therefore throw errors when they receive invalid attributes. This is not breaking in the sense that the API changes, but existing user code is likely to break because of misspelled attribute names etc. that have so far gone unnoticed. A likely culprit is the forwarding of attributes to child plots by splatting the parent attributes into it like `some_plot!(...; attrs...)`. To fix this, make sure that you are only passing along valid attributes to the child plots in your recipes. +- **Breaking (sort of)** Added a new `@recipe` variant which allows documenting attributes directly where they are defined and validating that all attributes are known whenever a plot is created. This is not breaking in the sense that the API changes, but user code is likely to break because of misspelled attribute names etc. that have so far gone unnoticed. +- Add axis converts, enabling unit/categorical support and more [#3226](https://github.com/MakieOrg/Makie.jl/pull/3226). - **Breaking** Streamlined `data_limits` and `boundingbox` [#3671](https://github.com/MakieOrg/Makie.jl/pull/3671) - `data_limits` now only considers plot positions, completely ignoring transformations - `boundingbox(p::Text)` is deprecated in favor of `boundingbox(p::Text, p.markerspace[])`. The more internal methods use `string_boundingbox(p)`. [#3723](https://github.com/MakieOrg/Makie.jl/pull/3723) @@ -39,6 +40,18 @@ - Fixed an issue with the texture atlas not updating in WGLMakie after display, causing new symbols to not show up [#3737](https://github.com/MakieOrg/Makie.jl/pull/3737) - Added `linecap` and `joinstyle` attributes for lines and linesegments. Also normalized `miter_limit` to 60° across all backends. [#3771](https://github.com/MakieOrg/Makie.jl/pull/3771) +## [0.20.9] - 2024-03-29 + +- Added supported markers hint to unsupported marker warn message [#3666](https://github.com/MakieOrg/Makie.jl/pull/3666). +- Fixed bug in CairoMakie line drawing when multiple successive points had the same color [#3712](https://github.com/MakieOrg/Makie.jl/pull/3712). +- Remove StableHashTraits in favor of calculating hashes directly with CRC32c [#3667](https://github.com/MakieOrg/Makie.jl/pull/3667). +- Fixed `contourf` bug where n levels would sometimes miss the uppermost value, causing gaps [#3713](https://github.com/MakieOrg/Makie.jl/pull/3713). +- Added `scale` attribute to `violin` [#3352](https://github.com/MakieOrg/Makie.jl/pull/3352). +- Use label formatter in barplot [#3718](https://github.com/MakieOrg/Makie.jl/pull/3718). +- Fix the incorrect shading with non uniform markerscale in meshscatter [#3722](https://github.com/MakieOrg/Makie.jl/pull/3722) +- Add `scale_to=:flip` option to `hist`, which flips the direction of the bars [#3732](https://github.com/MakieOrg/Makie.jl/pull/3732) +- Fixed an issue with the texture atlas not updating in WGLMakie after display, causing new symbols to not show up [#3737](https://github.com/MakieOrg/Makie.jl/pull/3737) + ## [0.20.8] - 2024-02-22 - Fixed excessive use of space with HTML image outputs [#3642](https://github.com/MakieOrg/Makie.jl/pull/3642). diff --git a/GLMakie/src/drawing_primitives.jl b/GLMakie/src/drawing_primitives.jl index 29e19a74e7e..8ef4e4a8817 100644 --- a/GLMakie/src/drawing_primitives.jl +++ b/GLMakie/src/drawing_primitives.jl @@ -226,7 +226,7 @@ const EXCLUDE_KEYS = Set([:transformation, :tickranges, :ticklabels, :raw, :SSAO :lightposition, :material, :axis_cycler, :inspector_label, :inspector_hover, :inspector_clear, :inspectable, :colorrange, :colormap, :colorscale, :highclip, :lowclip, :nan_color, - :calculated_colors, :space, :markerspace, :model]) + :calculated_colors, :space, :markerspace, :model, :dim_conversions]) function cached_robj!(robj_func, screen, scene, plot::AbstractPlot) diff --git a/GLMakie/src/precompiles.jl b/GLMakie/src/precompiles.jl index d2bd372aa14..311f553ad2c 100644 --- a/GLMakie/src/precompiles.jl +++ b/GLMakie/src/precompiles.jl @@ -68,3 +68,7 @@ precompile(glTexImage, (GLenum, Int, GLenum, Int, Int, Int, GLenum, GLenum, Ptr{ precompile(glTexImage, (GLenum, Int, GLenum, Int, Int, Int, GLenum, GLenum, Ptr{RGBA{Float16}})) precompile(glTexImage, (GLenum, Int, GLenum, Int, Int, Int, GLenum, GLenum, Ptr{N0f8})) precompile(setindex!, (GLMakie.GLAbstraction.Texture{Float16,2}, Matrix{Float32}, Rect2{Int32})) +precompile(getindex, (Makie.Text{Tuple{Vector{Point{2,Float32}}}}, Symbol)) +precompile(getproperty, (Makie.Text{Tuple{Vector{Point{2,Float32}}}}, Symbol)) +precompile(plot!, (Makie.Text{Tuple{Vector{Point{2,Float32}}}},)) +precompile(Base.getindex, (Attributes, Symbol)) diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index 8133ed61ed1..0bab3fd248e 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -497,7 +497,7 @@ function Base.delete!(screen::Screen, scene::Scene) # Remap scene IDs to a continuous range by replacing the largest ID # with the one that got removed - if deleted_id-1 != length(screen.screens) + if deleted_id - 1 != length(screen.screens) key, max_id = first(screen.screen2scene) for p in screen.screen2scene if p[2] > max_id diff --git a/MakieCore/Project.toml b/MakieCore/Project.toml index 683219e08bf..262c3af9c64 100644 --- a/MakieCore/Project.toml +++ b/MakieCore/Project.toml @@ -4,6 +4,9 @@ authors = ["Simon Danisch"] version = "0.8.0" [deps] +ColorTypes = "3da002f7-5984-5a60-b8a6-cbb66c0b333f" +GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326" +IntervalSets = "8197267c-284f-5f27-9208-e0e47529a953" Observables = "510215fc-4207-5dde-b226-833fc4488ee2" REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" diff --git a/MakieCore/src/MakieCore.jl b/MakieCore/src/MakieCore.jl index 3d0c55d1eee..ad043ec5f12 100644 --- a/MakieCore/src/MakieCore.jl +++ b/MakieCore/src/MakieCore.jl @@ -8,10 +8,9 @@ end using Observables using Observables: to_value using Base: RefValue -# Needing REPL for Base.Docs.doc on julia -# https://github.com/MakieOrg/Makie.jl/issues/3276 -using REPL - +using GeometryBasics +using ColorTypes +using IntervalSets: ClosedInterval, Interval include("types.jl") include("attributes.jl") diff --git a/MakieCore/src/attributes.jl b/MakieCore/src/attributes.jl index aecc9cedfc2..cf092c6eac6 100644 --- a/MakieCore/src/attributes.jl +++ b/MakieCore/src/attributes.jl @@ -118,17 +118,7 @@ function Base.setindex!(x::Attributes, value, key::Symbol) end function Base.setindex!(x::Attributes, value::Observable, key::Symbol) - if haskey(x, key) - # error("You're trying to update an attribute Observable with a new Observable. This is not supported right now. - # You can do this manually like this: - # lift(val-> attributes[$key] = val, Observable::$(typeof(value))) - # ") - return x.attributes[key] = node_any(value) - else - #TODO make this error. Attributes should be sort of immutable - return x.attributes[key] = node_any(value) - end - return x + return x.attributes[key] = node_any(value) end _indent_attrs(s, n) = join(split(s, '\n'), "\n" * " "^n) @@ -190,16 +180,16 @@ Base.get(x::AttributeOrPlot, key::Symbol, default) = get(()-> default, x, key) # Plot plots break this assumption in some way, but the way to look at it is, # that the plots contained in a Plot plot are not subplots, but _are_ actually # the plot itself. -Base.getindex(plot::AbstractPlot, idx::Integer) = plot.converted[idx] -Base.getindex(plot::AbstractPlot, idx::UnitRange{<:Integer}) = plot.converted[idx] -Base.setindex!(plot::AbstractPlot, value, idx::Integer) = (plot.args[idx][] = value) -Base.length(plot::AbstractPlot) = length(plot.converted) +Base.getindex(plot::Plot, idx::Integer) = plot.converted[idx] +Base.getindex(plot::Plot, idx::UnitRange{<:Integer}) = plot.converted[idx] +Base.setindex!(plot::Plot, value, idx::Integer) = (plot.args[idx][] = value) +Base.length(plot::Plot) = length(plot.converted) -function Base.getindex(x::AbstractPlot, key::Symbol) - argnames = argument_names(typeof(x), length(x.converted)) +function Base.getindex(x::T, key::Symbol) where {T <: Plot} + argnames = argument_names(T, length(x.converted)) idx = findfirst(isequal(key), argnames) if idx === nothing - return x.attributes[key] + return attributes(x)[key] else return x.converted[idx] end @@ -233,15 +223,7 @@ function Base.setindex!(x::AbstractPlot, value::Observable, key::Symbol) argnames = argument_names(typeof(x), length(x.converted)) idx = findfirst(isequal(key), argnames) if idx === nothing - if haskey(x, key) - # error("You're trying to update an attribute Observable with a new Observable. This is not supported right now. - # You can do this manually like this: - # lift(val-> attributes[$key] = val, Observable::$(typeof(value))) - # ") - return x.attributes[key] = value - else - return x.attributes[key] = value - end + return attributes(x)[key] = value else return setindex!(x.converted[idx], value) end diff --git a/MakieCore/src/basic_plots.jl b/MakieCore/src/basic_plots.jl index ec2339f1232..0914dbaa6c9 100644 --- a/MakieCore/src/basic_plots.jl +++ b/MakieCore/src/basic_plots.jl @@ -200,7 +200,7 @@ calculated_attributes!(plot::T) where T = calculated_attributes!(T, plot) Plots an image on a rectangle bounded by `x` and `y` (defaults to size of image). """ -@recipe Image x y image begin +@recipe Image (x::ClosedInterval{<:FloatType}, y::ClosedInterval{<:FloatType}, image::AbstractMatrix{<:Union{FloatType,Colorant}}) begin "Sets whether colors should be interpolated between pixels." interpolate = true mixin_generic_plot_attributes()... @@ -238,7 +238,7 @@ If `x` and `y` are omitted with a matrix argument, they default to `x, y = axes( Note that `heatmap` is slower to render than `image` so `image` should be preferred for large, regularly spaced grids. """ -@recipe Heatmap x y values begin +@recipe Heatmap (x::RealVector, y::RealVector, values::AbstractMatrix{<:Union{FloatType,Colorant}}) begin "Sets whether colors should be interpolated" interpolate = false mixin_generic_plot_attributes()... @@ -258,7 +258,12 @@ Available algorithms are: * `:additive` => AdditiveRGBA * `:indexedabsorption` => IndexedAbsorptionRGBA """ -@recipe Volume x y z volume begin +@recipe Volume ( + x::ClosedInterval, + y::ClosedInterval, + z::ClosedInterval, + volume::AbstractArray{Float32,3} + ) begin "Sets the volume algorithm that is used." algorithm = :mip "Sets the range of values picked up by the IsoValue algorithm." @@ -276,6 +281,8 @@ Available algorithms are: mixin_colormap_attributes()... end +const VecOrMat{T} = Union{AbstractVector{T}, AbstractMatrix{T}} + """ surface(x, y, z) surface(z) @@ -283,7 +290,7 @@ end Plots a surface, where `(x, y)` define a grid whose heights are the entries in `z`. `x` and `y` may be `Vectors` which define a regular grid, **or** `Matrices` which define an irregular grid. """ -@recipe Surface x y z begin +@recipe Surface (x::VecOrMat{<:FloatType}, y::VecOrMat{<:FloatType}, z::VecOrMat{<:FloatType}) begin "Can be set to an `Matrix{<: Union{Number, Colorant}}` to color surface independent of the `z` component. If `color=nothing`, it defaults to `color=z`." color = nothing "Inverts the normals generated for the surface. This can be useful to illuminate the other side of the surface." @@ -302,7 +309,7 @@ Creates a connected line plot for each element in `(x, y, z)`, `(x, y)` or `posi `NaN` values are displayed as gaps in the line. """ -@recipe Lines positions begin +@recipe Lines (positions,) begin "The color of the line." color = @inherit linecolor "Sets the width of the line in screen units" @@ -329,22 +336,8 @@ end linesegments(x, y, z) Plots a line for each pair of points in `(x, y, z)`, `(x, y)`, or `positions`. - -## Attributes - -### Specific to `LineSegments` - -- `color=theme(scene, :linecolor)` sets the color of the linesegments. If no color is set, multiple calls to `linesegments!` will cycle through the axis color palette. - Otherwise, one can set one color per line point or one color per linesegment by passing a `Vector{<:Colorant}`, or one colorant for the whole line. If color is a vector of numbers, the colormap args are used to map the numbers to colors. -- `cycle::Vector{Symbol} = [:color]` sets which attributes to cycle when creating multiple plots. -- `linestyle::Union{Nothing, Symbol, Vector} = nothing` sets the pattern of the line (e.g. `:solid`, `:dot`, `:dashdot`) -- `linewidth::Union{Real, Vector} = 1.5` sets the width of the line in pixel units. - -$(Base.Docs.doc(colormap_attributes!)) - -$(Base.Docs.doc(MakieCore.generic_plot_attributes!)) """ -@recipe LineSegments positions begin +@recipe LineSegments (positions,) begin "The color of the line." color = @inherit linecolor "Sets the width of the line in pixel units" @@ -369,7 +362,7 @@ end Plots a 3D or 2D mesh. Supported `mesh_object`s include `Mesh` types from [GeometryBasics.jl](https://github.com/JuliaGeometry/GeometryBasics.jl). """ -@recipe Mesh mesh begin +@recipe Mesh (mesh::Union{AbstractVector{<:GeometryBasics.Mesh},GeometryBasics.Mesh},) begin "Sets the color of the mesh. Can be a `Vector{<:Colorant}` for per vertex colors or a single `Colorant`. A `Matrix{<:Colorant}` can be used to color the mesh with a texture, which requires the mesh to contain texture coordinates." color = @inherit patchcolor "sets whether colors should be interpolated" @@ -388,7 +381,7 @@ end Plots a marker for each element in `(x, y, z)`, `(x, y)`, or `positions`. """ -@recipe Scatter positions begin +@recipe Scatter (positions,) begin "Sets the color of the marker. If no color is set, multiple calls to `scatter!` will cycle through the axis color palette." color = @inherit markercolor "Sets the scatter marker." @@ -438,7 +431,7 @@ end Plots a mesh for each element in `(x, y, z)`, `(x, y)`, or `positions` (similar to `scatter`). `markersize` is a scaling applied to the primitive passed as `marker`. """ -@recipe MeshScatter positions begin +@recipe MeshScatter (positions,) begin "Sets the color of the marker." color = @inherit markercolor "Sets the scattered mesh." @@ -467,7 +460,7 @@ end Plots one or multiple texts passed via the `text` keyword. `Text` uses the `PointBased` conversion trait. """ -@recipe Text positions begin +@recipe Text (positions,) begin "Specifies one piece of text or a vector of texts to show, where the number has to match the number of positions given. Makie supports `String` which is used for all normal text and `LaTeXString` which layouts mathematical expressions using `MathTeXEngine.jl`." text = "" "Sets the color of the text. One can set one color per glyph by passing a `Vector{<:Colorant}`, or one colorant for the whole text. If color is a vector of numbers, the colormap args are used to map the numbers to colors." @@ -613,7 +606,7 @@ Draws a wireframe, either interpreted as a surface or as a mesh. depth_shift = -1f-5 end -@recipe Arrows points directions begin +@recipe Arrows (points, directions) begin "Sets the color of arrowheads and lines. Can be overridden separately using `linecolor` and `arrowcolor`." color = :black """Scales the size of the arrow head. This defaults to diff --git a/MakieCore/src/conversion.jl b/MakieCore/src/conversion.jl index 2b2b9635d2d..16c450ffc17 100644 --- a/MakieCore/src/conversion.jl +++ b/MakieCore/src/conversion.jl @@ -1,5 +1,4 @@ -function convert_arguments end """ convert_attribute(value, attribute::Key[, plottype::Key]) @@ -31,8 +30,6 @@ struct NoConversion <: ConversionTrait end conversion_trait(::Type) = NoConversion() conversion_trait(T::Type, args...) = conversion_trait(T) -convert_arguments(::NoConversion, args...) = args - """ PointBased() <: ConversionTrait @@ -41,7 +38,6 @@ Plots with the `PointBased` trait convert their input data to a """ struct PointBased <: ConversionTrait end conversion_trait(::Type{<: XYBased}) = PointBased() -conversion_trait(::Type{<: Text}) = PointBased() """ GridBased <: ConversionTrait @@ -100,3 +96,57 @@ conversion_trait(::Type{<: Image}) = ImageLike() struct VolumeLike <: ConversionTrait end conversion_trait(::Type{<: Volume}) = VolumeLike() + +function convert_arguments end + +convert_arguments(::NoConversion, args...; kw...) = args + +get_element_type(::T) where {T} = T +function get_element_type(arr::AbstractArray{T}) where {T} + if T == Any + return mapreduce(typeof, promote_type, arr) + else + return T + end +end + +types_for_plot_arguments(trait) = nothing +function types_for_plot_arguments(P::Type{<:Plot}, Trait::ConversionTrait) + p = types_for_plot_arguments(P) + isnothing(p) || return p + return types_for_plot_arguments(Trait) +end + +function types_for_plot_arguments(::PointBased) + return Tuple{AbstractVector{<:Union{Point2, Point3}}} +end + +should_dim_convert(::Type) = false + +""" + MakieCore.should_dim_convert(::Type{<: Plot}, args)::Bool + MakieCore.should_dim_convert(eltype::DataType)::Bool + +Returns `true` if the plot type should convert its arguments via DimConversions. +Needs to be overloaded for recipes that want to use DimConversions. Also needs +to be overloaded for DimConversions, e.g. for CategoricalConversion: + +```julia + MakieCore.should_dim_convert(::Type{Categorical}) = true +``` + +`should_dim_convert(::Type{<: Plot}, args)` falls back on checking if +`has_typed_convert(plot_or_trait)` and `should_dim_convert(get_element_type(args))` + are true. The former is defined as true by `@convert_target`, i.e. when +`convert_arguments_typed` is defined for the given plot type or conversion trait. +The latter marks specific types as convertable. + +If a recipe wants to use dim conversions, it should overload this function: +```julia + MakieCore.should_dim_convert(::Type{<:MyPlotType}, args) = should_dim_convert(get_element_type(args)) +`` +""" +function should_dim_convert(P, arg) + isnothing(types_for_plot_arguments(P)) && return false + return should_dim_convert(get_element_type(arg)) +end diff --git a/MakieCore/src/recipes.jl b/MakieCore/src/recipes.jl index 6b18f3ca241..2ccad91511e 100644 --- a/MakieCore/src/recipes.jl +++ b/MakieCore/src/recipes.jl @@ -340,19 +340,53 @@ function _attribute_docs(T::Type{<:Plot}) end -macro recipe(Tsym::Symbol, args...) +function create_args_type_expr(PlotType, args::Nothing) + return [], :() +end +function create_args_type_expr(PlotType, args) + if Meta.isexpr(args, :tuple) + all_fields = args.args + else + throw(ArgumentError("Recipe arguments need to be a tuple of the form (name::OptionalType, name,). Found: $(args)")) + end + if any(x -> !(Meta.isexpr(x, :(::)) || x isa Symbol), all_fields) + throw(ArgumentError("All fields need to be of type `name::Type` or `name`. Found: $(all_fields)")) + end + types = []; names = Symbol[] + if all(x-> x isa Symbol, all_fields) + return all_fields, :() + end + for field in all_fields + if field isa Symbol + error("All fields need to be typed if one is. Please either type all fields or none. Found: $(all_fields)") + end + push!(names, field.args[1]) + push!(types, field.args[2]) + end + expr = quote + MakieCore.types_for_plot_arguments(::Type{<:$(PlotType)}) = Tuple{$(types...)} + end + return names, expr +end + +macro recipe(Tsym::Symbol, attrblock) + return create_recipe_expr(Tsym, nothing, attrblock) +end + +macro recipe(Tsym::Symbol, args, attrblock) + return create_recipe_expr(Tsym, args, attrblock) +end +function types_for_plot_arguments end + +function create_recipe_expr(Tsym, args, attrblock) funcname_sym = to_func_name(Tsym) funcname!_sym = Symbol("$(funcname_sym)!") funcname! = esc(funcname!_sym) PlotType = esc(Tsym) funcname = esc(funcname_sym) - syms = args[1:end-1] - for sym in syms - sym isa Symbol || throw(ArgumentError("Found argument that is not a symbol in the position where optional argument names should appear: $sym")) - end - attrblock = args[end] + syms, arg_type_func = create_args_type_expr(PlotType, args) if !(attrblock isa Expr && attrblock.head === :block) throw(ArgumentError("Last argument is not a begin end block")) end @@ -360,7 +394,6 @@ macro recipe(Tsym::Symbol, args...) # attrs = [extract_attribute_metadata(arg) for arg in attrblock.args if !(arg isa LineNumberNode)] docs_placeholder = gensym() - attr_placeholder = gensym() q = quote @@ -414,6 +447,7 @@ macro recipe(Tsym::Symbol, args...) function $(MakieCore).default_theme(scene, T::Type{<:$(PlotType)}) Attributes(documented_attributes(T).closure(scene)) end + $(arg_type_func) docstring_modified = make_recipe_docstring($PlotType, $(QuoteNode(Tsym)), $(QuoteNode(funcname_sym)),user_docstring) @doc docstring_modified $funcname_sym @@ -427,12 +461,12 @@ macro recipe(Tsym::Symbol, args...) q.args, :( $(esc(:($(MakieCore).argument_names)))(::Type{<:$PlotType}, len::Integer) = - $syms + ($(QuoteNode.(syms)...),) ), ) end - q + return q end function make_recipe_docstring(P::Type{<:Plot}, Tsym, funcname_sym, docstring) @@ -678,7 +712,8 @@ function Base.showerror(io::IO, i::InvalidAttributeError) end function attribute_name_allowlist() - (:xautolimits, :yautolimits, :zautolimits, :label, :rasterize, :model, :transformation) + return (:xautolimits, :yautolimits, :zautolimits, :label, :rasterize, :model, :transformation, + :dim_conversions) end function validate_attribute_keys(P::Type{<:Plot}, kw::Dict{Symbol}) @@ -700,4 +735,4 @@ function validate_attribute_keys(P::Type{<:Plot}, kw::Dict{Symbol}) end end end -end \ No newline at end of file +end diff --git a/MakieCore/src/types.jl b/MakieCore/src/types.jl index d634647c2a4..fc0740e9532 100644 --- a/MakieCore/src/types.jl +++ b/MakieCore/src/types.jl @@ -10,6 +10,7 @@ abstract type AbstractPlot{Typ} <: Transformable end abstract type AbstractScene <: Transformable end abstract type ScenePlot{Typ} <: AbstractPlot{Typ} end + """ Screen constructors implemented by all backends: @@ -70,7 +71,7 @@ mutable struct Plot{PlotFunc, T} <: ScenePlot{PlotFunc} kw::Dict{Symbol,Any} args::Vector{Any} - converted::NTuple{N,Observable} where {N} + converted::Vector{Observable} # Converted and processed arguments attributes::Attributes @@ -78,10 +79,12 @@ mutable struct Plot{PlotFunc, T} <: ScenePlot{PlotFunc} deregister_callbacks::Vector{Observables.ObserverFunction} parent::Union{AbstractScene,Plot} - function Plot{Typ,T}(kw::Dict{Symbol, Any}, args::Vector{Any}, converted::NTuple{N, Observable}) where {Typ,T,N} + function Plot{Typ,T}( + kw::Dict{Symbol,Any}, args::Vector{Any}, converted::Vector{Observable}, + deregister_callbacks::Vector{Observables.ObserverFunction}=Observables.ObserverFunction[] + ) where {Typ,T} validate_attribute_keys(Plot{Typ}, kw) - return new{Typ,T}(nothing, kw, args, converted, Attributes(), Plot[], - Observables.ObserverFunction[]) + return new{Typ,T}(nothing, kw, args, converted, Attributes(), Plot[], deregister_callbacks) end end @@ -142,3 +145,8 @@ Billboard(angles::Vector) = Billboard(Float32.(angles)) FastShading MultiLightShading end + +const RealArray{T,N} = AbstractArray{T,N} where {T<:Real} +const RealVector{T} = RealArray{1} +const RealMatrix{T} = RealArray{2} +const FloatType = Union{Float32,Float64} diff --git a/Project.toml b/Project.toml index 4b503ecb882..9d2e8bdedf5 100644 --- a/Project.toml +++ b/Project.toml @@ -12,6 +12,7 @@ ColorSchemes = "35d6a980-a343-548e-a6ea-1d62b119f2f4" ColorTypes = "3da002f7-5984-5a60-b8a6-cbb66c0b333f" Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" Contour = "d38c429a-6771-53c6-b99e-75d170b6e991" +Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" DelaunayTriangulation = "927a84f5-c5f4-47a5-9785-b46e178433df" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" @@ -57,6 +58,7 @@ StatsFuns = "4c63d2b9-4356-54db-8cca-17b64c39e42c" StructArrays = "09ab397b-f2b6-538f-b94a-2f83cf4a842a" TriplotBase = "981d1d27-644d-49a2-9326-4793e63143c3" UnicodeFun = "1cfade01-22cf-5700-b092-accc4b62d6e1" +Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" [compat] Animations = "0.4" diff --git a/ReferenceTests/src/tests/categorical.jl b/ReferenceTests/src/tests/categorical.jl new file mode 100644 index 00000000000..bd2bd4865cb --- /dev/null +++ b/ReferenceTests/src/tests/categorical.jl @@ -0,0 +1,70 @@ +using Test + +using Makie: Categorical + +@reference_test "multi plot, error with non categorical" begin + f, ax, p = scatter(1:4, Categorical(["a", "b", "c", "a"]), color=1:4, colormap=:viridis, markersize=20) + scatter!(ax, 1:4, Categorical(["b", "x", "a", "c"]), color=1:4, colormap=:reds, markersize=20) + # TODO, throw better error (not that easy since we need to check for sortability) + @test_throws MethodError scatter!(ax, 1:4, 1:4) # error + f +end + +@reference_test "different types without sorting function" begin + # If we set the ticks explicitely, with sortby defaulting to nothing, + # we can combine all objects: + f = Figure() + ax = Axis(f[1, 1]; + dim1_conversion=Makie.CategoricalConversion(; sortby=nothing), + dim2_conversion=Makie.CategoricalConversion(; sortby=nothing)) + + p = scatter!(ax, 1:4, Categorical(["a", "b", "c", "a"]); color=1:4, colormap=:viridis, markersize=20) + sp = scatter!(ax, 1:4, 1:4; color=1:4, colormap=:reds, markersize=20) + scatter!(ax, [1im, 2im], 1:2, color=[:red, :black], markersize=20) + f +end + +@reference_test "new random categories, interactive" begin + obs = Observable(Categorical(["o", "m", "d", "p", "p"])) + obs2 = Observable(Categorical(["q", "f", "y", "e", "n"])) + f, ax, pl = scatter(1:5, obs, markersize=20, color=1:5, colormap=:viridis) + scatter!(1:5, obs2, markersize=20, color=1:5, colormap=:reds) + obs[] = Categorical(["f", "z", "a", "u", "z"]) + obs2[] = Categorical(["i", "s", "n", "i", "o"]) + autolimits!(ax) + f +end + +@reference_test "changing order of categorical values" begin + obs = Observable(Categorical(["a", "a", "b", "b"])) + f, ax, p = scatter(1:4, obs; markersize=20, color=1:4, colormap=:viridis) + obs[] = Categorical(["a", "b", "a", "b"]) + f +end + +@reference_test "new categories, inbetween old values" begin + obs = Observable(Categorical(["a", "c", "e", "g"])) + f, ax, p = scatter(1:4, obs, markersize=20, color=1:4, colormap=:viridis) + obs[] = Categorical(["b", "d", "f", "h"]) + f +end + +struct SomeStruct + value +end + +@reference_test "custom struct, with custom sorting function" begin + f = Figure() + conversion = Makie.CategoricalConversion(sortby=x->x.value) + xtickformat = x-> string.(getfield.(x, :value)) .* " val" + ax = Axis(f[1, 1]; dim1_conversion=conversion, xtickformat=xtickformat) + barplot!(ax, SomeStruct.([:a, :b, :c]), 1:3) + f +end + +@reference_test "Categorical xticks yticks" begin + f, ax, p = scatter(Categorical(["a", "b", "c", "d"]), Categorical(["a", "a", "c", "x"]), markersize=20) + ax.xticks = ["a", "d"] + ax.yticks = ["a", "b", "d", "x"] + f +end diff --git a/ReferenceTests/src/tests/dates.jl b/ReferenceTests/src/tests/dates.jl new file mode 100644 index 00000000000..5d68b360f7d --- /dev/null +++ b/ReferenceTests/src/tests/dates.jl @@ -0,0 +1,48 @@ +using Makie.Unitful, Makie.Dates, Test + +some_time = Time("11:11:55.914") +date = Date("2021-10-27") +date_time = DateTime("2021-10-27T11:11:55.914") +time_range = some_time .+ range(Second(0); step=Second(5), length=10) +date_range = range(date, step=Day(5), length=10) +date_time_range = range(date_time, step=Week(5), length=10) + +@reference_test "time_range" scatter(time_range, 1:10) +@reference_test "date_range" scatter(date_range, 1:10) +@reference_test "date_time_range" scatter(date_time_range, 1:10) + +@reference_test "Don'some_time allow mixing units incorrectly" begin + date_time_range = range(date_time, step=Second(5), length=10) + f, ax, pl = scatter(date_time_range, 1:10) + @test_throws ErrorException scatter!(time_range, 1:10) + f +end + +@reference_test "Force Unitful to be rendered as Time" begin + yconversion = Makie.DateTimeConversion(Time) + scatter(1:4, (1:4) .* u"s"; axis=(dim2_conversion=yconversion,)) +end + +@reference_test "Time Observable" begin + obs = Observable(time_range) + f, ax, pl = scatter(obs, 1:10) + obs[] = some_time .+ range(Second(0); step=Second(1), length=10) + autolimits!(ax) + f +end + +@reference_test "Date Observable" begin + obs = Observable(date_range) + f, ax, pl = scatter(obs, 1:10) + obs[] = range(date, step=Day(1), length=10) + autolimits!(ax) + f +end + +@reference_test "DateTime Observable" begin + obs = Observable(date_time_range) + f, ax, pl = scatter(obs, 1:10) + obs[] = range(date_time, step=Week(3), length=10) + autolimits!(ax) + f +end diff --git a/ReferenceTests/src/tests/figures_and_makielayout.jl b/ReferenceTests/src/tests/figures_and_makielayout.jl index fb49156980b..0fc062cb03e 100644 --- a/ReferenceTests/src/tests/figures_and_makielayout.jl +++ b/ReferenceTests/src/tests/figures_and_makielayout.jl @@ -282,7 +282,7 @@ end values = [sin(x[i]) * cos(y[j]) * sin(z[k]) for i in 1:20, j in 1:20, k in 1:20] # TO not make this fail in CairoMakie, we dont actually plot the volume - _f, ax, cp = contour(x, y, z, values; levels=10, colormap=:viridis) + _f, ax, cp = contour(-1..1, -1..1, -1..1, values; levels=10, colormap=:viridis) Colorbar(fig[2, 1], cp; size=300) _f, ax, vs = volumeslices(x, y, z, values, colormap=:bluesreds) diff --git a/ReferenceTests/src/tests/refimages.jl b/ReferenceTests/src/tests/refimages.jl index f66d70b4e30..54fbbd9a2da 100644 --- a/ReferenceTests/src/tests/refimages.jl +++ b/ReferenceTests/src/tests/refimages.jl @@ -13,6 +13,15 @@ using ReferenceTests.Colors: RGB, N0f8 using ReferenceTests.DelaunayTriangulation using Makie: Record, volume +@testset "categorical" begin + include("categorical.jl") +end +@testset "dates" begin + include("dates.jl") +end +@testset "unitful" begin + include("unitful.jl") +end @testset "specapi" begin include("specapi.jl") end diff --git a/ReferenceTests/src/tests/unitful.jl b/ReferenceTests/src/tests/unitful.jl new file mode 100644 index 00000000000..d3d4ac7be52 --- /dev/null +++ b/ReferenceTests/src/tests/unitful.jl @@ -0,0 +1,36 @@ +using Makie.Dates, Makie.Unitful, Test + +@reference_test "combining units, error for numbers" begin + f, ax, pl = scatter(Second(1):Second(600):Second(100*60), 1:10, markersize=20, color=1:10) + scatter!(ax, Hour(1):Hour(1):Hour(10), 1:10; markersize=20, color=1:10, colormap=:reds) + @test_throws Unitful.DimensionError scatter!(ax, rand(10), 1:10) # should error! + f +end + +@reference_test "different units for x + y" begin + scatter(u"ns" .* (1:10), u"d" .* (1:10), markersize=20, color=1:10) +end + +@reference_test "Nanoseconds on y" begin + linesegments(1:10, Nanosecond.(round.(LinRange(0, 4599800000000, 10)))) +end + +@reference_test "Meter & time on x, y" begin + scatter(u"cm" .* (1:10), u"d" .* (1:10)) +end + +@reference_test "Auto units for observables" begin + obs = Observable{Any}(u"s" .* (1:10)) + f, ax, pl = scatter(1:10, obs) + st = Stepper(f) + + obs[] = u"yr" .* (1:10) + autolimits!(ax) + Makie.step!(st) + + obs[] = u"ns" .* (1:10) + autolimits!(ax) + Makie.step!(st) + + st +end diff --git a/WGLMakie/src/particles.jl b/WGLMakie/src/particles.jl index 7e2dac5f645..1139347b8a3 100644 --- a/WGLMakie/src/particles.jl +++ b/WGLMakie/src/particles.jl @@ -40,7 +40,7 @@ const IGNORE_KEYS = Set([ :visible, :transformation, :alpha, :linewidth, :transparency, :marker, :light_direction, :light_color, :cycle, :label, :inspector_clear, :inspector_hover, - :inspector_label, :axis_cycler + :inspector_label, :axis_cyclerr, :dim_conversions # TODO add model here since we generally need to apply patch_model? ]) diff --git a/docs/explanations/conversion_pipeline.md b/docs/explanations/conversion_pipeline.md index 374402b7dd4..ebd83bf747c 100644 --- a/docs/explanations/conversion_pipeline.md +++ b/docs/explanations/conversion_pipeline.md @@ -186,11 +186,13 @@ scene ### Argument Conversions When calling a plot function, e.g. `scatter!(axis_or_scene, args...)` a new plot object is constructed. -The plot object keeps track of the input arguments in `plot.args`, promoting them to observables if need be. -It also keeps track of a type normalized set of inputs in `plot.converted` which are generated from `plot.args` using various `convert_arguments()` functions. -Generally speaking these functions either dispatch on the plot type or the result of `conversion_trait(PlotType, args...)`, i.e. `convert_arguments(type_or_traint, args...)`. +The plot object keeps track of the original input arguments converted to Observables in `plot.args`. +Those input arguments are then converted via `convert_arguments` and stored in `plot.converted`. +Generally speaking these methods either dispatch on the plot type or the result of `conversion_trait(PlotType, args...)`, i.e. `convert_arguments(type_or_trait, args...)`. They are expected to generalize and simplify the structure of data given to a plot while leaving the numeric type as either a Float32 or Float64 as appropriate. +The full conversion pipeline is run in `Makie.conversion_pipeline` which also applies `dim converts` and checks if the conversion was successful. + ### Transformation Objects The remaining transformed versions of data are not accessible, but rather abstract representations which the data goes through. @@ -201,7 +203,7 @@ Ignoring `Float32Convert` for now, the next two transformations are summarized u The first transformation is `transformation.transform_func`, which holds a function which is applied to a `Vector{Point{N, T}}` element by element. It is meant to resolve transformations that cannot be represented as a matrix operations, for example moving data into a logarithmic space or into Polar coordinates. They are implemented using the `apply_transform(func, data)` methods. -Generally we also expect transform function to be (partially) invertable and their inverse to be returned by `inverse_transform(func)`. +Generally we also expect transform function to be (partially) invertible and their inverse to be returned by `inverse_transform(func)`. The second transformation is `transformation.model`, which combines `translate!(plot, ...)`, `scale!(plot, ...)` and `rotate!(plot, ...)` into a matrix. The order of operations here is fixed - rotations apply first, then scaling and finally translations. diff --git a/docs/explanations/dim-converts.md b/docs/explanations/dim-converts.md new file mode 100644 index 00000000000..91686de6a52 --- /dev/null +++ b/docs/explanations/dim-converts.md @@ -0,0 +1,144 @@ +# Dim Converts + +Starting with `Makie@0.21`, support for types like units, categorical values and Dates has been added. +They are converted to a plottable representation by dim converts, which also take care of axis ticks. +In the following sections we will explain their usage and how to extend the interface with your own types. + +## Examples + +The basic usage is as easy as replacing numbers with any supported type, e.g. `Dates.Second`: + +\begin{examplefigure}{} +```julia +using CairoMakie, Makie.Dates, Makie.Unitful +CairoMakie.activate!() # hide +Makie.inline!(true) # hide + +f, ax, pl = scatter(rand(Second(1):Second(60):Second(20*60), 10)) +``` +\end{examplefigure} + +Once an axis dimension is set to a certain unit, one must plot into that axis with compatible units. +So e.g. hours work, since they're compatible with the unitful conversion: + +\begin{examplefigure}{} +```julia +scatter!(ax, rand(Hour(1):Hour(1):Hour(20), 10)) +# Unitful works as well +scatter!(ax, LinRange(0u"yr", 0.1u"yr", 5)) +f +``` +\end{examplefigure} + +Note that the units displayed in ticks will adjust to the given range of values. + +Going back to just numbers errors since the axis is unitful now: + +```julia +try + scatter!(ax, 1:4) +catch e + return e +end +``` + +Similarly, trying to plot units into a unitless axis dimension errors too, since otherwise it would alter the meaning of the previous plotted values: + +```julia +try + scatter!(ax, LinRange(0u"yr", 0.1u"yr", 10), rand(Hour(1):Hour(1):Hour(20), 10)) +catch e + return e +end +``` + +you can access the conversion via `ax.dim1_conversion` and `ax.dim2_conversion`: + +```julia +(ax.dim1_conversion[], ax.dim2_conversion[]) +``` + +And set them accordingly: + +```julia +f = Figure() +ax = Axis(f[1, 1]; dim1_conversion=Makie.CategoricalConversion()) +``` + +### Scope + +Currently, dim conversions only works for x and y arguments for the standard 2D Axis. It's setup to generalize to other Axis types, but is currently only supported by `Axis`. + +### Current conversions in Makie + +{{doc CategoricalConversion}} +{{doc UnitfulConversion}} +{{doc DateTimeConversion}} + +## Dev docs + +You can overload the API to define your own dim converts by overloading the following functions: + +\begin{examplefigure}{} +```julia +struct MyDimConversion <: Makie.AbstractDimConversion end + +# The type you target with the dim conversion +struct MyUnit + value::Float64 +end + +# This is currently needed because `expand_dimensions` can only be narrowly defined for `Vector{<:Real}` in Makie. +# So, if you want to make `plot(some_y_values)` work for your own types, you need to define this method: +Makie.expand_dimensions(::PointBased, y::AbstractVector{<:MyUnit}) = (keys(y.values), y) + +function Makie.needs_tick_update_observable(conversion::MyDimConversion) + # return an observable that indicates when ticks need to update e.g. in case the unit changes or new categories get added. + # For a simple unit conversion this is not needed, so we return nothing. + return nothing +end + +# Indicate that this type should be converted using MyDimConversion +# The Type gets extracted via `Makie.get_element_type(plot_argument_for_dim_n)` +# so e.g. `plot(1:10, ["a", "b", "c"])` would call `Makie.get_element_type(["a", "b", "c"])` and return `String` for axis dim 2. +Makie.create_dim_conversion(::Type{MyUnit}) = MyDimConversion() + +# This function needs to be overloaded too, even though it's redundant to the above in a sense. +# We did not want to use `hasmethod(MakieCore.should_dim_convert, (MyDimTypes,))` because it can be slow and error prown. +Makie.MakieCore.should_dim_convert(::Type{MyUnit}) = true + +# The non observable version of the actual conversion function +# This is needed to convert axis limits, and should be a pure version of the below `convert_dim_observable` +function Makie.convert_dim_value(::MyDimConversion, values) + return [v.value for v in values] +end + +function Makie.convert_dim_observable(conversion::MyDimConversion, values_obs::Observable, deregister) + # Do the actual conversion here + # Most complex dim conversions need to operate on the observable (e.g. to create a Dict of all used categories), so `convert_dim_value` alone is not enough. + result = Observable(Float64[]) + f = on(values_obs; update=true) do values + result[] = Makie.convert_dim_value(conversion, values) + end + + # any observable operation like `on` or `map` should be pushed to `deregister`, to clean up state properly if e.g. the plot gets destroyed. + # for `result = map(func, values_obs)` one can use `append!(deregister, result.inputs)` + push!(deregister, f) + return result +end + +function Makie.get_ticks(::MyDimConversion, user_set_ticks, user_dim_scale, user_formatter, limits_min, limits_max) + # Don't do anything special to ticks for this example, just append `myunit` to the labels and leave the rest to Makie's usual tick finding methods. + ticknumbers, ticklabels = Makie.get_ticks(user_set_ticks, user_dim_scale, user_formatter, limits_min, + limits_max) + return ticknumbers, ticklabels .* "myunit" +end + +barplot([MyUnit(1), MyUnit(2), MyUnit(3)], 1:3) +``` +\end{examplefigure} + +For more complex examples, you should look at the implementation in: +`Makie/src/dim-converts`. + +The conversions get applied in the function `Makie.conversion_pipeline` in `Makie/src/interfaces.jl`. diff --git a/relocatability.jl b/relocatability.jl index b5d00892f81..2d8894ae508 100644 --- a/relocatability.jl +++ b/relocatability.jl @@ -14,8 +14,6 @@ end # module MakieApp """ using Pkg, Test -pkg"registry up" -Pkg.update() makie_dir = pwd() tmpdir = mktempdir() # create a temporary project diff --git a/src/Makie.jl b/src/Makie.jl index e6b941bdd70..c07bd502645 100644 --- a/src/Makie.jl +++ b/src/Makie.jl @@ -45,7 +45,9 @@ using MakieCore using OffsetArrays using Downloads using ShaderAbstractions +using Dates +import Unitful import UnicodeFun import RelocatableFolders import StatsBase @@ -91,15 +93,12 @@ import MakieCore: create_axis_like, create_axis_like!, figurelike_return, figure import MakieCore: arrows, heatmap, image, lines, linesegments, mesh, meshscatter, poly, scatter, surface, text, volume, voxels import MakieCore: arrows!, heatmap!, image!, lines!, linesegments!, mesh!, meshscatter!, poly!, scatter!, surface!, text!, volume!, voxels! import MakieCore: convert_arguments, convert_attribute, default_theme, conversion_trait - +import MakieCore: RealVector, RealMatrix, RealArray, FloatType export @L_str, @colorant_str export ConversionTrait, NoConversion, PointBased, GridBased, VertexGrid, CellGrid, ImageLike, VolumeLike export Pixel, px, Unit, plotkey, attributes, used_attributes export Linestyle -const RealArray{T, N} = AbstractArray{T, N} where {T<:Real} -const RealVector{T} = RealArray{1} -const RealMatrix{T} = RealArray{2} const RGBAf = RGBA{Float32} const RGBf = RGB{Float32} @@ -121,6 +120,12 @@ include("patterns.jl") include("utilities/utilities.jl") # need Makie.AbstractPattern include("lighting.jl") # Basic scene/plot/recipe interfaces + types + +include("dim-converts/dim-converts.jl") +include("dim-converts/unitful-integration.jl") +include("dim-converts/categorical-integration.jl") +include("dim-converts/dates-integration.jl") + include("scenes.jl") include("float32-scaling.jl") @@ -240,6 +245,7 @@ export xtickrange, ytickrange, ztickrange export xticks!, yticks!, zticks! export xtickrotation, ytickrotation, ztickrotation export xtickrotation!, ytickrotation!, ztickrotation! +export Categorical # Observable/Signal related export Observable, Observable, lift, to_value, on, onany, @lift, off, connect! @@ -362,8 +368,8 @@ include("basic_recipes/text.jl") include("basic_recipes/raincloud.jl") include("deprecated.jl") -export Arrows , Heatmap , Image , Lines , LineSegments , Mesh , MeshScatter , Poly , Scatter , Surface , Text , Volume , Wireframe, Voxels -export arrows , heatmap , image , lines , linesegments , mesh , meshscatter , poly , scatter , surface , text , volume , wireframe, voxels +export Arrows , Heatmap , Image , Lines , LineSegments , Mesh , MeshScatter , Poly , Scatter , Surface , Text , Volume , Wireframe, Voxels +export arrows , heatmap , image , lines , linesegments , mesh , meshscatter , poly , scatter , surface , text , volume , wireframe, voxels export arrows! , heatmap! , image! , lines! , linesegments! , mesh! , meshscatter! , poly! , scatter! , surface! , text! , volume! , wireframe!, voxels! export AmbientLight, PointLight, DirectionalLight, SpotLight, EnvironmentLight, RectLight, SSAO diff --git a/src/basic_recipes/annotations.jl b/src/basic_recipes/annotations.jl index 6f0906ad954..23836caf2d3 100644 --- a/src/basic_recipes/annotations.jl +++ b/src/basic_recipes/annotations.jl @@ -3,7 +3,7 @@ Plots an array of texts at each position in `positions`. """ -@recipe Annotations text position begin +@recipe Annotations (text, position) begin MakieCore.documented_attributes(Text)... end diff --git a/src/basic_recipes/arc.jl b/src/basic_recipes/arc.jl index 3e283811243..b89e9bd5efd 100644 --- a/src/basic_recipes/arc.jl +++ b/src/basic_recipes/arc.jl @@ -12,7 +12,7 @@ Examples: `arc(Point2f(1, 2), 0.3, π, -π)` """ -@recipe Arc origin radius start_angle stop_angle begin +@recipe Arc (origin, radius, start_angle, stop_angle) begin MakieCore.documented_attributes(Lines)... "The number of line points approximating the arc." resolution = 361 diff --git a/src/basic_recipes/axis.jl b/src/basic_recipes/axis.jl index 6336234dfd2..806f9f5286a 100644 --- a/src/basic_recipes/axis.jl +++ b/src/basic_recipes/axis.jl @@ -209,7 +209,7 @@ end function labelposition(ranges, dim, dir, tgap, origin::StaticVector{N}) where N a, b = extrema(ranges[dim]) whalf = Float32(((b - a) / 2)) - halfaxis = unit(Point{N, Float32}, dim) .* whalf + halfaxis = GeometryBasics.unit(Point{N, Float32}, dim) .* whalf origin .+ (halfaxis .+ (normalize(dir) * tgap)) end @@ -258,7 +258,7 @@ function draw_axis3d(textbuffer, linebuffer, scale, limits, ranges_labels, fonts tgap = 0.01limit_widths[offset_indices] .* tgap for i in 1:N - axis_vec = unit(Point{N, Float32}, i) + axis_vec = GeometryBasics.unit(Point{N, Float32}, i) width = Float32(limit_widths[i]) stop = origin .+ (width .* axis_vec) if showaxis[i] @@ -267,7 +267,7 @@ function draw_axis3d(textbuffer, linebuffer, scale, limits, ranges_labels, fonts if showticks[i] range = ranges[i] j = offset_indices[i] - tickdir = unit(Vec{N, Float32}, j) + tickdir = GeometryBasics.unit(Vec{N, Float32}, j) offset2 = Float32(limit_widths[j] + tgap[i]) * tickdir for (j, tick) in enumerate(range) labels = ticklabels[i] @@ -301,7 +301,7 @@ function draw_axis3d(textbuffer, linebuffer, scale, limits, ranges_labels, fonts thickness = gridthickness[i] for _j = (i + 1):(i + N - 1) j = mod1(_j, N) - dir = unit(Point{N, Float32}, j) + dir = GeometryBasics.unit(Point{N, Float32}, j) range = ranges[j] for tick in range offset = Float32(tick - origin[j]) * dir diff --git a/src/basic_recipes/band.jl b/src/basic_recipes/band.jl index 05c3a23aefe..f84e78a6be8 100644 --- a/src/basic_recipes/band.jl +++ b/src/basic_recipes/band.jl @@ -5,7 +5,7 @@ Plots a band from `ylower` to `yupper` along `x`. The form `band(lower, upper)` plots a [ruled surface](https://en.wikipedia.org/wiki/Ruled_surface) between the points in `lower` and `upper`. """ -@recipe Band lowerpoints upperpoints begin +@recipe Band (lowerpoints, upperpoints) begin MakieCore.documented_attributes(Mesh)... shading = NoShading end diff --git a/src/basic_recipes/barplot.jl b/src/basic_recipes/barplot.jl index 8d9e941bc35..479fc2c3cb0 100644 --- a/src/basic_recipes/barplot.jl +++ b/src/basic_recipes/barplot.jl @@ -39,7 +39,7 @@ end Plots a barplot. """ -@recipe BarPlot x y begin +@recipe BarPlot (x, y) begin """Controls the baseline of the bars. This is zero in the default `automatic` case unless the barplot is in a log-scaled `Axis`. With a log scale, the automatic default is half the minimum value because zero is an invalid value for a log scale. """ @@ -239,7 +239,7 @@ end function Makie.plot!(p::BarPlot) bar_points = p[1] if !(eltype(bar_points[]) <: Point2) - error("barplot only accepts x/y coordinates. Use `barplot(x, y)` or `barplot(xy::Vector{<:Point2})`.") + error("barplot only accepts x/y coordinates. Use `barplot(x, y)` or `barplot(xy::Vector{<:Point2})`. Found: $(bar_points[])") end labels = Observable(Tuple{Union{String,LaTeXStrings.LaTeXString}, Point2d}[]) label_aligns = Observable(Vec2d[]) diff --git a/src/basic_recipes/buffers.jl b/src/basic_recipes/buffers.jl index 823ddbcae32..bbaac64b6f2 100644 --- a/src/basic_recipes/buffers.jl +++ b/src/basic_recipes/buffers.jl @@ -81,7 +81,6 @@ function finish!(tb::Annotations) return end - function push!(tb::Annotations, text::String, position::VecTypes{N}; kw_args...) where N append!(tb, [(String(text), Point{N, Float32}(position))]; kw_args...) end diff --git a/src/basic_recipes/contours.jl b/src/basic_recipes/contours.jl index f33bd3c9656..487c8211c0b 100644 --- a/src/basic_recipes/contours.jl +++ b/src/basic_recipes/contours.jl @@ -260,10 +260,10 @@ function plot!(plot::T) where T <: Union{Contour, Contour3d} labels, labelcolor, labelformatter, lev_pos_col ) do _, _, _, labels, labelcolor, labelformatter, lev_pos_col labels || return - pos = texts.positions.val; empty!(pos) - rot = texts.rotation.val; empty!(rot) - col = texts.color.val; empty!(col) - lbl = texts.text.val; empty!(lbl) + pos = texts.positions[]; empty!(pos) + rot = texts.rotation[]; empty!(rot) + col = texts.color[]; empty!(col) + lbl = texts.text[]; empty!(lbl) for (lev, (p1, p2, p3), color) in lev_pos_col px_pos1 = project(scene, apply_transform(transform_func(plot), p1, space)) px_pos3 = project(scene, apply_transform(transform_func(plot), p3, space)) @@ -357,4 +357,4 @@ end # TODO: should this have a data_limits overload? function boundingbox(plot::Contour3d, space::Symbol = :data) return transform_bbox(plot, data_limits(plot)) -end \ No newline at end of file +end diff --git a/src/basic_recipes/datashader.jl b/src/basic_recipes/datashader.jl index f4ae09fe851..a94ff2d800a 100644 --- a/src/basic_recipes/datashader.jl +++ b/src/basic_recipes/datashader.jl @@ -286,7 +286,7 @@ Do pay attention though, that if x and y don't have a fast iteration/getindex im For best performance, use `method=Makie.AggThreads()` and make sure to start julia with `julia -tauto` or have the environment variable `JULIA_NUM_THREADS` set to the number of cores you have. """ -@recipe DataShader points begin +@recipe DataShader (points,) begin """ Can be `AggCount()`, `AggAny()` or `AggMean()`. User-extensible by overloading: diff --git a/src/basic_recipes/error_and_rangebars.jl b/src/basic_recipes/error_and_rangebars.jl index 2cc6711a5fb..93d770a527f 100644 --- a/src/basic_recipes/error_and_rangebars.jl +++ b/src/basic_recipes/error_and_rangebars.jl @@ -14,7 +14,7 @@ Plots errorbars at xy positions, extending by errors in the given `direction`. If you want to plot intervals from low to high values instead of relative errors, use `rangebars`. """ -@recipe Errorbars begin +@recipe Errorbars (val_low_high::AbstractVector{<:Union{Vec3, Vec4}},) begin "The width of the whiskers or line caps in screen units." whiskerwidth = 0 "The color of the lines. Can be an array to color each bar separately." @@ -29,6 +29,7 @@ If you want to plot intervals from low to high values instead of relative errors MakieCore.mixin_generic_plot_attributes()... end +const RealOrVec = Union{Real, RealVector} """ rangebars(val, low, high; kwargs...) @@ -57,22 +58,22 @@ end ### conversions for errorbars -function convert_arguments(::Type{<:Errorbars}, x, y, error_both) +function convert_arguments(::Type{<:Errorbars}, x::RealOrVec, y::RealOrVec, error_both::RealVector) T = float_type(x, y, error_both) xyerr = broadcast(x, y, error_both) do x, y, e Vec4{T}(x, y, e, e) end (xyerr,) end - -function convert_arguments(::Type{<:Errorbars}, x, y, error_low, error_high) +RealOrVec +function convert_arguments(::Type{<:Errorbars}, x::RealOrVec, y::RealOrVec, error_low::RealOrVec, error_high::RealOrVec) T = float_type(x, y, error_low, error_high) xyerr = broadcast(Vec4{T}, x, y, error_low, error_high) (xyerr,) end -function convert_arguments(::Type{<:Errorbars}, x, y, error_low_high::AbstractVector{<:VecTypes{2, T}}) where T +function convert_arguments(::Type{<:Errorbars}, x::RealOrVec, y::RealOrVec, error_low_high::AbstractVector{<:VecTypes{2, T}}) where T T_out = float_type(float_type(x, y), T) xyerr = broadcast(x, y, error_low_high) do x, y, (el, eh) Vec4{T_out}(x, y, el, eh) @@ -80,7 +81,8 @@ function convert_arguments(::Type{<:Errorbars}, x, y, error_low_high::AbstractVe (xyerr,) end -function convert_arguments(::Type{<:Errorbars}, xy::AbstractVector{<:VecTypes{2, T}}, error_both) where T +function convert_arguments(::Type{<:Errorbars}, xy::AbstractVector{<:VecTypes{2,T}}, + error_both::RealOrVec) where {T} T_out = float_type(T, float_type(error_both)) xyerr = broadcast(xy, error_both) do (x, y), e Vec4{T_out}(x, y, e, e) @@ -88,7 +90,7 @@ function convert_arguments(::Type{<:Errorbars}, xy::AbstractVector{<:VecTypes{2, (xyerr,) end -function convert_arguments(::Type{<:Errorbars}, xy::AbstractVector{<:VecTypes{2, T}}, error_low, error_high) where T +function convert_arguments(::Type{<:Errorbars}, xy::AbstractVector{<:VecTypes{2, T}}, error_low::RealOrVec, error_high::RealOrVec) where T T_out = float_type(T, float_type(error_low, error_high)) xyerr = broadcast(xy, error_low, error_high) do (x, y), el, eh Vec4{T_out}(x, y, el, eh) @@ -114,13 +116,14 @@ end ### conversions for rangebars -function convert_arguments(::Type{<:Rangebars}, val, low, high) +function convert_arguments(::Type{<:Rangebars}, val::RealOrVec, low::RealOrVec, high::RealOrVec) T = float_type(val, low, high) val_low_high = broadcast(Vec3{T}, val, low, high) - (val_low_high,) + return (val_low_high,) end -function convert_arguments(::Type{<:Rangebars}, val, low_high::AbstractVector{<:VecTypes{2, T}}) where T +function convert_arguments(::Type{<:Rangebars}, val::RealOrVec, + low_high::AbstractVector{<:VecTypes{2,T}}) where {T} T_out = float_type(float_type(val), T) val_low_high = broadcast(val, low_high) do val, (low, high) Vec3{T_out}(val, low, high) @@ -131,7 +134,7 @@ end ### the two plotting functions create linesegpairs in two different ways ### and then hit the same underlying implementation in `_plot_bars!` -function Makie.plot!(plot::Errorbars{T}) where T <: Tuple{AbstractVector{<:VecTypes{4}}} +function Makie.plot!(plot::Errorbars{<:Tuple{AbstractVector{<:Vec{4}}}}) x_y_low_high = plot[1] @@ -161,7 +164,7 @@ function Makie.plot!(plot::Errorbars{T}) where T <: Tuple{AbstractVector{<:VecTy end -function Makie.plot!(plot::Rangebars{T}) where T <: Tuple{AbstractVector{<:VecTypes{3}}} +function Makie.plot!(plot::Rangebars{<:Tuple{AbstractVector{<:Vec{3}}}}) val_low_high = plot[1] diff --git a/src/basic_recipes/pie.jl b/src/basic_recipes/pie.jl index 43f0e1454c8..39783283fcc 100644 --- a/src/basic_recipes/pie.jl +++ b/src/basic_recipes/pie.jl @@ -3,7 +3,7 @@ Creates a pie chart from the given `values`. """ -@recipe Pie values begin +@recipe Pie (values,) begin "If `true`, the sum of all values is normalized to 2π (a full circle)." normalize = true color = :gray diff --git a/src/basic_recipes/poly.jl b/src/basic_recipes/poly.jl index 5462f4f9fa2..c7d4430e366 100644 --- a/src/basic_recipes/poly.jl +++ b/src/basic_recipes/poly.jl @@ -3,22 +3,34 @@ const PolyElements = Union{Polygon, MultiPolygon, Circle, Rect, AbstractMesh, Ve convert_arguments(::Type{<: Poly}, v::AbstractVector{<: PolyElements}) = (v,) convert_arguments(::Type{<: Poly}, v::Union{Polygon, MultiPolygon}) = (v,) -convert_arguments(::Type{<: Poly}, args...) = ([convert_arguments(Scatter, args...)[1]],) -function convert_arguments(::Type{<:Poly}, vertices::AbstractArray, indices::AbstractArray) - return convert_arguments(Mesh, vertices, indices) + +function convert_pointlike(args...) + return convert_arguments(PointBased(), args...) end function convert_arguments(::Type{<:Poly}, x::RealVector, y::RealVector) - return convert_arguments(PointBased(), x, y) + return convert_pointlike(x, y) +end + +function convert_arguments(::Type{<:Poly}, path::AbstractVector{<:VecTypes}) + return convert_pointlike(path) +end + +function convert_arguments(::Type{<:Poly}, path::BezierPath) + return convert_pointlike(path) +end + + +function convert_arguments(::Type{<:Poly}, vertices::AbstractArray, indices::AbstractArray) + return convert_arguments(Mesh, vertices, indices) end convert_arguments(::Type{<: Poly}, m::GeometryBasics.Mesh) = (m,) convert_arguments(::Type{<: Poly}, m::GeometryBasics.GeometryPrimitive) = (m,) function plot!(plot::Poly{<: Tuple{Union{GeometryBasics.Mesh, GeometryPrimitive}}}) - mesh!( - plot, lift(m -> convert_arguments(Mesh, m)[1], plot, plot[1]), + plot, plot[1], color = plot.color, colormap = plot.colormap, colorscale = plot.colorscale, diff --git a/src/basic_recipes/raincloud.jl b/src/basic_recipes/raincloud.jl index c99e54eed7d..767d6a32264 100644 --- a/src/basic_recipes/raincloud.jl +++ b/src/basic_recipes/raincloud.jl @@ -36,7 +36,7 @@ between each. - `jitter_width=0.05`: Determines the width of the scatter-plot bar in category x-axis absolute terms. """ -@recipe RainClouds category_labels data_array begin +@recipe RainClouds (category_labels, data_array) begin """ Can take values of `:left`, `:right`, determines where the violin plot will be, relative to the scatter points diff --git a/src/basic_recipes/series.jl b/src/basic_recipes/series.jl index cbd7fe51eee..7579c4f5a8f 100644 --- a/src/basic_recipes/series.jl +++ b/src/basic_recipes/series.jl @@ -11,7 +11,7 @@ Curves can be: If any of `marker`, `markersize`, `markercolor`, `strokecolor` or `strokewidth` is set != nothing, a scatterplot is added. """ -@recipe Series curves begin +@recipe Series (curves,) begin linewidth=2 color=:lighttest solid_color=nothing diff --git a/src/basic_recipes/spy.jl b/src/basic_recipes/spy.jl index feab9efc753..3e693000ea9 100644 --- a/src/basic_recipes/spy.jl +++ b/src/basic_recipes/spy.jl @@ -11,7 +11,7 @@ spy(x) spy(0..1, 0..1, x) ``` """ -@recipe Spy x y z begin +@recipe Spy (x, y, z) begin marker = automatic markersize = automatic framecolor = :black diff --git a/src/basic_recipes/streamplot.jl b/src/basic_recipes/streamplot.jl index 909eaf030e8..17602262e26 100644 --- a/src/basic_recipes/streamplot.jl +++ b/src/basic_recipes/streamplot.jl @@ -14,7 +14,7 @@ streamplot(v, -2..2, -2..2) ## Implementation See the function `Makie.streamplot_impl` for implementation details. """ -@recipe StreamPlot f limits begin +@recipe StreamPlot (f, limits) begin stepsize = 0.01 gridsize = (32, 32, 32) maxsteps = 500 diff --git a/src/basic_recipes/text.jl b/src/basic_recipes/text.jl index 70ede844dcb..54b231be875 100644 --- a/src/basic_recipes/text.jl +++ b/src/basic_recipes/text.jl @@ -4,6 +4,16 @@ function check_textsize_deprecation(@nospecialize(dictlike)) end end +# conversion stopper for previous methods +convert_arguments(::Type{<:Text}, gcs::AbstractVector{<:GlyphCollection}) = (gcs,) +convert_arguments(::Type{<:Text}, gc::GlyphCollection) = (gc,) +convert_arguments(::Type{<:Text}, vec::AbstractVector{<:Tuple{<:Any,<:Point}}) = (vec,) +convert_arguments(::Type{<:Text}, strings::AbstractVector{<:AbstractString}) = (strings,) +convert_arguments(::Type{<:Text}, string::AbstractString) = (string,) +# Fallback to PointBased +convert_arguments(::Type{<:Text}, args...) = convert_arguments(PointBased(), args...) + + function plot!(plot::Text) positions = plot[1] # attach a function to any text that calculates the glyph layout and stores it @@ -12,6 +22,9 @@ function plot!(plot::Text) linewidths = Observable(Float32[]; ignore_equal_values=true) linecolors = Observable(RGBAf[]; ignore_equal_values=true) lineindices = Ref(Int[]) + if !haskey(plot, :text) + attributes(plot)[:text] = plot[2] + end onany(plot, plot.text, plot.fontsize, plot.font, plot.fonts, plot.align, plot.rotation, plot.justification, plot.lineheight, plot.calculated_colors, @@ -71,19 +84,18 @@ function plot!(plot::Text) attrs = copy(plot.attributes) # remove attributes that are already in the glyphcollection - pop!(attrs, :position) + attributes(attrs)[:position] = positions pop!(attrs, :text) pop!(attrs, :align) pop!(attrs, :color) pop!(attrs, :calculated_colors) - t = text!(plot, glyphcollections; attrs..., position = positions) + t = text!(plot, attrs, glyphcollections) # remove attributes that the backends will choke on pop!(t.attributes, :font) pop!(t.attributes, :fonts) pop!(t.attributes, :text) linesegments!(plot, linesegs_shifted; linewidth = linewidths, color = linecolors, space = :pixel) - plot end @@ -131,12 +143,7 @@ function plot!(plot::Text{<:Tuple{<:AbstractString}}) plot end -# conversion stopper for previous methods -convert_arguments(::Type{<: Text}, gcs::AbstractVector{<:GlyphCollection}) = (gcs,) -convert_arguments(::Type{<: Text}, gc::GlyphCollection) = (gc,) -convert_arguments(::Type{<: Text}, vec::AbstractVector{<:Tuple{<:Any, <:Point}}) = (vec,) -convert_arguments(::Type{<: Text}, strings::AbstractVector{<:AbstractString}) = (strings,) -convert_arguments(::Type{<: Text}, string::AbstractString) = (string,) + # TODO: is this necessary? there seems to be a recursive loop with the above # function without these two interceptions, but I didn't need it before merging diff --git a/src/basic_recipes/timeseries.jl b/src/basic_recipes/timeseries.jl index 510f2d2abd1..3c48ebf1d9d 100644 --- a/src/basic_recipes/timeseries.jl +++ b/src/basic_recipes/timeseries.jl @@ -21,7 +21,7 @@ end ``` """ -@recipe TimeSeries signal begin +@recipe TimeSeries (signal,) begin history = 100 MakieCore.documented_attributes(Lines)... end diff --git a/src/basic_recipes/tooltip.jl b/src/basic_recipes/tooltip.jl index b22b2321035..32802bb027a 100644 --- a/src/basic_recipes/tooltip.jl +++ b/src/basic_recipes/tooltip.jl @@ -4,7 +4,7 @@ Creates a tooltip pointing at `position` displaying the given `string """ -@recipe Tooltip position begin +@recipe Tooltip (position,) begin # General text = "" "Sets the offset between the given `position` and the tip of the triangle pointing at that position." @@ -262,4 +262,4 @@ function plot!(p::Tooltip{<:Tuple{<:VecTypes}}) notify(p[1]) return p -end \ No newline at end of file +end diff --git a/src/basic_recipes/triplot.jl b/src/basic_recipes/triplot.jl index d83247f4a3e..153ecc99e49 100644 --- a/src/basic_recipes/triplot.jl +++ b/src/basic_recipes/triplot.jl @@ -5,7 +5,7 @@ Plots a triangulation based on the provided position or `Triangulation` from DelaunayTriangulation.jl. """ -@recipe Triplot triangles begin +@recipe Triplot (triangles,) begin # Toggles "Determines whether to plot the individual points. Note that this will only plot points included in the triangulation." show_points=false @@ -248,4 +248,4 @@ function data_limits(p::Triplot{<:Tuple{<:Vector{<:Point}}}) return data_limits(p.plots[1]) end end -boundingbox(p::Triplot{<:Tuple{<:Vector{<:Point}}}, space::Symbol = :data) = transform_bbox(p, data_limits(p)) \ No newline at end of file +boundingbox(p::Triplot{<:Tuple{<:Vector{<:Point}}}, space::Symbol = :data) = transform_bbox(p, data_limits(p)) diff --git a/src/basic_recipes/volumeslices.jl b/src/basic_recipes/volumeslices.jl index 4a8b19bf3a2..eaea8ec89c5 100644 --- a/src/basic_recipes/volumeslices.jl +++ b/src/basic_recipes/volumeslices.jl @@ -6,13 +6,13 @@ VolumeSlices Draws heatmap slices of the volume v """ -@recipe VolumeSlices x y z volume begin +@recipe VolumeSlices (x, y, z, volume) begin MakieCore.documented_attributes(Heatmap)... bbox_visible = true bbox_color = RGBAf(0.5, 0.5, 0.5, 0.5) end -function plot!(plot::VolumeSlices) +function Makie.plot!(plot::VolumeSlices) @extract plot (x, y, z, volume) replace_automatic!(plot, :colorrange) do map(extrema, volume) diff --git a/src/basic_recipes/voronoiplot.jl b/src/basic_recipes/voronoiplot.jl index 44c88606446..086302d2eca 100644 --- a/src/basic_recipes/voronoiplot.jl +++ b/src/basic_recipes/voronoiplot.jl @@ -9,7 +9,7 @@ Generates and plots a Voronoi tessalation from `heatmap`- or point-like data. The tessellation can also be passed directly as a `VoronoiTessellation` from DelaunayTriangulation.jl. """ -@recipe Voronoiplot vorn begin +@recipe Voronoiplot (vorn,) begin "Determines whether to plot the individual generators." show_generators=true smooth=false diff --git a/src/basic_recipes/waterfall.jl b/src/basic_recipes/waterfall.jl index 562eb301ae4..7387442e788 100644 --- a/src/basic_recipes/waterfall.jl +++ b/src/basic_recipes/waterfall.jl @@ -5,7 +5,7 @@ Plots a [waterfall chart](https://en.wikipedia.org/wiki/Waterfall_chart) to visu positive and negative components that add up to a net result as a barplot with stacked bars next to each other. """ -@recipe Waterfall x y begin +@recipe Waterfall (x, y) begin color = @inherit patchcolor dodge=automatic n_dodge=automatic diff --git a/src/colorsampler.jl b/src/colorsampler.jl index a1abf9cbd0b..c80753f932e 100644 --- a/src/colorsampler.jl +++ b/src/colorsampler.jl @@ -225,17 +225,18 @@ fig, ax, pl = barplot(1:3; color=1:3, colormap=Makie.Categorical(:viridis)) !!! warning This feature might change outside breaking releases, since the API is not yet finalized """ -struct Categorical{T} <: AbstractVector{RGBAf} - values::Vector{T} +struct Categorical + values::Any end -Categorical(values) = Categorical(to_colormap(values)) Base.getindex(c::Categorical, i) = c.values[i] Base.size(c::Categorical) = size(c.values) -_array_value_type(::Categorical{T}) where T = Vector{T} +_array_value_type(::Categorical) = Vector{eltype(values)} _array_value_type(A::AbstractArray{<:Number}) = typeof(A) _array_value_type(r::AbstractRange) = Vector{eltype(r)} # use vector instead, to have a few less types to worry about +to_colormap(x::Categorical) = to_colormap(x.values) +_to_colormap(x::Categorical) = to_colormap(x.values) _to_colormap(x::PlotUtils.ColorGradient) = to_colormap(x.colors) _to_colormap(x) = to_colormap(x) diff --git a/src/conversions.jl b/src/conversions.jl index 963f87e7b64..d5b38102943 100644 --- a/src/conversions.jl +++ b/src/conversions.jl @@ -1,58 +1,35 @@ ################################################################################ # Type Conversions # ################################################################################ +const RangeLike = Union{AbstractVector,ClosedInterval,Tuple{Real,Real}} - -const RangeLike = Union{AbstractVector, ClosedInterval, Tuple{Any,Any}} -# if no plot type based conversion is defined, we try using a trait -function convert_arguments(T::PlotFunc, args...; kw...) - ct = conversion_trait(T, args...) - try - convert_arguments(ct, args...; kw...) - catch e - if e isa MethodError - try - convert_arguments_individually(T, args...) - catch ee - if ee isa MethodError - error(""" - `Makie.convert_arguments` for the plot type $T and its conversion trait $ct was unsuccessful. - The signature that could not be converted was: - $(join("::" .* string.(typeof.(args)), ", ")) - Makie needs to convert all plot input arguments to types that can be consumed by the backends (typically Arrays with Float32 elements). - You can define a method for `Makie.convert_arguments` (a type recipe) for these types or their supertypes to make this set of arguments convertible (See http://docs.makie.org/stable/documentation/recipes/index.html). - Alternatively, you can define `Makie.convert_single_argument` for single arguments which have types that are unknown to Makie but which can be converted to known types and fed back to the conversion pipeline. - """) - else - rethrow(ee) - end - end - else - rethrow(e) - end +function convert_arguments(CT::ConversionTrait, args...) + expanded = expand_dimensions(CT, args...) + if !isnothing(expanded) + return convert_arguments(CT, expanded...) end -end -# in case no trait matches we try to convert each individual argument -# and reconvert the whole tuple in order to handle missings centrally, e.g. -function convert_arguments_individually(T::PlotFunc, args...) - # convert each single argument until it doesn't change type anymore - single_converted = map(recursively_convert_argument, args) - # if the type of args hasn't changed this function call didn't help and we error - if typeof(single_converted) == typeof(args) - throw(MethodError(convert_arguments, (T, args...))) - end - # otherwise we try converting our newly single-converted args again because - # now a normal conversion method might work again - convert_arguments(T, single_converted...) -end - -function recursively_convert_argument(x) - newx = convert_single_argument(x) - if typeof(newx) == typeof(x) - return x - else - return recursively_convert_argument(newx) + return args +end + +function convert_arguments(T::Type{<:AbstractPlot}, args...; kw...) + # landing here means, that there is no matching `convert_arguments` method for the plot type + # Meaning, it needs to be a conversion trait, or it needs single_convert_arguments or expand_dimensions + CT = conversion_trait(T, args...) + + # Try to expand dimensions first, as this is the most basic step! + expanded = expand_dimensions(CT, args...) + !isnothing(expanded) && return convert_arguments(T, expanded...; kw...) + # Try single argument convert after + arguments_converted = map(convert_single_argument, args) + if arguments_converted !== args + # This changed something, so we start back with convert_arguments + return convert_arguments(T, arguments_converted...; kw...) end + # next we try to convert the arguments with the conversion trait + trait_converted = convert_arguments(CT, args...; kw...) + trait_converted !== args && return convert_arguments(T, trait_converted...; kw...) + # else we give up! + return args end ################################################################################ @@ -60,7 +37,7 @@ end ################################################################################ # if no specific conversion is defined, we don't convert -convert_single_argument(x) = x +convert_single_argument(@nospecialize(x)) = x # replace missings with NaNs function convert_single_argument(a::AbstractArray{<:Union{Missing, <:Real}}) @@ -73,6 +50,12 @@ function convert_single_argument(a::AbstractArray{<:Union{Missing, <:Point{N, PT return Point{N,T}[ismissing(x) ? Point{N,T}(NaN) : Point{N,T}(x) for x in a] end +convert_single_argument(a::AbstractArray{Any}) = convert_single_argument([x for x in a]) +# Leave concretely typed vectors alone (AbstractArray{<:Union{Missing, <:Real}} also dispatches for `Vector{Float32}`) +convert_single_argument(a::AbstractArray{T}) where {T<:Real} = a +convert_single_argument(a::AbstractArray{<:Point{N, T}}) where {N, T} = a + + ################################################################################ # PointBased # ################################################################################ @@ -95,7 +78,12 @@ function convert_arguments(::PointBased, position::VecTypes{N, T}) where {N, T < end function convert_arguments(::PointBased, positions::AbstractVector{<: VecTypes{N, T}}) where {N, T <: Real} - return (float_convert(positions),) + # VecTypes{N, T} will have T undefined if tuple has different number types + _T = @isdefined(T) ? T : Float64 + if !(N in (2, 3)) + throw(ArgumentError("Only 2D and 3D points are supported.")) + end + return (elconvert(Point{N, float_type(_T)}, positions),) end function convert_arguments(::PointBased, positions::SubArray{<: VecTypes, 1}) @@ -107,7 +95,7 @@ end Enables to use scatter like a surface plot with x::Vector, y::Vector, z::Matrix spanning z over the grid spanned by x y """ -function convert_arguments(::PointBased, x::RealArray, y::RealVector, z::RealArray) +function convert_arguments(::PointBased, x::RealArray, y::RealVector, z::RealMatrix) T = float_type(x, y, z) (vec(Point{3, T}.(x, y', z)),) end @@ -118,7 +106,7 @@ function convert_arguments(::PointBased, x::RealVector, y::RealVector, z::RealVe end -function convert_arguments(p::PointBased, x::AbstractInterval, y::AbstractInterval, z::RealArray) +function convert_arguments(p::PointBased, x::AbstractInterval, y::AbstractInterval, z::RealMatrix) return convert_arguments(p, to_linspace(x, size(z, 1)), to_linspace(y, size(z, 2)), z) end @@ -129,7 +117,7 @@ Takes vectors `x`, `y`, and `z` and turns it into a vector of 3D points of the v from `x`, `y`, and `z`. `P` is the plot Type (it is optional). """ -function convert_arguments(::PointBased, x::RealArray, y::RealMatrix, z::RealArray) +function convert_arguments(::PointBased, x::RealArray, y::RealMatrix, z::RealMatrix) T = float_type(x, y, z) (vec(Point{3, T}.(x, y, z)),) end @@ -149,18 +137,10 @@ function convert_arguments(p::PointBased, x::GeometryPrimitive{Dim, T}) where {D return convert_arguments(p, decompose(Point{Dim, float_type(T)}, x)) end -function convert_arguments(::PointBased, pos::AbstractMatrix{<: Real}) +function convert_arguments(::PointBased, pos::RealMatrix) (to_vertices(pos),) end -""" - convert_arguments(P, y)::Vector -Takes vector `y` and generates a range from 1 to the length of `y`, for plotting on -an arbitrary `x` axis. - -`P` is the plot Type (it is optional). -""" -convert_arguments(P::PointBased, y::RealVector) = convert_arguments(P, keys(y), y) """ convert_arguments(P, x, y)::(Vector) @@ -338,7 +318,7 @@ function edges(v::AbstractVector{T}) where T end end -function adjust_axes(::CellGrid, x::AbstractVector{<:Number}, y::AbstractVector{<:Number}, z::AbstractMatrix) +function adjust_axes(::CellGrid, x::RealVector, y::RealVector, z::AbstractMatrix) x̂, ŷ = map((x, y), size(z)) do v, sz return length(v) == sz ? edges(v) : v end @@ -347,6 +327,7 @@ end adjust_axes(::VertexGrid, x, y, z) = x, y, z + """ convert_arguments(ct::GridBased, x::VecOrMat, y::VecOrMat, z::Matrix) @@ -361,7 +342,7 @@ function convert_arguments(ct::GridBased, x::AbstractVecOrMat{<:Real}, y::Abstra return (float_convert(nx), float_convert(ny), el32convert(nz)) end -convert_arguments(ct::VertexGrid, x::AbstractMatrix, y::AbstractMatrix) = convert_arguments(ct, x, y, zeros(size(y))) +convert_arguments(ct::VertexGrid, x::RealMatrix, y::RealMatrix) = convert_arguments(ct, x, y, zeros(size(y))) """ convert_arguments(P, x::RangeLike, y::RangeLike, z::AbstractMatrix) @@ -373,41 +354,33 @@ function convert_arguments(P::GridBased, x::RangeLike, y::RangeLike, z::Abstract convert_arguments(P, to_linspace(x, size(z, 1)), to_linspace(y, size(z, 2)), z) end -""" - convert_arguments(::ImageLike, mat::AbstractMatrix) +function print_range_warning(side::String, value) + @warn "Encountered an `AbstractVector` with value $value on side $side in `convert_arguments` for the `ImageLike` trait. + Using an `AbstractVector` to specify one dimension of an `ImageLike` is deprecated because `ImageLike` sides always need exactly two values, start and stop. + Use interval notation `start .. stop` or a two-element tuple `(start, stop)` instead." +end -Generates `ClosedInterval`s of size `0 .. size(mat, 1/2)` as x and y values. -""" -function convert_arguments(::ImageLike, data::AbstractMatrix{<: Union{Real, Colorant}}) - n, m = Float32.(size(data)) - return (Float32(0) .. n, Float32(0) .. m, el32convert(data)) +to_interval(x::Tuple{<: Real, <: Real}) = float_convert(x[1]) .. float_convert(x[2]) +function to_interval(x::Union{Interval,AbstractVector,ClosedInterval}) + return float_convert(minimum(x)) .. float_convert(maximum(x)) end -function print_range_warning(side::String, value) - @warn "Encountered an `AbstractVector` with value $value on side $side in `convert_arguments` for the `ImageLike` trait. Using an `AbstractVector` to specify one dimension of an `ImageLike` is deprecated because `ImageLike` sides always need exactly two values, start and stop. Use interval notation `start .. stop` or a two-element tuple `(start, stop)` instead." + +function to_interval(x, dim) + # having minimum and maximum here actually invites bugs + x isa AbstractVector && print_range_warning(dim, x) + return to_interval(x) end + + function convert_arguments(::ImageLike, xs::RangeLike, ys::RangeLike, data::AbstractMatrix{<:Union{Real,Colorant}}) - if xs isa AbstractVector - print_range_warning("x", xs) - end - if ys isa AbstractVector - print_range_warning("y", ys) - end - # having minimum and maximum here actually invites bugs - _interval(v::Union{Interval,AbstractVector}) = float_convert(minimum(v)) .. float_convert(maximum(v)) - _interval(t::Tuple{Any, Any}) = float_convert(t[1]) .. float_convert(t[2]) - x = _interval(xs) - y = _interval(ys) + x = to_interval(xs, "x") + y = to_interval(ys, "y") return (x, y, el32convert(data)) end -function convert_arguments(ct::GridBased, data::AbstractMatrix{<:Union{Real,Colorant}}) - n, m = Float32.(size(data)) - convert_arguments(ct, 1f0 .. n, 1f0 .. m, el32convert(data)) -end - function convert_arguments(ct::GridBased, x::RealVector, y::RealVector, z::RealVector) if !(length(x) == length(y) == length(z)) error("x, y and z need to have the same length. Lengths are $(length.((x, y, z)))") @@ -433,7 +406,6 @@ function convert_arguments(ct::GridBased, x::RealVector, y::RealVector, z::RealV return convert_arguments(ct, x_centers, y_centers, zs) end - """ convert_arguments(P, x, y, f)::(Vector, Vector, Matrix) @@ -452,23 +424,11 @@ end # VolumeLike # ################################################################################ -""" - convert_arguments(P, Matrix)::Tuple{ClosedInterval, ClosedInterval, ClosedInterval, Matrix} - -Takes an array of `{T, 3} where T`, converts the dimensions `n`, `m` and `k` into `ClosedInterval`, -and stores the `ClosedInterval` to `n`, `m` and `k`, plus the original array in a Tuple. - -`P` is the plot Type (it is optional). -""" -function convert_arguments(::VolumeLike, data::RealArray{3}) - n, m, k = Float32.(size(data)) - return (0f0 .. n, 0f0 .. m, 0f0 .. k, el32convert(data)) -end - function convert_arguments(::VolumeLike, x::RangeLike, y::RangeLike, z::RangeLike, data::RealArray{3}) - return (el32convert(x), el32convert(y), el32convert(z), el32convert(data)) + return (to_interval(x, "x"), to_interval(y, "y"), to_interval(z, "z"), el32convert(data)) end + """ convert_arguments(P, x, y, z, i)::(Vector, Vector, Vector, Matrix) @@ -476,8 +436,8 @@ Takes 3 `AbstractVector` `x`, `y`, and `z` and the `AbstractMatrix` `i`, and put `P` is the plot Type (it is optional). """ -function convert_arguments(::VolumeLike, x::RealVector, y::AbstractVector, z::RealVector, i::RealArray{3}) - (el32convert(x), el32convert(y), el32convert(z), el32convert(i)) +function convert_arguments(::VolumeLike, x::RealVector, y::RealVector, z::RealVector, i::RealArray{3}) + (to_interval(x, "x"), to_interval(y, "y"), to_interval(z, "z"), el32convert(i)) end ################################################################################ @@ -634,13 +594,13 @@ end # Allow the user to pass a function to `arrows` which determines the direction # and magnitude of the arrows. The function must accept `Point2f` as input. # and return Point2f or Vec2f or some array like structure as output. -function convert_arguments(::Type{<:Arrows}, x::AbstractVector, y::AbstractVector, f::Function) +function convert_arguments(::Type{<:Arrows}, x::RealVector, y::RealVector, f::Function) points = Point2{float_type(x, y)}.(x, y') f_out = Vec2{float_type(x, y)}.(f.(points)) return (vec(points), vec(f_out)) end -function convert_arguments(::Type{<:Arrows}, x::AbstractVector, y::AbstractVector, z::AbstractVector, +function convert_arguments(::Type{<:Arrows}, x::RealVector, y::RealVector, z::RealVector, f::Function) points = [Point3{float_type(x, y, z)}(x, y, z) for x in x, y in y, z in z] f_out = Vec3{float_type(x, y, z)}.(f.(points)) @@ -655,7 +615,7 @@ spanned by `x`, `y` and `z`, and puts `x`, `y`, `z` and `f(x,y,z)` in a Tuple. `P` is the plot Type (it is optional). """ -function convert_arguments(::VolumeLike, x::AbstractVector, y::AbstractVector, z::AbstractVector, f::Function) +function convert_arguments(VL::VolumeLike, x::RealVector, y::RealVector, z::RealVector, f::Function) if !applicable(f, x[1], y[1], z[1]) error("You need to pass a function with signature f(x, y, z). Found: $f") end @@ -663,14 +623,15 @@ function convert_arguments(::VolumeLike, x::AbstractVector, y::AbstractVector, z A = (x, y, z)[i] return reshape(A, ntuple(j -> j != i ? 1 : length(A), Val(3))) end - return (el32convert(x), el32convert(y), el32convert(z), el32convert(f.(_x, _y, _z))) + # TODO only allow unitranges to map over since we dont support irregular x/y/z values + return (map(to_interval, (x, y, z))..., el32convert.(f.(_x, _y, _z))) end -function convert_arguments(P::PlotFunc, r::AbstractVector, f::Function) +function convert_arguments(P::Type{<:AbstractPlot}, r::RealVector, f::Function) return convert_arguments(P, r, map(f, r)) end -function convert_arguments(P::PlotFunc, i::AbstractInterval, f::Function) +function convert_arguments(P::Type{<:AbstractPlot}, i::AbstractInterval, f::Function) x, y = PlotUtils.adapted_grid(f, endpoints(i)) return convert_arguments(P, x, y) end @@ -704,10 +665,11 @@ function elconvert(::Type{T}, x::AbstractArray{<: Union{Missing, <:Real}}) where end end +float_type(args::Type) = error("Type $(args) not supported") float_type(a, rest...) = float_type(typeof(a), map(typeof, rest)...) float_type(a::AbstractArray, rest...) = float_type(float_type(a), map(float_type, rest)...) float_type(a::AbstractPolygon, rest...) = float_type(float_type(a), map(float_type, rest)...) -float_type(a::Type, rest::Type...) = float_type(promote_type(a, rest...)) +float_type(a::Type, rest::Type...) = promote_type(map(float_type, (a, rest...))...) float_type(::Type{Float64}) = Float64 float_type(::Type{Float32}) = Float32 float_type(::Type{<:Real}) = Float64 @@ -719,7 +681,9 @@ float_type(::Type{NTuple{N, T}}) where {N,T} = Point{N,float_type(T)} float_type(::Type{Tuple{T1, T2}}) where {T1,T2} = Point2{promote_type(float_type(T1), float_type(T2))} float_type(::Type{Tuple{T1, T2, T3}}) where {T1,T2,T3} = Point3{promote_type(float_type(T1), float_type(T2), float_type(T3))} float_type(::Type{Union{Missing, T}}) where {T} = float_type(T) -float_type(::Type{Union{Nothing, T}}) where {T} = float_type(T) +float_type(::Type{Union{Nothing,T}}) where {T} = float_type(T) +float_type(::Type{ClosedInterval{T}}) where {T} = ClosedInterval{T} +float_type(::Type{ClosedInterval}) = ClosedInterval{Float32} float_type(::AbstractArray{T}) where {T} = float_type(T) float_type(::AbstractPolygon{N, T}) where {N, T} = Point{N, float_type(T)} @@ -806,7 +770,7 @@ function to_vertices(verts::AbstractVector{<: VecTypes{N, T}}) where {N, T} return map(Point{N, float_type(T)}, verts) end -function to_vertices(verts::AbstractMatrix{<: Number}) +function to_vertices(verts::AbstractMatrix{<: Real}) if size(verts, 1) in (2, 3) to_vertices(verts, Val(1)) elseif size(verts, 2) in (2, 3) diff --git a/src/dim-converts/categorical-integration.jl b/src/dim-converts/categorical-integration.jl new file mode 100644 index 00000000000..126aa859231 --- /dev/null +++ b/src/dim-converts/categorical-integration.jl @@ -0,0 +1,160 @@ +""" + CategoricalConversion(; sortby=identity) + +Categorical conversion. Gets chosen automatically only for `Categorical(array_of_objects)` right now. +The categories work with any sortable value though, so one can always do `Axis(fig; dim1_conversion=CategoricalConversion())`, +to use it for other categories. +One can use `CategoricalConversion(sortby=func)`, to change the sorting, or make unsortable objects sortable. + +# Examples + +```julia +# Ticks get chosen automatically as categorical +scatter(1:4, Categorical(["a", "b", "c", "a"])) +``` + +```julia +# Explicitely set them for other types: +struct Named + value +end +Base.show(io::IO, s::SomeStruct) = println(io, "[\$(s.value)]") + +conversion = Makie.CategoricalConversion(sortby=x->x.value) +barplot(Named.([:a, :b, :c]), 1:3, axis=(dim1_conversion=conversion,)) +``` +""" +struct CategoricalConversion <: AbstractDimConversion + # TODO, use ordered sets/dicts? + # I've run into problems with OrderedCollections.jl + # Which seems to be the only ordered set/dict implementation + # It's another dependency as well, so right now we just use vectors + sets::Vector{Pair{String,Vector{Any}}} + category_to_int::Observable{Dict{Any,Int}} + int_to_category::Vector{Pair{Int,Any}} + sortby::Union{Nothing,Function} +end + +function CategoricalConversion(; sortby=nothing) + return CategoricalConversion(Pair{String,Vector{Any}}[], + Observable(Dict{Any,Int}(); ignore_equal_values=true), + Pair{Int,Any}[], + sortby) +end + +expand_dimensions(::PointBased, y::Categorical) = (keys(y.values), y) +needs_tick_update_observable(conversion::CategoricalConversion) = conversion.category_to_int +MakieCore.should_dim_convert(::Type{Categorical}) = true +create_dim_conversion(::Type{Categorical}) = CategoricalConversion(; sortby=identity) + +function recalculate_categories!(conversion::CategoricalConversion) + all_categories = [] + for (id, set) in conversion.sets + append!(all_categories, set) + end + unique!(all_categories) + if !isnothing(conversion.sortby) + sort!(all_categories; by=conversion.sortby) + end + empty!(conversion.category_to_int[]) + empty!(conversion.int_to_category) + i2c = pairs(all_categories) + append!(conversion.int_to_category, i2c) + return merge!(conversion.category_to_int[], Dict(reverse(p) for p in i2c)) +end + + +get_values(x) = x +get_values(x::Categorical) = x.values + +function convert_dim_value(conversion::CategoricalConversion, value::Categorical) + return getindex.(Ref(conversion.category_to_int[]), get_values(value)) +end + +# TODO, use ordered sets/dicts? +function dict_get!(f, dict, key) + idx = findfirst(x -> x[1] == key, dict) + if isnothing(idx) + val = f() + push!(dict, key => val) + return val + else + return dict[idx][2] + end +end + +function dict_setindex!(dict, key, value) + idx = findfirst(x -> x[1] == key, dict) + if isnothing(idx) + push!(dict, key => value) + else + dict[idx] = key => value + end +end + +function convert_dim_value(conversion::CategoricalConversion, value) + if !haskey(conversion.category_to_int[], value) + set = dict_get!(() -> [], conversion.sets, "") + push!(set, value) + unique!(set) + recalculate_categories!(conversion) + notify(conversion.category_to_int) + end + return conversion.category_to_int[][value] +end + +function convert_categorical(conversion::CategoricalConversion, value) + return conversion.category_to_int[][value] +end + +function convert_categorical(conversion::CategoricalConversion, value::Integer) + return conversion.category_to_int[][value] +end + +function convert_dim_observable(conversion::CategoricalConversion, values_obs::Observable, deregister) + prev_values = [] + # This is a bit tricky... + # We need to recalculate the categories on each values_obs update, + # but we also need to update the cat->int mapping each time the categories get recalculated + # So category_to_int needs to be notified every time values_obs introduces new categories + # but we don't want to recalculate cat->int two times, when value changes + category_to_int + # so we introduce a placeholder observable that gets triggered when an update is needed + # outside of category_to_int updating + update_needed = Observable(nothing) + f = on(values_obs; update=true) do values + new_values = unique!(Any[get_values(values)...]) + if new_values != prev_values + dict_setindex!(conversion.sets, values_obs.id, new_values) + prev_values = new_values + recalculate_categories!(conversion) + notify(conversion.category_to_int) + else + # If values doesn't introduce new categories, + # it still may need updating (["a", "a", "b"] -> ["a", "b"]) + # If we'd really clever, we'd also track prev_values not as a set + notify(update_needed) + end + return + end + push!(deregister, f) + # So now we update when either category_to_int changes, or + # when values changes and an update is needed + return map(update_needed, conversion.category_to_int) do _, categories + return convert_categorical.(Ref(conversion), get_values(values_obs[])) + end +end + +function get_ticks(conversion::CategoricalConversion, ticks, scale, formatter, vmin, vmax) + scale != identity && error("Scale $(scale) not supported for categorical conversion") + if ticks isa Automatic + # TODO, do we want to support leaving out conversion? Right now, every category will become a tick + # Maybe another function like filter? + categories = last.(conversion.int_to_category) + else + categories = ticks + end + # TODO filter out ticks greater vmin vmax? + numbers = convert_dim_value.(Ref(conversion), categories) + labels_str = formatter isa Automatic ? string.(categories) : get_ticklabels(formatter, categories) + return numbers, labels_str +end diff --git a/src/dim-converts/dates-integration.jl b/src/dim-converts/dates-integration.jl new file mode 100644 index 00000000000..449ad0b699e --- /dev/null +++ b/src/dim-converts/dates-integration.jl @@ -0,0 +1,115 @@ +""" + number_to_date(::Type{T}, i::Int) + +Attempts to reconstruct a Dates type by inverting `Dates.value(obj::T)`. +""" +number_to_date(::Type{Time}, i) = Time(Nanosecond(round(Int64, Float64(i)))) # TODO, lossless TwicePrecision -> Nanosecond +number_to_date(::Type{Date}, i) = Date(Dates.UTInstant{Day}(Day(round(Int64, Float64(i))))) +number_to_date(::Type{DateTime}, i) = DateTime(Dates.UTM(round(Int64, Float64(i)))) + +date_to_number(::Type{T}, value::Dates.AbstractTime) where {T} = Dates.value(value) +date_to_number(value::Dates.AbstractTime) = Dates.value(value) + +# Allow to plot quantities into a Time unit axis +function date_to_number(::Type{Time}, value::Unitful.Quantity) + isnan(value) && return NaN + nanis = Nanosecond(round(u"ns", value)) + return Dates.value(Time(nanis)) +end + +""" + DateTimeConversion(type=Automatic; k_min=automatic, k_max=automatic, k_ideal=automatic) + +Creates conversion and conversions for Date, DateTime and Time. For other time units one should use `UnitfulConversion`, which work with e.g. Seconds. + +For DateTimes `PlotUtils.optimize_datetime_ticks` is used for getting the conversion, otherwise `axis.(x/y)ticks` are used on the integer representation of the date. + +# Arguments + +- `type=automatic`: when left at automatic, the first plot into the axis will determine the type. Otherwise, one can set this to `Time`, `Date`, or `DateTime`. + +# Examples + +```julia +date_time = DateTime("2021-10-27T11:11:55.914") +date_time_range = range(date_time, step=Week(5), length=10) +# Automatically chose xticks as DateTeimeTicks: +scatter(date_time_range, 1:10) + +# explicitely chose DateTimeConversion and use it to plot unitful values into it and display in the `Time` format: +using Makie.Unitful +conversion = Makie.DateTimeConversion(Time) +scatter(1:4, (1:4) .* u"s", axis=(dim2_conversion=conversion,)) +``` +""" +struct DateTimeConversion <: AbstractDimConversion + # first element in tuple is the time type we converted from, which can be: + # Time, Date, DateTime + # Second entry in tuple is a value we use to normalize the number range, + # so that they fit into float32 + type::Observable{DataType} + function DateTimeConversion(type=Automatic) + obs = Observable{DataType}(type; ignore_equal_values=true) + return new(obs) + end +end + +expand_dimensions(::PointBased, y::AbstractVector{<:Dates.AbstractTime}) = (keys(y), y) +needs_tick_update_observable(conversion::DateTimeConversion) = conversion.type +create_dim_conversion(::Type{<:Dates.AbstractTime}) = DateTimeConversion() +MakieCore.should_dim_convert(::Type{<:Dates.AbstractTime}) = true + + +function convert_dim_value(conversion::DateTimeConversion, value::Dates.TimeType) + return date_to_number(conversion.type[], value) +end + +function convert_dim_value(conversion::DateTimeConversion, value::AbstractArray) + return date_to_number.(conversion.type[], value) +end + +function convert_dim_observable(conversion::DateTimeConversion, values::Observable, deregister) + T = conversion.type[] + eltype = MakieCore.get_element_type(values[]) + if T <: Automatic + new_type = eltype + conversion.type[] = new_type + elseif T != eltype + if !(T <: Time && eltype <: Unitful.Quantity) + error("Plotting unit $(eltype) into axis with type $(T) not supported.") + end + end + result = map(values, conversion.type) do vals, T + return date_to_number.(T, vals) + end + append!(deregister, result.inputs) + return result +end + +function get_ticks(conversion::DateTimeConversion, ticks, scale, formatter, vmin, vmax) + + if scale != identity + error("$(scale) scale not supported for DateTimeConversion") + end + T = conversion.type[] + # When automatic, we haven't actually plotted anything yet, so no unit chosen + # in that case, we can't really have any conversion + T <: Automatic && return [], [] + + if T <: DateTime + if ticks isa WilkinsonTicks + k_min = formatter.k_min + k_max = formatter.k_max + else + k_min = 2 + k_max = 3 + end + conversion, dates = PlotUtils.optimize_datetime_ticks(vmin, vmax; k_min=k_min, k_max=k_max) + return conversion, dates + else + # TODO implement proper ticks for Time Date + tickvalues = get_tickvalues(formatter, scale, vmin, vmax) + dates = number_to_date.(T, round.(Int64, tickvalues)) + return tickvalues, string.(dates) + end +end diff --git a/src/dim-converts/dim-converts.jl b/src/dim-converts/dim-converts.jl new file mode 100644 index 00000000000..d4c32829311 --- /dev/null +++ b/src/dim-converts/dim-converts.jl @@ -0,0 +1,203 @@ +abstract type AbstractDimConversion end + +struct NoDimConversion <: AbstractDimConversion end + +struct DimConversions + conversions::NTuple{3,Observable{Union{Nothing,AbstractDimConversion}}} + function DimConversions() + conversions = map((1, 2, 3)) do i + Observable{Union{Nothing,AbstractDimConversion}}(nothing) + end + return new(conversions) + end +end + +dim_observable(conversions::DimConversions, dim::Int) = conversions.conversions[dim] + +function Base.getindex(conversions::DimConversions, i::Int) + return conversions.conversions[i][] +end + +function Base.setindex!(conversions::DimConversions, value::Observable, i::Int) + on(value; update=true) do val + conversions[i] = val + end +end + +function Base.setindex!(conversions::DimConversions, value, i::Int) + isnothing(value) && return # ignore no conversions + conversions[i] === value && return # ignore same conversion + if isnothing(conversions[i]) + # only set new conversion if there is none yet + conversions.conversions[i][] = value + return + else + throw(ArgumentError("Cannot change dim conversion for dimension $i, since it already is set to a conversion: $(conversions[i]).")) + end +end + + +## Interface to be overloaded for any AbstractDimConversion type +function convert_dim_value(conversions::DimConversions, dim::Int, value) + if isnothing(conversions[dim]) + return value + end + return convert_dim_value(conversions[dim], value) +end + + +function convert_dim_value(axislike::AbstractAxis, dim::Int, value) + return convert_dim_value(get_conversions(axislike), dim, value) +end + +convert_dim_value(::NoDimConversion, value) = value +function convert_dim_value(conversion::AbstractDimConversion, value, deregister) + error("AbstractDimConversion $(typeof(conversion)) not supported for value of type $(typeof(value))") +end + +using MakieCore: should_dim_convert + +# Return instance of AbstractDimConversion for a given type +create_dim_conversion(argument_eltype::DataType) = NoDimConversion() +MakieCore.should_dim_convert(::Type{<:Real}) = true +function convert_dim_observable(::NoDimConversion, value::Observable, deregister) + return value +end + +# get_ticks needs overloading for Dim Conversion +# Which gets ignored for no conversion/nothing +function get_ticks(::Union{Nothing,NoDimConversion}, ticks, scale, formatter, vmin, vmax) + return get_ticks(ticks, scale, formatter, vmin, vmax) +end + +# The below is defined in MakieCore, to be accessible by `@recipe` +# MakieCore.should_dim_convert(eltype) = false + + +# Recursively gets the dim convert from the plot +# This needs to be recursive to allow recipes to use dim converst +# TODO, should a recipe always set the dim convert to it's parent? +get_conversions(any) = nothing + +function get_conversions(ax::AbstractAxis) + if hasproperty(ax, :scene) + return get_conversions(ax.scene) + else + return nothing + end +end +function get_conversions(plot::Plot) + if haskey(plot.kw, :dim_conversions) + return to_value(plot.kw[:dim_conversions]) + else + for elem in plot.plots + x = get_conversions(elem) + isnothing(x) || return x + end + end + return nothing +end + +# For e.g. Axis attributes +function get_conversions(attr::Union{Attributes, Dict, NamedTuple}) + conversions = DimConversions() + for i in 1:3 + dim_sym = Symbol("dim$(i)_conversion") + if haskey(attr, dim_sym) + conversions[i] = to_value(attr[dim_sym]) + end + end + return conversions +end + +function dim_conversion_from_args(values) + return create_dim_conversion(MakieCore.get_element_type(values)) +end + +function connect_conversions!(new_conversions::DimConversions, ax::AbstractAxis) + for i in 1:3 + dim_sym = Symbol("dim$(i)_conversion") + if hasproperty(ax, dim_sym) + # merge + ax_conversion = getproperty(ax, dim_sym) + new_conversions[i] = ax_conversion + # update in case new_conversions has a new conversion + getproperty(ax, dim_sym)[] = new_conversions[i] + deregister = nothing + # if the conversion changes, update the axis as well. + # This should only ever happen once, since conversions are mutable after setting it to a new value + deregister = on(dim_observable(new_conversions, i)) do val + getproperty(ax, dim_sym)[] = val + off(deregister) + end + end + end +end + +function connect_conversions!(conversions::DimConversions, new_conversions::DimConversions) + for i in 1:3 + conversions[i] = new_conversions.conversions[i] + end +end + +# If axis conversion has global state which needs an update of the tick values, +# This functions needs to be overloaded, returning an observable that updates +# When ticks need to be updated. The concrete value doesn't matterm, since the AbstractDimConversion type will get passed to get_ticks regardless +#= + obs = needs_tick_update_observable(dim_convert) # make sure we update tick calculation when needed + ticks = map(obs, ...) do _, args... + return get_ticks(dim_convert, args...) + end +=# +needs_tick_update_observable(x) = nothing + +function needs_tick_update_observable(conversion::Observable) + if isnothing(conversion[]) + # At any point, conversion may change from nothing to an actual AbstractDimConversion + # so we need to listen for that change and then listen to the updates from that conversion. + # This should only ever happen once, since you can only change a conversion once, IFF it was nothing. + tick_update = Observable{Any}(nothing) + deregister = nothing + deregister = on(conversion) do conversion + if !isnothing(conversion) + obs = needs_tick_update_observable(conversion) + if !isnothing(obs) + connect!(tick_update, obs) + end + # this one doesn't need to listen anymore, since this update can only happen once + off(deregister) + end + end + return tick_update + else + return needs_tick_update_observable(conversion[]) + end +end + +function try_dim_convert(P::Type{<:Plot}, PTrait::ConversionTrait, user_attributes, args_obs::Tuple, deregister) + # Only 2 and 3d conversions are supported, and only + if !(length(args_obs) in (2, 3)) + return args_obs + end + converts = to_value(get!(() -> DimConversions(), user_attributes, :dim_conversions)) + return ntuple(length(args_obs)) do i + arg = args_obs[i] + argval = to_value(arg) + # We only convert if we have a conversion struct (which isn't NoDimConversion), + # or if we we should dim_convert + if !isnothing(converts[i]) || should_dim_convert(P, argval) || should_dim_convert(PTrait, argval) + return convert_dim_observable(converts, i, arg, deregister) + end + return arg + end +end + +function convert_dim_observable(conversions::DimConversions, dim::Int, value::Observable, deregister) + conversion = conversions[dim] + if !(conversion isa Union{Nothing,NoDimConversion}) + return convert_dim_observable(conversion, value, deregister) + end + c = dim_conversion_from_args(value[]) + conversions[dim] = c + return convert_dim_observable(c, value, deregister) +end diff --git a/src/dim-converts/unitful-integration.jl b/src/dim-converts/unitful-integration.jl new file mode 100644 index 00000000000..7e84d451b9f --- /dev/null +++ b/src/dim-converts/unitful-integration.jl @@ -0,0 +1,198 @@ +using Dates, Observables +import Unitful +using Unitful: Quantity, @u_str, uconvert, ustrip + +const SupportedUnits = Union{Period,Unitful.Quantity,Unitful.Units} + +expand_dimensions(::PointBased, y::AbstractVector{<:SupportedUnits}) = (keys(y), y) +create_dim_conversion(::Type{<:SupportedUnits}) = UnitfulConversion() +MakieCore.should_dim_convert(::Type{<:SupportedUnits}) = true + +const UNIT_POWER_OF_TENS = sort!(collect(keys(Unitful.prefixdict))) +const TIME_UNIT_NAMES = [:yr, :wk, :d, :hr, :minute, :s, :ds, :cs, :ms, :μs, :ns, :ps, :fs, :as, :zs, :ys] + +base_unit(q::Quantity) = base_unit(typeof(q)) +base_unit(::Type{Quantity{NumT, DimT, U}}) where {NumT, DimT, U} = base_unit(U) +base_unit(::Type{Unitful.FreeUnits{U, DimT, nothing}}) where {DimT, U} = U[1] +base_unit(::Unitful.FreeUnits{U, DimT, nothing}) where {DimT, U} = U[1] +base_unit(x::Unitful.Unit) = x +base_unit(x::Period) = base_unit(Quantity(x)) + +unit_string(::Type{T}) where T <: Unitful.AbstractQuantity = string(Unitful.unit(T)) +unit_string(unit::Type{<: Unitful.FreeUnits}) = string(unit()) +unit_string(unit::Unitful.FreeUnits) = unit_string(base_unit(unit)) +unit_string(unit::Unitful.Unit) = string(unit) +unit_string(::Union{Number, Nothing}) = "" + +unit_string_long(unit) = unit_string_long(base_unit(unit)) +unit_string_long(::Unitful.Unit{Sym, D}) where {Sym, D} = string(Sym) + +function eltype_extrema(values) + isempty(values) && return (eltype(values), nothing) + + new_eltype = typeof(first(values)) + new_min = new_max = first(values) + + for elem in Iterators.drop(values, 1) + new_eltype = promote_type(new_eltype, typeof(elem)) + new_min = min(elem, new_min) + new_max = max(elem, new_max) + end + return new_eltype, (new_min, new_max) +end + +function new_unit(unit, values) + new_eltype, extrema = eltype_extrema(values) + # empty vector case: + isnothing(extrema) && return nothing + new_min, new_max = extrema + if new_eltype <: Union{Quantity, Period} + qmin = Quantity(new_min) + qmax = Quantity(new_max) + return best_unit(qmin, qmax) + end + + new_eltype <: Number && isnothing(unit) && return nothing + + error("Plotting $(new_eltype) into an axis set to: $(unit_string(unit)). Please convert the data to $(unit_string(unit))") +end + +to_free_unit(unit::Unitful.FreeUnits, _) = unit +to_free_unit(unit::Unitful.FreeUnits, ::Quantity) = unit +to_free_unit(unit::Unitful.FreeUnits, ::Quantity{T, Dim, Unitful.FreeUnits{U, Dim, nothing}}) where {T, Dim, U} = unit +function to_free_unit(unit, ::Quantity{T, Dim, Unitful.FreeUnits{U, Dim, nothing}}) where {T, Dim, U} + return Unitful.FreeUnits{(unit,), Dim, nothing}() +end + +to_free_unit(unit::Unitful.FreeUnits) = unit +function to_free_unit(unit::Unitful.Unit{Sym, Dim}) where {Sym, Dim} + return Unitful.FreeUnits{(unit,), Dim, nothing}() +end + +get_all_base10_units(value) = get_all_base10_units(base_unit(value)) + +function get_all_base10_units(value::Unitful.Unit{Sym, Unitful.𝐋}) where {Sym} + return Unitful.Unit{Sym, Unitful.𝐋}.(UNIT_POWER_OF_TENS, value.power) +end + +function get_all_base10_units(value::Unitful.Unit) + # TODO, why does nothing work in a generic way in Unitful!? + # By only returning this one value, we simply don't chose any different unit as a fallback + return [value] +end + +function get_all_base10_units(x::Unitful.Unit{Sym, Unitful.𝐓}) where {Sym} + return getfield.((Unitful,), TIME_UNIT_NAMES) +end + +function best_unit(min, max) + middle = (min + max) / 2.0 + all_units = get_all_base10_units(middle) + _, index = findmin(all_units) do unit + raw_value = abs(unit_convert(unit, middle)) + # We want the unit that displays the value with the smallest number possible, but not something like 1.0e-19 + # So, for fractions between 0..1, we use inv to penalize really small fractions + positive = raw_value < 1.0 ? (inv(raw_value) + 100) : raw_value + return positive + end + return all_units[index] +end + +unit_convert(::Automatic, x) = x + +function unit_convert(unit::T, x::AbstractArray) where T <: Union{Type{<:Unitful.AbstractQuantity}, Unitful.FreeUnits, Unitful.Unit} + return unit_convert.(Ref(unit), x) +end + +# We always convert to preferred unit! +function unit_convert(unit::T, value) where T <: Union{Type{<:Unitful.AbstractQuantity}, Unitful.FreeUnits, Unitful.Unit} + conv = uconvert(to_free_unit(unit, value), value) + return Float64(ustrip(conv)) +end + + +# Overload conversion functions for Axis, to properly display units + +""" + UnitfulConversion(unit=automatic; units_in_label=false) + +Allows to plot arrays of unitful objects into an axis. + +# Arguments + +- `unit=automatic`: sets the unit as conversion target. If left at automatic, the best unit will be chosen for all plots + values plotted to the axis (e.g. years for long periods, or km for long distances, or nanoseconds for short times). +- `units_in_label=true`: controls, whether plots are shown in the label_prefix of the axis labels, or in the tick labels + +# Examples + +```julia +using Unitful, CairoMakie + +# UnitfulConversion will get chosen automatically: +scatter(1:4, [1u"ns", 2u"ns", 3u"ns", 4u"ns"]) +``` + +Fix unit to always use Meter & display unit in the xlabel: +```julia +uc = Makie.UnitfulConversion(u"m"; units_in_label=false) +scatter(1:4, [0.01u"km", 0.02u"km", 0.03u"km", 0.04u"km"]; axis=(dim2_conversion=uc, xlabel="x (km)")) +``` +""" +struct UnitfulConversion <: AbstractDimConversion + unit::Observable{Any} + automatic_units::Bool + units_in_label::Observable{Bool} + extrema::Dict{String, Tuple{Any, Any}} +end + +function UnitfulConversion(unit=automatic; units_in_label=true) + extrema = Dict{String,Tuple{Any,Any}}() + return UnitfulConversion(unit, unit isa Automatic, units_in_label, extrema) +end + +function update_extrema!(conversion::UnitfulConversion, value_obs::Observable) + conversion.automatic_units || return + eltype, extrema = eltype_extrema(value_obs[]) + conversion.extrema[value_obs.id] = promote(Quantity.(extrema)...) + imini, imaxi = extrema + for (mini, maxi) in values(conversion.extrema) + imini = min(imini, mini) + imaxi = max(imaxi, maxi) + end + new_unit = best_unit(imini, imaxi) + if new_unit != conversion.unit[] + conversion.unit[] = new_unit + end +end + +needs_tick_update_observable(conversion::UnitfulConversion) = conversion.unit + +function get_ticks(conversion::UnitfulConversion, ticks, scale, formatter, vmin, vmax) + unit = conversion.unit[] + unit isa Automatic && return [], [] + unit_str = unit_string(unit) + tick_vals = get_tickvalues(ticks, scale, vmin, vmax) + labels = get_ticklabels(formatter, tick_vals) + if conversion.units_in_label[] + labels = labels .* unit_str + end + return tick_vals, labels +end + +function convert_dim_observable(conversion::UnitfulConversion, value_obs::Observable, deregister) + result = map(conversion.unit, value_obs; ignore_equal_values=true) do unit, values + if !isempty(values) + # try if conversion works, to through error if not! + # Is there a function for this to check in Unitful? + unit_convert(unit, values[1]) + end + update_extrema!(conversion, value_obs) + return unit_convert(conversion.unit[], values) + end + append!(deregister, result.inputs) + return result +end + +function convert_dim_value(conversion::UnitfulConversion, value::SupportedUnits) + return unit_convert(conversion.unit[], value) +end diff --git a/src/figureplotting.jl b/src/figureplotting.jl index 24dde103026..b90f52852f1 100644 --- a/src/figureplotting.jl +++ b/src/figureplotting.jl @@ -82,14 +82,14 @@ function preferred_axis_type(p::Plot{F}) where F return result end -to_dict(dict::Dict) = dict +to_dict(dict::Dict) = convert(Dict{Symbol, Any}, dict) to_dict(nt::NamedTuple) = Dict{Symbol,Any}(pairs(nt)) to_dict(attr::Attributes) = attributes(attr) -function extract_attributes(dict, key) - attributes = pop!(dict, key, Dict{Symbol,Any}()) - _validate_nt_like_keyword(attributes, key) - return to_dict(attributes) +function extract_attributes(dictlike, key) + dictlike = pop!(dictlike, key, Dict{Symbol,Any}()) + _validate_nt_like_keyword(dictlike, key) + return to_dict(dictlike) end function create_axis_for_plot(figure::Figure, plot::AbstractPlot, attributes::Dict) @@ -103,24 +103,58 @@ function create_axis_for_plot(figure::Figure, plot::AbstractPlot, attributes::Di return nothing end bbox = pop!(axis_kw, :bbox, nothing) + set_axis_attributes!(AxType, axis_kw, plot) return _block(AxType, figure, [], axis_kw, bbox) end -function create_axis_like(plot::AbstractPlot, attributes::Dict, ::Nothing) - figure_kw = extract_attributes(attributes, :figure) - figure = Figure(; figure_kw...) - ax = create_axis_for_plot(figure, plot, attributes) - if isnothing(ax) # For FigureSpec - return figure - else - figure[1, 1] = ax - return FigureAxis(figure, ax) +const PlotOrNot = Union{AbstractPlot, Nothing} + +# For recipes (plot!(plot_object, ...))) +MakieCore.create_axis_like!(::Dict, s::Union{Plot, Scene}) = s + +# For plotspec +# MakieCore.create_axis_like!(::PlotSpecPlot, ::Dict, fig::Figure) = fig +MakieCore.create_axis_like!(::Dict, f::Figure) = f + +""" + create_axis_like!(attributes::Dict, ax::AbstractAxis) + +Method to plot to an existing axis. + +E.g.: `plot!(ax, 1:4)` which plots to `ax`. +""" +function create_axis_like!(attributes::Dict, ax::AbstractAxis) + _disallow_keyword(:axis, attributes) + return ax +end + +""" + create_axis_like!(attributes::Dict, gsp::GridSubposition) + +Method to plot to an axis defined at a given sub-grid position. + +E.g.: `plot!(fig[1, 1][1, 1], 1:4)` which needs an axis to exist at f[1, 1][1, 1]. +""" +function MakieCore.create_axis_like!(attributes::Dict, gsp::GridSubposition) + _disallow_keyword(:figure, attributes) + layout = GridLayoutBase.get_layout_at!(gsp.parent; createmissing=false) + gp = layout[gsp.rows, gsp.cols, gsp.side] + c = contents(gp; exact=true) + if !(length(c) == 1 && can_be_current_axis(c[1])) + error("There is not just one axis at $(gp).") end + _disallow_keyword(:axis, attributes) + return first(c) end -MakieCore.create_axis_like!(@nospecialize(::AbstractPlot), attributes::Dict, s::Union{Plot, Scene}) = s +""" + create_axis_like!(attributes::Dict, ::Nothing) -function MakieCore.create_axis_like!(@nospecialize(::AbstractPlot), attributes::Dict, ::Nothing) +Method to plot to the last created axis. + +E.g.: `plot!(1:4)` which requires a current figure and axis. +""" +function MakieCore.create_axis_like!(attributes::Dict, ::Nothing) figure = current_figure() isnothing(figure) && error("There is no current figure to plot into.") _disallow_keyword(:figure, attributes) @@ -130,8 +164,14 @@ function MakieCore.create_axis_like!(@nospecialize(::AbstractPlot), attributes:: return ax end +""" + create_axis_like!(attributes::Dict, gp::GridPosition) + +Method to plot to an axis defined at a given grid position. -function MakieCore.create_axis_like!(@nospecialize(::AbstractPlot), attributes::Dict, gp::GridPosition) +E.g.: `plot!(fig[1, 1], 1:4)` which requires an axis to exist at `f[1, 1]`. +""" +function MakieCore.create_axis_like!(attributes::Dict, gp::GridPosition) _disallow_keyword(:figure, attributes) c = contents(gp; exact=true) if !(length(c) == 1 && can_be_current_axis(c[1])) @@ -142,7 +182,38 @@ function MakieCore.create_axis_like!(@nospecialize(::AbstractPlot), attributes:: return ax end +function create_axis_like(::AbstractPlot, ::Dict, ::Union{Scene,AbstractAxis}) + return error("Plotting into an axis without `!` (e.g. `scatter` instead of `scatter!`)") +end + +""" + create_axis_like(plot::AbstractPlot, attributes::Dict, ::Nothing) + +Method to create a default Figure and Axis from a plot function. + +E.g.: `plot(1:4)` which requires a new Figure and Axis to be created. +""" +function create_axis_like(plot::AbstractPlot, attributes::Dict, ::Nothing) + figure_kw = extract_attributes(attributes, :figure) + figure = Figure(; figure_kw...) + ax = create_axis_for_plot(figure, plot, attributes) + if isnothing(ax) # For FigureSpec + return figure + else + figure[1, 1] = ax + return FigureAxis(figure, ax) + end +end + +""" + create_axis_like(plot::AbstractPlot, attributes::Dict, gp::GridPosition) + +Method to create an Axis at a grid position given int a plot call. + +E.g.: `plot(fig[1, 1], 1:4)` which requires and Axis to be created at f[1, 1]. +""" function create_axis_like(plot::AbstractPlot, attributes::Dict, gp::GridPosition) + isnothing(plot) && return nothing _disallow_keyword(:figure, attributes) figure = get_top_parent(gp) c = contents(gp; exact=true) @@ -163,19 +234,15 @@ function create_axis_like(plot::AbstractPlot, attributes::Dict, gp::GridPosition end end -function MakieCore.create_axis_like!(@nospecialize(::AbstractPlot), attributes::Dict, gsp::GridSubposition) - _disallow_keyword(:figure, attributes) - layout = GridLayoutBase.get_layout_at!(gsp.parent; createmissing=false) - gp = layout[gsp.rows, gsp.cols, gsp.side] - c = contents(gp; exact=true) - if !(length(c) == 1 && can_be_current_axis(c[1])) - error("There is not just one axis at $(gp).") - end - _disallow_keyword(:axis, attributes) - return first(c) -end +""" + create_axis_like(plot::AbstractPlot, attributes::Dict, gsp::GridSubposition) +Method to create an Axis at a sub-grid position given in a plot. + +E.g.: `plot(fig[1, 1][1, 1], 1:4)` which creates an Axis at f[1, 1][1, 1]. +""" function create_axis_like(plot::AbstractPlot, attributes::Dict, gsp::GridSubposition) + isnothing(plot) && return nothing _disallow_keyword(:figure, attributes) GridLayoutBase.get_layout_at!(gsp.parent; createmissing=true) c = contents(gsp; exact=true) @@ -196,15 +263,6 @@ function create_axis_like(plot::AbstractPlot, attributes::Dict, gsp::GridSubposi return ax end -function create_axis_like!(@nospecialize(::AbstractPlot), attributes::Dict, ax::AbstractAxis) - _disallow_keyword(:axis, attributes) - return ax -end - -function create_axis_like(@nospecialize(::AbstractPlot), ::Dict, ::Union{Scene,AbstractAxis}) - return error("Plotting into an axis without !") -end - figurelike_return(fa::FigureAxis, plot::AbstractPlot) = FigureAxisPlot(fa.figure, fa.axis, plot) figurelike_return(ax::AbstractAxis, plot::AbstractPlot) = AxisPlot(ax, plot) figurelike_return!(::AbstractAxis, plot::AbstractPlot) = plot @@ -219,8 +277,6 @@ function update_state_before_display!(f::Figure) return end - - @inline plot_args(args...) = (nothing, args) @inline function plot_args(a::Union{Figure,AbstractAxis,Scene,Plot,GridSubposition,GridPosition}, args...) @@ -245,12 +301,45 @@ default_plot_func(::typeof(plot), args) = plotfunc(plottype(map(to_value, args). @noinline function MakieCore._create_plot(F, attributes::Dict, args...) figarg, pargs = plot_args(args...) figkws = fig_keywords!(attributes) + if haskey(figkws, :axis) + ax_kw = figkws[:axis] + _validate_nt_like_keyword(ax_kw, :axis) + if any(x-> x in [:dim1_conversion, :dim2_conversion, :dim3_conversion], keys(ax_kw)) + conversions = get_conversions(ax_kw) + if haskey(attributes, :dim_conversions) + connect_conversions!(attributes[:dim_conversions], conversions) + else + attributes[:dim_conversions] = conversions + end + end + end plot = Plot{default_plot_func(F, pargs)}(pargs, attributes) ax = create_axis_like(plot, figkws, figarg) plot!(ax, plot) return figurelike_return(ax, plot) end +function set_axis_attributes!(T::Type{<:AbstractAxis}, attributes::Dict, plot::Plot) + conversions = get(plot.kw, :dim_conversions, nothing) + isnothing(conversions) && return + for i in 1:3 + key = Symbol("dim$(i)_conversion") + if hasfield(T, key) + attributes[key] = conversions[i] + end + end + return +end + + +# This enables convert_arguments(::Type{<:AbstractPlot}, ::X) -> FigureSpec +# Which skips axis creation +# TODO, what to return for the dynamically created axes? +const PlotSpecPlot = Plot{plot, Tuple{<: GridLayoutSpec}} + +get_conversions(scene::Scene) = scene.conversions +get_conversions(fig::Figure) = get_conversions(fig.scene) + @noinline function MakieCore._create_plot!(F, attributes::Dict, args...) if length(args) > 0 if args[1] isa FigureAxisPlot @@ -258,9 +347,9 @@ end Tried plotting with `$(F)!` into a `FigureAxisPlot` object, this is not allowed. The `FigureAxisPlot` object is returned by plotting functions not ending in `!` like `lines(...)` or `scatter(...)`. - + It contains the new `Figure`, the new axis object, for example an `Axis`, `LScene` or `Axis3`, and the new plot object. It exists just as a convenience because returning it displays the contained figure. For all further operations, you should split it into its parts instead. This way, it is clear which of its components you are targeting. - + You can do this with the destructuring syntax `fig, ax, plt = some_plot(...)` and then continue, for example with `$(F)!(ax, ...)`. """)) end @@ -270,36 +359,41 @@ end The `AxisPlot` object is returned by plotting functions not ending in `!` with a `GridPosition` as the first argument, like `lines(fig[1, 2], ...)` or `scatter(fig[1, 2], ...)`. - + It contains the new axis object, for example an `Axis`, `LScene` or `Axis3`, and the new plot object. For all further operations, you should split it into its parts instead. This way, it is clear which of its components you are targeting. - + You can do this with the destructuring syntax `ax, plt = some_plot(fig[1, 2], ...)` and then continue, for example with `$(F)!(ax, ...)`. """)) end end figarg, pargs = plot_args(args...) figkws = fig_keywords!(attributes) + # we need to see if we plot into an existing axis before creating the plot + # For axis specific converts. + ax = create_axis_like!(figkws, figarg) + # inserts global state from axis into plot attributes if they exist + get!(attributes, :dim_conversions, get_conversions(ax)) + plot = Plot{default_plot_func(F, pargs)}(pargs, attributes) - ax = create_axis_like!(plot, figkws, figarg) + if ax isa Figure && !(plot isa PlotSpecPlot) + error("You cannot plot into a figure without an axis. Use `plot(fig[1, 1], ...)` instead.") + end plot!(ax, plot) return figurelike_return!(ax, plot) end @noinline function MakieCore._create_plot!(F, attributes::Dict, scene::SceneLike, args...) + conversion = get_conversions(scene) + if !isnothing(conversion) + get!(attributes, :dim_conversions, conversion) + end plot = Plot{default_plot_func(F, args)}(args, attributes) plot!(scene, plot) return plot end -# This enables convert_arguments(::Type{<:AbstractPlot}, ::X) -> FigureSpec -# Which skips axis creation -# TODO, what to return for the dynamically created axes? -const PlotSpecPlot = Plot{plot, Tuple{<: GridLayoutSpec}} - figurelike_return(f::GridPosition, p::PlotSpecPlot) = p figurelike_return(f::Figure, p::PlotSpecPlot) = FigureAxisPlot(f, nothing, p) -MakieCore.create_axis_like!(::PlotSpecPlot, attributes::Dict, fig::Figure) = fig -MakieCore.create_axis_like!(::AbstractPlot, attributes::Dict, fig::Figure) = nothing # Axis interface @@ -312,8 +406,14 @@ end plot!(fa::FigureAxis, plot) = plot!(fa.axis, plot) + + function plot!(ax::AbstractAxis, plot::AbstractPlot) plot!(ax.scene, plot) + if !isnothing(get_conversions(plot)) + connect_conversions!(ax.scene.conversions, get_conversions(plot)) + end + # some area-like plots basically always look better if they cover the whole plot area. # adjust the limit margins in those cases automatically. needs_tight_limits(plot) && tightlimits!(ax) diff --git a/src/interfaces.jl b/src/interfaces.jl index 13dfeeac88e..014b7eb4f91 100644 --- a/src/interfaces.jl +++ b/src/interfaces.jl @@ -105,55 +105,175 @@ const atomic_functions = ( ) const Atomic{Arg} = Union{map(x-> Plot{x, Arg}, atomic_functions)...} -function convert_arguments!(plot::Plot{F}) where {F} - P = Plot{F,Any} - function on_update(kw, args...) - nt = convert_arguments(P, args...; kw...) - pnew, converted = apply_convert!(P, plot.attributes, nt) - @assert plotfunc(pnew) === F "Changed the plot type in convert_arguments. This isn't allowed!" - for (obs, new_val) in zip(plot.converted, converted) - obs[] = new_val - end +function get_kw_values(func, names, kw) + return NamedTuple([Pair{Symbol,Any}(k, func(kw[k])) + for k in names if haskey(kw, k)]) +end + +function get_kw_obs(names, kw) + isempty(names) && return Observable((;)) + kw_copy = copy(kw) + init = get_kw_values(to_value, names, kw_copy) + obs = Observable(init; ignore_equal_values=true) + in_obs = [kw_copy[k] for k in names if haskey(kw_copy, k)] + onany(in_obs...) do args... + obs[] = get_kw_values(to_value, names, kw_copy) + return end - used_attrs = used_attributes(P, to_value.(plot.args)...) - convert_keys = intersect(used_attrs, keys(plot.attributes)) - kw_signal = if isempty(convert_keys) - # lift(f) isn't supported so we need to catch the empty case - Observable(()) + return obs +end + + +""" + expand_dimensions(plottrait, plotargs...) + +Expands the dims for e.g. `scatter(1:4)` becoming `scatter(1:4, 1:4)` for 2D plots. +We're separating this state from convert_arguments, to better apply `dim_converts` before convert_arguments. +""" +expand_dimensions(trait, args...) = nothing + +expand_dimensions(::PointBased, y::VecTypes) = nothing # VecTypes are nd points +expand_dimensions(::PointBased, y::RealVector) = (keys(y), y) + +function expand_dimensions(::ImageLike, data::AbstractMatrix{<:Union{<:Real,<:Colorant}}) + n, m = Float32.(size(data)) + return (Float32(0) .. n, Float32(0) .. m, el32convert(data)) +end + +function expand_dimensions(::GridBased, data::AbstractMatrix{<:Union{<:Real, <:Colorant}}) + n, m = Float32.(size(data)) + return (1.0f0 .. n, 1.0f0 .. m, data) +end + +function expand_dimensions(::VolumeLike, data::RealArray{3}) + n, m, k = Float32.(size(data)) + return (0.0f0 .. n, 0.0f0 .. m, 0.0f0 .. k, data) +end + +function apply_expand_dimensions(trait, args, args_obs, deregister) + expanded = expand_dimensions(trait, args...) + if isnothing(expanded) + return args_obs else - # Remove used attributes from `attributes` and collect them in a `Tuple` to pass them more easily - lift((args...) -> Pair.(convert_keys, args), plot, pop!.(plot.attributes, convert_keys)...) + new_obs = map(Observable, expanded) + fs = onany(args_obs...) do args... + expanded = expand_dimensions(trait, args...) + for (obs, arg) in zip(new_obs, expanded) + obs.val = arg + end + foreach(notify, new_obs) + return + end + append!(deregister, fs) + return new_obs end - onany(on_update, plot, kw_signal, plot.args...) - return end -function Plot{Func}(args::Tuple, plot_attributes::Dict) where {Func} - if !isempty(args) && first(args) isa Attributes - merge!(plot_attributes, attributes(first(args))) - return Plot{Func}(Base.tail(args), plot_attributes) + +# Internal function to apply convert_arguments to observable arguments +function convert_observable_args(P, args_obs, kw_obs, converted, deregister) + # Fully converted arguments to target type for Plot + new_args_obs = map(Observable, converted) + fs = onany(kw_obs, args_obs...) do kw, args... + conv = convert_arguments(P, args...; kw...) + if !(conv isa Tuple) + conv = (conv,) # for PlotSpec + end + for (obs, arg) in zip(new_args_obs, conv) + obs.val = arg + end + foreach(notify, new_args_obs) + return + end + append!(deregister, fs) + return new_args_obs +end + +function got_converted(P::Type, PTrait::ConversionTrait, result) + if result isa Union{PlotSpec,BlockSpec,GridLayoutSpec,AbstractVector{PlotSpec}} + return SpecApi end - P = Plot{Func} - used_attrs = used_attributes(P, to_value.(args)...) - if used_attrs === () - args_converted = convert_arguments(P, map(to_value, args)...) - else - kw = [Pair(k, to_value(pop!(plot_attributes, k))) for k in used_attrs if haskey(plot_attributes, k)] - args_converted = convert_arguments(P, map(to_value, args)...; kw...) + types = MakieCore.types_for_plot_arguments(P, PTrait) + if !isnothing(types) + return result isa types end - preconvert_attr = Attributes() - PNew, converted = apply_convert!(P, preconvert_attr, args_converted) + return nothing +end - obs_args = Any[convert(Observable, x) for x in args] +""" + conversion_pipeline(P::Type{<:Plot}, used_attrs::Tuple, args::Tuple, + args_obs::Tuple, user_attributes::Dict{Symbol, Any}, deregister, recursion=1) - ArgTyp = MakieCore.argtypes(converted) - converted_obs = map(Observable, converted) - FinalPlotFunc = plotfunc(plottype(PNew, converted...)) - plot = Plot{FinalPlotFunc,ArgTyp}(plot_attributes, obs_args, converted_obs) - for (k, v) in preconvert_attr - attributes(plot.attributes)[k] = v +The main conversion pipeline for converting arguments for a plot type. +Applies dim_converts, expand_dimensions (in `try_dim_convert`), convert_arguments and checks if the conversion was successful. +""" +function conversion_pipeline( + P::Type{<:Plot}, used_attrs::Tuple, args::Tuple, + args_obs::Tuple, user_attributes::Dict{Symbol, Any}, deregister, recursion=1) + if recursion === 3 + error("Recursion limit reached. This should not happen, please open an issue with Makie.jl and provide a minimal working example.") + return P, args_obs end - return plot + kw_obs = get_kw_obs(used_attrs, user_attributes) + kw = to_value(kw_obs) + PTrait = conversion_trait(P, args...) + dim_converted = try_dim_convert(P, PTrait, user_attributes, args_obs, deregister) + args = map(to_value, dim_converted) + converted = convert_arguments(P, args...; kw...) + status = got_converted(P, PTrait, converted) + if status === true + # We're done converting! + return convert_observable_args(P, dim_converted, kw_obs, converted, deregister) + elseif status === SpecApi + return convert_observable_args(P, dim_converted, kw_obs, (converted,), deregister) + elseif status === false && recursion === 1 + # We haven't reached a target type, so we try to apply convert arguments again and try_dim_convert + # This is the case for e.g. convert_arguments returning types that need dim_convert + new_args_obs = convert_observable_args(P, dim_converted, kw_obs, converted, deregister) + return conversion_pipeline(P, used_attrs, map(to_value, new_args_obs), new_args_obs, user_attributes, deregister, + recursion + 1) + elseif status === false && recursion === 2 + kw_str = isempty(kw) ? "" : " and kw: $(typeof(kw))" + kw_convert = isempty(kw) ? "" : "; kw..." + conv_trait = PTrait isa NoConversion ? "" : " (With conversion trait $(PTrait))" + types = MakieCore.types_for_plot_arguments(P, PTrait) + throw(ArgumentError(""" + Conversion failed for $(P)$(conv_trait) with args: $(typeof.(args)) $(kw_str). + $(P) requires to convert to argument types $(types), which convert_arguments didn't succeed in. + To fix this overload convert_arguments(P, args...$(kw_convert)) for $(P) or $(PTrait) and return an object of type $(types).` + """)) + elseif isnothing(status) + # No types_for_plot_arguments defined, so we don't know what we need to convert to. + # This is for backwards compatibility for recipes that don't define argument types + return convert_observable_args(P, dim_converted, kw_obs, converted, deregister) + else + error("Unknown status: $(status)") + end +end + +function Plot{Func}(user_args::Tuple, user_attributes::Dict) where {Func} + # Handle plot!(plot, attributes::Attributes, args...) here + if !isempty(user_args) && first(user_args) isa Attributes + merge!(user_attributes, attributes(first(user_args))) + return Plot{Func}(Base.tail(user_args), user_attributes) + end + P = Plot{Func} + args = map(to_value, user_args) + attr = used_attributes(P, args...) + # don't use convert(Observable{Any}, x) here, + # We assume if a user passes the observable, they type it correctly + # And if they pass a value, they may want to change the type, so we need Observable{Any} + args_obs = map(x -> x isa Observable ? x : Observable{Any}(x), user_args) + deregister = Observables.ObserverFunction[] + PTrait = conversion_trait(P, args...) + expanded_args_obs = apply_expand_dimensions(PTrait, args, args_obs, deregister) + converted_obs = conversion_pipeline(P, attr, args, expanded_args_obs, user_attributes, deregister) + args2 = map(to_value, converted_obs) + ArgTyp = MakieCore.argtypes(args2) + FinalPlotFunc = plotfunc(plottype(P, args2...)) + foreach(x -> delete!(user_attributes, x), attr) + return Plot{FinalPlotFunc,ArgTyp}(user_attributes, Any[args_obs...], Observable[converted_obs...], + deregister) end """ @@ -181,7 +301,6 @@ Usage: used_attributes(::Type{<:Plot}, args...) = used_attributes(args...) used_attributes(args...) = () - ## generic definitions # Chose the more specific plot type from arguments or input type # Note the plottype(Scatter, Plot{plot}) will return Scatter @@ -190,6 +309,7 @@ plottype(P::Type{<: Plot{T}}, argvalues...) where T = plottype(P, plottype(argva plottype(P::Type{<:Plot{T}}) where {T} = P plottype(P1::Type{<:Plot{T1}}, ::Type{<:Plot{T2}}) where {T1, T2} = P1 plottype(::Type{Plot{plot}}, ::Type{Plot{plot}}) = Plot{plot} + """ plottype(P1::Type{<: Plot{T1}}, P2::Type{<: Plot{T2}}) @@ -225,18 +345,18 @@ plottype(::MultiPolygon) = Lines # all the plotting functions that get a plot type -const PlotFunc = Union{Type{Any},Type{<:AbstractPlot}} +const PlotFunc = Type{<:AbstractPlot} -function plot!(::Plot{F}) where {F} +function plot!(::Plot{F, Args}) where {F, Args} if !(F in atomic_functions) - error("No recipe for $(F)") + error("No recipe for $(F) with args: $(Args)") end end function connect_plot!(parent::SceneLike, plot::Plot{F}) where {F} plot.parent = parent - - apply_theme!(parent_scene(parent), plot) + scene = parent_scene(parent) + apply_theme!(scene, plot) t_user = to_value(get(attributes(plot), :transformation, automatic)) if t_user isa Transformation plot.transformation = t_user @@ -254,10 +374,13 @@ function connect_plot!(parent::SceneLike, plot::Plot{F}) where {F} end end plot.model = transformationmatrix(plot) - convert_arguments!(plot) calculated_attributes!(Plot{F}, plot) default_shading!(plot, parent_scene(parent)) plot!(plot) + conversions = get_conversions(plot) + if !isnothing(conversions) + connect_conversions!(scene.conversions, conversions) + end return plot end diff --git a/src/makielayout/blocks/axis.jl b/src/makielayout/blocks/axis.jl index c31683776f3..013506e7135 100644 --- a/src/makielayout/blocks/axis.jl +++ b/src/makielayout/blocks/axis.jl @@ -161,8 +161,8 @@ function compute_protrusions(title, titlesize, titlegap, titlevisible, spinewidt end function initialize_block!(ax::Axis; palette = nothing) - blockscene = ax.blockscene + blockscene = ax.blockscene elements = Dict{Symbol, Any}() ax.elements = elements @@ -187,6 +187,9 @@ function initialize_block!(ax::Axis; palette = nothing) scene = Scene(blockscene, viewport=scenearea) ax.scene = scene + # transfer conversions from axis to scene if there are any + # or the other way around + connect_conversions!(scene.conversions, ax) setfield!(scene, :float32convert, Float32Convert()) @@ -331,12 +334,13 @@ function initialize_block!(ax::Axis; palette = nothing) ticklabelalign = ax.xticklabelalign, labelsize = ax.xlabelsize, labelpadding = ax.xlabelpadding, ticklabelpad = ax.xticklabelpad, labelvisible = ax.xlabelvisible, label = ax.xlabel, labelfont = ax.xlabelfont, labelrotation = ax.xlabelrotation, ticklabelfont = ax.xticklabelfont, ticklabelcolor = ax.xticklabelcolor, labelcolor = ax.xlabelcolor, tickalign = ax.xtickalign, - ticklabelspace = ax.xticklabelspace, ticks = ax.xticks, tickformat = ax.xtickformat, ticklabelsvisible = ax.xticklabelsvisible, + ticklabelspace = ax.xticklabelspace, dim_convert = ax.dim1_conversion, ticks = ax.xticks, tickformat = ax.xtickformat, ticklabelsvisible = ax.xticklabelsvisible, ticksvisible = ax.xticksvisible, spinevisible = xspinevisible, spinecolor = xspinecolor, spinewidth = ax.spinewidth, ticklabelsize = ax.xticklabelsize, trimspine = ax.xtrimspine, ticksize = ax.xticksize, reversed = ax.xreversed, tickwidth = ax.xtickwidth, tickcolor = ax.xtickcolor, minorticksvisible = ax.xminorticksvisible, minortickalign = ax.xminortickalign, minorticksize = ax.xminorticksize, minortickwidth = ax.xminortickwidth, minortickcolor = ax.xminortickcolor, minorticks = ax.xminorticks, scale = ax.xscale, ) + ax.xaxis = xaxis yaxis = LineAxis(blockscene, endpoints = yaxis_endpoints, limits = ylims, @@ -344,7 +348,7 @@ function initialize_block!(ax::Axis; palette = nothing) ticklabelalign = ax.yticklabelalign, labelsize = ax.ylabelsize, labelpadding = ax.ylabelpadding, ticklabelpad = ax.yticklabelpad, labelvisible = ax.ylabelvisible, label = ax.ylabel, labelfont = ax.ylabelfont, labelrotation = ax.ylabelrotation, ticklabelfont = ax.yticklabelfont, ticklabelcolor = ax.yticklabelcolor, labelcolor = ax.ylabelcolor, tickalign = ax.ytickalign, - ticklabelspace = ax.yticklabelspace, ticks = ax.yticks, tickformat = ax.ytickformat, ticklabelsvisible = ax.yticklabelsvisible, + ticklabelspace = ax.yticklabelspace, dim_convert = ax.dim2_conversion, ticks = ax.yticks, tickformat = ax.ytickformat, ticklabelsvisible = ax.yticklabelsvisible, ticksvisible = ax.yticksvisible, spinevisible = yspinevisible, spinecolor = yspinecolor, spinewidth = ax.spinewidth, trimspine = ax.ytrimspine, ticklabelsize = ax.yticklabelsize, ticksize = ax.yticksize, flip_vertical_label = ax.flip_ylabel, reversed = ax.yreversed, tickwidth = ax.ytickwidth, tickcolor = ax.ytickcolor, @@ -1247,6 +1251,7 @@ function Base.show(io::IO, ax::Axis) end function Makie.xlims!(ax::Axis, xlims) + xlims = map(x -> convert_dim_value(ax, 1, x), xlims) if length(xlims) != 2 error("Invalid xlims length of $(length(xlims)), must be 2.") elseif xlims[1] == xlims[2] && xlims[1] !== nothing @@ -1257,6 +1262,7 @@ function Makie.xlims!(ax::Axis, xlims) else ax.xreversed[] = false end + mlims = convert_limit_attribute(ax.limits[]) ax.limits.val = (xlims, mlims[2]) reset_limits!(ax, yauto = false) @@ -1264,6 +1270,7 @@ function Makie.xlims!(ax::Axis, xlims) end function Makie.ylims!(ax::Axis, ylims) + ylims = map(x -> convert_dim_value(ax, 2, x), ylims) if length(ylims) != 2 error("Invalid ylims length of $(length(ylims)), must be 2.") elseif ylims[1] == ylims[2] && ylims[1] !== nothing diff --git a/src/makielayout/blocks/colorbar.jl b/src/makielayout/blocks/colorbar.jl index 20887a36605..e637f7f5556 100644 --- a/src/makielayout/blocks/colorbar.jl +++ b/src/makielayout/blocks/colorbar.jl @@ -382,8 +382,9 @@ function initialize_block!(cb::Colorbar) limits=lims, ticklabelalign=cb.ticklabelalign, label=cb.label, labelpadding = cb.labelpadding, labelvisible = cb.labelvisible, labelsize = cb.labelsize, labelcolor = cb.labelcolor, labelrotation = cb.labelrotation, - labelfont=cb.labelfont, ticklabelfont=cb.ticklabelfont, ticks=ticks, - tickformat=cb.tickformat, + labelfont=cb.labelfont, ticklabelfont=cb.ticklabelfont, + dim_convert=nothing, # TODO, we should also have a dim convert for Colorbar + ticks=ticks, tickformat=cb.tickformat, ticklabelsize = cb.ticklabelsize, ticklabelsvisible = cb.ticklabelsvisible, ticksize = cb.ticksize, ticksvisible = cb.ticksvisible, ticklabelpad = cb.ticklabelpad, tickalign = cb.tickalign, ticklabelrotation = cb.ticklabelrotation, diff --git a/src/makielayout/lineaxis.jl b/src/makielayout/lineaxis.jl index c715dd10c08..d90122093d5 100644 --- a/src/makielayout/lineaxis.jl +++ b/src/makielayout/lineaxis.jl @@ -259,7 +259,7 @@ function LineAxis(parent::Scene, attrs::Attributes) decorations = Dict{Symbol, Any}() @extract attrs (endpoints, ticksize, tickwidth, - tickcolor, tickalign, ticks, tickformat, ticklabelalign, ticklabelrotation, ticksvisible, + tickcolor, tickalign, dim_convert, ticks, tickformat, ticklabelalign, ticklabelrotation, ticksvisible, ticklabelspace, ticklabelpad, labelpadding, ticklabelsize, ticklabelsvisible, spinewidth, spinecolor, label, labelsize, labelcolor, labelfont, ticklabelfont, ticklabelcolor, @@ -417,10 +417,10 @@ function LineAxis(parent::Scene, attrs::Attributes) tickvalues = Observable(Float64[]; ignore_equal_values=true) tickvalues_labels_unfiltered = Observable{Tuple{Vector{Float64},Vector{Any}}}() - map!(parent, tickvalues_labels_unfiltered, pos_extents_horizontal, limits, ticks, tickformat, - attrs.scale) do (position, extents, horizontal), - limits, ticks, tickformat, scale - get_ticks(ticks, scale, tickformat, limits...) + obs = needs_tick_update_observable(dim_convert) # make sure we update tick calculation when needed + map!(parent, tickvalues_labels_unfiltered, pos_extents_horizontal, obs, limits, ticks, tickformat, + attrs.scale) do (position, extents, horizontal), _, limits, ticks, tickformat, scale + return get_ticks(dim_convert[], ticks, scale, tickformat, limits...) end tickpositions = Observable(Point2f[]; ignore_equal_values=true) @@ -611,10 +611,6 @@ function get_ticks(l::LogTicks, scale::LogFunctions, ::Automatic, vmin, vmax) ticks, labels end -# function get_ticks(::Automatic, scale::typeof(Makie.logit), any_formatter, vmin, vmax) -# get_ticks(LogitTicks(WilkinsonTicks(5, k_min = 3)), scale, any_formatter, vmin, vmax) -# end - logit_10(x) = Makie.logit(x) / log(10) expit_10(x) = Makie.logistic(log(10) * x) diff --git a/src/makielayout/types.jl b/src/makielayout/types.jl index 00f15cbec13..27104c40ea1 100644 --- a/src/makielayout/types.jl +++ b/src/makielayout/types.jl @@ -212,6 +212,15 @@ end yaxis::LineAxis elements::Dict{Symbol, Any} @attributes begin + """ + Global state for the x dimension conversion. + """ + dim1_conversion = nothing + """ + Global state for the y dimension conversion. + """ + dim2_conversion = nothing + """ The content of the x axis label. The value can be any non-vector-valued object that the `text` primitive supports. @@ -1265,6 +1274,19 @@ end @Block LScene <: AbstractAxis begin scene::Scene @attributes begin + """ + Global state for the x dimension conversion. + """ + dim1_conversion = nothing + """ + Global state for the y dimension conversion. + """ + dim2_conversion = nothing + """ + Global state for the z dimension conversion. + """ + dim3_conversion = nothing + "The height setting of the scene." height = nothing "The width setting of the scene." @@ -1363,6 +1385,18 @@ end keysevents::Observable{KeysEvent} interactions::Dict{Symbol, Tuple{Bool, Any}} @attributes begin + """ + Global state for the x dimension conversion. + """ + dim1_conversion = nothing + """ + Global state for the y dimension conversion. + """ + dim2_conversion = nothing + """ + Global state for the z dimension conversion. + """ + dim3_conversion = nothing "The height setting of the scene." height = nothing "The width setting of the scene." @@ -1651,6 +1685,15 @@ end target_r0::Observable{Float32} @attributes begin # Generic + """ + Global state for the x dimension conversion. + """ + dim1_conversion = nothing + """ + Global state for the y dimension conversion. + """ + dim2_conversion = nothing + "The height setting of the scene." height = nothing diff --git a/src/scenes.jl b/src/scenes.jl index fe17e12cbc4..4b139cceb3b 100644 --- a/src/scenes.jl +++ b/src/scenes.jl @@ -92,6 +92,8 @@ mutable struct Scene <: AbstractScene deregister_callbacks::Vector{Observables.ObserverFunction} cycler::Cycler + conversions::DimConversions + function Scene( parent::Union{Nothing, Scene}, events::Events, @@ -127,7 +129,8 @@ mutable struct Scene <: AbstractScene ssao, convert(Vector{AbstractLight}, lights), Observables.ObserverFunction[], - Cycler() + Cycler(), + DimConversions() ) finalizer(free, scene) return scene diff --git a/src/specapi.jl b/src/specapi.jl index ad26b89206c..d965eacd43e 100644 --- a/src/specapi.jl +++ b/src/specapi.jl @@ -194,34 +194,6 @@ end GridLayoutSpec(contents...; kwargs...) = GridLayoutSpec([contents...]; kwargs...) -""" -apply for return type PlotSpec -""" -function apply_convert!(P, attributes::Attributes, x::PlotSpec) - args, kwargs = x.args, x.kwargs - # Note that kw_args in the plot spec that are not part of the target plot type - # will end in the "global plot" kw_args (rest) - for (k, v) in pairs(kwargs) - attributes[k] = v - end - return (plottype(plottype(x), P), (args...,)) -end - -function apply_convert!(P, ::Attributes, x::AbstractVector{PlotSpec}) - return (PlotList, (x,)) -end - -""" -apply for return type - (args...,) -""" -apply_convert!(P, ::Attributes, x::Tuple) = (P, x) - -function MakieCore.argtypes(plot::PlotSpec) - args_converted = convert_arguments(plottype(plot), plot.args...) - return MakieCore.argtypes(args_converted) -end - """ See documentation for specapi. """ @@ -377,8 +349,39 @@ plots[] = [ Attributes() end +function Base.propertynames(pl::PlotList) + if length(pl.plots) == 1 + return Base.propertynames(pl.plots[1]) + else + return () + end +end + +function Base.getproperty(pl::PlotList, property::Symbol) + hasfield(typeof(pl), property) && return getfield(pl, property) + property === :model && return pl.attributes[:model] + if length(pl.plots) == 1 + return getproperty(pl.plots[1], property) + else + error("Can't get property $property on PlotList with multiple plots.") + end +end + +function Base.setproperty!(pl::PlotList, property::Symbol, value) + hasfield(typeof(pl), property) && return setfield!(pl, property, value) + property === :model && return setproperty!(pl.attributes, property, value) + if length(pl.plots) == 1 + setproperty!(pl.plots[1], property, value) + else + error("Can't set property $property on PlotList with multiple plots.") + end +end + convert_arguments(::Type{<:AbstractPlot}, args::AbstractArray{<:PlotSpec}) = (args,) -plottype(::AbstractVector{PlotSpec}) = PlotList + +plottype(::Type{<:Plot{F}}, ::Union{PlotSpec,AbstractVector{PlotSpec}}) where {F} = PlotList +plottype(::Type{<:Plot{F}}, ::Union{GridLayoutSpec,BlockSpec}) where {F} = Plot{plot} +plottype(::Type{<:Plot}, ::Union{GridLayoutSpec,BlockSpec}) = Plot{plot} # Since we directly plot into the parent scene (hacky), we need to overload these Base.insert!(::MakieScreen, ::Scene, ::PlotList) = nothing @@ -448,6 +451,8 @@ function update_plotspecs!(scene::Scene, list_of_plotspecs::Observable, plotlist # and re-create it if it ever returns. unused_plots = IdDict{PlotSpec,Plot}() obs_to_notify = Observable[] + + update_plotlist(spec::PlotSpec) = update_plotlist([spec]) function update_plotlist(plotspecs) # Global list of observables that need updating # Updating them all at once in the end avoids problems with triggering updates while updating @@ -483,12 +488,14 @@ function update_plotspecs!(scene::Scene, list_of_plotspecs::Observable, plotlist return end -function Makie.plot!(p::PlotList{<: Tuple{<: AbstractArray{PlotSpec}}}) +function Makie.plot!(p::PlotList{<: Tuple{<: Union{PlotSpec, AbstractArray{PlotSpec}}}}) scene = Makie.parent_scene(p) update_plotspecs!(scene, p[1], p) return end + + ## BlockSpec function compare_layout_slot((anesting, ap, a)::Tuple{Int,GP,BlockSpec}, (bnesting, bp, b)::Tuple{Int,GP,BlockSpec}) where {GP<:GridLayoutPosition} @@ -732,12 +739,6 @@ plot!(plot::Plot{MakieCore.plot,Tuple{GridLayoutSpec}}) = plot function plot!(fig::Union{Figure, GridLayoutBase.GridPosition}, plot::Plot{MakieCore.plot,Tuple{GridLayoutSpec}}) figure = fig isa Figure ? fig : get_top_parent(fig) connect_plot!(figure.scene, plot) - update_fig!(fig, plot[1]) + update_fig!(fig, plot.converted[1]) return fig end - -function apply_convert!(P, ::Attributes, x::GridLayoutSpec) - return (Plot{plot}, (x,)) -end - -MakieCore.argtypes(::GridLayoutSpec) = Tuple{Nothing} diff --git a/src/stats/boxplot.jl b/src/stats/boxplot.jl index eeb80996792..10cc2f9c6aa 100644 --- a/src/stats/boxplot.jl +++ b/src/stats/boxplot.jl @@ -17,7 +17,7 @@ The boxplot has 3 components: - `x`: positions of the categories - `y`: variables within the boxes """ -@recipe BoxPlot x y begin +@recipe BoxPlot (x, y) begin "Vector of statistical weights (length of data). By default, each observation has weight `1`." weights = automatic color = @inherit patchcolor diff --git a/src/stats/crossbar.jl b/src/stats/crossbar.jl index bd60b5abde4..7bc615c383c 100644 --- a/src/stats/crossbar.jl +++ b/src/stats/crossbar.jl @@ -12,7 +12,7 @@ It is most commonly used as part of the `boxplot`. - `ymin`: lower limit of the box - `ymax`: upper limit of the box """ -@recipe CrossBar x y ymin ymax begin +@recipe CrossBar (x, y, ymin, ymax) begin color= @inherit patchcolor colormap= @inherit colormap colorscale=identity diff --git a/src/stats/density.jl b/src/stats/density.jl index 36d35bb8dc1..c3884a51d88 100644 --- a/src/stats/density.jl +++ b/src/stats/density.jl @@ -1,4 +1,4 @@ -function convert_arguments(P::PlotFunc, d::KernelDensity.UnivariateKDE) +function convert_arguments(P::Type{<:AbstractPlot}, d::KernelDensity.UnivariateKDE) ptype = plottype(P, Lines) # choose the more concrete one to_plotspec(ptype, convert_arguments(ptype, d.x, d.density)) end @@ -11,7 +11,7 @@ function convert_arguments(::Type{<:Poly}, d::KernelDensity.UnivariateKDE) (points,) end -function convert_arguments(P::PlotFunc, d::KernelDensity.BivariateKDE) +function convert_arguments(P::Type{<:AbstractPlot}, d::KernelDensity.BivariateKDE) ptype = plottype(P, Heatmap) to_plotspec(ptype, convert_arguments(ptype, d.x, d.y, d.density)) end diff --git a/src/stats/distributions.jl b/src/stats/distributions.jl index 0586f590800..b6b9db27ed8 100644 --- a/src/stats/distributions.jl +++ b/src/stats/distributions.jl @@ -16,9 +16,9 @@ isdiscrete(::Distribution{<:VariateForm,<:Discrete}) = true support(dist::Distribution) = default_range(dist) support(dist::Distribution{<:VariateForm,<:Discrete}) = UnitRange(endpoints(default_range(dist))...) -convert_arguments(P::PlotFunc, dist::Distribution) = convert_arguments(P, support(dist), dist) +convert_arguments(P::Type{<:AbstractPlot}, dist::Distribution) = convert_arguments(P, support(dist), dist) -function convert_arguments(P::PlotFunc, x::Union{Interval,AbstractVector}, dist::Distribution) +function convert_arguments(P::Type{<:AbstractPlot}, x::Union{Interval,AbstractVector}, dist::Distribution) default_ptype = isdiscrete(dist) ? ScatterLines : Lines ptype = plottype(P, default_ptype) to_plotspec(ptype, convert_arguments(ptype, x, x -> pdf(dist, x))) @@ -97,10 +97,15 @@ end maybefit(D::Type{<:Distribution}, y) = Distributions.fit(D, y) maybefit(x, _) = x +function convert_arguments(::Type{<:QQPlot}, points::AbstractVector{<:Point2}, + lines::AbstractVector{<:Point2}; qqline = :none) + return (points, lines) +end + function convert_arguments(::Type{<:QQPlot}, x′, y; qqline = :none) x = maybefit(x′, y) points, line = fit_qqplot(x, y; qqline = qqline) - return PlotSpec(:QQPlot, points, line) + return (points, line) end convert_arguments(::Type{<:QQNorm}, y; qqline = :none) = @@ -109,6 +114,8 @@ convert_arguments(::Type{<:QQNorm}, y; qqline = :none) = used_attributes(::Type{<:QQNorm}, y) = (:qqline,) used_attributes(::Type{<:QQPlot}, x, y) = (:qqline,) +plottype(::Type{<:QQNorm}, args...) = QQPlot + function Makie.plot!(p::QQPlot) points, line = p[1], p[2] diff --git a/src/stats/ecdf.jl b/src/stats/ecdf.jl index 9c89e3cc5b7..78c7d051c8c 100644 --- a/src/stats/ecdf.jl +++ b/src/stats/ecdf.jl @@ -5,8 +5,8 @@ function ecdf_xvalues(ecdf::StatsBase.ECDF, npoints) return @inbounds range(x[1], x[n]; length=npoints) end -used_attributes(::PlotFunc, ::StatsBase.ECDF) = (:npoints,) -function convert_arguments(P::PlotFunc, ecdf::StatsBase.ECDF; npoints=10_000) +used_attributes(::Type{<:AbstractPlot}, ::StatsBase.ECDF) = (:npoints,) +function convert_arguments(P::Type{<:AbstractPlot}, ecdf::StatsBase.ECDF; npoints=10_000) ptype = plottype(P, Stairs) x0 = ecdf_xvalues(ecdf, npoints) if ptype <: Stairs @@ -20,13 +20,13 @@ function convert_arguments(P::PlotFunc, ecdf::StatsBase.ECDF; npoints=10_000) return to_plotspec(ptype, convert_arguments(ptype, x, ecdf(x)); kwargs...) end -function convert_arguments(P::PlotFunc, x::AbstractVector, ecdf::StatsBase.ECDF) +function convert_arguments(P::Type{<:AbstractPlot}, x::AbstractVector, ecdf::StatsBase.ECDF) ptype = plottype(P, Stairs) kwargs = ptype <: Stairs ? (; step=:post) : NamedTuple() return to_plotspec(ptype, convert_arguments(ptype, x, ecdf(x)); kwargs...) end -function convert_arguments(P::PlotFunc, x0::AbstractInterval, ecdf::StatsBase.ECDF) +function convert_arguments(P::Type{<:AbstractPlot}, x0::AbstractInterval, ecdf::StatsBase.ECDF) xmin, xmax = extrema(x0) z = ecdf_xvalues(ecdf, Inf) n = length(z) diff --git a/src/stats/hist.jl b/src/stats/hist.jl index 5c9c9a0a9f8..807ea81086d 100644 --- a/src/stats/hist.jl +++ b/src/stats/hist.jl @@ -27,7 +27,7 @@ end Plot a step histogram of `values`. """ -@recipe StepHist values begin +@recipe StepHist (values,) begin "Can be an `Int` to create that number of equal-width bins over the range of `values`. Alternatively, it can be a sorted iterable of bin edges." bins = 15 # Int or iterable of edges """Allows to apply a normalization to the histogram. @@ -90,7 +90,7 @@ end Plot a histogram of `values`. """ -@recipe Hist values begin +@recipe Hist (values,) begin """ Can be an `Int` to create that number of equal-width bins over the range of `values`. Alternatively, it can be a sorted iterable of bin edges. """ diff --git a/src/stats/violin.jl b/src/stats/violin.jl index 6aa7473fa6f..2e4f418223c 100644 --- a/src/stats/violin.jl +++ b/src/stats/violin.jl @@ -5,7 +5,7 @@ Draw a violin plot. - `x`: positions of the categories - `y`: variables whose density is computed """ -@recipe Violin x y begin +@recipe Violin (x, y) begin npoints = 200 boundary = automatic bandwidth = automatic diff --git a/src/utilities/quaternions.jl b/src/utilities/quaternions.jl index d6648306dcc..6891a13e97d 100644 --- a/src/utilities/quaternions.jl +++ b/src/utilities/quaternions.jl @@ -134,7 +134,7 @@ end function orthogonal(v::T) where T <: StaticVector{3} x, y, z = abs.(v) - other = x < y ? (x < z ? unit(T, 1) : unit(T, 3)) : (y < z ? unit(T, 2) : unit(T, 3)) + other = x < y ? (x < z ? GeometryBasics.unit(T, 1) : GeometryBasics.unit(T, 3)) : (y < z ? GeometryBasics.unit(T, 2) : GeometryBasics.unit(T, 3)) return cross(v, other) end diff --git a/src/utilities/utilities.jl b/src/utilities/utilities.jl index 5d3d85bdd9b..bd95f1b3a49 100644 --- a/src/utilities/utilities.jl +++ b/src/utilities/utilities.jl @@ -471,6 +471,25 @@ function available_plotting_methods() return meths end +function extract_method_arguments(m::Method) + tv, decls, file, line = Base.arg_decl_parts(m) + tnames = map(decls[3:end]) do (n, t) + return string(n, "::", t) + end + return join(tnames, ", ") +end + +function available_conversions(PlotType) + result = [] + for m in methods(convert_arguments, (PlotType, Vararg{Any})) + push!(result, extract_method_arguments(m)) + end + for m in methods(convert_arguments, (typeof(Makie.conversion_trait(PlotType)), Vararg{Any})) + push!(result, extract_method_arguments(m)) + end + return result +end + mindist(x, a, b) = min(abs(a - x), abs(b - x)) function gappy(x, ps) n = length(ps) diff --git a/test/conversions.jl b/test/conversions.jl index 4a068c79286..c9969e14651 100644 --- a/test/conversions.jl +++ b/test/conversions.jl @@ -172,7 +172,8 @@ end @testset "single conversions" begin myvector = MyVector(collect(1:10)) mynestedvector = MyNestedVector(MyVector(collect(11:20))) - @test_throws ErrorException convert_arguments(Lines, myvector, mynestedvector) + @test convert_arguments(Lines, myvector, mynestedvector) === + (myvector, mynestedvector) Makie.convert_single_argument(v::MyNestedVector) = v.v Makie.convert_single_argument(v::MyVector) = v.v @@ -328,8 +329,8 @@ end @test convert_arguments(Image, m3) == (0f0..10f0, 0f0..6f0, o3) @test convert_arguments(Image, v1, r2, m3) == (1f0..10f0, 1f0..6f0, o3) @test convert_arguments(Image, i1, v2, m3) == (1f0..10f0, 1f0..6f0, o3) - @test_throws ErrorException convert_arguments(Image, m1, m2, m3) - @test_throws ErrorException convert_arguments(Heatmap, m1, m2) + @test convert_arguments(Image, m1, m2, m3) === (m1, m2, m3) + @test convert_arguments(Heatmap, m1, m2) === (m1, m2) end @testset "VertexGrid conversion" begin @@ -351,9 +352,10 @@ end @test convert_arguments(Heatmap, r1, i2, m3) == (o1, o2, o3) @test convert_arguments(Heatmap, v1, r2, m3) == (o1, o2, o3) @test convert_arguments(Heatmap, 0:10, v2, m3) == (collect(0f0:10f0), o2, o3) - @test_throws ErrorException convert_arguments(Heatmap, m1, m2, m3) - @test_throws ErrorException convert_arguments(Heatmap, m1, m2) - + # TODO, this throws ERROR: MethodError: no method matching adjust_axes(::CellGrid, ::Matrix{Int64}, ::Matrix{Int64}, ::Matrix{Float64}) + # Is this what we want to test for? + @test_throws MethodError convert_arguments(Heatmap, m1, m2, m3) === (m1, m2, m3) + @test convert_arguments(Heatmap, m1, m2) === (m1, m2) # https://github.com/MakieOrg/Makie.jl/issues/3515 @test convert_arguments(Heatmap, 1:8, 1:8, Array{Union{Float64,Missing}}(zeros(8, 8))) == (0.5:8.5, 0.5:8.5, zeros(8, 8)) end diff --git a/test/convert_arguments.jl b/test/convert_arguments.jl index b1decd46b5d..90ab2608f4e 100644 --- a/test/convert_arguments.jl +++ b/test/convert_arguments.jl @@ -1,12 +1,71 @@ +using Makie using Makie: NoConversion, - convert_arguments, conversion_trait, convert_single_argument, + PointBased, ClosedInterval - using Logging +function apply_conversion(trait, args...) + return Makie.convert_arguments(trait, args...) +end + +@testset "tuples" begin + @test convert_arguments(PointBased(), [(1, 2), (1.0, 1.0f0)]) == (Point{2,Float64}[[1.0, 2.0], [1.0, 1.0]],) +end + + +struct CustomType + v::Float64 +end + +Makie.convert_single_argument(c::CustomType) = c.v +Makie.convert_single_argument(cs::AbstractArray{<:CustomType}) = [c.v for c in cs] +# Example of the U type +struct UnitSquare + origin::Point2 +end +function Makie.convert_single_argument(ss::Vector{UnitSquare}) + return map(ss) do s + return Rect(s.origin..., 1, 1) + end +end + +@testset "single_convert_arguments recursion" begin + # issue https://github.com/MakieOrg/Makie.jl/issues/3655 + xs = 1:10 + ys = CustomType.(Float64.(1:10)) + @test Makie.convert_arguments(Rangebars, ys, xs .- 1, xs .+ 1)[1] isa Vector{<:Vec3} + square = UnitSquare(Point2(0, 0)) + data = [square] + after_conversion = Makie.convert_single_argument(data) + expected_conversion = [Rect(0, 0, 1, 1)] + # Although the types are the same + @test typeof(after_conversion) == typeof(expected_conversion) + @test expected_conversion == Makie.convert_arguments(Poly, data)[1] + m = Makie.to_spritemarker(:circle) + res = Makie.convert_arguments(Poly, m)[1] + @test res isa Vector{<:Point2} +end + + +@testset "wrong arguments" begin + # Only works for recipes defined via the new recipe with typed arguments + @test_throws ArgumentError scatter(1im) + @test_throws ArgumentError scatter(1im, 1im) + @test_throws ArgumentError scatter(Figure(), 1im) + f = Figure() + ax = Axis(f[1, 1]) + @test_throws ArgumentError scatter!(ax, 1im) + @test_throws ArgumentError scatter(rand(Point4f, 10)) + @test_throws ArgumentError lines(1im) + @test_throws ArgumentError linesegments(1im) + @test_throws ArgumentError volume(1im) + @test_throws ArgumentError image(1im) + @test_throws ArgumentError heatmap(1im) +end + @testset "convert_arguments" begin #= TODO: @@ -33,10 +92,10 @@ using Logging end return end - +\ indices = [1, 2, 3, 2, 3, 4, 5, 6, 7, 8, 9, 10] str = "test" - strings = ["test" for _ in 1:10] + strings = fill(str, 10) @testset "input type -> output type" begin for (T_in, T_out) in [ @@ -100,74 +159,74 @@ using Logging # - FaceView # - SubArra{<: VecTypes} - @test convert_arguments(CT, xs[1], xs[2]) isa Tuple{Vector{Point2{T_out}}} - @test convert_arguments(CT, xs[1], xs[2], xs[3]) isa Tuple{Vector{Point3{T_out}}} - @test convert_arguments(CT, ps2[1]) isa Tuple{Vector{Point2{T_out}}} - @test convert_arguments(CT, ps3[1]) isa Tuple{Vector{Point3{T_out}}} + @test apply_conversion(CT, xs[1], xs[2]) isa Tuple{Vector{Point2{T_out}}} + @test apply_conversion(CT, xs[1], xs[2], xs[3]) isa Tuple{Vector{Point3{T_out}}} + @test apply_conversion(CT, ps2[1]) isa Tuple{Vector{Point2{T_out}}} + @test apply_conversion(CT, ps3[1]) isa Tuple{Vector{Point3{T_out}}} # because indices are Int we end up converting to Float64 no matter what - @test convert_arguments(CT, xs) isa Tuple{Vector{Point2{Float64}}} - - @test convert_arguments(CT, xs, ys) isa Tuple{Vector{Point2{T_out}}} - @test convert_arguments(CT, xs, v32) isa Tuple{Vector{Point2{T_out}}} - @test convert_arguments(CT, i, ys) isa Tuple{Vector{Point2{T_out}}} - @test convert_arguments(CT, xs, i) isa Tuple{Vector{Point2{T_out}}} - @test convert_arguments(CT, r, ys) isa Tuple{Vector{Point2{T_out}}} - # @test convert_arguments(CT, vt, ys) isa Tuple{Vector{Point2{T_out}}} - # @test convert_arguments(CT, m, m) isa Tuple{Vector{Point2{T_out}}} - - @test convert_arguments(CT, xs, ys, zs) isa Tuple{Vector{Point3{T_out}}} - @test convert_arguments(CT, vt, ys, vt) isa Tuple{Vector{Point3{T_out}}} - @test convert_arguments(CT, xs, ys, vt) isa Tuple{Vector{Point3{T_out}}} - @test convert_arguments(CT, xs, ys, v32) isa Tuple{Vector{Point3{T_out}}} - @test convert_arguments(CT, r, v32, zs) isa Tuple{Vector{Point3{T_out}}} - @test convert_arguments(CT, m, m, m) isa Tuple{Vector{Point3{T_out}}} + @test apply_conversion(CT, xs) isa Tuple{Vector{Point2{Float64}}} + + @test apply_conversion(CT, xs, ys) isa Tuple{Vector{Point2{T_out}}} + @test apply_conversion(CT, xs, v32) isa Tuple{Vector{Point2{T_out}}} + @test apply_conversion(CT, i, ys) isa Tuple{Vector{Point2{T_out}}} + @test apply_conversion(CT, xs, i) isa Tuple{Vector{Point2{T_out}}} + @test apply_conversion(CT, r, ys) isa Tuple{Vector{Point2{T_out}}} + # @test apply_conversion(CT, vt, ys) isa Tuple{Vector{Point2{T_out}}} + # @test apply_conversion(CT, m, m) isa Tuple{Vector{Point2{T_out}}} + + @test apply_conversion(CT, xs, ys, zs) isa Tuple{Vector{Point3{T_out}}} + @test apply_conversion(CT, vt, ys, vt) isa Tuple{Vector{Point3{T_out}}} + @test apply_conversion(CT, xs, ys, vt) isa Tuple{Vector{Point3{T_out}}} + @test apply_conversion(CT, xs, ys, v32) isa Tuple{Vector{Point3{T_out}}} + @test apply_conversion(CT, r, v32, zs) isa Tuple{Vector{Point3{T_out}}} + @test apply_conversion(CT, m, m, m) isa Tuple{Vector{Point3{T_out}}} # TODO: Does this make sense? - @test convert_arguments(CT, i, i, m) isa Tuple{Vector{Point3{T_out}}} - # @test convert_arguments(CT, r, i, zs) isa Tuple{Vector{Point3{T_out}}} - # @test convert_arguments(CT, i, i, zs) isa Tuple{Vector{Point3{T_out}}} + @test apply_conversion(CT, i, i, m) isa Tuple{Vector{Point3{T_out}}} + # @test apply_conversion(CT, r, i, zs) isa Tuple{Vector{Point3{T_out}}} + # @test apply_conversion(CT, i, i, zs) isa Tuple{Vector{Point3{T_out}}} # TODO: implement as PointBased conversion? if CT !== PointBased() - @test convert_arguments(CT, xs, identity) isa Tuple{Vector{Point2{T_out}}} - @test convert_arguments(CT, r, identity) isa Tuple{Vector{Point2{T_out}}} - @test convert_arguments(CT, i, identity) isa Tuple{Vector{Point2{T_out}}} + @test apply_conversion(CT, xs, identity) isa Tuple{Vector{Point2{T_out}}} + @test apply_conversion(CT, r, identity) isa Tuple{Vector{Point2{T_out}}} + @test apply_conversion(CT, i, identity) isa Tuple{Vector{Point2{T_out}}} - @test convert_arguments(CT, xs, miss) isa Tuple{Vector{Point2{T_out}}} - @test convert_arguments(CT, miss, ys, zs) isa Tuple{Vector{Point3{T_out}}} + @test apply_conversion(CT, xs, miss) isa Tuple{Vector{Point2{T_out}}} + @test apply_conversion(CT, miss, ys, zs) isa Tuple{Vector{Point3{T_out}}} - @test convert_arguments(CT, miss2) isa Tuple{Vector{Point2{T_out}}} + @test apply_conversion(CT, miss2) isa Tuple{Vector{Point2{T_out}}} end - @test convert_arguments(CT, ps2) isa Tuple{Vector{Point2{T_out}}} - @test convert_arguments(CT, ps3) isa Tuple{Vector{Point3{T_out}}} - # @test convert_arguments(CT, Point.(miss, ys)) isa Tuple{Vector{Point2{T_out}}} + @test apply_conversion(CT, ps2) isa Tuple{Vector{Point2{T_out}}} + @test apply_conversion(CT, ps3) isa Tuple{Vector{Point3{T_out}}} + # @test apply_conversion(CT, Point.(miss, ys)) isa Tuple{Vector{Point2{T_out}}} # TODO: Should this be Point? - @test convert_arguments(CT, Vec.(xs, ys)) isa Tuple{Vector{Vec2{T_out}}} - @test convert_arguments(CT, Vec.(xs, ys, zs)) isa Tuple{Vector{Vec3{T_out}}} - # @test convert_arguments(CT, Vec.(miss, ys)) isa Tuple{Vector{Point2{T_out}}} - - @test convert_arguments(CT, tuple.(xs, ys)) isa Tuple{Vector{Point2{T_out}}} - @test convert_arguments(CT, tuple.(xs, v32)) isa Tuple{Vector{Point2{T_out}}} - @test convert_arguments(CT, tuple.(xs, ys, zs)) isa Tuple{Vector{Point3{T_out}}} - @test convert_arguments(CT, tuple.(v32, ys, zs)) isa Tuple{Vector{Point3{T_out}}} - # @test convert_arguments(CT, tuple.(miss, ys)) isa Tuple{Vector{Point2{T_out}}} - - @test convert_arguments(CT, rect2) isa Tuple{Vector{Point2{T_out}}} - @test convert_arguments(CT, rect3) isa Tuple{Vector{Point3{T_out}}} - @test convert_arguments(CT, geom) isa Tuple{Vector{Point3{T_out}}} - @test convert_arguments(CT, _mesh) isa Tuple{Vector{Point3{T_out}}} - @test convert_arguments(CT, polygon) isa Tuple{Vector{Point2{T_out}}} - @test convert_arguments(CT, [polygon, polygon]) isa Tuple{Vector{Point2{T_out}}} - @test convert_arguments(CT, MultiPolygon([polygon, polygon])) isa Tuple{Vector{Point2{T_out}}} - @test convert_arguments(CT, line) isa Tuple{Vector{Point3{T_out}}} - @test convert_arguments(CT, [line, line]) isa Tuple{Vector{Point3{T_out}}} - @test convert_arguments(CT, MultiLineString([line, line])) isa Tuple{Vector{Point3{T_out}}} - @test convert_arguments(CT, bp) isa Tuple{Vector{Point2d}} # BezierPath uses Float64 internally - - @test convert_arguments(CT, m2) isa Tuple{Vector{Point2{T_out}}} - @test convert_arguments(CT, m3) isa Tuple{Vector{Point3{T_out}}} + @test apply_conversion(CT, Vec.(xs, ys)) isa Tuple{Vector{Point2{T_out}}} + @test apply_conversion(CT, Vec.(xs, ys, zs)) isa Tuple{Vector{Point3{T_out}}} + # @test apply_conversion(CT, Vec.(miss, ys)) isa Tuple{Vector{Point2{T_out}}} + + @test apply_conversion(CT, tuple.(xs, ys)) isa Tuple{Vector{Point2{T_out}}} + @test apply_conversion(CT, tuple.(xs, v32)) isa Tuple{Vector{Point2{T_out}}} + @test apply_conversion(CT, tuple.(xs, ys, zs)) isa Tuple{Vector{Point3{T_out}}} + @test apply_conversion(CT, tuple.(v32, ys, zs)) isa Tuple{Vector{Point3{T_out}}} + # @test apply_conversion(CT, tuple.(miss, ys)) isa Tuple{Vector{Point2{T_out}}} + + @test apply_conversion(CT, rect2) isa Tuple{Vector{Point2{T_out}}} + @test apply_conversion(CT, rect3) isa Tuple{Vector{Point3{T_out}}} + @test apply_conversion(CT, geom) isa Tuple{Vector{Point3{T_out}}} + @test apply_conversion(CT, _mesh) isa Tuple{Vector{Point3{T_out}}} + @test apply_conversion(CT, polygon) isa Tuple{Vector{Point2{T_out}}} + @test apply_conversion(CT, [polygon, polygon]) isa Tuple{Vector{Point2{T_out}}} + @test apply_conversion(CT, MultiPolygon([polygon, polygon])) isa Tuple{Vector{Point2{T_out}}} + @test apply_conversion(CT, line) isa Tuple{Vector{Point3{T_out}}} + @test apply_conversion(CT, [line, line]) isa Tuple{Vector{Point3{T_out}}} + @test apply_conversion(CT, MultiLineString([line, line])) isa Tuple{Vector{Point3{T_out}}} + @test apply_conversion(CT, bp) isa Tuple{Vector{Point2d}} # BezierPath uses Float64 internally + + @test apply_conversion(CT, m2) isa Tuple{Vector{Point2{T_out}}} + @test apply_conversion(CT, m3) isa Tuple{Vector{Point3{T_out}}} end end @@ -175,11 +234,11 @@ using Logging @testset "LineSegments Extras" begin if T_out == T_in - @test convert_arguments(LineSegments, Pair.(ps2, ps2)) isa Tuple{<: Base.ReinterpretArray} - @test convert_arguments(LineSegments, tuple.(ps2, ps2)) isa Tuple{<: Base.ReinterpretArray} + @test apply_conversion(LineSegments, Pair.(ps2, ps2)) isa Tuple{<: Base.ReinterpretArray} + @test apply_conversion(LineSegments, tuple.(ps2, ps2)) isa Tuple{<: Base.ReinterpretArray} else - @test convert_arguments(LineSegments, Pair.(ps2, ps2)) isa Tuple{Vector{Point2{T_out}}} - @test convert_arguments(LineSegments, tuple.(ps2, ps2)) isa Tuple{Vector{Point2{T_out}}} + @test apply_conversion(LineSegments, Pair.(ps2, ps2)) isa Tuple{Vector{Point2{T_out}}} + @test apply_conversion(LineSegments, tuple.(ps2, ps2)) isa Tuple{Vector{Point2{T_out}}} end end @@ -187,15 +246,15 @@ using Logging for CT in (CellGrid(), Heatmap) @testset "$CT" begin - @test convert_arguments(CT, m) isa Tuple{Vector{Float32}, Vector{Float32}, Matrix{Float32}} - - @test convert_arguments(CT, xs, ys, m) isa Tuple{Vector{T_out}, Vector{T_out}, Matrix{Float32}} - @test convert_arguments(CT, xs, r, m) isa Tuple{Vector{T_out}, Vector{T_out}, Matrix{Float32}} - @test convert_arguments(CT, r, ys, +) isa Tuple{Vector{T_out}, Vector{T_out}, Matrix{Float32}} - @test convert_arguments(CT, i, r, m) isa Tuple{Vector{T_out}, Vector{T_out}, Matrix{Float32}} - @test convert_arguments(CT, i, i, m) isa Tuple{Vector{T_out}, Vector{T_out}, Matrix{Float32}} - @test convert_arguments(CT, r, i, m) isa Tuple{Vector{T_out}, Vector{T_out}, Matrix{Float32}} - @test convert_arguments(CT, xgridvec, ygridvec, xgridvec) isa Tuple{Vector{T_out}, Vector{T_out}, Matrix{Float32}} + @test apply_conversion(CT, m) isa Tuple{Vector{Float32}, Vector{Float32}, Matrix{Float32}} + + @test apply_conversion(CT, xs, ys, m) isa Tuple{Vector{T_out}, Vector{T_out}, Matrix{Float32}} + @test apply_conversion(CT, xs, r, m) isa Tuple{Vector{T_out}, Vector{T_out}, Matrix{Float32}} + @test apply_conversion(CT, r, ys, +) isa Tuple{Vector{T_out}, Vector{T_out}, Matrix{Float32}} + @test apply_conversion(CT, i, r, m) isa Tuple{Vector{T_out}, Vector{T_out}, Matrix{Float32}} + @test apply_conversion(CT, i, i, m) isa Tuple{Vector{T_out}, Vector{T_out}, Matrix{Float32}} + @test apply_conversion(CT, r, i, m) isa Tuple{Vector{T_out}, Vector{T_out}, Matrix{Float32}} + @test apply_conversion(CT, xgridvec, ygridvec, xgridvec) isa Tuple{Vector{T_out}, Vector{T_out}, Matrix{Float32}} # TODO OffsetArray end end @@ -204,21 +263,21 @@ using Logging for CT in (VertexGrid(), Surface) @testset "$CT" begin - @test convert_arguments(CT, xs, ys, m) isa Tuple{Vector{T_out}, Vector{T_out}, Matrix{Float32}} - @test convert_arguments(CT, m, m) isa Tuple{Matrix{T_out}, Matrix{T_out}, Matrix{Float32}} + @test apply_conversion(CT, xs, ys, m) isa Tuple{Vector{T_out}, Vector{T_out}, Matrix{Float32}} + @test apply_conversion(CT, m, m) isa Tuple{Matrix{T_out}, Matrix{T_out}, Matrix{Float32}} # TODO: Should these be normalized to Vector? if T_in == T_out - @test convert_arguments(CT, xs, r, m) isa Tuple{Vector{T_out}, AbstractRange{T_out}, Matrix{Float32}} - @test convert_arguments(CT, r, ys, +) isa Tuple{AbstractRange{T_out}, Vector{T_out}, Matrix{Float32}} + @test apply_conversion(CT, xs, r, m) isa Tuple{Vector{T_out}, AbstractRange{T_out}, Matrix{Float32}} + @test apply_conversion(CT, r, ys, +) isa Tuple{AbstractRange{T_out}, Vector{T_out}, Matrix{Float32}} else - @test convert_arguments(CT, xs, r, m) isa Tuple{Vector{T_out}, Vector{T_out}, Matrix{Float32}} - @test convert_arguments(CT, r, ys, +) isa Tuple{Vector{T_out}, Vector{T_out}, Matrix{Float32}} + @test apply_conversion(CT, xs, r, m) isa Tuple{Vector{T_out}, Vector{T_out}, Matrix{Float32}} + @test apply_conversion(CT, r, ys, +) isa Tuple{Vector{T_out}, Vector{T_out}, Matrix{Float32}} end - @test convert_arguments(CT, m) isa Tuple{AbstractRange{Float32}, AbstractRange{Float32}, Matrix{Float32}} - @test convert_arguments(CT, i, r, m) isa Tuple{AbstractRange{T_out}, AbstractRange{T_out}, Matrix{Float32}} - @test convert_arguments(CT, i, i, m) isa Tuple{AbstractRange{T_out}, AbstractRange{T_out}, Matrix{Float32}} - @test convert_arguments(CT, r, i, m) isa Tuple{AbstractRange{T_out}, AbstractRange{T_out}, Matrix{Float32}} - @test convert_arguments(CT, xgridvec, ygridvec, xgridvec) isa Tuple{Vector{T_out}, Vector{T_out}, Matrix{Float32}} + @test apply_conversion(CT, m) isa Tuple{AbstractRange{Float32}, AbstractRange{Float32}, Matrix{Float32}} + @test apply_conversion(CT, i, r, m) isa Tuple{AbstractRange{T_out}, AbstractRange{T_out}, Matrix{Float32}} + @test apply_conversion(CT, i, i, m) isa Tuple{AbstractRange{T_out}, AbstractRange{T_out}, Matrix{Float32}} + @test apply_conversion(CT, r, i, m) isa Tuple{AbstractRange{T_out}, AbstractRange{T_out}, Matrix{Float32}} + @test apply_conversion(CT, xgridvec, ygridvec, xgridvec) isa Tuple{Vector{T_out}, Vector{T_out}, Matrix{Float32}} # TODO OffsetArray end end @@ -227,17 +286,17 @@ using Logging for CT in (ImageLike(), Image) @testset "$CT" begin - @test convert_arguments(CT, img) isa Tuple{ClosedInterval{Float32}, ClosedInterval{Float32}, Matrix{RGBf}} - @test convert_arguments(CT, m) isa Tuple{ClosedInterval{Float32}, ClosedInterval{Float32}, Matrix{Float32}} - @test convert_arguments(CT, i, i, m) isa Tuple{ClosedInterval{T_out}, ClosedInterval{T_out}, Matrix{Float32}} + @test apply_conversion(CT, img) isa Tuple{ClosedInterval{Float32}, ClosedInterval{Float32}, Matrix{RGBf}} + @test apply_conversion(CT, m) isa Tuple{ClosedInterval{Float32}, ClosedInterval{Float32}, Matrix{Float32}} + @test apply_conversion(CT, i, i, m) isa Tuple{ClosedInterval{T_out}, ClosedInterval{T_out}, Matrix{Float32}} # deprecated Logging.disable_logging(Logging.Warn) # skip warnings - @test convert_arguments(CT, xs, ys, m) isa Tuple{ClosedInterval{T_out}, ClosedInterval{T_out}, Matrix{Float32}} - @test convert_arguments(CT, xs, r, m) isa Tuple{ClosedInterval{T_out}, ClosedInterval{T_out}, Matrix{Float32}} - @test convert_arguments(CT, i, r, m) isa Tuple{ClosedInterval{T_out}, ClosedInterval{T_out}, Matrix{Float32}} - @test convert_arguments(CT, r, i, m) isa Tuple{ClosedInterval{T_out}, ClosedInterval{T_out}, Matrix{Float32}} - @test convert_arguments(CT, r, ys, +) isa Tuple{ClosedInterval{T_out}, ClosedInterval{T_out}, Matrix{Float32}} + @test apply_conversion(CT, xs, ys, m) isa Tuple{ClosedInterval{T_out}, ClosedInterval{T_out}, Matrix{Float32}} + @test apply_conversion(CT, xs, r, m) isa Tuple{ClosedInterval{T_out}, ClosedInterval{T_out}, Matrix{Float32}} + @test apply_conversion(CT, i, r, m) isa Tuple{ClosedInterval{T_out}, ClosedInterval{T_out}, Matrix{Float32}} + @test apply_conversion(CT, r, i, m) isa Tuple{ClosedInterval{T_out}, ClosedInterval{T_out}, Matrix{Float32}} + @test apply_conversion(CT, r, ys, +) isa Tuple{ClosedInterval{T_out}, ClosedInterval{T_out}, Matrix{Float32}} Logging.disable_logging(Logging.Debug) end end @@ -247,39 +306,42 @@ using Logging for CT in (VolumeLike(), Volume) @testset "$CT" begin # TODO: Should these be normalized more? - @test convert_arguments(CT, vol) isa Tuple{ClosedInterval{Float32}, ClosedInterval{Float32}, ClosedInterval{Float32}, Array{Float32, 3}} - @test convert_arguments(CT, i, i, i, vol) isa Tuple{ClosedInterval{Float32}, ClosedInterval{Float32}, ClosedInterval{Float32}, Array{Float32, 3}} - @test convert_arguments(CT, xs, ys, zs, vol) isa Tuple{Vector{Float32}, Vector{Float32}, Vector{Float32}, Array{Float32, 3}} - @test convert_arguments(CT, xs, ys, zs, +) isa Tuple{Vector{Float32}, Vector{Float32}, Vector{Float32}, Array{Float32, 3}} + @test apply_conversion(CT, vol) isa Tuple{ClosedInterval{Float32}, ClosedInterval{Float32}, ClosedInterval{Float32}, Array{Float32,3}} + @test apply_conversion(CT, i, i, i, vol) isa Tuple{ClosedInterval{T_out}, ClosedInterval{T_out}, ClosedInterval{T_out}, Array{Float32,3}} + + Logging.disable_logging(Logging.Warn) # skip warnings + @test apply_conversion(CT, xs, ys, zs, vol) isa Tuple{ClosedInterval{T_out}, ClosedInterval{T_out}, ClosedInterval{T_out}, Array{Float32,3}} + @test apply_conversion(CT, xs, ys, zs, +) isa Tuple{ClosedInterval{T_out}, ClosedInterval{T_out}, ClosedInterval{T_out}, Array{Float32,3}} if T_in == Float32 - @test convert_arguments(CT, r, r, r, vol) isa Tuple{AbstractRange{Float32}, AbstractRange{Float32}, AbstractRange{Float32}, Array{Float32, 3}} - @test convert_arguments(CT, xs, r, i, vol) isa Tuple{Vector{Float32}, AbstractRange{Float32}, ClosedInterval{Float32}, Array{Float32, 3}} + @test apply_conversion(CT, r, r, r, vol) isa Tuple{ClosedInterval{T_out}, ClosedInterval{T_out}, ClosedInterval{T_out}, Array{Float32,3}} + @test apply_conversion(CT, xs, r, i, vol) isa Tuple{ClosedInterval{T_out}, ClosedInterval{T_out}, ClosedInterval{T_out}, Array{Float32,3}} else - @test convert_arguments(CT, r, r, r, vol) isa Tuple{Vector{Float32}, Vector{Float32}, Vector{Float32}, Array{Float32, 3}} - @test convert_arguments(CT, xs, r, i, vol) isa Tuple{Vector{Float32}, Vector{Float32}, ClosedInterval{Float32}, Array{Float32, 3}} + @test apply_conversion(CT, r, r, r, vol) isa Tuple{ClosedInterval{T_out}, ClosedInterval{T_out}, ClosedInterval{T_out}, Array{Float32,3}} + @test apply_conversion(CT, xs, r, i, vol) isa Tuple{ClosedInterval{T_out}, ClosedInterval{T_out}, ClosedInterval{T_out}, Array{Float32,3}} end + Logging.disable_logging(Logging.Debug) end end # Mesh @testset "Mesh" begin - test_mesh_result(convert_arguments(Makie.Mesh, xs, ys, zs), 3, T_out, true) - test_mesh_result(convert_arguments(Makie.Mesh, ps3), 3, T_out, true) - test_mesh_result(convert_arguments(Makie.Mesh, _mesh), 3, T_out, true) - test_mesh_result(convert_arguments(Makie.Mesh, geom), 3, T_out, true) - test_mesh_result(convert_arguments(Makie.Mesh, xs, ys, zs, indices), 3, T_out, true) - test_mesh_result(convert_arguments(Makie.Mesh, ps3, indices), 3, T_out, true) - - test_mesh_result(convert_arguments(Makie.Mesh, polygon), 2, T_out) - test_mesh_result(convert_arguments(Makie.Mesh, ps2), 2, T_out) - test_mesh_result(convert_arguments(Makie.Mesh, ps2, indices), 2, T_out) + test_mesh_result(apply_conversion(Makie.Mesh, xs, ys, zs), 3, T_out, true) + test_mesh_result(apply_conversion(Makie.Mesh, ps3), 3, T_out, true) + test_mesh_result(apply_conversion(Makie.Mesh, _mesh), 3, T_out, true) + test_mesh_result(apply_conversion(Makie.Mesh, geom), 3, T_out, true) + test_mesh_result(apply_conversion(Makie.Mesh, xs, ys, zs, indices), 3, T_out, true) + test_mesh_result(apply_conversion(Makie.Mesh, ps3, indices), 3, T_out, true) + + test_mesh_result(apply_conversion(Makie.Mesh, polygon), 2, T_out) + test_mesh_result(apply_conversion(Makie.Mesh, ps2), 2, T_out) + test_mesh_result(apply_conversion(Makie.Mesh, ps2, indices), 2, T_out) end # internally converted @testset "Text" begin - @test convert_arguments(Makie.Text, tuple.(strings, ps2)) isa Tuple{Vector{Tuple{String, Point2{T_in}}}} - @test convert_arguments(Makie.Text, tuple.(strings, ps3)) isa Tuple{Vector{Tuple{String, Point3{T_in}}}} + @test apply_conversion(Makie.Text, tuple.(strings, ps2)) isa Tuple{Vector{Tuple{String, Point2{T_in}}}} + @test apply_conversion(Makie.Text, tuple.(strings, ps3)) isa Tuple{Vector{Tuple{String, Point3{T_in}}}} end @@ -288,130 +350,122 @@ using Logging ################################################################ # If a recipe transforms its input arguments it is fine for it - # to keep T_in in convert_arguments. + # to keep T_in in apply_conversion. @testset "Annotations" begin - @test convert_arguments(Annotations, strings, ps2) isa Tuple{Vector{Tuple{String, Point{2, T_out}}}} - @test convert_arguments(Annotations, strings, ps3) isa Tuple{Vector{Tuple{String, Point{3, T_out}}}} + @test apply_conversion(Annotations, strings, ps2) isa Tuple{Vector{Tuple{String, Point{2, T_out}}}} + @test apply_conversion(Annotations, strings, ps3) isa Tuple{Vector{Tuple{String, Point{3, T_out}}}} end @testset "Arrows" begin - @test convert_arguments(Arrows, xs, ys, xs, ys) isa Tuple{Vector{Point2{T_out}}, Vector{Vec2{T_out}}} - @test convert_arguments(Arrows, xs, ys, m, m) isa Tuple{Vector{Point2{T_out}}, Vector{Vec2{T_out}}} - @test convert_arguments(Arrows, xs, ys, zs, xs, ys, zs) isa Tuple{Vector{Point3{T_out}}, Vector{Vec3{T_out}}} - @test convert_arguments(Arrows, xs, ys, identity) isa Tuple{Vector{Point2{T_out}}, Vector{Vec2{T_out}}} - @test convert_arguments(Arrows, xs, ys, zs, identity) isa Tuple{Vector{Point3{T_out}}, Vector{Vec3{T_out}}} + @test apply_conversion(Arrows, xs, ys, xs, ys) isa Tuple{Vector{Point2{T_out}}, Vector{Vec2{T_out}}} + @test apply_conversion(Arrows, xs, ys, m, m) isa Tuple{Vector{Point2{T_out}}, Vector{Vec2{T_out}}} + @test apply_conversion(Arrows, xs, ys, zs, xs, ys, zs) isa Tuple{Vector{Point3{T_out}}, Vector{Vec3{T_out}}} + @test apply_conversion(Arrows, xs, ys, identity) isa Tuple{Vector{Point2{T_out}}, Vector{Vec2{T_out}}} + @test apply_conversion(Arrows, xs, ys, zs, identity) isa Tuple{Vector{Point3{T_out}}, Vector{Vec3{T_out}}} end @testset "Band" begin - @test convert_arguments(Band, xs, ys, zs) isa Tuple{Vector{Point2{T_out}}, Vector{Point2{T_out}}} + @test apply_conversion(Band, xs, ys, zs) isa Tuple{Vector{Point2{T_out}}, Vector{Point2{T_out}}} end @testset "Bracket" begin - @test convert_arguments(Bracket, ps2[1], ps2[2]) isa Tuple{Vector{Tuple{Point2{T_out}, Point2{T_out}}}} - @test convert_arguments(Bracket, xs[1], ys[1], xs[2], ys[2]) isa Tuple{Vector{Tuple{Point2{T_out}, Point2{T_out}}}} - @test convert_arguments(Bracket, xs, ys, xs, ys) isa Tuple{Vector{Tuple{Point2{T_out}, Point2{T_out}}}} + @test apply_conversion(Bracket, ps2[1], ps2[2]) isa Tuple{Vector{Tuple{Point2{T_out}, Point2{T_out}}}} + @test apply_conversion(Bracket, xs[1], ys[1], xs[2], ys[2]) isa Tuple{Vector{Tuple{Point2{T_out}, Point2{T_out}}}} + @test apply_conversion(Bracket, xs, ys, xs, ys) isa Tuple{Vector{Tuple{Point2{T_out}, Point2{T_out}}}} end @testset "Errorbars & Rangebars" begin - @test convert_arguments(Errorbars, xs, ys, zs) isa Tuple{Vector{Vec4{T_out}}} - @test convert_arguments(Errorbars, xs, ys, xs, ys) isa Tuple{Vector{Vec4{T_out}}} - @test convert_arguments(Errorbars, xs, ys, ps2) isa Tuple{Vector{Vec4{T_out}}} - @test convert_arguments(Errorbars, ps2, zs) isa Tuple{Vector{Vec4{T_out}}} - @test convert_arguments(Errorbars, ps2, xs, ys) isa Tuple{Vector{Vec4{T_out}}} - @test convert_arguments(Errorbars, ps2, ps2) isa Tuple{Vector{Vec4{T_out}}} - @test convert_arguments(Errorbars, ps3) isa Tuple{Vector{Vec4{T_out}}} - - @test convert_arguments(Rangebars, xs, ys, zs) isa Tuple{Vector{Vec3{T_out}}} - @test convert_arguments(Rangebars, xs, ps2) isa Tuple{Vector{Vec3{T_out}}} + @test apply_conversion(Errorbars, xs, ys, zs) isa Tuple{Vector{Vec4{T_out}}} + @test apply_conversion(Errorbars, xs, ys, xs, ys) isa Tuple{Vector{Vec4{T_out}}} + @test apply_conversion(Errorbars, xs, ys, ps2) isa Tuple{Vector{Vec4{T_out}}} + @test apply_conversion(Errorbars, ps2, zs) isa Tuple{Vector{Vec4{T_out}}} + @test apply_conversion(Errorbars, ps2, xs, ys) isa Tuple{Vector{Vec4{T_out}}} + @test apply_conversion(Errorbars, ps2, ps2) isa Tuple{Vector{Vec4{T_out}}} + @test apply_conversion(Errorbars, ps3) isa Tuple{Vector{Vec4{T_out}}} + + @test apply_conversion(Rangebars, xs, ys, zs) isa Tuple{Vector{Vec3{T_out}}} + @test apply_conversion(Rangebars, xs, ps2) isa Tuple{Vector{Vec3{T_out}}} end @testset "Poly" begin # TODO: Are these ok? All of these are just reflection... - @test convert_arguments(Poly, ps2) isa Tuple{Vector{Point2{T_in}}} - @test convert_arguments(Poly, ps3) isa Tuple{Vector{Point3{T_in}}} - @test convert_arguments(Poly, [polygon]) isa Tuple{Vector{typeof(polygon)}} - @test convert_arguments(Poly, [rect2]) isa Tuple{Vector{typeof(rect2)}} - @test convert_arguments(Poly, polygon) isa Tuple{typeof(polygon)} - @test convert_arguments(Poly, rect2) isa Tuple{typeof(rect2)} - - # And these aren't mesh-like - @test convert_arguments(Poly, xs, ys) isa Tuple{Vector{Point2{T_out}}} - # Vector{Vector{...}} ? - @test convert_arguments(Poly, xs, ys, zs) isa Tuple{Vector{Vector{Point3{T_out}}}} - - @test convert_arguments(Poly, ps2, indices) isa Tuple{<: GeometryBasics.Mesh{2, T_out}} - @test convert_arguments(Poly, ps3, indices) isa Tuple{<: GeometryBasics.Mesh{3, T_out}} + @test apply_conversion(Poly, ps2) isa Tuple{Vector{Point2{T_out}}} + @test apply_conversion(Poly, [polygon]) isa Tuple{Vector{typeof(polygon)}} + @test apply_conversion(Poly, [rect2]) isa Tuple{Vector{typeof(rect2)}} + @test apply_conversion(Poly, polygon) isa Tuple{typeof(polygon)} + @test apply_conversion(Poly, rect2) isa Tuple{typeof(rect2)} + @test apply_conversion(Poly, ps2, indices) isa Tuple{<: GeometryBasics.Mesh{2, T_out}} + @test apply_conversion(Poly, ps3, indices) isa Tuple{<: GeometryBasics.Mesh{3, T_out}} end @testset "Series" begin - @test convert_arguments(Series, m) isa Tuple{Vector{Vector{Point2{Float64}}}} - @test convert_arguments(Series, xs, m) isa Tuple{Vector{Vector{Point2{T_out}}}} - @test convert_arguments(Series, miss, m) isa Tuple{Vector{Vector{Point2{T_out}}}} - @test convert_arguments(Series, [(xs, ys)]) isa Tuple{Vector{Vector{Point2{T_out}}}} - @test convert_arguments(Series, (xs, ys)) isa Tuple{Vector{Vector{Point2{T_out}}}} - @test convert_arguments(Series, [ps2, ps2]) isa Tuple{Vector{Vector{Point2{T_out}}}} + @test apply_conversion(Series, m) isa Tuple{Vector{Vector{Point2{Float64}}}} + @test apply_conversion(Series, xs, m) isa Tuple{Vector{Vector{Point2{T_out}}}} + @test apply_conversion(Series, miss, m) isa Tuple{Vector{Vector{Point2{T_out}}}} + @test apply_conversion(Series, [(xs, ys)]) isa Tuple{Vector{Vector{Point2{T_out}}}} + @test apply_conversion(Series, (xs, ys)) isa Tuple{Vector{Vector{Point2{T_out}}}} + @test apply_conversion(Series, [ps2, ps2]) isa Tuple{Vector{Vector{Point2{T_out}}}} end @testset "Spy" begin # TODO: assuming internal processing - @test convert_arguments(Spy, sparse) isa Tuple{ClosedInterval{Int}, ClosedInterval{Int}, typeof(sparse)} - @test convert_arguments(Spy, xs, ys, sparse) isa Tuple{typeof(xs), typeof(ys), typeof(sparse)} + @test apply_conversion(Spy, sparse) isa Tuple{ClosedInterval{Int}, ClosedInterval{Int}, typeof(sparse)} + @test apply_conversion(Spy, xs, ys, sparse) isa Tuple{typeof(xs), typeof(ys), typeof(sparse)} end @testset "StreamPlot" begin # TODO: these have a different argument order than other Function plots... - @test convert_arguments(StreamPlot, identity, xs, ys) isa Tuple{typeof(identity), Rect2{T_in}} - @test convert_arguments(StreamPlot, identity, i, r) isa Tuple{typeof(identity), Rect2{T_in}} - @test convert_arguments(StreamPlot, identity, xs, ys, zs) isa Tuple{typeof(identity), Rect3{T_in}} - @test convert_arguments(StreamPlot, identity, r, i, zs) isa Tuple{typeof(identity), Rect3{T_in}} - @test convert_arguments(StreamPlot, identity, rect2) isa Tuple{typeof(identity), Rect2{T_in}} - @test convert_arguments(StreamPlot, identity, rect3) isa Tuple{typeof(identity), Rect3{T_in}} + @test apply_conversion(StreamPlot, identity, xs, ys) isa Tuple{typeof(identity), Rect2{T_in}} + @test apply_conversion(StreamPlot, identity, i, r) isa Tuple{typeof(identity), Rect2{T_in}} + @test apply_conversion(StreamPlot, identity, xs, ys, zs) isa Tuple{typeof(identity), Rect3{T_in}} + @test apply_conversion(StreamPlot, identity, r, i, zs) isa Tuple{typeof(identity), Rect3{T_in}} + @test apply_conversion(StreamPlot, identity, rect2) isa Tuple{typeof(identity), Rect2{T_in}} + @test apply_conversion(StreamPlot, identity, rect3) isa Tuple{typeof(identity), Rect3{T_in}} end @testset "Tooltip" begin - @test convert_arguments(Tooltip, xs[1], ys[1], str) isa Tuple{Point2{T_out}, String} - @test convert_arguments(Tooltip, xs[1], ys[1]) isa Tuple{Point2{T_out}} + @test apply_conversion(Tooltip, xs[1], ys[1], str) isa Tuple{Point2{T_out}, String} + @test apply_conversion(Tooltip, xs[1], ys[1]) isa Tuple{Point2{T_out}} end @testset "Tricontourf" begin - @test convert_arguments(Tricontourf, xs, ys, zs) isa Tuple{<: Makie.DelTri.Triangulation{Matrix{T_out}}, Vector{T_out}} + @test apply_conversion(Tricontourf, xs, ys, zs) isa Tuple{<: Makie.DelTri.Triangulation{Matrix{T_out}}, Vector{T_out}} end @testset "Triplot" begin - @test convert_arguments(Triplot, ps2) isa Tuple{Vector{Point2{T_out}}} - @test convert_arguments(Triplot, xs, ys) isa Tuple{Vector{Point2{T_out}}} + @test apply_conversion(Triplot, ps2) isa Tuple{Vector{Point2{T_out}}} + @test apply_conversion(Triplot, xs, ys) isa Tuple{Vector{Point2{T_out}}} # TODO: DelTri.Triangulation end @testset "Voronoiplot" begin - @test convert_arguments(Voronoiplot, m) isa Tuple{Vector{Point3{Float64}}} - @test convert_arguments(Voronoiplot, xs, ys, zs) isa Tuple{Vector{Point3{T_out}}} - @test convert_arguments(Voronoiplot, xs, ys) isa Tuple{Vector{Point2{T_out}}} - @test convert_arguments(Voronoiplot, ps2) isa Tuple{Vector{Point2{T_out}}} - @test convert_arguments(Voronoiplot, ps3) isa Tuple{Vector{Point3{T_out}}} + @test apply_conversion(Voronoiplot, m) isa Tuple{Vector{Point3{Float64}}} + @test apply_conversion(Voronoiplot, xs, ys, zs) isa Tuple{Vector{Point3{T_out}}} + @test apply_conversion(Voronoiplot, xs, ys) isa Tuple{Vector{Point2{T_out}}} + @test apply_conversion(Voronoiplot, ps2) isa Tuple{Vector{Point2{T_out}}} + @test apply_conversion(Voronoiplot, ps3) isa Tuple{Vector{Point3{T_out}}} # TODO: VoronoiTessellation end # pure 3D plots don't implement Float64 -> Float32 rescaling yet @testset "Voxels" begin - @test convert_arguments(Voxels, vol) isa Tuple{ClosedInterval{Float32}, ClosedInterval{Float32}, ClosedInterval{Float32}, Array{UInt8, 3}} - @test convert_arguments(Voxels, xs, ys, zs, vol) isa Tuple{ClosedInterval{Float32}, ClosedInterval{Float32}, ClosedInterval{Float32}, Array{UInt8, 3}} - @test convert_arguments(Voxels, i, i, i, vol) isa Tuple{ClosedInterval{Float32}, ClosedInterval{Float32}, ClosedInterval{Float32}, Array{UInt8, 3}} + @test apply_conversion(Voxels, vol) isa Tuple{ClosedInterval{Float32}, ClosedInterval{Float32}, ClosedInterval{Float32}, Array{UInt8, 3}} + @test apply_conversion(Voxels, xs, ys, zs, vol) isa Tuple{ClosedInterval{Float32}, ClosedInterval{Float32}, ClosedInterval{Float32}, Array{UInt8, 3}} + @test apply_conversion(Voxels, i, i, i, vol) isa Tuple{ClosedInterval{Float32}, ClosedInterval{Float32}, ClosedInterval{Float32}, Array{UInt8, 3}} end @testset "Wireframe" begin - @test convert_arguments(Wireframe, xs, ys, zs) isa Tuple{Vector{T_in}, Vector{T_in}, Vector{T_in}} + @test apply_conversion(Wireframe, xs, ys, zs) isa Tuple{Vector{T_in}, Vector{T_in}, Vector{T_in}} end end - end # These have nothing to do with Numeric types... @testset "Text" begin - @test convert_arguments(Makie.Text, str) isa Tuple{String} - @test convert_arguments(Makie.Text, strings) isa Tuple{Vector{String}} + @test apply_conversion(Makie.Text, str) isa Tuple{String} + @test apply_conversion(Makie.Text, strings) isa Tuple{Vector{String}} # TODO glyphcollection end end diff --git a/test/dim-converts.jl b/test/dim-converts.jl new file mode 100644 index 00000000000..6f68ffbb039 --- /dev/null +++ b/test/dim-converts.jl @@ -0,0 +1,93 @@ +using Makie.Unitful +using Makie.Dates + +@testset "1 arg expansion" begin + f, ax, pl = scatter(u"m" .* (1:10)) + @test pl isa Scatter{Tuple{Vector{Point2{Float64}}}} + f, ax, pl = scatter(Categorical(["a", "b", "c"])) + @test pl isa Scatter{Tuple{Vector{Point2{Float64}}}} + f, ax, pl = scatter(now() .+ range(Second(0); step=Second(5), length=10)) + @test pl isa Scatter{Tuple{Vector{Point2{Float64}}}} +end + +@recipe(UnitfulPlot, x) do scene + return Attributes() +end + +function Makie.plot!(plot::UnitfulPlot) + return scatter!(plot, plot.x, map(x -> x .* u"s", plot.x)) +end + +@testset "dates in recipe" begin + f, ax, pl = unitfulplot(1:5) + pl_conversion = Makie.get_conversions(pl) + ax_conversion = Makie.get_conversions(ax) + @test pl_conversion[2] isa Makie.UnitfulConversion + @test ax_conversion[2] isa Makie.UnitfulConversion + @test pl.plots[1][1][] == Point{2,Float32}.(1:5, 1:5) +end + + +struct DateStruct end + +function Makie.convert_arguments(::PointBased, ::DateStruct) + return (1:5, DateTime.(1:5)) +end + +@testset "dates in convert_arguments" begin + f, ax, pl = scatter(DateStruct()) + pl_conversion = Makie.get_conversions(pl) + ax_conversion = Makie.get_conversions(ax) + @test pl_conversion[2] isa Makie.DateTimeConversion + @test pl_conversion[2] isa Makie.DateTimeConversion + + @test pl[1][] == Point.(1:5, Float64.(Makie.date_to_number.(DateTime.(1:5)))) +end + +@testset "Categorical ylims!" begin + f, ax, p = scatter(1:4, Categorical(["a", "b", "c", "a"])) + scatter!(ax, 1:4, Categorical(["b", "d", "a", "c"])) + ylims!(ax, "0", "x") + Makie.update_state_before_display!(ax) + lims = Makie.convert_dim_value.(Ref(ax), 2, ["0", "x"]) + (xmin, ymin), (xmax, ymax) = extrema(ax.finallimits[]) + @test [ymin, ymax] == lims +end + +@testset "Conversion with implicit axis" begin + conversion = Makie.CategoricalConversion(; sortby=identity) + f, ax, pl = barplot([:a, :b, :c], 1:3; axis=(dim1_conversion=conversion,)) + @test ax.dim1_conversion[] == Makie.get_conversions(pl)[1] + @test conversion == Makie.get_conversions(pl)[1] + @test ax.scene.conversions[1] == Makie.get_conversions(pl)[1] + @test pl[1][] == Point.(1:3, 1:3) +end + +@testset "unit switching" begin + f, ax, pl = scatter(rand(Hour(1):Hour(1):Hour(20), 10)) + # Unitful works as well + scatter!(ax, LinRange(0u"yr", 0.1u"yr", 5)) + @test_throws Unitful.DimensionError scatter!(ax, 1:4) + @test_throws ArgumentError scatter!(ax, Hour(1):Hour(1):Hour(4), 1:4) +end + +function test_cleanup(arg) + obs = Observable(arg) + f, ax, pl = scatter(obs) + @test length(obs.listeners) == 1 + delete!(ax, pl) + @test length(obs.listeners) == 0 +end + +@testset "clean up observables" begin + @testset "UnitfulConversion" begin + test_cleanup([0.01u"km", 0.02u"km", 0.03u"km", 0.04u"km"]) + end + @testset "CategoricalConversion" begin + test_cleanup(Categorical(["a", "b", "c"])) + end + @testset "DateTimeConversion" begin + dates = now() .+ range(Second(0); step=Second(5), length=10) + test_cleanup(dates) + end +end diff --git a/test/figures.jl b/test/figures.jl index d6968de4aba..e1637544e0d 100644 --- a/test/figures.jl +++ b/test/figures.jl @@ -136,19 +136,19 @@ end @testset "Nested axis assignment" begin fig = Figure() - @test Axis(fig[1, 1]) isa Axis - @test Axis(fig[1, 1][2, 3]) isa Axis - @test Axis(fig[1, 1][2, 3][4, 5]) isa Axis - @test_throws ErrorException scatter(fig[1, 1]) - @test_throws ErrorException scatter(fig[1, 1][2, 3]) - @test_throws ErrorException scatter(fig[1, 1][2, 3][4, 5]) - @test scatter(fig[1, 2], 1:10) isa Makie.AxisPlot - @test scatter(fig[1, 1][1, 1], 1:10) isa Makie.AxisPlot - @test scatter(fig[1, 1][1, 1][1, 1], 1:10) isa Makie.AxisPlot + Axis(fig[1, 1]) isa Axis + Axis(fig[1, 1][2, 3]) isa Axis + Axis(fig[1, 1][2, 3][4, 5]) isa Axis + @test_throws ArgumentError scatter(fig[1, 1]) + @test_throws ArgumentError scatter(fig[1, 1][2, 3]) + @test_throws ArgumentError scatter(fig[1, 1][2, 3][4, 5]) + scatter(fig[1, 2], 1:10) isa Makie.AxisPlot + scatter(fig[1, 1][1, 1], 1:10) isa Makie.AxisPlot + scatter(fig[1, 1][1, 1][1, 1], 1:10) isa Makie.AxisPlot fig = Figure() fig[1, 1] = GridLayout() - @test Axis(fig[1, 1][1, 1]) isa Axis + Axis(fig[1, 1][1, 1]) isa Axis fig[1, 1] = GridLayout() @test_throws ErrorException Axis(fig[1, 1][1, 1]) end diff --git a/test/pipeline.jl b/test/pipeline.jl index fcc4c0c3ec0..d8eee4494bf 100644 --- a/test/pipeline.jl +++ b/test/pipeline.jl @@ -160,4 +160,4 @@ end @test_throws InvalidAttributeError meshscatter(1:10; does_not_exist = 123) @test_throws InvalidAttributeError poly(Point2f[]; does_not_exist = 123) @test_throws InvalidAttributeError mesh(rand(Point3f, 3); does_not_exist = 123) -end \ No newline at end of file +end diff --git a/test/primitives.jl b/test/primitives.jl index 2a38916cc1e..92fe8a054b3 100644 --- a/test/primitives.jl +++ b/test/primitives.jl @@ -8,7 +8,6 @@ @test points[] == [Point2f(5), Point2f(15)] end - @testset "arrows" begin # Test for: # https://github.com/MakieOrg/Makie.jl/issues/3273 diff --git a/test/runtests.jl b/test/runtests.jl index f613a81b78b..b73563e695f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -13,7 +13,7 @@ using Makie: volume @testset "Unit tests" begin @testset "#659 Volume errors if data is not a cube" begin - fig, ax, vplot = volume(1:8, 1:8, 1:10, rand(8, 8, 10)) + fig, ax, vplot = volume(1..8, 1..8, 1..10, rand(8, 8, 10)) lims = Makie.data_limits(vplot) lo, hi = extrema(lims) @test all(lo .<= 1) @@ -47,4 +47,5 @@ using Makie: volume include("conversions.jl") include("float32convert.jl") + include("dim-converts.jl") end diff --git a/test/specapi.jl b/test/specapi.jl index a31b45373c0..c05cf218b6f 100644 --- a/test/specapi.jl +++ b/test/specapi.jl @@ -88,3 +88,18 @@ end pl.color = [0, 1, 2, 3, 4] @test pl.color[] == [0, 1, 2, 3, 4] end + + +@testset "Specapi and Dim conversions" begin + f, ax, pl = plot(S.GridLayout([S.Axis(; plots=[S.Scatter(1:4, Categorical(["a", "b", "c", "d"]); markersize=20)])])) + # make sure ticks change correctly + p = scatter!(1:2, Categorical(["x", "y"]); markersize=20) + ax = current_axis() + conversion = Makie.get_conversions(ax) + pconversion = Makie.get_conversions(p) + + @test conversion == pconversion + @test conversion[2] isa Makie.CategoricalConversion + @test ax.dim2_conversion[] isa Makie.CategoricalConversion + f +end \ No newline at end of file