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: