Axis Macro - Macro

Code

The macro can either be downloaded as a SAS file ( Download ), or it can be copied as text into a SAS program.

SAS
/*----------------------------------------------------------------------------------*

   *******************************************************
   ***  Copyright Rho, Inc. 2015, all rights reserved  ***
   *******************************************************

   MACRO:      AxisOrder.sas

   PURPOSE:    Generate components of the ORDER option in an AXIS statement for linear-scale axes.

   ARGUMENTS:  Data     => REQUIRED.   Input dataset.
               Var      => REQUIRED.   Space-separated list of variables on which to base axis range.
               Val      => OPTIONAL.   Space-separated list of additional values to include within axis range.
               By       => OPTIONAL.   By value override.
               Prefix   => OPTIONAL.   Prefix to add to output macro variable names.
               MajTarg  => OPTIONAL.   Desired number of major tick marks. 
                                       Default value is 6.
                                       This parameter has no effect if a value of By is specified.
               MajDev   => OPTIONAL.   Acceptable deviation from MajTarg. 
                                       Once the algorithm gets within MajDev of MajTarg, white space reduction
                                          drives the axis selection process.
                                       Default value is 1.
               Threshold=> OPTIONAL.   Thresholdmin/max value to use in deriving _&Prefix.AxisListUnb.
                                       Set to 0.7 to most closely approximate SGPLOT behavior.
                                       Default values is 0.
               RoundBys => OPTIONAL.   Roundness of by value candidates.
                                       Y=yes limits the tick interval search to 10, 20, 25, or 50. 
                                       N=no expands the tick interval search to 10, 12.5, 15, 20, 25, 30, 40, 
                                          50, 60, 75, or 80.
                                       Default value is Y.
               MinDens  => OPTIONAL.   Density of minor tick marks. 
                                       L=low, M=medium, H=high.
                                       Default value is M.

   OUTPUTS:    Macro variables
                  _&Prefix.AxisStart   Axis start value.
                  _&Prefix.AxisEnd     Axis end value.
                  _&Prefix.AxisBy      Axis increment/by value.
                  _&Prefix.AxisList    Space-separated list of major tick mark values.
                  _&Prefix.AxisMin     Minimum observed value in VAR or VAL.
                  _&Prefix.AxisMax     Maximum observed value in VAR or VAL.
                  _&Prefix.AxisListUnb Alternate version of AxisList that allows for unbounded data
                                       values by removing the first/last values if they are smaller/
                                       larger than the observed minimum/maximum.
                  _&Prefix.AxisMinor   Recommended number of minor tick marks.
                                       Returns zero when By is specified.

   EXAMPLE 1:  %AxisOrder(Data=work.adlb
                         ,Var=aval anrlo anrhi
                         ,Prefix=y
                         ,MajTarg=3
                         );

               axis1 order=(&_yAxisStart to &_yAxisEnd by &_yAxisBy) minor=(n=&_yAxisMinor);
               
   EXAMPLE 2:  %AxisOrder(Data=lb_alt
                         ,Var=lbstresn
                         ,Val=0
                         ,By=10
                         );

               axis1 order=(&_AxisStart to &_AxisEnd by &_AxisBy);
                               
   PROGRAM HISTORY:

   DATE        PROGRAMMER        DESCRIPTION
   ---------   ---------------   ----------------------------------------------------
   20140313    Shane Rosanbalm   Original program.
   20150616    Shane Rosanbalm   Add _AxisMin, _AxisMax, and _AxisListUnb.
   20150713    Shane Rosanbalm   Add Threshold.

*-----------------------------------------------------------------------------------*/

%MACRO AxisOrder
         (Data=
         ,Var=
         ,Val=
         ,By=
         ,Prefix=
         ,MajTarg=6
         ,MajDev=1
         ,Threshold=0
         ,RoundBys=Y
         ,MinDens=M
         );


   %*------------------------------------------------------------------------------;
   %*-------------------- before we begin --------------------;
   %*------------------------------------------------------------------------------;


   %*---------- output macro variables to be created ----------;
   %GLOBAL _&Prefix.AxisStart _&Prefix.AxisEnd _&Prefix.AxisBy _&Prefix.AxisList _&Prefix.AxisMinor
           _&Prefix.AxisMin _&Prefix.AxisMax _&Prefix.AxisListUnb;
      
      
   %*---------- capture option settings at macro invocation ----------;
   %let _mprint = %sysfunc(getoption(mprint));
   %let _notes  = %sysfunc(getoption(notes));
   %let _source = %sysfunc(getoption(source));
      
      
   %*---------- reset some options ----------;
   options nomprint nonotes nosource;
   
   
   %*------------------------------------------------------------------------------;
   %*-------------------- lots of pre-processing of parameters --------------------;
   %*------------------------------------------------------------------------------;
   
   
   %*---------- were required parameters provided ----------;
   %if %nrbquote(&Data) eq %then %do;
      %put AxisOrder -> NO INPUT DATASET WAS SPECIFIED.; 
      %put THE MACRO WILL STOP EXECUTING.;
      %goto badending;
   %end;
   %if %nrbquote(&Var) eq %then %do;
      %put AxisOrder -> NO INPUT VARIABLES WERE SPECIFIED.; 
      %put THE MACRO WILL STOP EXECUTING.;
      %goto badending;
   %end;

   
   %*---------- does Data exist ----------;
   %if %sysfunc(exist(&Data)) eq 0 %then %do;
      %put AxisOrder -> THE INPUT DATASET [ &Data ] DOES NOT EXIST.; 
      %put THE MACRO WILL STOP EXECUTING.;
      %goto badending;
   %end;
   
   
   %*---------- do Var items exist ----------;
   %let _badvar = 0;
   %do _vari = 1 %to %sysfunc(countw(&Var));;
      %let _varname = %scan(&Var,&_vari,%str( ));
      data _null_;
         dsid = open("&Data");
         check = varnum(dsid,"&_varname");
         if check = 0 then call symput('_badvar','1');
      run;
      %if &_badvar = 1 %then %do;
         %put AxisOrder -> THE INPUT VARIABLE [ &_varname ] DOES NOT EXIST.; 
         %put THE MACRO WILL STOP EXECUTING.;
         %goto badending;
      %end;
   %end;
   
   
   %*---------- are Val items numbers ----------;
   %if %length(%sysfunc(compress(%nrbquote(&val.),0123456789.-))) > 0 %then %do;
      %put AxisOrder -> THE INPUT VALUE [ &Val ] CONTAINS NON-NUMERIC CHARACTERS.; 
      %put THE MACRO WILL STOP EXECUTING.;
      %goto badending;
   %end;
   %if %nrbquote(&Val) ne %then %do;
      %let _badval = 0;
      %do _vali = 1 %to %sysfunc(countw(&Val));;
         %let _valvalue = %scan(&Val,&_vali,%str( ));
         data _null_;
            check = input("&_valvalue",??best.);
            if missing(check) then call symput('_badval','1');
         run;
         %if &_badval = 1 %then %do;
            %put AxisOrder -> THE Val ELEMENT [ &_valvalue ] IS NOT A VALID NUMBER.; 
            %put THE MACRO WILL STOP EXECUTING.;
            %goto badending;
         %end;
      %end;
   %end;
   
   
   %*---------- is By a positive number ----------;
   %if %nrbquote(&By) ne %then %do;
      %let _badby = 0;
      data _null_;
         check = input("&By",??best.);
         if check < 1e-5 then call symput('_badby','1');
      run;
      %if &_badby = 1 %then %do;
         %put AxisOrder -> THE INPUT BY VALUE [ &By ] IS NOT COMPATIBLE WITH THIS MACRO.; 
         %put THE MACRO WILL STOP EXECUTING.;
         %goto badending;
      %end;
   %end;
   
   
   %*---------- is MajTarg a positive integer ----------;
   %if %nrbquote(&MajTarg) ne %then %do;
      %let _badmajtarg = 0;
      data _null_;
         check = input("&MajTarg",??8.);
         if check < 1 then call symput('_badmajtarg','1');
      run;
      %if &_badmajtarg = 1 %then %do;
         %put AxisOrder -> THE INPUT VALUE [ &MajTarg ] IS NOT A POSITIVE INTEGER.; 
         %put THE MACRO WILL STOP EXECUTING.;
         %goto badending;
      %end;
   %end;
   
   
   %*---------- is MajDev a non-negative integer ----------;
   %if %nrbquote(&MajDev) ne %then %do;
      %let _badmajdev = 0;
      data _null_;
         check = input("&MajDev",??8.);
         if check < 0 then call symput('_badmajdev','1');
      run;
      %if &_badmajdev = 1 %then %do;
         %put AxisOrder -> THE INPUT VALUE [ &MajDev ] IS NOT A NON-NEGATIVE INTEGER.; 
         %put THE MACRO WILL STOP EXECUTING.;
         %goto badending;
      %end;
   %end;
   
   
   %*---------- is RoundBys valid ----------;
   %if %nrbquote(&RoundBys) ne %then %do;
      %let RoundBys = %upcase(&RoundBys);
      %let _badroundbys = 0;
      data _null_;
         if "&RoundBys" not in ('Y' 'N') then call symput('_badroundbys','1');
      run;
      %if &_badroundbys = 1 %then %do;
         %put AxisOrder -> THE INPUT VALUE [ &RoundBys ] IS NOT A VALID ROUNDNESS.; 
         %put THE MACRO WILL STOP EXECUTING.;
         %goto badending;
      %end;
   %end;
   
   
   %*---------- is MinDens valid ----------;
   %if %nrbquote(&MinDens) ne %then %do;
      %let MinDens = %upcase(&MinDens);
      %let _badmindens = 0;
      data _null_;
         if "&MinDens" not in ('L' 'M' 'H') then call symput('_badmindens','1');
      run;
      %if &_badmindens = 1 %then %do;
         %put AxisOrder -> THE INPUT VALUE [ &MinDens ] IS NOT A VALID MINOR DENSITY.; 
         %put THE MACRO WILL STOP EXECUTING.;
         %goto badending;
      %end;
   %end;
   
   
   %*------------------------------------------------------------------------------;
   %*-------------------- prepare the data for axis selection --------------------;
   %*------------------------------------------------------------------------------;
   
   
   %*---------- stack Val and Var information into vertical structure ----------;
   data _vert&Data;
      set &Data (keep=&Var) end=eof;
      %if &Var ne %then %do;
         %do _vari = 1 %to %sysfunc(countw(&Var));
            vert = %scan(&Var,&_vari,%str( ));
            output;
         %end;
      %end;
      if eof then do;
         %if &Val ne %then %do;
            %do _vali = 1 %to %sysfunc(countw(&Val));
               vert = %scan(&Val,&_vali,%str( ));
               output;
            %end;
         %end;
      end;
   run;
      
      
   %*---------- calculate min, max, range based on variables and values of interest ----------;
   proc means data=_vert&Data noprint;
      var vert;
      output out=_minmaxrange min=varmin max=varmax range=varrange;
   run;
   
   
   %*------------------------------------------------------------------------------;
   %*-------------------- the actual axis selection steps --------------------;
   %*------------------------------------------------------------------------------;
   
   
   data _axisorder;
      set _minmaxrange;
      if varrange = 0 then varrange = 1;

      %*---------- centralize constants associated with each by level ----------;
      %if &RoundBys eq Y %then %do;      
      
         %let _byn = 3;
         root1 = 1;     mult1 = 2/1;   low1 = 1;   med1 = 4;   high1 = 9;
         root2 = 2;     mult2 = 2/1;   low2 = 1;   med2 = 3;   high2 = 7;
         root3 = 2.5;   mult3 = 5/4;   low3 = 1;   med3 = 4;   high3 = 9;
         root0 = 5;     mult0 = 2/1;   low0 = 1;   med0 = 4;   high0 = 9;
         
      %end;
      
      %if &RoundBys eq N %then %do;
      
         %let _byn = 10;
         root1 = 1;     mult1 = 5/4;   low1 = 1;   med1 = 4;   high1 = 9;
         root2 = 1.25;  mult2 = 5/4;   low2 = 1;   med2 = 4;   high2 = 9;
         root3 = 1.5;   mult3 = 6/5;   low3 = 1;   med3 = 2;   high3 = 5;
         root4 = 2;     mult4 = 4/3;   low4 = 1;   med4 = 3;   high4 = 7;
         root5 = 2.5;   mult5 = 5/4;   low5 = 1;   med5 = 4;   high5 = 9;
         root6 = 3;     mult6 = 6/5;   low6 = 1;   med6 = 2;   high6 = 8;
         root7 = 4;     mult7 = 4/3;   low7 = 1;   med7 = 3;   high7 = 7;
         root8 = 5;     mult8 = 5/4;   low8 = 1;   med8 = 4;   high8 = 9;
         root9 = 6;     mult9 = 6/5;   low9 = 1;   med9 = 2;   high9 = 5;
         root10= 7.5;   mult10= 5/4;   low10= 1;   med10= 2;   high10= 5;
         root0 = 8;     mult0 = 16/15; low0 = 1;   med0 = 3;   high0 = 7;
      
      %end;
      
      %let _mod = %eval(&_byn+1);
      %let _min = 0.00001;
      %let _max = 1000000000;
      %let _precision = 0.0000001;
      %let _ratio = %sysfunc(log10(&_max/&_min));
      %let _arraymax = %eval(&_ratio*&_mod+1);
      
      array root[0:&_byn] root0-root&_byn;
      array mult[0:&_byn] mult0-mult&_byn;
      array low[0:&_byn] low0-low&_byn;
      array med[0:&_byn] med0-med&_byn;
      array high[0:&_byn] high0-high&_byn;
         
      %*---------- more work if By not prespecified ----------;
      %if &By eq %then %do;
      
         %*---------- create list of candidate levels ----------;
         array level {&_arraymax};
         do i = 1 to &_arraymax;
            if i = 1 then level[i] = &_min;
            else if i > 1 then do;
               level[i] = level[i-1] * mult[mod(i,&_mod)];
               level[i] = round(level[i],&_precision);
            end;
         end;
            
         array tempdiff {&_arraymax};
         array tempwaste {&_arraymax};
         ibest = 1;
         do i = 1 to &_arraymax;
         
            %*---------- calculate target deviation of candidate levels ----------;
            tempmin = level[i]*floor(varmin/level[i]);
            tempmax = level[i]*ceil(varmax/level[i]);
            tempmaj = round((tempmax-tempmin)/level[i] + 1, 1);
            tempdiff[i] = abs(tempmaj-&MajTarg);
            
            %*---------- calculate waste of candidate levels ----------;
            temprange = tempmax-tempmin;
            if temprange > 0 then do;
               topwaste = abs(tempmax-varmax)/temprange;
               botwaste = abs(tempmin-varmin)/temprange;
            end;
            else do;
               topwaste = 1;
               botwaste = 1;
            end;
            tempwaste[i] = max(topwaste,botwaste);
            
            %*---------- first get within MajDev of MajTarg, and then let white space drive ----------;
            if tempdiff[ibest] > &MajDev then do;
               if tempdiff[i] < tempdiff[ibest] then ibest = i;
               else if tempdiff[i] = tempdiff[ibest] and tempwaste[i] < tempwaste[ibest] then ibest = i;
            end;
            else if tempdiff[ibest] <= &MajDev then do;
               if tempdiff[i] <= &MajDev and tempwaste[i] < tempwaste[ibest] then ibest = i;
            end;
               
         end;
         AxisBy = level[ibest];
            
      %end;
   
      %*---------- less work if By is prespecified ----------;
      %if &By ne %then %do;
      
         AxisBy = &By;
         
      %end;
      
      %*---------- establish start and end ----------;
      AxisStart = round(AxisBy*floor(varmin/AxisBy),&_precision);
      AxisEnd = round(AxisBy*ceil(varmax/AxisBy),&_precision);
      
      %*---------- create axis list ----------;
      length AxisList $200;
      do value = AxisStart to AxisEnd by AxisBy;
         AxisList = catx(' ',AxisList,put(value,best.));
      end;
      
      %*---------- establish min and max ----------;
      AxisMin = round(varmin,&_precision);
      AxisMax = round(varmax,&_precision);
      
      %*---------- trim first/last value from list if inside threshold ----------;
      unbstart = AxisStart;
      secondTick = round(AxisStart+AxisBy,&_precision);
      unbstartcut = secondTick - (1-&Threshold)*AxisBy;
      if unbstartcut < AxisMin then unbstart = round(AxisStart+AxisBy,&_precision);
      else AxisMin = unbstart;
      
      unbend = AxisEnd;
      penultTick = round(AxisEnd-AxisBy,&_precision);
      unbendcut = penultTick + (1-&Threshold)*AxisBy;
      if AxisMax < unbendcut then unbend = round(AxisEnd-AxisBy,&_precision);
      else AxisMax = unbend;
      
      length AxisListUnb $200;
      do value = unbstart to unbend by AxisBy;
         AxisListUnb = catx(' ',AxisListUnb,put(value,best.));
      end;
      
      %*---------- choose minor value ----------;
      %if &By eq %then %do;
      
         modibest = mod(ibest,&_mod);
         if      "&MinDens" = "L" then AxisMinor = low[modibest];
         else if "&MinDens" = "M" then AxisMinor = med[modibest];
         else if "&MinDens" = "H" then AxisMinor = high[modibest];
         
      %end;
      
      %if &By ne %then %do;
      
         AxisMinor = 0;
         
      %end;
      
      %*---------- send results to macro variables ----------;
      call symputx("_&Prefix.AxisStart"  ,put(AxisStart,best.));
      call symputx("_&Prefix.AxisEnd"    ,put(AxisEnd,best.));
      call symputx("_&Prefix.AxisBy"     ,put(AxisBy,best.));
      call symputx("_&Prefix.AxisList"   ,AxisList);
      call symputx("_&Prefix.AxisMin"    ,put(AxisMin,best.));
      call symputx("_&Prefix.AxisMax"    ,put(AxisMax,best.));
      call symputx("_&Prefix.AxisListUnb",AxisListUnb);
      call symputx("_&Prefix.AxisMinor"  ,put(AxisMinor,best.));
      
   run;
   
   
   %*---------- remove temporary datasets ----------;
   proc sql noprint;
      drop table _vert&Data, _minmaxrange, _axisorder;
   quit;

   
   %*---------- this is the goto landing point ----------;
   %badending:
   
   
   %*---------- reset options  ----------;
   options &_mprint. &_notes. &_source. ;


   %*---------- inform user of macro variable values ----------;
   %put _&Prefix.AxisStart   = [ &&_&Prefix.AxisStart ];
   %put _&Prefix.AxisEnd     = [ &&_&Prefix.AxisEnd ];
   %put _&Prefix.AxisBy      = [ &&_&Prefix.AxisBy ];
   %put _&Prefix.AxisList    = [ &&_&Prefix.AxisList ];
   %put _&Prefix.AxisMin     = [ &&_&Prefix.AxisMin ];
   %put _&Prefix.AxisMax     = [ &&_&Prefix.AxisMax ];
   %put _&Prefix.AxisListUnb = [ &&_&Prefix.AxisListUnb ];
   %put _&Prefix.AxisMinor   = [ &&_&Prefix.AxisMinor ];
   
   
%MEND AxisOrder;
top