Merge pull request #3743 from t-bltg/ann

Allow passing collection of tuples to series_annotations
This commit is contained in:
t-bltg 2021-08-03 20:58:12 +02:00 committed by GitHub
commit 40b5df38f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 150 additions and 178 deletions

View File

@ -996,9 +996,9 @@ function processFontArg!(plotattributes::AKW, fontname::Symbol, arg)
elseif arg == :center
plotattributes[Symbol(fontname, :halign)] = :hcenter
plotattributes[Symbol(fontname, :valign)] = :vcenter
elseif arg in (:hcenter, :left, :right)
elseif arg _haligns
plotattributes[Symbol(fontname, :halign)] = arg
elseif arg in (:vcenter, :top, :bottom)
elseif arg _valigns
plotattributes[Symbol(fontname, :valign)] = arg
elseif T <: Colorant
plotattributes[Symbol(fontname, :color)] = arg

View File

@ -70,6 +70,7 @@ function text_size(lablen::Int, sz::Number, rot::Number = 0)
width, height
end
text_size(lab::AbstractString, sz::Number, rot::Number = 0) = text_size(length(lab), sz, rot)
text_size(lab::PlotText, sz::Number, rot::Number = 0) = text_size(length(lab.str), sz, rot)
# account for the size/length/rotation of tick labels
function tick_padding(sp::Subplot, axis::Axis)

View File

@ -1,10 +1,13 @@
const P2 = GeometryBasics.Point2{Float64}
const P3 = GeometryBasics.Point3{Float64}
nanpush!(a::AbstractVector{P2}, b) = (push!(a, P2(NaN,NaN)); push!(a, b))
nanappend!(a::AbstractVector{P2}, b) = (push!(a, P2(NaN,NaN)); append!(a, b))
nanpush!(a::AbstractVector{P3}, b) = (push!(a, P3(NaN,NaN,NaN)); push!(a, b))
nanappend!(a::AbstractVector{P3}, b) = (push!(a, P3(NaN,NaN,NaN)); append!(a, b))
const _haligns = :hcenter, :left, :right
const _valigns = :vcenter, :top, :bottom
nanpush!(a::AVec{P2}, b) = (push!(a, P2(NaN, NaN)); push!(a, b))
nanappend!(a::AVec{P2}, b) = (push!(a, P2(NaN, NaN)); append!(a, b))
nanpush!(a::AVec{P3}, b) = (push!(a, P3(NaN, NaN, NaN)); push!(a, b))
nanappend!(a::AVec{P3}, b) = (push!(a, P3(NaN, NaN, NaN)); append!(a, b))
compute_angle(v::P2) = (angle = atan(v[2], v[1]); angle < 0 ? 2π - angle : angle)
# -------------------------------------------------------------
@ -38,9 +41,7 @@ vertices(shape::Shape) = collect(zip(shape.x, shape.y))
@deprecate shape_coords coords
"return the vertex points from a Shape or Segments object"
function coords(shape::Shape)
shape.x, shape.y
end
coords(shape::Shape) = shape.x, shape.y
#coords(shapes::AVec{Shape}) = unzip(map(coords, shapes))
function coords(shapes::AVec{<:Shape})
@ -51,9 +52,9 @@ function coords(shapes::AVec{<:Shape})
end
"get an array of tuples of points on a circle with radius `r`"
function partialcircle(start_θ, end_θ, n = 20, r=1)
[(r*cos(u), r*sin(u)) for u in range(start_θ, stop=end_θ, length=n)]
end
partialcircle(start_θ, end_θ, n=20, r=1) = [
(r*cos(u), r*sin(u)) for u in range(start_θ, stop=end_θ, length=n)
]
"interleave 2 vectors into each other (like a zipper's teeth)"
function weave(x, y; ordering=Vector[x, y])
@ -81,10 +82,9 @@ function makestar(n; offset = -0.5, radius = 1.0)
end
"create a shape by picking points around the unit circle. `n` is the number of point/sides, `offset` is the starting angle"
function makeshape(n; offset = -0.5, radius = 1.0)
z = offset * π
Shape(partialcircle(z, z + 2π, n+1, radius))
end
makeshape(n; offset=-0.5, radius=1.0) = Shape(
partialcircle(offset * π, offset * π + 2π, n+1, radius)
)
function makecross(; offset=-0.5, radius=1.0)
z2 = offset * π
@ -97,11 +97,9 @@ end
from_polar(angle, dist) = P2(dist*cos(angle), dist*sin(angle))
function makearrowhead(angle; h = 2.0, w = 0.4)
tip = from_polar(angle, h)
Shape(P2[(0,0), from_polar(angle - 0.5π, w) - tip,
from_polar(angle + 0.5π, w) - tip, (0,0)])
end
makearrowhead(angle; h=2.0, w=0.4, tip=from_polar(angle, h)) = Shape(
P2[(0, 0), from_polar(angle - 0.5π, w) - tip, from_polar(angle + 0.5π, w) - tip, (0, 0)]
)
const _shapes = KW(
:circle => makeshape(20),
@ -121,7 +119,7 @@ const _shapes = KW(
:hline => Shape([(1, 0), (-1, 0)]),
)
for n in [4,5,6,7,8]
for n in 4:8
_shapes[Symbol("star$n")] = makestar(n)
end
@ -129,19 +127,18 @@ Shape(k::Symbol) = deepcopy(_shapes[k])
# -----------------------------------------------------------------------
# uses the centroid calculation from https://en.wikipedia.org/wiki/Centroid#Centroid_of_polygon
"return the centroid of a Shape"
function center(shape::Shape)
x, y = coords(shape)
n = length(x)
A, Cx, Cy = 0.0, 0.0, 0.0
for i=1:n
A, Cx, Cy = 0, 0, 0
for i 1:n
ip1 = i == n ? 1 : i+1
A += x[i] * y[ip1] - x[ip1] * y[i]
end
A *= 0.5
for i=1:n
for i 1:n
ip1 = i == n ? 1 : i+1
m = (x[i] * y[ip1] - x[ip1] * y[i])
Cx += (x[i] + x[ip1]) * m
@ -153,52 +150,46 @@ end
function scale!(shape::Shape, x::Real, y::Real=x, c=center(shape))
sx, sy = coords(shape)
cx, cy = c
for i=eachindex(sx)
for i eachindex(sx)
sx[i] = (sx[i] - cx) * x + cx
sy[i] = (sy[i] - cy) * y + cy
end
shape
end
function scale(shape::Shape, x::Real, y::Real = x, c = center(shape))
shapecopy = deepcopy(shape)
scale!(shapecopy, x, y, c)
end
scale(shape::Shape, x::Real, y::Real=x, c=center(shape)) = scale!(deepcopy(shape), x, y, c)
"translate a Shape in space"
function translate!(shape::Shape, x::Real, y::Real=x)
sx, sy = coords(shape)
for i=eachindex(sx)
for i eachindex(sx)
sx[i] += x
sy[i] += y
end
shape
end
function translate(shape::Shape, x::Real, y::Real = x)
shapecopy = deepcopy(shape)
translate!(shapecopy, x, y)
end
translate(shape::Shape, x::Real, y::Real=x) = translate!(deepcopy(shape), x, y)
function rotate_x(x::Real, y::Real, Θ::Real, centerx::Real, centery::Real)
rotate_x(x::Real, y::Real, Θ::Real, centerx::Real, centery::Real) = (
(x - centerx) * cos(Θ) - (y - centery) * sin(Θ) + centerx
end
)
function rotate_y(x::Real, y::Real, Θ::Real, centerx::Real, centery::Real)
rotate_y(x::Real, y::Real, Θ::Real, centerx::Real, centery::Real) = (
(y - centery) * cos(Θ) + (x - centerx) * sin(Θ) + centery
end
)
function rotate(x::Real, y::Real, θ::Real, c = center(shape))
cx, cy = c
rotate_x(x, y, Θ, cx, cy), rotate_y(x, y, Θ, cx, cy)
end
rotate(x::Real, y::Real, θ::Real, c=center(shape)) = (
rotate_x(x, y, Θ, c...),
rotate_y(x, y, Θ, c...),
)
function rotate!(shape::Shape, Θ::Real, c=center(shape))
x, y = coords(shape)
cx, cy = c
for i=eachindex(x)
xi = rotate_x(x[i], y[i], Θ, cx, cy)
yi = rotate_y(x[i], y[i], Θ, cx, cy)
for i eachindex(x)
xi = rotate_x(x[i], y[i], Θ, c...)
yi = rotate_y(x[i], y[i], Θ, c...)
x[i], y[i] = xi, yi
end
shape
@ -207,15 +198,13 @@ end
"rotate an object in space"
function rotate(shape::Shape, θ::Real, c=center(shape))
x, y = coords(shape)
cx, cy = c
x_new = rotate_x.(x, y, θ, cx, cy)
y_new = rotate_y.(x, y, θ, cx, cy)
x_new = rotate_x.(x, y, θ, c...)
y_new = rotate_y.(x, y, θ, c...)
Shape(x_new, y_new)
end
# -----------------------------------------------------------------------
mutable struct Font
family::AbstractString
pointsize::Int
@ -243,13 +232,12 @@ julia> font(family="serif",halign=:center,rotation=45.0)
```
"""
function font(args...; kw...)
# defaults
family = "sans-serif"
pointsize = 14
halign = :hcenter
valign = :vcenter
rotation = 0.0
rotation = 0
color = colorant"black"
for arg in args
@ -265,9 +253,9 @@ function font(args...;kw...)
elseif arg == :center
halign = :hcenter
valign = :vcenter
elseif arg in (:hcenter, :left, :right)
elseif arg _haligns
halign = arg
elseif arg in (:vcenter, :top, :bottom)
elseif arg _valigns
valign = arg
elseif T <: Colorant
color = arg
@ -282,33 +270,29 @@ function font(args...;kw...)
elseif typeof(arg) <: Real
rotation = convert(Float64, arg)
else
@warn("Unused font arg: $arg ($(typeof(arg)))")
@warn "Unused font arg: $arg ($(typeof(arg)))"
end
end
for symbol in keys(kw)
if symbol == :family
family = string(kw[:family])
elseif symbol == :pointsize
pointsize = kw[:pointsize]
elseif symbol == :halign
halign = kw[:halign]
if halign == :center
halign = :hcenter
end
@assert halign in (:hcenter, :left, :right)
elseif symbol == :valign
valign = kw[:valign]
if valign == :center
valign = :vcenter
end
@assert valign in (:vcenter, :top, :bottom)
elseif symbol == :rotation
rotation = kw[:rotation]
elseif symbol == :color
color = parse(Colorant, kw[:color])
for sym in keys(kw)
if sym == :family
family = string(kw[sym])
elseif sym == :pointsize
pointsize = kw[sym]
elseif sym == :halign
halign = kw[sym]
halign == :center && (halign = :hcenter)
@assert halign _haligns
elseif sym == :valign
valign = kw[sym]
valign == :center && (valign = :vcenter)
@assert valign _valigns
elseif sym == :rotation
rotation = kw[sym]
elseif sym == :color
color = parse(Colorant, kw[sym])
else
@warn("Unused font kwarg: $symbol")
@warn "Unused font kwarg: $sym"
end
end
@ -381,16 +365,12 @@ Create a PlotText object wrapping a string with font info, for plot annotations.
text(t::PlotText) = t
text(t::PlotText, font::Font) = PlotText(t.str, font)
text(str::AbstractString, f::Font) = PlotText(str, f)
function text(str, args...;kw...)
PlotText(string(str), font(args...;kw...))
end
text(str, args...; kw...) = PlotText(string(str), font(args...; kw...))
Base.length(t::PlotText) = length(t.str)
# -----------------------------------------------------------------------
# -----------------------------------------------------------------------
struct Stroke
width
color
@ -426,7 +406,7 @@ function stroke(args...; alpha = nothing)
elseif allReals(arg)
width = arg
else
@warn("Unused stroke arg: $arg ($(typeof(arg)))")
@warn "Unused stroke arg: $arg ($(typeof(arg)))"
end
end
@ -459,7 +439,7 @@ function brush(args...; alpha = nothing)
elseif allReals(arg)
size = arg
else
@warn("Unused brush arg: $arg ($(typeof(arg)))")
@warn "Unused brush arg: $arg ($(typeof(arg)))"
end
end
@ -469,33 +449,40 @@ end
# -----------------------------------------------------------------------
mutable struct SeriesAnnotations
strs::AbstractVector # the labels/names
strs::AVec # the labels/names
font::Font
baseshape::Union{Shape, AbstractVector{Shape}, Nothing}
baseshape::Union{Shape, AVec{Shape}, Nothing}
scalefactor::Tuple
end
_text_label(lab::Tuple, font) = text(lab[1], font, lab[2:end]...)
_text_label(lab::PlotText, font) = lab
_text_label(lab, font) = text(lab, font)
series_annotations(anns::AMat) = map(series_annotations, anns)
series_annotations(scalar) = series_annotations([scalar])
function series_annotations(anns::AMat)
map(series_annotations, anns)
end
function series_annotations(strs::AbstractVector, args...)
series_annotations(anns::SeriesAnnotations) = anns
series_annotations(::Nothing) = nothing
function series_annotations(strs::AVec, args...)
fnt = font()
shp = nothing
scalefactor = (1,1)
scalefactor = 1, 1
for arg in args
if isa(arg, Shape) || (isa(arg, AbstractVector) && eltype(arg) == Shape)
if isa(arg, Shape) || (isa(arg, AVec) && eltype(arg) == Shape)
shp = arg
elseif isa(arg, Font)
fnt = arg
elseif isa(arg, Symbol) && haskey(_shapes, arg)
shp = _shapes[arg]
elseif isa(arg, Number)
scalefactor = (arg,arg)
scalefactor = arg, arg
elseif is_2tuple(arg)
scalefactor = arg
elseif isa(arg, AVec)
strs = collect(zip(strs, arg))
else
@warn("Unused SeriesAnnotations arg: $arg ($(typeof(arg)))")
@warn "Unused SeriesAnnotations arg: $arg ($(typeof(arg)))"
end
end
# if scalefactor != 1
@ -503,16 +490,14 @@ function series_annotations(strs::AbstractVector, args...)
# scale!(s, scalefactor, scalefactor, (0, 0))
# end
# end
SeriesAnnotations(strs, fnt, shp, scalefactor)
SeriesAnnotations([_text_label(s, fnt) for s strs], fnt, shp, scalefactor)
end
series_annotations(anns::SeriesAnnotations) = anns
series_annotations(::Nothing) = nothing
function series_annotations_shapes!(series::Series, scaletype::Symbol=:pixels)
anns = series[:series_annotations]
# msw, msh = anns.scalefactor
# ms = series[:markersize]
# msw,msh = if isa(ms, AbstractVector)
# msw, msh = if isa(ms, AVec)
# 1, 1
# elseif is_2tuple(ms)
# ms
@ -527,7 +512,7 @@ function series_annotations_shapes!(series::Series, scaletype::Symbol = :pixels)
msw, msh = anns.scalefactor
msize = Float64[]
shapes = Vector{Shape}(undef, length(anns.strs))
for i in eachindex(anns.strs)
for i eachindex(anns.strs)
str = _cycle(anns.strs, i)
# get the width and height of the string (in mm)
@ -561,7 +546,7 @@ end
function Base.iterate(ea::EachAnn, i=1)
if ea.anns === nothing || isempty(ea.anns.strs) || i > length(ea.y)
return nothing
return
end
tmp = _cycle(ea.anns.strs, i)
@ -574,11 +559,11 @@ function Base.iterate(ea::EachAnn, i = 1)
end
# -----------------------------------------------------------------------
annotations(::Nothing) = []
annotations(anns::AVec) = anns
annotations(anns::AMat) = map(annotations, anns)
annotations(anns) = Any[anns]
annotations(sa::SeriesAnnotations) = sa
annotations(anns::AVec) = anns
annotations(anns) = Any[anns]
annotations(::Nothing) = []
_annotationfont(sp::Subplot) = Plots.font(;
family=sp[:annotationfontfamily],
@ -589,15 +574,12 @@ _annotationfont(sp::Subplot) = Plots.font(;
color=sp[:annotationcolor],
)
_annotation(sp, font, lab, pos...; alphabet="abcdefghijklmnopqrstuvwxyz") = (
if lab == :auto
(pos..., text("($(alphabet[sp[:subplot_index]]))", font))
else
(pos..., isa(lab, PlotText) ? lab : isa(lab, Tuple) ? text(lab[1], font, lab[2:end]...) : text(lab, font))
end
_annotation(sp::Subplot, font, lab, pos...; alphabet="abcdefghijklmnopqrstuvwxyz") = (
pos...,
lab == :auto ? text("($(alphabet[sp[:subplot_index]]))", font) : _text_label(lab, font)
)
# Expand arrays of coordinates, positions and labels into induvidual annotations
# Expand arrays of coordinates, positions and labels into individual annotations
# and make sure labels are of type PlotText
function process_annotation(sp::Subplot, xs, ys, labs, font=_annotationfont(sp))
anns = []
@ -623,8 +605,6 @@ function process_annotation(sp::Subplot, positions::Union{AVec{Symbol},Symbol,Tu
anns
end
process_any_label(lab, font=Font()) = lab isa Tuple ? text(lab...) : text(lab, font)
_relative_position(xmin, xmax, pos::Length{:pct}) = xmin + pos.value * (xmax - xmin)
# Give each annotation coordinates based on specified position
@ -693,8 +673,6 @@ end
# # I don't want to clash with ValidatedNumerics, but this would be nice:
# ..(a::T, b::T) = (a, b)
# -----------------------------------------------------------------------
# style is :open or :closed (for now)
@ -714,8 +692,7 @@ Define arrowheads to apply to lines - args are `style` (`:open` or `:closed`),
function arrow(args...)
style = :simple
side = :head
headlength = 0.3
headwidth = 0.3
headlength = headwidth = 0.3
setlength = false
for arg in args
T = typeof(arg)
@ -735,7 +712,7 @@ function arrow(args...)
elseif T <: Tuple && length(arg) == 2
headlength, headwidth = Float64(arg[1]), Float64(arg[2])
else
@warn("Skipped arrow arg $arg")
@warn "Skipped arrow arg $arg"
end
end
Arrow(style, side, headlength, headwidth)
@ -745,7 +722,7 @@ end
# allow for do-block notation which gets called on every valid start/end pair which
# we need to draw an arrow
function add_arrows(func::Function, x::AVec, y::AVec)
for i=2:length(x)
for i 2:length(x)
xyprev = (x[i-1], y[i-1])
xy = (x[i], y[i])
if ok(xyprev) && ok(xy)
@ -774,13 +751,9 @@ end
@deprecate curve_points coords
coords(curve::BezierCurve, n::Integer = 30; range = [0,1]) = map(curve, Base.range(first(range), stop=last(range), length=n))
# build a BezierCurve which leaves point p vertically upwards and arrives point q vertically upwards.
# may create a loop if necessary. Assumes the view is [0,1]
function directed_curve(args...; kw...)
error("directed_curve has been moved to PlotRecipes")
end
coords(curve::BezierCurve, n::Integer=30; range=[0, 1]) = map(
curve, Base.range(first(range), stop=last(range), length=n)
)
function extrema_plus_buffer(v, buffmult=0.2)
vmin, vmax = ignorenan_extrema(v)

View File

@ -116,7 +116,7 @@ end
end
@testset "Series Annotations" begin
square = Shape([(0, 0), (1, 0), (1, 1), (0, 1)])
square = Shape([(0., 0.), (1., 0.), (1., 1.), (0., 1.)])
@test_logs (:warn, "Unused SeriesAnnotations arg: triangle (Symbol)") begin
p = plot(
[1, 2, 3],
@ -130,7 +130,7 @@ end
),
)
sa = p.series_list[1].plotattributes[:series_annotations]
@test sa.strs == ["a"]
@test only(sa.strs).str == "a"
@test sa.font.family == "courier"
@test sa.baseshape == square
@test sa.scalefactor == (1, 4)
@ -143,19 +143,17 @@ end
xlims = (0, 5),
series_annotations = permutedims([["1/1"], ["1/2"], ["1/3"], ["1/4"], ["1/5"]]),
)
@test spl.series_list[1].plotattributes[:series_annotations].strs == ["1/1"]
@test spl.series_list[2].plotattributes[:series_annotations].strs == ["1/2"]
@test spl.series_list[3].plotattributes[:series_annotations].strs == ["1/3"]
@test spl.series_list[4].plotattributes[:series_annotations].strs == ["1/4"]
@test spl.series_list[5].plotattributes[:series_annotations].strs == ["1/5"]
for i 1:5
@test only(spl.series_list[i].plotattributes[:series_annotations].strs).str == "1/$i"
end
p = plot([1, 2], annotations=(1.5, 2, text("foo", :left)))
x, y, txt = p.subplots[end][:annotations][end]
x, y, txt = only(p.subplots[end][:annotations])
@test (x, y) == (1.5, 2)
@test txt.str == "foo"
p = plot([1, 2], annotations=((.1, .5), :auto))
pos, txt = p.subplots[end][:annotations][end]
pos, txt = only(p.subplots[end][:annotations])
@test pos == (.1, .5)
@test txt.str == "(a)"
end