title: Boxplot by groups, plus a user-defined scatter plot (markers for a subset of values)
body: <p>Working with lab data, I want to overlay a subset of data points on a boxplot grouped by treatment and sequenced by timepoint. Bringing all elements together is not straightforward in SAS, and requires a clever approach that I can't devise or find myself :)</p>
  
  <p>The beauty of the desired plot is that it displays 2 distinct types of outliers:</p>
  
  <ul>
  <li>The boxplots include statistical outliers - square markers (1.5 IQR)</li>
  <li>Then overlay markers for "normal range" outliers - a clinical definition, specific to each lab test.</li>
  </ul>
  
  <p>This is difficult when grouping data (e.g., by treatment) and then blocking or categorizing by another variable (e.g., a timepoint). SAS internally determines the spacing of the boxplots, so this spacing is difficult to mimic for the overlayed normal-range data markers. A generic solution in this direction would be an unreliable kludge.</p>
  
  <p>I've demoed this approach, below, of manually mimicking the group separation for the overlay markers -- just to give an idea of intent. As expected, normal range outliers do not line up with the boxplot groups. Plus, data points that meet both outlier criteria (statistical and clinical) appear as separate points, rather than single points with overlayed markers. My annotations in green:</p>
  
  <p><a href="http://i.stack.imgur.com/7ZVTw.png" rel="nofollow">SGPLOT-overlay-fail</a></p>
  
  <p>Is there an easy, robust way to instruct SAS to overlay grouped data points on a boxplot, keeping everything aligned as intended?</p>
  
  <p>Here's the code to reproduce that miss:</p>
  
  <pre><code>proc sql;
    create table labstruct
      (  mygroup         char(3) label='Treatment Group'
       , myvisitnum      num     label='Visit number'
       , myvisitname     char(8) label='Visit name'
       , labtestname     char(8) label='Name of lab test'
       , labseed         num     label='Lab measurement seed'
       , lablow          num     label='Low end of normal range'
       , labhigh         num     label='High end of normal range'
      )
    ;
    insert into labstruct
      values('A', 1,  'Day 1',  'Test XYZ', 48, 40, 60)
      values('A', 5,  'Week 1', 'Test XYZ', 50, 40, 60)
      values('A', 10, 'Week 2', 'Test XYZ', 52, 40, 60)
      values('B', 1,  'Day 1',  'Test XYZ', 52, 40, 60)
      values('B', 5,  'Week 1', 'Test XYZ', 50, 40, 60)
      values('B', 10, 'Week 2', 'Test XYZ', 48, 40, 60)
    ;
  quit;
  
  data labdata;
    set labstruct;
  
    * Put normal range outliers on 2nd axis, manually separate groups on 2nd axis *;
    select (mygroup);
      when ('A') scatternum = myvisitnum - 1;
      when ('B') scatternum = myvisitnum + 1;
      otherwise;
    end;
  
    * Make more obs from the seeds above *;
    label labvalue = 'Lab measurement';
    do repeat = 1 to 20;
      labvalue = labseed + 6*rannor(3297);
  
      * Scatter plot ONLY normal range outliers *;
      if labvalue &lt; lablow or labvalue &gt; labhigh 
         then scattervalue = labvalue;
      else scattervalue = .;
  
      output;
    end;
    drop repeat labseed;
  run;
  
  proc sgplot data=labdata;
    block x=myvisitnum block=myvisitname / 
          nofill 
          lineattrs=(color=lightgray);
    vbox labvalue / 
         category=myvisitnum
         group=mygroup
         outlierattrs=(symbol=square);
    scatter x=scatternum y=scattervalue /
         group=mygroup
         x2axis
         jitter;
    x2axis display=none;
    keylegend / position=bottom type=marker;
  run;
  </code></pre>