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 elseif arg == :center
plotattributes[Symbol(fontname, :halign)] = :hcenter plotattributes[Symbol(fontname, :halign)] = :hcenter
plotattributes[Symbol(fontname, :valign)] = :vcenter plotattributes[Symbol(fontname, :valign)] = :vcenter
elseif arg in (:hcenter, :left, :right) elseif arg _haligns
plotattributes[Symbol(fontname, :halign)] = arg plotattributes[Symbol(fontname, :halign)] = arg
elseif arg in (:vcenter, :top, :bottom) elseif arg _valigns
plotattributes[Symbol(fontname, :valign)] = arg plotattributes[Symbol(fontname, :valign)] = arg
elseif T <: Colorant elseif T <: Colorant
plotattributes[Symbol(fontname, :color)] = arg plotattributes[Symbol(fontname, :color)] = arg

View File

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

View File

@ -1,10 +1,13 @@
const P2 = GeometryBasics.Point2{Float64} const P2 = GeometryBasics.Point2{Float64}
const P3 = GeometryBasics.Point3{Float64} const P3 = GeometryBasics.Point3{Float64}
nanpush!(a::AbstractVector{P2}, b) = (push!(a, P2(NaN,NaN)); push!(a, b)) const _haligns = :hcenter, :left, :right
nanappend!(a::AbstractVector{P2}, b) = (push!(a, P2(NaN,NaN)); append!(a, b)) const _valigns = :vcenter, :top, :bottom
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)) 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) 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 @deprecate shape_coords coords
"return the vertex points from a Shape or Segments object" "return the vertex points from a Shape or Segments object"
function coords(shape::Shape) coords(shape::Shape) = shape.x, shape.y
shape.x, shape.y
end
#coords(shapes::AVec{Shape}) = unzip(map(coords, shapes)) #coords(shapes::AVec{Shape}) = unzip(map(coords, shapes))
function coords(shapes::AVec{<:Shape}) function coords(shapes::AVec{<:Shape})
@ -51,12 +52,12 @@ function coords(shapes::AVec{<:Shape})
end end
"get an array of tuples of points on a circle with radius `r`" "get an array of tuples of points on a circle with radius `r`"
function partialcircle(start_θ, end_θ, n = 20, r=1) partialcircle(start_θ, end_θ, n=20, r=1) = [
[(r*cos(u), r*sin(u)) for u in range(start_θ, stop=end_θ, length=n)] (r*cos(u), r*sin(u)) for u in range(start_θ, stop=end_θ, length=n)
end ]
"interleave 2 vectors into each other (like a zipper's teeth)" "interleave 2 vectors into each other (like a zipper's teeth)"
function weave(x,y; ordering = Vector[x,y]) function weave(x, y; ordering=Vector[x, y])
ret = eltype(x)[] ret = eltype(x)[]
done = false done = false
while !done while !done
@ -72,7 +73,7 @@ function weave(x,y; ordering = Vector[x,y])
end end
"create a star by weaving together points from an outer and inner circle. `n` is the number of arms" "create a star by weaving together points from an outer and inner circle. `n` is the number of arms"
function makestar(n; offset = -0.5, radius = 1.0) function makestar(n; offset=-0.5, radius=1.0)
z1 = offset * π z1 = offset * π
z2 = z1 + π / (n) z2 = z1 + π / (n)
outercircle = partialcircle(z1, z1 + 2π, n+1, radius) outercircle = partialcircle(z1, z1 + 2π, n+1, radius)
@ -81,27 +82,24 @@ function makestar(n; offset = -0.5, radius = 1.0)
end end
"create a shape by picking points around the unit circle. `n` is the number of point/sides, `offset` is the starting angle" "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) makeshape(n; offset=-0.5, radius=1.0) = Shape(
z = offset * π partialcircle(offset * π, offset * π + 2π, n+1, radius)
Shape(partialcircle(z, z + 2π, n+1, radius)) )
end
function makecross(; offset = -0.5, radius = 1.0) function makecross(; offset=-0.5, radius=1.0)
z2 = offset * π z2 = offset * π
z1 = z2 - π/8 z1 = z2 - π/8
outercircle = partialcircle(z1, z1 + 2π, 9, radius) outercircle = partialcircle(z1, z1 + 2π, 9, radius)
innercircle = partialcircle(z2, z2 + 2π, 5, 0.5radius) innercircle = partialcircle(z2, z2 + 2π, 5, 0.5radius)
Shape(weave(outercircle, innercircle, Shape(weave(outercircle, innercircle,
ordering=Vector[outercircle,innercircle,outercircle])) ordering=Vector[outercircle, innercircle, outercircle]))
end end
from_polar(angle, dist) = P2(dist*cos(angle), dist*sin(angle)) from_polar(angle, dist) = P2(dist*cos(angle), dist*sin(angle))
function makearrowhead(angle; h = 2.0, w = 0.4) makearrowhead(angle; h=2.0, w=0.4, tip=from_polar(angle, h)) = Shape(
tip = from_polar(angle, h) P2[(0, 0), from_polar(angle - 0.5π, w) - tip, from_polar(angle + 0.5π, w) - tip, (0, 0)]
Shape(P2[(0,0), from_polar(angle - 0.5π, w) - tip, )
from_polar(angle + 0.5π, w) - tip, (0,0)])
end
const _shapes = KW( const _shapes = KW(
:circle => makeshape(20), :circle => makeshape(20),
@ -117,11 +115,11 @@ const _shapes = KW(
:octagon => makeshape(8), :octagon => makeshape(8),
:cross => makecross(offset=-0.25), :cross => makecross(offset=-0.25),
:xcross => makecross(), :xcross => makecross(),
:vline => Shape([(0,1),(0,-1)]), :vline => Shape([(0, 1), (0, -1)]),
:hline => Shape([(1,0),(-1,0)]), :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) _shapes[Symbol("star$n")] = makestar(n)
end end
@ -129,20 +127,19 @@ Shape(k::Symbol) = deepcopy(_shapes[k])
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# uses the centroid calculation from https://en.wikipedia.org/wiki/Centroid#Centroid_of_polygon # uses the centroid calculation from https://en.wikipedia.org/wiki/Centroid#Centroid_of_polygon
"return the centroid of a Shape" "return the centroid of a Shape"
function center(shape::Shape) function center(shape::Shape)
x, y = coords(shape) x, y = coords(shape)
n = length(x) n = length(x)
A, Cx, Cy = 0.0, 0.0, 0.0 A, Cx, Cy = 0, 0, 0
for i=1:n for i 1:n
ip1 = i==n ? 1 : i+1 ip1 = i == n ? 1 : i+1
A += x[i] * y[ip1] - x[ip1] * y[i] A += x[i] * y[ip1] - x[ip1] * y[i]
end end
A *= 0.5 A *= 0.5
for i=1:n for i 1:n
ip1 = i==n ? 1 : i+1 ip1 = i == n ? 1 : i+1
m = (x[i] * y[ip1] - x[ip1] * y[i]) m = (x[i] * y[ip1] - x[ip1] * y[i])
Cx += (x[i] + x[ip1]) * m Cx += (x[i] + x[ip1]) * m
Cy += (y[i] + y[ip1]) * m Cy += (y[i] + y[ip1]) * m
@ -150,72 +147,64 @@ function center(shape::Shape)
Cx / 6A, Cy / 6A Cx / 6A, Cy / 6A
end end
function scale!(shape::Shape, x::Real, y::Real = x, c = center(shape)) function scale!(shape::Shape, x::Real, y::Real=x, c=center(shape))
sx, sy = coords(shape) sx, sy = coords(shape)
cx, cy = c cx, cy = c
for i=eachindex(sx) for i eachindex(sx)
sx[i] = (sx[i] - cx) * x + cx sx[i] = (sx[i] - cx) * x + cx
sy[i] = (sy[i] - cy) * y + cy sy[i] = (sy[i] - cy) * y + cy
end end
shape shape
end end
function scale(shape::Shape, x::Real, y::Real = x, c = center(shape)) scale(shape::Shape, x::Real, y::Real=x, c=center(shape)) = scale!(deepcopy(shape), x, y, c)
shapecopy = deepcopy(shape)
scale!(shapecopy, x, y, c)
end
"translate a Shape in space" "translate a Shape in space"
function translate!(shape::Shape, x::Real, y::Real = x) function translate!(shape::Shape, x::Real, y::Real=x)
sx, sy = coords(shape) sx, sy = coords(shape)
for i=eachindex(sx) for i eachindex(sx)
sx[i] += x sx[i] += x
sy[i] += y sy[i] += y
end end
shape shape
end end
function translate(shape::Shape, x::Real, y::Real = x) translate(shape::Shape, x::Real, y::Real=x) = translate!(deepcopy(shape), x, y)
shapecopy = deepcopy(shape)
translate!(shapecopy, x, y)
end
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 (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 (y - centery) * cos(Θ) + (x - centerx) * sin(Θ) + centery
end )
function rotate(x::Real, y::Real, θ::Real, c = center(shape)) rotate(x::Real, y::Real, θ::Real, c=center(shape)) = (
cx, cy = c rotate_x(x, y, Θ, c...),
rotate_x(x, y, Θ, cx, cy), rotate_y(x, y, Θ, cx, cy) rotate_y(x, y, Θ, c...),
end )
function rotate!(shape::Shape, Θ::Real, c = center(shape)) function rotate!(shape::Shape, Θ::Real, c=center(shape))
x, y = coords(shape) x, y = coords(shape)
cx, cy = c for i eachindex(x)
for i=eachindex(x) xi = rotate_x(x[i], y[i], Θ, c...)
xi = rotate_x(x[i], y[i], Θ, cx, cy) yi = rotate_y(x[i], y[i], Θ, c...)
yi = rotate_y(x[i], y[i], Θ, cx, cy)
x[i], y[i] = xi, yi x[i], y[i] = xi, yi
end end
shape shape
end end
"rotate an object in space" "rotate an object in space"
function rotate(shape::Shape, θ::Real, c = center(shape)) function rotate(shape::Shape, θ::Real, c=center(shape))
x, y = coords(shape) x, y = coords(shape)
cx, cy = c x_new = rotate_x.(x, y, θ, c...)
x_new = rotate_x.(x, y, θ, cx, cy) y_new = rotate_y.(x, y, θ, c...)
y_new = rotate_y.(x, y, θ, cx, cy)
Shape(x_new, y_new) Shape(x_new, y_new)
end end
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
mutable struct Font mutable struct Font
family::AbstractString family::AbstractString
pointsize::Int pointsize::Int
@ -239,17 +228,16 @@ arguments (which are distinguished by type/value) or as keyword arguments.
# Examples # Examples
```julia-repl ```julia-repl
julia> font(8) julia> font(8)
julia> font(family="serif",halign=:center,rotation=45.0) julia> font(family="serif", halign=:center, rotation=45.0)
``` ```
""" """
function font(args...;kw...) function font(args...; kw...)
# defaults # defaults
family = "sans-serif" family = "sans-serif"
pointsize = 14 pointsize = 14
halign = :hcenter halign = :hcenter
valign = :vcenter valign = :vcenter
rotation = 0.0 rotation = 0
color = colorant"black" color = colorant"black"
for arg in args for arg in args
@ -265,9 +253,9 @@ function font(args...;kw...)
elseif arg == :center elseif arg == :center
halign = :hcenter halign = :hcenter
valign = :vcenter valign = :vcenter
elseif arg in (:hcenter, :left, :right) elseif arg _haligns
halign = arg halign = arg
elseif arg in (:vcenter, :top, :bottom) elseif arg _valigns
valign = arg valign = arg
elseif T <: Colorant elseif T <: Colorant
color = arg color = arg
@ -282,33 +270,29 @@ function font(args...;kw...)
elseif typeof(arg) <: Real elseif typeof(arg) <: Real
rotation = convert(Float64, arg) rotation = convert(Float64, arg)
else else
@warn("Unused font arg: $arg ($(typeof(arg)))") @warn "Unused font arg: $arg ($(typeof(arg)))"
end end
end end
for symbol in keys(kw) for sym in keys(kw)
if symbol == :family if sym == :family
family = string(kw[:family]) family = string(kw[sym])
elseif symbol == :pointsize elseif sym == :pointsize
pointsize = kw[:pointsize] pointsize = kw[sym]
elseif symbol == :halign elseif sym == :halign
halign = kw[:halign] halign = kw[sym]
if halign == :center halign == :center && (halign = :hcenter)
halign = :hcenter @assert halign _haligns
end elseif sym == :valign
@assert halign in (:hcenter, :left, :right) valign = kw[sym]
elseif symbol == :valign valign == :center && (valign = :vcenter)
valign = kw[:valign] @assert valign _valigns
if valign == :center elseif sym == :rotation
valign = :vcenter rotation = kw[sym]
end elseif sym == :color
@assert valign in (:vcenter, :top, :bottom) color = parse(Colorant, kw[sym])
elseif symbol == :rotation
rotation = kw[:rotation]
elseif symbol == :color
color = parse(Colorant, kw[:color])
else else
@warn("Unused font kwarg: $symbol") @warn "Unused font kwarg: $sym"
end end
end end
@ -331,7 +315,7 @@ function scalefontsizes(factor::Number)
scalefontsize(k, factor) scalefontsize(k, factor)
end end
for letter in (:x,:y,:z) for letter in (:x, :y, :z)
for k in keys(_initial_ax_fontsizes) for k in keys(_initial_ax_fontsizes)
scalefontsize(Symbol(letter, k), factor) scalefontsize(Symbol(letter, k), factor)
end end
@ -352,7 +336,7 @@ function scalefontsizes()
end end
end end
for letter in (:x,:y,:z) for letter in (:x, :y, :z)
for k in keys(_initial_ax_fontsizes) for k in keys(_initial_ax_fontsizes)
if k in keys(_initial_fontsizes) if k in keys(_initial_fontsizes)
f = default(Symbol(letter, k)) f = default(Symbol(letter, k))
@ -381,16 +365,12 @@ Create a PlotText object wrapping a string with font info, for plot annotations.
text(t::PlotText) = t text(t::PlotText) = t
text(t::PlotText, font::Font) = PlotText(t.str, font) text(t::PlotText, font::Font) = PlotText(t.str, font)
text(str::AbstractString, f::Font) = PlotText(str, f) text(str::AbstractString, f::Font) = PlotText(str, f)
function text(str, args...;kw...) text(str, args...; kw...) = PlotText(string(str), font(args...; kw...))
PlotText(string(str), font(args...;kw...))
end
Base.length(t::PlotText) = length(t.str) Base.length(t::PlotText) = length(t.str)
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# -----------------------------------------------------------------------
struct Stroke struct Stroke
width width
color color
@ -403,7 +383,7 @@ end
Define the properties of the stroke used in plotting lines Define the properties of the stroke used in plotting lines
""" """
function stroke(args...; alpha = nothing) function stroke(args...; alpha=nothing)
width = 1 width = 1
color = :black color = :black
style = :solid style = :solid
@ -426,7 +406,7 @@ function stroke(args...; alpha = nothing)
elseif allReals(arg) elseif allReals(arg)
width = arg width = arg
else else
@warn("Unused stroke arg: $arg ($(typeof(arg)))") @warn "Unused stroke arg: $arg ($(typeof(arg)))"
end end
end end
@ -440,7 +420,7 @@ struct Brush
alpha alpha
end end
function brush(args...; alpha = nothing) function brush(args...; alpha=nothing)
size = 1 size = 1
color = :black color = :black
@ -459,7 +439,7 @@ function brush(args...; alpha = nothing)
elseif allReals(arg) elseif allReals(arg)
size = arg size = arg
else else
@warn("Unused brush arg: $arg ($(typeof(arg)))") @warn "Unused brush arg: $arg ($(typeof(arg)))"
end end
end end
@ -469,72 +449,77 @@ end
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
mutable struct SeriesAnnotations mutable struct SeriesAnnotations
strs::AbstractVector # the labels/names strs::AVec # the labels/names
font::Font font::Font
baseshape::Union{Shape, AbstractVector{Shape}, Nothing} baseshape::Union{Shape, AVec{Shape}, Nothing}
scalefactor::Tuple scalefactor::Tuple
end 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]) series_annotations(scalar) = series_annotations([scalar])
function series_annotations(anns::AMat) series_annotations(anns::SeriesAnnotations) = anns
map(series_annotations, anns) series_annotations(::Nothing) = nothing
end
function series_annotations(strs::AbstractVector, args...) function series_annotations(strs::AVec, args...)
fnt = font() fnt = font()
shp = nothing shp = nothing
scalefactor = (1,1) scalefactor = 1, 1
for arg in args 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 shp = arg
elseif isa(arg, Font) elseif isa(arg, Font)
fnt = arg fnt = arg
elseif isa(arg, Symbol) && haskey(_shapes, arg) elseif isa(arg, Symbol) && haskey(_shapes, arg)
shp = _shapes[arg] shp = _shapes[arg]
elseif isa(arg, Number) elseif isa(arg, Number)
scalefactor = (arg,arg) scalefactor = arg, arg
elseif is_2tuple(arg) elseif is_2tuple(arg)
scalefactor = arg scalefactor = arg
elseif isa(arg, AVec)
strs = collect(zip(strs, arg))
else else
@warn("Unused SeriesAnnotations arg: $arg ($(typeof(arg)))") @warn "Unused SeriesAnnotations arg: $arg ($(typeof(arg)))"
end end
end end
# if scalefactor != 1 # if scalefactor != 1
# for s in get(shp) # for s in get(shp)
# scale!(s, scalefactor, scalefactor, (0,0)) # scale!(s, scalefactor, scalefactor, (0, 0))
# end # end
# end # end
SeriesAnnotations(strs, fnt, shp, scalefactor) SeriesAnnotations([_text_label(s, fnt) for s strs], fnt, shp, scalefactor)
end end
series_annotations(anns::SeriesAnnotations) = anns
series_annotations(::Nothing) = nothing
function series_annotations_shapes!(series::Series, scaletype::Symbol = :pixels) function series_annotations_shapes!(series::Series, scaletype::Symbol=:pixels)
anns = series[:series_annotations] anns = series[:series_annotations]
# msw,msh = anns.scalefactor # msw, msh = anns.scalefactor
# ms = series[:markersize] # ms = series[:markersize]
# msw,msh = if isa(ms, AbstractVector) # msw, msh = if isa(ms, AVec)
# 1,1 # 1, 1
# elseif is_2tuple(ms) # elseif is_2tuple(ms)
# ms # ms
# else # else
# ms,ms # ms, ms
# end # end
# @show msw msh # @show msw msh
if anns !== nothing && anns.baseshape !== nothing if anns !== nothing && anns.baseshape !== nothing
# we use baseshape to overwrite the markershape attribute # we use baseshape to overwrite the markershape attribute
# with a list of custom shapes for each # with a list of custom shapes for each
msw,msh = anns.scalefactor msw, msh = anns.scalefactor
msize = Float64[] msize = Float64[]
shapes = Vector{Shape}(undef, length(anns.strs)) shapes = Vector{Shape}(undef, length(anns.strs))
for i in eachindex(anns.strs) for i eachindex(anns.strs)
str = _cycle(anns.strs,i) str = _cycle(anns.strs, i)
# get the width and height of the string (in mm) # get the width and height of the string (in mm)
sw, sh = text_size(str, anns.font.pointsize) sw, sh = text_size(str, anns.font.pointsize)
# how much to scale the base shape? # how much to scale the base shape?
# note: it's a rough assumption that the shape fills the unit box [-1,-1,1,1], # note: it's a rough assumption that the shape fills the unit box [-1, -1, 1, 1],
# so we scale the length-2 shape by 1/2 the total length # so we scale the length-2 shape by 1/2 the total length
scalar = (backend() == PyPlotBackend() ? 1.7 : 1.0) scalar = (backend() == PyPlotBackend() ? 1.7 : 1.0)
xscale = 0.5to_pixels(sw) * scalar xscale = 0.5to_pixels(sw) * scalar
@ -545,7 +530,7 @@ function series_annotations_shapes!(series::Series, scaletype::Symbol = :pixels)
maxscale = max(xscale, yscale) maxscale = max(xscale, yscale)
push!(msize, maxscale) push!(msize, maxscale)
baseshape = _cycle(anns.baseshape, i) baseshape = _cycle(anns.baseshape, i)
shapes[i] = scale(baseshape, msw*xscale/maxscale, msh*yscale/maxscale, (0,0)) shapes[i] = scale(baseshape, msw*xscale/maxscale, msh*yscale/maxscale, (0, 0))
end end
series[:markershape] = shapes series[:markershape] = shapes
series[:markersize] = msize series[:markersize] = msize
@ -559,26 +544,26 @@ mutable struct EachAnn
y y
end end
function Base.iterate(ea::EachAnn, i = 1) function Base.iterate(ea::EachAnn, i=1)
if ea.anns === nothing || isempty(ea.anns.strs) || i > length(ea.y) if ea.anns === nothing || isempty(ea.anns.strs) || i > length(ea.y)
return nothing return
end end
tmp = _cycle(ea.anns.strs,i) tmp = _cycle(ea.anns.strs, i)
str,fnt = if isa(tmp, PlotText) str, fnt = if isa(tmp, PlotText)
tmp.str, tmp.font tmp.str, tmp.font
else else
tmp, ea.anns.font tmp, ea.anns.font
end end
((_cycle(ea.x,i), _cycle(ea.y,i), str, fnt), i+1) ((_cycle(ea.x, i), _cycle(ea.y, i), str, fnt), i+1)
end end
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
annotations(::Nothing) = []
annotations(anns::AVec) = anns
annotations(anns::AMat) = map(annotations, anns) annotations(anns::AMat) = map(annotations, anns)
annotations(anns) = Any[anns]
annotations(sa::SeriesAnnotations) = sa annotations(sa::SeriesAnnotations) = sa
annotations(anns::AVec) = anns
annotations(anns) = Any[anns]
annotations(::Nothing) = []
_annotationfont(sp::Subplot) = Plots.font(; _annotationfont(sp::Subplot) = Plots.font(;
family=sp[:annotationfontfamily], family=sp[:annotationfontfamily],
@ -589,15 +574,12 @@ _annotationfont(sp::Subplot) = Plots.font(;
color=sp[:annotationcolor], color=sp[:annotationcolor],
) )
_annotation(sp, font, lab, pos...; alphabet="abcdefghijklmnopqrstuvwxyz") = ( _annotation(sp::Subplot, font, lab, pos...; alphabet="abcdefghijklmnopqrstuvwxyz") = (
if lab == :auto pos...,
(pos..., text("($(alphabet[sp[:subplot_index]]))", font)) lab == :auto ? text("($(alphabet[sp[:subplot_index]]))", font) : _text_label(lab, font)
else
(pos..., isa(lab, PlotText) ? lab : isa(lab, Tuple) ? text(lab[1], font, lab[2:end]...) : text(lab, font))
end
) )
# 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 # and make sure labels are of type PlotText
function process_annotation(sp::Subplot, xs, ys, labs, font=_annotationfont(sp)) function process_annotation(sp::Subplot, xs, ys, labs, font=_annotationfont(sp))
anns = [] anns = []
@ -613,7 +595,7 @@ function process_annotation(sp::Subplot, xs, ys, labs, font=_annotationfont(sp))
anns anns
end end
function process_annotation(sp::Subplot, positions::Union{AVec{Symbol},Symbol,Tuple}, labs, font=_annotationfont(sp)) function process_annotation(sp::Subplot, positions::Union{AVec{Symbol}, Symbol, Tuple}, labs, font=_annotationfont(sp))
anns = [] anns = []
positions, labs = makevec(positions), makevec(labs) positions, labs = makevec(positions), makevec(labs)
for i in 1:max(length(positions), length(labs)) for i in 1:max(length(positions), length(labs))
@ -623,14 +605,12 @@ function process_annotation(sp::Subplot, positions::Union{AVec{Symbol},Symbol,Tu
anns anns
end 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) _relative_position(xmin, xmax, pos::Length{:pct}) = xmin + pos.value * (xmax - xmin)
# Give each annotation coordinates based on specified position # Give each annotation coordinates based on specified position
function locate_annotation( function locate_annotation(
sp::Subplot, pos::Symbol, label::PlotText; sp::Subplot, pos::Symbol, label::PlotText;
position_multiplier=Dict{Symbol, Tuple{Float64,Float64}}( position_multiplier=Dict{Symbol, Tuple{Float64, Float64}}(
:topleft => (0.1pct, 0.9pct), :topleft => (0.1pct, 0.9pct),
:topcenter => (0.5pct, 0.9pct), :topcenter => (0.5pct, 0.9pct),
:topright => (0.9pct, 0.9pct), :topright => (0.9pct, 0.9pct),
@ -649,12 +629,12 @@ end
locate_annotation(sp::Subplot, x, y, label::PlotText) = (x, y, label) locate_annotation(sp::Subplot, x, y, label::PlotText) = (x, y, label)
locate_annotation(sp::Subplot, x, y, z, label::PlotText) = (x, y, z, label) locate_annotation(sp::Subplot, x, y, z, label::PlotText) = (x, y, z, label)
locate_annotation(sp::Subplot, rel::NTuple{2,<:Number}, label::PlotText) = ( locate_annotation(sp::Subplot, rel::NTuple{2, <:Number}, label::PlotText) = (
_relative_position(axis_limits(sp, :x)..., rel[1] * Plots.pct), _relative_position(axis_limits(sp, :x)..., rel[1] * Plots.pct),
_relative_position(axis_limits(sp, :y)..., rel[2] * Plots.pct), _relative_position(axis_limits(sp, :y)..., rel[2] * Plots.pct),
label label
) )
locate_annotation(sp::Subplot, rel::NTuple{3,<:Number}, label::PlotText) = ( locate_annotation(sp::Subplot, rel::NTuple{3, <:Number}, label::PlotText) = (
_relative_position(axis_limits(sp, :x)..., rel[1] * Plots.pct), _relative_position(axis_limits(sp, :x)..., rel[1] * Plots.pct),
_relative_position(axis_limits(sp, :y)..., rel[2] * Plots.pct), _relative_position(axis_limits(sp, :y)..., rel[2] * Plots.pct),
_relative_position(axis_limits(sp, :z)..., rel[3] * Plots.pct), _relative_position(axis_limits(sp, :z)..., rel[3] * Plots.pct),
@ -665,10 +645,10 @@ locate_annotation(sp::Subplot, rel::NTuple{3,<:Number}, label::PlotText) = (
"type which represents z-values for colors and sizes (and anything else that might come up)" "type which represents z-values for colors and sizes (and anything else that might come up)"
struct ZValues struct ZValues
values::Vector{Float64} values::Vector{Float64}
zrange::Tuple{Float64,Float64} zrange::Tuple{Float64, Float64}
end end
function zvalues(values::AVec{T}, zrange::Tuple{T,T} = (ignorenan_minimum(values), ignorenan_maximum(values))) where T<:Real function zvalues(values::AVec{T}, zrange::Tuple{T, T}=(ignorenan_minimum(values), ignorenan_maximum(values))) where T<:Real
ZValues(collect(float(values)), map(Float64, zrange)) ZValues(collect(float(values)), map(Float64, zrange))
end end
@ -691,9 +671,7 @@ end
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# # I don't want to clash with ValidatedNumerics, but this would be nice: # # I don't want to clash with ValidatedNumerics, but this would be nice:
# ..(a::T, b::T) = (a,b) # ..(a::T, b::T) = (a, b)
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
@ -714,8 +692,7 @@ Define arrowheads to apply to lines - args are `style` (`:open` or `:closed`),
function arrow(args...) function arrow(args...)
style = :simple style = :simple
side = :head side = :head
headlength = 0.3 headlength = headwidth = 0.3
headwidth = 0.3
setlength = false setlength = false
for arg in args for arg in args
T = typeof(arg) T = typeof(arg)
@ -735,7 +712,7 @@ function arrow(args...)
elseif T <: Tuple && length(arg) == 2 elseif T <: Tuple && length(arg) == 2
headlength, headwidth = Float64(arg[1]), Float64(arg[2]) headlength, headwidth = Float64(arg[1]), Float64(arg[2])
else else
@warn("Skipped arrow arg $arg") @warn "Skipped arrow arg $arg"
end end
end end
Arrow(style, side, headlength, headwidth) Arrow(style, side, headlength, headwidth)
@ -745,11 +722,11 @@ end
# allow for do-block notation which gets called on every valid start/end pair which # allow for do-block notation which gets called on every valid start/end pair which
# we need to draw an arrow # we need to draw an arrow
function add_arrows(func::Function, x::AVec, y::AVec) 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]) xyprev = (x[i-1], y[i-1])
xy = (x[i], y[i]) xy = (x[i], y[i])
if ok(xyprev) && ok(xy) if ok(xyprev) && ok(xy)
if i==length(x) || !ok(x[i+1], y[i+1]) if i == length(x) || !ok(x[i+1], y[i+1])
# add the arrow from xyprev to xy # add the arrow from xyprev to xy
func(xyprev, xy) func(xyprev, xy)
end end
@ -774,16 +751,12 @@ end
@deprecate curve_points coords @deprecate curve_points coords
coords(curve::BezierCurve, n::Integer = 30; range = [0,1]) = map(curve, Base.range(first(range), stop=last(range), length=n)) 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. function extrema_plus_buffer(v, buffmult=0.2)
# may create a loop if necessary. Assumes the view is [0,1] vmin, vmax = ignorenan_extrema(v)
function directed_curve(args...; kw...)
error("directed_curve has been moved to PlotRecipes")
end
function extrema_plus_buffer(v, buffmult = 0.2)
vmin,vmax = ignorenan_extrema(v)
vdiff = vmax-vmin vdiff = vmax-vmin
buffer = vdiff * buffmult buffer = vdiff * buffmult
vmin - buffer, vmax + buffer vmin - buffer, vmax + buffer

View File

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