diff --git a/CHANGELOG.md b/CHANGELOG.md index 961bda451d0..ee8f95e83cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## Breaking + +- Expanded scope of dim converts [#5323](https://github.com/MakieOrg/Makie.jl/pull/5323) + - **breaking** most plot recipes now set the target types for their conversions. This means `plot!(::PlotType{<:Tuple{<:MyArgType}})` requires introducing a conversion trait and extending `Makie.types_for_plot_arguments()`. See docs. + - **breaking** `UnitfulConversion` no longer rescales units and dropped the `units_in_label` option/field. + - **breaking** The dim converts interface has changed. See dim converts docs. + - Added `argument_dims()` and `argument_dim_kwargs()` to handle dim converts for various argument configurations, including point-like arguments, dimensionless arguments (i.e. not dim-convertible) and handling of attributes like `direction` and `orientation`. + - Updated almost every Makie recipe to work with dim converts. + - Added support for x/y/zlabel suffixes based on dim converts via Axis/Axis3 attributes. + - Adjusted conversion logic to avoid applying dim converts when `space != :data`, and allow early `convert_arguments()` application when dim converts are forced. (I.e. when the parent scene/Axis/etc. has set dim converts.) + - Added `force_dimconverts` as a generic plot keyword argument. This can be set to `false` to allow a numeric plot to plot in a scene with fixed dim converts. (E.g. for axis decorations.) + ## Unreleased - Fixes for is_same to work with missing [#5327](https://github.com/MakieOrg/Makie.jl/pull/5327). diff --git a/Makie/ext/MakieDynamicQuantitiesExt.jl b/Makie/ext/MakieDynamicQuantitiesExt.jl index 148451d6432..b6dbb268b12 100644 --- a/Makie/ext/MakieDynamicQuantitiesExt.jl +++ b/Makie/ext/MakieDynamicQuantitiesExt.jl @@ -5,7 +5,6 @@ import DynamicQuantities as DQ M.expand_dimensions(::M.PointBased, y::AbstractVector{<:DQ.UnionAbstractQuantity}) = (keys(y), y) M.create_dim_conversion(::Type{<:DQ.UnionAbstractQuantity}) = M.DQConversion() -M.should_dim_convert(::Type{<:DQ.UnionAbstractQuantity}) = true unit_string(quantity::DQ.UnionAbstractQuantity) = string(DQ.dimension(quantity)) @@ -24,18 +23,22 @@ function unit_convert(quantity::DQ.UnionAbstractQuantity, value) end needs_tick_update_observable(conversion::M.DQConversion) = conversion.quantity +show_dim_convert_in_ticklabel(::M.DQConversion) = false +show_dim_convert_in_axis_label(::M.DQConversion) = true -function M.get_ticks(conversion::M.DQConversion, ticks, scale, formatter, vmin, vmax) +function M.get_ticks(conversion::M.DQConversion, ticks, scale, formatter, vmin, vmax, show_in_label) quantity = conversion.quantity[] quantity isa M.Automatic && return [], [] unit_str = unit_string(quantity) tick_vals, labels = M.get_ticks(ticks, scale, formatter, vmin, vmax) - if conversion.units_in_label[] + if show_in_label labels = labels .* unit_str end return tick_vals, labels end +M.get_label_suffix(conversion::M.DQConversion) = unit_string(conversion.quantity[]) + function M.convert_dim_value(conversion::M.DQConversion, attr, values, last_values) if conversion.quantity[] isa M.Automatic conversion.quantity[] = oneunit(first(values)) diff --git a/Makie/src/Makie.jl b/Makie/src/Makie.jl index 9ae5ffced03..7b06fc59c11 100644 --- a/Makie/src/Makie.jl +++ b/Makie/src/Makie.jl @@ -131,6 +131,7 @@ include("dim-converts/unitful-integration.jl") include("dim-converts/dynamic-quantities-integration.jl") include("dim-converts/categorical-integration.jl") include("dim-converts/dates-integration.jl") +include("dim-converts/argument_dims.jl") include("scenes.jl") include("float32-scaling.jl") diff --git a/Makie/src/basic_plots.jl b/Makie/src/basic_plots.jl index 47471b80b3b..f0e6242e904 100644 --- a/Makie/src/basic_plots.jl +++ b/Makie/src/basic_plots.jl @@ -316,13 +316,7 @@ All volume plots are derived from casting rays for each drawn pixel. These rays intersect with the volume data to derive some color, usually based on the given colormap. How exactly the color is derived depends on the algorithm used. """ -@recipe Volume ( - x::EndPoints, - y::EndPoints, - z::EndPoints, - # TODO: consider using RGB{N0f8}, RGBA{N0f8} instead of Vec/RGB(A){Float32} - volume::AbstractArray{<:Union{Float32, Vec3f, RGB{Float32}, Vec4f, RGBA{Float32}}, 3}, -) begin +@recipe Volume (x, y, z, volume) begin """ Sets the volume algorithm that is used. Available algorithms are: * `:iso`: Shows an isovalue surface within the given float data. For this only samples within `isovalue - isorange .. isovalue + isorange` are included in the final color of a pixel. @@ -361,7 +355,7 @@ const VecOrMat{T} = Union{AbstractVector{T}, AbstractMatrix{T}} 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::VecOrMat{<:FloatType}, y::VecOrMat{<:FloatType}, z::VecOrMat{<:FloatType}) begin +@recipe Surface (x, y, z) 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`. Can also be a `Makie.AbstractPattern`." color = nothing """ @@ -713,7 +707,12 @@ representation and may behave a bit differently than usual. Note that `voxels` is currently considered experimental and may still see breaking changes in patch releases. """ -@recipe Voxels (x, y, z, chunk) begin +@recipe Voxels ( + x::EndPoints{Float32}, + y::EndPoints{Float32}, + z::EndPoints{Float32}, + chunk::Array{<:Real, 3}, +) begin "A function that controls which values in the input data are mapped to invisible (air) voxels." is_air = x -> isnothing(x) || ismissing(x) || isnan(x) """ diff --git a/Makie/src/basic_recipes/ablines.jl b/Makie/src/basic_recipes/ablines.jl index f2fc398ca0c..6b3c2413515 100644 --- a/Makie/src/basic_recipes/ablines.jl +++ b/Makie/src/basic_recipes/ablines.jl @@ -8,6 +8,8 @@ You can pass one or multiple intercepts or slopes. documented_attributes(LineSegments)... end +argument_dims(::Type{<:ABLines}, args...) = nothing + function Makie.plot!(p::ABLines) scene = Makie.parent_scene(p) transf = transform_func(scene) diff --git a/Makie/src/basic_recipes/annotation.jl b/Makie/src/basic_recipes/annotation.jl index 4e5161b248f..6c849aab204 100644 --- a/Makie/src/basic_recipes/annotation.jl +++ b/Makie/src/basic_recipes/annotation.jl @@ -65,7 +65,7 @@ If no label positions are given, they will be determined automatically such that overlaps between labels and data points are reduced. In this mode, the labels should be very close to their associated data points so connection plots are typically not visible. """ -@recipe Annotation begin +@recipe Annotation (merged_positions::VecTypesVector{4, <:Real},) begin """ The color of the text labels. If `automatic`, `textcolor` matches `color`. """ @@ -167,40 +167,51 @@ function closest_point_on_rectangle(r::Rect2, p) return argmin(c -> norm(c - p), candidates) end -function Makie.convert_arguments(::Type{<:Annotation}, x::Real, y::Real) +argument_dims(::Type{<:Annotation}, x, y) = (1, 2) +argument_dims(::Type{<:Annotation}, xy::VecTypes{2}) = ((1, 2),) +argument_dims(::Type{<:Annotation}, xy::VecTypesVector{2}) = ((1, 2),) +argument_dims(::Type{<:Annotation}, xy::VecTypes{4}) = ((1, 2, 1, 2),) +argument_dims(::Type{<:Annotation}, xy::VecTypesVector{4}) = ((1, 2, 1, 2),) +argument_dims(::Type{<:Annotation}, x, y, x2, y2) = (1, 2, 1, 2) + +function convert_arguments(::Type{<:Annotation}, x::Real, y::Real) return ([Vec4d(NaN, NaN, x, y)],) end -function Makie.convert_arguments(::Type{<:Annotation}, p::VecTypes{2}) +function convert_arguments(::Type{<:Annotation}, p::VecTypes{2, <:Real}) return ([Vec4d(NaN, NaN, p...)],) end -function Makie.convert_arguments(::Type{<:Annotation}, x::Real, y::Real, x2::Real, y2::Real) +function convert_arguments(::Type{<:Annotation}, x::Real, y::Real, x2::Real, y2::Real) return ([Vec4d(x, y, x2, y2)],) end -function Makie.convert_arguments(::Type{<:Annotation}, p1::VecTypes{2}, p2::VecTypes{2}) +function convert_arguments(::Type{<:Annotation}, p1::VecTypes{2, <:Real}, p2::VecTypes{2, <:Real}) return ([Vec4d(p1..., p2...)],) end -function Makie.convert_arguments(::Type{<:Annotation}, v::AbstractVector{<:VecTypes{2}}) +function convert_arguments(::Type{<:Annotation}, p1::VecTypes{2}, p2::VecTypes{2}) + return ([Vec4(p1..., p2...)],) +end + +function convert_arguments(::Type{<:Annotation}, v::AbstractVector{<:VecTypes{2, <:Real}}) return (Vec4d.(NaN, NaN, getindex.(v, 1), getindex.(v, 2)),) end -function Makie.convert_arguments(::Type{<:Annotation}, v1::AbstractVector{<:VecTypes{2}}, v2::AbstractVector{<:VecTypes{2}}) - return (Vec4d.(getindex.(v1, 1), getindex.(v1, 2), getindex.(v2, 1), getindex.(v2, 2)),) +function convert_arguments(::Type{<:Annotation}, v1::AbstractVector{<:VecTypes{2}}, v2::AbstractVector{<:VecTypes{2}}) + return (Vec4.(getindex.(v1, 1), getindex.(v1, 2), getindex.(v2, 1), getindex.(v2, 2)),) end -function Makie.convert_arguments(::Type{<:Annotation}, v1::AbstractVector{<:Real}, v2::AbstractVector{<:Real}) +function convert_arguments(::Type{<:Annotation}, v1::AbstractVector{<:Real}, v2::AbstractVector{<:Real}) return (Vec4d.(NaN, NaN, v1, v2),) end -function Makie.convert_arguments(::Type{<:Annotation}, v1::AbstractVector{<:Real}, v2::AbstractVector{<:Real}, v3::AbstractVector{<:Real}, v4::AbstractVector{<:Real}) +function convert_arguments(::Type{<:Annotation}, v1::AbstractVector{<:Real}, v2::AbstractVector{<:Real}, v3::AbstractVector{<:Real}, v4::AbstractVector{<:Real}) return (Vec4d.(v1, v2, v3, v4),) end -function Makie.plot!(p::Annotation{<:Tuple{<:AbstractVector{<:Vec4}}}) - scene = Makie.get_scene(p) +function plot!(p::Annotation{<:Tuple{<:AbstractVector{<:VecTypes{4}}}}) + scene = get_scene(p) textpositions = lift(p[1]) do vecs Point2d.(getindex.(vecs, 3), getindex.(vecs, 4)) @@ -538,8 +549,8 @@ function startpoint(::Ann.Paths.Corner, text_bb, p2) return Point2d(x, y) end -Makie.data_limits(p::Annotation) = Rect3f(Rect2f([Vec2f(x[3], x[4]) for x in p[1][]])) -Makie.boundingbox(p::Annotation, space::Symbol = :data) = Makie.apply_transform_and_model(p, Makie.data_limits(p)) +data_limits(p::Annotation) = Rect3f(Rect2f([Vec2f(x[3], x[4]) for x in p[1][]])) +boundingbox(p::Annotation, space::Symbol = :data) = apply_transform_and_model(p, data_limits(p)) function connection_path(::Ann.Paths.Line, p1, p2) return BezierPath( @@ -966,7 +977,7 @@ function line_rectangle_intersection(p1::Point2, p2::Point2, rect::Rect2) end end -annotation_style_plotspecs(::Makie.Automatic, path, p1, p2; kwargs...) = annotation_style_plotspecs(Ann.Styles.Line(), path, p1, p2; kwargs...) +annotation_style_plotspecs(::Automatic, path, p1, p2; kwargs...) = annotation_style_plotspecs(Ann.Styles.Line(), path, p1, p2; kwargs...) function annotation_style_plotspecs(l::Ann.Styles.LineArrow, path::BezierPath, p1, p2; color, linewidth) length(path.commands) < 2 && return PlotSpec[] @@ -1006,7 +1017,7 @@ function annotation_style_plotspecs(::Ann.Styles.Line, path::BezierPath, p1, p2; ] end -_auto(x::Makie.Automatic, default) = default +_auto(x::Automatic, default) = default _auto(x, default) = x shrinksize(other) = 0.0 @@ -1024,7 +1035,7 @@ function plotspecs(l::Ann.Arrows.Line, pos; rotation, color, linewidth) p1 = pos + dir1 * sidelen p2 = pos + dir2 * sidelen return [ - Makie.PlotSpec(:Lines, [p1, pos, p2]; space = :pixel, color, linewidth), + PlotSpec(:Lines, [p1, pos, p2]; space = :pixel, color, linewidth), ] end @@ -1038,7 +1049,7 @@ function plotspecs(h::Ann.Arrows.Head, pos; rotation, color, linewidth) marker = BezierPath([MoveTo(0, 0), LineTo(p1), LineTo(p2), LineTo(p3), ClosePath()]) return [ - Makie.PlotSpec(:Scatter, pos; space = :pixel, rotation, color, marker, markersize = len), + PlotSpec(:Scatter, pos; space = :pixel, rotation, color, marker, markersize = len), ] end diff --git a/Makie/src/basic_recipes/arc.jl b/Makie/src/basic_recipes/arc.jl index 4a2b3555b55..a5d423cfc4f 100644 --- a/Makie/src/basic_recipes/arc.jl +++ b/Makie/src/basic_recipes/arc.jl @@ -18,6 +18,8 @@ Examples: resolution = 361 end +argument_dims(::Type{<:Arc}, args...) = nothing + function plot!(p::Arc) map!(p, [:origin, :radius, :start_angle, :stop_angle, :resolution], :positions) do origin, radius, start_angle, stop_angle, resolution return map(range(start_angle, stop = stop_angle, length = resolution)) do angle diff --git a/Makie/src/basic_recipes/arrows.jl b/Makie/src/basic_recipes/arrows.jl index 7a8a1845e6b..cb6e9b4aabe 100644 --- a/Makie/src/basic_recipes/arrows.jl +++ b/Makie/src/basic_recipes/arrows.jl @@ -4,32 +4,41 @@ struct ArrowLike <: ConversionTrait end +function types_for_plot_arguments(::ArrowLike) + return Tuple{VecTypesVector{N, <:Real}, VecTypesVector{N, <:Real}} where {N} +end + +argument_dims(::ArrowLike, x, y, f) = (1, 2) +argument_dims(::ArrowLike, x, y, z, f::Function) = (1, 2, 3) +argument_dims(::ArrowLike, x, y, u, v) = (1, 2, 1, 2) +argument_dims(::ArrowLike, x, y, z, u, v, w) = (1, 2, 3, 1, 2, 3) + # vec(::Point) and vec(::Vec) works (returns input), but vec(::Tuple) errors convert_arguments(::ArrowLike, pos::VecTypes{N}, dir::VecTypes{N}) where {N} = ([pos], [dir]) function convert_arguments(::ArrowLike, pos::AbstractArray, dir::AbstractArray) return ( - convert_arguments(PointBased(), vec(pos))[1], - convert_arguments(PointBased(), vec(dir))[1], + convert_arguments(PointBased(), vec(pos))..., + convert_arguments(PointBased(), vec(dir))..., ) end function convert_arguments(::ArrowLike, x, y, u, v) return ( - convert_arguments(PointBased(), vec(x), vec(y))[1], - convert_arguments(PointBased(), vec(u), vec(v))[1], + convert_arguments(PointBased(), vec(x), vec(y))..., + convert_arguments(PointBased(), vec(u), vec(v))..., ) end function convert_arguments(::ArrowLike, x::AbstractVector, y::AbstractVector, u::AbstractMatrix, v::AbstractMatrix) return ( vec(Point{2, float_type(x, y)}.(x, y')), - convert_arguments(PointBased(), vec(u), vec(v))[1], + convert_arguments(PointBased(), vec(u), vec(v))..., ) end function convert_arguments(::ArrowLike, x, y, z, u, v, w) return ( - convert_arguments(PointBased(), vec(x), vec(y), vec(z))[1], - convert_arguments(PointBased(), vec(u), vec(v), vec(w))[1], + convert_arguments(PointBased(), vec(x), vec(y), vec(z))..., + convert_arguments(PointBased(), vec(u), vec(v), vec(w))..., ) end @@ -314,6 +323,7 @@ end conversion_trait(::Type{<:Arrows2D}) = ArrowLike() + function _get_arrow_shape(f::Function, length, width, metrics) nt = NamedTuple{(:taillength, :tailwidth, :shaftlength, :shaftwidth, :tiplength, :tipwidth)}(metrics) return poly_convert(f(length, width, nt)) diff --git a/Makie/src/basic_recipes/axis.jl b/Makie/src/basic_recipes/axis.jl index 94dbfe85fcb..49cb843cb0a 100644 --- a/Makie/src/basic_recipes/axis.jl +++ b/Makie/src/basic_recipes/axis.jl @@ -100,6 +100,9 @@ $(ATTRIBUTES) ) end +argument_dim_kwargs(::Type{<:Axis3D}) = tuple() +argument_dims(::Type{<:Axis3D}, args...) = nothing + isaxis(x) = false isaxis(x::Axis3D) = true diff --git a/Makie/src/basic_recipes/band.jl b/Makie/src/basic_recipes/band.jl index fd577993c37..a6016fad489 100644 --- a/Makie/src/basic_recipes/band.jl +++ b/Makie/src/basic_recipes/band.jl @@ -7,7 +7,10 @@ Plots a band from `ylower` to `yupper` along `x`. The form `band(lower, upper)` between the points in `lower` and `upper`. Both bounds can be passed together as `lowerupper`, a vector of intervals. """ -@recipe Band (lowerpoints, upperpoints) begin +@recipe Band ( + lowerpoints::VecTypesVector{N, <:Real} where {N}, + upperpoints::VecTypesVector{N, <:Real} where {N}, +) begin documented_attributes(Mesh)... "The direction of the band. If set to `:y`, x and y coordinates will be flipped, resulting in a vertical band. This setting applies only to 2D bands." direction = :x @@ -20,12 +23,21 @@ Both bounds can be passed together as `lowerupper`, a vector of intervals. shading = NoShading end -function convert_arguments(::Type{<:Band}, x, ylower, yupper) +argument_dim_kwargs(::Type{<:Band}) = (:direction,) +function argument_dims(::Type{<:Band}, x, ylower, yupper; direction) + return direction === :x ? (1, 2, 2) : (2, 1, 1) +end +function argument_dims(::Type{<:Band}, lower::VecTypesVector{N}, upper::VecTypesVector{N}; direction) where {N} + return direction === :x ? ((1, 2), (1, 2)) : ((2, 1), (2, 1)) +end + +function convert_arguments(::Type{<:Band}, x::RealVector, ylower::RealVector, yupper::RealVector) return (Point2{float_type(x, ylower)}.(x, ylower), Point2{float_type(x, yupper)}.(x, yupper)) end -convert_arguments(P::Type{<:Band}, x::AbstractVector{<:Number}, y::AbstractVector{<:Interval}) = - convert_arguments(P, x, leftendpoint.(y), rightendpoint.(y)) +function convert_arguments(P::Type{<:Band}, x::AbstractVector, y::AbstractVector{<:Interval}) + return convert_arguments(P, x, leftendpoint.(y), rightendpoint.(y)) +end function band_connect(n) ns = 1:(n - 1) diff --git a/Makie/src/basic_recipes/barplot.jl b/Makie/src/basic_recipes/barplot.jl index c821e6b9abe..61471dd22d7 100644 --- a/Makie/src/basic_recipes/barplot.jl +++ b/Makie/src/basic_recipes/barplot.jl @@ -129,6 +129,7 @@ Plots bars of the given `heights` at the given (scalar) `positions`. end conversion_trait(::Type{<:BarPlot}) = PointBased() +argument_dim_kwargs(::Type{<:BarPlot}) = (:direction,) function bar_rectangle(x, y, width, fillto, in_y_direction) # y could be smaller than fillto... diff --git a/Makie/src/basic_recipes/bracket.jl b/Makie/src/basic_recipes/bracket.jl index 9d298dfab18..36fe48db8fc 100644 --- a/Makie/src/basic_recipes/bracket.jl +++ b/Makie/src/basic_recipes/bracket.jl @@ -8,7 +8,7 @@ Draws a bracket between each pair of points (x1, y1) and (x2, y2) with a text la By default each label is rotated parallel to the line between the bracket points. """ -@recipe Bracket (positions,) begin +@recipe Bracket (positions::Vector{<:Tuple{Point2{<:Real}, Point2{<:Real}}},) begin """ The offset of the bracket perpendicular to the line from start to end point in screen units. The direction depends on the `orientation` attribute. @@ -78,10 +78,16 @@ By default each label is rotated parallel to the line between the bracket points space = :data end -function convert_arguments(::Type{<:Bracket}, point1::VecTypes{2, T1}, point2::VecTypes{2, T2}) where {T1, T2} +argument_dims(::Type{<:Bracket}, x1, y1, x2, y2) = (1, 2, 1, 2) + +function convert_arguments(::Type{<:Bracket}, point1::VecTypes{2, T1}, point2::VecTypes{2, T2}) where {T1 <: Real, T2 <: Real} return ([(Point2{float_type(T1)}(point1), Point2{float_type(T2)}(point2))],) end +function convert_arguments(::Type{<:Bracket}, point1::VecTypesVector{2, T1}, point2::VecTypesVector{2, T2}) where {T1 <: Real, T2 <: Real} + return (tuple.(Point2{float_type(T1)}.(point1), Point2{float_type(T2)}.(point2)),) +end + function convert_arguments(::Type{<:Bracket}, x1::Real, y1::Real, x2::Real, y2::Real) return ([(Point2{float_type(x1, y1)}(x1, y1), Point2{float_type(x2, y2)}(x2, y2))],) end diff --git a/Makie/src/basic_recipes/contourf.jl b/Makie/src/basic_recipes/contourf.jl index 0c16a060b32..925915d0068 100644 --- a/Makie/src/basic_recipes/contourf.jl +++ b/Makie/src/basic_recipes/contourf.jl @@ -44,12 +44,6 @@ similar to how [`surface`](@ref) works. mixin_colormap_attributes(allow = (:colormap, :colorscale, :nan_color))... end -# these attributes are computed dynamically and needed for colorbar e.g. -# _computed_levels -# _computed_colormap -# _computed_extendlow -# _computed_extendhigh - _get_isoband_levels(levels::Int, mi, ma) = collect(range(Float32(mi), nextfloat(Float32(ma)), length = levels + 1)) function _get_isoband_levels(levels::AbstractVector{<:Real}, mi, ma) diff --git a/Makie/src/basic_recipes/error_and_rangebars.jl b/Makie/src/basic_recipes/error_and_rangebars.jl index 58f081ba252..3bb7291fd89 100644 --- a/Makie/src/basic_recipes/error_and_rangebars.jl +++ b/Makie/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 (val_low_high::AbstractVector{<:Union{Vec3, Vec4}},) begin +@recipe Errorbars (val_low_high::AbstractVector{<:Vec{4, <:Real}},) begin documented_attributes(LineSegments)... "The width of the whiskers or line caps in screen units." @@ -38,7 +38,7 @@ The `low_high` argument can be a vector of tuples or intervals. If you want to plot errors relative to a reference value, use `errorbars`. """ -@recipe Rangebars (val_low_high::AbstractVector{<:Union{Vec3, Vec4}},) begin +@recipe Rangebars (val_low_high::AbstractVector{<:Vec{3, <:Real}},) begin documented_attributes(LineSegments)... "The width of the whiskers or line caps in screen units." @@ -51,6 +51,40 @@ end ### conversions for errorbars +argument_dim_kwargs(::Type{<:Union{Errorbars, Rangebars}}) = (:direction,) + +function argument_dims(::Type{<:Errorbars}, x, y, e; direction) + return ifelse(direction === :y, (1, 2, 2), (1, 2, 1)) +end + +function argument_dims(::Type{<:Errorbars}, x, y, l, h; direction) + return ifelse(direction === :y, (1, 2, 2, 2), (1, 2, 1, 1)) +end + +function argument_dims(::Type{<:Errorbars}, x, y, lh::VecTypesVector{2}; direction) + return ifelse(direction === :y, (1, 2, (2, 2)), (1, 2, (1, 1))) +end + +function argument_dims(::Type{<:Errorbars}, xy::VecTypesVector{2}, e; direction) + return ifelse(direction === :y, ((1, 2), 2), ((1, 2), 1)) +end + +function argument_dims(::Type{<:Errorbars}, xy::VecTypesVector{2}, l, h; direction) + return ifelse(direction === :y, ((1, 2), 2, 2), ((1, 2), 1, 1)) +end + +function argument_dims(::Type{<:Errorbars}, xy::VecTypesVector{2}, lh::VecTypesVector{2}; direction) + return ifelse(direction === :y, ((1, 2), (2, 2)), ((1, 2), (1, 1))) +end + +function argument_dims(::Type{<:Errorbars}, xye::VecTypesVector{3}; direction) + return ifelse(direction === :y, ((1, 2, 2),), ((1, 2, 1),)) +end + +function argument_dims(::Type{<:Errorbars}, xylh::VecTypesVector{4}; direction) + return ifelse(direction === :y, ((1, 2, 2, 2),), ((1, 2, 1, 1),)) +end + function convert_arguments(::Type{<:Errorbars}, x::RealOrVec, y::RealOrVec, error_both::RealOrVec) T = float_type(x, y, error_both) xyerr = broadcast(x, y, error_both) do x, y, e @@ -85,7 +119,7 @@ function convert_arguments( return (xyerr,) end -function convert_arguments(::Type{<:Errorbars}, xy::AbstractVector{<:VecTypes{2, T}}, error_low::RealOrVec, error_high::RealOrVec) where {T} +function convert_arguments(::Type{<:Errorbars}, xy::AbstractVector{<:VecTypes{2, T}}, error_low::RealOrVec, error_high::RealOrVec) where {T <: Real} 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) @@ -93,7 +127,7 @@ function convert_arguments(::Type{<:Errorbars}, xy::AbstractVector{<:VecTypes{2, return (xyerr,) end -function convert_arguments(::Type{<:Errorbars}, xy::AbstractVector{<:VecTypes{2, T1}}, error_low_high::AbstractVector{<:VecTypes{2, T2}}) where {T1, T2} +function convert_arguments(::Type{<:Errorbars}, xy::AbstractVector{<:VecTypes{2, T1}}, error_low_high::AbstractVector{<:VecTypes{2, T2}}) where {T1 <: Real, T2 <: Real} T_out = float_type(T1, T2) xyerr = broadcast(xy, error_low_high) do (x, y), (el, eh) Vec4{T_out}(x, y, el, eh) @@ -101,7 +135,7 @@ function convert_arguments(::Type{<:Errorbars}, xy::AbstractVector{<:VecTypes{2, return (xyerr,) end -function convert_arguments(::Type{<:Errorbars}, xy_error_both::AbstractVector{<:VecTypes{3, T}}) where {T} +function convert_arguments(::Type{<:Errorbars}, xy_error_both::AbstractVector{<:VecTypes{3, T}}) where {T <: Real} T_out = float_type(T) xyerr = broadcast(xy_error_both) do (x, y, e) Vec4{T_out}(x, y, e, e) @@ -109,8 +143,28 @@ function convert_arguments(::Type{<:Errorbars}, xy_error_both::AbstractVector{<: return (xyerr,) end +function convert_arguments(::Type{<:Errorbars}, xy_low_high::VecTypesVector{4, T}) where {T <: Real} + T_out = float_type(T) + xyerr = broadcast(xy_low_high) do (x, y, l, h) + Vec4{T_out}(x, y, l, h) + end + return (xyerr,) +end + ### conversions for rangebars +function argument_dims(::Type{<:Rangebars}, x, l, h; direction) + return ifelse(direction === :y, (1, 2, 2), (2, 1, 1)) +end + +function argument_dims(::Type{<:Rangebars}, x, lh::VecTypesVector{2}; direction) + return ifelse(direction === :y, (1, (2, 2)), (2, (1, 1))) +end + +function argument_dims(::Type{<:Rangebars}, xlh::VecTypesVector{3}; direction) + return ifelse(direction === :y, ((1, 2, 2),), ((2, 1, 1),)) +end + 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) @@ -120,7 +174,7 @@ end function convert_arguments( ::Type{<:Rangebars}, val::RealOrVec, low_high::AbstractVector{<:VecTypes{2, T}} - ) where {T} + ) where {T <: Real} T_out = float_type(float_type(val), T) T_out_ref = Ref{Type{T_out}}(T_out) # for type-stable capture in the closure below val_low_high = broadcast(val, low_high) do val, (low, high) @@ -129,16 +183,25 @@ function convert_arguments( return (val_low_high,) end -Makie.convert_arguments(P::Type{<:Rangebars}, x::AbstractVector{<:Number}, y::AbstractVector{<:Interval}) = - convert_arguments(P, x, endpoints.(y)) +function convert_arguments(P::Type{<:Rangebars}, x::AbstractVector, y::AbstractVector{<:Interval}) + return convert_arguments(P, x, endpoints.(y)) +end + +function convert_arguments(::Type{<:Rangebars}, x_low_high::VecTypesVector{3, T}) where {T <: Real} + T_out = float_type(T) + xlh = broadcast(x_low_high) do (x, l, h) + Vec3{T_out}(x, l, h) + end + return (xlh,) +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{<:Tuple{AbstractVector{<:Vec{4}}}}) +function Makie.plot!(plot::Errorbars{<:Tuple{AbstractVector{<:Vec{4, <:Real}}}}) return _plot_bars!(plot) end -function Makie.plot!(plot::Rangebars{<:Tuple{AbstractVector{<:Vec{3}}}}) +function Makie.plot!(plot::Rangebars{<:Tuple{AbstractVector{<:Vec{3, <:Real}}}}) return _plot_bars!(plot) end diff --git a/Makie/src/basic_recipes/hvlines.jl b/Makie/src/basic_recipes/hvlines.jl index 3a342961f67..759ee1bf21b 100644 --- a/Makie/src/basic_recipes/hvlines.jl +++ b/Makie/src/basic_recipes/hvlines.jl @@ -6,7 +6,7 @@ The lines will be placed at `ys` in data coordinates and `xmin` to `xmax` in scene coordinates (0 to 1). All three of these can have single or multiple values because they are broadcast to calculate the final line segments. """ -@recipe HLines begin +@recipe HLines (ys::Union{Real, RealVector},) begin "The start of the lines in relative axis units (0 to 1) along the x dimension." xmin = 0 "The end of the lines in relative axis units (0 to 1) along the x dimension." @@ -22,7 +22,7 @@ The lines will be placed at `xs` in data coordinates and `ymin` to `ymax` in scene coordinates (0 to 1). All three of these can have single or multiple values because they are broadcast to calculate the final line segments. """ -@recipe VLines begin +@recipe VLines (xs::Union{Real, RealVector},) begin "The start of the lines in relative axis units (0 to 1) along the y dimension." ymin = 0 "The start of the lines in relative axis units (0 to 1) along the y dimension." @@ -42,11 +42,15 @@ function projview_to_2d_limits(plot::AbstractPlot) end end +argument_dims(::Type{<:HLines}, y) = (2,) +argument_dims(::Type{<:VLines}, x) = (1,) + function Makie.plot!(p::Union{HLines, VLines}) mi = p isa HLines ? (:xmin) : (:ymin) ma = p isa HLines ? (:xmax) : (:ymax) + converted_name = p isa HLines ? (:ys) : (:xs) add_axis_limits!(p) - map!(p.attributes, [:axis_limits_transformed, :converted_1, mi, ma, :transform_func], :points) do lims, vals, mi, ma, transf + map!(p.attributes, [:axis_limits_transformed, converted_name, mi, ma, :transform_func], :points) do lims, vals, mi, ma, transf points = Point2d[] min_x, min_y = minimum(lims) max_x, max_y = maximum(lims) diff --git a/Makie/src/basic_recipes/hvspan.jl b/Makie/src/basic_recipes/hvspan.jl index bb421bc7fc0..156646caab9 100644 --- a/Makie/src/basic_recipes/hvspan.jl +++ b/Makie/src/basic_recipes/hvspan.jl @@ -8,7 +8,7 @@ in scene coordinates (0 to 1). All four of these can have single or multiple val they are broadcast to calculate the final spans. Both bounds can be passed together as an interval `ys_lowhigh`. """ -@recipe HSpan (low, high) begin +@recipe HSpan (low::Union{Real, RealVector}, high::Union{Real, RealVector}) begin "The start of the bands in relative axis units (0 to 1) along the x dimension." xmin = 0 "The end of the bands in relative axis units (0 to 1) along the x dimension." @@ -26,7 +26,7 @@ in scene coordinates (0 to 1). All four of these can have single or multiple val they are broadcast to calculate the final spans. Both bounds can be passed together as an interval `xs_lowhigh`. """ -@recipe VSpan (low, high) begin +@recipe VSpan (low::Union{Real, RealVector}, high::Union{Real, RealVector}) begin "The start of the bands in relative axis units (0 to 1) along the y dimension." ymin = 0 "The end of the bands in relative axis units (0 to 1) along the y dimension." @@ -34,6 +34,9 @@ Both bounds can be passed together as an interval `xs_lowhigh`. documented_attributes(Poly)... end +argument_dims(::Type{<:HSpan}, ylow, yhigh) = (2, 2) +argument_dims(::Type{<:VSpan}, xlow, yhigh) = (1, 1) + function Makie.plot!(p::Union{HSpan, VSpan}) mi = p isa HSpan ? :xmin : :ymin ma = p isa HSpan ? :xmax : :ymax diff --git a/Makie/src/basic_recipes/pie.jl b/Makie/src/basic_recipes/pie.jl index 4f85c78191a..e8fcf767d8c 100644 --- a/Makie/src/basic_recipes/pie.jl +++ b/Makie/src/basic_recipes/pie.jl @@ -24,6 +24,8 @@ Creates a pie chart from the given `values`. mixin_generic_plot_attributes()... end +argument_dims(::Type{<:Pie}, args...) = nothing + convert_arguments(PT::Type{<:Pie}, values::RealVector) = convert_arguments(PT, 0.0, 0.0, values) convert_arguments(PT::Type{<:Pie}, point::VecTypes{2}, values::RealVector) = convert_arguments(PT, point[1], point[2], values) convert_arguments(PT::Type{<:Pie}, ps::AbstractVector{<:VecTypes{2}}, values::RealVector) = convert_arguments(PT, getindex.(ps, 1), getindex.(ps, 2), values) diff --git a/Makie/src/basic_recipes/poly.jl b/Makie/src/basic_recipes/poly.jl index 3fdf5858a1e..3ab527bad9f 100644 --- a/Makie/src/basic_recipes/poly.jl +++ b/Makie/src/basic_recipes/poly.jl @@ -3,6 +3,7 @@ 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,) +argument_dims(::Type{<:Poly}, vertices::VecTypesVector{N}, indices) where {N} = (1:N,) function convert_pointlike(args...) return convert_arguments(PointBased(), args...) @@ -24,7 +25,11 @@ function convert_arguments(::Type{<:Poly}, path::AbstractMatrix{<:Number}) return convert_pointlike(path) end -function convert_arguments(::Type{<:Poly}, vertices::AbstractArray, indices::AbstractArray) +function convert_arguments(::Type{<:Poly}, vertices::RealArray, indices::AbstractArray) + return convert_arguments(Mesh, vertices, indices) +end + +function convert_arguments(::Type{<:Poly}, vertices::AbstractArray{<:VecTypes}, indices::AbstractArray) return convert_arguments(Mesh, vertices, indices) end diff --git a/Makie/src/basic_recipes/spy.jl b/Makie/src/basic_recipes/spy.jl index ed96f2392a2..fc1a8111f1a 100644 --- a/Makie/src/basic_recipes/spy.jl +++ b/Makie/src/basic_recipes/spy.jl @@ -14,7 +14,7 @@ spy(x) spy(0..1, 0..1, x) ``` """ -@recipe Spy (x::EndPoints, y::EndPoints, z::RealMatrix) begin +@recipe Spy (x::EndPoints{<:Real}, y::EndPoints{<:Real}, z::RealMatrix) begin """ Can be any of the markers supported by `scatter!`. Note, for huge sparse arrays, one should use `FastPixel`, which is a very fast, but can only render square markers. @@ -66,6 +66,8 @@ function boundingbox(p::Spy, space::Symbol = :data) return apply_transform_and_model(p, data_limits(p)) end +argument_dims(::Type{<:Spy}, x, y, mat) = (1, 2) + function convert_arguments(::Type{<:Spy}, matrix::AbstractMatrix{T}) where {T} Tr = Makie.float_type(T) return convert_arguments(Spy, Tr.((0, size(matrix, 1))), Tr.((0, size(matrix, 2))), matrix) diff --git a/Makie/src/basic_recipes/stairs.jl b/Makie/src/basic_recipes/stairs.jl index b5cb60ad999..48f91f393ad 100644 --- a/Makie/src/basic_recipes/stairs.jl +++ b/Makie/src/basic_recipes/stairs.jl @@ -5,7 +5,7 @@ Plot a stair function. The conversion trait of `stairs` is `PointBased`. """ -@recipe Stairs begin +@recipe Stairs (positions,) begin """ The `step` parameter can take the following values: - `:pre`: horizontal part of step extends to the left of each value in `xs`. @@ -19,7 +19,7 @@ end conversion_trait(::Type{<:Stairs}) = PointBased() function plot!(p::Stairs{<:Tuple{<:AbstractVector{T}}}) where {T <: Point2} - map!(p, [:converted_1, :step], :steppoints) do points, step + map!(p, [:positions, :step], :steppoints) do points, step if step === :pre s_points = Vector{T}(undef, length(points) * 2 - 1) s_points[1] = point = points[1] diff --git a/Makie/src/basic_recipes/streamplot.jl b/Makie/src/basic_recipes/streamplot.jl index 8d599af8323..7b3f407a686 100644 --- a/Makie/src/basic_recipes/streamplot.jl +++ b/Makie/src/basic_recipes/streamplot.jl @@ -82,13 +82,25 @@ See the function `Makie.streamplot_impl` for implementation details. mixin_generic_plot_attributes()... end -function convert_arguments(::Type{<:StreamPlot}, f::Function, xrange, yrange) +argument_dims(::Type{<:StreamPlot}, f, rect) = nothing +argument_dims(::Type{<:StreamPlot}, f, x, y) = (0, 1, 2) +argument_dims(::Type{<:StreamPlot}, f, x, y, z) = (0, 1, 2, 3) + +# Normalize x, y, (z) types for dim converts +function convert_arguments(::Type{<:StreamPlot}, f::Function, xrange::RangeLike, yrange::RangeLike) + return (f, extrema(xrange), extrema(yrange)) +end +function convert_arguments(::Type{<:StreamPlot}, f::Function, xrange::RangeLike, yrange::RangeLike, zrange::RangeLike) + return (f, extrema(xrange), extrema(yrange), extrema(zrange)) +end + +function convert_arguments(::Type{<:StreamPlot}, f::Function, xrange::RangeLike{<:Real}, yrange::RangeLike{<:Real}) xmin, xmax = extrema(xrange) ymin, ymax = extrema(yrange) return (f, Rect(xmin, ymin, xmax - xmin, ymax - ymin)) end -function convert_arguments(::Type{<:StreamPlot}, f::Function, xrange, yrange, zrange) +function convert_arguments(::Type{<:StreamPlot}, f::Function, xrange::RangeLike{<:Real}, yrange::RangeLike{<:Real}, zrange::RangeLike{<:Real}) xmin, xmax = extrema(xrange) ymin, ymax = extrema(yrange) zmin, zmax = extrema(zrange) diff --git a/Makie/src/basic_recipes/text.jl b/Makie/src/basic_recipes/text.jl index 079de4a821a..d58b267bb89 100644 --- a/Makie/src/basic_recipes/text.jl +++ b/Makie/src/basic_recipes/text.jl @@ -1,13 +1,3 @@ -struct RichText - type::Symbol - children::Vector{Union{RichText, String}} - attributes::Dict{Symbol, Any} - function RichText(type::Symbol, children...; kwargs...) - cs = Union{RichText, String}[children...] - return new(type, cs, Dict(kwargs)) - end -end - function check_textsize_deprecation(@nospecialize(dictlike)) return if haskey(dictlike, :textsize) throw(ArgumentError("`textsize` has been renamed to `fontsize` in Makie v0.19. Please change all occurrences of `textsize` to `fontsize` or revert back to an earlier version.")) @@ -855,7 +845,7 @@ struct GlyphInfo origin::Point2f extent::GlyphExtent size::Vec2f - rotation::Quaternion + rotation::Quaternionf color::RGBAf strokecolor::RGBAf strokewidth::Float32 diff --git a/Makie/src/basic_recipes/textlabel.jl b/Makie/src/basic_recipes/textlabel.jl index 770fc2f8dc4..ed31faf9774 100644 --- a/Makie/src/basic_recipes/textlabel.jl +++ b/Makie/src/basic_recipes/textlabel.jl @@ -5,7 +5,14 @@ Plots the given text(s) with a background(s) at the given position(s). """ -@recipe TextLabel (positions,) begin +@recipe TextLabel ( + positions::Union{ + VecTypesVector{N, <:Real} where {N}, + AbstractArray{<:Tuple{<:AbstractString, <:VecTypes{N, <:Real} where {N}}}, + AbstractArray{<:AbstractString}, + AbstractString, + }, +) begin # text-like args interface "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 = "" diff --git a/Makie/src/basic_recipes/timeseries.jl b/Makie/src/basic_recipes/timeseries.jl index 0ff40815728..e238a4f92fb 100644 --- a/Makie/src/basic_recipes/timeseries.jl +++ b/Makie/src/basic_recipes/timeseries.jl @@ -21,12 +21,14 @@ end ``` """ -@recipe TimeSeries (signal,) begin +@recipe TimeSeries (signal::Real,) begin "Number of tracked points." history = 100 documented_attributes(Lines)... end +argument_dims(::Type{<:TimeSeries}, signal) = (2,) + signal2point(signal::Number, start) = Point2f(time() - start, signal) signal2point(signal::Point2, start) = signal signal2point(signal, start) = error( @@ -35,7 +37,6 @@ signal2point(signal, start) = error( """ ) - function Makie.plot!(plot::TimeSeries) # normal plotting code, building on any previously defined recipes # or atomic plotting operations, and adding to the combined `plot`: diff --git a/Makie/src/basic_recipes/tooltip.jl b/Makie/src/basic_recipes/tooltip.jl index dafb2cece5f..dde70a223de 100644 --- a/Makie/src/basic_recipes/tooltip.jl +++ b/Makie/src/basic_recipes/tooltip.jl @@ -4,7 +4,12 @@ Creates a tooltip pointing at `position` displaying the given `string """ -@recipe Tooltip begin +@recipe Tooltip ( + position::Union{ + VecTypes{N, <:Real} where {N}, + Tuple{<:VecTypes{N, <:Real} where {N}, <:AbstractString}, + }, +) begin # General text = "" "Sets the offset between the given `position` and the tip of the triangle pointing at that position." @@ -55,14 +60,18 @@ Creates a tooltip pointing at `position` displaying the given `string end function convert_arguments(::Type{<:Tooltip}, x::Real, y::Real, str::AbstractString) - return (Point2{float_type(x, y)}(x, y), str) + return ((Point2{float_type(x, y)}(x, y), str),) end function convert_arguments(::Type{<:Tooltip}, x::Real, y::Real) return (Point2{float_type(x, y)}(x, y),) end +function convert_arguments(::Type{<:Tooltip}, xy::VecTypes, s::AbstractString) + return ((xy, s),) +end -function plot!(plot::Tooltip{<:Tuple{<:VecTypes, <:AbstractString}}) - tooltip!(plot, Attributes(plot), plot[1]; text = plot[2]) +function plot!(plot::Tooltip{<:Tuple{<:Tuple{<:VecTypes, <:AbstractString}}}) + map!(identity, plot, :position, [:extracted_position, :extracted_text]) + tooltip!(plot, Attributes(plot), plot.extracted_position; text = plot.extracted_text) return plot end @@ -164,7 +173,7 @@ function plot!(p::Tooltip{<:Tuple{<:VecTypes}}) end p = textlabel!( - p, p[1], p.text, shape = p.shape, + p, p[1], text = p.text, shape = p.shape, padding = p.text_padding, justification = p.justification, text_align = p.text_align, offset = p.text_offset, fontsize = p.fontsize, font = p.font, diff --git a/Makie/src/basic_recipes/tricontourf.jl b/Makie/src/basic_recipes/tricontourf.jl index 8f44384b385..1c85377a9ba 100644 --- a/Makie/src/basic_recipes/tricontourf.jl +++ b/Makie/src/basic_recipes/tricontourf.jl @@ -8,7 +8,7 @@ Plots a filled tricontour of the height information in `zs` at the horizontal po vertical positions `ys`. A `Triangulation` from DelaunayTriangulation.jl can also be provided instead of `xs` and `ys` for specifying the triangles, otherwise an unconstrained triangulation of `xs` and `ys` is computed. """ -@recipe Tricontourf begin +@recipe Tricontourf (tri::DelTri.Triangulation, zs::RealVector) begin """ Can be either an `Int` which results in n bands delimited by n+1 equally spaced levels, or it can be an `AbstractVector{<:Real}` that lists n consecutive edges @@ -59,6 +59,9 @@ for specifying the triangles, otherwise an unconstrained triangulation of `xs` a mixin_generic_plot_attributes()... end +argument_dims(::Type{<:Tricontourf}, x, y, z) = (1, 2) +argument_dims(::Type{<:Tricontourf}, triangulation, z) = nothing + function Makie.used_attributes(::Type{<:Tricontourf}, ::AbstractVector{<:Real}, ::AbstractVector{<:Real}, ::AbstractVector{<:Real}) return (:triangulation,) end @@ -90,7 +93,7 @@ function Makie.plot!(c::Tricontourf{<:Tuple{<:DelTri.Triangulation, <:AbstractVe graph = c.attributes # prepare levels, colormap related nodes - register_contourf_computations!(graph, :converted_2) + register_contourf_computations!(graph, :zs) function calculate_polys!(polys, colors, triangulation, zs, levels::Vector{Float32}, is_extended_low, is_extended_high) levels = copy(levels) @@ -130,7 +133,7 @@ function Makie.plot!(c::Tricontourf{<:Tuple{<:DelTri.Triangulation, <:AbstractVe register_computation!( graph, - [:converted_1, :converted_2, :computed_levels, :computed_lowcolor, :computed_highcolor], + [:tri, :zs, :computed_levels, :computed_lowcolor, :computed_highcolor], [:polys, :computed_colors] ) do (tri, zs, levels, low, high), changed, cached is_extended_low = !isnothing(low) diff --git a/Makie/src/basic_recipes/triplot.jl b/Makie/src/basic_recipes/triplot.jl index 48b9c962972..3ae8438c025 100644 --- a/Makie/src/basic_recipes/triplot.jl +++ b/Makie/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::Union{DelTri.Triangulation, VecTypesVector{N, <:Real} where {N}},) begin # Toggles "Determines whether to plot the individual points. Note that this will only plot points included in the triangulation." show_points = false diff --git a/Makie/src/basic_recipes/volumeslices.jl b/Makie/src/basic_recipes/volumeslices.jl index a4036f508ec..57669b8cee1 100644 --- a/Makie/src/basic_recipes/volumeslices.jl +++ b/Makie/src/basic_recipes/volumeslices.jl @@ -3,7 +3,12 @@ Draws heatmap slices of the volume `v`. """ -@recipe VolumeSlices (x, y, z, volume) begin +@recipe VolumeSlices ( + x::Union{RangeLike{<:Real}, EndPoints{<:Real}}, + y::Union{RangeLike{<:Real}, EndPoints{<:Real}}, + z::Union{RangeLike{<:Real}, EndPoints{<:Real}}, + volume::AbstractArray{<:Union{Real, Colorant}, 3}, +) begin documented_attributes(Heatmap)... "Controls whether the bounding box outline is visible" bbox_visible = true @@ -11,6 +16,8 @@ Draws heatmap slices of the volume `v`. bbox_color = RGBAf(0.5, 0.5, 0.5, 0.5) end +argument_dims(::Type{<:VolumeSlices}, x, y, z, vol) = (1, 2, 3) + function Makie.plot!(plot::VolumeSlices) @extract plot (x, y, z, volume) diff --git a/Makie/src/basic_recipes/voronoiplot.jl b/Makie/src/basic_recipes/voronoiplot.jl index b2fd0705ffb..57e7553b4c2 100644 --- a/Makie/src/basic_recipes/voronoiplot.jl +++ b/Makie/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 begin +@recipe Voronoiplot (input::Union{VecTypesVector{N, <:Real} where {N}, DelTri.VoronoiTessellation},) begin "Determines whether to plot the individual generators." show_generators = true "If true, then the Voronoi tessellation is smoothed into a centroidal tessellation." @@ -117,6 +117,7 @@ function convert_arguments(::Type{<:Voronoiplot}, mat::AbstractMatrix) return convert_arguments(PointBased(), axes(mat, 1), axes(mat, 2), mat) end convert_arguments(::Type{<:Voronoiplot}, xs, ys, zs) = convert_arguments(PointBased(), xs, ys, zs) +argument_dims(::Type{Voronoiplot}, x, y, z) = (1, 2) # last dim treated as colormap values # For scatter-like inputs convert_arguments(::Type{<:Voronoiplot}, ps) = convert_arguments(PointBased(), ps) convert_arguments(::Type{<:Voronoiplot}, xs, ys) = convert_arguments(PointBased(), xs, ys) @@ -126,12 +127,12 @@ function plot!(p::Voronoiplot{<:Tuple{<:Vector{<:Point{N}}}}) where {N} if N == 3 # from call pattern (::Vector, ::Vector, ::Matrix) - map!(ps -> (Point2.(ps), last.(ps)), p, :converted_1, [:positions, :extracted_colors]) + map!(ps -> (Point2.(ps), last.(ps)), p, :input, [:positions, :extracted_colors]) positions = :positions color = :extracted_colors else # from xs, ys or Points call pattern - positions = :converted_1 + positions = :input color = :color end @@ -177,7 +178,7 @@ end boundingbox(p::Voronoiplot{<:Tuple{<:Vector{<:Point}}}, space::Symbol = :data) = apply_transform_and_model(p, data_limits(p)) function plot!(p::Voronoiplot{<:Tuple{<:DelTri.VoronoiTessellation}}) - ComputePipeline.alias!(p.attributes, :converted_1, :vorn) + ComputePipeline.alias!(p.attributes, :input, :vorn) map!(p, [:color, :vorn], :calculated_colors) do color, vorn if color === automatic diff --git a/Makie/src/basic_recipes/voxels.jl b/Makie/src/basic_recipes/voxels.jl index e18a51c28ad..09f99128fe5 100644 --- a/Makie/src/basic_recipes/voxels.jl +++ b/Makie/src/basic_recipes/voxels.jl @@ -1,3 +1,5 @@ +argument_dims(::Type{<:Voxels}, x, y, z, chunk) = (1, 2, 3) + # expand_dimensions would require conversion trait function convert_arguments(::Type{<:Voxels}, chunk::Array{<:Real, 3}) X, Y, Z = map(x -> EndPoints(Float32(-0.5 * x), Float32(0.5 * x)), size(chunk)) diff --git a/Makie/src/compute-plots.jl b/Makie/src/compute-plots.jl index 30190596e3b..7bb26f0db79 100644 --- a/Makie/src/compute-plots.jl +++ b/Makie/src/compute-plots.jl @@ -444,61 +444,142 @@ function add_convert_kwargs!(attr, user_kw, P, args) end end -function add_dim_converts!(attr::ComputeGraph, dim_converts, args, input = :args) - if !(length(args) in (2, 3)) - # We only support plots with 2 or 3 dimensions right now - map!(attr, :args, :dim_converted) do args - return Ref{Any}(args) +function add_dim_converts!(::Type{P}, attr::ComputeGraph, dim_converts, args, args_converted, user_kw) where {P} + # Get dim of each argument. This needs to be reactive if we allow dynamic + # attributes that change dim-mapping, e.g. direction + kwarg_names = argument_dim_kwargs(P) + + # initialize the necessary attributes early + defaults = default_theme(nothing, P) + for key in kwarg_names + if !haskey(attr.inputs, key) + haskey(defaults, key) || error("Cannot use `argument_dim_kwargs(::$P) = (:$key, ...)` as it is not a valid recipe Attribute.") + add_input!(attr, key, pop!(user_kw, key, defaults[key])) end + end + + kwargs = NamedTuple{kwarg_names}([getproperty(attr, name)[] for name in kwarg_names]) + dim_tuple = argument_dims(P, args_converted...; kwargs...) + + if dim_tuple === nothing + # args declared not dim-convertible by argument_dims(). + map!(args -> Ref{Any}(args), attr, :args, :dim_converted) return + + elseif !(dim_tuple isa Tuple) + # Format check + error("`arguments_dims() must return a `Tuple` of integers or `Nothing` but returned $dim_tuple") + end + + # If convert_arguments() caused a change in dim-convertable arguments they + # should apply before treating dim converts + if args_converted !== args + map!(attr, [:args, :convert_kwargs], :recursive_convert) do args, kwargs + return convert_arguments(P, args...; kwargs...) + end + input = :recursive_convert + else + input = :args end - inputs = Symbol[] - for (i, arg) in enumerate(args) - update_dim_conversion!(dim_converts, i, arg) + # Add node for arg -> dim mapping. Should be dynamic for attributes like + # direction at least. + map!(attr, [input, kwarg_names...], :arg_dims) do args, kwargs... + nt = NamedTuple{kwarg_names}(kwargs) + return argument_dims(P, args...; nt...) + end + + # This sets conversions per dimension if they have not already been set. + # If a recipe has multiple arguments for one dimension that dimension may + # be set multiple times here (but only the first one will actually be used) + maxdim = 0 + for (i, dim) in enumerate(dim_tuple) + dim == 0 && continue + if dim isa Integer + update_dim_conversion!(dim_converts, dim, args_converted[i]) + maxdim = max(maxdim, dim) + else + for (j, d) in enumerate(dim) + update_dim_conversion!(dim_converts, d, args_converted[i], j) + maxdim = max(maxdim, d) + end + end + end + + # Add input containing Symbol(:dim_convert_, i) which triggers when the + # conversion changes. (One per dimension, so use unique on dim_tuple) + # Note that the order in dim_convert_names is important + dim_convert_names = Symbol[] + for i in 1:maxdim obs = convert(Observable{Any}, needs_tick_update_observable(Observable{Any}(dim_converts[i]))) converts_updated = map!(x -> dim_converts[i], Observable{Any}(), obs) add_input!(attr, Symbol(:dim_convert_, i), converts_updated) - push!(inputs, Symbol(:dim_convert_, i)) + push!(dim_convert_names, Symbol(:dim_convert_, i)) end - return register_computation!(attr, [input, inputs...], [:dim_converted]) do (expanded, converts...), changed, last - last_vals = isnothing(last) ? ntuple(i -> nothing, length(converts)) : last.dim_converted - result = ntuple(length(converts)) do i - return convert_dim_value(converts[i], attr, expanded[i], last_vals[i]) + + # Apply dim_convert + # TODO: Do we really need last here? + register_computation!( + attr, [input, :arg_dims, dim_convert_names...], [:dim_converted] + ) do (expanded, dims, converts...), changed, last + + last_vals = isnothing(last) ? ntuple(i -> nothing, length(dims)) : last.dim_converted + result = ntuple(length(expanded)) do i + # argument i is associated with the dim convert of dimension dims[i] + if i <= length(dims) && dims[i] != 0 + if dims[i] isa Integer + return convert_dim_value(converts[dims[i]], attr, expanded[i], last_vals[i]) + else + # Vector{<:VecTypes} case, where dim converts are expected to + # return an array for VecTypes dimension + # These arrays are repackaged as a Point array which hopefully + # goes through the remaining conversions without issues + parts = map(eachindex(dims[i]), dims[i]) do idx, dim + return convert_dim_value(converts[dim], attr, expanded[i], last_vals[i], idx) + end + return Point.(parts...) + end + else + return expanded[i] + end end return (Ref{Any}(result),) end + + return end function _register_argument_conversions!(::Type{P}, attr::ComputeGraph, user_kw) where {P} dim_converts = to_value(get!(() -> DimConversions(), user_kw, :dim_conversions)) + args = attr.args[] add_convert_kwargs!(attr, user_kw, P, args) kw = attr.convert_kwargs[] args_converted = convert_arguments(P, args...; kw...) status = got_converted(P, conversion_trait(P, args...), args_converted) - force_dimconverts = needs_dimconvert(dim_converts) - if force_dimconverts - add_dim_converts!(attr, dim_converts, args) + + # Controls whether the plot is forced to apply dim converts or allowed to + # use plain data in a dim_convert scene. Typically true for plots to scenes + # and false for plots to other plots + force_dimconverts = pop!(user_kw, :force_dimconverts) + defaults = default_theme(nothing, P) + space = to_value(get(user_kw, :space, get(defaults, :space, :data))) + + if !is_data_space(space) + # dim converts do not apply in relative, pixel or clip space + map!(attr, :args, :dim_converted) do args + return Ref{Any}(args) + end + elseif force_dimconverts && needs_dimconvert(dim_converts) + add_dim_converts!(P, attr, dim_converts, args, args_converted, user_kw) elseif (status === true || status === SpecApi) # Nothing needs to be done, since we can just use convert_arguments without dim_converts # And just pass the arguments through map!(attr, :args, :dim_converted) do args return Ref{Any}(args) end - elseif isnothing(status) || status == true # we don't know (e.g. recipes) - add_dim_converts!(attr, dim_converts, args) - elseif status === false - if args_converted !== args - # Not at target conversion, but something got converted - # This means we need to convert the args before doing a dim conversion - map!(attr, :args, :recursive_convert) do args - return convert_arguments(P, args...) - end - add_dim_converts!(attr, dim_converts, args_converted, :recursive_convert) - else - add_dim_converts!(attr, dim_converts, args) - end + elseif isnothing(status) || status === false # we don't know (e.g. recipes) or incomplete conversion + add_dim_converts!(P, attr, dim_converts, args, args_converted, user_kw) end # backwards compatibility for plot.converted (and not only compatibility, but it's just convenient to have) @@ -751,7 +832,9 @@ function Plot{Func}(user_args::Tuple, user_attributes::Dict) where {Func} end ArgTyp = typeof(converted) FinalPlotFunc = plotfunc(plottype(P, converted...)) + add_attributes!(Plot{FinalPlotFunc}, attr, user_attributes) + return Plot{FinalPlotFunc, ArgTyp}(user_attributes, attr) end diff --git a/Makie/src/conversion.jl b/Makie/src/conversion.jl index 029729ab24c..46e8196d30c 100644 --- a/Makie/src/conversion.jl +++ b/Makie/src/conversion.jl @@ -101,6 +101,7 @@ function convert_arguments end convert_arguments(::NoConversion, args...; kw...) = args get_element_type(::T) where {T} = T +get_element_type(t::Tuple) = get_element_type(first(t)) function get_element_type(arr::AbstractArray{T}) where {T} if T == Any return mapreduce(typeof, promote_type, arr) @@ -117,35 +118,19 @@ function types_for_plot_arguments(P::Type{<:Plot}, Trait::ConversionTrait) end function types_for_plot_arguments(::PointBased) - return Tuple{AbstractVector{<:Union{Point2, Point3}}} + return Tuple{AbstractVector{<:Union{Point2{<:Real}, Point3{<:Real}}}} end -should_dim_convert(::Type) = false +types_for_plot_arguments(::ImageLike) = Tuple{EndPoints{<:Real}, EndPoints{<:Real}, Matrix{<:Real}} -""" - should_dim_convert(::Type{<: Plot}, args)::Bool - 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 - 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 convertible. +function types_for_plot_arguments(::GridBased) + return Tuple{AbstractArray{<:Real}, AbstractArray{<:Real}, Matrix{<:Real}} +end -If a recipe wants to use dim conversions, it should overload this function: -```julia - 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)) +function types_for_plot_arguments(::VolumeLike) + # TODO: consider using RGB{N0f8}, RGBA{N0f8} instead of Vec/RGB(A){Float32} + return Tuple{ + EndPoints{<:Real}, EndPoints{<:Real}, EndPoints{<:Real}, + AbstractArray{<:Union{Float32, Vec3f, RGB{Float32}, Vec4f, RGBA{Float32}}, 3}, + } end diff --git a/Makie/src/conversions.jl b/Makie/src/conversions.jl index 181171448fb..346ddb33349 100644 --- a/Makie/src/conversions.jl +++ b/Makie/src/conversions.jl @@ -1,7 +1,6 @@ ################################################################################ # Type Conversions # ################################################################################ -const RangeLike = Union{AbstractVector, ClosedInterval, Tuple{Real, Real}} function convert_arguments(CT::ConversionTrait, args...) expanded = expand_dimensions(CT, args...) @@ -369,7 +368,7 @@ Takes one or two ClosedIntervals `x` and `y` and converts them to closed ranges with size(z, 1/2). """ function convert_arguments(P::GridBased, x::RangeLike, y::RangeLike, z::AbstractMatrix{<:Union{Real, Colorant}}) - return convert_arguments(P, to_linspace(x, size(z, 1)), to_linspace(y, size(z, 2)), z) + return (to_linspace(x, size(z, 1)), to_linspace(y, size(z, 2)), z) end function convert_arguments( @@ -379,6 +378,9 @@ function convert_arguments( return (to_linspace(x, size(z, 1)), to_linspace(y, size(z, 2)), el32convert(z)) end +# for dim_converts +to_endpoints(x::Tuple{<:Any, <:Any}) = x + function to_endpoints(x::Tuple{<:Real, <:Real}) T = float_type(x...) return EndPoints(T.(x)) @@ -438,6 +440,7 @@ function convert_arguments( return (EndPoints{Tx}(xe[1] - xstep, xe[2] + xstep), EndPoints{Ty}(ye[1] - ystep, ye[2] + ystep), el32convert(z)) end +# Note: used by dim_converts to normalize xs, ys, so no eltype on RangeLike function convert_arguments( ::ImageLike, xs::RangeLike, ys::RangeLike, data::AbstractMatrix{<:Union{Real, Colorant}} @@ -652,9 +655,9 @@ accepted types. """ function convert_arguments( ::Type{<:Mesh}, - vertices::AbstractArray, + vertices::AbstractArray{<:Union{VecTypes{N, <:Real}, <:Real}}, indices::AbstractArray - ) + ) where {N} vs = to_vertices(vertices) fs = to_triangles(indices) if eltype(vs) <: Point{3} @@ -734,6 +737,7 @@ end ################################################################################ to_linspace(interval::Interval, N) = range(leftendpoint(interval), stop = rightendpoint(interval), length = N) +to_linspace(x::AbstractVector, N) = x to_linspace(x, N) = range(first(x), stop = last(x), length = N) """ diff --git a/Makie/src/coretypes.jl b/Makie/src/coretypes.jl index c649d172793..447831a3bb9 100644 --- a/Makie/src/coretypes.jl +++ b/Makie/src/coretypes.jl @@ -163,3 +163,6 @@ Base.broadcasted(f, a, b::EndPoints) = EndPoints(f.(a, b.data)) Base.:(==)(a::EndPoints, b::NTuple{2}) = a.data == b # Something we can convert to an EndPoints type const EndPointsLike = Union{ClosedInterval, Tuple{Real, Real}} + +const RangeLike = Union{AbstractVector{T}, ClosedInterval{T}, Tuple{T, T}} where {T} +const VecTypesVector = AbstractVector{<:VecTypes{N, T}} where {N, T} diff --git a/Makie/src/dim-converts/argument_dims.jl b/Makie/src/dim-converts/argument_dims.jl new file mode 100644 index 00000000000..326a79f3990 --- /dev/null +++ b/Makie/src/dim-converts/argument_dims.jl @@ -0,0 +1,111 @@ +# loses dispatch to type calls, e.g. +# argument_dims(::Type{<:Scatter}, args...; kwargs...) +""" + argument_dims(P::Type{<:Plot}, args...; attributes...) + argument_dims(trait::ConversionTrait, args...; attributes...) + +Maps arguments to spatial dimensions for dim converts. This optionally includes +the attributes defined via `argument_dim_kwargs(P)`. + +The return type of this function can be `nothing` to indicate that the plot/trait +arguments are not compatible with dim converts or a `tuple` otherwise. The +elements can be `1, 2, 3` to connect the argument to the respective dim convert, +or `0` to mark it as non-dimensonal. Trailing `0`s can be omitted. Point-like +arguments can be represented by an inner tuple, range or array of integers. + +For example: + +``` +Makie.argument_dims(::Type{<:MyPlot}, xs, ys) = (1, 2) # default +Makie.argument_dims(::Type{<:MyPlot}, f::Function, xs) = (0, 1) +Makie.argument_dims(::Type{<:MyPlot}, xs, f::Function) = (1,) + +# default +function Makie.argument_dims(::Type{<:MyPlot}, ps::AbstractVector{<:Point{N}}) where {N} + return (1:N,) +end + +# default +Makie.argument_dim_kwargs(::Type{<:MyPlot2}) = (:direction,) +function Makie.argument_dims(::Type{<:MyPlot2}, xs, ys; direction) + return direction == :y ? (1, 2) : (2, 1) +end +``` + +The default implementation treats the common cases of `PointBased` data, i.e. +`xs, ys` and `xs, ys, zs` vectors (or values) as well as vectors of `VecTypes`. +The latter also allows multiple vectors with matching inner dimension `VecTypes{N}`. +If included via `argument_dim_kwargs()`, `:direction` and `:orientation` are also +handled by the default path in the 2D case. For this `direction == :y` and +`orientation == :vertical` are considered neutral, not swapping dimensions. +Examples marked `# default` above mirror the default path. + +Note that the `::Type{<:Plot}` methods take precedence over `::ConversionTrait`. +""" +function argument_dims(PT, args...; kwargs...) + CT = conversion_trait(PT, args...) + return argument_dims(CT, args...; kwargs...) +end + +# Loses dispatch to specific traits, e.g. +# argument_dims(trait::PointBased, args...; kwargs...) +function argument_dims(trait::ConversionTrait, args...; kwargs...) + return _argument_dims(args; kwargs...) +end + +# Default handling + +# point like data +function _argument_dims( + t::Tuple{Vararg{Union{VecTypes{N}, VecTypesVector{N}}}}; + direction::Symbol = :y, orientation::Symbol = :vertical + ) where {N} + + dims = ntuple(identity, N) + if N == 2 + dims = ifelse(direction === :y, dims, (dims[2], dims[1])) + dims = ifelse(orientation === :vertical, dims, (dims[2], dims[1])) + end + return ntuple(i -> dims, length(t)) +end + +# 2 or 3 values/arrays of values +function _argument_dims(args; direction::Symbol = :y, orientation::Symbol = :vertical) + # Block any one argument case by default, e.g. VecTypes, GeometryPrimitive + length(args) in (2, 3) || return nothing + + # disallow VecTypes + if any(arg -> arg isa Union{VecTypes, AbstractArray{<:VecTypes}}, args) + return nothing + end + + dims = ntuple(identity, length(args)) + if length(args) == 2 + dims = ifelse(direction === :y, dims, (dims[2], dims[1])) + dims = ifelse(orientation === :vertical, dims, (dims[2], dims[1])) + end + return dims +end + + +argument_dims(::ImageLike, x, y, z) = (1, 2) +argument_dims(::VertexGrid, x, y, z) = (1, 2) # contour, contourf +argument_dims(::CellGrid, x, y, z) = (1, 2) +argument_dims(::VolumeLike, x, y, z, volume) = (1, 2, 3) + +argument_dims(::Type{<:Mesh}, ps::VecTypesVector{N}, faces) where {N} = (1:N,) +argument_dims(::Type{<:Mesh}, x, y, z, faces) = (1, 2, 3) +argument_dims(::Type{<:Surface}, x, y, z) = (1, 2, 3) # not like contour + +# attributes that are needed to map args to dims, e.g. direction/orientation +""" + argument_dim_kwargs(P::Type{<:Plot}) + +Returns a tuple of symbols marking attributes that need to be passed to +`argument_dims(P, args; attributes...)`. + +This is meant to be extended for recipes. For example: + + Makie.argument_dim_kwargs(::Type{<:MyPlot}) = (:direction,) +""" +argument_dim_kwargs(::Type{<:Plot}) = tuple() diff --git a/Makie/src/dim-converts/categorical-integration.jl b/Makie/src/dim-converts/categorical-integration.jl index fe9267d519e..bbbf25bc421 100644 --- a/Makie/src/dim-converts/categorical-integration.jl +++ b/Makie/src/dim-converts/categorical-integration.jl @@ -46,12 +46,10 @@ end expand_dimensions(::PointBased, y::Categorical) = (keys(y.values), y) needs_tick_update_observable(conversion::CategoricalConversion) = conversion.category_to_int -should_dim_convert(::Type{Categorical}) = true create_dim_conversion(::Type{Categorical}) = CategoricalConversion(; sortby = identity) # Support enums as categorical per default expand_dimensions(::PointBased, y::AbstractVector{<:Enum}) = (keys(y), y) -should_dim_convert(::Type{<:Enum}) = true create_dim_conversion(::Type{<:Enum}) = CategoricalConversion(; sortby = identity) function recalculate_categories!(conversion::CategoricalConversion) @@ -144,8 +142,11 @@ function convert_dim_value(conversion::CategoricalConversion, attr, values, prev return convert_categorical.(Ref(conversion), unwrapped_values) end +# TODO: Does it make sense to allow discarding all the categorical information +# and go back to default tick finding? +show_dim_convert_in_ticklabel(::CategoricalConversion) = true -function get_ticks(conversion::CategoricalConversion, ticks, scale, formatter, vmin, vmax) +function get_ticks(conversion::CategoricalConversion, ticks, scale, formatter, vmin, vmax, show_in_label) 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 @@ -156,6 +157,18 @@ function get_ticks(conversion::CategoricalConversion, ticks, scale, formatter, v 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 + if show_in_label + labels_str = formatter isa Automatic ? string.(categories) : get_ticklabels(formatter, categories) + return numbers, labels_str + else + vmin, vmax = extrema(numbers) + return get_ticks(ticks, scale, formatter, vmin, vmax) + end end + +show_dim_convert_in_axis_label(::CategoricalConversion) = false + +# TODO: +# Allow this to succeed so x/ylabel_suffix can be used? +# Or just error and force people to use x/ylabel instead? +get_label_suffix(dc::CategoricalConversion) = "" diff --git a/Makie/src/dim-converts/dates-integration.jl b/Makie/src/dim-converts/dates-integration.jl index d6dc3b2f996..7066ba851e2 100644 --- a/Makie/src/dim-converts/dates-integration.jl +++ b/Makie/src/dim-converts/dates-integration.jl @@ -59,7 +59,6 @@ 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() -should_dim_convert(::Type{<:Dates.AbstractTime}) = true function convert_dim_value(conversion::DateTimeConversion, value::Dates.TimeType) @@ -85,7 +84,12 @@ function convert_dim_value(conversion::DateTimeConversion, attr, values, previou return date_to_number.(conversion.type[], values) end -function get_ticks(conversion::DateTimeConversion, ticks, scale, formatter, vmin, vmax) +# TODO: Is there a point in allowing Date ticks to not be displayed? +# What would be shown instead? +# show_dim_convert_in_ticklabel(::DateTimeConversion, ::Bool) = true +show_dim_convert_in_ticklabel(::DateTimeConversion) = true + +function get_ticks(conversion::DateTimeConversion, ticks, scale, formatter, vmin, vmax, show_in_label) T = conversion.type[] # When automatic, we haven't actually plotted anything yet, so no unit chosen @@ -575,3 +579,12 @@ function datetime_range_ticklabels(tickobj::DateTimeTicks, datetimes::Vector{<:D error("invalid kind $kind") end end + +# TODO: Consider reworking offset ticks so that the origin time stamp is in the label? +# show_dim_convert_in_ticklabel(::DateTimeConversion) = true +# get_label_suffix(dc::DateTimeConversion, format) = get_formatted_timestamp(dc) + +# This only makes sense for Time which goes through units, not Dates or DateTime +show_dim_convert_in_axis_label(::DateTimeConversion) = false +# show_dim_convert_in_axis_label(::DateTimeConversion, ::Bool) = false +get_label_suffix(::DateTimeConversion) = error("Cannot produce a label suffix for Dates.") diff --git a/Makie/src/dim-converts/dim-converts.jl b/Makie/src/dim-converts/dim-converts.jl index f678c90d122..06dd4b19a1b 100644 --- a/Makie/src/dim-converts/dim-converts.jl +++ b/Makie/src/dim-converts/dim-converts.jl @@ -66,17 +66,41 @@ end # Return instance of AbstractDimConversion for a given type create_dim_conversion(argument_eltype::DataType) = NoDimConversion() -should_dim_convert(::Type{<:Real}) = false +should_dim_convert() = nothing 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) +function get_ticks(::Union{Nothing, NoDimConversion}, ticks, scale, formatter, vmin, vmax, show_in_label) return get_ticks(ticks, scale, formatter, vmin, vmax) end +# TODO: temporary +function get_ticks(c::Union{Nothing, AbstractDimConversion}, ticks, scale, formatter, vmin, vmax) + return get_ticks(c, ticks, scale, formatter, vmin, vmax, true) +end + +show_dim_convert_in_ticklabel(dc::Union{AbstractDimConversion, Nothing}, ::Automatic) = show_dim_convert_in_ticklabel(dc) +show_dim_convert_in_ticklabel(::Union{AbstractDimConversion, Nothing}) = false +show_dim_convert_in_ticklabel(::Union{AbstractDimConversion, Nothing}, option::Bool) = option + +# Should this trigger an error or just return ""? +get_label_suffix(dc, format, use_short) = apply_format(get_label_suffix(dc, use_short), format) +get_label_suffix(dc, format) = apply_format(get_label_suffix(dc), format) +get_label_suffix(dc, ::Bool) = get_label_suffix(dc) +get_label_suffix(dc) = error("No axis label suffix defined for conversion $dc.") +get_label_suffix(dc::Union{Nothing, NoDimConversion}) = "" + +# Don't default to generating a suffix for no dim conversion. +# TODO: Maybe allow option cases to go through though so `suffix` can be used w/o dimconverts? +show_dim_convert_in_axis_label(::Union{Nothing, NoDimConversion}, ::Automatic) = false + +show_dim_convert_in_axis_label(dc::AbstractDimConversion, ::Automatic) = show_dim_convert_in_axis_label(dc) +show_dim_convert_in_axis_label(::AbstractDimConversion) = true +show_dim_convert_in_axis_label(::Union{AbstractDimConversion, Nothing}, option::Bool) = option + # Recursively gets the dim convert from the plot # This needs to be recursive to allow recipes to use dim convert # TODO, should a recipe always set the dim convert to it's parent? @@ -182,31 +206,42 @@ end convert_dim_value(conv, attr, value, last_value) = value -function update_dim_conversion!(conversions::DimConversions, dim, value) - conversion = conversions[dim] - if !(conversion isa Union{Nothing, NoDimConversion}) - return - end - c = dim_conversion_from_args(value) - return conversions[dim] = c +function convert_dim_value(conv, attr, value, last_value, element_index) + return convert_dim_value(conv, attr, value, last_value) 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 +function convert_dim_value(conv, attr, points::AbstractArray{<:VecTypes}, last_value, element_index) + isempty(points) && return Float64[] + dim_value = [p[element_index] for p in points] + last_dim_value = last_value === nothing ? nothing : [p[element_index] for p in last_value] + return convert_dim_value(conv, attr, dim_value, last_dim_value) +end + +function convert_dim_value(conv, attr, point::VecTypes, last_value, element_index) + last = last_value === nothing ? nothing : last_value[element_index] + return convert_dim_value(conv, attr, point[element_index], last) +end + +function update_dim_conversion!(conversions::DimConversions, dim, value, element_idx) + return update_dim_conversion!(conversions, dim, value) +end + +function update_dim_conversion!(conversions::DimConversions, dim, value::VecTypes, element_idx) + return update_dim_conversion!(conversions, dim, value[element_idx]) +end + +function update_dim_conversion!(conversions::DimConversions, dim, points::AbstractArray{<:VecTypes}, element_idx) + isempty(points) && return Float64[] + return update_dim_conversion!(conversions, dim, first(points)[element_idx]) +end + +function update_dim_conversion!(conversions::DimConversions, dim, value) + conversion = conversions[dim] + if conversion isa Union{Nothing, NoDimConversion} + c = dim_conversion_from_args(value) + return conversions[dim] = c end + return end function convert_dim_observable(conversions::DimConversions, dim::Int, value::Observable, deregister) diff --git a/Makie/src/dim-converts/dynamic-quantities-integration.jl b/Makie/src/dim-converts/dynamic-quantities-integration.jl index 9a811166c20..7257da56cf3 100644 --- a/Makie/src/dim-converts/dynamic-quantities-integration.jl +++ b/Makie/src/dim-converts/dynamic-quantities-integration.jl @@ -24,9 +24,6 @@ scatter(1:4, [0.01u"km", 0.02u"km", 0.03u"km", 0.04u"km"]; axis=(dim2_conversion """ struct DQConversion <: AbstractDimConversion quantity::Observable{Any} - units_in_label::Observable{Bool} end -function DQConversion(quantity = automatic; units_in_label = true) - return DQConversion(quantity, units_in_label) -end +DQConversion() = DQConversion(automatic) diff --git a/Makie/src/dim-converts/unitful-integration.jl b/Makie/src/dim-converts/unitful-integration.jl index 22e4d48bac4..5c6c6b9803e 100644 --- a/Makie/src/dim-converts/unitful-integration.jl +++ b/Makie/src/dim-converts/unitful-integration.jl @@ -6,10 +6,6 @@ const SupportedUnits = Union{Period, Unitful.Quantity, Unitful.LogScaled, Unitfu expand_dimensions(::PointBased, y::AbstractVector{<:SupportedUnits}) = (keys(y), y) create_dim_conversion(::Type{<:SupportedUnits}) = UnitfulConversion() -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) @@ -30,41 +26,6 @@ unit_string_long(unit) = unit_string_long(base_unit(unit)) unit_string_long(::Unitful.Unit{Sym, D}) where {Sym, D} = string(Sym) unit_string_long(unit::Unitful.LogScaled) = string(unit) -is_compound_unit(x::Period) = is_compound_unit(Quantity(x)) -is_compound_unit(::Quantity{T, D, U}) where {T, D, U} = is_compound_unit(U) -is_compound_unit(::Unitful.FreeUnits{U}) where {U} = length(U) != 1 -is_compound_unit(::Type{<:Unitful.FreeUnits{U}}) where {U} = length(U) != 1 -is_compound_unit(::T) where {T <: Union{Unitful.LogScaled, Quantity{<:Unitful.LogScaled, DimT, U}}} where {DimT, U} = false - -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 @@ -78,40 +39,20 @@ 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)) +to_unit(x::LogScaled) = Unitful.logunit(x) +to_unit(x::Quantity{NumT, DimT, U}) where {NumT <: LogScaled, DimT, U} = Unitful.logunit(NumT) * U() +to_unit(x) = Unitful.unit(x) -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 +unit_convert(::Automatic, x) = x -function get_all_base10_units(x::Unitful.Unit{Sym, Unitful.𝐓}) where {Sym} - return getfield.((Unitful,), TIME_UNIT_NAMES) +function unit_convert(unit::T, x::Tuple) where {T <: Union{Type{<:Unitful.AbstractQuantity}, Unitful.FreeUnits, Unitful.Unit}} + return unit_convert.(Ref(unit), x) 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] +function unit_convert(unit::Unitful.MixedUnits, x::Tuple) + return unit_convert.(Ref(unit), x) end -best_unit(min::LogScaled, max) = Unitful.logunit(min) -best_unit(min::Quantity{NumT, DimT, U}, max) where {NumT <: LogScaled, DimT, U} = Unitful.logunit(NumT) * U() - -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 @@ -155,49 +96,20 @@ scatter(1:4, [0.01u"km", 0.02u"km", 0.03u"km", 0.04u"km"]; axis=(dim2_conversion """ 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 +UnitfulConversion() = UnitfulConversion(automatic) -function update_extrema!(conversion::UnitfulConversion, id::String, vals) - conversion.automatic_units || return +function update_unit!(conversion::UnitfulConversion, vals) + if conversion.unit[] === automatic + conversion.unit[] = to_unit(first(vals)) - eltype, extrema = eltype_extrema(vals) - conversion.extrema[id] = if eltype <: Unitful.LogScaled - extrema - else - promote(Quantity.(extrema)...) - end - imini, imaxi = extrema - for (mini, maxi) in values(conversion.extrema) - imini = min(imini, mini) - imaxi = max(imaxi, maxi) - end - # If a unit only consists off of one element, e.g. "mm" or "J", try to find - # the best prefix. Otherwise (e.g. "kg/m^3") use the unit as is and don't - # change it. - if is_compound_unit(imini) - if conversion.unit[] === automatic - new_unit = Unitful.unit(0.5 * Quantity(imini + imaxi)) - else - return - end - else - new_unit = best_unit(imini, imaxi) - end - return if new_unit != conversion.unit[] - conversion.unit[] = new_unit # TODO, somehow we need another notify to update the axis label # The interactions in Lineaxis are too complex to debug this in a sane amount of time # So, I think we should just revisit this once we move lineaxis to use compute graph notify(conversion.unit) end + return end needs_tick_update_observable(conversion::UnitfulConversion) = conversion.unit @@ -215,19 +127,30 @@ function unit_string_to_rich(str::String) return rich(output...) end -function get_ticks(conversion::UnitfulConversion, ticks, scale, formatter, vmin, vmax) +show_dim_convert_in_ticklabel(::UnitfulConversion) = false +show_dim_convert_in_axis_label(::UnitfulConversion) = true + +function get_ticks(conversion::UnitfulConversion, ticks, scale, formatter, vmin, vmax, show_in_label) unit = conversion.unit[] unit isa Automatic && return [], [] unit_str = unit_string(unit) rich_unit_str = unit_string_to_rich(unit_str) tick_vals = get_tickvalues(ticks, scale, vmin, vmax) labels = get_ticklabels(formatter, tick_vals) - if conversion.units_in_label[] + if show_in_label labels = map(lbl -> rich(lbl, rich_unit_str), labels) end return tick_vals, labels end +function get_label_suffix(conversion::UnitfulConversion, format, use_short_units) + unit = conversion.unit[] + unit isa Automatic && return rich("") + ustr = use_short_units ? unit_string(unit) : unit_string_long(unit) + str = unit_string_to_rich(ustr) + return apply_format(str, format) +end + function convert_dim_value(conversion::UnitfulConversion, attr, values, last_values) unit = conversion.unit[] if !isempty(values) @@ -235,7 +158,8 @@ function convert_dim_value(conversion::UnitfulConversion, attr, values, last_val # Is there a function for this to check in Unitful? unit_convert(unit, values[1]) end - update_extrema!(conversion, string(objectid(attr)), values) + + update_unit!(conversion, values) return unit_convert(conversion.unit[], values) end diff --git a/Makie/src/figureplotting.jl b/Makie/src/figureplotting.jl index 1aee2f955d2..dbdf71121dc 100644 --- a/Makie/src/figureplotting.jl +++ b/Makie/src/figureplotting.jl @@ -331,6 +331,7 @@ default_plot_func(::typeof(plot), args) = plotfunc(plottype(map(to_value, args). end end end + get!(attributes, :force_dimconverts, true) plot = Plot{default_plot_func(F, pargs)}(pargs, attributes) ax = create_axis_like(plot, figkws, figarg) plot!(ax, plot) @@ -400,6 +401,7 @@ get_conversions(fig::Figure) = get_conversions(fig.scene) # inserts global state from axis into plot attributes if they exist get!(attributes, :dim_conversions, get_conversions(ax)) + get!(attributes, :force_dimconverts, true) plot = Plot{default_plot_func(F, pargs)}(pargs, attributes) if ax isa Figure && !(plot isa PlotSpecPlot) error("You cannot plot into a figure without an axis. Use `plot(fig[1, 1], ...)` instead.") @@ -413,6 +415,7 @@ end if !isnothing(conversion) get!(attributes, :dim_conversions, conversion) end + get!(attributes, :force_dimconverts, scene isa Scene) plot = Plot{default_plot_func(F, args)}(args, attributes) plot!(scene, plot) return plot diff --git a/Makie/src/makielayout/blocks/axis.jl b/Makie/src/makielayout/blocks/axis.jl index bfdb0859745..0438f17f01d 100644 --- a/Makie/src/makielayout/blocks/axis.jl +++ b/Makie/src/makielayout/blocks/axis.jl @@ -355,6 +355,8 @@ function initialize_block!(ax::Axis; palette = nothing) 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, minorticksused = ax.xminorgridvisible, + unit_in_ticklabel = ax.x_unit_in_ticklabel, unit_in_label = ax.x_unit_in_label, + label_suffix = ax.xlabel_suffix, use_short_unit = ax.use_short_x_units ) ax.xaxis = xaxis @@ -371,6 +373,8 @@ function initialize_block!(ax::Axis; palette = nothing) tickcolor = ax.ytickcolor, minorticksvisible = ax.yminorticksvisible, minortickalign = ax.yminortickalign, minorticksize = ax.yminorticksize, minortickwidth = ax.yminortickwidth, minortickcolor = ax.yminortickcolor, minorticks = ax.yminorticks, scale = ax.yscale, minorticksused = ax.yminorgridvisible, + unit_in_ticklabel = ax.y_unit_in_ticklabel, unit_in_label = ax.y_unit_in_label, + label_suffix = ax.ylabel_suffix, use_short_unit = ax.use_short_y_units ) ax.yaxis = yaxis diff --git a/Makie/src/makielayout/blocks/axis3d.jl b/Makie/src/makielayout/blocks/axis3d.jl index 50b42a0162e..356e9458274 100644 --- a/Makie/src/makielayout/blocks/axis3d.jl +++ b/Makie/src/makielayout/blocks/axis3d.jl @@ -88,19 +88,66 @@ function initialize_block!(ax::Axis3) cam.view_direction[] = viewdir end + x_dim_convert_updater = needs_tick_update_observable(ax.dim1_conversion) + y_dim_convert_updater = needs_tick_update_observable(ax.dim2_conversion) + z_dim_convert_updater = needs_tick_update_observable(ax.dim3_conversion) + ticknode_1 = Observable{Any}() - map!(scene, ticknode_1, finallimits, ax.xticks, ax.xtickformat) do lims, ticks, format - get_ticks(ax.scene.conversions[1], ticks, identity, format, minimum(lims)[1], maximum(lims)[1]) + map!( + scene, ticknode_1, finallimits, ax.xticks, ax.xtickformat, ax.x_unit_in_ticklabel, + x_dim_convert_updater + ) do lims, ticks, format, show_unit, _ + dc = ax.scene.conversions[1] + should_show = show_dim_convert_in_ticklabel(dc, show_unit) + get_ticks(dc, ticks, identity, format, minimum(lims)[1], maximum(lims)[1], should_show) end ticknode_2 = Observable{Any}() - map!(scene, ticknode_2, finallimits, ax.yticks, ax.ytickformat) do lims, ticks, format - get_ticks(ax.scene.conversions[2], ticks, identity, format, minimum(lims)[2], maximum(lims)[2]) + map!( + scene, ticknode_2, finallimits, ax.yticks, ax.ytickformat, ax.y_unit_in_ticklabel, + y_dim_convert_updater + ) do lims, ticks, format, show_unit, _ + dc = ax.scene.conversions[2] + should_show = show_dim_convert_in_ticklabel(dc, show_unit) + get_ticks(dc, ticks, identity, format, minimum(lims)[2], maximum(lims)[2], should_show) end ticknode_3 = Observable{Any}() - map!(scene, ticknode_3, finallimits, ax.zticks, ax.ztickformat) do lims, ticks, format - get_ticks(ax.scene.conversions[3], ticks, identity, format, minimum(lims)[3], maximum(lims)[3]) + map!( + scene, ticknode_3, finallimits, ax.zticks, ax.ztickformat, ax.z_unit_in_ticklabel, + z_dim_convert_updater + ) do lims, ticks, format, show_unit, _ + dc = ax.scene.conversions[3] + should_show = show_dim_convert_in_ticklabel(dc, show_unit) + get_ticks(dc, ticks, identity, format, minimum(lims)[3], maximum(lims)[3], should_show) + end + + xlabel_node = Observable{Any}() + map!( + xlabel_node, ax.xlabel, ax.xlabel_suffix, ax.x_unit_in_label, ax.use_short_x_units, + x_dim_convert_updater, update = true + ) do label, formatter, show_unit_in_label, use_short_unit, _ + dc = ax.scene.conversions[1] + return build_label_with_unit_suffix(dc, formatter, label, show_unit_in_label, use_short_unit) + end + + ylabel_node = Observable{Any}() + map!( + ylabel_node, ax.ylabel, ax.ylabel_suffix, ax.y_unit_in_label, ax.use_short_y_units, + y_dim_convert_updater, update = true + ) do label, formatter, show_unit_in_label, use_short_unit, _ + dc = ax.scene.conversions[2] + return build_label_with_unit_suffix(dc, formatter, label, show_unit_in_label, use_short_unit) + end + + zlabel_node = Observable{Any}() + map!( + zlabel_node, ax.zlabel, ax.zlabel_suffix, ax.z_unit_in_label, ax.use_short_z_units, + z_dim_convert_updater, update = true + ) do label, formatter, show_unit_in_label, use_short_unit, _ + dc = ax.scene.conversions[3] + x = build_label_with_unit_suffix(dc, formatter, label, show_unit_in_label, use_short_unit) + return x end add_panel!(scene, ax, 1, 2, 3, finallimits, mi3) @@ -126,12 +173,18 @@ function initialize_block!(ax::Axis3) ax.xreversed, ax.yreversed, ax.zreversed ) - xticks, xticklabels, xlabel = - add_ticks_and_ticklabels!(blockscene, scene, ax, 1, finallimits, ticknode_1, mi1, mi2, mi3, ax.azimuth, ax.xreversed, ax.yreversed, ax.zreversed) - yticks, yticklabels, ylabel = - add_ticks_and_ticklabels!(blockscene, scene, ax, 2, finallimits, ticknode_2, mi2, mi1, mi3, ax.azimuth, ax.xreversed, ax.yreversed, ax.zreversed) - zticks, zticklabels, zlabel = - add_ticks_and_ticklabels!(blockscene, scene, ax, 3, finallimits, ticknode_3, mi3, mi1, mi2, ax.azimuth, ax.xreversed, ax.yreversed, ax.zreversed) + xticks, xticklabels, xlabel = add_ticks_and_ticklabels!( + blockscene, scene, ax, 1, finallimits, ticknode_1, mi1, mi2, mi3, + ax.azimuth, ax.xreversed, ax.yreversed, ax.zreversed, xlabel_node + ) + yticks, yticklabels, ylabel = add_ticks_and_ticklabels!( + blockscene, scene, ax, 2, finallimits, ticknode_2, mi2, mi1, mi3, + ax.azimuth, ax.xreversed, ax.yreversed, ax.zreversed, ylabel_node + ) + zticks, zticklabels, zlabel = add_ticks_and_ticklabels!( + blockscene, scene, ax, 3, finallimits, ticknode_3, mi3, mi1, mi2, + ax.azimuth, ax.xreversed, ax.yreversed, ax.zreversed, zlabel_node + ) titlepos = lift(scene, ax.layoutobservables.computedbbox, ax.titlegap, ax.titlealign) do a, titlegap, align @@ -450,7 +503,7 @@ function add_gridlines_and_frames!(topscene, scene, overlay, ax, dim::Int, limit scene, endpoints, color = attr(:gridcolor), linewidth = attr(:gridwidth), clip_planes = Plane3f[], xautolimits = false, yautolimits = false, zautolimits = false, transparency = true, - visible = attr(:gridvisible), inspectable = false + visible = attr(:gridvisible), inspectable = false, force_dimconverts = false ) endpoints2 = lift(limits, tickvalues, min1, min2, xreversed, yreversed, zreversed) do lims, ticks, min1, min2, xrev, yrev, zrev @@ -469,7 +522,7 @@ function add_gridlines_and_frames!(topscene, scene, overlay, ax, dim::Int, limit scene, endpoints2, color = attr(:gridcolor), linewidth = attr(:gridwidth), clip_planes = Plane3f[], xautolimits = false, yautolimits = false, zautolimits = false, transparency = true, - visible = attr(:gridvisible), inspectable = false + visible = attr(:gridvisible), inspectable = false, force_dimconverts = false ) @@ -516,14 +569,16 @@ function add_gridlines_and_frames!(topscene, scene, overlay, ax, dim::Int, limit framelines = linesegments!( scene, framepoints, color = colors, linewidth = attr(:spinewidth), transparency = true, visible = attr(:spinesvisible), inspectable = false, - xautolimits = false, yautolimits = false, zautolimits = false, clip_planes = Plane3f[] + xautolimits = false, yautolimits = false, zautolimits = false, + clip_planes = Plane3f[], force_dimconverts = false ) front_framelines = linesegments!( overlay, framepoints_front_spines, color = attr(:spinecolor_4), linewidth = attr(:spinewidth), visible = map((a, b) -> a && b, ax.front_spines, attr(:spinesvisible)), transparency = true, inspectable = false, - xautolimits = false, yautolimits = false, zautolimits = false, clip_planes = Plane3f[] + xautolimits = false, yautolimits = false, zautolimits = false, + clip_planes = Plane3f[], force_dimconverts = false ) #= On transparency and render order @@ -554,7 +609,10 @@ function add_gridlines_and_frames!(topscene, scene, overlay, ax, dim::Int, limit return gridline1, gridline2, framelines end -function add_ticks_and_ticklabels!(topscene, scene, ax, dim::Int, limits, ticknode, miv, min1, min2, azimuth, xreversed, yreversed, zreversed) +function add_ticks_and_ticklabels!( + topscene, scene, ax, dim::Int, limits, ticknode, miv, min1, min2, + azimuth, xreversed, yreversed, zreversed, label + ) dimsym(sym) = Symbol(string((:x, :y, :z)[dim]) * string(sym)) attr(sym) = getproperty(ax, dimsym(sym)) @@ -738,9 +796,9 @@ function add_ticks_and_ticklabels!(topscene, scene, ax, dim::Int, limits, tickno end notify(attr(:labelalign)) - label = text!( + labelplot = text!( topscene, label_position, - text = attr(:label), + text = label, color = attr(:labelcolor), fontsize = attr(:labelsize), font = attr(:labelfont), @@ -750,7 +808,7 @@ function add_ticks_and_ticklabels!(topscene, scene, ax, dim::Int, limits, tickno inspectable = false ) - return ticks, ticklabels_text, label + return ticks, ticklabels_text, labelplot end function dim3point(dim1, dim2, dim3, v1, v2, v3) diff --git a/Makie/src/makielayout/helpers.jl b/Makie/src/makielayout/helpers.jl index 032e870353c..b33a20888a7 100644 --- a/Makie/src/makielayout/helpers.jl +++ b/Makie/src/makielayout/helpers.jl @@ -466,6 +466,17 @@ function apply_format(value, formatstring::String) return Format.format(formatstring, value) end +function apply_format(value::RichText, formatstring::String) + placeholder = "{PLACEHOLDER}" + formatted = Format.format(formatstring, placeholder) + if contains(formatted, placeholder) + pre, post = String.(split(formatted, placeholder)) + return rich(pre, value, post) + else + return rich(formatted) + end +end + Makie.get_scene(ax::Axis) = ax.scene Makie.get_scene(ax::Axis3) = ax.scene Makie.get_scene(ax::LScene) = ax.scene diff --git a/Makie/src/makielayout/lineaxis.jl b/Makie/src/makielayout/lineaxis.jl index a2248673ab3..12a88fbd9b6 100644 --- a/Makie/src/makielayout/lineaxis.jl +++ b/Makie/src/makielayout/lineaxis.jl @@ -38,7 +38,10 @@ function calculate_protrusion( real_labelsize::Float32 = if label_is_empty 0.0f0 else - boundingbox(labeltext, :data).widths[horizontal[] ? 2 : 1] + # TODO: This can probably be + # widths(fast_string_boundingboxes(labeltext)[1]) + # to skip positions? (This only runs for axis labels) + widths(boundingbox(labeltext, :data))[horizontal[] ? 2 : 1] end labelspace::Float32 = (labelvisible && !label_is_empty) ? real_labelsize + labelpadding : 0.0f0 @@ -261,6 +264,16 @@ function update_minor_ticks(minortickpositions, limits::NTuple{2, Float64}, pos_ return end +function build_label_with_unit_suffix(dim_convert, formatter, label, show_unit_in_label, use_short_units) + should_show = show_dim_convert_in_axis_label(dim_convert, show_unit_in_label) + if should_show + suffix = get_label_suffix(dim_convert, formatter, use_short_units) + return isempty(label) ? suffix : rich("$label ", suffix) + else + return label + end +end + function LineAxis(parent::Scene, attrs::Attributes) decorations = Dict{Symbol, Any}() @@ -412,8 +425,22 @@ function LineAxis(parent::Scene, attrs::Attributes) end::Float32 end + # label + dim convert suffix + # TODO probably make these mandatory + suffix_formatter = get(attrs, :label_suffix, Observable("")) + unit_in_label = get(attrs, :unit_in_label, Observable(false)) + use_short_unit = get(attrs, :use_short_unit, Observable(true)) + + obs = needs_tick_update_observable(dim_convert) # make sure we update tick calculation when needed + label_with_suffix = Observable{Any}() + map!( + label_with_suffix, label, suffix_formatter, unit_in_label, use_short_unit, obs, update = true + ) do label, formatter, show_unit_in_label, use_short_unit, _ + return build_label_with_unit_suffix(dim_convert[], formatter, label, show_unit_in_label, use_short_unit) + end + labeltext = text!( - parent, labelpos, text = label, fontsize = labelsize, color = labelcolor, + parent, labelpos, text = label_with_suffix, fontsize = labelsize, color = labelcolor, visible = labelvisible, align = labelalign, rotation = labelrot, font = labelfont, markerspace = :data, inspectable = false @@ -425,7 +452,9 @@ function LineAxis(parent::Scene, attrs::Attributes) xs::Float32, ys::Float32 = if labelrotation isa Automatic 0.0f0, 0.0f0 else - wx, wy = widths(boundingbox(labeltext, :data)) + # There is only one string here and if we only case about widths + # we don't need to include positions through a higher level bbox function + wx, wy = widths(string_boundingboxes(labeltext)[1]) sign::Int = flipped ? 1 : -1 if horizontal 0.0f0, Float32(sign * 0.5f0 * wy) @@ -439,14 +468,16 @@ function LineAxis(parent::Scene, attrs::Attributes) decorations[:labeltext] = labeltext tickvalues = Observable(Float64[]; ignore_equal_values = true) + unit_in_ticklabel = get(attrs, :unit_in_ticklabel, Observable(true)) tickvalues_labels_unfiltered = Observable{Tuple{Vector{Float64}, Vector{Any}}}() - 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...) + attrs.scale, unit_in_ticklabel + ) do (position, extents, horizontal), _, limits, ticks, tickformat, scale, show_option + dc = dim_convert[] + should_show = show_dim_convert_in_ticklabel(dc, show_option) + return get_ticks(dim_convert[], ticks, scale, tickformat, limits..., should_show) end tickpositions = Observable(Point2f[]; ignore_equal_values = true) @@ -510,8 +541,11 @@ function LineAxis(parent::Scene, attrs::Attributes) calculate_protrusion, parent, protrusion, # we pass these as observables, to not trigger on them Observable((horizontal, labeltext, ticklabel_annotation_obs)), - ticksvisible, label, labelvisible, labelpadding, tickspace, ticklabelsvisible, actual_ticklabelspace, ticklabelpad, - # we don't need these as arguments to calculate it, but we need to pass it because it indirectly influences the protrusion + ticksvisible, label_with_suffix, labelvisible, labelpadding, tickspace, + ticklabelsvisible, actual_ticklabelspace, ticklabelpad, + # TODO: this can rely on a ...boundingbox_obs() function instead now + # we don't need these as arguments to calculate it, but we need to pass it because it + # indirectly influences the protrusion labelfont, labelalign, labelrot, labelsize, ticklabelfont, tickalign ) diff --git a/Makie/src/makielayout/types.jl b/Makie/src/makielayout/types.jl index 1fec40bf17b..b3b20799d4d 100644 --- a/Makie/src/makielayout/types.jl +++ b/Makie/src/makielayout/types.jl @@ -301,6 +301,33 @@ Axis(fig_or_scene; palette = nothing, kwargs...) """ dim2_conversion = nothing + "Controls whether the x dim_convert is shown in ticklabels." + x_unit_in_ticklabel::Union{Bool, Automatic} = automatic + "Controls whether the y dim_convert is shown in ticklabels." + y_unit_in_ticklabel::Union{Bool, Automatic} = automatic + "Controls whether the x dim_convert is shown in the xlabel." + x_unit_in_label::Union{Bool, Automatic} = automatic + "Controls whether the y dim_convert is shown in the ylabel." + y_unit_in_label::Union{Bool, Automatic} = automatic + """ + Formatter for the xlabel suffix generated from dim_converts. Can be a + Format.jl format string or a callback function acting acting on the + string or rich text generated from the dim convert. + Can also be a plain String replacing an active dim_convert label. + """ + xlabel_suffix = "[{}]" + """ + Formatter for the ylabel suffix generated from dim_converts. Can be a + Format.jl format string or a callback function acting acting on the + string or rich text generated from the dim convert. + Can also be a plain String replacing an active dim_convert label. + """ + ylabel_suffix = "[{}]" + "Switches between short and long x units, e.g. \"s\" vs \"Second\"" + use_short_x_units::Bool = true + "Switches between short and long y units, e.g. \"s\" vs \"Second\"" + use_short_y_units::Bool = true + """ The content of the x axis label. The value can be any non-vector-valued object that the `text` primitive supports. @@ -1757,6 +1784,47 @@ end Global state for the z dimension conversion. """ dim3_conversion = nothing + + "Controls whether the x dim_convert is shown in ticklabels." + x_unit_in_ticklabel::Union{Bool, Automatic} = automatic + "Controls whether the y dim_convert is shown in ticklabels." + y_unit_in_ticklabel::Union{Bool, Automatic} = automatic + "Controls whether the z dim_convert is shown in ticklabels." + z_unit_in_ticklabel::Union{Bool, Automatic} = automatic + "Controls whether the x dim_convert is shown in the xlabel." + x_unit_in_label::Union{Bool, Automatic} = automatic + "Controls whether the y dim_convert is shown in the ylabel." + y_unit_in_label::Union{Bool, Automatic} = automatic + "Controls whether the z dim_convert is shown in the zlabel." + z_unit_in_label::Union{Bool, Automatic} = automatic + """ + Formatter for the xlabel suffix generated from dim_converts. Can be a + Format.jl format string or a callback function acting acting on the + string or rich text generated from the dim convert. + Can also be a plain String replacing an active dim_convert label. + """ + xlabel_suffix = "[{}]" + """ + Formatter for the ylabel suffix generated from dim_converts. Can be a + Format.jl format string or a callback function acting acting on the + string or rich text generated from the dim convert. + Can also be a plain String replacing an active dim_convert label. + """ + ylabel_suffix = "[{}]" + """ + Formatter for the zlabel suffix generated from dim_converts. Can be a + Format.jl format string or a callback function acting acting on the + string or rich text generated from the dim convert. + Can also be a plain String replacing an active dim_convert label. + """ + zlabel_suffix = "[{}]" + "Switches between short and long x units, e.g. \"s\" vs \"Second\"" + use_short_x_units::Bool = true + "Switches between short and long y units, e.g. \"s\" vs \"Second\"" + use_short_y_units::Bool = true + "Switches between short and long z units, e.g. \"s\" vs \"Second\"" + use_short_z_units::Bool = true + "The height setting of the scene." height = nothing "The width setting of the scene." diff --git a/Makie/src/specapi.jl b/Makie/src/specapi.jl index 604d38f3430..f52d4b8254d 100644 --- a/Makie/src/specapi.jl +++ b/Makie/src/specapi.jl @@ -578,7 +578,9 @@ plottype(::Type{<:Plot}, ::Union{GridLayoutSpec, BlockSpec}) = Plot{plot} function to_plot_object(ps::PlotSpec) P = plottype(ps) - return P((ps.args...,), copy(ps.kwargs)) + attr = copy(ps.kwargs) + get!(attr, :force_dimconverts, false) + return P((ps.args...,), attr) end diff --git a/Makie/src/stats/boxplot.jl b/Makie/src/stats/boxplot.jl index 90ff53f9bec..29fd6932e1e 100644 --- a/Makie/src/stats/boxplot.jl +++ b/Makie/src/stats/boxplot.jl @@ -18,7 +18,7 @@ the 75% percentile) with a midline marking the median - `x`: positions of the categories - `y`: variables within the boxes """ -@recipe BoxPlot (x, y) begin +@recipe BoxPlot (x::RealVector, y::RealVector) begin filtered_attributes(CrossBar, exclude = (:notchmin, :notchmax, :show_midline, :midlinecolor, :midlinewidth))... "Vector of statistical weights (length of data). By default, each observation has weight `1`." @@ -64,6 +64,7 @@ the 75% percentile) with a midline marking the median end conversion_trait(x::Type{<:BoxPlot}) = SampleBased() +argument_dim_kwargs(::Type{<:BoxPlot}) = (:orientation,) _cycle(v::AbstractVector, idx::Integer) = v[mod1(idx, length(v))] _cycle(v, idx::Integer) = v diff --git a/Makie/src/stats/crossbar.jl b/Makie/src/stats/crossbar.jl index e9272413491..0954d2c1a90 100644 --- a/Makie/src/stats/crossbar.jl +++ b/Makie/src/stats/crossbar.jl @@ -14,7 +14,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::RealVector, y::RealVector, ymin::RealVector, ymax::RealVector) begin "Sets the color of the drawn boxes. These can be values for colormapping." color = @inherit patchcolor @@ -78,6 +78,11 @@ It is most commonly used as part of the `boxplot`. mixin_generic_plot_attributes()... end +argument_dim_kwargs(::Type{<:CrossBar}) = (:orientation,) +function argument_dims(::Type{<:CrossBar}, x, y, ymin, ymax; orientation) + return ifelse(orientation === :vertical, (1, 2, 2, 2), (2, 1, 1, 1)) +end + function Makie.plot!(plot::CrossBar) map!( plot, [ diff --git a/Makie/src/stats/dendrogram.jl b/Makie/src/stats/dendrogram.jl index 3c3ac134a1c..31af60dc116 100644 --- a/Makie/src/stats/dendrogram.jl +++ b/Makie/src/stats/dendrogram.jl @@ -17,7 +17,7 @@ That node is then added to the list and can be merged with another. Note that this recipe is still experimental and subject to change in the future. """ -@recipe Dendrogram (nodes,) begin +@recipe Dendrogram (nodes::Vector{DNode},) begin """ Specifies how node connections are drawn. Can be `:tree` for direct lines or `:box` for rectangular lines. Other styles can be defined by overloading @@ -248,11 +248,15 @@ function find_merge(n1::DNode, n2::DNode; height = 1, index = max(n1.idx, n2.idx return DNode(index, Point2d(newx, newy), (n1.idx, n2.idx)) end +# TODO: What about rotation? Does this make sense with units/categorical in the first place? +argument_dims(::Type{<:Dendrogram}, x, y, merges) = (1, 2) +argument_dims(::Type{<:Dendrogram}, xy, merges) = ((1, 2),) + function convert_arguments(::Type{<:Dendrogram}, x::RealVector, y::RealVector, merges::Vector{<:Tuple{<:Integer, <:Integer}}) return convert_arguments(Dendrogram, convert_arguments(PointBased(), x, y)[1], merges) end -function convert_arguments(::Type{<:Dendrogram}, leaves::Vector{<:VecTypes{2}}, merges::Vector{<:Tuple{<:Integer, <:Integer}}) +function convert_arguments(::Type{<:Dendrogram}, leaves::Vector{<:VecTypes{2, <:Real}}, merges::Vector{<:Tuple{<:Integer, <:Integer}}) nodes = [DNode(i, n, nothing) for (i, n) in enumerate(leaves)] for m in merges push!(nodes, find_merge(nodes[m[1]], nodes[m[2]]; index = length(nodes) + 1)) diff --git a/Makie/src/stats/density.jl b/Makie/src/stats/density.jl index d388f306dcf..8425d25f187 100644 --- a/Makie/src/stats/density.jl +++ b/Makie/src/stats/density.jl @@ -21,7 +21,7 @@ end Plot a kernel density estimate of `values`. """ -@recipe Density begin +@recipe Density (values::RealVector,) begin mixin_colormap_attributes()... mixin_generic_plot_attributes()... """ @@ -62,9 +62,12 @@ Plot a kernel density estimate of `values`. cycle = [:color => :patchcolor] end -function plot!(plot::Density{<:Tuple{<:AbstractVector}}) +argument_dim_kwargs(::Type{<:Density}) = (:direction,) +argument_dims(::Type{<:Density}, vals; direction) = (ifelse(direction === :x, 1, 2),) + +function plot!(plot::Density{<:Tuple{<:RealVector}}) map!( - plot, [:converted_1, :direction, :boundary, :offset, :npoints, :bandwidth, :weights], + plot, [:values, :direction, :boundary, :offset, :npoints, :bandwidth, :weights], [:lower, :upper] ) do x, dir, bound, offs, n, bw, weights diff --git a/Makie/src/stats/distributions.jl b/Makie/src/stats/distributions.jl index 29f2c67bdbe..ae2d193a680 100644 --- a/Makie/src/stats/distributions.jl +++ b/Makie/src/stats/distributions.jl @@ -46,7 +46,7 @@ Broadly speaking, `qqline = :identity` is useful to see if `x` and `y` follow th whereas `qqline = :fit` and `qqline = :fitrobust` are useful to see if the distribution of `y` can be obtained from the distribution of `x` via an affine transformation. """ -@recipe QQPlot begin +@recipe QQPlot (points::VecTypesVector{2, <:Real}, line::VecTypesVector{2, <:Real}) begin filtered_attributes(ScatterLines, exclude = (:joinstyle, :miter_limit))... end @@ -56,7 +56,7 @@ end Shorthand for `qqplot(Normal(0,1), y)`, i.e., draw a Q-Q plot of `y` against the standard normal distribution. See `qqplot` for more details. """ -@recipe QQNorm begin +@recipe QQNorm (points::VecTypesVector{2, <:Real}, line::VecTypesVector{2, <:Real}) begin documented_attributes(QQPlot)... end @@ -88,6 +88,9 @@ end maybefit(D::Type{<:Distribution}, y) = Distributions.fit(D, y) maybefit(x, _) = x +argument_dims(::Type{<:QQPlot}, x, y) = (1, 2) +argument_dims(::Type{<:QQNorm}, y) = (2,) + function convert_arguments( ::Type{<:QQPlot}, points::AbstractVector{<:Point2}, lines::AbstractVector{<:Point2}; qqline = :none @@ -95,13 +98,13 @@ function convert_arguments( return (points, lines) end -function convert_arguments(::Type{<:QQPlot}, x′, y; qqline = :none) +function convert_arguments(::Type{<:QQPlot}, x′, y::RealVector; qqline = :none) x = maybefit(x′, y) points, line = fit_qqplot(x, y; qqline = qqline) return (points, line) end -convert_arguments(::Type{<:QQNorm}, y; qqline = :none) = +convert_arguments(::Type{<:QQNorm}, y::RealVector; qqline = :none) = convert_arguments(QQPlot, Distributions.Normal(0, 1), y; qqline = qqline) used_attributes(::Type{<:QQNorm}, y) = (:qqline,) diff --git a/Makie/src/stats/ecdf.jl b/Makie/src/stats/ecdf.jl index 5142149ea50..5b6a4ad25eb 100644 --- a/Makie/src/stats/ecdf.jl +++ b/Makie/src/stats/ecdf.jl @@ -20,13 +20,13 @@ function convert_arguments(P::Type{<:Plot}, ecdf::StatsBase.ECDF; npoints = 10_0 return to_plotspec(ptype, convert_arguments(ptype, x, ecdf(x)); kwargs...) end -function convert_arguments(P::Type{<:Plot}, x::AbstractVector, ecdf::StatsBase.ECDF) +function convert_arguments(P::Type{<:Plot}, x::RealVector, 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::Type{<:Plot}, x0::AbstractInterval, ecdf::StatsBase.ECDF) +function convert_arguments(P::Type{<:Plot}, x0::AbstractInterval{<:Real}, ecdf::StatsBase.ECDF) xmin, xmax = extrema(x0) z = ecdf_xvalues(ecdf, Inf) n = length(z) @@ -49,11 +49,13 @@ If `weights` for the values are provided, a weighted ECDF is plotted. documented_attributes(Stairs)... end +argument_dims(::Type{<:ECDFPlot}, x) = (1,) + used_attributes(::Type{<:ECDFPlot}, ::AbstractVector) = (:npoints, :weights) function Makie.convert_arguments( ::Type{<:ECDFPlot}, - x::AbstractVector; + x::RealVector; npoints = 10_000, weights = StatsBase.Weights(Float64[]), ) diff --git a/Makie/src/stats/hist.jl b/Makie/src/stats/hist.jl index fbe36c9baa8..8ce7f632844 100644 --- a/Makie/src/stats/hist.jl +++ b/Makie/src/stats/hist.jl @@ -27,7 +27,7 @@ end Plot a step histogram of `values`. """ -@recipe StepHist (values,) begin +@recipe StepHist (values::RealVector,) begin documented_attributes(Stairs)... """ @@ -55,6 +55,8 @@ Plot a step histogram of `values`. scale_to = nothing end +argument_dims(::Type{<:StepHist}, vals) = (1,) + function plot!(plot::StepHist) map!(pick_hist_edges, plot, [:values, :bins], :edges) @@ -81,7 +83,7 @@ end Plot a histogram of `values`. """ -@recipe Hist (values,) begin +@recipe Hist (values::RealVector,) begin """ Sets the number of bins if set to an integer or the edges of bins if set to an sorted collection of real numbers. @@ -124,6 +126,9 @@ Plot a histogram of `values`. over_bar_color = automatic end +argument_dim_kwargs(::Type{<:Hist}) = (:direction,) +argument_dims(::Type{<:Hist}, vals; direction) = (ifelse(direction === :y, 1, 2),) + function pick_hist_edges(vals, bins) if bins isa Int mi, ma = float.(extrema(vals)) diff --git a/Makie/src/stats/violin.jl b/Makie/src/stats/violin.jl index 48fee04937e..2017a42f10c 100644 --- a/Makie/src/stats/violin.jl +++ b/Makie/src/stats/violin.jl @@ -8,7 +8,7 @@ The density pairs can be sourced from the same or from different data. - `x`: positions of the categories - `y`: variables whose density is computed """ -@recipe Violin (x, y) begin +@recipe Violin (x::RealVector, y::RealVector) begin "Number of points used per density plot." npoints = 200 "Boundary of the density estimation, determined automatically if `automatic`." @@ -81,6 +81,7 @@ The density pairs can be sourced from the same or from different data. end conversion_trait(::Type{<:Violin}) = SampleBased() +argument_dim_kwargs(::Type{<:Violin}) = (:orientation,) getuniquevalue(v, idxs) = v diff --git a/Makie/src/types.jl b/Makie/src/types.jl index ba2de98252b..5239c02ddcb 100644 --- a/Makie/src/types.jl +++ b/Makie/src/types.jl @@ -499,6 +499,16 @@ function Base.:(==)(a::GlyphCollection, b::GlyphCollection) a.strokewidths == b.strokewidths end +struct RichText + type::Symbol + children::Vector{Union{RichText, String}} + attributes::Dict{Symbol, Any} + function RichText(type::Symbol, children...; kwargs...) + cs = Union{RichText, String}[children...] + return new(type, cs, Dict(kwargs)) + end +end + # The color type we ideally use for most color attributes const RGBColors = Union{RGBAf, Vector{RGBAf}, Vector{Float32}} diff --git a/Makie/test/conversions/conversions.jl b/Makie/test/conversions/conversions.jl index dcd8052c46d..0c247e376a4 100644 --- a/Makie/test/conversions/conversions.jl +++ b/Makie/test/conversions/conversions.jl @@ -17,7 +17,9 @@ using Makie: plotfunc, plotfunc!, func2type end @testset "Heatmapshader with ranges" begin - hm = Heatmap(((0, 1), (0, 1), Resampler(zeros(4, 4))), Dict{Symbol, Any}()) + # force_dimconverts is set by plot!(parent, ...) which this doesn't call. + # To avoid erroring we pass it manually + hm = Heatmap(((0, 1), (0, 1), Resampler(zeros(4, 4))), Dict{Symbol, Any}(:force_dimconverts => true)) @test hm.converted[][1] isa Makie.EndPoints{Float32} @test hm.converted[][2] isa Makie.EndPoints{Float32} @test hm.converted[][3].data == Resampler(zeros(4, 4)).data diff --git a/Makie/test/conversions/convert_arguments.jl b/Makie/test/conversions/convert_arguments.jl index 1cd71e6a806..cce0d2a2a4c 100644 --- a/Makie/test/conversions/convert_arguments.jl +++ b/Makie/test/conversions/convert_arguments.jl @@ -275,10 +275,15 @@ Makie.convert_arguments(::PointBased, ::MyConvVector) = ([Point(10, 20)],) @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{EndPoints{T_out}, EndPoints{T_out}, Matrix{Float32}} - @test apply_conversion(CT, r, i, m) isa Tuple{Vector{T_out}, Vector{T_out}, Matrix{Float32}} + # Trait doesn't recursively reapply, so it doesn't do the conversion from ranges to Vectors + if CT isa CellGrid + @test apply_conversion(CT, i, r, m) isa Tuple{AbstractRange{T_out}, typeof(r), Matrix{T_in}} + @test apply_conversion(CT, r, i, m) isa Tuple{typeof(r), AbstractRange{T_out}, Matrix{T_in}} + else + @test apply_conversion(CT, i, r, 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}} + end + @test apply_conversion(CT, i, i, m) isa Tuple{EndPoints{T_out}, EndPoints{T_out}, Matrix{Float32}} @test apply_conversion(CT, xgridvec, ygridvec, xgridvec) isa Tuple{Vector{T_out}, Vector{T_out}, Matrix{Float32}} # TODO OffsetArray end @@ -294,14 +299,17 @@ Makie.convert_arguments(::PointBased, ::MyConvVector) = ([Point(10, 20)],) if T_in == T_out @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}} + OT = CT isa VertexGrid ? AbstractRange{T_in} : AbstractRange{T_out} else @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}} + OT = CT isa VertexGrid ? AbstractRange{T_in} : Vector{T_out} end + MT = CT isa VertexGrid ? T_in : 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, r, m) isa Tuple{AbstractRange{T_out}, OT, Matrix{MT}} @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, r, i, m) isa Tuple{OT, AbstractRange{T_out}, Matrix{MT}} @test apply_conversion(CT, xgridvec, ygridvec, xgridvec) isa Tuple{Vector{T_out}, Vector{T_out}, Matrix{Float32}} # TODO OffsetArray end @@ -456,7 +464,7 @@ Makie.convert_arguments(::PointBased, ::MyConvVector) = ([Point(10, 20)],) end @testset "Tooltip" begin - @test apply_conversion(Tooltip, xs[1], ys[1], str) isa Tuple{Point2{T_out}, String} + @test apply_conversion(Tooltip, xs[1], ys[1], str) isa Tuple{Tuple{Point2{T_out}, String}} @test apply_conversion(Tooltip, xs[1], ys[1]) isa Tuple{Point2{T_out}} end diff --git a/Makie/test/conversions/dim-converts.jl b/Makie/test/conversions/dim-converts.jl index f4b0e352f8e..ab504aaa41d 100644 --- a/Makie/test/conversions/dim-converts.jl +++ b/Makie/test/conversions/dim-converts.jl @@ -128,3 +128,284 @@ end obs.val = [1, 1] # Integers are not convertible to Irrational, so if the type was "solidified" here, there should be a conversion error @test_nowarn notify(obs) end + +@testset "Recipe dim converts" begin + # These tests mainly test that one set of arguments can correctly generate + # dim_converts + + UC = Makie.UnitfulConversion + CC = Makie.CategoricalConversion + NC = Makie.NoDimConversion + + function test_plot(func, args...; dims = (1, 2), kwargs...) + get_value(x) = first(x) + get_value(x::Makie.ClosedInterval) = minimum(x) + + simplify(t::Tuple) = simplify.(t) + simplify(r::AbstractRange) = ntuple(i -> r[i], length(r)) + simplify(::Nothing) = nothing + simplify(i::Integer) = i + + @testset "$func" begin + f, a, p = func(args...; kwargs...) + + args = convert_arguments(typeof(p), args...) + + @test simplify(p.arg_dims[]) == dims + + dc_args = Any[nothing, nothing, nothing] + # UnitfulConversion only sees the first arg, so overwrite back to front + for i in reverse(eachindex(dims)) + dims[i] == 0 && continue + if dims[i] isa Integer + dc_args[dims[i]] = args[i] + else + for (j, d) in enumerate(dims[i]) + dc_args[d] = getindex.(args[i], j) + end + end + end + + dim_converts = p.dim_conversions[] + for i in 1:3 + dc = dim_converts[i] + + if dc isa Makie.CategoricalConversion + @test dc_args[i] isa Categorical + @test keys(dc.category_to_int[]) == Set(dc_args[i].values) + elseif dc isa Makie.UnitfulConversion + @test !(dc_args[i] isa Categorical) + @test dc.unit[] == Unitful.unit(get_value(dc_args[i])) + elseif dc isa Makie.NoDimConversion + @test !(dc_args[i] isa Categorical) + # @test !(dc_args[i] isa UnitfulThing) + @test eltype(dc_args[i]) <: Real + @test !isnothing(dc_args[i]) + elseif dc isa Nothing + @test nothing === dc_args[i] + else + error("Did not implement $(typeof(dc))") + end + end + end + end + + # Skipped: + # - ablines + # - arc + # - axis + # - LineSegmentBuffer, TextBuffer + # - datashader + # - pie + # - rainclouds + + # dims are always on the next line so they are easier to see + + # TODO: Primitives + @testset "Primitives" begin + test_plot(heatmap, (1:5) .* u"s", Categorical(["A", "B"]), rand(5, 2)) + test_plot(image, 0u"m" .. 1u"m", 0 .. 1, rand(10, 10)) + test_plot( + surface, (1:5) .* u"m", (1:5) .* u"cm", rand(5, 5) .* u"W", + dims = (1, 2, 3) + ) + test_plot(scatter, Categorical(["A", "C", "D"]), (1:3) .* u"N") + test_plot(meshscatter, (1:3) .* u"m", (1:3) .* u"cm") + test_plot(lines, (1:3) .* u"s", Categorical(["A", "C", "D"])) + test_plot(linesegments, (4:-1:1) .* u"s", (1:4) .* u"N") + test_plot(text, 1u"m", 1u"s", text = "here") + test_plot( + volume, 0u"m" .. 1u"m", 0u"g" .. 1u"g", 0u"s" .. 1u"s", rand(10, 10, 10), + dims = (1, 2, 3) + ) + test_plot( + mesh, rand(5) .* u"m", rand(5) .* u"s", rand(5) .* u"g", + dims = (1, 2, 3) + ) + test_plot( + voxels, 0u"m" .. 1u"m", 0u"g" .. 1u"g", 0u"s" .. 1u"s", rand(10, 10, 10), + dims = (1, 2, 3) + ) + end + + # Recipes (basic_recipes) + @testset "Basic Recipes" begin + test_plot(annotation, Categorical(["A", "B", "E"]), (1:3) .* u"m", text = ["one", "two", "three"]) + test_plot( + arrows2d, (1:5) .* u"m", 1:5, (0.1:0.1:0.5) .* u"m", zeros(5), + dims = (1, 2, 1, 2) + ) + # test_plot(band, 1:4, Categorical(["A", "A", "B", "B"]), Categorical(["D", "D", "C", "C"])) # Broken + test_plot( + band, Categorical(["A", "B", "C", "D"]), rand(4) .* u"cm", rand(4) .* u"m", + dims = (1, 2, 2) + ) + test_plot( + band, Categorical(["A", "B", "C", "D"]), rand(4) .* u"cm", rand(4) .* u"m", direction = :y, + dims = (2, 1, 1) + ) + test_plot( + barplot, + Categorical(["A", "B"][mod1.(1:20, 2)]), rand(20) .* u"m", + stack = fld1.(1:20, 2), color = fld1.(1:20, 2) + ) + test_plot( + bracket, 1u"m", 0, 1u"m", 2, + dims = (1, 2, 1, 2) + ) + test_plot( + bracket, 1u"m", 0u"s", 1u"m", 2u"s", + dims = (1, 2, 1, 2) + ) + test_plot(contourf, (1:10) .* u"m", (1:10) .* u"s", rand(10, 10)) + test_plot(contour, (1:10) .* u"m", (1:10) .* u"s", rand(10, 10)) + test_plot( + contour, 0u"m" .. 1u"m", 0u"s" .. 1u"s", 0 .. 1, rand(10, 10, 10), + dims = (1, 2, 3) + ) + + test_plot( + errorbars, 1:3, (1:3) .* u"m", (1:3) .* u"dm", + dims = (1, 2, 2) + ) + test_plot( + rangebars, 1:3, (1:3) .* u"cm", (1:3) .* u"dm", + dims = (1, 2, 2) + ) + test_plot( + errorbars, (1:3) .* u"m", (1:3), (1:3) .* u"dm", direction = :x, + dims = (1, 2, 1) + ) + test_plot( + rangebars, 1:3, (1:3) .* u"cm", (1:3) .* u"dm", direction = :x, + dims = (2, 1, 1) + ) + + test_plot( + hlines, rand(3) .* u"m", + dims = (2,) + ) + test_plot( + vlines, Categorical(["A", "C", "D"]), + dims = (1,) + ) + test_plot( + vspan, 1u"m", 2u"m", + dims = (1, 1) + ) + test_plot( + hspan, 1u"m", 2u"m", + dims = (2, 2) + ) + + test_plot(poly, rand(10) .* u"m", rand(10) .* u"s") + test_plot(scatterlines, rand(10) .* u"m", rand(10) .* u"s") + test_plot(series, rand(10) .* u"m", rand(3, 10) .* u"s") + test_plot(spy, 1u"m" .. 10u"m", 1u"s" .. 10u"s", rand(10, 10)) + test_plot(stairs, rand(10) .* u"m", rand(10) .* u"s") + test_plot(stem, rand(10) .* u"m", rand(10) .* u"s") + test_plot( + streamplot, p -> p, 0u"m" .. 1u"m", (1:10) .* u"s", + dims = (0, 1, 2) + ) + test_plot(textlabel, rand(10), rand(10) .* u"m", text = string.(1:10)) + test_plot( + timeseries, 1.0 * u"µm", + dims = (2,) + ) + test_plot(tooltip, 1, 2u"m", text = "woo") + test_plot(tricontourf, rand(10), rand(10) .* u"m", rand(10)) + test_plot(voronoiplot, rand(10), rand(10) .* u"m") + test_plot(voronoiplot, rand(10), rand(10) .* u"m", rand(10)) + test_plot(waterfall, 1:10, rand(10) .* u"m") + # test_plot(waterfall, rand(10) .* u"m") # test doesn't handle expand_arguments() + end + + @testset "stats plots" begin + test_plot( + boxplot, Categorical(rand(["A", "B"], 10)), rand(10) .* u"m", + dims = (1, 2) + ) + + test_plot( + crossbar, Categorical(["A", "B", "C", "D"]), rand(4) .* u"m", + (rand(4) .- 1) .* u"m", (rand(4) .+ 1) .* u"m", + orientation = :vertical, + dims = (1, 2, 2, 2) + ) + test_plot( + crossbar, Categorical(["A", "B", "C", "D"]), rand(4) .* u"m", + (rand(4) .- 1) .* u"m", (rand(4) .+ 1) .* u"m", + orientation = :horizontal, + dims = (2, 1, 1, 1) + ) + + # probably doesn't make sense but it works... + test_plot(dendrogram, (1:16) .* u"m", rand(16) .* u"s", [(2i - 1, 2i) for i in 1:15]) + + test_plot( + density, rand(100) .* u"s", + dims = (1,) + ) + test_plot( + density, rand(100) .* u"s", direction = :y, + dims = (2,) + ) + + test_plot(qqplot, rand(100) .* u"m", rand(100) .* u"cm") + # qqplot(rand(100) .* u"cm", rand(100)) # doesn't work, shouldn't work? + test_plot( + qqnorm, rand(100) .* u"cm", + dims = (2,) + ) + test_plot( + ecdfplot, 10 .* rand(100) .* u"m", + dims = (1,) + ) + + test_plot(hexbin, rand(10) .* u"m", rand(10) .* u"s") + test_plot( + stephist, rand(100) .* u"g", + dims = (1,) + ) + test_plot( + hist, rand(100) .* u"g", + dims = (1,) + ) + test_plot( + hist, rand(100) .* u"g", direction = :x, + dims = (2,) + ) + test_plot(violin, Categorical(rand(["A", "B"], 100)), rand(100) .* u"s") + test_plot( + violin, Categorical(rand(["A", "B"], 100)), rand(100) .* u"s", orientation = :horizontal, + dims = (2, 1) + ) + end + + # Sample plots that allow unique Point[] args + @testset "point-like conversions" begin + # PointBased() with different input types + x = rand(10) * u"s" + y = rand(10) * u"m" + test_plot(scatter, collect(zip(x, y)), dims = ((1, 2),)) + test_plot(barplot, Vec.(x, y), direction = :x, dims = ((2, 1),)) + test_plot(scatterlines, Point.(x, y, y), dims = ((1, 2, 3),)) + + # Other independent cases + ps = Point.(x, y) + test_plot(annotation, ps, text = string.(1:10), dims = ((1, 2),)) + test_plot(annotation, ps, ps, text = string.(1:10), dims = ((1, 2, 1, 2),)) + test_plot(arrows2d, ps, ps, dims = ((1, 2), (1, 2))) + test_plot(bracket, ps, ps, dims = ((1, 2), (1, 2))) + test_plot(band, ps, ps, dims = ((1, 2), (1, 2))) + test_plot(errorbars, ps, y, dims = ((1, 2), 2)) + test_plot(errorbars, ps, Vec.(y, y), dims = ((1, 2), (2, 2))) + test_plot(errorbars, Point.(x, y, x), direction = :x, dims = ((1, 2, 1),)) + test_plot(errorbars, Point.(x, y, x, x), direction = :x, dims = ((1, 2, 1, 1),)) + test_plot(rangebars, x, tuple.(y, y), dims = (1, (2, 2))) + test_plot(rangebars, tuple.(x, y, y), dims = ((1, 2, 2),)) + test_plot(poly, ps, 1:9, dims = ((1, 2),)) + test_plot(dendrogram, ps, [(i, i + 1) for i in 1:2:13], dims = ((1, 2),)) + end +end diff --git a/Makie/test/issues.jl b/Makie/test/issues.jl index 2bcae5b5e27..5accbe86869 100644 --- a/Makie/test/issues.jl +++ b/Makie/test/issues.jl @@ -88,7 +88,7 @@ @test propertynames(foo) == (:bar,) @test Dict(Makie.default_attribute(Attributes(; foo), (:foo, Attributes()))) == Dict(foo) - pl = Scatter((1:4,), Dict{Symbol, Any}()) + pl = Scatter((1:4,), Dict{Symbol, Any}(:force_dimconverts => true)) @test Set(propertynames(pl)) == keys(pl.attributes.outputs) end end diff --git a/ReferenceTests/src/tests/unitful.jl b/ReferenceTests/src/tests/unitful.jl index 54ec5135c37..8d9132b5597 100644 --- a/ReferenceTests/src/tests/unitful.jl +++ b/ReferenceTests/src/tests/unitful.jl @@ -55,3 +55,27 @@ end scatter(fig[1, 2], x, y, t .* u"s", markersize = 15, color = t, alpha = 0.8, transparency = true, axis = (; type = Axis3)) fig end + +@reference_test "Axis unit attributes" begin + fig = Figure(size = (400, 600)) + ax = Axis( + fig[1, 1], + x_unit_in_label = true, x_unit_in_ticklabel = false, + xlabel_suffix = "unit: {}", use_short_x_units = true, + y_unit_in_label = true, y_unit_in_ticklabel = true, + ylabel_suffix = "{}", use_short_y_units = false + ) + scatterlines!(ax, Point2.((1:10) .* u"s", sin.(1:10) .* u"m")) + + ax = Axis3( + fig[2, 1], + x_unit_in_label = true, x_unit_in_ticklabel = false, + xlabel_suffix = "unit: {}", use_short_x_units = true, + y_unit_in_label = true, y_unit_in_ticklabel = true, + ylabel_suffix = "{}", use_short_y_units = false, + z_unit_in_label = false, z_unit_in_ticklabel = true, + ) + scatterlines!(ax, Point3.((1:10) .* u"s", sin.(1:10) .* u"m", cos.(1:10) .* u"m")) + + fig +end diff --git a/docs/src/explanations/conversion_pipeline.md b/docs/src/explanations/conversion_pipeline.md index b9b27af0616..9a66c1f9a6f 100644 --- a/docs/src/explanations/conversion_pipeline.md +++ b/docs/src/explanations/conversion_pipeline.md @@ -11,6 +11,7 @@ The pipeline can be broadly be summarized in 3 parts each with a few steps:
  • Conversions which mainly normalize types
    1. `expand_dimensions()` adds defaulted/generated data (e.g. x, y in `image()`)
    2. +
    3. optional `convert_arguments()` application to prepare for `dim_convert`
    4. `dim_convert` processes special types like Units
    5. `convert_arguments()` normalizes numeric types & data formats
    @@ -58,19 +59,19 @@ This data is generated by `expand_dimensions(::Trait, args...)` where the `Trait ### Special Type Processing -The second step handles special types like `Unitful` types, `Dates` types or categorical values which need to be synchronized within the scene. -For example, if one plot uses "hours" as unit for its x values other plots need to also use time units for x. -If the scale of the unit differs between plots, i.e. one uses hours, the other minutes, then a common unit must be found and the values need to be scaled appropriately. +The second step handles special types like `Unitful` units, `Dates` types or categorical values. +These need to be synchronized across multiple plots within a scene. +For example, if one plot uses hours as their x unit, any other plot needs to also use a time unit, and that time unit needs to be scaled correctly (hours). This is what **dim_converts** handles. You can find more documentation on them in the [Dimension conversions](@ref) docs. ### Convert Arguments The last step and main work-horse in the conversion pipeline is the `convert_arguments()` function. -It's purpose is to convert different data types and layouts into one or a select few formats. +It's purpose is to convert different data types and layouts into one of a select few formats. For example, any data passed to `scatter()` is converted to a `Vector{Point{D, T}}` where `D = 2` or `3` and `T = Float32` or `Float64`. These conversions can happen based on the plot type or its conversion trait. -For `scatter()` the conversion trait `PointBased` is used. +For `scatter()` the conversion trait `PointBased()` is used. `convert_arguments()` can also accept keyword arguments sourced from plot attributes. For this the attribute needs to be marked with `used_attribute(::PlotType) = (names...)`. @@ -89,7 +90,6 @@ end to make that possible. You can use the plot type (i.e. `Scatter`) as the first argument as well. - ## [Transformations](@id pipeline_transformations) After conversions have normalized the type and layout of plot data, transformations can now adjust it. @@ -210,4 +210,3 @@ register_projected_positions!( ``` Alternatively you can also call `Makie.register_positions_transformed!(plot, input_name = ..., output_name = ...)`. - diff --git a/docs/src/explanations/dim-converts.md b/docs/src/explanations/dim-converts.md index 13be2003ee8..a066c62185f 100644 --- a/docs/src/explanations/dim-converts.md +++ b/docs/src/explanations/dim-converts.md @@ -1,23 +1,22 @@ # Dimension conversions Starting with Makie v0.21, support for types like units, categorical values and dates has been added. -They are converted to a plottable representation by dim(ension) converts, which also take care of axis ticks. +They are converted to a plottable representation by dim(ensional) converts, which synchronize information between different plots in a scene and generate appropriate ticks for an axis. In the following sections we will explain their usage and how to extend the interface with your own types. -## Examples +## Usage Examples The basic usage is as easy as replacing numbers with any supported type, e.g. `Dates.Second`: ```@figure dimconverts -using CairoMakie, Makie.Dates, Makie.Unitful -CairoMakie.activate!() # hide +using Makie.Dates, Makie.Unitful Makie.inline!(true) # hide f, ax, pl = scatter(rand(Second(1):Second(60):Second(20*60), 10)) ``` 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: +In this case time units like hours work, since they're compatible with the Unitful conversion: ```@figure dimconverts scatter!(ax, rand(Hour(1):Hour(1):Hour(20), 10)) @@ -26,9 +25,12 @@ scatter!(ax, LinRange(0u"yr", 0.1u"yr", 5)) f ``` -Note that the units displayed in ticks will adjust to the given range of values. +Note that the units displayed in ticks are set by first plot. +if you want a different unit of the same scale to be used you have three options. +You can adjust the units in the initial plot, initialize the axis with e.g. `Makie.UnitfulConversion(u"hr")` or change the unit of an existing dimensional conversion manually. +The latter two options are shown later in this section. -Going back to just numbers errors since the axis is unitful now: +Plotting just numbers into the axis errors since the axis is unitful now: ```julia try @@ -38,7 +40,7 @@ catch 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: +Similarly, trying to plot units into a unitless axis dimension errors too, as otherwise it would alter the meaning of the previous plotted values: ```julia try @@ -48,19 +50,93 @@ catch e end ``` -you can access the conversion via `ax.dim1_conversion` and `ax.dim2_conversion`: +Units can be shown in both axis labels and tick labels. +This can be controlled from the axis: -```julia -(ax.dim1_conversion[], ax.dim2_conversion[]) +```@setup dc_axis +using Makie.Unitful ``` -And set them accordingly: +```@figure dc_axis +using Makie.Unitful + +f,a,p = scatter((0:15:150) .* u"s", (0:1:10).^2 .* u"m") +a.x_unit_in_ticklabel = false +a.x_unit_in_label = true + +a.y_unit_in_ticklabel = true +a.y_unit_in_label = false +f +``` + +You can set the formatter for units in axis labels via `x/ylabel_suffix`. +It accepts either a `Format.jl` compatible String or a function that acts on the suffix generated by the dim convert. +This maybe rich text (e.g. units), strings (e.g. categorical) or LaTeXStrings (unused by Makie). + +```@figure dc_axis +a.xlabel_suffix = "unit: ({})" +f +``` + +Note that is also possible to completely replace the unit by passing a string without a value placeholder (`{}`) here. + +Units also support switching to a long representation for axis labels. +This is controlled via + +```@figure dc_axis +a.use_short_x_units = false +f +``` + +The dim converts are kept track of in the (generated) axis. +They can be accessed with `ax.dim1_conversion` and `ax.dim2_conversion`: + +```@example dc_axis +(a.dim1_conversion[], a.dim2_conversion[]) +``` + +and in the case of `UnitfulConversion`s their units can be adjusted by setting + +```@figure dc_axis +a.dim1_conversion[].unit[] = u"minute" +reset_limits!(a) +f +``` + +An axis can also be created explicitly with a dimensional conversion by setting those attributes in the constructor: + +```@setup dc_axis_construction +using Makie.Unitful +``` + +```@figure dc_axis_construction +using Makie.Unitful -```julia f = Figure() -ax = Axis(f[1, 1]; dim1_conversion=Makie.CategoricalConversion()) +ax = Axis(f[1, 1]; dim1_conversion=Makie.UnitfulConversion(u"km")) +scatter!(ax, (1:20).^2 .* u"m", 1:20) +f ``` +### Categorical Examples + +Categorical values need to be wrapped in `Cateogrical()` when constructing a plot: + +```@figure dc_categorical +f,a,p = scatter(Categorical([:A, :D, :F]), 1:3, color = :blue, marker = :circle, markersize = 30) +``` + +Adding another plot with (partially) different categories will result in the those categories being added to the conversion: + +```@figure dc_categorical +scatter!(a, Categorical([:B, :C, :E, :F]), 4:-1:1, color = :orange, marker = :rect, markersize = 30) +f +``` + +Similar to units, categories need to be of the same type. +Adding a plot with `Categorical(["A", "B"])` would thus error here. +The axis attributes also work with categorical conversions, though axis labels won't show anything as the conversion does not define an output for them. + ### Experimental DynamicQuantities.jl support !!! warning @@ -69,7 +145,6 @@ ax = Axis(f[1, 1]; dim1_conversion=Makie.CategoricalConversion()) Makie also provides support for [DynamicQuantities.jl](https://github.com/SymbolicML/DynamicQuantities.jl). This can used almost as a drop-in replacement for Unitful.jl, with the following key design differences: * There is no conversion support between Dates.jl and DynamicQuantities.jl objects. Use `1.0u"hr"` in place of `Dates.Hour(1.0)`, `1.0u"d"` in place of `Dates.day(1.0)`, etc. -* Units are set by the first object plotted by default. This is to prevent unexpected changes to other units when plotting multiple objects on the same axis. * Units are displayed in SI be default. Use `us""` (note the `s`) to explicitly set your desired units, either implicitly in a plot call or explicitly as a `dim_conversion` axis argument. Below are a few common usage examples to construct the same plot. Please file a bug report if you find an issue with this experimental feature! @@ -105,11 +180,11 @@ f ### Limitations -- For now, dim conversions only works for vectors with supported types for the x and y arguments for the standard 2D Axis. It's setup to generalize to other Axis types, but the full integration hasn't been done yet. -- Keywords like `direction=:y` in e.g. Barplot will not propagate to the Axis correctly, since the first argument is currently always x and second always y. We're still trying to figure out how to solve this properly +- For now, dim conversions only work with data that is split by dimension, i.e. separate x and y (and z) values. +- Only `Axis` and `Axis3` consider dim converts when generating ticks, tick labels and axis labels. - Categorical values need to be wrapped in `Categorical`, since it's hard to find a good type that isn't ambiguous when defaulting to a categorical conversion. You can find a work around in the docs. -- Date Time ticks simply use `PlotUtils.optimize_datetime_ticks` which is also used by Plots.jl. It doesn't generate optimally readable ticks yet and can generate overlaps and goes out of axis bounds quickly. This will need more polish to create readable ticks as default. - To properly apply dim conversions only when applicable, one needs to use the new undocumented `@recipe` macro and define a conversion target type. This means user recipes only work if they pass through the arguments to any basic plotting type without conversion. +- Plots like `Heatmap` which use dimensionally separated data `(x, y, matrix)` as their final conversion target need to define their target types in `@recipe` to properly detect when not to apply dim converts. Otherwise Makie will fail to detect cases where dim converts should not apply, e.g. when `Heatmap` is used within a recipe that has already resolved dim converts. ### Current conversions in Makie @@ -122,53 +197,191 @@ Makie.DateTimeConversion ## Developer docs -You can overload the API to define your own dim converts by overloading the following functions: +### Overview -```@figure dimconverts -struct MyDimConversion <: Makie.AbstractDimConversion end +When building a plot a few conditions are evaluated to decide whether a plot applies dim converts or not. +They are given in two layers. +The first layer checks the state of the plot and conversion: (This is an if, elseif, ... pattern.) +1. If a plot is not in data space it does not apply dim converts. +2. If the plot has `force_dimconverts == true` which means it plots to a scene rather than another plot (unless set explicitly), and dim converts are already fixed by the parent, then they are applied. If applying `convert_arguments()` resulted in a change they are applied before applying dim converts. +3. If `convert_arguments()` reached the final conversion target or if it returned a SpecApi type then dim converts are not applied. +4. If `convert_arguments()` did not reach the final conversion target or if it is unknown, then dim_converts do apply. If applying `convert_arguments()` resulted in a change they are applied before applying dim converts. + +If the first layer decides to apply dim converts we move to the second. +Here we check `argument_dims()` with partially converted or raw arguments. +If it returns `nothing` we assume dim converts can not be applied and continue without. +Otherwise dim converts get initialized via `update_dim_conversion!()`, added to the plot and included in the conversion pipeline using `convert_dim_value!()`. +### Target Conversion Types / Target Recipe Types + +In order for `convert_arguments()` to reach the final conversion target (i.e. hit case 3.) that type needs to be defined. +This can be done when creating a recipe for a plot type or for its conversion trait (lower priority): + +```julia +# Sets the target type of convert_arguments() for a plot type. +# This doesn't need to be a concrete type and can be a Union of types +@recipe MyPlot (converted1::TargetType1, converted2::TargetType2) begin + ... +end + +# Sets the target type for a conversion trait. +# This must be a Tuple type +Makie.type_for_plot_arguments(::ConversionTrait) = Tuple{TargetType1, TargetType2} +``` + +### Partial Conversions + +`convert_arguments()` is allowed to act on arguments before dim converts. +Whether a method is used or not depends on its types. + +```julia +# These can match arrays before dim conversion and thus apply +Makie.convert_arguments(::MyConversionTrait, data::AbstractVector) = ... +Makie.convert_arguments(::Type{<:MyPlot}, data::AbstractVector) = ... + +# These are only defined for numerical data as is present after dim converts. +# They will not apply before them. +Makie.convert_arguments(::MyConversionTrait, data::AbstractVector{<:Real}) = ... +Makie.convert_arguments(::MyConversionTrait, data::AbstractVector{<:VecTypes}) = ... +Makie.convert_arguments(::Type{<:MyPlot}, data::AbstractVector{<:Real}) = ... +Makie.convert_arguments(::Type{<:MyPlot}, data::AbstractVector{<:VecTypes}) = ... +``` + +### Argument Dims + +The `argument_dims(::PlotTypeOrTrait, args...; kwargs...)` function declares how plot arguments map to to dimensions for which dim converts are defined. +The arguments may be partially converted by `convert_arguments()` methods that apply before dim converts. +They can include plot attributes as keyword arguments by defining `argument_dim_kwargs(::PlotTypeOrTrait) = (:attribute1, :attribute2, ...)`. + +```julia +# For MyPlot with 4 arguments, args[1] and args[3] act in dimension 1, +# and args[2] and args[4] act in dimension 2 +Makie.argument_dims(::Type{<:MyPlot}, x, y, dx, dy) = (1, 2, 1, 2) + +# Plots with MyConversionTrait have an attribute `direction` which +# swaps the which dimension arguments apply to +Makie.argument_dim_kwargs(::MyConversionTrait) = (:direction,) +function Makie.argument_dims(::MyConversionTrait, x, y; direction) + return direction === :y ? (1, 2) : (2, 1) +end + +# MyPlot2 has a 4 argument version where the first and last argument +# do not relate to a dimension +# (0 means no dimension and trailing 0s can be omited) +function Makie.argument_dims(::MyPlot2, f, x, y, color_data) + return (0, 1, 2) +end + +# VecTypes (Points, Tuples, ...) can be handled with "inner" dimensions: +function Makie.argument_dims( + ::MyPlot, + xy::AbstractVector{VecTypes{N}}, + dxy::AbstractVector{VecTypes{N}} + ) where {N} + # can also use ntuple(identity, N) instead of 1:N + return (1:N, 1:N) +end + +# These arguments should never be dim converted +Makie.argument_dims(::MyPlot, unconvertable) = nothing +``` + +Note that `argument_dims` handle `x, y`, `x, y, z` vectors as well as their point-like representations automatically. +These cases also treat `direction` and `orientation` automatically if included via `argument_dim_kwargs()`. +For this `direction == :y` and `orientation = :vertical` are treated as the neutral (no swap) case. + +### Creating a new dim convert + +You can extend the dim convert API to define your own by overloading the following functions: + +```@figure dimconverts # 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) +# Required -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 +# A struct representing the Conversion that resolves your type to something numeric +struct MyDimConversion <: Makie.AbstractDimConversion end -# Indicate that this type should be converted using MyDimConversion +# Creates the Conversion based on the element type used in plot data. # 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. +# 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. +# The result is then used to call `create_dim_conversion(String)` 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(Makie.should_dim_convert, (MyDimTypes,))` because it can be slow and error prown. -Makie.should_dim_convert(::Type{MyUnit}) = true +# Applies the conversion to plot data. +# This is called whenever plot data or the dim_convert changes. +# `attr` is the ComputeGraph of the plot. It can be used to cache results or +# identify the plot by `objectid(attr)`. +# `prev_values` contains the last result of the conversion, or nothing if this +# is the first call +function Makie.convert_dim_value(conversion::MyDimConversion, attr, values, prev_values) + return Makie.convert_dim_value(conversion, values) +end -# 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` +# The non observable version of the dim_convert. +# This is needed to convert axis limits, and should be a pure version of the +# `convert_dim_observable` method above function Makie.convert_dim_value(::MyDimConversion, values) return [v.value for v in values] end -function Makie.convert_dim_value(conversion::MyDimConversion, attr, values, prev_values) - # Do the actual conversion here - # The `attr` can be used to identify the conversion, e.g. if you want to cache results. - # Take a look at categorical-integration.jl for an example of how to use it. - return Makie.convert_dim_value(conversion, values) +# Generates ticks for the axis when using a dim convert. +# This function applies per dimension. +# `ticks` are ticks set directly by the user through `ax.xticks` etc +# `scale` is the x/y/zscale function of the axis (e.g. log10) +# `formatter` is the x/y/ztickformat of the axis +# `limits_min, limits_max` are the limits of the axis dimension, after applying +# dim converts +# `show_units::Bool` sets whether units should be shown in ticklabels +function Makie.get_ticks( + ::MyDimConversion, ticks, scale, formatter, + limits_min, limits_max, show_units + ) + # 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( + ticks, scale, formatter, limits_min, limits_max + ) + if show_units + ticklabels = ticklabels .* "myunit" + end + return ticknumbers, ticklabels 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" +# Optional + +# 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: +function Makie.expand_dimensions(::PointBased, y::AbstractVector{<:MyUnit}) + return keys(y.values), y +end + +# This function should return an Observable which acts as an update trigger for +# axis ticks and labels. For example, UnitfulConversion returns the unit +# observable here to trigger updates when the unit changes. +Makie.needs_tick_update_observable(conversion::MyDimConversion) = nothing + +# Handles the resolution of `ax.x/y/z_unit_in_ticklabel = automatic`. +# The result is passed to `get_ticks(..., show_unit)`. +Makie.show_dim_convert_in_ticklabel(::MyDimConversion) = true + +# Handles the resolution of `ax.x/y/z_unit_in_label = automatic`. +Makie.show_dim_convert_in_axis_label(::MyDimConversion) = true + +# Generates axis labels for the conversion. +# This is called when `x/y/z_unit_in_label` resolves to true, and defaults to +# an error when not implemented. +# `format` contains the content of x/y/zlabel_suffix +# `use_short_units` is a bool for switching to a long representation of a unit +function Makie.get_label_suffix(::MyDimConversion, format, use_short_units) + # Here we just use "my unit" as the unit string for the label and apply + # the passed formatter. + return Makie.apply_format("my unit", format) end barplot([MyUnit(1), MyUnit(2), MyUnit(3)], 1:3) @@ -176,5 +389,3 @@ barplot([MyUnit(1), MyUnit(2), MyUnit(3)], 1:3) 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/docs/src/explanations/recipes.md b/docs/src/explanations/recipes.md index 34b58751098..0ce0cbc6c31 100644 --- a/docs/src/explanations/recipes.md +++ b/docs/src/explanations/recipes.md @@ -107,7 +107,7 @@ myplot(args...; kw_args...) = ... myplot!(args...; kw_args...) = ... ``` -#### Argument Names +#### Converted Argument Names A specialization of `argument_names` is emitted if you have an argument list provided to the recipe macro. Otherwise a set of default names `Symbol(:converted_, i)` is used. @@ -123,6 +123,41 @@ If you leave out the `(x, y, z)` the default version of `argument_names` will pr To get the unconverted arguments of the plot, i.e. the `rand(10)` vectors, `plot_object.arg1` etc can be used. Unlike converted argument, their names can not be changed. +#### Converted Argument Types + +Converted arguments can also be typed. +This will mainly have two effects. +The first is that plot construction now checks that the type after `convert_arguments()` matches the types and layout given in the recipe. +I.e. it improves error checking. +The second is that Makie can now differentiate if a conversion has finished converting. +This is used in conjunction with dim converts when a plot is part of a recipe. +In that case a finished conversion is treated as already dim-converted while an unfinished or unknown conversion state is treated as (potentially) needing dim converts. + +To set up target types in a recipe, they are simply added as type annotations to the converted argument names: + +```julia +@recipe MyPlot (x::Vector{<:Real}, y::Vector{<:Real}, z::Matrix{<:Real}) begin + ... +end +``` + +An alternative option is to define the types for a conversion trait. + +```julia +function Makie.types_for_plot_arguments(::MyConversionTrait) + return Tuple{Convert1Type, Converted2Type, ...} +end +``` + +Note that these methods are only considered when `@recipe` does not specify types. +If both are needed one can overload this method instead: + +```julia +function Makie.types_for_plot_arguments(::Type{<:MyPlot}, ::MyConversionTrait) + return Tuple{Convert1Type, Converted2Type, ...} +end +``` + #### Recipe Attributes The attributes given in the body of `@recipe` define the possible attributes that can be passed to a plot as well as their default values. diff --git a/docs/src/tutorials/wrap-existing-recipe.md b/docs/src/tutorials/wrap-existing-recipe.md index 187fbe89145..72412bec97b 100644 --- a/docs/src/tutorials/wrap-existing-recipe.md +++ b/docs/src/tutorials/wrap-existing-recipe.md @@ -35,7 +35,9 @@ fields we have in the `MyHist` type basically tell us how to draw it as a BarPlo following method for this type of customization: ```@example recipe -Makie.convert_arguments(P::Type{<:BarPlot}, h::MyHist) = convert_arguments(P, h.bincenters, h.bincounts) +function Makie.convert_arguments(P::Type{<:BarPlot}, h::MyHist) + return convert_arguments(P, h.bincenters, h.bincounts) +end nothing # hide ``` @@ -49,16 +51,36 @@ barplot(h) The second recipe we want to customize for our `MyHist` type is the `Hist()` recipe. This cannot be achieved by `convert_arguments` as we did for `BarPlot()`, because normally `Makie.hist()` takes raw -data as input, but we already have the binned data in our `MyHist` type. +data as input instead of the already binned data in our `MyHist` type. The first thing one might try is to override the `plot!` method for `Hist` recipe: -```@figure recipe +```@example recipe function Makie.plot!(plot::Hist{<:Tuple{<:MyHist}}) barplot!(plot, plot[1]) plot end h = MyHist([1, 10, 100], 1:3) +try # hide +hist(h; color=:red, direction=:x) +catch e; showerror(stderr, e); end # hide +``` + +As you can see this produces error complaining about `MyHist` not converting to the correct type. +Any plot that includes typed converted arguments in `@recipe PlotName (converted1::Type, ...)` or defines `Makie.types_for_plot_arguments(::Trait)` for the plots conversion trait will fail like this. +To fix this we need introduce a conversion trait for `MyHist` and tell Makie that it is a valid conversion target, i.e. a valid input for the function we defined above. + +```@figure recipe +struct MyHistConversion <: Makie.ConversionTrait end +Makie.conversion_trait(::Type{<:Hist}, ::MyHist) = MyHistConversion() + +# Note: +# types_for_plot_arguments(::Trait) also exists, but will be ignored if +# the plot is typed via @recipe. This method will work in either case. +function Makie.types_for_plot_arguments(::Type{<:Hist}, ::MyHistConversion) + return Tuple{MyHist} +end + hist(h; color=:red, direction=:x) ```