plotlabelstataaxis-labelsyaxis

Force y axis to start at 0 and still use automated labeling


I have a plot whose y min starts well above 0. But I want to include 0 as the min of the y-axis and still have Stata automatically create evenly-spaced y-axis labels.

Here is the baseline:

sysuse auto2, clear
scatter turn displacement

This produces: enter image description here

This is almost what I want, except that the y range does not start at 0.

Based on this answer by Nick Cox (https://www.statalist.org/forums/forum/general-stata-discussion/general/1598753-force-chart-y-axis-to-start-at-0), I modify the code to be:

scatter turn displacement, yscale(range(0 .)) ylabel(0)

This succeeds in starting the y-axis at 0, but the labeling besides 0 goes away:

enter image description here

I proceed to remove `ylabel(0):

scatter turn displacement, yscale(range(0 .)) 

This produces the opposite problem - the y-axis labels are the same as in the first plot.

How can I have Stata automatically produce the y-axis labels from 0 to the max? For instance, 0, 10, 20, 30, 40, 50 - importantly, though, I have many plots and need a solution that determines the exact values automatically, without needing me to input the y max, etc. So it would not be me who chooses 10, 20, ..., 50, but Stata.

enter image description here


Solution

  • By coincidence, I have been working on a command in this territory. Here is a reproducible example.

    sysuse auto, clear
    summarize turn, meanonly
    local max = r(max) 
    nicelabels 0 `max', local(yla)
    * shows 0 20 40 60
    scatter turn displacement, yla(`yla', ang(h))
    nicelabels 0 `max', local(yla) nvals(10)
    * shows 0 10 20 30 40 50 60
    scatter turn displacement, yla(`yla', ang(h))
    

    where nicelabels is at present this code.

    *! 1.0.0 NJC 25 April 2022 
    program nicelabels           
        /// fudge() undocumented 
        version 9
    
        gettoken first 0 : 0, parse(" ,")  
    
        capture confirm numeric variable `first' 
    
        if _rc == 0 {
            // syntax varname(numeric), Local(str) [ nvals(int 5) tight Fudge(real 0) ] 
    
            syntax [if] [in] , Local(str) [ nvals(int 5) tight Fudge(real 0) ] 
            local varlist `first'  
    
            marksample touse 
            quietly count if `touse'    
            if r(N) == 0 exit 2000 
        } 
        else { 
            // syntax #1 #2 , Local(str) [ nvals(int 5) tight Fudge(real 0) ] 
    
            confirm number `first' 
            gettoken second 0 : 0, parse(" ,") 
            syntax , Local(str) [ nvals(int 5) tight Fudge(real 0) ]
     
            if _N < 2 { 
                preserve 
                quietly set obs 2 
            }
        
            tempvar varlist touse 
            gen double `varlist' = cond(_n == 1, `first', `second') 
            gen byte `touse' = _n <= 2 
        }   
    
        su `varlist' if `touse', meanonly
        local min = r(min) - (r(max) - r(min)) * `fudge'/100 
        local max = r(max) + (r(max) - r(min)) * `fudge'/100 
     
        local tight = "`tight'" == "tight"
        mata: nicelabels(`min', `max', `nvals', `tight') 
    
        di "`results'"
        c_local `local' "`results'"
    end  
    
    mata : 
    
    void nicelabels(real min, real max, real nvals, real tight) { 
        if (min == max) {
            st_local("results", min) 
            exit(0) 
        }
    
        real range, d, newmin, newmax
        colvector nicevals 
        range = nicenum(max - min, 0) 
        d = nicenum(range / (nvals - 1), 1)
        newmin = tight == 0 ? d * floor(min / d) : d * ceil(min / d)
        newmax = tight == 0 ? d * ceil(max / d) : d * floor(max / d)  
        nvals = 1 + (newmax - newmin) / d 
        nicevals = newmin :+ (0 :: nvals - 1) :* d  
        st_local("results", invtokens(strofreal(nicevals')))   
    }
    
    real nicenum(real x, real round) { 
        real expt, f, nf 
        
        expt = floor(log10(x)) 
        f = x / (10^expt) 
        
        if (round) { 
            if (f < 1.5) nf = 1 
            else if (f < 3) nf = 2
            else if (f < 7) nf = 5
            else nf = 10 
        }
        else { 
            if (f <= 1) nf = 1 
            else if (f <= 2) nf = 2 
            else if (f <= 5) nf = 5 
            else nf = 10 
        }
    
        return(nf * 10^expt)
    }
    
    end 
    

    EDIT

    If you go

    sysuse auto, clear 
    summarize turn, meanonly 
    local max = r(max) 
    scatter turn displacement, yla(0(10)`max', ang(h))
    scatter turn displacement, yla(0(20)`max', ang(h))
    

    you get good solutions. Clearly in this case we need to know that 10 or 20 is a step size to use. There would be scope to calculate a good width programmatically using your own recipe.

    EDIT 10 May 2022

    A revised and documented nicelabels is now downloadable from SSC.

    EDIT 2 28 Sept 2023 A write-up is now accessible in this Stata Journal paper.