[Tex/LaTex] Horizontal waterfall chart with labels for each bar

bar chartpgfplotstikz-pgf

I've looked through similar questions here and here, but I'm still stuck. Basically what I'm trying to achieve is this plus bar labels:

enter image description here

I have two problems:

1) My code sucks. Right now the y-axis labels and the waterfall connectors are placed manually, which is suboptimal since I'll be making 15 or so versions of the chart.

2) I can't get the bar labels to show up in the right place. I've used Jake's Centered Nodes Near Coords solution before (can't find the reference stack exchange question at the moment), but for some reason they refuse to go in the proper place in this example.

Any suggestions on how to improve this / make the labels work would be greatly appreciated.

[EDIT] I really need an answer using PGFplots, please. This needs a quick turnaround and I don't have time to learn a different .cvs to latex implementation.

MWE:

\documentclass{article}
\usepackage{pgfplots, pgfplotstable}
\usepackage{filecontents}

\pgfdeclareplotmark{waterfall bridge}{\pgfpathmoveto{\pgfpoint{0pt}{33pt}}\pgfpathlineto{\pgfpoint{0pt}{9pt}}\pgfusepathqstroke}
\pgfdeclareplotmark{waterfall bridge 2}{\pgfpathmoveto{\pgfpoint{0pt}{19pt}}\pgfpathlineto{\pgfpoint{0pt}{-5pt}}\pgfusepathqstroke}
\pgfdeclareplotmark{waterfall bridge 3}{\pgfpathmoveto{\pgfpoint{0pt}{5pt}}\pgfpathlineto{\pgfpoint{0pt}{-19pt}}\pgfusepathqstroke}
\pgfdeclareplotmark{waterfall bridge 4}{\pgfpathmoveto{\pgfpoint{0pt}{-9pt}}\pgfpathlineto{\pgfpoint{0pt}{-33pt}}\pgfusepathqstroke}
%~~ALL OF THE MARK POSITIONING IS UGLY~~
% I would like them to use relative, rather than absolute, positioning

\begin{filecontents}{datatable.csv}
2 3 2 -6 -3
\end{filecontents}

\makeatletter
\pgfplotsset{
    calculate xoffset/.code={
        \pgfkeys{/pgf/fpu=true,/pgf/fpu/output format=fixed}
        \pgfmathsetmacro\labelshift{(\pgfplotspointmeta*10^\pgfplots@data@scale@trafo@EXPONENT@x)/2)*\pgfplots@x@veclength)}
        \pgfkeys{/pgf/fpu=false}
        },
    nodes near coords horizontally centered/.style={
        every node near coord/.append style={
            /pgfplots/calculate xoffset,
            xshift=-\labelshift,yshift=-.25pt
            },
        nodes near coords align=center
    }
}
\makeatother

\begin{document}
\begin{tikzpicture}
\begin{axis}[
        xbar stacked,
        point meta=explicit,
        nodes near coords horizontally centered, %~~THIS ISN'T WORKING~~
        bar width=10pt,
        axis x line*=bottom,
        axis on top=true,
        ytick style={opacity=0},
       yticklabel style={font=\small, text width=2cm, align=center},
       x axis line style={opacity=0},
       y axis line style={opacity=0},
        y dir=reverse,
        ytick={-.35,-.175,0,.175,.35}, %~~THIS IS UGLY!~~
        yticklabels={
                label1,label2,label3,label4,label5
            },
    ]
      \addplot[
        fill=cyan,
        draw=none,
        bar shift=28pt,
        mark options={
            lightgray,
            thick,
        },
        mark=waterfall bridge,
      ] table [y expr=\coordindex, x index=0, meta index=0] {datatable.csv};
      \addplot[
        fill=orange,
        draw=none,
        bar shift=14pt,
        mark options={
            lightgray,
            thick,
        },
        mark=waterfall bridge 2,
      ] table [y expr=\coordindex, x index=1, meta index=1] {datatable.csv};
      \addplot[
        fill=blue,
        draw=none,
        bar shift=0pt,
        mark options={
            lightgray,
            thick,
        },
        mark=waterfall bridge 3,
      ] table [y expr=\coordindex, x index=2, meta index=2] {datatable.csv};
      \addplot[
        fill=red,
        draw=none,
        bar shift=-14pt,
        mark options={
            lightgray,
            thick,
        },
        mark=waterfall bridge 4,
      ] table [y expr=\coordindex, x index=3, meta index=3] {datatable.csv};
      \addplot[
        fill=yellow,
        draw=none,
        bar shift=-28pt,
      ] table [y expr=\coordindex, x index=4, meta index=4] {datatable.csv};
    \draw[ultra thin,lightgray] (axis cs:0,\pgfkeysvalueof{/pgfplots/ymin}) -- (axis cs:0,\pgfkeysvalueof{/pgfplots/ymax}); % adds x=0 line
\end{axis}
\end{tikzpicture}


\end{document}

Best Answer

Well, just for fun (and to better learn new tools) I did an implementation in lua, which can be run through LuaLatex.

The lua code reads from an external .cvs, in which data is expected to be in different lines (a number per line), and generates a tikz graphic which self-adapts its axis to the data read. Also, the generated tikz defines a set of coordinates named row1, row2 and so on located a the center of each bar, and a coordinate named min located at the x position of the leftmost bar. These coordinates can be used to put labels in the diagram, either centered on the bars or at the left of the graphic.

The colors used in the graphic are user-definable. If there are less colors than bars, they are cycled.

This is the LaTeX code:

\documentclass{book}
\usepackage{xcolor}
\usepackage{tikz}
\usepackage{filecontents}
\directlua{dofile("luaFunctions.lua")}

%create a pair of datafiles
\begin{filecontents*}{datafile1.csv}
  2
  3
  2
  -6
  -3
\end{filecontents*}
\begin{filecontents*}{datafile2.csv}
  3 
  4 
  2 
  3 
  -4   
  -6
  2 
  -5
\end{filecontents*}

% latex commands to execute the lua functions
\def\waterfallChart#1{\directlua{waterfallChart("#1")}}
\def\setColors#1{%
   \directlua{emptyColors()}%
   \foreach \c in {#1} {\directlua{addColor("\c")}}
}

% set some styles
\tikzset{bar connection/.style = {black!50, thick}}
\setColors{cyan!80!black!50, orange, blue!80!black, red, yellow}

\begin{document}
\begin{tikzpicture} % First graph
  \waterfallChart{datafile1.csv}  % This draws the chart
  % Now, adding labels, centered at each bar
  \foreach \label [count=\n from 1] in {foo, bar, bad, foobar, spam}
    \node at (row\n) {\label};
\end{tikzpicture}

\vskip 2cm

\begin{tikzpicture} % Second graph
  \setColors{brown,red,orange}   % Different colors for this one
  \waterfallChart{datafile2.csv} % Draw the chart
  % Put labels (at the left of the figure in this case)
  \foreach \label [count=\n from 1] in {foo, bar, bad, foobar, spam, eggs, lorem, ipsum}
    \node[left] at (row\n-|min) {\label};
\end{tikzpicture}
\end{document}

This is the result:

Result

And this is the content of the file luaFunctions.lua:

colors = {"blue","green"}  -- Default colors

function readDataFile(filename)
    local input = io.open(filename, 'r')
    local dataTable = {}
    local n
    for line in input:lines() do
       table.insert(dataTable, line)
    end
    input:close()
    return dataTable
end

function emptyColors()
    colors = {}
end

function addColor(c)
    table.insert(colors,c)
end

function computeExtremes(dataTable)
    local max, min, x
    x = 0.0
    min = 0.0
    max = 0.0
    for i,p in ipairs(dataTable) do
        x = x + p
        if (x<min) then min = x end
        if (x>max) then max = x end
    end
    return min, max
end

function waterfallChart(filename)
    local data = readDataFile(filename)
    local min, max, n_steps, step, color, xpos, ypos, barwidth, aux, spread

    -- Configure here as required
    barwidth = 0.5  -- Height of each bar in the chart
    spread = 1.4    -- Distance among baselines of the bars (in barwidth units)
    n_ticks = 6     -- Number of ticks in the x-axis

    min, max = computeExtremes(data)
    step = (max-min)/n_ticks
    max = min + n_ticks*step
    xpos = 0.0
    ypos = 0.0
    aux = 0
    -- Draw axes
    -- Vertical axis
    tex.print(string.format("\\draw (0,%f) -- (0, %f);",
               1.1*barwidth, -#data*spread*barwidth))
    -- Horizontal axis
    tex.print(string.format("\\foreach \\tick in {%d, %d, ..., %d}",
              min, min+step, max))
    tex.print(string.format("  \\draw (\\tick, %f) -- +(0, -2mm) node[below] {\\tick};",
              -#data*spread*barwidth))
    tex.print(string.format("\\coordinate (min) at (%f,%f);",
                  min+0.0, -#data*spread*barwidth))

    -- Draw the bars
    color = 1
    for i,p in ipairs(data) do
        tex.print(string.format("\\fill[%s] (%f,%f) rectangle +(%f, %f) coordinate[midway] (row%d);",
                  colors[color], xpos, ypos, p+0.0, barwidth, i))
        tex.print(string.format("\\draw[bar connection] (%f, %f) -- +(0,%f);",
                  xpos, ypos, spread*aux*barwidth))
        aux =   1
        ypos = ypos - spread*barwidth
        xpos = xpos + 1.0 * p
        color = color + 1
        if (color > #colors) then color = 1; end
    end
end