# How to draw a Sankey Diagram using TikZ

This question led to a new package:
sankey

I would like to draw something similar to this (not exactly):

using TikZ. Ideally I would be able to set the width by variables and rejoin two flows into one again. Is there a good way to do this?

Since this will be my first TikZ diagram I am looking for pointers how to generally approach this problem, not for a complete solution, so that I can read the documentation effectively. How would I branch of a (variable) portion of a rectangle? How would I rejoin such portions into one again?

partial solution:

\documentclass{article}
\usepackage{tikz}
\begin{document}
\begin{tikzpicture}[scale=.5]
\draw[very thick] (1,0) -- (4,-1) -- (7,0) -- (7,-4) to [out=-90,in=180] (8,-5) -- (23,-5) -- (24,-6.5) --  (23,-8) -- (20,-8) to [out=180,in=0] (15,-10) -- (15,-10) (14,-10) -- (10,-10) to [out=0, in=90] (14,-14) -- (14,-8) to [out=90,in=0] (13,-7) -- (11,-7);
\draw[very thick] (15,-7) to [out=0,in=180] (20,-6) -- (11,-6) -- (13,-6) to [out=0,in=90] (15,-8) to (15,-20) -- (13.5,-21) -- (12,-20) to (12,-14) to [out=90,in=0] (11,-13) -- (5,-13) to [out=180,in=-90] (1,-9) to (1,0);
\draw[very thick] (4,-4) to [out=-90, in=180] (8,-8) -- (14,-8) (15,-8) -- (16,-8) to [out=0,in=180] (20,-7) to [out=180, in=0] (15,-9) (14,-9) -- (6,-9) to [out=180,in=-90] (4,-7) -- (4,-4);
\draw[very thick] (2,-6) to (2,-7) to [out=-90,in=180] (6,-11) -- (10,-11) to [out=0,in=90] (13,-14) -- (13,-14) to [out=90,in=0] (11,-12) -- (5,-12) to [out=180,in=-90] (2,-9) -- (2,-7);
\end{tikzpicture}
\end{document}


Gives:

but this is all hardcoded. I would ideally want to have the width of the flows variable (without needing to recalculate all the points by hand)

Syntax-wise I could imagine something like this:

\stream[width=x,direction=down] (initial_amount)
\redirect[initialamount,x\%][from=rightedge][to=right] (choide_A)
\redirect[choiceA,y\%][from=middle,rejoinrest=true][to=down] (choice_C)
\redirect[initial_amount,g\%][from=right][to=right] (choice_D) %i.e. that what's left of it after redirecting choice_A
\redirect[choid_D,h\%][from=left][to=down] (choice_E) %left in the sense that it the left side turned by 90 degrees

\join[initial_amount][choice_D][choice_C]
\join[choice_A][choice_E]


Points I'm thinking about

• streams have a flow direction, but tikz should be allowed to shift it (a right turn followed by a left turn or vice versa) for example when joining streams.
• layering should probably be so that first == lowest layer and whatever comes after should just pile on it
• straight portions, or shifting are adaptable in length/shift amount to whatever the spacing needs.

Update:

The following has a portion branching off to the right (\def'd by \choiceA)

\documentclass{article}
\usepackage{tikz}
\begin{document}
\def\startstreamx{1}
\def\startstreamy{1}
\def\initialwidth{6}
\def\initialheight{4}
\def\choiceA{.9}
\begin{tikzpicture}[scale=.5]
\draw[very thick] (\startstreamx,-\startstreamy-\initialheight) -- (\startstreamx,-\startstreamy) --    (\startstreamx+\initialwidth,-\startstreamy) -- (\startstreamx+\initialwidth,-\startstreamy-\initialheight);
\draw[very thick] (\startstreamx+\initialwidth,-\startstreamy-\initialheight) to [out=-90, in=180] (\startstreamx+\initialwidth+1,-\startstreamy-\initialheight-1);
\draw[very thick] (\startstreamx+\initialwidth-\choiceA*\initialwidth,-\startstreamy-\initialheight) to [out=-90, in=180] (\startstreamx+\initialwidth+1,-\startstreamy-\initialheight-1-\choiceA*\initialwidth);
\draw[very thick] (\startstreamx,-\startstreamy-\initialheight) to (\startstreamx,-\startstreamy-\initialheight-1-\choiceA*\initialwidth);
\draw[very thick] (\startstreamx+\initialwidth-\choiceA*\initialwidth,-\startstreamy-\initialheight) to (\startstreamx+\initialwidth-\choiceA*\initialwidth,-\startstreamy-\initialheight-1-\choiceA*\initialwidth);
\end{tikzpicture}
\end{document}


one could add now another such code snippet with the inital x and y and width values set to the end of the remaining (downwards) stream and again branch something off to the right.

• In English it's called a 'flow diagram' ;), or more specifically a 'Sankey diagram' Jan 5, 2012 at 17:45
• Generally, we take questions more seriously when the questioner demonstrates that they have at least attempted to do what they are asking about. You'll probably get better answers if you show us your attempts at drawing the diagram yourself… Jan 5, 2012 at 18:39
• That is an excellent first solution.. So, you mention that you don't like the hard coding. It would be good to see how you would like to be able to specify this. An example syntax would be useful. Jan 5, 2012 at 22:20
• I think this calls for a new package... :) Jan 6, 2012 at 19:44
• Just noting that matplotlib (plotting library for Python) has a module for creating Sankey diagrams, and with matplotlib2tikz you could get pgfplots/TikZ code. The code won't be necessarily elegant, and it will need some tweeks, but I figured it was worth mentioning. If you feel it can be useful, I can add an answer elaborating a little. Sep 2, 2012 at 15:31

# Edit

Since release 3.0 of TikZ/PGF, the arguments of atan2 are swapped.

# New Version (TikZ/PGF 3.0)

Here is a first attempt with my new environment sankeydiagram.

Its optional argument is useful to fix some global parameters:

• sankey tot quantity is a number and represents the total quantity of the global flow (default value: 100 for 100%).

• sankey tot length is the width of the global flow (default value: 100pt).

• sankey min radius is the minimum radius of each turn (default value: 30pt).

• sankey fill is the style used to fill the flows.

• sankey draw is the style used to draw the flows.

• sankey debug (a flag) is useful to debug a diagram during its construction (default value: false).

The sankeydiagram environment defines some useful commands to construct a sankey diagram:

• \sankeynode{prop}{angle}{name}{pos} makes a sankey node (a flow) of prop capacity (in quantity units), named name. Its orientation and position are given by angle and pos.

• \sankeynodestart and \sankeynodeend are similar to \sankeynode (same arguments) but make respectively and flow starting and a flow ending.

• \sankeyadvance{name}{distance} moves forward the sankey node named name.

• \sankeyturn{name}{angle} turns the sankey node named name.

• \sankeyfork{name}{list of forks} forks the sankey node named name. The list of forks is a list of pairs: quantity/name (the sum of quantities must be equal to the quantity of sankey node to fork).

Here is the results (first without sankey debug then with sankey debug).

And, now, the code:

\documentclass{standalone}
\usepackage{tikz}
\usetikzlibrary{calc}
\usepackage{etoolbox}

\pgfdeclarelayer{background}
\pgfdeclarelayer{foreground}
\pgfdeclarelayer{sankeydebug}
\pgfsetlayers{background,main,foreground,sankeydebug}

\newif\ifsankeydebug

\newenvironment{sankeydiagram}[1][]{

\def\sankeyflow##1##2{% sn, en
\path[sankey fill]
let
\p1=(##1.north east),\p2=(##1.south east),
\n1={atan2(\y1-\y2,\x1-\x2)-90},
\p3=(##2.north west),\p4=(##2.south west),
\n2={atan2(\y3-\y4,\x3-\x4)+90}
in
(\p1) to[out=\n1,in=\n2] (\p3) --
(\p4) to[in=\n1,out=\n2] (\p2) -- cycle;
\draw[sankey draw]
let
\p1=(##1.north east),\p2=(##1.south east),
\n1={atan2(\y1-\y2,\x1-\x2)-90},
\p3=(##2.north west),\p4=(##2.south west),
\n2={atan2(\y3-\y4,\x3-\x4)+90}
in
(\p1) to[out=\n1,in=\n2] (\p3)
(\p4) to[in=\n1,out=\n2] (\p2);
}

\tikzset{
sankey tot length/.store in=\sankeytotallen,
sankey tot quantity/.store in=\sankeytotalqty,
sankey min radius/.store in=\sankeyminradius,
sankey arrow length/.store in=\sankeyarrowlen,
sankey debug/.is if=sankeydebug,
sankey debug=false,
sankey flow/.style={
to path={
\pgfextra{
\pgfinterruptpath
\edef\sankeystart{\tikztostart}
\edef\sankeytarget{\tikztotarget}
\sankeyflow{\sankeystart}{\sankeytarget}
\endpgfinterruptpath
}
},
},
sankey node/.style={
inner sep=0,minimum height={sankeyqtytolen(##1)},
minimum width=0,draw=none,line width=0pt,
},
% sankey angle
sankey angle/.store in=\sankeyangle,
% sankey default styles
sankey fill/.style={line width=0pt,fill,white},
sankey draw/.style={draw=black,line width=.4pt},
}

\newcommand\sankeynode[4]{%prop,orientation,name,pos
\node[sankey node=##1,rotate=##2] (##3) at (##4) {};
\ifsankeydebug
\begin{pgfonlayer}{sankeydebug}
\draw[red,|-|] (##3.north west) -- (##3.south west);
\pgfmathsetmacro{\len}{sankeyqtytolen(##1)/3}
\draw[red] (##3.west)
-- ($(##3.west)!\len pt!90:(##3.south west)$)
node[font=\tiny,text=black] {##3};
\end{pgfonlayer}
\fi
}

\newcommand\sankeynodestart[4]{%prop,orientation,name,pos
\sankeynode{##1}{##2}{##3}{##4}
\begin{scope}[shift={(##3)},rotate=##2]
\path[sankey fill]
(##3.north west) -- ++(-\sankeyarrowlen,0)
-- ([xshift=-\sankeyarrowlen/6]##3.west)
-- ([xshift=-\sankeyarrowlen]##3.south west)
-- (##3.south west) -- cycle;
\path[sankey draw]
(##3.north west) -- ++(-\sankeyarrowlen,0)
-- ([xshift=-\sankeyarrowlen/6]##3.west)
-- ([xshift=-\sankeyarrowlen]##3.south west)
-- (##3.south west);
\end{scope}
}

\newcommand\sankeynodeend[4]{%prop,orientation,name,pos
\sankeynode{##1}{##2}{##3}{##4}
\begin{scope}[shift={(##3)},rotate=##2]
\path[sankey fill]
(##3.north east)
-- ([xshift=\sankeyarrowlen]##3.east)
-- (##3.south west) -- cycle;
\path[sankey draw]
(##3.north east)
-- ([xshift=\sankeyarrowlen]##3.east)
-- (##3.south west);
\end{scope}
}

\newcommand\sankeyadvance[3][]{%newname,name,distance
\edef\name{##2}
\ifstrempty{##1}{
\def\newname{##2}
\edef\name{##2-old}
\path [late options={name=##2,alias=\name}];
}{
\def\newname{##1}
}
\path
let
% sankey node angle
\p1=(##2.north east),
\p2=(##2.south east),
\n1={atan2(\y1-\y2,\x1-\x2)-90},
% sankey prop
\p3=($(\p1)-(\p2)$),
\n2={sankeylentoqty(veclen(\x3,\y3))},
% next position
\p4=($(##2.east)!##3!-90:(##2.north east)$)
in
\pgfextra{
\pgfmathsetmacro{\prop}{\n2}
\pgfinterruptpath
\sankeynode{\prop}{\n1}{\newname}{\p4}
\path (\name) to[sankey flow] (\newname);
\endpgfinterruptpath
};
}

\newcommand\sankeyturn[3][]{%newname,name,angle
\edef\name{##2}
\ifstrempty{##1}{
\def\newname{##2}
\edef\name{##2-old}
\path [late options={name=##2,alias=\name}];
}{
\def\newname{##1}
}
\ifnumgreater{##3}{0}{
\typeout{turn acw: ##3}
\path
let
% sankey node angle
\p1=(##2.north east),
\p2=(##2.south east),
\p3=($(\p1)!-\sankeyminradius!(\p2)$),
\n1={atan2(\y1-\y2,\x1-\x2)-90},
% sankey prop
\p4=($(\p1)-(\p2)$),
\n2={sankeylentoqty(veclen(\x4,\y4))},
\p5=(##2.east),
\p6=($(\p3)!1!##3:(\p5)$)
in
\pgfextra{
\pgfmathsetmacro{\prop}{\n2}
\pgfinterruptpath
% \fill[red] (\p3) circle (2pt);
% \fill[blue](\p6) circle (2pt);
\sankeynode{\prop}{\n1+##3}{\newname}{\p6}
\path (\name) to[sankey flow] (\newname);
\endpgfinterruptpath
};
}{
\typeout{turn acw: ##3}
\path
let
% sankey node angle
\p1=(##2.south east),
\p2=(##2.north east),
\p3=($(\p1)!-\sankeyminradius!(\p2)$),
\n1={atan2(\y1-\y2,\x1-\x2)+90},
% sankey prop
\p4=($(\p1)-(\p2)$),
\n2={sankeylentoqty(veclen(\x4,\y4))},
\p5=(##2.east),
\p6=($(\p3)!1!##3:(\p5)$)
in
\pgfextra{
\pgfmathsetmacro{\prop}{\n2}
\pgfinterruptpath
% \fill[red] (\p3) circle (2pt);
% \fill[blue](\p6) circle (2pt);
\sankeynode{\prop}{\n1+##3}{\newname}{\p6}
\path (\name) to[sankey flow] (\newname);
\endpgfinterruptpath
};
}
}

\newcommand\sankeyfork[2]{%name,list of forks
\def\name{##1}
\def\listofforks{##2}
\xdef\sankeytot{0}
\path
let
% sankey node angle
\p1=(\name.north east),
\p2=(\name.south east),
\n1={atan2(\y1-\y2,\x1-\x2)-90},
% sankey prop
\p4=($(\p1)-(\p2)$),
\n2={sankeylentoqty(veclen(\x4,\y4))}
in
\pgfextra{
\pgfmathsetmacro{\iprop}{\n2}
}
\foreach \prop/\name[count=\c] in \listofforks {
let
\p{start \name}=($(\p1)!\sankeytot/\iprop!(\p2)$),
\n{nexttot}={\sankeytot+\prop},
\p{end \name}=($(\p1)!\n{nexttot}/\iprop!(\p2)$),
\p{mid \name}=($(\p{start \name})!.5!(\p{end \name})$)
in
\pgfextra{
\xdef\sankeytot{\n{nexttot}}
\pgfinterruptpath
\sankeynode{\prop}{\n1}{\name}{\p{mid \name}}
\endpgfinterruptpath
}
}
\pgfextra{
\pgfmathsetmacro{\diff}{abs(\iprop-\sankeytot)}
\pgfmathtruncatemacro{\finish}{\diff<0.01?1:0}
\ifnumequal{\finish}{1}{}{
\message{*** Warning: bad sankey fork (maybe)...}
\message{\iprop-\sankeytot}
}
};
}

\tikzset{
% default values,
declare function={
sankeyqtytolen(\qty)=\qty/\sankeytotalqty*\sankeytotallen;
sankeylentoqty(\len)=\len/\sankeytotallen*\sankeytotalqty;
},
sankey tot length=100pt,
sankey tot quantity=100,
sankey min radius=30pt,%
sankey arrow length=10pt,%
% user values
#1}
}{
}

\begin{document}
\begin{tikzpicture}[x=1pt,y=1pt]

\begin{sankeydiagram}[
sankey tot length=90pt,%
sankey tot quantity=6,%
sankey min radius=15pt,%
sankey fill/.style={
draw,line width=0pt,
fill,
lime!50,
},
sankey draw/.style={
draw=black,
line width=1pt,
line cap=round,
line join=round,
},
sankey debug,
]
\sankeynodestart{6}{-90}{p0}{0,100};
\sankeyadvance{p0}{50pt}

\sankeyfork{p0}{3/p1,3/p2}

\sankeyturn{p1}{90}
\sankeyadvance{p1}{20pt}

\sankeyadvance{p2}{60pt}

\sankeyfork{p2}{2/p3,1/p4}

\sankeyturn{p3}{90}
\sankeyadvance{p3}{50pt}

\sankeyfork{p3}{1/p5,1/p6}

\sankeyadvance{p5}{70pt}

\sankeyfork{p1}{1/p7,1/p8,1/p9}
\sankeyadvance{p7}{50pt}
\sankeyadvance{p9}{50pt}

\sankeyadvance{p4}{40pt}
\sankeyturn{p4}{90}
\sankeyadvance{p4}{65pt}

\sankeyadvance{p7}{40pt}

\sankeynode{3}{0}{p11}{[shift={(50pt,-15pt)}]p7}
\sankeyfork{p11}{1/p7a,1/p9a,1/p5a}
\path (p7) to[sankey flow] (p7a);
\path (p9) to[sankey flow] (p9a);
\path (p5) to[sankey flow] (p5a);
\sankeyadvance{p11}{30pt}
\sankeynodeend{3}{0}{p11}{p11}

{
\tikzset{
sankey fill/.append style={
line width=0pt,
lime!50!green!50,
}
}
\sankeyturn{p8}{-90}
\sankeyadvance{p8}{40pt}

\sankeyturn{p6}{-90}
\sankeyturn{p4}{-90}

\sankeynode{3}{-90}{p10}{[shift={(-15pt,-60pt)}]p8}
\sankeyfork{p10}{1/p8a,1/p6a,1/p4a}
\path (p4) to[sankey flow] (p4a);
\path (p6) to[sankey flow] (p6a);
\path (p8) to[sankey flow] (p8a);
\sankeyadvance{p10}{30pt}
\sankeynodeend{3}{-90}{p10}{p10}
}

\end{sankeydiagram}
\end{tikzpicture}
\end{document}


Here is another example with the same preamble (it's the same sankey diagram as your first example but with adjusted value: Industrie=86.1 . Without adjustment, there are bad sums...) :

Then the code (without preamble):

\usetikzlibrary{positioning}
\begin{document}
\begin{tikzpicture}

\begin{sankeydiagram}[
sankey tot length=5cm,%
sankey tot quantity=524.3,%
sankey min radius=3mm,%
sankey fill/.style={
draw,line width=0pt,
fill,
cyan!50!blue!50!black,
},
sankey draw/.style={
draw=none,
line width=1pt,
line cap=round,
line join=round,
},
%sankey debug,
]

\sankeynodestart{7.2}{-90}{B}{-.5,0}
\coordinate[below=1mm of B.center] (B label);
\sankeyadvance{B}{5mm}
\sankeynodestart{137.3}{-90}{GI}{1,0}
\coordinate[below=1mm of GI.center] (GI label);
\sankeyadvance{GI}{5mm}
\sankeynodestart{397.8}{-90}{I}{4,0}
\coordinate[below=1mm of I.center] (I label);
\sankeyadvance{I}{5mm}

\sankeynode{542.3}{-90}{EI}{2.86,-1}
\sankeyfork{EI}{397.8/Ia,137.3/GIa,7.2/Ba}
\path (I) to[sankey flow] (Ia);
\path (GI) to[sankey flow] (GIa);
\path (B) to[sankey flow] (Ba);
\sankeyadvance{EI}{5mm}
\coordinate (EI label) at (EI);
\sankeyadvance{EI}{5mm}

\sankeyfork{EI}{63.1/EB,479.2/P}

\sankeyturn{EB}{90}
\sankeyadvance{EB}{2cm}
\coordinate (EB label) at (EB.center);
\sankeyadvance{EB}{2cm}
\sankeynodeend{63.1}{0}{EB}{EB}

\sankeyadvance{P}{10mm}
\coordinate (P label) at (P);
\sankeyadvance{P}{5mm}

\sankeyfork{P}{33.5/NV,445.7/P}

{
\tikzset{sankey fill/.append style={cyan!80!lime!50!gray}}
\sankeyturn{NV}{90}
\sankeyadvance{NV}{2cm}
\coordinate (NV label) at (NV);
\sankeyadvance{NV}{2cm}
\sankeynodeend{33.5}{0}{NV}{NV}
}

\sankeyadvance{P}{10mm}

\sankeyfork{P}{118.1/U,327.6/P}

{
\tikzset{sankey fill/.append style={orange!70!gray!50}}
\sankeyturn{U}{90}
\sankeyadvance{U}{2cm}
\coordinate (U label) at (U);
\sankeyadvance{U}{2cm}
\sankeynodeend{118.1}{0}{U}{U}
}

\sankeyadvance{P}{10mm}

\sankeyfork{P}{327.2/P,0.4/SD}

{
\sankeyturn{SD}{-90}
\sankeyadvance{SD}{15mm}
\coordinate (SD label) at (SD);
\sankeyadvance{SD}{15mm}
\sankeynodeend{0.4}{0}{SD}{SD}
}

\sankeyadvance{P}{8mm}

\sankeyfork{P}{18.8/VE,308.4/E}

{
\tikzset{sankey fill/.append style={orange!70!gray!30}}
\sankeyturn{VE}{90}
\sankeyadvance{VE}{2cm}
\coordinate (VE label) at (VE);
\sankeyadvance{VE}{2cm}
\sankeynodeend{18.8}{0}{VE}{VE}
}

\sankeyadvance{E}{8mm}
\coordinate (E label) at (E);
\sankeyadvance{E}{20mm}

\sankeyfork{E}{135.1/H+GHD,87.2/V,86.1/In}

\sankeyturn{In}{-90}
\sankeyadvance{In}{10mm}
\sankeyturn{In}{90}
\sankeyadvance{In}{5mm}
\coordinate (In label)  at (In);
\sankeyadvance{In}{10mm}
\sankeynodeend{86.7}{-90}{In}{In}

\sankeyadvance{V}{19mm}
\coordinate (V label) at (V);
\sankeyadvance{V}{10mm}
\sankeynodeend{87.2}{-90}{V}{V}

\sankeyturn{H+GHD}{90}
\sankeyadvance{H+GHD}{10mm}
\sankeyfork{H+GHD}{47.0/GHD,88.1/H}

\sankeyturn{H}{-90}
\sankeyadvance{H}{.5mm}
\coordinate (H label) at (H);
\sankeyadvance{H}{10mm}
\sankeynodeend{88.1}{-90}{H}{H}

\sankeyadvance{GHD}{30mm}
\sankeyturn{GHD}{-90}
\sankeyadvance{GHD}{8.5mm}
\coordinate (GHD label) at (GHD);
\sankeyadvance{GHD}{10mm}
\sankeynodeend{47}{-90}{GHD}{GHD}

% labels
\tikzset{
label/.style={
fill=white,inner sep=.5mm,text=cyan!50!blue!50!black,
font=\sffamily\bfseries\small,inner sep=1mm,
align=center,
},
}
\node[label,anchor=north] (B label) at (B label) {7.1};
\node[label,left=1mm of B label] {Bestands-\\entnahme};
\node[label,anchor=north] at (GI label) {137.3};
\node[label,above=5mm of GI label] {Gewinnung\\im Inland};
\node[label,anchor=north] at (I label) {397.8};
\node[label,above=5mm of I label] {Import};

\node[label] at (EI label) {542.3\\Energieaufkommen im Inland};

\node[label,anchor=center] (EB label) at (EB label) {63.1};
\node[label,above=1mm of EB label] {Export und\\Bunkerung};

\node[label] at (P label) {479.1\\Primärenergieverbrauch};

\node[label,anchor=center] (NV label) at (NV label) {33.5};
\node[label,above=0mm of NV label] {Nichtenergetischer Verbrauch};

\node[label,anchor=center] (U label) at (U label) {118.1};
\node[label,below=4mm of U label] {Umwandlungsverluste};

\node[label,anchor=center] (SD label) at (SD label) {0.4};
\node[label,above=0mm of SD label] {Statistische\\Differenzen};

\node[label,anchor=center] (VE label) at (VE label) {18.8};
\node[label,below=0mm of VE label] {Verbrauch in den\\Energiesktoren};

\node[label,anchor=north] (E label) at (E label) {308.4\\Endenergieverbrauch};

\node[label,anchor=north] (In label) at (In label) {86.1};
\node[label,anchor=north,below=1cm of In label] {Industrie};

\node[label,anchor=north] (V label) at (V label) {87.2};
\node[label,anchor=north,below=1cm of V label] {Verkehr};

\node[label,anchor=north] (H label) at (H label) {87.2};
\node[label,anchor=north,below=1cm of H label] {Haushalte};

\node[label,anchor=north] (GHD label) at (GHD label) {47.0};
\node[label,anchor=north,below=1cm of GHD label] {Gewerbe, Handel\\Diensleistungen};
\end{sankeydiagram}
\end{tikzpicture}
\end{document}

• @morbusg Nope. This is Paul Gaborit:-)
– user10274
Jan 1, 2013 at 21:07
• This should be some kind of package.
– You
Jan 2, 2013 at 1:47
• Very nice! I added this example to the TikZ Gallery. Feb 27, 2014 at 10:06
• This code appears to be broken in latest tikz? see tex.stackexchange.com/questions/179132/… May 19, 2014 at 19:35
• @DavidCarlisle Thanks. I edited my answer to correct this bug. May 19, 2014 at 21:13

A great alternative to using tikz directly is Matplotlib and the Sankey module. You can see example codes here: Matplotlib Sankey example

I think it is a very much better way in creating a sankey diagram.

With XeLaTeX and matplotlib 1.2 you can use matplotlib-backend-pgf

A short code example using the sankey example diagram and creating a pdf diagram:

import matplotlib as mpl
mpl.use("module://backend_pgf")
import matplotlib.pyplot as plt
from matplotlib.sankey import Sankey

Sankey(flows=[0.25, 0.15, 0.60, -0.20, -0.15, -0.05, -0.50, -0.10],
labels=['', '', '', 'First', 'Second', 'Third', 'Fourth', 'Fifth'],
orientations=[-1, 1, 0, 1, 1, 1, 0, -1]).finish()

# remove the frame in the sankey diagram:
a = plt.gca()
a.set_frame_on(False)

# Save it to pdf:
plt.savefig("sankey.pdf", bbox_inches='tight')


In your tex file you can then include the pdf image:

% tex file:
\includegraphics[width=0.7\textwidth]{img/sankey.pdf}

• He also mentioned the detail given in your answer no? Dec 20, 2012 at 13:22
• A short example of how this works, with both Python and LaTeX code, would be a nice addition. (<nitpickmode>Also, matplotlib2tikz isn't really part of matplotlib IIUC, it is a separate project.</nitpickmode>) Edit: Yes, I know there are examples on the Matplotlib website, but it would make the answer more self-contained. Dec 20, 2012 at 14:11
• I think I had to make a new answer because your comment is the last comment and I overlooked it before. I found the matplotlib example myself. This answer is a better place to find for somebody. Feel free to add your content to my answer or add another (more useful) answer. BTW I upvoted your comment to appear in the non-hidden comments. Dec 20, 2012 at 14:33
• Note: I'm not notified of your comment unless you write @TorbjørnT. I would have added an answer had someone asked me to, I didn't at first because I was unsure whether it was of interest to the OP. But never mind. I don't think two answers about this is necessary, but I may expand your answer later, if you haven't already. Dec 20, 2012 at 14:40
• @TorbjørnT. I added a example based on matplotlib-backend-pgf which I am using right now. It is included in matplotlib 1.2 Dec 29, 2012 at 12:11

I think that the usage of double distance is the key of this type of drawings. Here it is a little example

\documentclass{standalone}

\usepackage{tikz}

\begin{document}
\begin{tikzpicture}
\draw[help lines] (-2,-4)grid(9,5);
\def\lI{4cm}
\def\rIfrazI{.4}
\pgfmathsetmacro\rIfrazII{1-\rIfrazI}

\def\rl{2}
\draw[line width      = 2pt,
double distance = \lI-\pgflinewidth] (0,0)--(\rl,0);

%%%%%%%%%%%%
% BRANCH I %
%%%%%%%%%%%%
\def\rIl{5}
\draw[line width      = 2pt,
double distance = \rIfrazI*\lI-\pgflinewidth] (\rl,{(1/2-\rIfrazI/2)*\lI})--++(\rIl,0);

% ... you continue

%%%%%%%%%%%%%
% BRANCH II %
%%%%%%%%%%%%%
\def\rIIl{2.7}
\draw[line width      = 2pt,
double distance = \rIfrazII*\lI-\pgflinewidth]
(\rl,{(-1/2+\rIfrazII/2)*\lI})--++
(\rIIl,0)arc[radius      = {(\rIfrazII*\lI-\pgflinewidth)/2+1cm},
start angle = 90,
end angle   = 0]
coordinate(r);

% ... you continue
\end{tikzpicture}
\end{document}


Obviously, to develop and improve.

• this indeed looks helpful Jan 7, 2012 at 15:59

Just to let you know: the atan2 function was changed with pgf/tikz 3.0. You need to switch x and y argument for every atan2 call. Saying this, the working code is:

\pgfdeclarelayer{background}
\pgfdeclarelayer{foreground}
\pgfdeclarelayer{sankeydebug}
\pgfsetlayers{background,main,foreground,sankeydebug}

\newif\ifsankeydebug

\newenvironment{sankeydiagram}[1][]{

\def\sankeyflow##1##2{% sn, en
\path[sankey fill]
let
\p1=(##1.north east),\p2=(##1.south east),
\n1={atan2(\y1-\y2,\x1-\x2)-90},
\p3=(##2.north west),\p4=(##2.south west),
\n2={atan2(\y3-\y4,\x3-\x4)+90}
in
(\p1) to[out=\n1,in=\n2] (\p3) --
(\p4) to[in=\n1,out=\n2] (\p2) -- cycle;
\draw[sankey draw]
let
\p1=(##1.north east),\p2=(##1.south east),
\n1={atan2(\y1-\y2,\x1-\x2)-90},
\p3=(##2.north west),\p4=(##2.south west),
\n2={atan2(\y3-\y4,\x3-\x4)+90}
in
(\p1) to[out=\n1,in=\n2] (\p3)
(\p4) to[in=\n1,out=\n2] (\p2);
}

\tikzset{
sankey tot length/.store in=\sankeytotallen,
sankey tot quantity/.store in=\sankeytotalqty,
sankey min radius/.store in=\sankeyminradius,
sankey arrow length/.store in=\sankeyarrowlen,
sankey debug/.is if=sankeydebug,
sankey debug=false,
sankey flow/.style={
to path={
\pgfextra{
\pgfinterruptpath
\edef\sankeystart{\tikztostart}
\edef\sankeytarget{\tikztotarget}
\sankeyflow{\sankeystart}{\sankeytarget}
\endpgfinterruptpath
}
},
},
sankey node/.style={
inner sep=0,minimum height={sankeyqtytolen(##1)},
minimum width=0,draw=none,line width=0pt,
},
% sankey angle
sankey angle/.store in=\sankeyangle,
% sankey default styles
sankey fill/.style={line width=0pt,fill,white},
sankey draw/.style={draw=black,line width=.4pt},
}

\newcommand\sankeynode[4]{%prop,orientation,name,pos
\node[sankey node=##1,rotate=##2] (##3) at (##4) {};
\ifsankeydebug
\begin{pgfonlayer}{sankeydebug}
\draw[red,|-|] (##3.north west) -- (##3.south west);
\pgfmathsetmacro{\len}{sankeyqtytolen(##1)/3}
\draw[red] (##3.west)
-- ($(##3.west)!\len pt!90:(##3.south west)$)
node[font=\tiny,text=black] {##3};
\end{pgfonlayer}
\fi
}

\newcommand\sankeynodestart[4]{%prop,orientation,name,pos
\sankeynode{##1}{##2}{##3}{##4}
\begin{scope}[shift={(##3)},rotate=##2]
\path[sankey fill]
(##3.north west) -- ++(-\sankeyarrowlen,0)
-- ([xshift=-\sankeyarrowlen/6]##3.west)
-- ([xshift=-\sankeyarrowlen]##3.south west)
-- (##3.south west) -- cycle;
\path[sankey draw]
(##3.north west) -- ++(-\sankeyarrowlen,0)
-- ([xshift=-\sankeyarrowlen/6]##3.west)
-- ([xshift=-\sankeyarrowlen]##3.south west)
-- (##3.south west);
\end{scope}
}

\newcommand\sankeynodeend[4]{%prop,orientation,name,pos
\sankeynode{##1}{##2}{##3}{##4}
\begin{scope}[shift={(##3)},rotate=##2]
\path[sankey fill]
(##3.north east)
-- ([xshift=\sankeyarrowlen]##3.east)
-- (##3.south west) -- cycle;
\path[sankey draw]
(##3.north east)
-- ([xshift=\sankeyarrowlen]##3.east)
-- (##3.south west);
\end{scope}
}

\newcommand\sankeyadvance[3][]{%newname,name,distance
\edef\name{##2}
\ifstrempty{##1}{
\def\newname{##2}
\edef\name{##2-old}
\path [late options={name=##2,alias=\name}];
}{
\def\newname{##1}
}
\path
let
% sankey node angle
\p1=(##2.north east),
\p2=(##2.south east),
\n1={atan2(\y1-\y2,\x1-\x2)-90},
% sankey prop
\p3=($(\p1)-(\p2)$),
\n2={sankeylentoqty(veclen(\x3,\y3))},
% next position
\p4=($(##2.east)!##3!-90:(##2.north east)$)
in
\pgfextra{
\pgfmathsetmacro{\prop}{\n2}
\pgfinterruptpath
\sankeynode{\prop}{\n1}{\newname}{\p4}
\path (\name) to[sankey flow] (\newname);
\endpgfinterruptpath
};
}

\newcommand\sankeyturn[3][]{%newname,name,angle
\edef\name{##2}
\ifstrempty{##1}{
\def\newname{##2}
\edef\name{##2-old}
\path [late options={name=##2,alias=\name}];
}{
\def\newname{##1}
}
\ifnumgreater{##3}{0}{
\typeout{turn acw: ##3}
\path
let
% sankey node angle
\p1=(##2.north east),
\p2=(##2.south east),
\p3=($(\p1)!-\sankeyminradius!(\p2)$),
\n1={atan2(\y1-\y2,\x1-\x2)-90},
% sankey prop
\p4=($(\p1)-(\p2)$),
\n2={sankeylentoqty(veclen(\x4,\y4))},
\p5=(##2.east),
\p6=($(\p3)!1!##3:(\p5)$)
in
\pgfextra{
\pgfmathsetmacro{\prop}{\n2}
\pgfinterruptpath
% \fill[red] (\p3) circle (2pt);
% \fill[blue](\p6) circle (2pt);
\sankeynode{\prop}{\n1+##3}{\newname}{\p6}
\path (\name) to[sankey flow] (\newname);
\endpgfinterruptpath
};
}{
\typeout{turn acw: ##3}
\path
let
% sankey node angle
\p1=(##2.south east),
\p2=(##2.north east),
\p3=($(\p1)!-\sankeyminradius!(\p2)$),
\n1={atan2(\y1-\y2,\x1-\x2)+90},
% sankey prop
\p4=($(\p1)-(\p2)$),
\n2={sankeylentoqty(veclen(\x4,\y4))},
\p5=(##2.east),
\p6=($(\p3)!1!##3:(\p5)$)
in
\pgfextra{
\pgfmathsetmacro{\prop}{\n2}
\pgfinterruptpath
% \fill[red] (\p3) circle (2pt);
% \fill[blue](\p6) circle (2pt);
\sankeynode{\prop}{\n1+##3}{\newname}{\p6}
\path (\name) to[sankey flow] (\newname);
\endpgfinterruptpath
};
}
}

\newcommand\sankeyfork[2]{%name,list of forks
\def\name{##1}
\def\listofforks{##2}
\xdef\sankeytot{0}
\path
let
% sankey node angle
\p1=(\name.north east),
\p2=(\name.south east),
\n1={atan2(\y1-\y2,\x1-\x2)-90},
% sankey prop
\p4=($(\p1)-(\p2)$),
\n2={sankeylentoqty(veclen(\x4,\y4))}
in
\pgfextra{
\pgfmathsetmacro{\iprop}{\n2}
}
\foreach \prop/\name[count=\c] in \listofforks {
let
\p{start \name}=($(\p1)!\sankeytot/\iprop!(\p2)$),
\n{nexttot}={\sankeytot+\prop},
\p{end \name}=($(\p1)!\n{nexttot}/\iprop!(\p2)$),
\p{mid \name}=($(\p{start \name})!.5!(\p{end \name})$)
in
\pgfextra{
\xdef\sankeytot{\n{nexttot}}
\pgfinterruptpath
\sankeynode{\prop}{\n1}{\name}{\p{mid \name}}
\endpgfinterruptpath
}
}
\pgfextra{
\pgfmathsetmacro{\diff}{abs(\iprop-\sankeytot)}
\pgfmathtruncatemacro{\finish}{\diff<0.01?1:0}
\ifnumequal{\finish}{1}{}{
\message{*** Warning: bad sankey fork (maybe)...}
\message{\iprop-\sankeytot}
}
};
}

\tikzset{
% default values,
declare function={
sankeyqtytolen(\qty)=\qty/\sankeytotalqty*\sankeytotallen;
sankeylentoqty(\len)=\len/\sankeytotallen*\sankeytotalqty;
},
sankey tot length=100pt,
sankey tot quantity=100,
sankey min radius=30pt,%
sankey arrow length=10pt,%
% user values
#1}
}{
}

• Welcome to TeX.SX! This should probably be a comment on the answers using this function rather than a separate answer. Apr 25, 2014 at 16:01
• Actually I wanted to do this, but since my reputation is not high enough I can't. I don't care for the credit - but it took me so long to go though Paul Gaborit's code to get it working that I wanted to share this information. Fell free to add a comment to his answer in case my answer get's deleted again.
– xamf
Apr 25, 2014 at 16:42
• @xamf Thanks. I edited my answer to correct this bug. May 19, 2014 at 21:14