From 6193519b1f0ef4db2f0f4122429d59e3f1d9bd81 Mon Sep 17 00:00:00 2001 From: David Gustavsson Date: Thu, 25 Feb 2021 12:20:59 +0100 Subject: [PATCH 1/7] Correct erroneous offset for legend=:outerbottom --- src/backends/gr.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backends/gr.jl b/src/backends/gr.jl index 56693c00..6a0ab1ee 100644 --- a/src/backends/gr.jl +++ b/src/backends/gr.jl @@ -1081,7 +1081,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 From 2cee039dbc5ba543f3ebdc735d52186f859f26ed Mon Sep 17 00:00:00 2001 From: David Gustavsson Date: Thu, 25 Feb 2021 15:08:33 +0100 Subject: [PATCH 2/7] Add polar legend position --- src/args.jl | 2 + src/backends/gr.jl | 96 ++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 91 insertions(+), 7 deletions(-) diff --git a/src/args.jl b/src/args.jl index 519fea3c..3a3c963c 100644 --- a/src/args.jl +++ b/src/args.jl @@ -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) # ----------------------------------------------------------------------------- diff --git a/src/backends/gr.jl b/src/backends/gr.jl index 6a0ab1ee..29ac071c 100644 --- a/src/backends/gr.jl +++ b/src/backends/gr.jl @@ -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" @@ -1098,6 +1115,55 @@ 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) + xmean = +(viewport_plotarea[1:2]...)/2 + ymean = +(viewport_plotarea[3:4]...)/2 + (s,c) = sincosd(theta) + + if isnothing(axisclearance) + # Inner + # rectangle relative to midpoint where the anchor can legally be + rect = viewport_plotarea .+ [ + - xmean + leg.xoffset + leg.leftw, + - xmean - leg.xoffset - leg.rightw - leg.textw, + - ymean + leg.yoffset + leg.h, + - ymean - leg.yoffset - leg.dy, + ] + x = c < 0 ? rect[1]/c : rect[2]/c + y = s < 0 ? rect[3]/s : rect[4]/s + A = min(x,y) # Biggest A that places (Acos(theta),Asin(theta)) inside rectangle + else + # Outer + # rectangle relative to midpoint where the anchor is forbidden + rect = viewport_plotarea .+ [ + - xmean - leg.xoffset - leg.rightw - leg.textw - axisclearance[1], + - xmean + leg.xoffset + leg.leftw + axisclearance[2], + - ymean - leg.yoffset - leg.dy - axisclearance[3], + - ymean + leg.yoffset + leg.h + axisclearance[4], + ] + t = tand(theta) + if c < 0 + if t < rect[4]/rect[1] + A = rect[4]/s + elseif t < rect[3]/rect[1] + A = rect[1]/c + else + A = rect[3]/s + end + else + if t < rect[3]/rect[2] + A = rect[3]/s + elseif t 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 +1253,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 +1546,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) @@ -1732,7 +1814,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) From 6724a3a2fe6fe411787baa94baf9c52a4d3e1bdd Mon Sep 17 00:00:00 2001 From: David Gustavsson Date: Sun, 28 Feb 2021 23:40:06 +0100 Subject: [PATCH 3/7] Centralize utility functions, add legend-angle to plotly, pgfplotsx --- src/Plots.jl | 1 + src/backends/gr.jl | 52 +++++++++------------------------------ src/backends/pgfplotsx.jl | 20 ++++++++++++++- src/backends/plotly.jl | 23 +++++++++++++++-- src/legend.jl | 24 ++++++++++++++++++ 5 files changed, 77 insertions(+), 43 deletions(-) create mode 100644 src/legend.jl diff --git a/src/Plots.jl b/src/Plots.jl index 50b4205c..5378fb73 100644 --- a/src/Plots.jl +++ b/src/Plots.jl @@ -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") diff --git a/src/backends/gr.jl b/src/backends/gr.jl index 29ac071c..3e89ac90 100644 --- a/src/backends/gr.jl +++ b/src/backends/gr.jl @@ -1116,52 +1116,24 @@ function gr_legend_pos(v::Tuple{S,T}, viewport_plotarea) where {S<:Real, T<:Real end function gr_legend_pos(theta::Real, leg, viewport_plotarea; axisclearance=nothing) - xmean = +(viewport_plotarea[1:2]...)/2 - ymean = +(viewport_plotarea[3:4]...)/2 - (s,c) = sincosd(theta) + xcenter = +(viewport_plotarea[1:2]...)/2 + ycenter = +(viewport_plotarea[3:4]...)/2 if isnothing(axisclearance) # Inner - # rectangle relative to midpoint where the anchor can legally be - rect = viewport_plotarea .+ [ - - xmean + leg.xoffset + leg.leftw, - - xmean - leg.xoffset - leg.rightw - leg.textw, - - ymean + leg.yoffset + leg.h, - - ymean - leg.yoffset - leg.dy, - ] - x = c < 0 ? rect[1]/c : rect[2]/c - y = s < 0 ? rect[3]/s : rect[4]/s - A = min(x,y) # Biggest A that places (Acos(theta),Asin(theta)) inside rectangle + # 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 - # rectangle relative to midpoint where the anchor is forbidden - rect = viewport_plotarea .+ [ - - xmean - leg.xoffset - leg.rightw - leg.textw - axisclearance[1], - - xmean + leg.xoffset + leg.leftw + axisclearance[2], - - ymean - leg.yoffset - leg.dy - axisclearance[3], - - ymean + leg.yoffset + leg.h + axisclearance[4], - ] - t = tand(theta) - if c < 0 - if t < rect[4]/rect[1] - A = rect[4]/s - elseif t < rect[3]/rect[1] - A = rect[1]/c - else - A = rect[3]/s - end - else - if t < rect[3]/rect[2] - A = rect[3]/s - elseif t 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 = [ + "north west" "north" "north east"; + "west" "center" "east"; + "south west" "south" "south 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, "") diff --git a/src/backends/plotly.jl b/src/backends/plotly.jl index ce2b704c..3b6d2794 100644 --- a/src/backends/plotly.jl +++ b/src/backends/plotly.jl @@ -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.")) diff --git a/src/legend.jl b/src/legend.jl new file mode 100644 index 00000000..7037bc14 --- /dev/null +++ b/src/legend.jl @@ -0,0 +1,24 @@ +""" +```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]` into an integer `[1,2,3]` +""" +function legend_anchor_index(x) + return ceil(Integer,2//3*(x+1)) +end From 1d3e0a5d5db0edc846408b4d12be0ec04d3a1eb3 Mon Sep 17 00:00:00 2001 From: David Gustavsson Date: Tue, 2 Mar 2021 08:01:59 +0100 Subject: [PATCH 4/7] Better legend anchor algorithm --- src/backends/plotly.jl | 2 +- src/legend.jl | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/backends/plotly.jl b/src/backends/plotly.jl index 3b6d2794..b7b21590 100644 --- a/src/backends/plotly.jl +++ b/src/backends/plotly.jl @@ -384,7 +384,7 @@ 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)) +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]) diff --git a/src/legend.jl b/src/legend.jl index 7037bc14..bb1e0a46 100644 --- a/src/legend.jl +++ b/src/legend.jl @@ -20,5 +20,7 @@ end Split continuous range `[-1,1]` into an integer `[1,2,3]` """ function legend_anchor_index(x) - return ceil(Integer,2//3*(x+1)) + x<-1//3 && return 1 + x<1//3 && return 2 + return 3 end From 37252ec5622f4e5c6468b631fd81e041cb4adbec Mon Sep 17 00:00:00 2001 From: David Gustavsson Date: Tue, 2 Mar 2021 10:07:49 +0100 Subject: [PATCH 5/7] Correct anchor order --- src/backends/pgfplotsx.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backends/pgfplotsx.jl b/src/backends/pgfplotsx.jl index c44afdbd..73e5d48f 100644 --- a/src/backends/pgfplotsx.jl +++ b/src/backends/pgfplotsx.jl @@ -775,9 +775,9 @@ 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 = [ - "north west" "north" "north east"; - "west" "center" "east"; "south west" "south" "south east"; + "west" "center" "east"; + "north west" "north" "north east"; ] if v[2] === :inner From d6a72a5df5d01b024237ef1fbc4bebfd513541bc Mon Sep 17 00:00:00 2001 From: David Gustavsson Date: Tue, 2 Mar 2021 10:19:26 +0100 Subject: [PATCH 6/7] Add legend angle to pyplot --- src/backends/pyplot.jl | 63 +++++++++++++++--------------------------- src/legend.jl | 35 ++++++++++++++++++++++- 2 files changed, 57 insertions(+), 41 deletions(-) diff --git a/src/backends/pyplot.jl b/src/backends/pyplot.jl index 83899620..d89ac3aa 100644 --- a/src/backends/pyplot.jl +++ b/src/backends/pyplot.jl @@ -492,11 +492,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 @@ -1305,44 +1305,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) @@ -1388,6 +1370,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), @@ -1398,7 +1381,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)) diff --git a/src/legend.jl b/src/legend.jl index bb1e0a46..0b996619 100644 --- a/src/legend.jl +++ b/src/legend.jl @@ -17,10 +17,43 @@ end """ -Split continuous range `[-1,1]` into an integer `[1,2,3]` +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) + ) From 9cee7a2c47280d72766116f9adf52c0f33b50227 Mon Sep 17 00:00:00 2001 From: David Gustavsson Date: Tue, 2 Mar 2021 11:53:47 +0100 Subject: [PATCH 7/7] Documentation for legend angle --- src/arg_desc.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/arg_desc.jl b/src/arg_desc.jl index 9adad50b..a5139c1b 100644 --- a/src/arg_desc.jl +++ b/src/arg_desc.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_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",