Merge pull request #3324 from gustaphe/legendangle

[WIP] position legend at angle
This commit is contained in:
Daniel Schwabeneder 2021-03-06 12:37:35 +01:00 committed by GitHub
commit 6201dfe580
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 188 additions and 52 deletions

View File

@ -211,6 +211,7 @@ include("output.jl")
include("ijulia.jl")
include("fileio.jl")
include("init.jl")
include("legend.jl")
include("backends/plotly.jl")
include("backends/gr.jl")

View File

@ -90,7 +90,7 @@ const _arg_desc = KW(
:foreground_color_legend => "Color Type or `:match` (matches `:foreground_color_subplot`). Foreground color of the legend.",
:foreground_color_title => "Color Type or `:match` (matches `:foreground_color_subplot`). Color of subplot title.",
:color_palette => "Vector of colors (cycle through) or color gradient (generate list from gradient) or `:auto` (generate a color list using `Colors.distiguishable_colors` and custom seed colors chosen to contrast with the background). The color palette is a color list from which series colors are automatically chosen.",
:legend => "Bool (show the legend?) or (x,y) tuple or Symbol (legend position). Bottom left corner of legend is placed at (x,y). Symbol values: `:none`; `:best`; `:inline`; `:inside`; `:legend`; any valid combination of `:(outer ?)(top/bottom ?)(right/left ?)`, i.e.: `:top`, `:topright`, `:outerleft`, `:outerbottomright` ... (note: only some may be supported in each backend)",
:legend => "Bool (show the legend?) or (x,y) tuple or Symbol (legend position) or angle or (angle,inout) tuple. Bottom left corner of legend is placed at (x,y). Symbol values: `:none`; `:best`; `:inline`; `:inside`; `:legend`; any valid combination of `:(outer ?)(top/bottom ?)(right/left ?)`, i.e.: `:top`, `:topright`, `:outerleft`, `:outerbottomright` ... (note: only some may be supported in each backend). Legend is positioned at (angle degrees) (so (90,:outer) is roughly equivalent to :outertop), close to the inside of the axes or the outside if inout=:outer.",
:legendfontfamily => "String or Symbol. Font family of legend entries.",
:legendfontsize => "Integer. Font pointsize of legend entries.",
:legendfonthalign => "Symbol. Font horizontal alignment of legend entries: :hcenter, :left, :right or :center",

View File

@ -1262,6 +1262,8 @@ end
convertLegendValue(val::Bool) = val ? :best : :none
convertLegendValue(val::Nothing) = :none
convertLegendValue(v::Tuple{S,T}) where {S<:Real, T<:Real} = v
convertLegendValue(v::Tuple{<:Real,Symbol}) = v
convertLegendValue(v::Real) = v
convertLegendValue(v::AbstractArray) = map(convertLegendValue, v)
# -----------------------------------------------------------------------------

View File

@ -911,7 +911,7 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas)
gr_update_viewport_ratio!(viewport_plotarea, sp)
leg = gr_get_legend_geometry(viewport_plotarea, sp)
gr_update_viewport_legend!(viewport_plotarea, sp, leg)
# fill in the plot area background
gr_fill_plotarea(sp, viewport_plotarea)
@ -1047,7 +1047,24 @@ end
function gr_legend_pos(sp::Subplot, leg, viewport_plotarea)
s = sp[:legend]
typeof(s) <: Symbol || return gr_legend_pos(s, viewport_plotarea)
s isa Real && return gr_legend_pos(s, leg, viewport_plotarea)
if s isa Tuple{<:Real,Symbol}
if s[2] !== :outer
return gr_legend_pos(s[1], leg, viewport_plotarea)
end
xaxis, yaxis = sp[:xaxis], sp[:yaxis]
xmirror = xaxis[:guide_position] == :top || (xaxis[:guide_position] == :auto && xaxis[:mirror] == true)
ymirror = yaxis[:guide_position] == :right || (yaxis[:guide_position] == :auto && yaxis[:mirror] == true)
axisclearance = [
!ymirror*gr_axis_width(sp, sp[:yaxis]),
ymirror*gr_axis_width(sp,sp[:yaxis]),
!xmirror*gr_axis_height(sp,sp[:xaxis]),
xmirror*gr_axis_height(sp,sp[:xaxis]),
]
return gr_legend_pos(s[1], leg, viewport_plotarea; axisclearance)
end
s isa Symbol || return gr_legend_pos(s, viewport_plotarea)
str = string(s)
if str == "best"
str = "topright"
@ -1081,7 +1098,7 @@ function gr_legend_pos(sp::Subplot, leg, viewport_plotarea)
end
elseif occursin("bottom", str)
if s == :outerbottom
ypos = viewport_plotarea[3] - leg.yoffset - leg.h - !xmirror * gr_axis_height(sp, sp[:xaxis])
ypos = viewport_plotarea[3] - leg.yoffset - leg.dy - !xmirror * gr_axis_height(sp, sp[:xaxis])
else
ypos = viewport_plotarea[3] + leg.yoffset + leg.h
end
@ -1098,6 +1115,27 @@ function gr_legend_pos(v::Tuple{S,T}, viewport_plotarea) where {S<:Real, T<:Real
(xpos,ypos)
end
function gr_legend_pos(theta::Real, leg, viewport_plotarea; axisclearance=nothing)
xcenter = +(viewport_plotarea[1:2]...)/2
ycenter = +(viewport_plotarea[3:4]...)/2
if isnothing(axisclearance)
# Inner
# rectangle where the anchor can legally be
xmin = viewport_plotarea[1] + leg.xoffset + leg.leftw
xmax = viewport_plotarea[2] - leg.xoffset - leg.rightw - leg.textw
ymin = viewport_plotarea[3] + leg.yoffset + leg.h
ymax = viewport_plotarea[4] - leg.yoffset - leg.dy
else
# Outer
xmin = viewport_plotarea[1] - leg.xoffset - leg.rightw - leg.textw - axisclearance[1]
xmax = viewport_plotarea[2] + leg.xoffset + leg.leftw + axisclearance[2]
ymin = viewport_plotarea[3] - leg.yoffset - leg.dy - axisclearance[3]
ymax = viewport_plotarea[4] + leg.yoffset + leg.h + axisclearance[4]
end
return legend_pos_from_angle(theta,xmin,xcenter,xmax,ymin,ycenter,ymax)
end
function gr_get_legend_geometry(viewport_plotarea, sp)
legendn = 0
legendw = 0
@ -1154,12 +1192,28 @@ end
## Viewport, window and scale
function gr_update_viewport_legend!(viewport_plotarea, sp, leg)
leg_str = string(sp[:legend])
s = sp[:legend]
xaxis, yaxis = sp[:xaxis], sp[:yaxis]
xmirror = xaxis[:guide_position] == :top || (xaxis[:guide_position] == :auto && xaxis[:mirror] == true)
ymirror = yaxis[:guide_position] == :right || (yaxis[:guide_position] == :auto && yaxis[:mirror] == true)
if s isa Tuple{<:Real,Symbol}
if s[2] === :outer
(x,y) = gr_legend_pos(sp, leg, viewport_plotarea) # Dry run, to figure out
if x < viewport_plotarea[1]
viewport_plotarea[1] += leg.leftw + leg.textw + leg.rightw + leg.xoffset + !ymirror * gr_axis_width(sp, sp[:yaxis])
elseif x > viewport_plotarea[2]
viewport_plotarea[2] -= leg.leftw + leg.textw + leg.rightw + leg.xoffset
end
if y < viewport_plotarea[3]
viewport_plotarea[3] += leg.h + leg.dy + leg.yoffset + !xmirror * gr_axis_height(sp, sp[:xaxis])
elseif y > viewport_plotarea[4]
viewport_plotarea[4] -= leg.h + leg.dy + leg.yoffset
end
end
end
leg_str = string(s)
if occursin("outer", leg_str)
if occursin("right", leg_str)
viewport_plotarea[2] -= leg.leftw + leg.textw + leg.rightw + leg.xoffset
@ -1171,7 +1225,7 @@ function gr_update_viewport_legend!(viewport_plotarea, sp, leg)
viewport_plotarea[3] += leg.h + leg.dy + leg.yoffset + !xmirror * gr_axis_height(sp, sp[:xaxis])
end
end
if sp[:legend] == :inline
if s === :inline
if sp[:yaxis][:mirror]
viewport_plotarea[1] += leg.w
else
@ -1464,10 +1518,10 @@ function gr_label_axis_3d(sp, letter)
if ax[:guide] != ""
near_letter = letter in (:x, :z) ? :y : :x
far_letter = letter in (:x, :y) ? :z : :x
nax = sp[Symbol(near_letter, :axis)]
fax = sp[Symbol(far_letter, :axis)]
amin, amax = axis_limits(sp, letter)
namin, namax = axis_limits(sp, near_letter)
famin, famax = axis_limits(sp, far_letter)
@ -1737,7 +1791,7 @@ function gr_draw_contour(series, x, y, z, clims)
end
function gr_draw_surface(series, x, y, z, clims)
if series[:seriestype] === :surface
fillalpha = get_fillalpha(series)
fillcolor = get_fillcolor(series)

View File

@ -770,8 +770,26 @@ pgfx_get_legend_pos(k) = get(
Symbol(k),
("at" => string((1.02, 1)), "anchor" => "north west"),
)
pgfx_get_legend_pos(t::Tuple) = ("at" => "{$(string(t))}", "anchor" => "north west")
pgfx_get_legend_pos(t::Tuple{S,T}) where {S<:Real,T<:Real} = ("at" => "{$(string(t))}", "anchor" => "north west")
pgfx_get_legend_pos(nt::NamedTuple) = ("at" => "{$(string(nt.at))}", "anchor" => string(nt.anchor))
pgfx_get_legend_pos(theta::Real) = pgfx_get_legend_pos((theta,:inner))
function pgfx_get_legend_pos(v::Tuple{S,Symbol}) where S <: Real
(s,c) = sincosd(v[1])
anchors = [
"south west" "south" "south east";
"west" "center" "east";
"north west" "north" "north east";
]
if v[2] === :inner
rect = (0.07,0.5,1.0,0.07,0.52,1.0)
anchor = anchors[legend_anchor_index(s),legend_anchor_index(c)]
else
rect = (-0.15,0.5,1.05,-0.15,0.52,1.1)
anchor = anchors[4-legend_anchor_index(s),4-legend_anchor_index(c)]
end
return ("at"=>"$(string(legend_pos_from_angle(v[1],rect...)))", "anchor"=>anchor)
end
pgfx_get_colorbar_pos(s) =
get((left = " left", bottom = " horizontal", top = " horizontal"), s, "")

View File

@ -384,6 +384,25 @@ end
plotly_legend_pos(v::Tuple{S,T}) where {S<:Real, T<:Real} = (coords=v, xanchor="left", yanchor="top")
plotly_legend_pos(theta::Real) = plotly_legend_pos((theta, :inner))
function plotly_legend_pos(v::Tuple{S,Symbol}) where S<:Real
(s,c) = sincosd(v[1])
xanchors = ["left", "center", "right"]
yanchors = ["bottom", "middle", "top"]
if v[2] === :inner
rect = (0.07,0.5,1.0,0.07,0.52,1.0)
xanchor = xanchors[legend_anchor_index(c)]
yanchor = yanchors[legend_anchor_index(s)]
else
rect = (-0.15,0.5,1.05,-0.15,0.52,1.1)
xanchor = xanchors[4-legend_anchor_index(c)]
yanchor = yanchors[4-legend_anchor_index(s)]
end
return (coords=legend_pos_from_angle(v[1],rect...), xanchor=xanchor, yanchor=yanchor)
end
function plotly_layout_json(plt::Plot)
JSON.json(plotly_layout(plt), 4)
@ -595,9 +614,9 @@ function plotly_series(plt::Plot, series::Series)
elseif st == :mesh3d
plotattributes_out[:type] = "mesh3d"
plotattributes_out[:x], plotattributes_out[:y], plotattributes_out[:z] = x, y, z
if series[:connections] !== nothing
if typeof(series[:connections]) <: Tuple{Array,Array,Array}
if typeof(series[:connections]) <: Tuple{Array,Array,Array}
i,j,k = series[:connections]
if !(length(i) == length(j) == length(k))
throw(ArgumentError("Argument connections must consist of equally sized arrays."))

View File

@ -494,11 +494,11 @@ function py_add_series(plt::Plot{PyPlotBackend}, series::Series)
y[rng], x[rng], z[rng]
else
y[rng], x[rng]
end
end
else
if RecipesPipeline.is3d(sp)
x[rng], y[rng], z[rng]
else
else
x[rng], y[rng]
end
end
@ -1309,44 +1309,26 @@ end
# -----------------------------------------------------------------
py_legend_pos(pos::Symbol) = get(
(
right = "right",
left = "center left",
top = "upper center",
bottom = "lower center",
bottomleft = "lower left",
bottomright = "lower right",
topright = "upper right",
topleft = "upper left",
outerright = "center left",
outerleft = "right",
outertop = "lower center",
outerbottom = "upper center",
outerbottomleft = "lower right",
outerbottomright = "lower left",
outertopright = "upper left",
outertopleft = "upper right",
),
pos,
"best",
)
py_legend_pos(pos) = "lower left"
py_legend_pos(pos::Tuple{S,T}) where {S<:Real,T<:Real} = "lower left"
function py_legend_pos(pos::Tuple{<:Real,Symbol})
(s,c) = sincosd(pos[1])
if pos[2] === :outer
s = -s
c = -c
end
yanchors = ["lower","center","upper"]
xanchors = ["left","center","right"]
return join([yanchors[legend_anchor_index(s)], xanchors[legend_anchor_index(c)]], ' ')
end
function py_legend_bbox(pos::Tuple{T,Symbol}) where T<:Real
if pos[2] === :outer
return legend_pos_from_angle(pos[1],-0.15,0.5,1.0,-0.15,0.5,1.0)
end
legend_pos_from_angle(pos[1],0.0,0.5,1.0,0.0,0.5,1.0)
end
py_legend_bbox(pos::Symbol) = get(
(
outerright = (1.0, 0.5, 0.0, 0.0),
outerleft = (-0.15, 0.5, 0.0, 0.0),
outertop = (0.5, 1.0, 0.0, 0.0),
outerbottom = (0.5, -0.15, 0.0, 0.0),
outerbottomleft = (-0.15, 0.0, 0.0, 0.0),
outerbottomright = (1.0, 0.0, 0.0, 0.0),
outertopright = (1.0, 1.0, 0.0, 0.0),
outertopleft = (-0.15, 1.0, 0.0, 0.0),
),
pos,
(0.0, 0.0, 1.0, 1.0),
)
py_legend_bbox(pos) = pos
function py_add_legend(plt::Plot, sp::Subplot, ax)
@ -1392,6 +1374,7 @@ function py_add_legend(plt::Plot, sp::Subplot, ax)
# if anything was added, call ax.legend and set the colors
if !isempty(handles)
leg = legend_angle(leg)
leg = ax."legend"(handles,
labels,
loc = py_legend_pos(leg),
@ -1402,7 +1385,7 @@ function py_add_legend(plt::Plot, sp::Subplot, ax)
edgecolor = py_color(sp[:foreground_color_legend]),
framealpha = alpha(plot_color(sp[:background_color_legend])),
fancybox = false, # makes the legend box square
borderpad=0.8 # to match GR legendbox
borderpad = 0.8 # to match GR legendbox
)
frame = leg."get_frame"()
frame."set_linewidth"(py_thickness_scale(plt, 1))

59
src/legend.jl Normal file
View File

@ -0,0 +1,59 @@
"""
```julia
legend_pos_from_angle(theta, xmin, xcenter, xmax, ymin, ycenter, ymax, inout)
```
Return `(x,y)` at an angle `theta` degrees from
`(xcenter,ycenter)` on a rectangle defined by (`xmin`,
`xmax`, `ymin`, `ymax`).
"""
function legend_pos_from_angle(theta, xmin, xcenter, xmax, ymin, ycenter, ymax)
(s,c) = sincosd(theta)
x = c < 0 ? (xmin-xcenter)/c : (xmax-xcenter)/c
y = s < 0 ? (ymin-ycenter)/s : (ymax-ycenter)/s
A = min(x,y)
return (xcenter + A*c, ycenter + A*s)
end
"""
Split continuous range `[-1,1]` evenly into an integer `[1,2,3]`
"""
function legend_anchor_index(x)
x<-1//3 && return 1
x<1//3 && return 2
return 3
end
"""
Turn legend argument into a (theta, :inner) or (theta, :outer) tuple.
For backends where legend position is given in normal coordinates (0,0) -- (1,1),
so :topleft exactly corresponds to (45, :inner) etc.
If `leg` is a (::Real,::Real) tuple, keep it as is.
"""
legend_angle(leg::Real) = (leg,:inner)
legend_angle(leg::Tuple{S,T}) where {S<:Real,T<:Real} = leg
legend_angle(leg::Tuple{S,Symbol}) where S<:Real = leg
legend_angle(leg::Symbol) = get(
(
topleft = (135,:inner),
top = (90, :inner),
topright = (45, :inner),
left = (180,:inner),
right = (0, :inner),
bottomleft = (225,:inner),
bottom = (270,:inner),
bottomright = (315,:inner),
outertopleft = (135,:outer),
outertop = (90, :outer),
outertopright = (45, :outer),
outerleft = (180,:outer),
outerright = (0, :outer),
outerbottomleft = (225,:outer),
outerbottom = (270,:outer),
outerbottomright = (315,:outer),
),
leg,
(45, :inner)
)