<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="gfn2html.xsl"?>
<gretl-functions>
<gretl-function-package name="SankeyPlot" no-data-ok="true" minver="2025a">
<author email="r.lucchetti@univpm.it">Riccardo &quot;Jack&quot; Lucchetti</author>
<version>1.0</version>
<date>2026-11-08</date>
<description>Sankey (ribbon) plots</description>
<tags>C14</tags>
<label>Sankey plot</label>
<menu-attachment>MAINWIN/View/GraphVars</menu-attachment>
<help filename="SankeyPlot_help.md">
Here you find function for generating &quot;Sankey plots&quot;, that is plots
used for depicting graphically the contents of a contingency table
using coloured ribbons. A typical use case is for transition matrices:
each ribbon identifies a flow going from the initial to the final
state.

The package contains two functions: the main function is `SankeyPlot`;
the other one, `SP_series`, is a convenience function that makes
things a little easier in some cases.

# SankeyPlot()

```
string SankeyPlot(const matrix X, bundle optbun[null])
```

Draws a Sankey diagram in which each row of `X` is a node on the left,
each column is a node on the right, and the ribbon joining row *i* to
column *j* has a width proportional to `X[i,j]`. Ribbons take the
colour of their originating row. Row nodes are drawn as coloured boxes
on the left, column nodes as boxes on the right, coloured in shades of
grey. The function returns the gnuplot script that was used to produce the
plot, which can be saved, inspected or modified.

## Arguments

- *X*: matrix, non-negative flows. Rows are the left-hand nodes, columns the right-hand nodes. At most 8 rows and 8 columns.
- *opt*: bundle (optional), see below.

## Options

Pass options as members of a bundle.

- *colors*: matrix, row colours, expressed in hexadecimal notation
  (`0xRRGGBB`). Row *i* uses `colors[i]`. The defaults are
  1. 0xFF0000 (red)
  2. 0x0000FF (blue)
  3. 0x00CC00 (green)
  4. 0xBF25B2 (purple)
  5. 0xB58E2C (brown)
  6. 0x8FAAB3 (grey)
  7. 0xFFAA00 (orange)
  8. 0x91A4FF (light blue)
    
- *sidelabels*, 2-element string array: rotated labels placed outside
  the left and right edges of the plot (default: no labels). Note that
  any additional labels in the array do no harm, but will be ignored.
- *title*, string: Plot title. An empty string means no title, as per default.
- *output*, string `&quot;display&quot;` shows it on screen (default); a
   filename (for example `&quot;flows.pdf&quot;` or `&quot;flows.png&quot;`) writes to a
   file, with the format inferred from the extension.

## Labels

Node labels are taken from the row and column names of `X`. If no
names are set, the labels default to `Row 1`, `Row 2`, ... and `Col 1`, `Col 2`, ...

## Return value

A string holding the gnuplot script.

## Limits and notes

- `X` must have at most 8 rows and 8 columns, otherwise the function stops with an error.
- Entries of `X` should be non-negative. Zero entries produce no ribbon.


# SP_series()

```
string SP_series(const series a, const series b, bundle opts[null])
```

Builds the contingency table of two series with `mxtab()` and passes
it to `SankeyPlot()`. The categories of `a` form the left-hand nodes
and the categories of `b` the right-hand nodes; each ribbon's width
reflects the number of observations falling in that combination. If a
series is string-valued, its string labels are used automatically as
row or column names. Otherwise the default `Row n` / `Col n` labels
apply. A simplified versaion of this functionis also available via the
GUI under `View/Graph specified vars`.

## Arguments

- *a*, series: series for the left-hand categories.
- *b*, series: series defining the right-hand categories.
- *opts*, bundle (optional): options bundle, exactly as for `SankeyPlot()`.

## Return value

The gnuplot script string returned by `SankeyPlot()`.

## Notes

- each series can have at most 8 distinct values.
</help>
<gretl-function name="SankeyPlot" type="string">
 <params count="2">
  <param name="X" type="matrix" const="true"/>
  <param name="optbun" type="bundle" optional="true"/>
 </params>
<code>r = rows(X)
c = cols(X)

errorif(r &gt; 8, &quot;too many rows&quot;)
errorif(c &gt; 8, &quot;too many columns&quot;)
errorif(min(X) &lt; 0, argname(X) ~ &quot; contains negative entries&quot;)

opt = default_opts()
if exists(optbun)
  opt = optbun + opt
endif

# rescale if needed (maybe unnecessary? I'll keep it just in case)

scalar rescale = 1
tot = sum(X)
if rescale
  fX = X ./ tot
  tot = 1
else
  fX = X
endif

# side labels

strings rlab = rnameget(X)
if nelem(rlab) == 0
  rlab = &quot;Row &quot; ~ seq(1,r)
endif

strings clab = cnameget(X)
if nelem(clab) == 0
  clab = &quot;Col &quot; ~ seq(1,c)
endif

# compoute the ribbon edges

widths = vec(fX')

y0 = (0 | cum(vec(fX')))[-end]
y1 = (0 | cum(vec(fX)))[-end]
y1 = commute(y1, r, c)

# initialise stuff for gnuplot script creation

strings ribbons = array(0)
np = 0
colors = opt.colors
k = 1
sidewid = 0.167
set force_decpoint on
# start gnuplot script

gp = preamble(tot, sidewid)
gp ~= do_labels(opt, tot, sidewid)

# do side labels

boxfmt = &quot;set object %d rect from %g,%g to %g, %g fc rgb \&quot;0xaa%06x\&quot; noclip fillstyle solid 0.5&quot;

pts = y0[1 + seq(0, r*c-1, c) ] | tot
loop i = 1 .. r
  box = sprintf(boxfmt, i, -sidewid, pts[i], 0, pts[i+1], colors[i])
  gp ~= sprintf(&quot;\n%s&quot;, box)
  lab = sprintf(&quot;set label \&quot;%s\&quot; at -%g, %g&quot;, rlab[i], sidewid-0.01, 0.5*(pts[i] + pts[i+1]))
  gp ~= sprintf(&quot;\n%s&quot;, lab)
endloop
gp ~= sprintf(&quot;\n&quot;)

pts = mshape(y1, c, r)[,1] | tot

color = 0xffffff
loop i = 1 .. c
  box = sprintf(boxfmt, i+r, 1, pts[i], 1+sidewid, pts[i+1], color)
  gp ~= sprintf(&quot;\n%s&quot;, box)
  lab = sprintf(&quot;set label \&quot;%s\&quot; at 1.05, %g&quot;, clab[i], 0.5*(pts[i] + pts[i+1]))
  gp ~= sprintf(&quot;\n%s&quot;, lab)
  color -= 0x111111 * floor(7/r)
endloop
gp ~= sprintf(&quot;\n&quot;)

# do ribbons

fmtstr = &quot;'+' using 1:(bot_%d_%d($1)):(top_%d_%d($1)) with filledcurves lc rgb \&quot;0xbb%06x\&quot;&quot;

loop i = 1 .. r
  loop j = 1 .. c
    if widths[k] &gt; 0
      x0 = y0[k] ~ y1[k]
      x1 = x0 + widths[k]
      gp ~= sankey_func(i,j, x0, x1)
      ribbons += sprintf(fmtstr, i,j,i,j, colors[i])
      np++
    endif
    k++
  endloop
endloop

gp ~= sprintf(&quot;\n\nplot &quot;) ~ ribbons[1]  ~ sprintf(&quot;, \\\n&quot;)
tmp = &quot;     &quot; ~ ribbons[2:end-1] ~ sprintf(&quot;, \\&quot;)
gp ~= flatten(tmp)
gp ~= sprintf(&quot;\n     &quot;) ~ ribbons[end] ~ sprintf(&quot;\n&quot;)

# finalise the plot

dest = opt.output
gnuplot --inbuf=gp --output=&quot;@dest&quot;

return gp
</code>
</gretl-function>
<gretl-function name="SP_series" type="string">
 <params count="3">
  <param name="a" type="series" const="true"/>
  <param name="b" type="series" const="true"/>
  <param name="opts" type="bundle" optional="true"/>
 </params>
<code>X = mxtab(a, b)
l = strvals(a)
if nelem(l)
  rnameset(X, l)
else
  rnameset(X, &quot;&quot; ~ values(a))
endif

l = strvals(b)
if nelem(l)
  cnameset(X, l)
else
  cnameset(X, &quot;&quot; ~ values(b))
endif

if !exists(opts)
  opts = _()
endif

ret = SankeyPlot(X, opts)
return ret
</code>
</gretl-function>
<gretl-function name="SP_GUI" type="string" no-print="1" pkg-role="gui-main">
 <params count="3">
  <param name="a" type="series" const="true"/>
  <param name="b" type="series" const="true"/>
  <param name="title" type="string" optional="true"/>
 </params>
<code>opts= _()
if exists(title)
  opts.title = title
endif
opts.sidelabels = defarray(argname(a), argname(b))

ret = SP_series(a, b, opts)
return ret
</code>
</gretl-function>
<gretl-function name="default_opts" type="bundle" private="1">
<code>bundle opts = empty
matrix opts.colors = {0xFF0000;0x0000FF;0x00CC00;0xBF25B2;0xB58E2C;0x8FAAB3;0xFFAA00;0x91A4FF}
strings opts.sidelabels = defarray(&quot;&quot;, &quot;&quot;)
string opts.title = &quot;&quot;
opts.output = &quot;display&quot;
return opts
</code>
</gretl-function>
<gretl-function name="smcurve_coeff" type="matrix" private="1">
 <params count="1">
  <param name="y" type="matrix" const="true"/>
 </params>
<code>/* -doc-
This function computes the coefficients of a cubic function such that
f(0) = y[1]
f(1) = y[2]
and f'(0) = f'(1) = 0
*/
Ainv = {1,0,0,0; 0,0,1,0; -3,3,-2,-1; 2,-2,1,1}
matrix ret = (vec(y) | {0;0})
return Ainv * ret
</code>
</gretl-function>
<gretl-function name="smcurve_gpstring" type="string" private="1">
 <params count="2">
  <param name="y" type="matrix" const="true"/>
  <param name="name" type="string"/>
 </params>
<code>/* -doc-
This function creates a string defining a cubic function in gnuplot
syntax with a given name and coefficients such that the function has
beginning and end points given by the 2-vector y
*/
coef = smcurve_coeff(y)
ret = sprintf(&quot;%s(x) = %g + (%g) * x + (%g) * x*x + (%g) * x*x*x&quot;, name, coef[1], coef[2], coef[3], coef[4])
return ret
</code>
</gretl-function>
<gretl-function name="sankey_func" type="string" private="1">
 <params count="4">
  <param name="i" type="scalar"/>
  <param name="j" type="scalar"/>
  <param name="y0" type="matrix"/>
  <param name="y1" type="matrix"/>
 </params>
<code>/* -doc-
This function creates a gnuplot snipptes with the functions defining
the top and botton of the (i,j) ribbon
*/

name = sprintf(&quot;bot_%d_%d&quot;, i, j)
ret = sprintf(&quot;%s\n&quot;, smcurve_gpstring(y0, name))
name = sprintf(&quot;top_%d_%d&quot;, i, j)
ret = ret ~ sprintf(&quot;%s\n&quot;, smcurve_gpstring(y1, name))

return ret
</code>
</gretl-function>
<gretl-function name="preamble" type="string" private="1">
 <params count="2">
  <param name="tot" type="scalar"/>
  <param name="sidewid" type="scalar"/>
 </params>
<code>ret = sprintf(&quot;unset key\nset xrange [0:1]\nset yrange [0:%g]\n&quot;, tot)
ret = ret ~ sprintf(&quot;unset xtics\nunset ytics\n&quot;)
ret = sprintf(&quot;%s\nset size %g,1\n&quot;, ret, 1-2*sidewid)
ret = sprintf(&quot;%s\nset origin %g,0\n&quot;, ret, sidewid)
return ret
</code>
</gretl-function>
<gretl-function name="do_labels" type="string" private="1">
 <params count="3">
  <param name="opt" type="bundle"/>
  <param name="tot" type="scalar"/>
  <param name="sidewid" type="scalar"/>
 </params>
<code>ret = &quot;&quot;
if strlen(opt.title)
  ret = ret ~ sprintf(&quot;set title \&quot;%s\&quot;\n&quot;, opt.title)
endif

sidefmt = &quot;set label \&quot;%s\&quot; at %g, %g center rotate by 90\n&quot;
if nelem(opt.sidelabels[1]) &gt; 0
  ret = ret ~ sprintf(sidefmt, opt.sidelabels[1], -sidewid*1.3, tot/2)
endif

if nelem(opt.sidelabels[2]) &gt; 0
  ret = ret ~ sprintf(sidefmt, opt.sidelabels[2], 1+sidewid*1.3, tot/2)
endif
return ret
</code>
</gretl-function>
<sample-script>
set verbose off
clear --all
include SankeyPlot.gfn

EXAMPLE = 1

if EXAMPLE == 1
    # simple artificial example showing all the default colours
    r = 8
    c = 4
    X = mrandgen(i, 1, 10, r, c)
    print X
    
    SankeyPlot(X)

elif EXAMPLE == 2
    # matrix-oriented function with options
    open keane.gdt
    series edu_rec = (educ&gt;11) + (educ&gt;12)
    X = mxtab(edu_rec, choice)
    rnameset(X, strsplit(&quot;less than 12y,12y,college or more&quot;,&quot;,&quot;))
    X = mxtab(black, choice)
    rnameset(X, strsplit(&quot;white black&quot;))
    cnameset(X, strsplit(&quot;sch home wc bc serv&quot;))
    opts = _(sidelabels=defarray(&quot;ethnicity&quot;, &quot;choice&quot;),
      title = &quot;Keane &amp; Wolpin&quot;)

    SankeyPlot(X, opts)

elif EXAMPLE == 3
    # series-oriented function
    open mrw.gdt
    smpl gdp60 gdp85 --no-missing
    p = {0.333, 0.667}
    lab = strsplit(&quot;lo mid hi&quot;)
    
    q = quantile({gdp60}, p)
    series category0 = 1 + sumr({gdp60} .&gt; q')
    stringify(category0, lab ~ &quot;60&quot;)

    q = quantile({gdp85}, p)
    series category1 = 1 + sumr({gdp85} .&gt; q')
    stringify(category1, lab ~ &quot;85&quot;)
    
    xtab category0 category1
    SP_series(category0, category1, _(sidelabels=strsplit(&quot;1960 1985&quot;)))
    
endif
</sample-script>
</gretl-function-package>
</gretl-functions>
