Graphical swiss army knife

10

The Swiss army knife is known for its versatility, with a variety of tools and blades to help you complete the task at hand. When you are creating graphics, you sometimes have a special feature you want to add, but you can't seem to find the right syntax "tool" to get it done. In SAS 9.3, we added an annotation facility to the SG procedures that gives you the ability to add these features when it becomes too difficult or impossible to add them through the standard syntax.

Like the "classic" annotate facility in SAS/GRAPH, SG annotation uses a data set of reserved column names to define the type of annotation and its attributes. However, the similarities stop there. The data set definition has been redefined in a way that, hopefully,  makes it easier to use, while taking advantage of features available in the ODS GRAPHICS system. For example, features such as drawing spaces and anchor positions are now defined using keywords instead of numbers to make the values easier to remember. The new definition contains 45 column names, but you will typically use only a few at a time.

The FUNCTION column defines the  type of annotation to draw. For the SAS 9.3 release, the following are the available functions:

  • Text - draw a text string
  • TextCont - continue a text string from a previous TEXT or TEXTCONT. Used for rich text.
  • Image - draw an image
  • Line - draw a line
  • Arrow - draw an arrow
  • Rectangle - draw a rectangle or square
  • Oval - draw a circle or oval
  • Polygon - start a closed polygon
  • Polyline - start an open polyline
  • PolyCont - provides points for a POLYGON or POLYLINE

The following examples will give you some ideas on how these functions can be used.

In this example, I wanted to add the "less-than-or-equal-to" symbol to each of my axis values. There is currently not a a way to do this directly, so I took advantage of the TEXT functions ability to handle inline text functions, such as Unicode, superscript, and subscript. Many of the column values in this example can be held constant using the RETAIN statement. The only values that change are the X1 values (used to position the values along the X-axis) and the labels themselves.

data anno;
retain function 'text' y1space 'graphpercent' x1space 'datavalue'
          y1 7 width 15;
input x1 label $ 4-33;
cards;
21 20 (*ESC*){unicode '2264'x} 30
31 30 (*ESC*){unicode '2264'x} 40
41 40 (*ESC*){unicode '2264'x} 50
51 50 (*ESC*){unicode '2264'x} 60
61 60 (*ESC*){unicode '2264'x} 70
run;

The LABEL text contains the ODS ESCAPECHAR (which is (*ESC*) by default) to mark where the Unicode function is inserted. The new PAD option is used in PROC SGPLOT to reserve room along the bottom of the graph for the labels because annotations do not reserve space.

proc format;
   value agefmt
   20-<30 = "20-30"
   30-<40 = "30-40"
   40-<50 = "40-50"
   50-<60 = "50-60"
   60-<70 = "60-70"
   ;
run;
 
Title1 "Cholesterol Level by Age Range";
proc sgplot data=sashelp.heart sganno=anno pad=(bottom=8%);
   format AgeAtStart agefmt.;
   xaxis display=(nolabel novalues);
   vbox cholesterol / category=AgeAtStart;
run;

Images are another feature that can easily be added using annotation. In the following example, images are used to label the curves instead of the typical text label. Also, a custom company logo is added to the bottom right corner.

  

data anno;
length x1space $ 13 y1space $ 13 anchor $ 11;
set meat_consumption (where=(year='01jan2000'd)) end=_last_;
retain anchor 'left' y1space 'datavalue' x1space 'datapercent' width 40
       widthunit 'pixel' function 'image' x1 102;
y1 = chicken; image = "chicken.jpg"; output;
y1 = beef; image = "cow.jpg"; output;
y1 = pork; image = "pig.jpg"; output;
if (_last_) then do;
   x1space = "graphpercent";
   y1space = "graphpercent";
   anchor = "bottomright";
   x1 = 99;
   y1 = 1;
   width=90;
   image = "Logo.png";
   output; /* the image logo */
   function = "text";
   anchor = "bottomleft";
   x1 = 1;
   width=150;
   textsize = 6;
   label = "Source: USDA";
   output; /* the footnote */
end;
run;

Notice that only the image width is specified. In this system, if only the width or the height is specified for the image, the other attribute is automatically calculated based on the aspect of the image data. Specifying only one value is a great way to ensure that the image is never distorted.

For a final example, I created a bubble legend as an alternative to displaying the value on the bubble. That way, I could use the DATALABEL option to display the Y-axis value.

 

data anno;
retain drawspace "wallpercent" widthunit "pixel" heightunit "pixel" linethickness 1 
       textsize 8;
length function $ 9;
input function $ x1 y1 width height x2 y2 textsize anchor $ label $ 48-66 display $ 68-70
      fillcolor $ 72-76;
cards;
Rectangle  86 76.5  140  87  .     . 12 bottom                     all white
Oval       80 76.5   44  44  .     . 12 bottom                              
Oval       80 76.5   16  16  .     . 12 bottom                              
Line       80 87.9    .   . 87  87.9 12 bottom                              
Line       80 80.75   .   . 87  80.5 12 bottom                              
Text       86 98    140   .  .     . 12 top    Salary (in dollars)          
Text       87 88.1  140   .  .     .  8 left   $32,816                      
Text       87 80.4  140   .  .     .  8 left   $18,444                      
;
run;

These are but a few of the examples covered in my paper located here on the support site. There are also more details about using the SG annotation system in general. Please check out the paper and post here if you have any questions or comments.

Tags
Share

About Author

Dan Heath

Principal Systems Developer

Dan Heath is a principal systems developer at SAS Institute. A SAS user for more than 28 years, Dan specializes in SAS/GRAPH software, ODS Graphics, and related graphing technologies. Dan has been a speaker at a number of regional and local users' group meetings, including SAS Global Forum, PharmaSUG, and WUSS. He received a BS degree in computer science from North Carolina State University.

Related Posts

10 Comments

  1. In the box plot example, it is not clear which category a 30-year-old person is in. This is determined by the AgeAtStart format, which is not shown. The categories are half-open intervals, meaning that they are either (20, 30], (30, 40], etc, or are [20, 30), [30, 40), etc.

    The purpose of the first example is to demonstrate unicode text in annotation, but can't you create this box plot by using PROC FORMAT to define the categories and labels, and skip annotation?

    • Dan Heath

      I updated the picture and code to make it clear where the data fell. I did find a mistake in my original format, which is why I updated the picture. As for PROC FORMAT, it is currently not possible to embed Unicode characters in the format label. That is why the annotation was needed.

  2. Is it possible to get the data and the chart code for the meat consumption chart, I would like to see how all fits together

    • Dan Heath

      Sure, here is the full code:

      data meat_consumption;
      informat year monyy7.;
      format year year.;
      input year chicken beef pork;
      cards;
      jan1950 21  44  65
      jan1955 21  56  62
      jan1960 28  59  59
      jan1965 33  70  52
      jan1970 40  82  55
      jan1975 39  85  43
      jan1980 47  75  57
      jan1985 52  77  51
      jan1990 61  66  49
      jan1995 69  65  51
      jan2000 77  67  51
      ;
      run;
       
      data anno;
      length x1space $ 13 y1space $ 13 anchor $ 11;
      set meat_consumption (where=(year='01jan2000'd)) end=_last_;
      retain anchor 'left' y1space 'datavalue' x1space 'datapercent' width 40 
             widthunit 'pixel' function 'image' x1 102;
      y1 = chicken;
      image = "chicken.jpg";
      output;
      y1 = beef;
      image = "cow.jpg";
      output;
      y1 = pork;
      image = "pig.jpg";
      output;
      if (_last_) then do;
      x1space = "graphpercent";
      y1space = "graphpercent";
      anchor = "bottomright";
      x1 = 99;
      y1 = 1;
      width=90;
      image = "Logo.png";
      output;
      function = "text";
      anchor = "bottomleft";
      x1 = 1;
      width=150;
      textsize = 6;
      label = "Source: USDA";
      output;
      end;
      run;
       
      ods graphics / reset imagename='Meat';
      Title1 "U.S. Per Capita Meat Consumption";
      title2 "1950-2000";
       
      proc sgplot data=meat_consumption noautolegend sganno=anno pad=(bottom=8%);
      xaxis display=(nolabel) offsetmax=0.1;
      yaxis label="Retail Cut Equivalent / Lb. per Person";
      series x=year y=chicken / markers markerattrs=(symbol=circlefilled size=12px color=CXa5aace) 
             lineattrs=(thickness=10 color=CXa5aace) transparency=0;
      scatter x=year y=chicken / markerattrs=(symbol=circlefilled size=10px color=white);
      series x=year y=pork / markers markerattrs=(symbol=circlefilled  size=12px color=CX84b2b3)
             lineattrs=(thickness=10 color=CX84b2b3) transparency=0;
      scatter x=year y=pork / markerattrs=(symbol=circlefilled size=10px color=white);
      series x=year y=beef / markers markerattrs=(symbol=circlefilled  size=12px color=CXd69e94)
             lineattrs=(thickness=10 color=CXd69e94) transparency=0;
      scatter x=year y=beef / markerattrs=(symbol=circlefilled size=10px color=white);
      run;
  3. Pingback: Risk tables, annotated or not - Graphically Speaking

  4. Pingback: Reading to beat the heat - Key Happenings at support.sas

  5. Pingback: Unicode Tick Values using GTL - Graphically Speaking

  6. Pingback: Basic ODS Graphics: What is wrong with my SG annotation data set? - Graphically Speaking

  7. Pingback: Basic ODS Graphics: What is wrong with my SG annotation data set? - Graphically Speaking

  8. Pingback: How to add an annotation to a mosaic plot in SAS - The DO Loop

Back to Top