DCR
DCR

Reputation: 15685

Create 3d clustered bar chart 2 variables side by side using SAS 9.4

UPDATE: Here's a link to the complete dataset sas data

I have a data set that consists of 11 provinces, semi annual time periods, a indicator, actual value and target value. I'm trying to create a 3d bar chart with axises province X time-period X value and then for each province and time period show 2 bars one for actual and one for target.

My dataset looks like this:

PROVINCE    PERIOD  VAR VALUE
Bandundu    2013S1  OUT_BUD_ODE 250000
Bandundu    2013S1  TAR_BUD_ODE 545000
Bandundu    2013S2  OUT_BUD_ODE 283000
Bandundu    2013S2  TAR_BUD_ODE 545000
Bandundu    2014S1  OUT_BUD_ODE 800000
Bandundu    2014S1  TAR_BUD_ODE 645000
Bandundu    2014S2  OUT_BUD_ODE 700000
Bandundu    2014S2  TAR_BUD_ODE 645000
Bandundu    2015S1  OUT_BUD_ODE 369363
Bandundu    2015S1  TAR_BUD_ODE 945288
Bandundu    2015S2  OUT_BUD_ODE 1217449
Bandundu    2015S2  TAR_BUD_ODE 958417
Bandundu    2016S1  OUT_BUD_ODE 96618
Bandundu    2016S1  TAR_BUD_ODE 787740
Bandundu    2016S2  OUT_BUD_ODE 1358568
Bandundu    2016S2  TAR_BUD_ODE 787740
Bandundu    2017S1  OUT_BUD_ODE 1312900
Bandundu    2017S1  TAR_BUD_ODE 311729
Bandundu    2017S2  OUT_BUD_ODE .
Bandundu    2017S2  TAR_BUD_ODE 1299771
Bas-Congo   2013S1  OUT_BUD_ODE 150000
Bas-Congo   2013S1  TAR_BUD_ODE 450000
Bas-Congo   2013S2  OUT_BUD_ODE 145000
Bas-Congo   2013S2  TAR_BUD_ODE 450000
Bas-Congo   2014S1  OUT_BUD_ODE 1000000
Bas-Congo   2014S1  TAR_BUD_ODE 900965
Bas-Congo   2014S2  OUT_BUD_ODE 900000
Bas-Congo   2014S2  TAR_BUD_ODE 900965
Bas-Congo   2015S1  OUT_BUD_ODE 433892
Bas-Congo   2015S1  TAR_BUD_ODE 1115965
Bas-Congo   2015S2  OUT_BUD_ODE 943387
Bas-Congo   2015S2  TAR_BUD_ODE 1115965
Bas-Congo   2016S1  OUT_BUD_ODE 552002
.
.
.

the chart if we were just plotting for 1 province would look something like the following in 2D, we have side by side bars, value and time period. We need to add Province as the third axis and also make the bars 3d:

2d picture

we want the axises to be province, period, value but for each point we have two bars one for OUT_BOD_ODE AND ONE FOR TAR_BAR_ODE The bars would probably look better if they were 3d too

Upvotes: 2

Views: 1247

Answers (3)

Richard
Richard

Reputation: 27508

There is no simple built in SAS statements that will do side by side 3d bars in a block like manner. The GCHART BLOCK statement uses the subgroup option to build a stacked bar at a block intersection, but can not do a grouped bar.

Step 1 - The data and almost chart

data have;
    infile datalines truncover;
    length PROVINCE $13. PERIOD $6. VAR $13. VALUE 8.;
    input PROVINCE PERIOD VAR VALUE;
    datalines;
Bandundu    2013S1  OUT_BUD_ODE 250000
Bandundu    2013S1  TAR_BUD_ODE 545000
Bandundu    2013S2  OUT_BUD_ODE 283000
Bandundu    2013S2  TAR_BUD_ODE 545000
Bandundu    2014S1  OUT_BUD_ODE 800000
Bandundu    2014S1  TAR_BUD_ODE 645000
Bandundu    2014S2  OUT_BUD_ODE 700000
Bandundu    2014S2  TAR_BUD_ODE 645000
Bandundu    2015S1  OUT_BUD_ODE 369363
Bandundu    2015S1  TAR_BUD_ODE 945288
Bandundu    2015S2  OUT_BUD_ODE 1217449
Bandundu    2015S2  TAR_BUD_ODE 958417
Bandundu    2016S1  OUT_BUD_ODE 96618
Bandundu    2016S1  TAR_BUD_ODE 787740
Bandundu    2016S2  OUT_BUD_ODE 1358568
Bandundu    2016S2  TAR_BUD_ODE 787740
Bandundu    2017S1  OUT_BUD_ODE 1312900
Bandundu    2017S1  TAR_BUD_ODE 311729
Bandundu    2017S2  OUT_BUD_ODE .
Bandundu    2017S2  TAR_BUD_ODE 1299771
Bas-Congo   2013S1  OUT_BUD_ODE 150000
Bas-Congo   2013S1  TAR_BUD_ODE 450000
Bas-Congo   2013S2  OUT_BUD_ODE 145000
Bas-Congo   2013S2  TAR_BUD_ODE 450000
Bas-Congo   2014S1  OUT_BUD_ODE 1000000
Bas-Congo   2014S1  TAR_BUD_ODE 900965
Bas-Congo   2014S2  OUT_BUD_ODE 900000
Bas-Congo   2014S2  TAR_BUD_ODE 900965
Bas-Congo   2015S1  OUT_BUD_ODE 433892
Bas-Congo   2015S1  TAR_BUD_ODE 1115965
Bas-Congo   2015S2  OUT_BUD_ODE 943387
Bas-Congo   2015S2  TAR_BUD_ODE 1115965
Bas-Congo   2016S1  OUT_BUD_ODE 552002
;
run;

The not quite there BLOCK chart

proc gchart data=have;
  block province / group=period subgroup=var;
run;
quit;

SAS Proc GCHART, BLOCK chart

Going for it

The code in "A 3D Scatter Plot Macro" by Sanjay Matange, Blog "Graphically Speaking March 10, 2015", shows how to 3D represent data in SGPLOT.

After some data work and small modifications to the macro, this chart can be rendered:

enter image description here

Other, more sophisticated renderings are possible, but likely found outside the current SAS product offerings:

Step 2 - Data prep, convert categorical values into monotonic identity values

proc sql; 
  create table uniqs as
  select distinct 'province' as _var_, province as key from have
  union
  select distinct 'period'   as _var_, period as key from have
  union
  select distinct 'var'      as _var_, var as key from have
  ;
quit;

data fmt(keep=fmtname start label);
  set uniqs;
  by _var_;
  if first._var_ then id=1; else id+1;
  fmtname = _var_;
  start = id;
  label = key;
run;

proc format cntlin = fmt;
run;

proc sql;
  create table have_mapped as 
  select
    have.*
  , f1.start as province_id
  , f2.start as period_id
  , f3.start as var_id
  from have
  left join fmt f1 on f1.label = province
  left join fmt f2 on f2.label = period
  left join fmt f3 on f3.label = var
  where f1.fmtname = 'province'
    and f2.fmtname = 'period'
    and f3.fmtname = 'var'
  ;
quit;

Jitter the province identity value in order to separate the ODE type within each period.

data have_jittered;
  set have_mapped;
  if var_id = 2 then province_id = province_id + 0.2;
  size = 2;
run;

3D Scatter Plot Macro

If you want to submit the blog entry code as-is the following will do so:

filename source1 http "https://blogs.sas.com/content/graphicallyspeaking/files/2015/03/Matrix_Functions.txt";
filename source2 http "https://blogs.sas.com/content/graphicallyspeaking/files/2015/03/Ortho_3D_Macro_94.txt";

%include source1;
%include source2;

filename source1;
filename source2;

Step 3 - modified macro

I modified Sanjay's macro to handle more inputs that allowed a more 'bar-like' rendering to be produced.

No changes to the matrix support:

options cmplib=sasuser.funcs;

proc fcmp outlib=sasuser.funcs.mat;
  subroutine MatInv(Mat[*,*], InvMat[*,*]);
  outargs InvMat;
  call inv(Mat, InvMat);
  endsub;

  subroutine MatMult(A[*,*], B[*,*], C[*,*]);
  outargs C;
  call mult(A, B, C);
  endsub;

  subroutine MatIdent(A[*,*]);
  outargs A;
  call identity(A);
  endsub;
run;
quit;

Ortho 3D macro updated for turing walls on or off, setting marker sizes and symbols and relative scale of the 3D box. There are some future-proof arguments for adding tic marks (tic drawing is not in this version)

%macro Ortho3D_Macro 
( Data=, X=, Y=, Z=, Group=, Size=, Lblx=X, Lbly=Y, Lblz=Z, 
  Tilt=65, Rotate=-55, Attrmap=, Title=
  , shadewalls = yes
  , outlineaxis = yes
  , backwall = yes
  , sidewall = yes
  , floorwall = yes
  , floor_marker_symbol = circlefilled
  , floor_marker_size = 5
  , showdata = yes
  , xnormalscale = 1
  , ynormalscale = 2
  , znormalscale = 1
  , xtic=, xtic_jitter=
  , ytic=, ytic_jitter=
  , ztic=, ztic_jitter=
);

%local A B C WallData;

%let A=&Tilt;
%let B=0;
%let C=&Rotate;

%let WallData=wall_Axes; 

/*--Define walls and axes--*/
filename walldata "work.walldata.source";
options noquotelenmax;
data _null_;
datalines="
|  X1-Axis  D -1  -1   -1     1  -1  -1     0   -1  -1  1
|  X3-Axis  L -1  -1    1     1  -1   1     .    .   .  .
|  X4-Axis  D -1   1    1     1   1   1     .    .   .  .
|  Y2-Axis  D -1  -1    1    -1   1   1     .    .   .  .
|  Y3-Axis  D  1  -1   -1     1   1  -1     1    0  -1  2
|  Y4-Axis  L  1  -1    1     1   1   1     .    .   .  .
|  Z1-Axis  D -1  -1   -1    -1  -1   1    -1   -1   0  3
|  Z2-Axis  L  1  -1   -1     1  -1   1     .    .   .  .
|  Z4-Axis  D  1   1   -1     1   1   1     .    .   .  .
|  Bottom   D -1  -1   -1    .   .   .      .    .   .  .
|  Bottom   D  1  -1   -1    .   .   .      .    .   .  .
|  Bottom   D  1   1   -1    .   .   .      .    .   .  .
|  Bottom   D -1   1   -1    .   .   .      .    .   .  .
|  Back     D -1  -1   -1    .   .   .      .    .   .  .
|  Back     D -1   1   -1    .   .   .      .    .   .  .
|  Back     D -1   1    1    .   .   .      .    .   .  .
|  Back     D -1  -1    1    .   .   .      .    .   .  .
|  Right    D -1   1   -1    .   .   .      .    .   .  .
|  Right    D  1   1   -1    .   .   .      .    .   .  .
|  Right    D  1   1    1    .   .   .      .    .   .  .
|  Right    D -1   1    1    .   .   .      .    .   .  .
";
  do _n_ = 1 by 1;
    file walldata;
    line = scan(datalines,_n_,'|');
    if line = '' then leave;
    put line;
  end;
run;

data wall_Axes;
  infile walldata;
  input @1 id $ group $ xw yw zw xw2 yw2 zw2 xl yl zl label;
run;

/*--Project the walls and axes--*/
data projected_walls;
  keep id group xw yw zw xw2 yw2 zw2 xl yl zl lbx lby lbz label;
  array u[4,4] _temporary_;  /*--Intermediate Matrix--*/
  array v[4,4] _temporary_;  /*--Intermediate Matrix--*/
  array w[4,4] _temporary_;  /*--Final View Matrix--*/
  array m[4,4] _temporary_;  /*--Projection Matrix--*/
  array rx[4,4] _temporary_; /*--X rotation Matrix--*/
  array ry[4,4] _temporary_; /*--Y rotation Matrix--*/
  array rz[4,4] _temporary_; /*--Z rotation Matrix--*/
  array d[4,1] _temporary_;  /*--World Data Array --*/
  array p[4,1] _temporary_;  /*--Projected Data Array --*/
  retain r t f n;
  r=1; t=1; f=1; n=-1;
  pi=constant("PI");
  fac=pi/180;
  A=&A*fac; B=&B*fac; C=&C*fac;

  /*--Set up projection matrix--*/
  m[1,1]=1/r;   m[1,2]=0.0;  m[1,3]=0.0;      m[1,4]=0.0;
  m[2,1]=0.0;   m[2,2]=1/t;  m[2,3]=0.0;      m[2,4]=0.0;
  m[3,1]=0.0;   m[3,2]=0.0;  m[3,3]=-2/(f-n); m[3,4]=-(f+n)/(f-n);
  m[4,1]=0.0;   m[4,2]=0.0;  m[4,3]=0.0;      m[4,4]=1.0;

  /*--Set up X rotation matrix--*/
  rx[1,1]=1;     rx[1,2]=0.0;     rx[1,3]=0.0;      rx[1,4]=0.0;
  rx[2,1]=0.0;   rx[2,2]=cos(A);  rx[2,3]=-sin(A);  rx[2,4]=0.0;
  rx[3,1]=0.0;   rx[3,2]=sin(A);  rx[3,3]=cos(A);   rx[3,4]=0.0;
  rx[4,1]=0.0;   rx[4,2]=0.0;     rx[4,3]=0.0;      rx[4,4]=1.0;

  /*--Set up Y rotation matrix--*/
  ry[1,1]=cos(B);  ry[1,2]=0.0;  ry[1,3]=sin(B);  ry[1,4]=0.0;
  ry[2,1]=0.0;     ry[2,2]=1.0;  ry[2,3]=0.0;     ry[2,4]=0.0;
  ry[3,1]=-sin(B); ry[3,2]=0.0;  ry[3,3]=cos(B);  ry[3,4]=0.0;
  ry[4,1]=0.0;     ry[4,2]=0.0;  ry[4,3]=0.0;     ry[4,4]=1.0;

  /*--Set up Z rotation matrix--*/
  rz[1,1]=cos(C);  rz[1,2]=-sin(C); rz[1,3]=0.0;  rz[1,4]=0.0;
  rz[2,1]=sin(C);  rz[2,2]=cos(C);  rz[2,3]=0.0;  rz[2,4]=0.0;
  rz[3,1]=0.0;     rz[3,2]=0.0;     rz[3,3]=1.0;  rz[3,4]=0.0;
  rz[4,1]=0.0;     rz[4,2]=0.0;     rz[4,3]=0.0;  rz[4,4]=1.0;

  /*--Build transform matrix--*/
  call MatMult(rz, m, u);
  call MatMult(ry, u, v);
  call MatMult(rx, v, w);

  set &WallData;

  /*--Set axis labels--*/
  if label eq 1 then lbx="&Lblx";
  if label eq 2 then lby="&Lbly";
  if label eq 3 then lbz="&Lblz";

  /*--Transform walls--*/
  d[1,1]=xw*&xnormalscale; d[2,1]=yw*&ynormalscale; d[3,1]=zw*&znormalscale; d[4,1]=1;
  call MatMult(w, d, p);
  xw=p[1,1]; yw=p[2,1]; zw=p[3,1];

  /*--Transform axes--*/
  d[1,1]=xw2*&xnormalscale; d[2,1]=yw2*&ynormalscale; d[3,1]=zw2*&znormalscale; d[4,1]=1;
  call MatMult(w, d, p);
  xw2=p[1,1]; yw2=p[2,1]; zw2=p[3,1];

  /*--Transform labels--*/
  d[1,1]=xl*&xnormalscale; d[2,1]=yl*&ynormalscale; d[3,1]=zl*&znormalscale; d[4,1]=1;
  call MatMult(w, d, p);
  xl=p[1,1]; yl=p[2,1]; zl=p[3,1];
run;
/**/

/*--Compute data ranges--*/
data _null_;
  retain xmin 1e10 xmax -1e10 ymin 1e10 ymax -1e10 zmin 1e10 zmax -1e10;
  set &Data end=last;
  xmin=min(xmin, &X);
  xmax=max(xmax, &X);
  ymin=min(ymin, &Y);
  ymax=max(ymax, &Y);
  zmin=min(zmin, &Z);
  zmax=max(zmax, &Z);
  if last then do;
    call symputx("xmin", xmin); call symputx("xmax", xmax);
    call symputx("ymin", ymin); call symputx("ymax", ymax);
    call symputx("zmin", zmin); call symputx("zmax", zmax);
  end;
run;

/*--Normalize the data to -1 to +1 ranges--*/
data normalized;
  keep &Group &Size x y z xf yf zf xb yb zb xb2 yb2 zb2 xs ys zs xs2 ys2 zs2;

  xrange=&xmax-&xmin;
  yrange=&ymax-&ymin;
  zrange=&zmax-&zmin;

  set &data;

  /*--data points--*/
  x=(2*&xnormalscale)*(&X-&xmin)/xrange - &xnormalscale;
  y=(2*&ynormalscale)*(&Y-&ymin)/yrange - &ynormalscale;
  z=(2*&znormalscale)*(&Z-&zmin)/zrange - &znormalscale;

  /*--Floor--*/
  xf=x; yf=y; zf=-1;

  /*--Back Wall--*/
  xb=-1; yb=y; zb=z;
  xb2=-1; yb2=y; zb2=-1;

  /*--Side Wall--*/
  xs=x; ys=1; zs=z;
  xs2=x; ys2=1; zs2=-1;
run;

/*--Project the data--*/
data projected_data;
  keep &Group &Size xd yd zd xf yf zf xb yb zb xb2 yb2 zb2 xs ys zs xs2 ys2 zs2;
  array u[4,4] _temporary_;  /*--Intermediate Matrix--*/
  array v[4,4] _temporary_;  /*--Intermediate Matrix--*/
  array w[4,4] _temporary_;  /*--Final View Matrix--*/
  array m[4,4] _temporary_;  /*--Projection Matrix--*/
  array rx[4,4] _temporary_; /*--X rotation Matrix--*/
  array ry[4,4] _temporary_; /*--Y rotation Matrix--*/
  array rz[4,4] _temporary_; /*--Z rotation Matrix--*/
  array d[4,1] _temporary_;  /*--World Data Array --*/
  array p[4,1] _temporary_;  /*--Projected Data Array --*/
  retain r t f n;
  r=1; t=1; f=1; n=-1;
  pi=constant("PI");
  fac=pi/180;
/*  call symput ("X", A); call symput ("Y", B); call symput ("Z", C);*/
  A=&A*fac; B=&B*fac; C=&C*fac;

  /*--Set up projection matrix--*/
  m[1,1]=1/r;   m[1,2]=0.0;  m[1,3]=0.0;      m[1,4]=0.0;
  m[2,1]=0.0;   m[2,2]=1/t;  m[2,3]=0.0;      m[2,4]=0.0;
  m[3,1]=0.0;   m[3,2]=0.0;  m[3,3]=-2/(f-n); m[3,4]=-(f+n)/(f-n);
  m[4,1]=0.0;   m[4,2]=0.0;  m[4,3]=0.0;      m[4,4]=1.0;

  /*--Set up X rotation matrix--*/
  rx[1,1]=1;     rx[1,2]=0.0;     rx[1,3]=0.0;      rx[1,4]=0.0;
  rx[2,1]=0.0;   rx[2,2]=cos(A);  rx[2,3]=-sin(A);  rx[2,4]=0.0;
  rx[3,1]=0.0;   rx[3,2]=sin(A);  rx[3,3]=cos(A);   rx[3,4]=0.0;
  rx[4,1]=0.0;   rx[4,2]=0.0;     rx[4,3]=0.0;      rx[4,4]=1.0;

  /*--Set up Y rotation matrix--*/
  ry[1,1]=cos(B);  ry[1,2]=0.0;  ry[1,3]=sin(B);  ry[1,4]=0.0;
  ry[2,1]=0.0;     ry[2,2]=1.0;  ry[2,3]=0.0;     ry[2,4]=0.0;
  ry[3,1]=-sin(B); ry[3,2]=0.0;  ry[3,3]=cos(B);  ry[3,4]=0.0;
  ry[4,1]=0.0;     ry[4,2]=0.0;  ry[4,3]=0.0;     ry[4,4]=1.0;

  /*--Set up Z rotation matrix--*/
  rz[1,1]=cos(C);  rz[1,2]=-sin(C); rz[1,3]=0.0;  rz[1,4]=0.0;
  rz[2,1]=sin(C);  rz[2,2]=cos(C);  rz[2,3]=0.0;  rz[2,4]=0.0;
  rz[3,1]=0.0;     rz[3,2]=0.0;     rz[3,3]=1.0;  rz[3,4]=0.0;
  rz[4,1]=0.0;     rz[4,2]=0.0;     rz[4,3]=0.0;  rz[4,4]=1.0;

  /*--Build transform matris--*/
  call MatMult(rz, m, u);
  call MatMult(ry, u, v);
  call MatMult(rx, v, w);

  set normalized;

  /*--Transform data--*/
  d[1,1]=x; d[2,1]=y; d[3,1]=z; d[4,1]=1;
  call MatMult(w, d, p);
  xd=p[1,1]; yd=p[2,1]; zd=p[3,1]; wd=p[4,1];

  /*--Transform floor drop shadow--*/
  d[1,1]=xf; d[2,1]=yf; d[3,1]=zf; d[4,1]=1;
  call MatMult(w, d, p);
  xf=p[1,1]; yf=p[2,1]; zf=p[3,1]; wf=p[4,1];

  /*--Transform back wall shadow--*/
  d[1,1]=xb; d[2,1]=yb; d[3,1]=zb; d[4,1]=1;
  call MatMult(w, d, p);
  xb=p[1,1]; yb=p[2,1]; zb=p[3,1]; wb=p[4,1];

  d[1,1]=xb2; d[2,1]=yb2; d[3,1]=zb2; d[4,1]=1;
  call MatMult(w, d, p);
  xb2=p[1,1]; yb2=p[2,1]; zb2=p[3,1]; wb2=p[4,1];

  /*--Transform side wall shadow--*/
  d[1,1]=xs; d[2,1]=ys; d[3,1]=zs; d[4,1]=1;
  call MatMult(w, d, p);
  xs=p[1,1]; ys=p[2,1]; zs=p[3,1]; ws=p[4,1];

  d[1,1]=xs2; d[2,1]=ys2; d[3,1]=zs2; d[4,1]=1;
  call MatMult(w, d, p);
  xs2=p[1,1]; ys2=p[2,1]; zs2=p[3,1]; ws2=p[4,1];
run;

/*--Combine data with walls--*/
data combined;
  merge projected_walls projected_data;
run;

%let h=_; 
%let suf=&a&h&c;

/*--Draw the graph--*/
options mprint;

title "&Title";
footnote j=l  h=0.7 "X:&X-Rotation=&A  Y:&Y-Rotation=&B  Z:&Z-Rotation=&C";

proc sgplot data=combined nowall noborder aspect=1 noautolegend dattrmap=&Attrmap des="Ortho3D plot";

  %if &shadewalls = yes %then %do;
  polygon id=id x=xw y=yw / fill lineattrs=(color=lightgray) 
          group=id transparency=0 attrid=walls;
  %end;

  %if &outlineaxis = yes %then %do;
  vector x=xw2 y=yw2 / xorigin=xw yorigin=yw group=group noarrowheads attrid=Axes;
  %end;

  text x=xl y=yl text=lbx / position=bottomleft;
  text x=xl y=yl text=lby / position=bottomright;
  text x=xl y=yl text=lbz / position=left;

  %if &backwall = yes %then %do;
  * --Back wall shadow--;
  vector x=xb y=yb / xorigin=xb2 yorigin=yb2 noarrowheads lineattrs=(color=gray) transparency=0.9;
  scatter x=xb y=yb / markerattrs=(symbol=circlefilled size=5) group=&group transparency=0.9;
  %end;

  %if &sidewall = yes %then %do;
  *--Side wall shadow--;
  vector x=xs y=ys / xorigin=xs2 yorigin=ys2 noarrowheads lineattrs=(color=gray) transparency=0.9;
  scatter x=xs y=ys / markerattrs=(symbol=circlefilled size=5) group=&group transparency=0.9;
  %end;

  %if &floorwall = yes %then %do;
  *--Floor line to data;
  vector x=xd y=yd / xorigin=xf yorigin=yf noarrowheads lineattrs=(thickness=&floor_marker_size) group=&group transparency=0.7;
  *--Floor shadow--;
  scatter x=xf y=yf / markerattrs=(symbol=&floor_marker_symbol size=&floor_marker_size) group=&group transparency=0.7;
  %end;

  %if &showdata = yes %then %do;
  *--Data--;
  scatter x=xd y=yd / group=&Group name='s' nomissinggroup dataskin=gloss
         filledoutlinedmarkers markerattrs=(symbol=&floor_marker_symbol size=&floor_marker_size) dataskin=gloss;
  %end;

  keylegend 's' / autoitemsize;
  xaxis display=none offsetmin=0.05 offsetmax=0.05 min=-1.8 max=1.8;
  yaxis display=none offsetmin=0.05 offsetmax=0.05 min=-1.8 max=1.8;
  run;
footnote;

%finished:
%mend Ortho3D_Macro;

Step 4 - Using the macro

/*--Define Attributes map for walls and axes--*/
data attrmap;
length ID $ 9 fillcolor $ 10 linecolor $ 10 linepattern $ 10;
input id $ value $10-20 fillcolor $ linecolor $ linepattern $;
datalines;
Walls    Bottom     cxdfdfdf   cxdfdfdf   Solid     
Walls    Back       cxefefef   cxefefef   Solid    
Walls    Right      cxffffff   cxffffff   Solid    
Axes     D          white      black      Solid
Axes     L          white      black      ShortDash
;
run;

options source;

options mautosource nomprint nomlogic;
ods listing close;

ods html5 file='c:\temp\sample.html' gpath='c:\temp';

options cmplib=sasuser.funcs;

%let dpi=200;

ods graphics / reset
  attrpriority=color 
  imagefmt=png
  imagename="Sample"
  width=11in
  height=8in
;

%Ortho3D_Macro (
      Title=Comparative ODE values by province over period
    , Data=work.have_jittered, Attrmap=attrmap
    , X=province_id, Lblx=Province, xtic=1 to 2, xtic_jitter=0.1
    , Y=period_id,   Lbly=Period,   ytic=1 to 10, ytic_jitter=0
    , Z=value,       Lblz=Value,    ztic=0 to 1e6 by 2e5, ztic_jitter=0
    , Group=Var
    , Tilt=55, Rotate=-25
    , Size=size
    , outlineaxis = no
    , shadewalls = yes
    , backwall = no
    , sidewall = no
    , floorwall = yes
    , floor_marker_symbol = diamondfilled
    , floor_marker_size = 6
    , showdata = yes
    );

ods html5 close;

Other considerations

As with any 3d plot the question arises "do you need a viewer" for spinning, tilting and zooming the data presentation. SAS ActiveX driver does produce such an output, however, most current browsers have disabled, deprecated or removed plug-ins (needed to run ActiveX or Java applets).

Instead of diving enormously deep into SGPLOT and GTL, perhaps the effort is better spent learning WebGL and the three.js library. Once that understanding is gained, a SAS program can write html pages that contain or refer to the chart data and a viewer that a modern browser will render natively.

I am not up on latest SAS R&D into new presentation drivers, perhaps there is already work afoot on a WebGL device (similar to ActiveX or Java.) Heck, there is a SAS VRML graphics driver -- but I haven't fiddled with that in years.

Upvotes: 1

Dirk Horsten
Dirk Horsten

Reputation: 3845

*

About this answer

The procedure G3D allows plotting the type of 3 dimensional graphs you want, but

  • you need numeric class variables, so use a format to make them display as characters
  • it does not overlay two plots, but allows coloring data points

Tested Sollution

This does not meet some of your criteria, but I created it at work and could test it.

Put OUT_BUD_ODE and TAR_BUD_ODE side by side;

data IN_CHAR;
    infile datalines truncover;
    input PROVINCE $13. PERIOD $6. VAR $13. VALUE 8.;
    datalines;
Bandundu    2013S1  OUT_BUD_ODE 250000
Bandundu    2013S1  TAR_BUD_ODE 545000
Bandundu    2013S2  OUT_BUD_ODE 283000
Bandundu    2013S2  TAR_BUD_ODE 545000
Bandundu    2014S1  OUT_BUD_ODE 800000
Bandundu    2014S1  TAR_BUD_ODE 645000
Bandundu    2014S2  OUT_BUD_ODE 700000
Bandundu    2014S2  TAR_BUD_ODE 645000
Bandundu    2015S1  OUT_BUD_ODE 369363
Bandundu    2015S1  TAR_BUD_ODE 945288
Bandundu    2015S2  OUT_BUD_ODE 1217449
Bandundu    2015S2  TAR_BUD_ODE 958417
Bandundu    2016S1  OUT_BUD_ODE 96618
Bandundu    2016S1  TAR_BUD_ODE 787740
Bandundu    2016S2  OUT_BUD_ODE 1358568
Bandundu    2016S2  TAR_BUD_ODE 787740
Bandundu    2017S1  OUT_BUD_ODE 1312900
Bandundu    2017S1  TAR_BUD_ODE 311729
Bandundu    2017S2  OUT_BUD_ODE .
Bandundu    2017S2  TAR_BUD_ODE 1299771
Bas-Congo   2013S1  OUT_BUD_ODE 150000
Bas-Congo   2013S1  TAR_BUD_ODE 450000
Bas-Congo   2013S2  OUT_BUD_ODE 145000
Bas-Congo   2013S2  TAR_BUD_ODE 450000
Bas-Congo   2014S1  OUT_BUD_ODE 1000000
Bas-Congo   2014S1  TAR_BUD_ODE 900965
Bas-Congo   2014S2  OUT_BUD_ODE 900000
Bas-Congo   2014S2  TAR_BUD_ODE 900965
Bas-Congo   2015S1  OUT_BUD_ODE 433892
Bas-Congo   2015S1  TAR_BUD_ODE 1115965
Bas-Congo   2015S2  OUT_BUD_ODE 943387
Bas-Congo   2015S2  TAR_BUD_ODE 1115965
Bas-Congo   2016S1  OUT_BUD_ODE 552002
;
run;

*

Create formats for the class variables

This could also be achieved with sql select distinct;

proc means data=IN_CHAR noprint;
    class province period;
    types province period;
    output out=CNT_VALUES n=;
run;

data CNTL;
    set CNT_VALUES;
    * Create a format for the provices *;
    if not missing(PROVINCE) then do;
        FmtName ='province';
        label = PROVINCE;
    end;

    * Create a format for the periods *;
    if not missing(PERIOD) then do;
        FmtName ='period';
        label = PERIOD;
    end;

    start = _N_; * Just to have some unique numeric identifier *;
    keep FmtName label start;
run;
proc format cntlin=CNTL;
run;

*

Make class variables numeric but display as text;

proc sql;
    create table IN_NUM as
    select IN_.VALUE
         , PROV.start as PROVINCE format=province.
         , PER.start as PERIOD format=period.
         , case VAR
               when 'TAR_BUD_ODE' then 'blue'
               when 'OUT_BUD_ODE' then 'crimson'
           end as color
    from IN_CHAR as IN_
    left join CNTL as PROV on PROV.label = IN_.PROVINCE
    left join CNTL as PER on PER.label = IN_.PERIOD
    order by IN_.PROVINCE, IN_.PERIOD;
quit;

*

creat chart ;

proc g3d data=IN_NUM;
    scatter PERIOD * PROVINCE = VALUE / color=color;
run;

enter image description here

How I would proceed

I will not test this now, as I should start packing for a vacation, but here are some ideas. There is surely an option of g3d to replace the needles by cilinders or bars. To introduce a shift, I would

Shift the Province of the actual, i.e. of OUT_BUD_ODE

    create table IN_NUM as
    select IN_.VALUE
         , case VAR
               when 'TAR_BUD_ODE' then PROV.start
               when 'OUT_BUD_ODE' then PROV.start + 0.2
           end as  PROVINCE format=province.
         , PROV.start as PROVINCE format=province.
         , PER.start as PERIOD format=period.
         , case VAR
               when 'TAR_BUD_ODE' then 'blue'
               when 'OUT_BUD_ODE' then 'crimson'
           end as color
    ...

Extend the format with an end, so that the province of OUT_BUD_ODE displays correctly

data CNTL;
    set CNT_VALUES;
    ...

    start = _N_; 
    end = _N_ + 0.5;
    keep FmtName label start;
run;

Upvotes: 1

Dirk Horsten
Dirk Horsten

Reputation: 3845

Replace proc sgplot with proc sgpanel

There is a lot to tune about the solution I propose, but basically it is:

proc sgpanel data=Before;
    panelby PROVINCE ;
    vbar PERIOD / freq=VALUE group= VAR groupdisplay=cluster;
run;

Preparing the data

I loaded the data with:

data Before;
    infile datalines truncover;
    input PROVINCE $13. year 4. semester $2. VAR $13. VALUE 8.;
    format period date9.;
    select (semester);
        when ('S1') PERIOD = mdy(1, 1, year);
        when ('S2') PERIOD = mdy(7, 1, year);
    end;
    datalines;
Bandundu    2013S1  OUT_BUD_ODE 250000
Bandundu    2013S1  TAR_BUD_ODE 545000
Bandundu    2013S2  OUT_BUD_ODE 283000
Bandundu    2013S2  TAR_BUD_ODE 545000
Bandundu    2014S1  OUT_BUD_ODE 800000
Bandundu    2014S1  TAR_BUD_ODE 645000
Bandundu    2014S2  OUT_BUD_ODE 700000
Bandundu    2014S2  TAR_BUD_ODE 645000
Bandundu    2015S1  OUT_BUD_ODE 369363
Bandundu    2015S1  TAR_BUD_ODE 945288
Bandundu    2015S2  OUT_BUD_ODE 1217449
Bandundu    2015S2  TAR_BUD_ODE 958417
Bandundu    2016S1  OUT_BUD_ODE 96618
Bandundu    2016S1  TAR_BUD_ODE 787740
Bandundu    2016S2  OUT_BUD_ODE 1358568
Bandundu    2016S2  TAR_BUD_ODE 787740
Bandundu    2017S1  OUT_BUD_ODE 1312900
Bandundu    2017S1  TAR_BUD_ODE 311729
Bandundu    2017S2  OUT_BUD_ODE .
Bandundu    2017S2  TAR_BUD_ODE 1299771
Bas-Congo   2013S1  OUT_BUD_ODE 150000
Bas-Congo   2013S1  TAR_BUD_ODE 450000
Bas-Congo   2013S2  OUT_BUD_ODE 145000
Bas-Congo   2013S2  TAR_BUD_ODE 450000
Bas-Congo   2014S1  OUT_BUD_ODE 1000000
Bas-Congo   2014S1  TAR_BUD_ODE 900965
Bas-Congo   2014S2  OUT_BUD_ODE 900000
Bas-Congo   2014S2  TAR_BUD_ODE 900965
Bas-Congo   2015S1  OUT_BUD_ODE 433892
Bas-Congo   2015S1  TAR_BUD_ODE 1115965
Bas-Congo   2015S2  OUT_BUD_ODE 943387
Bas-Congo   2015S2  TAR_BUD_ODE 1115965
Bas-Congo   2016S1  OUT_BUD_ODE 552002
;

Sugestion: if you include your code in the answer, I will tune my sollution to look more as you expected.

Upvotes: 0

Related Questions