[Tex/LaTex] Energy level transition pgfplots/Tikz

tikz-pgf

I have been putting together a LaTeX code for a friend to automate plotting of energy transition diagrams. The code is as follows:

\documentclass{standalone}
\usepackage{pgfplots,pgfplotstable}
\usepgflibrary{plotmarks}
\usetikzlibrary{calc}

%%% Example data file

\pgfplotstableread{
one two three
0.0   0.0     0.0
-44.2   -42.8   -58.6
150.4   155.8   217.6
84.4    83.9    167.4
146.2   150.7   231.5
250.8   234.5   276.7
nan   61.4  62.0
nan   119.0 162.7
86.1    49.0    105.5
213.9   212.4   325.4
}\datatable

\begin{document}
\begin{tikzpicture}
\begin{axis}[
%only marks,
every axis plot post/.style={mark=-,thick,mark size=7pt},
ylabel=Energy (kJmol$^{-1}$), 
xtick=\empty,
legend pos=outer north east,
xmin=-1,
%xmax=10,
ymin=-100,
%ymax=350,
axis lines=left,  
xtick=\empty,
hide x axis,
legend entries={\small one, \small two,\small three},
legend style={draw=none},
title=Insert better title here,]
\pgfplotstablegetcolsof{\datatable}
\pgfmathsetmacro\numberofycols{\pgfplotsretval-1}
\pgfplotsinvokeforeach {0,...,\numberofycols}{  
\addplot table[x expr=\coordindex, y index=#1] {\datatable};

}

\end{axis}
\end{tikzpicture}
\end{document}

This produces a plot which is half decent:

enter image description here

The part I am having trouble with is to automate the joining of the levels together. In the picture, the levels are joined by a solid line which links the centre of the lines together. What is required is the end of one level joins the start of the next level and so on over all the levels. An example of such is:

enter image description here

I have used the 'only marks' option which removes the lines altogether and tried to insert a separate plot which deals with just the lines; but that has not worked. Is there an easy way to accomplish this which I am missing?

Many thanks in advance.

Best Answer

This code produces the wanted output (I think), but also produces errors (which should be skipped) due to some values being nan. Explanations below the image. Update: error free code at the end.

\documentclass{standalone}
\usepackage{pgfplots,pgfplotstable}
\usepgflibrary{plotmarks}
\usetikzlibrary{calc}

%%% Example data file

\pgfplotstableread{
one two three
0.0   0.0     0.0
-44.2   -42.8   -58.6
150.4   155.8   217.6
84.4    83.9    167.4
146.2   150.7   231.5
250.8   234.5   276.7
nan   61.4  62.0
nan   119.0 162.7
86.1    49.0    105.5
213.9   212.4   325.4
}\datatable

\begin{document}
\begin{tikzpicture}[x=1cm, y=0.2mm]
\begin{axis}[
%only marks,
every axis plot post/.style={mark=-,thick,mark size=2mm},
ylabel=Energy (kJmol$^{-1}$), 
xtick=\empty, 
legend pos=outer north east,
xmin=-1,
%xmax=10,
ymin=-100,
%ymax=350,
axis lines=left,  
xtick=\empty,
hide x axis,
legend entries={\small one, \small two,\small three},
legend style={draw=none},
title=Insert better title here,
% Extra options added
anchor=origin,
disabledatascaling,
only marks,
x=1cm, y=0.2mm,]
\pgfplotstablegetcolsof{\datatable}
\pgfmathsetmacro\numberofycols{\pgfplotsretval-1}
\pgfplotsinvokeforeach {0,...,\numberofycols}{  
\addplot table[x expr=\coordindex, y index=#1] {\datatable};
}
\end{axis}

% Extra code added
\foreach \case in {one,two,three} {
  \xdef\previndex{0}
  \xdef\prevlevel{0}
  \pgfplotstableforeachcolumnelement{\case}\of\datatable\as\level{%
    \draw[densely dotted] ($(\previndex,\prevlevel)+(0.2,0)$) -- 
                          ($(\pgfplotstablerow,\level)+(-0.2,0)$);
    \xdef\previndex{\pgfplotstablerow}
    \xdef\prevlevel{\level}
  }
}
\end{tikzpicture}
\end{document}

Result

Explanation

The main idea behind the above code is to use pgfplots to draw the marks (horizontal lines), and then use "pure tikz" to connect them with dotted lines. In order to be able to use "pure tikz" on top of a pgfplots graph, some additional settings are required, as explained in pgfplots manual, section 4.27 "Tikz Interoperability".

In our case, these required settings are:

  • Use the same values for x and y units in the tikzpicture and in the axis. In this case I used x=1cm, y=0.2mm. These values affect the final size of the figure (and also to the numbers appearing in the Y axis, since greater values of y unit provide more room to show more numbers).
  • Options anchor=origin, disabledatascaling added to the axis environment.

Once these settins are used, you can use standard TikZ commands, such as \draw, etc. using the same coordinate system than your data.

To draw the connecting lines I used two nested loops.

  • The outer loop is a standard TikZ \foreach loop, which iterates over the string values one, two and three to select the appropiate column in the data table.
  • For each of the above, the inner loop draws the lines which connect all marks in that column. This inner loop is a pgftables macro (\pgfplotstableforeachcolumnelement) which iterates over the values in a column. For each value, I draw a line from the previous one to the current one. The x coordinate is simply the index of the row, and the y coordinate is the value read from the table for that row.

Note that some tikz calc is used to take into account the size of the marks (which I fixed to 2mm in every axis plot post/.style={mark=-,thick,mark size=2mm})

Now, there is a problem with the data in column "one". Two of the rows contain the text nan instead of a number. When those values are used as a y coordinate, TikZ complains. If we ignore the errors, the result is as if the y coordinate to draw is equal to the last one drawn (hence the horizontal dotted lines with gaps).

I'm not sure what should be the expected result for those nan values. Should them be skipped? I mean, should the bar for row 5 be connected with the bar for row 8, skipping rows 6 and 7?

Update: skipping NaN

To skip the NaN values in the "pure tikz" part, the solution is simple: we have to detect which values are "nan" and do nothing for them (not even updating the "previous" auxiliar variables). This will produce the desired result.

Detecting NaNs is a bit convoluted, but easy, as shown in Using nan values in pgfplots. We have to use \pgfmathfloatparsenumber to let pgf to parse the data and detect if it is nan, inf, etc. and then use \pgfmathfloatgetflagstomacro to extract the "flags" of the result. This flag is an integer which is 3 for the nan case.

So, the new "pure tikz" part should be:

\foreach \case in {one,two,three} {
    \xdef\previndex{0}
    \xdef\prevlevel{0}
    \pgfplotstableforeachcolumnelement{\case}\of\datatable\as\level{%
        \pgfmathfloatparsenumber{\level}
        \pgfmathfloatgetflagstomacro\pgfmathresult\flags
        \ifnum\flags=3\relax\else
            \draw[densely dotted] ($(\previndex,\prevlevel)+(0.2,0)$) -- ($(\pgfplotstablerow,\level)+(-0.2,0)$);
            \xdef\previndex{\pgfplotstablerow}
            \xdef\prevlevel{\level}
        \fi
    }
}

And the result is now:

New result

Related Question