Merge pull request #3743 from t-bltg/ann
Allow passing collection of tuples to series_annotations
This commit is contained in:
commit
40b5df38f4
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user