diff --git a/src/axes.jl b/src/axes.jl index c95b5db7..3d6bfbb8 100644 --- a/src/axes.jl +++ b/src/axes.jl @@ -152,8 +152,8 @@ scalefunc(scale::Symbol) = x -> get(_scale_funcs, scale, identity)(Float64(x)) invscalefunc(scale::Symbol) = x -> get(_inv_scale_funcs, scale, identity)(Float64(x)) labelfunc(scale::Symbol, backend::AbstractBackend) = get(_label_func, scale, string) -function optimal_ticks_and_labels(axis::Axis, ticks = nothing) - amin,amax = axis_limits(axis) +function optimal_ticks_and_labels(sp::Subplot, axis::Axis, ticks = nothing) + amin, amax = axis_limits(sp, axis[:letter]) # scale the limits scale = axis[:scale] @@ -238,7 +238,7 @@ function optimal_ticks_and_labels(axis::Axis, ticks = nothing) end # return (continuous_values, discrete_values) for the ticks on this axis -function get_ticks(axis::Axis) +function get_ticks(sp::Subplot, axis::Axis) ticks = _transform_ticks(axis[:ticks]) ticks in (:none, nothing, false) && return nothing @@ -261,7 +261,7 @@ function get_ticks(axis::Axis) (collect(0:pi/4:7pi/4), string.(0:45:315)) else # compute optimal ticks and labels - optimal_ticks_and_labels(axis) + optimal_ticks_and_labels(sp, axis) end elseif typeof(ticks) <: Union{AVec, Int} if !isempty(dvals) && typeof(ticks) <: Int @@ -269,7 +269,7 @@ function get_ticks(axis::Axis) axis[:continuous_values][rng], dvals[rng] else # override ticks, but get the labels - optimal_ticks_and_labels(axis, ticks) + optimal_ticks_and_labels(sp, axis, ticks) end elseif typeof(ticks) <: NTuple{2, Any} # assuming we're passed (ticks, labels) @@ -286,12 +286,12 @@ _transform_ticks(ticks) = ticks _transform_ticks(ticks::AbstractArray{T}) where T <: Dates.TimeType = Dates.value.(ticks) _transform_ticks(ticks::NTuple{2, Any}) = (_transform_ticks(ticks[1]), ticks[2]) -function get_minor_ticks(axis,ticks) +function get_minor_ticks(sp, axis, ticks) axis[:minorticks] in (:none, nothing, false) && !axis[:minorgrid] && return nothing ticks = ticks[1] length(ticks) < 2 && return nothing - amin, amax = axis_limits(axis) + amin, amax = axis_limits(sp, axis[:letter]) #Add one phantom tick either side of the ticks to ensure minor ticks extend to the axis limits if length(ticks) > 2 ratio = (ticks[3] - ticks[2])/(ticks[2] - ticks[1]) @@ -479,7 +479,8 @@ function round_limits(amin,amax) end # using the axis extrema and limit overrides, return the min/max value for this axis -function axis_limits(axis::Axis, should_widen::Bool = default_should_widen(axis)) +function axis_limits(sp, letter, should_widen = default_should_widen(sp[Symbol(letter, :axis)]), consider_aspect = true) + axis = sp[Symbol(letter, :axis)] ex = axis[:extrema] amin, amax = ex.emin, ex.emax lims = axis[:lims] @@ -497,7 +498,7 @@ function axis_limits(axis::Axis, should_widen::Bool = default_should_widen(axis) if !isfinite(amin) && !isfinite(amax) amin, amax = 0.0, 1.0 end - if ispolar(axis.sps[1]) + amin, amax = if ispolar(axis.sps[1]) if axis[:letter] == :x amin, amax = 0, 2pi elseif lims == :auto @@ -513,6 +514,33 @@ function axis_limits(axis::Axis, should_widen::Bool = default_should_widen(axis) else amin, amax end + + if consider_aspect && letter in (:x, :y) && !(sp[:aspect_ratio] in (:none, :auto) || is3d(:sp)) + aspect_ratio = isa(sp[:aspect_ratio], Number) ? sp[:aspect_ratio] : 1 + plot_ratio = height(plotarea(sp)) / width(plotarea(sp)) + bbox_ratio = height(sp.bbox) / width(sp.bbox) + dist = amax - amin + + if letter == :x + yamin, yamax = axis_limits(sp, :y, default_should_widen(sp[:yaxis]), false) + ydist = yamax - yamin + axis_ratio = aspect_ratio * ydist / dist + factor = axis_ratio / plot_ratio + else + xamin, xamax = axis_limits(sp, :x, default_should_widen(sp[:xaxis]), false) + xdist = xamax - xamin + axis_ratio = aspect_ratio * dist / xdist + factor = plot_ratio / axis_ratio + end + + if factor > 1 + center = (amin + amax) / 2 + amin = center + factor * (amin - center) + amax = center + factor * (amax - center) + end + end + + return amin, amax end # ------------------------------------------------------------------------- @@ -586,12 +614,12 @@ end # compute the line segments which should be drawn for this axis function axis_drawing_info(sp::Subplot) xaxis, yaxis = sp[:xaxis], sp[:yaxis] - xmin, xmax = axis_limits(xaxis) - ymin, ymax = axis_limits(yaxis) - xticks = get_ticks(xaxis) - yticks = get_ticks(yaxis) - xminorticks = get_minor_ticks(xaxis,xticks) - yminorticks = get_minor_ticks(yaxis,yticks) + xmin, xmax = axis_limits(sp, :x) + ymin, ymax = axis_limits(sp, :y) + xticks = get_ticks(sp, xaxis) + yticks = get_ticks(sp, yaxis) + xminorticks = get_minor_ticks(sp, xaxis, xticks) + yminorticks = get_minor_ticks(sp, yaxis, yticks) xaxis_segs = Segments(2) yaxis_segs = Segments(2) xtick_segs = Segments(2) diff --git a/src/backends.jl b/src/backends.jl index 8cb4c0ce..8e42442b 100644 --- a/src/backends.jl +++ b/src/backends.jl @@ -75,8 +75,8 @@ end text_size(lab::AbstractString, sz::Number, rot::Number = 0) = text_size(length(lab), sz, rot) # account for the size/length/rotation of tick labels -function tick_padding(axis::Axis) - ticks = get_ticks(axis) +function tick_padding(sp::Subplot, axis::Axis) + ticks = get_ticks(sp, axis) if ticks == nothing 0mm else @@ -106,10 +106,10 @@ end # to fit ticks, tick labels, guides, colorbars, etc. function _update_min_padding!(sp::Subplot) # TODO: something different when `is3d(sp) == true` - leftpad = tick_padding(sp[:yaxis]) + sp[:left_margin] + guide_padding(sp[:yaxis]) + leftpad = tick_padding(sp, sp[:yaxis]) + sp[:left_margin] + guide_padding(sp[:yaxis]) toppad = sp[:top_margin] + title_padding(sp) rightpad = sp[:right_margin] - bottompad = tick_padding(sp[:xaxis]) + sp[:bottom_margin] + guide_padding(sp[:xaxis]) + bottompad = tick_padding(sp, sp[:xaxis]) + sp[:bottom_margin] + guide_padding(sp[:xaxis]) # switch them? if sp[:xaxis][:mirror] diff --git a/src/backends/gr.jl b/src/backends/gr.jl index 7e41bfeb..29f6170b 100644 --- a/src/backends/gr.jl +++ b/src/backends/gr.jl @@ -162,7 +162,7 @@ function gr_polaraxes(rmin::Real, rmax::Real, sp::Subplot) a = α .+ 90 sinf = sind.(a) cosf = cosd.(a) - rtick_values, rtick_labels = get_ticks(yaxis) + rtick_values, rtick_labels = get_ticks(sp, yaxis) if yaxis[:formatter] in (:scientific, :auto) && yaxis[:ticks] in (:auto, :native) rtick_labels = convert_sci_unicode.(rtick_labels) end @@ -218,16 +218,16 @@ end # using the axis extrema and limit overrides, return the min/max value for this axis -gr_x_axislims(sp::Subplot) = axis_limits(sp[:xaxis]) -gr_y_axislims(sp::Subplot) = axis_limits(sp[:yaxis]) -gr_z_axislims(sp::Subplot) = axis_limits(sp[:zaxis]) +gr_x_axislims(sp::Subplot) = axis_limits(sp, :x) +gr_y_axislims(sp::Subplot) = axis_limits(sp, :y) +gr_z_axislims(sp::Subplot) = axis_limits(sp, :z) gr_xy_axislims(sp::Subplot) = gr_x_axislims(sp)..., gr_y_axislims(sp)... -function gr_lims(axis::Axis, adjust::Bool, expand = nothing) +function gr_lims(sp::Subplot, axis::Axis, adjust::Bool, expand = nothing) if expand != nothing expand_extrema!(axis, expand) end - lims = axis_limits(axis) + lims = axis_limits(sp, axis[:letter]) if adjust GR.adjustrange(lims...) else @@ -989,7 +989,7 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas) if is3d(sp) # TODO do we really need a different clims computation here from the one # computed above using get_clims(sp)? - zmin, zmax = gr_lims(zaxis, true) + zmin, zmax = gr_lims(sp, zaxis, true) clims3d = sp[:clims] if is_2tuple(clims3d) isfinite(clims3d[1]) && (zmin = clims3d[1]) @@ -1024,7 +1024,7 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas) elseif ispolar(sp) r = gr_set_viewport_polar() #rmin, rmax = GR.adjustrange(ignorenan_minimum(r), ignorenan_maximum(r)) - rmin, rmax = axis_limits(sp[:yaxis]) + rmin, rmax = axis_limits(sp, :y) gr_polaraxes(rmin, rmax, sp) elseif draw_axes diff --git a/src/backends/inspectdr.jl b/src/backends/inspectdr.jl index bbc268a3..c1e5c5b8 100644 --- a/src/backends/inspectdr.jl +++ b/src/backends/inspectdr.jl @@ -294,8 +294,8 @@ function _inspectdr_setupsubplot(sp::Subplot{InspectDRBackend}) plot.xscale = _inspectdr_getscale(xaxis[:scale], false) strip.yscale = _inspectdr_getscale(yaxis[:scale], true) - xmin, xmax = axis_limits(xaxis) - ymin, ymax = axis_limits(yaxis) + xmin, xmax = axis_limits(sp, :x) + ymin, ymax = axis_limits(sp, :y) if ispolar(sp) #Plots.jl appears to give (xmin,xmax) ≜ (Θmin,Θmax) & (ymin,ymax) ≜ (rmin,rmax) rmax = NaNMath.max(abs(ymin), abs(ymax)) diff --git a/src/backends/pgfplots.jl b/src/backends/pgfplots.jl index e7c805d0..96a83dd4 100644 --- a/src/backends/pgfplots.jl +++ b/src/backends/pgfplots.jl @@ -361,13 +361,13 @@ function pgf_axis(sp::Subplot, letter) # limits # TODO: support zlims if letter != :z - lims = ispolar(sp) && letter == :x ? rad2deg.(axis_limits(axis)) : axis_limits(axis) + lims = ispolar(sp) && letter == :x ? rad2deg.(axis_limits(sp, :x)) : axis_limits(sp, :x) kw[Symbol(letter,:min)] = lims[1] kw[Symbol(letter,:max)] = lims[2] end if !(axis[:ticks] in (nothing, false, :none, :native)) && framestyle != :none - ticks = get_ticks(axis) + ticks = get_ticks(sp, axis) #pgf plot ignores ticks with angle below 90 when xmin = 90 so shift values tick_values = ispolar(sp) && letter == :x ? [rad2deg.(ticks[1])[3:end]..., 360, 405] : ticks[1] push!(style, string(letter, "tick = {", join(tick_values,","), "}")) diff --git a/src/backends/plotly.jl b/src/backends/plotly.jl index 81d0bb7f..b9c6c7cf 100644 --- a/src/backends/plotly.jl +++ b/src/backends/plotly.jl @@ -114,8 +114,8 @@ function plotly_apply_aspect_ratio(sp::Subplot, plotarea, pcts) if aspect_ratio == :equal aspect_ratio = 1.0 end - xmin,xmax = axis_limits(sp[:xaxis]) - ymin,ymax = axis_limits(sp[:yaxis]) + xmin,xmax = axis_limits(sp, :x) + ymin,ymax = axis_limits(sp, :y) want_ratio = ((xmax-xmin) / (ymax-ymin)) / aspect_ratio parea_ratio = width(plotarea) / height(plotarea) if want_ratio > parea_ratio @@ -174,7 +174,7 @@ function plotly_axis(plt::Plot, axis::Axis, sp::Subplot) ax[:tickangle] = -axis[:rotation] ax[:type] = plotly_scale(axis[:scale]) - lims = axis_limits(axis) + lims = axis_limits(sp, letter) if axis[:ticks] != :native || axis[:lims] != :auto ax[:range] = map(scalefunc(axis[:scale]), lims) @@ -188,7 +188,7 @@ function plotly_axis(plt::Plot, axis::Axis, sp::Subplot) # ticks if axis[:ticks] != :native - ticks = get_ticks(axis) + ticks = get_ticks(sp, axis) ttype = ticksType(ticks) if ttype == :ticks ax[:tickmode] = "array" @@ -211,16 +211,16 @@ function plotly_axis(plt::Plot, axis::Axis, sp::Subplot) ax end -function plotly_polaraxis(axis::Axis) +function plotly_polaraxis(sp::Subplot, axis::Axis) ax = KW( :visible => axis[:showaxis], :showline => axis[:grid], ) if axis[:letter] == :x - ax[:range] = rad2deg.(axis_limits(axis)) + ax[:range] = rad2deg.(axis_limits(sp, :x)) else - ax[:range] = axis_limits(axis) + ax[:range] = axis_limits(sp, :y) ax[:orientation] = -90 end @@ -283,8 +283,8 @@ function plotly_layout(plt::Plot) ), ) elseif ispolar(sp) - plotattributes_out[Symbol("angularaxis$(spidx)")] = plotly_polaraxis(sp[:xaxis]) - plotattributes_out[Symbol("radialaxis$(spidx)")] = plotly_polaraxis(sp[:yaxis]) + plotattributes_out[Symbol("angularaxis$(spidx)")] = plotly_polaraxis(sp, sp[:xaxis]) + plotattributes_out[Symbol("radialaxis$(spidx)")] = plotly_polaraxis(sp, sp[:yaxis]) else plotattributes_out[Symbol("xaxis$(x_idx)")] = plotly_axis(plt, sp[:xaxis], sp) # don't allow yaxis to be reupdated/reanchored in a linked subplot diff --git a/src/backends/pyplot.jl b/src/backends/pyplot.jl index 9f9962e3..c510b3cc 100644 --- a/src/backends/pyplot.jl +++ b/src/backends/pyplot.jl @@ -862,9 +862,9 @@ end # -------------------------------------------------------------------------- -function py_set_lims(ax, axis::Axis) +function py_set_lims(ax, sp::Subplot, axis::Axis) letter = axis[:letter] - lfrom, lto = axis_limits(axis) + lfrom, lto = axis_limits(sp, letter) getproperty(ax, Symbol("set_", letter, "lim"))(lfrom, lto) end @@ -891,7 +891,7 @@ function py_set_ticks(ax, ticks, letter) end end -function py_compute_axis_minval(axis::Axis) +function py_compute_axis_minval(sp::Subplot, axis::Axis) # compute the smallest absolute value for the log scale's linear threshold minval = 1.0 sps = axis.sps @@ -905,13 +905,13 @@ function py_compute_axis_minval(axis::Axis) end # now if the axis limits go to a smaller abs value, use that instead - vmin, vmax = axis_limits(axis) + vmin, vmax = axis_limits(sp, axis[:letter]) minval = NaNMath.min(minval, abs(vmin), abs(vmax)) minval end -function py_set_scale(ax, axis::Axis) +function py_set_scale(ax, sp::Subplot, axis::Axis) scale = axis[:scale] letter = axis[:letter] scale in supported_scales() || return @warn("Unhandled scale value in pyplot: $scale") @@ -927,7 +927,7 @@ function py_set_scale(ax, axis::Axis) elseif scale == :log10 10 end - kw[Symbol(:linthresh,letter)] = NaNMath.min(1e-16, py_compute_axis_minval(axis)) + kw[Symbol(:linthresh,letter)] = NaNMath.min(1e-16, py_compute_axis_minval(sp, axis)) "symlog" end func(arg; kw...) @@ -1092,12 +1092,12 @@ function _before_layout_calcs(plt::Plot{PyPlotBackend}) if axis[:guide_position] != :auto && letter != :z pyaxis."set_label_position"(axis[:guide_position]) end - py_set_scale(ax, axis) - axis[:ticks] != :native ? py_set_lims(ax, axis) : nothing + py_set_scale(ax, sp, axis) + axis[:ticks] != :native ? py_set_lims(ax, sp, axis) : nothing if ispolar(sp) && letter == :y ax."set_rlabel_position"(90) end - ticks = sp[:framestyle] == :none ? nothing : get_ticks(axis) + ticks = sp[:framestyle] == :none ? nothing : get_ticks(sp, axis) # don't show the 0 tick label for the origin framestyle if sp[:framestyle] == :origin && length(ticks) > 1 ticks[2][ticks[1] .== 0] .= "" diff --git a/src/backends/unicodeplots.jl b/src/backends/unicodeplots.jl index a2e677c5..cabb89bb 100644 --- a/src/backends/unicodeplots.jl +++ b/src/backends/unicodeplots.jl @@ -27,8 +27,8 @@ function rebuildUnicodePlot!(plt::Plot, width, height) for sp in plt.subplots xaxis = sp[:xaxis] yaxis = sp[:yaxis] - xlim = axis_limits(xaxis) - ylim = axis_limits(yaxis) + xlim = axis_limits(sp, :x) + ylim = axis_limits(sp, :y) # make vectors xlim = [xlim[1], xlim[2]] diff --git a/src/layouts.jl b/src/layouts.jl index 2527cfc3..83ae259d 100644 --- a/src/layouts.jl +++ b/src/layouts.jl @@ -111,7 +111,7 @@ function resolve_mixed(mix::MixedMeasures, sp::Subplot, letter::Symbol) pct += mix.len / totlen end if pct != 0 - amin, amax = axis_limits(sp[Symbol(letter,:axis)]) + amin, amax = axis_limits(sp, letter) xy += pct * (amax-amin) end xy diff --git a/src/recipes.jl b/src/recipes.jl index ca2468e1..83869efa 100644 --- a/src/recipes.jl +++ b/src/recipes.jl @@ -96,19 +96,6 @@ const POTENTIAL_VECTOR_ARGUMENTS = [ end @deps line path - -function hvline_limits(axis::Axis) - vmin, vmax = axis_limits(axis) - if vmin >= vmax - if isfinite(vmin) - vmax = vmin + 1 - else - vmin, vmax = 0.0, 1.1 - end - end - vmin, vmax -end - @recipe function f(::Type{Val{:hline}}, x, y, z) n = length(y) newx = repeat(Float64[-1, 1, NaN], n) @@ -253,11 +240,12 @@ end n = length(x) fr = plotattributes[:fillrange] if fr == nothing - yaxis = plotattributes[:subplot][:yaxis] + sp = plotattributes[:subplot] + yaxis = sp[:yaxis] fr = if yaxis[:scale] == :identity 0.0 else - NaNMath.min(axis_limits(yaxis)[1], ignorenan_minimum(y)) + NaNMath.min(axis_limits(sp, :y)[1], ignorenan_minimum(y)) end end newx, newy = zeros(3n), zeros(3n) diff --git a/src/series.jl b/src/series.jl index cb6daaf2..89d41152 100644 --- a/src/series.jl +++ b/src/series.jl @@ -392,7 +392,7 @@ end @recipe function f(f::FuncOrFuncs{F}) where F<:Function plt = plotattributes[:plot_object] xmin, xmax = try - axis_limits(plt[1][:xaxis]) + axis_limits(plt[1], :x) catch xinv = invscalefunc(get(plotattributes, :xscale, :identity)) xm = tryrange(f, xinv.([-5,-1,0,0.01])) diff --git a/src/utils.jl b/src/utils.jl index 6a8cb81e..7b04e22a 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -508,7 +508,7 @@ function concatenate_fillrange(x,y::Tuple) end function get_sp_lims(sp::Subplot, letter::Symbol) - axis_limits(sp[Symbol(letter, :axis)]) + axis_limits(sp, letter) end """