Merge pull request #3324 from gustaphe/legendangle
[WIP] position legend at angle
This commit is contained in:
commit
6201dfe580
@ -211,6 +211,7 @@ include("output.jl")
|
|||||||
include("ijulia.jl")
|
include("ijulia.jl")
|
||||||
include("fileio.jl")
|
include("fileio.jl")
|
||||||
include("init.jl")
|
include("init.jl")
|
||||||
|
include("legend.jl")
|
||||||
|
|
||||||
include("backends/plotly.jl")
|
include("backends/plotly.jl")
|
||||||
include("backends/gr.jl")
|
include("backends/gr.jl")
|
||||||
|
|||||||
@ -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_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.",
|
: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.",
|
: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.",
|
:legendfontfamily => "String or Symbol. Font family of legend entries.",
|
||||||
:legendfontsize => "Integer. Font pointsize of legend entries.",
|
:legendfontsize => "Integer. Font pointsize of legend entries.",
|
||||||
:legendfonthalign => "Symbol. Font horizontal alignment of legend entries: :hcenter, :left, :right or :center",
|
:legendfonthalign => "Symbol. Font horizontal alignment of legend entries: :hcenter, :left, :right or :center",
|
||||||
|
|||||||
@ -1262,6 +1262,8 @@ end
|
|||||||
convertLegendValue(val::Bool) = val ? :best : :none
|
convertLegendValue(val::Bool) = val ? :best : :none
|
||||||
convertLegendValue(val::Nothing) = :none
|
convertLegendValue(val::Nothing) = :none
|
||||||
convertLegendValue(v::Tuple{S,T}) where {S<:Real, T<:Real} = v
|
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)
|
convertLegendValue(v::AbstractArray) = map(convertLegendValue, v)
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
@ -1047,7 +1047,24 @@ end
|
|||||||
|
|
||||||
function gr_legend_pos(sp::Subplot, leg, viewport_plotarea)
|
function gr_legend_pos(sp::Subplot, leg, viewport_plotarea)
|
||||||
s = sp[:legend]
|
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)
|
str = string(s)
|
||||||
if str == "best"
|
if str == "best"
|
||||||
str = "topright"
|
str = "topright"
|
||||||
@ -1081,7 +1098,7 @@ function gr_legend_pos(sp::Subplot, leg, viewport_plotarea)
|
|||||||
end
|
end
|
||||||
elseif occursin("bottom", str)
|
elseif occursin("bottom", str)
|
||||||
if s == :outerbottom
|
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
|
else
|
||||||
ypos = viewport_plotarea[3] + leg.yoffset + leg.h
|
ypos = viewport_plotarea[3] + leg.yoffset + leg.h
|
||||||
end
|
end
|
||||||
@ -1098,6 +1115,27 @@ function gr_legend_pos(v::Tuple{S,T}, viewport_plotarea) where {S<:Real, T<:Real
|
|||||||
(xpos,ypos)
|
(xpos,ypos)
|
||||||
end
|
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)
|
function gr_get_legend_geometry(viewport_plotarea, sp)
|
||||||
legendn = 0
|
legendn = 0
|
||||||
legendw = 0
|
legendw = 0
|
||||||
@ -1154,12 +1192,28 @@ end
|
|||||||
## Viewport, window and scale
|
## Viewport, window and scale
|
||||||
|
|
||||||
function gr_update_viewport_legend!(viewport_plotarea, sp, leg)
|
function gr_update_viewport_legend!(viewport_plotarea, sp, leg)
|
||||||
leg_str = string(sp[:legend])
|
s = sp[:legend]
|
||||||
|
|
||||||
xaxis, yaxis = sp[:xaxis], sp[:yaxis]
|
xaxis, yaxis = sp[:xaxis], sp[:yaxis]
|
||||||
xmirror = xaxis[:guide_position] == :top || (xaxis[:guide_position] == :auto && xaxis[:mirror] == true)
|
xmirror = xaxis[:guide_position] == :top || (xaxis[:guide_position] == :auto && xaxis[:mirror] == true)
|
||||||
ymirror = yaxis[:guide_position] == :right || (yaxis[:guide_position] == :auto && yaxis[: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("outer", leg_str)
|
||||||
if occursin("right", leg_str)
|
if occursin("right", leg_str)
|
||||||
viewport_plotarea[2] -= leg.leftw + leg.textw + leg.rightw + leg.xoffset
|
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])
|
viewport_plotarea[3] += leg.h + leg.dy + leg.yoffset + !xmirror * gr_axis_height(sp, sp[:xaxis])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if sp[:legend] == :inline
|
if s === :inline
|
||||||
if sp[:yaxis][:mirror]
|
if sp[:yaxis][:mirror]
|
||||||
viewport_plotarea[1] += leg.w
|
viewport_plotarea[1] += leg.w
|
||||||
else
|
else
|
||||||
|
|||||||
@ -770,8 +770,26 @@ pgfx_get_legend_pos(k) = get(
|
|||||||
Symbol(k),
|
Symbol(k),
|
||||||
("at" => string((1.02, 1)), "anchor" => "north west"),
|
("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(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) =
|
pgfx_get_colorbar_pos(s) =
|
||||||
get((left = " left", bottom = " horizontal", top = " horizontal"), s, "")
|
get((left = " left", bottom = " horizontal", top = " horizontal"), s, "")
|
||||||
|
|||||||
@ -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(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)
|
function plotly_layout_json(plt::Plot)
|
||||||
JSON.json(plotly_layout(plt), 4)
|
JSON.json(plotly_layout(plt), 4)
|
||||||
|
|||||||
@ -1309,44 +1309,26 @@ end
|
|||||||
|
|
||||||
# -----------------------------------------------------------------
|
# -----------------------------------------------------------------
|
||||||
|
|
||||||
py_legend_pos(pos::Symbol) = get(
|
py_legend_pos(pos::Tuple{S,T}) where {S<:Real,T<:Real} = "lower left"
|
||||||
(
|
|
||||||
right = "right",
|
function py_legend_pos(pos::Tuple{<:Real,Symbol})
|
||||||
left = "center left",
|
(s,c) = sincosd(pos[1])
|
||||||
top = "upper center",
|
if pos[2] === :outer
|
||||||
bottom = "lower center",
|
s = -s
|
||||||
bottomleft = "lower left",
|
c = -c
|
||||||
bottomright = "lower right",
|
end
|
||||||
topright = "upper right",
|
yanchors = ["lower","center","upper"]
|
||||||
topleft = "upper left",
|
xanchors = ["left","center","right"]
|
||||||
outerright = "center left",
|
return join([yanchors[legend_anchor_index(s)], xanchors[legend_anchor_index(c)]], ' ')
|
||||||
outerleft = "right",
|
end
|
||||||
outertop = "lower center",
|
|
||||||
outerbottom = "upper center",
|
function py_legend_bbox(pos::Tuple{T,Symbol}) where T<:Real
|
||||||
outerbottomleft = "lower right",
|
if pos[2] === :outer
|
||||||
outerbottomright = "lower left",
|
return legend_pos_from_angle(pos[1],-0.15,0.5,1.0,-0.15,0.5,1.0)
|
||||||
outertopright = "upper left",
|
end
|
||||||
outertopleft = "upper right",
|
legend_pos_from_angle(pos[1],0.0,0.5,1.0,0.0,0.5,1.0)
|
||||||
),
|
end
|
||||||
pos,
|
|
||||||
"best",
|
|
||||||
)
|
|
||||||
py_legend_pos(pos) = "lower left"
|
|
||||||
|
|
||||||
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
|
py_legend_bbox(pos) = pos
|
||||||
|
|
||||||
function py_add_legend(plt::Plot, sp::Subplot, ax)
|
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 anything was added, call ax.legend and set the colors
|
||||||
if !isempty(handles)
|
if !isempty(handles)
|
||||||
|
leg = legend_angle(leg)
|
||||||
leg = ax."legend"(handles,
|
leg = ax."legend"(handles,
|
||||||
labels,
|
labels,
|
||||||
loc = py_legend_pos(leg),
|
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]),
|
edgecolor = py_color(sp[:foreground_color_legend]),
|
||||||
framealpha = alpha(plot_color(sp[:background_color_legend])),
|
framealpha = alpha(plot_color(sp[:background_color_legend])),
|
||||||
fancybox = false, # makes the legend box square
|
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 = leg."get_frame"()
|
||||||
frame."set_linewidth"(py_thickness_scale(plt, 1))
|
frame."set_linewidth"(py_thickness_scale(plt, 1))
|
||||||
|
|||||||
59
src/legend.jl
Normal file
59
src/legend.jl
Normal 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)
|
||||||
|
)
|
||||||
Loading…
x
Reference in New Issue
Block a user