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/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", 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 1cc51624..b6bb9aa9 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" @@ -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) diff --git a/src/backends/pgfplotsx.jl b/src/backends/pgfplotsx.jl index a34c0065..7bb6ec32 100644 --- a/src/backends/pgfplotsx.jl +++ b/src/backends/pgfplotsx.jl @@ -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, "") diff --git a/src/backends/plotly.jl b/src/backends/plotly.jl index 597413f2..a9aff316 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/backends/pyplot.jl b/src/backends/pyplot.jl index 4c8ed18a..21243ca2 100644 --- a/src/backends/pyplot.jl +++ b/src/backends/pyplot.jl @@ -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)) diff --git a/src/legend.jl b/src/legend.jl new file mode 100644 index 00000000..0b996619 --- /dev/null +++ b/src/legend.jl @@ -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) + )