# xaxis(args...; kw...) = Axis(:x, args...; kw...) # yaxis(args...; kw...) = Axis(:y, args...; kw...) # zaxis(args...; kw...) = Axis(:z, args...; kw...) # ------------------------------------------------------------------------- function Axis(sp::Subplot, letter::Symbol, args...; kw...) explicit = KW( :letter => letter, :extrema => Extrema(), :discrete_map => Dict(), # map discrete values to discrete indices :continuous_values => zeros(0), :discrete_values => [], :use_minor => false, :show => true, # show or hide the axis? (useful for linked subplots) ) attr = DefaultsDict(explicit, _axis_defaults_byletter[letter]) # update the defaults attr!(Axis([sp], attr), args...; kw...) end function get_axis(sp::Subplot, letter::Symbol) axissym = Symbol(letter, :axis) if haskey(sp.attr, axissym) sp.attr[axissym] else sp.attr[axissym] = Axis(sp, letter) end::Axis end function process_axis_arg!(plotattributes::AKW, arg, letter = "") T = typeof(arg) arg = get(_scaleAliases, arg, arg) if typeof(arg) <: Font plotattributes[Symbol(letter,:tickfont)] = arg plotattributes[Symbol(letter,:guidefont)] = arg elseif arg in _allScales plotattributes[Symbol(letter,:scale)] = arg elseif arg in (:flip, :invert, :inverted) plotattributes[Symbol(letter,:flip)] = true elseif T <: AbstractString plotattributes[Symbol(letter,:guide)] = arg # xlims/ylims elseif (T <: Tuple || T <: AVec) && length(arg) == 2 sym = typeof(arg[1]) <: Number ? :lims : :ticks plotattributes[Symbol(letter,sym)] = arg # xticks/yticks elseif T <: AVec plotattributes[Symbol(letter,:ticks)] = arg elseif arg === nothing plotattributes[Symbol(letter,:ticks)] = [] elseif T <: Bool || arg in _allShowaxisArgs plotattributes[Symbol(letter,:showaxis)] = showaxis(arg, letter) elseif typeof(arg) <: Number plotattributes[Symbol(letter,:rotation)] = arg elseif typeof(arg) <: Function plotattributes[Symbol(letter,:formatter)] = arg elseif !handleColors!(plotattributes, arg, Symbol(letter, :foreground_color_axis)) @warn("Skipped $(letter)axis arg $arg") end end # update an Axis object with magic args and keywords function attr!(axis::Axis, args...; kw...) # first process args plotattributes = axis.plotattributes for arg in args process_axis_arg!(plotattributes, arg) end # then preprocess keyword arguments RecipesPipeline.preprocess_attributes!(KW(kw)) # then override for any keywords... only those keywords that already exists in plotattributes for (k,v) in kw if haskey(plotattributes, k) if k == :discrete_values # add these discrete values to the axis for vi in v discrete_value!(axis, vi) end else plotattributes[k] = v end end end # replace scale aliases if haskey(_scaleAliases, plotattributes[:scale]) plotattributes[:scale] = _scaleAliases[plotattributes[:scale]] end axis end # ------------------------------------------------------------------------- Base.show(io::IO, axis::Axis) = dumpdict(io, axis.plotattributes, "Axis", true) # Base.getindex(axis::Axis, k::Symbol) = getindex(axis.plotattributes, k) Base.setindex!(axis::Axis, v, ks::Symbol...) = setindex!(axis.plotattributes, v, ks...) Base.haskey(axis::Axis, k::Symbol) = haskey(axis.plotattributes, k) ignorenan_extrema(axis::Axis) = (ex = axis[:extrema]; (ex.emin, ex.emax)) const _label_func = Dict{Symbol,Function}( :log10 => x -> "10^$x", :log2 => x -> "2^$x", :ln => x -> "e^$x", ) labelfunc(scale::Symbol, backend::AbstractBackend) = get(_label_func, scale, string) function optimal_ticks_and_labels(sp::Subplot, axis::Axis, ticks = nothing) amin, amax = axis_limits(sp, axis[:letter]) # scale the limits scale = axis[:scale] sf = RecipesPipeline.scale_func(scale) # If the axis input was a Date or DateTime use a special logic to find # "round" Date(Time)s as ticks # This bypasses the rest of optimal_ticks_and_labels, because # optimize_datetime_ticks returns ticks AND labels: the label format (Date # or DateTime) is chosen based on the time span between amin and amax # rather than on the input format # TODO: maybe: non-trivial scale (:ln, :log2, :log10) for date/datetime if ticks === nothing && scale == :identity if axis[:formatter] == RecipesPipeline.dateformatter # optimize_datetime_ticks returns ticks and labels(!) based on # integers/floats corresponding to the DateTime type. Thus, the axes # limits, which resulted from converting the Date type to integers, # are converted to 'DateTime integers' (actually floats) before # being passed to optimize_datetime_ticks. # (convert(Int, convert(DateTime, convert(Date, i))) == 87600000*i) ticks, labels = optimize_datetime_ticks(864e5 * amin, 864e5 * amax; k_min = 2, k_max = 4) # Now the ticks are converted back to floats corresponding to Dates. return ticks / 864e5, labels elseif axis[:formatter] == RecipesPipeline.datetimeformatter return optimize_datetime_ticks(amin, amax; k_min = 2, k_max = 4) end end # get a list of well-laid-out ticks if ticks === nothing scaled_ticks = optimize_ticks( sf(amin), sf(amax); k_min = 4, # minimum number of ticks k_max = 8, # maximum number of ticks )[1] elseif typeof(ticks) <: Int scaled_ticks, viewmin, viewmax = optimize_ticks( sf(amin), sf(amax); k_min = ticks, # minimum number of ticks k_max = ticks, # maximum number of ticks k_ideal = ticks, # `strict_span = false` rewards cases where the span of the # chosen ticks is not too much bigger than amin - amax: strict_span = false, ) axis[:lims] = map(RecipesPipeline.inverse_scale_func(scale), (viewmin, viewmax)) else scaled_ticks = map(sf, (filter(t -> amin <= t <= amax, ticks))) end unscaled_ticks = map(RecipesPipeline.inverse_scale_func(scale), scaled_ticks) labels = if any(isfinite, unscaled_ticks) formatter = axis[:formatter] if formatter == :auto # the default behavior is to make strings of the scaled values and then apply the labelfunc map(labelfunc(scale, backend()), Showoff.showoff(scaled_ticks, :auto)) elseif formatter == :plain # Leave the numbers in plain format map(labelfunc(scale, backend()), Showoff.showoff(scaled_ticks, :plain)) elseif formatter == :scientific Showoff.showoff(unscaled_ticks, :scientific) elseif formatter == :latex map(x -> string("\$", replace(convert_sci_unicode(x), '×' => "\\times"), "\$"), Showoff.showoff(unscaled_ticks, :auto)) else # there was an override for the formatter... use that on the unscaled ticks map(formatter, unscaled_ticks) # if the formatter left us with numbers, still apply the default formatter # However it leave us with the problem of unicode number decoding by the backend # if eltype(unscaled_ticks) <: Number # Showoff.showoff(unscaled_ticks, :auto) # end end else # no finite ticks to show... String[] end # @show unscaled_ticks labels # labels = Showoff.showoff(unscaled_ticks, scale == :log10 ? :scientific : :auto) unscaled_ticks, labels end # return (continuous_values, discrete_values) for the ticks on this axis function get_ticks(sp::Subplot, axis::Axis) ticks = _transform_ticks(axis[:ticks]) ticks in (:none, nothing, false) && return nothing # treat :native ticks as :auto ticks = ticks == :native ? :auto : ticks dvals = axis[:discrete_values] cv, dv = if typeof(ticks) <: Symbol if !isempty(dvals) # discrete ticks... n = length(dvals) rng = if ticks == :auto Int[round(Int,i) for i in range(1, stop=n, length=min(n,15))] else # if ticks == :all 1:n end axis[:continuous_values][rng], dvals[rng] elseif ispolar(axis.sps[1]) && axis[:letter] == :x #force theta axis to be full circle (collect(0:pi/4:7pi/4), string.(0:45:315)) else # compute optimal ticks and labels optimal_ticks_and_labels(sp, axis) end elseif typeof(ticks) <: Union{AVec, Int} if !isempty(dvals) && typeof(ticks) <: Int rng = Int[round(Int,i) for i in range(1, stop=length(dvals), length=ticks)] axis[:continuous_values][rng], dvals[rng] else # override ticks, but get the labels optimal_ticks_and_labels(sp, axis, ticks) end elseif typeof(ticks) <: NTuple{2, Any} # assuming we're passed (ticks, labels) ticks else error("Unknown ticks type in get_ticks: $(typeof(ticks))") end # @show ticks dvals cv dv return cv, dv end _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(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(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]) elseif axis[:scale] in (:none, :identity) ratio = 1 else return nothing end first_step = ticks[2] - ticks[1] last_step = ticks[end] - ticks[end-1] ticks = [ticks[1] - first_step/ratio; ticks; ticks[end] + last_step*ratio] #Default to 5 intervals between major ticks n = typeof(axis[:minorticks]) <: Integer && axis[:minorticks] > 1 ? axis[:minorticks] : 5 minorticks = typeof(ticks[1])[] for (i,hi) in enumerate(ticks[2:end]) lo = ticks[i] if isfinite(lo) && isfinite(hi) && hi > lo append!(minorticks,collect(lo + (hi-lo)/n :(hi-lo)/n: hi - (hi-lo)/2n)) end end minorticks[amin .<= minorticks .<= amax] end # ------------------------------------------------------------------------- function reset_extrema!(sp::Subplot) for asym in (:x,:y,:z) sp[Symbol(asym,:axis)][:extrema] = Extrema() end for series in sp.series_list expand_extrema!(sp, series.plotattributes) end end function expand_extrema!(ex::Extrema, v::Number) ex.emin = isfinite(v) ? min(v, ex.emin) : ex.emin ex.emax = isfinite(v) ? max(v, ex.emax) : ex.emax ex end function expand_extrema!(axis::Axis, v::Number) expand_extrema!(axis[:extrema], v) end # these shouldn't impact the extrema expand_extrema!(axis::Axis, ::Nothing) = axis[:extrema] expand_extrema!(axis::Axis, ::Bool) = axis[:extrema] function expand_extrema!(axis::Axis, v::Tuple{MIN,MAX}) where {MIN<:Number,MAX<:Number} ex = axis[:extrema] ex.emin = isfinite(v[1]) ? min(v[1], ex.emin) : ex.emin ex.emax = isfinite(v[2]) ? max(v[2], ex.emax) : ex.emax ex end function expand_extrema!(axis::Axis, v::AVec{N}) where N<:Number ex = axis[:extrema] for vi in v expand_extrema!(ex, vi) end ex end function expand_extrema!(sp::Subplot, plotattributes::AKW) vert = isvertical(plotattributes) # first expand for the data for letter in (:x, :y, :z) data = plotattributes[if vert letter else letter == :x ? :y : letter == :y ? :x : :z end] if letter != :z && plotattributes[:seriestype] == :straightline && any(series[:seriestype] != :straightline for series in series_list(sp)) && data[1] != data[2] data = [NaN] end axis = sp[Symbol(letter, "axis")] if isa(data, Volume) expand_extrema!(sp[:xaxis], data.x_extents) expand_extrema!(sp[:yaxis], data.y_extents) expand_extrema!(sp[:zaxis], data.z_extents) elseif eltype(data) <: Number || (isa(data, Surface) && all(di -> isa(di, Number), data.surf)) if !(eltype(data) <: Number) # huh... must have been a mis-typed surface? lets swap it out data = plotattributes[letter] = Surface(Matrix{Float64}(data.surf)) end expand_extrema!(axis, data) elseif data !== nothing # TODO: need more here... gotta track the discrete reference value # as well as any coord offset (think of boxplot shape coords... they all # correspond to the same x-value) plotattributes[letter], plotattributes[Symbol(letter,"_discrete_indices")] = discrete_value!(axis, data) expand_extrema!(axis, plotattributes[letter]) end end # # expand for fillrange/bar_width # fillaxis, baraxis = sp.attr[:yaxis], sp.attr[:xaxis] # if isvertical(plotattributes) # fillaxis, baraxis = baraxis, fillaxis # end # expand for fillrange fr = plotattributes[:fillrange] if fr === nothing && plotattributes[:seriestype] == :bar fr = 0.0 end if fr !== nothing && !RecipesPipeline.is3d(plotattributes) axis = sp.attr[vert ? :yaxis : :xaxis] if typeof(fr) <: Tuple for fri in fr expand_extrema!(axis, fri) end else expand_extrema!(axis, fr) end end # expand for bar_width if plotattributes[:seriestype] == :bar dsym = vert ? :x : :y data = plotattributes[dsym] bw = plotattributes[:bar_width] if bw === nothing bw = plotattributes[:bar_width] = _bar_width * ignorenan_minimum(filter(x->x>0,diff(sort(data)))) end axis = sp.attr[Symbol(dsym, :axis)] expand_extrema!(axis, ignorenan_maximum(data) + 0.5maximum(bw)) expand_extrema!(axis, ignorenan_minimum(data) - 0.5minimum(bw)) end # expand for heatmaps if plotattributes[:seriestype] == :heatmap for letter in (:x, :y) data = plotattributes[letter] axis = sp[Symbol(letter, "axis")] scale = get(plotattributes, Symbol(letter, "scale"), :identity) expand_extrema!(axis, heatmap_edges(data, scale)) end end end function expand_extrema!(sp::Subplot, xmin, xmax, ymin, ymax) expand_extrema!(sp[:xaxis], (xmin, xmax)) expand_extrema!(sp[:yaxis], (ymin, ymax)) end # ------------------------------------------------------------------------- # push the limits out slightly function widen(lmin, lmax, scale = :identity) f, invf = RecipesPipeline.scale_func(scale), RecipesPipeline.inverse_scale_func(scale) span = f(lmax) - f(lmin) # eps = NaNMath.max(1e-16, min(1e-2span, 1e-10)) eps = NaNMath.max(1e-16, 0.03span) invf(f(lmin)-eps), invf(f(lmax)+eps) end # figure out if widening is a good idea. const _widen_seriestypes = (:line, :path, :steppre, :steppost, :sticks, :scatter, :barbins, :barhist, :histogram, :scatterbins, :scatterhist, :stepbins, :stephist, :bins2d, :histogram2d, :bar, :shape, :path3d, :scatter3d) function default_should_widen(axis::Axis) should_widen = false if !(is_2tuple(axis[:lims]) || axis[:lims] == :round) for sp in axis.sps for series in series_list(sp) if series.plotattributes[:seriestype] in _widen_seriestypes should_widen = true end end end end should_widen end function round_limits(amin,amax) scale = 10^(1-round(log10(amax - amin))) amin = floor(amin*scale)/scale amax = ceil(amax*scale)/scale amin, amax end # using the axis extrema and limit overrides, return the min/max value for this 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] has_user_lims = (isa(lims, Tuple) || isa(lims, AVec)) && length(lims) == 2 if has_user_lims lmin, lmax = lims if lmin != :auto && isfinite(lmin) amin = lmin end if lmax != :auto && isfinite(lmax) amax = lmax end end if amax <= amin && isfinite(amin) amax = amin + 1.0 end if !isfinite(amin) && !isfinite(amax) amin, amax = 0.0, 1.0 end amin, amax = if ispolar(axis.sps[1]) if axis[:letter] == :x amin, amax = 0, 2pi elseif lims == :auto #widen max radius so ticks dont overlap with theta axis 0, amax + 0.1 * abs(amax - amin) else amin, amax end elseif should_widen && axis[:widen] widen(amin, amax, axis[:scale]) elseif lims == :round round_limits(amin,amax) else amin, amax end if !has_user_lims && consider_aspect && letter in (:x, :y) && !(sp[:aspect_ratio] in (:none, :auto) || RecipesPipeline.is3d(:sp)) aspect_ratio = isa(sp[:aspect_ratio], Number) ? sp[:aspect_ratio] : 1 plot_ratio = height(plotarea(sp)) / width(plotarea(sp)) 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 # ------------------------------------------------------------------------- # these methods track the discrete (categorical) values which correspond to axis continuous values (cv) # whenever we have discrete values, we automatically set the ticks to match. # we return (continuous_value, discrete_index) function discrete_value!(axis::Axis, dv) cv_idx = get(axis[:discrete_map], dv, -1) # @show axis[:discrete_map], axis[:discrete_values], dv if cv_idx == -1 ex = axis[:extrema] cv = NaNMath.max(0.5, ex.emax + 1.0) expand_extrema!(axis, cv) push!(axis[:discrete_values], dv) push!(axis[:continuous_values], cv) cv_idx = length(axis[:discrete_values]) axis[:discrete_map][dv] = cv_idx cv, cv_idx else cv = axis[:continuous_values][cv_idx] cv, cv_idx end end # continuous value... just pass back with axis negative index function discrete_value!(axis::Axis, cv::Number) cv, -1 end # add the discrete value for each item. return the continuous values and the indices function discrete_value!(axis::Axis, v::AVec) n = eachindex(v) cvec = zeros(axes(v)) discrete_indices = similar(Array{Int}, axes(v)) for i in n cvec[i], discrete_indices[i] = discrete_value!(axis, v[i]) end cvec, discrete_indices end # add the discrete value for each item. return the continuous values and the indices function discrete_value!(axis::Axis, v::AMat) n,m = axes(v) cmat = zeros(axes(v)) discrete_indices = similar(Array{Int}, axes(v)) for i in n, j in m cmat[i,j], discrete_indices[i,j] = discrete_value!(axis, v[i,j]) end cmat, discrete_indices end function discrete_value!(axis::Axis, v::Surface) map(Surface, discrete_value!(axis, v.surf)) 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(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) ytick_segs = Segments(2) xgrid_segs = Segments(2) ygrid_segs = Segments(2) xminorgrid_segs = Segments(2) yminorgrid_segs = Segments(2) xborder_segs = Segments(2) yborder_segs = Segments(2) if sp[:framestyle] != :none # xaxis y1, y2 = if sp[:framestyle] in (:origin, :zerolines) 0.0, 0.0 else xor(xaxis[:mirror], yaxis[:flip]) ? (ymax, ymin) : (ymin, ymax) end if xaxis[:showaxis] if sp[:framestyle] != :grid push!(xaxis_segs, (xmin, y1), (xmax, y1)) # don't show the 0 tick label for the origin framestyle if sp[:framestyle] == :origin && !(xticks in (:none, nothing, false)) && length(xticks) > 1 showticks = xticks[1] .!= 0 xticks = (xticks[1][showticks], xticks[2][showticks]) end end sp[:framestyle] in (:semi, :box) && push!(xborder_segs, (xmin, y2), (xmax, y2)) # top spine end if !(xaxis[:ticks] in (:none, nothing, false)) f = RecipesPipeline.scale_func(yaxis[:scale]) invf = RecipesPipeline.inverse_scale_func(yaxis[:scale]) tick_start, tick_stop = if sp[:framestyle] == :origin t = invf(f(0) + 0.012 * (f(ymax) - f(ymin))) (-t, t) else ticks_in = xaxis[:tick_direction] == :out ? -1 : 1 t = invf(f(y1) + 0.012 * (f(y2) - f(y1)) * ticks_in) (y1, t) end for xtick in xticks[1] if xaxis[:showaxis] push!(xtick_segs, (xtick, tick_start), (xtick, tick_stop)) # bottom tick end xaxis[:grid] && push!(xgrid_segs, (xtick, ymin), (xtick, ymax)) # vertical grid end if !(xaxis[:minorticks] in (:none, nothing, false)) || xaxis[:minorgrid] tick_start, tick_stop = if sp[:framestyle] == :origin t = invf(f(0) + 0.006 * (f(ymax) - f(ymin))) (-t, t) else t = invf(f(y1) + 0.006 * (f(y2) - f(y1)) * ticks_in) (y1, t) end for xtick in xminorticks if xaxis[:showaxis] push!(xtick_segs, (xtick, tick_start), (xtick, tick_stop)) # bottom tick end xaxis[:minorgrid] && push!(xminorgrid_segs, (xtick, ymin), (xtick, ymax)) # vertical grid end end end # yaxis x1, x2 = if sp[:framestyle] in (:origin, :zerolines) 0.0, 0.0 else xor(yaxis[:mirror], xaxis[:flip]) ? (xmax, xmin) : (xmin, xmax) end if yaxis[:showaxis] if sp[:framestyle] != :grid push!(yaxis_segs, (x1, ymin), (x1, ymax)) # don't show the 0 tick label for the origin framestyle if sp[:framestyle] == :origin && !(yticks in (:none, nothing,false)) && length(yticks) > 1 showticks = yticks[1] .!= 0 yticks = (yticks[1][showticks], yticks[2][showticks]) end end sp[:framestyle] in (:semi, :box) && push!(yborder_segs, (x2, ymin), (x2, ymax)) # right spine end if !(yaxis[:ticks] in (:none, nothing, false)) f = RecipesPipeline.scale_func(xaxis[:scale]) invf = RecipesPipeline.inverse_scale_func(xaxis[:scale]) tick_start, tick_stop = if sp[:framestyle] == :origin t = invf(f(0) + 0.012 * (f(xmax) - f(xmin))) (-t, t) else ticks_in = yaxis[:tick_direction] == :out ? -1 : 1 t = invf(f(x1) + 0.012 * (f(x2) - f(x1)) * ticks_in) (x1, t) end for ytick in yticks[1] if yaxis[:showaxis] push!(ytick_segs, (tick_start, ytick), (tick_stop, ytick)) # left tick end yaxis[:grid] && push!(ygrid_segs, (xmin, ytick), (xmax, ytick)) # horizontal grid end if !(yaxis[:minorticks] in (:none, nothing, false)) || yaxis[:minorgrid] tick_start, tick_stop = if sp[:framestyle] == :origin t = invf(f(0) + 0.006 * (f(xmax) - f(xmin))) (-t, t) else t = invf(f(x1) + 0.006 * (f(x2) - f(x1)) * ticks_in) (x1, t) end for ytick in yminorticks if yaxis[:showaxis] push!(ytick_segs, (tick_start, ytick), (tick_stop, ytick)) # left tick end yaxis[:minorgrid] && push!(yminorgrid_segs, (xmin, ytick), (xmax, ytick)) # horizontal grid end end end end xticks, yticks, xaxis_segs, yaxis_segs, xtick_segs, ytick_segs, xgrid_segs, ygrid_segs, xminorgrid_segs, yminorgrid_segs, xborder_segs, yborder_segs end function axis_drawing_info_3d(sp::Subplot) xaxis, yaxis, zaxis = sp[:xaxis], sp[:yaxis], sp[:zaxis] xmin, xmax = axis_limits(sp, :x) ymin, ymax = axis_limits(sp, :y) zmin, zmax = axis_limits(sp, :z) xticks = get_ticks(sp, xaxis) yticks = get_ticks(sp, yaxis) zticks = get_ticks(sp, zaxis) xminorticks = get_minor_ticks(sp, xaxis, xticks) yminorticks = get_minor_ticks(sp, yaxis, yticks) zminorticks = get_minor_ticks(sp, zaxis, zticks) xaxis_segs = Segments(3) yaxis_segs = Segments(3) zaxis_segs = Segments(3) xtick_segs = Segments(3) ytick_segs = Segments(3) ztick_segs = Segments(3) xgrid_segs = Segments(3) ygrid_segs = Segments(3) zgrid_segs = Segments(3) xminorgrid_segs = Segments(3) yminorgrid_segs = Segments(3) zminorgrid_segs = Segments(3) xborder_segs = Segments(3) yborder_segs = Segments(3) zborder_segs = Segments(3) if sp[:framestyle] != :none # xaxis y1, y2 = if sp[:framestyle] in (:origin, :zerolines) 0.0, 0.0 else xor(xaxis[:mirror], yaxis[:flip]) ? (ymax, ymin) : (ymin, ymax) end z1, z2 = if sp[:framestyle] in (:origin, :zerolines) 0.0, 0.0 else xor(xaxis[:mirror], zaxis[:flip]) ? (zmax, zmin) : (zmin, zmax) end if xaxis[:showaxis] if sp[:framestyle] != :grid push!(xaxis_segs, (xmin, y1, z1), (xmax, y1, z1)) # don't show the 0 tick label for the origin framestyle if sp[:framestyle] == :origin && !(xticks in (:none, nothing, false)) && length(xticks) > 1 showticks = xticks[1] .!= 0 xticks = (xticks[1][showticks], xticks[2][showticks]) end end sp[:framestyle] in (:semi, :box) && push!(xborder_segs, (xmin, y2, z2), (xmax, y2, z2)) # top spine end if !(xaxis[:ticks] in (:none, nothing, false)) f = RecipesPipeline.scale_func(yaxis[:scale]) invf = RecipesPipeline.inverse_scale_func(yaxis[:scale]) tick_start, tick_stop = if sp[:framestyle] == :origin t = invf(f(0) + 0.012 * (f(ymax) - f(ymin))) (-t, t) else ticks_in = xaxis[:tick_direction] == :out ? -1 : 1 t = invf(f(y1) + 0.012 * (f(y2) - f(y1)) * ticks_in) (y1, t) end for xtick in xticks[1] if xaxis[:showaxis] push!(xtick_segs, (xtick, tick_start, z1), (xtick, tick_stop, z1)) # bottom tick end if xaxis[:grid] if sp[:framestyle] in (:origin, :zerolines) push!(xgrid_segs, (xtick, ymin, 0.0), (xtick, ymax, 0.0)) push!(xgrid_segs, (xtick, 0.0, zmin), (xtick, 0.0, zmax)) else push!(xgrid_segs, (xtick, y1, z1), (xtick, y2, z1)) push!(xgrid_segs, (xtick, y2, z1), (xtick, y2, z2)) end end end if !(xaxis[:minorticks] in (:none, nothing, false)) || xaxis[:minorgrid] tick_start, tick_stop = if sp[:framestyle] == :origin t = invf(f(0) + 0.006 * (f(ymax) - f(ymin))) (-t, t) else t = invf(f(y1) + 0.006 * (f(y2) - f(y1)) * ticks_in) (y1, t) end for xtick in xminorticks if xaxis[:showaxis] push!(xtick_segs, (xtick, tick_start, z1), (xtick, tick_stop, z1)) # bottom tick end if xaxis[:minorgrid] if sp[:framestyle] in (:origin, :zerolines) push!(xminorgrid_segs, (xtick, ymin, 0.0), (xtick, ymax, 0.0)) push!(xminorgrid_segs, (xtick, 0.0, zmin), (xtick, 0.0, zmax)) else push!(xminorgrid_segs, (xtick, y1, z1), (xtick, y2, z1)) push!(xminorgrid_segs, (xtick, y2, z1), (xtick, y2, z2)) end end end end end # yaxis x1, x2 = if sp[:framestyle] in (:origin, :zerolines) 0.0, 0.0 else xor(yaxis[:mirror], xaxis[:flip]) ? (xmin, xmax) : (xmax, xmin) end z1, z2 = if sp[:framestyle] in (:origin, :zerolines) 0.0, 0.0 else xor(yaxis[:mirror], zaxis[:flip]) ? (zmax, zmin) : (zmin, zmax) end if yaxis[:showaxis] if sp[:framestyle] != :grid push!(yaxis_segs, (x1, ymin, z1), (x1, ymax, z1)) # don't show the 0 tick label for the origin framestyle if sp[:framestyle] == :origin && !(yticks in (:none, nothing,false)) && length(yticks) > 1 showticks = yticks[1] .!= 0 yticks = (yticks[1][showticks], yticks[2][showticks]) end end sp[:framestyle] in (:semi, :box) && push!(yborder_segs, (x2, ymin, z2), (x2, ymax, z2)) # right spine end if !(yaxis[:ticks] in (:none, nothing, false)) f = RecipesPipeline.scale_func(xaxis[:scale]) invf = RecipesPipeline.inverse_scale_func(xaxis[:scale]) tick_start, tick_stop = if sp[:framestyle] == :origin t = invf(f(0) + 0.012 * (f(xmax) - f(xmin))) (-t, t) else ticks_in = yaxis[:tick_direction] == :out ? -1 : 1 t = invf(f(x1) + 0.012 * (f(x2) - f(x1)) * ticks_in) (x1, t) end for ytick in yticks[1] if yaxis[:showaxis] push!(ytick_segs, (tick_start, ytick, z1), (tick_stop, ytick, z1)) # left tick end if yaxis[:grid] if sp[:framestyle] in (:origin, :zerolines) push!(ygrid_segs, (xmin, ytick, 0.0), (xmax, ytick, 0.0)) push!(ygrid_segs, (0.0, ytick, zmin), (0.0, ytick, zmax)) else push!(ygrid_segs, (x1, ytick, z1), (x2, ytick, z1)) push!(ygrid_segs, (x2, ytick, z1), (x2, ytick, z2)) end end end if !(yaxis[:minorticks] in (:none, nothing, false)) || yaxis[:minorgrid] tick_start, tick_stop = if sp[:framestyle] == :origin t = invf(f(0) + 0.006 * (f(xmax) - f(xmin))) (-t, t) else t = invf(f(x1) + 0.006 * (f(x2) - f(x1)) * ticks_in) (x1, t) end for ytick in yminorticks if yaxis[:showaxis] push!(ytick_segs, (tick_start, ytick, z1), (tick_stop, ytick, z1)) # left tick end if yaxis[:minorgrid] if sp[:framestyle] in (:origin, :zerolines) push!(yminorgrid_segs, (xmin, ytick, 0.0), (xmax, ytick, 0.0)) push!(yminorgrid_segs, (0.0, ytick, zmin), (0.0, ytick, zmax)) else push!(yminorgrid_segs, (x1, ytick, z1), (x2, ytick, z1)) push!(yminorgrid_segs, (x2, ytick, z1), (x2, ytick, z2)) end end end end end # zaxis x1, x2 = if sp[:framestyle] in (:origin, :zerolines) 0.0, 0.0 else xor(zaxis[:mirror], xaxis[:flip]) ? (xmax, xmin) : (xmin, xmax) end y1, y2 = if sp[:framestyle] in (:origin, :zerolines) 0.0, 0.0 else xor(zaxis[:mirror], yaxis[:flip]) ? (ymax, ymin) : (ymin, ymax) end if zaxis[:showaxis] if sp[:framestyle] != :grid push!(zaxis_segs, (x1, y1, zmin), (x1, y1, zmax)) # don't show the 0 tick label for the origin framestyle if sp[:framestyle] == :origin && !(zticks in (:none, nothing,false)) && length(zticks) > 1 showticks = zticks[1] .!= 0 zticks = (zticks[1][showticks], zticks[2][showticks]) end end sp[:framestyle] in (:semi, :box) && push!(zborder_segs, (x2, y2, zmin), (x2, y2, zmax)) end if !(zaxis[:ticks] in (:none, nothing, false)) f = RecipesPipeline.scale_func(xaxis[:scale]) invf = RecipesPipeline.inverse_scale_func(xaxis[:scale]) tick_start, tick_stop = if sp[:framestyle] == :origin t = invf(f(0) + 0.012 * (f(ymax) - f(ymin))) (-t, t) else ticks_in = zaxis[:tick_direction] == :out ? -1 : 1 t = invf(f(y1) + 0.012 * (f(y2) - f(y1)) * ticks_in) (y1, t) end for ztick in zticks[1] if zaxis[:showaxis] push!(ztick_segs, (x1, tick_start, ztick), (x1, tick_stop, ztick)) # left tick end if zaxis[:grid] if sp[:framestyle] in (:origin, :zerolines) push!(zgrid_segs, (xmin, 0.0, ztick), (xmax, 0.0, ztick)) push!(ygrid_segs, (0.0, ymin, ztick), (0.0, ymax, ztick)) else push!(ygrid_segs, (x1, y1, ztick), (x1, y2, ztick)) push!(ygrid_segs, (x1, y2, ztick), (x2, y2, ztick)) end end end if !(zaxis[:minorticks] in (:none, nothing, false)) || zaxis[:minorgrid] tick_start, tick_stop = if sp[:framestyle] == :origin t = invf(f(0) + 0.006 * (f(ymax) - f(ymin))) (-t, t) else t = invf(f(y1) + 0.006 * (f(y2) - f(y1)) * ticks_in) (y1, t) end for ztick in zminorticks if zaxis[:showaxis] push!(ztick_segs, (x1, tick_start, ztick), (x1, tick_stop, ztick)) # left tick end if zaxis[:minorgrid] if sp[:framestyle] in (:origin, :zerolines) push!(zminorgrid_segs, (xmin, 0.0, ztick), (xmax, 0.0, ztick)) push!(zminorgrid_segs, (0.0, ymin, ztick), (0.0, ymax, ztick)) else push!(zminorgrid_segs, (x1, y1, ztick), (x1, y2, ztick)) push!(zminorgrid_segs, (x1, y2, ztick), (x2, y2, ztick)) end end end end end end xticks, yticks, zticks, xaxis_segs, yaxis_segs, zaxis_segs, xtick_segs, ytick_segs, ztick_segs, xgrid_segs, ygrid_segs, zgrid_segs, xminorgrid_segs, yminorgrid_segs, zminorgrid_segs, xborder_segs, yborder_segs, zborder_segs end