BookmarkSubscribeRSS Feed
rbettinger
Pyrite | Level 9

I have written a SAS/IML module to extend the SERIES subroutine to produce SGPLOT graphics using the SGPLOT SERIES statement and additional optional parameters. The PLOT_SERIES module accepts up to 6 pairs of (x, y) variables for plotting on the same set of axes. The series may be of differing lengths. Each series may be specified independently of the others using particular SGPLOT SERIES statements. You may specify PROC SGPLOT-specific statements also, and other statements are possible as well, such as XAXIS and YAXIS, &c.

 

The code for the PLOT_SERIES subroutine is included below.

start plot_series( p1, p2=, p3=, p4=, p5=, p6=, p7=, p8=, p9=, p10=, p11=, p12=,
                   procopt=,
                   s1opt=, s2opt=, s3opt=, s4opt=, s5opt=, s6opt=,
                   otheropts=
                 ) ;
            
   /* purpose: create 2-D plot of X vs Y as a series plot using SGPLOT called from within SAS/IML
    *          multiple variables may be plotted on the same set of axes
    *          up to 6 different pairs of variables may be specified, e.g., x1, y1, ..., x6, y6
    *
    *          series-specific options may be specified by keyword parameters, e.g., s1opt='group=variable datalabel markers'
    *          each series may be specified independently of others by using s[i]opt= optional parameter, i=1:6
    *
    *          other options, e.g., XAXIS, YAXIS are specified using the keyword parameter otheropts=
    *
    * parameters:
    *    p_i       ::= parameters of the plot_series() module, i = 1:12
    *    procopt   ::= PROC SGPLOT optional arguments
    *    s[i]opt   ::= SGPLOT SERIES optional statements (keyword parameters) in quotes, i = 1:6
    *    otheropts ::= other options, e.g., XAXIS, YAXIS
    *
    *    there may be up to six pairs of numeric x, numeric y comprising max of six series on same set of axes
    *
    * syntax:    
    *    call plot_series{ x, y ) ;
    *    call plot_series{ x, y, x1, y1, x2, y2, ..., x5, y5 )                       ; *** plot_series 6 different series in a single graph ***
    *    call plot{series( x, y, x1, y1, x2, y2, ..., x5, y5 ) s1opt='group=groupID' ; *** specify SGPLOT GROUP option for series 1         ***
    *    call plot_series{ x, y ) procopt='noautolegend noborder'                    ; *** turn off legend, border around graphs            ***
    *    call plot_series{ x, y ) otheropts='XAXIS GRID ; YAXIS GRID'                ; *** specify that grids be drawn on x, y axes         ***
    *
    * examples:
    *    ods graphics on / height=6in width=6in ;
    *    proc iml ;
    *       *** adapted from SAS ODS Graphics Procedures Guide, SGPLOT Procedure
    *       *** Example  3: Plotting Three Series
    *
    *       filename = 'sashelp.stocks' ;
    *
    *       use ( filename ) var { date close low high } where( date >= "01jan2002"d & stock = "IBM" ) ;
    *       read all var { date close low high } ;
    *       close ( filename ) ;
    *
    *       date1 = date ; date2 = date ; *** duplication necessary since IML does not allow repeated variable names in an argument list ***
    *
    *       call plot_series( date, close, date1, low, date2, high ) ;
    *
    *    quit ;
    *    ods graphics off ;
    *================================================================================
    *    ods graphics on / width=4in height=4in ;
    *
    *    title "Power Generation (Gigawatt Hours)" ;
    *    proc iml ;
    *       *** adapted from SAS ODS Graphics Procedures Guide, SGPLOT Procedure, Overview of the SGPLOT Procedure ***
    *
    *       filename = 'sashelp.electric' ;
    *
    *       use ( filename ) var { year coal naturalgas customer } where( year >= 2001 & customer = "Residential" ) ;
    *       read all var { year coal naturalgas } ;
    *       close ( filename ) ;
    *
* year1 = year ; *** duplication necessary since IML does not allow repeated variable names in an argument list ***
* * call plot_series( year , coal * , year1, naturalgas * ) * s1opt='datalabel' *** keyword parameter *** * , s2opt='datalabel y2axis' *** ditto *** * ; * quit ; * title ; * * ods graphics off ; */ /*============================================================================*/ /* parse parameter string for parm name (indep var, dep var), type (numeric, char) /*============================================================================*/ parm_list = 'p1' : 'p12' ; /* create vector of parameter types to control SGPLOT statement generation */ /* parse parameter list to extract parm names, types */ parm_type = '' ; do i = 1 to ncol( parm_list ) ; call execute( 'arg_type = type(' || parm_list[ i ] || ') ;' ) ; if arg_type = 'N' then arg_value = parentname( strip( parm_list[ i ] )) ; else if arg_type = 'U' then arg_value = '.' ; else if arg_type = 'C' then call error( 'plot_series', 'Parameters to be plotted must be numeric only' ) ; if arg_value = ' ' then call error( 'plot_series', 'Missing or duplicated parameter in position' + char( i )) ; parm_value = parm_value || arg_value ; parm_type = rowcatc( parm_type || arg_type ) ; end ; n_parms = max( loc( parm_value ^= '.' )) ; /* determine # of parameters in plot_series() call */ parm_value = parm_value[ , 1 : n_parms ] ; /* strip off unused parameters */ parm_type = substr( parm_type, 1, n_parms ) ; /* create "long" form of data matrix. distinguish btwn parameter pairs by group_num */ do i = 1 to n_parms by 2 ; val_x = colvec( value( parm_list[ i ] )) ; /* x */ val_y = colvec( value( parm_list[ i + 1 ] )) ; /* y */ group_pts = nrow( val_x ) ; group_num = repeat( i, group_pts ) ; /* group_num is join key for char var name, numeric (x,y) pair */ group_mat = group_mat // ( group_num || val_x || val_y ) ; group_dim = group_dim || group_pts ; end ; group_ID = group_mat[ , 1 ] ; /* vector of group IDs for selecting from "long" mat by group */ max_dim = max( group_dim ) ; /*============================================================================*/ /* create "wide" SAS dataset containing values of variables to be plotted /*============================================================================*/ series_mat = j( max_dim, n_parms, . ) ; /* convert "long" matrix to "wide" matrix */ uniq_group_ID = unique( group_ID ) ; do i = 1 to ncol( uniq_group_ID ) ; series_col = uniq_group_ID[ i ] ; ndx = loc( series_col = group_ID )` ; series_mat[ 1 : group_dim[ i ], series_col ] = group_mat[ ndx, 2 ] ; /* x */ series_mat[ 1 : group_dim[ i ], series_col + 1 ] = group_mat[ ndx, 3 ] ; /* y */ end ; /* create variable names in output dataset * write "wide" matrix to dataset */ series_names = parm_value ; /* switch to graphics terminology */ mattrib series_mat colname=series_names ; create sgplot_data from series_mat[ colname=series_names ] ; append from series_mat ; close sgplot_data ; /* use SAS/MACRO "call symput()" function to transmit PROC SGPLOT arguments to global environment */ if ^isEmpty( procopt ) then call symput( 'PROCOPT' , procopt ) ; else call symput( 'PROCOPT' , '' ) ; if ^isEmpty( otheropts ) then call symput( 'OTHEROPTS', cat( otheropts, ';' )) ; else call symput( 'OTHEROPTS', '' ) ; do i = 1 to 6 ; series_opt = cats( 'S', char( i ), 'OPT' ) ; call symput( series_opt, '' ) ; end ; do i = 1 to n_parms / 2; buffer = 'series x=' + series_names[ 2 # i - 1 ] + ' y=' + series_names[ 2 # i ] ; series_num = char( i ) ; series_opt = cats( 'S', series_num, 'OPT' ) ; if ^isEmpty( value( series_opt )) then buffer = cat( buffer, '/ ', value( series_opt )) ; buffer = buffer + ';' ; call symput( series_opt, buffer ) ; end ; submit ; proc sgplot data=sgplot_data &PROCOPT ; %if %length( &S1OPT ) %then %do ; &S1OPT ; %end ; %if %length( &S2OPT ) %then %do ; &S2OPT ; %end ; %if %length( &S3OPT ) %then %do ; &S3OPT ; %end ; %if %length( &S4OPT ) %then %do ; &S4OPT ; %end ; %if %length( &S5OPT ) %then %do ; &S5OPT ; %end ; %if %length( &S6OPT ) %then %do ; &S6OPT ; %end ; %if %length( &OTHEROPTS ) %then %do ; &OTHEROPTS ; %end ; run ; endsubmit ; finish plot_series ;

Here are some examples of use:

%macro CIRC ;
title 'circular functions' ;
twopi = 2 # constant( 'pi' ) ;

theta_01 = do( 0, twopi, .01 # twopi ) ; /* 101 points */
theta_02 = do( 0, twopi, .02 # twopi ) ; /*  51 points */

x1 = theta_01     ;
x2 = theta_02     ;
y1 = 2 # theta_01 ;
z1 = theta_01     ;
z2 = theta_02 ; sin_01 = sin( theta_01 ) ; cos_01 = cos( theta_01 ) ; cos_01a = cos_01 ; sin_02 = sin( theta_02 ) ; cos_02 = cos( theta_02 ) ; sin_cos_01 = sin_01 # cos_01 ; ods graphics on / height=5in width=5in ; run plot_series( x1, sin_01 ) ; /* simple x,y plot */ run plot_series( x1, sin_01, z1, cos_01 ) ; /* plot two variables on same axis */ run plot_series( x1, sin_01, z2, cos_02 ) ; /* plot differing-length series on same graph */ run plot_series( x2, sin_02, y1, cos_01, z1, sin_cos_01, sin_01, cos_01a ) /* use keyword parameters */ procopt='noborder' s1opt='datalabel' s2opt='x2axis' ; ods graphics off ; /* graph logarithmic spiral */ theta3 = do( 0, 8 # twopi, .02 # twopi ) ; sin3 = sin( theta3 ) ; cos3 = cos( theta3 ) ; /* equation of logarithmic spiral is r = exp( a + b # theta ) */ a = -1 ; /* right-handed spiral */ b = 0.1749 ; r = exp( a + b # theta3 ) ; spiral_x = r # cos3 ; spiral_y = r # sin3 ; title 'Logarithmic Spiral' ; ods graphics on / height=6in width=6in ; run plot_series( spiral_x, spiral_y ) s1opt ='arrowheadpos=end arrowheadshape=barbed arrowheadscale=1.25' otheropts='xaxis display=(nolabel) grid ; yaxis display=(nolabel) grid ;' ; title ; ods graphics off ; title ; %mend CIRC ; %macro POWER ; title "Power Generation (Gigawatt Hours)" ; filename = 'sashelp.electric' ; use ( filename ) var { year coal naturalgas customer } where( year >= 2001 & customer = "Residential") ; read all var { year coal naturalgas } ; close ( filename ) ; year_dup = year ; /* duplication required since IML does not allow repeated occurrences of same variable in a module's argument list */ ods graphics on ; call plot_series( year, coal , year_dup, naturalgas ) ; ods graphics off ; title ; %mend POWER ; %macro STOCK ; title "Stock Trends" ; filename = 'sashelp.stocks' ; use ( filename ) var { date close low high stock } where( date >= "01jan2000"d & stock = "IBM" ) ; read all var { date close low high stock } ; close ( filename ) ; date1 = date ; date2 = date ; ods graphics on / width=6in height=6in ; call plot_series( date, close, date1, low, date2, high ) ; ods graphics off ; title ; %mend STOCK ; %CIRC ; %POWER ; %STOCK ;

There is a caveat that must be observed when using PROC_SERIES: IML does not allow repeated parameters in an argument list because parameters in a module are called by reference, e.g., a parameter's values may be changed by the module, so if there is a sequence of parameters such as ( x, y, x, z ) then the second x is a duplication of the first one and PLOT_SERIES cannot refer to it by name since the parentname() function returns a blank value. The workaround is to duplicate x by creating a copy of x, e.g., x1 so that all of the names in the argument list are unique. Then we have the list ( x, y, x1, z ) which satisfies the IML parser.

The PLOT_SERIES code can be adapted to other PROC SGPLOT statements because the mechanism for invoking PROC SGPLOT via the IML SUBMIT/ENDSUBMIT bracket is easily modified to accommodate the requirements of other graphical displays, e.g., HBAR or SCATTER.

5 REPLIES 5
rbettinger
Pyrite | Level 9

I inadvertently omitted the code for the error subroutine. Here it is, with a simple test case.

proc iml ;
start error( fcn_name, msg ) ;
   /* purpose: display error msg, halt execution */
  
   msg = 'Error in ' + strip( fcn_name ) + ': ' + msg ;
   
   print msg[ label='' ] ;
   
   stop ;
finish ;

msg = 'main pgm' + ' this is a test ' + 'propName' ;
call error ('main pgm ', msg ) ;

quit ;

Or, if you like, you can use the IML stop statement, e.g.,

if error_condition then stop 'error message' ;
IanWakeling
Barite | Level 11

Many thanks for posting your code, it looks useful. You might want to add

options symbolgen;

to your code which will write the values of the macro variables inside the submit block to the log, making it easier to understand what it is going on.

rbettinger
Pyrite | Level 9

Thank you for your suggestion to add 'options symbolgen ;' to the SUBMIT/ENDSUBMIT bracket. Making the macro substitutions visible is a very useful aid to debugging.

It is also a good idea to add the following code indicated between the /* comments */ to the module:

   do i = 1 to n_parms by 2 ;                                                                                                                                                                                                               
      val_x = colvec( value( parm_list[ i     ] )) ; /* x */
      val_y = colvec( value( parm_list[ i + 1 ] )) ; /* y */
/* test for identical lengths of input parameter pairs =======================*/ if nrow( val_x ) ^= nrow( val_y ) then call error( 'plot_series', 'Lengths of variables ' + parm_value[ i ] + ' and ' + parm_value[ i + 1 ] + ' must match' ) ; /*============================================================================*/
group_pts = nrow( val_x ) ; group_num = repeat( i, group_pts ) ; /* group_num is join key for char var name, numeric (x,y) pair */ group_mat = group_mat // ( group_num || val_x || val_y ) ; group_dim = group_dim || group_pts ; end ;

Ofttimes, errors are made at the interface between one environment and another, and they must be detected and rectified before correct results are obtained.

WeiChen
Obsidian | Level 7

Your series_plot function good to plot (x1,y1,x2,y2,....) where series not mesured at same times.

 

For one question yuou ask, I remember @Rick_SAS mention built-in WideToLong function. He blog about it here: https://blogs.sas.com/content/iml/2015/02/27/multiple-series-iml.html With it, you can use regular series call when series measured at same times, like for POWER and STOCK macro.  See I use OTHER optin to add date format for X axis.

 

proc iml;
filename = 'sashelp.stocks' ;
varNames = { 'date' 'close' 'low' 'high' };
use ( filename ) where( date >= "01jan2002"d & stock = "IBM" ) ;
read all var varNames;
close ( filename ) ;

title "WideLong Example";
X = Date;
Y = close || low || high;
YLabels = {'Close'  'Low'  'High'};
CALL WIDETOLONG (Date, Price, Group, Y , X, YLabels);
call series (Date, Price) group=Group other="format Date DATE10.;";

You do not need to turn on and off ods graphics. Sgplot makes graphs always. 

rbettinger
Pyrite | Level 9

Thank you for showing the close correspondence between the CALL SERIES and RUN PLOT_SERIES modules. I wanted more control over the graphical parameters for each series presented, so I developed the capability to tailor each series individually as I saw fit.

I used the ODS GRAPHICS statement with the HEIGHT and WIDTH options to vary the image sizes for inclusion in conference papers or other applications.

Ready to join fellow brilliant minds for the SAS Hackathon?

Build your skills. Make connections. Enjoy creative freedom. Maybe change the world. Registration is now open through August 30th. Visit the SAS Hackathon homepage.

Register today!
Multiple Linear Regression in SAS

Learn how to run multiple linear regression models with and without interactions, presented by SAS user Alex Chaplin.

Find more tutorials on the SAS Users YouTube channel.

From The DO Loop
Want more? Visit our blog for more articles like these.
Discussion stats
  • 5 replies
  • 814 views
  • 1 like
  • 3 in conversation