4

I could use some help in creating a vertical timeline with specific requirements. The timeline should display dates on the left side of a vertical line, with the corresponding labels on the right. The dates should be formatted as "year, month day" (e.g. "1938, January 13") in English. Ideally, the year would only be printed the first time a new year is used. The entries should be distributed proportionally along the length of the timeline based on their exact date.

I have provided a code that attempts to achieve this, but I'm having trouble getting the desired output. It does succeed in points 1 - 3, but I'm struggling with 4 and 5 ― although №5 is far more important to me than 4. I would appreciate any help or suggestions to modify the code and achieve the desired vertical timeline.

\documentclass[10pt]{article}
\usepackage[paperwidth=210mm,paperheight=297mm,tmargin=7.5mm,rmargin=7.5mm,bmargin=7.5mm,lmargin=7.5mm,vscale=1,hscale=1]{geometry}
\usepackage{tikz}
\usetikzlibrary{arrows, calc, decorations.markings, positioning}
\pagestyle{empty}
\makeatletter
\newenvironment{timeline}[6]{%
    \newcommand{\startyear}{#1}
    \newcommand{\tlendyear}{#2}
    \newcommand{\yearcolumnwidth}{#3}
    \newcommand{\rulecolumnwidth}{#4}
    \newcommand{\entrycolumnwidth}{#5}
    \newcommand{\timelineheight}{#6}
    \newcommand{\templength}{}
    \newcommand{\entrycounter}{0}
    \long\def\ifnodedefined##1##2##3{%
        \@ifundefined{pgf@sh@ns@##1}{##3}{##2}%
    }
    \newcommand{\ifnodeundefined}[2]{%
        \ifnodedefined{##1}{}{##2}
    }
    \newcommand{\drawtimeline}{%
        \draw[timelinerule] (\yearcolumnwidth+5pt, 0pt) -- (\yearcolumnwidth+5pt, -\timelineheight);
        \draw (\yearcolumnwidth+0pt, -10pt) -- (\yearcolumnwidth+10pt, -10pt);
        \draw (\yearcolumnwidth+0pt, -\timelineheight+15pt) -- (\yearcolumnwidth+10pt, -\timelineheight+15pt);
        \pgfmathsetlengthmacro{\templength}{neg(add(multiply(subtract(\startyear, \startyear), divide(subtract(\timelineheight, 25), subtract(\tlendyear, \startyear))), 10))}
        \node[year] (year-\startyear) at (\yearcolumnwidth, \templength) {\startyear};
        \pgfmathsetlengthmacro{\templength}{neg(add(multiply(subtract(\tlendyear, \startyear), divide(subtract(\timelineheight, 25), subtract(\tlendyear, \startyear))), 10))}
        \node[year] (year-\tlendyear) at (\yearcolumnwidth, \templength) {\tlendyear};
    }
    \newcommand{\entry}[4]{%
        % #1 is the year
        % #2 is the month
        % #3 is the day
        % #4 is the entry text

        \pgfmathtruncatemacro{\lastentrycount}{\entrycounter}
        \pgfmathtruncatemacro{\entrycounter}{\entrycounter + 1}

        \ifdim \lastentrycount pt > 0 pt%
            \node[entry] (entry-\entrycounter) [below of=entry-\lastentrycount] {##4};
        \else%
            \pgfmathsetlengthmacro{\templength}{neg(add(multiply(subtract(\startyear, \startyear), divide(subtract(\timelineheight, 25), subtract(\tlendyear, \startyear))), 10))}
            \node[entry] (entry-\entrycounter) at (\yearcolumnwidth+\rulecolumnwidth+10pt, \templength) {##4};
        \fi

        \ifnodeundefined{year-##1-##2-##3}{%
            \pgfmathsetlengthmacro{\templength}{neg(add(multiply(subtract(##1, \startyear), divide(subtract(\timelineheight, 25), subtract(\tlendyear, \startyear))), 10))}
            \draw (\yearcolumnwidth+2.5pt, \templength) -- (\yearcolumnwidth+7.5pt, \templength);
            \node[year] (year-##1-##2-##3) at (\yearcolumnwidth, \templength) {##1, ##3 \ifcase##2\or January\or February\or March\or April\or May\or June\or July\or August\or September\or October\or November\or December\fi};
        }

        \draw ($(year-##1-##2-##3.east)+(2.5pt, 0pt)$) -- ($(year-##1-##2-##3.east)+(7.5pt, 0pt)$) -- ($(entry-\entrycounter.west)-(5pt,0)$) -- (entry-\entrycounter.west);
    }
    \begin{tikzpicture}
        \tikzstyle{entry} = [align=left,text width=\entrycolumnwidth,node distance=10mm,anchor=west]
        \tikzstyle{year} = [anchor=east]
        \tikzstyle{timelinerule} = [draw,decoration={markings, mark=at position 1 with {\arrow[scale=1.5]{latex'}}},postaction={decorate},shorten >=0.4pt]
        \drawtimeline
}
{
    \end{tikzpicture}
    \let\startyear\@undefined
    \let\tlendyear\@undefined
    \let\yearcolumnwidth\@undefined
    \let\rulecolumnwidth\@undefined
    \let\entrycolumnwidth\@undefined
    \let\timelineheight\@undefined
    \let\entrycounter\@undefined
    \let\ifnodedefined\@undefined
    \let\ifnodeundefined\@undefined
    \let\drawtimeline\@undefined
    \let\entry\@undefined
}
\makeatother
\begin{document}
\begin{timeline}{1935}{1950}{2cm}{1.5cm}{13cm}{26cm}
    \entry{1938}{9}{17}{Sudeten German Freikorps and other German paramilitary groups attacked Czechoslovak troops and institutions in the border areas}
    \entry{1938}{9}{30}{Signing of the Betrayal of Munich by Hitler, Chamberlain, Daladier, and Mussolini: annexation of Sudetenland by Germany}
    \entry{1939}{3}{15}{German army occupies Prague and establishes the Protectorate of Bohemia and Moravia, a puppet state with Konstantin Freiherr von Neurath at the helm}
    \entry{1939}{7}{6}{Zentralstelle für jüdische Auswanderung in Böhmen und Mähren [Center for Jewish Emigration in Bohemia and Moravia] is established following the one in Vienna}
    \entry{1939}{12}{1}{Jewish children are excluded from state schools}
    \entry{1940}{6}{14}{Auschwitz extermination camp is established}
    \entry{1941}{9}{27}{Reinhard Heydrich becomes the new Reichsprotektor and orders mass deportation of Jewish people and the reorganization of Terezín into a ghetto}
    \entry{1941}{10}{16}{First transport from Prague to the Lodz ghetto}
    \entry{1941}{11}{24}{First transport of Jewish people arrives in Terezín}
\end{timeline}
\end{document}

MWE result

9
  • subtract(\startyear, \startyear) is probably not correct. Commented Jun 12, 2023 at 12:47
  • Changing that to subtract(\tlendyear, \startyear) so it fits in better with the rest of the code, for instance, makes it so that the start year of the timeline (1935) is printed right over the end year (1950, in this case).
    – BlueIris
    Commented Jun 12, 2023 at 13:12
  • Because of Q688437 I've realized that I've done something similar but more manual for Q136244 Commented Jun 12, 2023 at 16:45
  • I find this general idea hard to read and a little convoluted, so in case you're interested in alternative approaches: I would list all events and their dates in one table where I assing numbers 1-9 to them, then make a horizontal time bar where only the numbers appear.
    – MaxD
    Commented Jun 12, 2023 at 20:12
  • 1
    @BlueIris For me the main advantage is that my eyes dont have to jump zig zag across the page. I have a timeline to give me an overview of the timespans between events at first glance, and I have the table where every detail is listed line by line without distractions. Also, a horizontal timeline feels much more natural to me.
    – MaxD
    Commented Jun 13, 2023 at 4:43

2 Answers 2

10

A different approach, hopefully helpful.

The y axis is oriented downwards and its scale is setup so that each year is 1 (→ year length).

The pgfcalendar package/module is used to evaluate the fraction of a year for every date.

The macro \tikz@timeline@<year> is used to cache some calculations for that year but also to check whether that year has been used in this diagram before which is used inside \tlDate to leave out the year number or not.

I've added a second example where there dates and the entries' text is on the same side. I think this is better to read. It also shows how flexible the environment can be since everything is accessible by a name, chain, number or style.

Code

%\documentclass[10pt]{article}
%\usepackage[paperwidth=210mm,paperheight=297mm,tmargin=7.5mm,rmargin=7.5mm,bmargin=7.5mm,lmargin=7.5mm,vscale=1,hscale=1]{geometry}
\documentclass[tikz]{standalone}
\usepackage{tikz, pgfcalendar}
\newcommand*\tikztimelineset{\pgfqkeys{/tikz/timeline}}
\usetikzlibrary{arrows.meta, chains}
\tikzset{west below/.style={below=#1.south west, anchor=north west},
         east below/.style={below=#1.south east, anchor=north east}}
\tikztimelineset{
  start year/.initial=1900,
  end year/.initial=2000,
  year length/.initial=1.5cm,
  entry text distance/.initial=2cm,
  entry date distance/.initial=1.5cm,
  every tick length/.initial=5pt,
  every rule/.style={->, draw, shorten <=+-5pt, shorten >=+-5pt},
  every tick line/.style={draw=gray},
  every diagram/.style={
    > = Latex[{round,open}],
    start chain=entry text going west below,
    start chain=entry date going east below,
    node distance=.5em,
  },
  every entry line/.style={rounded corners={.5*(\pgfkeysvalueof{/tikz/timeline/every tick length})}},
  every entry text/.style n args={4}{
    timeline/every entry text@, on chain=entry text, align=left, text width=10cm},
  every entry date/.style n args={4}{timeline/every entry date@, on chain=entry date},
  dates left and text right/.style={
    every entry text@/.style={% only the first, after that it's on the chain
      anchor=base west, at=(right:{\pgfkeysvalueof{/tikz/timeline/entry text distance}})},
    every entry date@/.style={% only the first, after that it's on the chain
      anchor=base east,
      at=(left:{\pgfkeysvalueof{/tikz/timeline/entry date distance}})},
    every entry text line/.style={
      timeline/every entry line,
      to path={
        (\tikztostart.mid west)
        -- +(left:{\pgfkeysvalueof{/tikz/timeline/every tick length}})
        -- ([xshift=\pgfkeysvalueof{/tikz/timeline/every tick length}]\tikztotarget)
        -- (\tikztotarget)}},
    every entry date line/.style={
      timeline/every entry line,
      to path={
        (\tikztostart.east)
        -- +(right:{\pgfkeysvalueof{/tikz/timeline/every tick length}})
        -- ([xshift=-(\pgfkeysvalueof{/tikz/timeline/every tick length})]\tikztotarget)
        -- (\tikztotarget)}}},
  dates and text right/.style={
    /tikz/continue chain=entry date going cal date,
    /tikz/continue chain=entry text going cal entry,
    /tikz/cal  date/.style={below left=3.5em and 0pt of entry text-end.south west},
    /tikz/cal entry/.style={base right=0pt           of entry date-end},
    every entry date@/.style={at=(right:1.5cm),            anchor=base west},
    every entry text@/.style={at=(entry date-1.base east), anchor=base west},
    every entry text line/.style={path only, overlay, to path=}, % disable
    every entry date line/.append style={
      timeline/every entry line,
      to path={
        --([xshift=-width("{0000, 00 September}")-1em]\tikztostart.east)
        \expanded{--([xshift=5pt]\tikztotarget)--([xshift=-5pt]\tikztotarget)}}},
    every tick line/.append style={every to/.append style={edge node={
      node[at start, below right, rotate=-90, append after command={
        edge[->, to path=(\tikzlastnode.north west)-|(\tikzlastnode.south east)]()}]{\tlYear}}}}},
}
\newcommand*{\tlDate}[3]{\tlIfYearNew{#1}{#1, }{}#3 \pgfcalendarmonthname{#2}}
\makeatletter
\newcommand*\tlIfYearNew[1]{\ifcsname tikz@timeline@#1\endcsname\expandafter\@secondoftwo\else\expandafter\@firstoftwo\fi}
\newenvironment{timeline}[1][]{
  \tikzpicture[timeline/every diagram,timeline/.cd,#1]%
    \pgfmathtruncatemacro\tlYearDifference{\pgfkeysvalueof{/tikz/timeline/end year}
                                         -(\pgfkeysvalueof{/tikz/timeline/start year})}%
    \tikzset{y=-(\pgfkeysvalueof{/tikz/timeline/year length})}%
    \path[timeline/every rule] (0,0) -- ++(0,\tlYearDifference);
    \foreach[count=\tlYear from \pgfkeysvalueof{/tikz/timeline/start year}]\tlY in {0,...,\tlYearDifference}
      \path[timeline/every tick line]
            ([xshift=-(\pgfkeysvalueof{/tikz/timeline/every tick length})]up:\tlY)
        to +(right:{2*(\pgfkeysvalueof{/tikz/timeline/every tick length})});
  \def\entry##1##2##3##4{% only defined inside timeline environment
    \tlIfYearNew{##1}{% calculate needed values
      \pgfcalendardatetojulian{##1-1-1}{\count@}%
      \edef\tlYearFirstDay{\the\count@}%
      \pgfcalendardatetojulian{\pgfinteval{##1+1}-1-1}{\count@}%
      \edef\tlYearTotalDays{\pgfinteval{\count@-\tlYearFirstDay}}%
    }{% extract previously stored values
      \edef\tlYearFirstDay {\expandafter\expandafter\expandafter\@firstoftwo \csname tikz@timeline@##1\endcsname}%
      \edef\tlYearTotalDays{\expandafter\expandafter\expandafter\@secondoftwo\csname tikz@timeline@##1\endcsname}%
    }%
    \pgfcalendardatetojulian{##1-##2-##3}{\count@}%
    \advance\count@ by -\tlYearFirstDay\relax % too big for PGFMath
    \pgfmathsetmacro\tlYearFraction{\count@/\tlYearTotalDays}%
    \coordinate(timeline point) at (up:{##1-(\pgfkeysvalueof{/tikz/timeline/start year})+\tlYearFraction});
    \node[timeline/every entry date={##1}{##2}{##3}{##4}]{\tlDate{##1}{##2}{##3}} edge[timeline/every entry date line] (timeline point);
    \node[timeline/every entry text={##1}{##2}{##3}{##4}]{##4}                    edge[timeline/every entry text line] (timeline point);
    \tlIfYearNew{##1}{\expandafter\edef\csname tikz@timeline@##1\endcsname{{\tlYearFirstDay}{\tlYearTotalDays}}}{}% store values for next time
  }%
}{\endtikzpicture}
\makeatother
\begin{document}
\noindent
\begin{timeline}[
  start year=1935,
  end year=1945,
  dates left and text right,
  every entry date@/.append style={node distance=3.5em},
]
\entry{1938}{9}{17}{Sudeten German Freikorps and other German paramilitary groups attacked Czechoslovak troops and institutions in the border areas}
\entry{1938}{9}{30}{Signing of the Betrayal of Munich by Hitler, Chamberlain, Daladier, and Mussolini: annexation of Sudetenland by Germany}
\entry{1939}{3}{15}{German army occupies Prague and establishes the Protectorate of Bohemia and Moravia, a puppet state with Konstantin Freiherr von Neurath at the helm}
\entry{1939}{7}{6}{Zentralstelle für jüdische Auswanderung in Böhmen und Mähren [Center for Jewish Emigration in Bohemia and Moravia] is established following the one in Vienna}
\entry{1939}{12}{1}{Jewish children are excluded from state schools}
\entry{1940}{6}{14}{Auschwitz extermination camp is established}
\entry{1941}{9}{27}{Reinhard Heydrich becomes the new Reichsprotektor and orders mass deportation of Jewish people and the reorganization of Terezín into a ghetto}
\entry{1941}{10}{16}{First transport from Prague to the Lodz ghetto}
\entry{1941}{11}{24}{First transport of Jewish people arrives in Terezín}
\end{timeline}

\begin{timeline}[
  start year=1937,
  end year=1944,
  year length=3cm,
  dates and text right,
]
\entry{1938}{9}{17}{Sudeten German Freikorps and other German paramilitary groups attacked Czechoslovak troops and institutions in the border areas}
\entry{1938}{9}{30}{Signing of the Betrayal of Munich by Hitler, Chamberlain, Daladier, and Mussolini: annexation of Sudetenland by Germany}
\entry{1939}{3}{15}{German army occupies Prague and establishes the Protectorate of Bohemia and Moravia, a puppet state with Konstantin Freiherr von Neurath at the helm}
\entry{1939}{7}{6}{Zentralstelle für jüdische Auswanderung in Böhmen und Mähren [Center for Jewish Emigration in Bohemia and Moravia] is established following the one in Vienna}
\entry{1939}{12}{1}{Jewish children are excluded from state schools}
\entry{1940}{6}{14}{Auschwitz extermination camp is established}
\entry{1941}{9}{27}{Reinhard Heydrich becomes the new Reichsprotektor and orders mass deportation of Jewish people and the reorganization of Terezín into a ghetto}
\entry{1941}{10}{16}{First transport from Prague to the Lodz ghetto}
\entry{1941}{11}{24}{First transport of Jewish people arrives in Terezín}
\end{timeline}
\end{document}

Output

enter image description here enter image description here

7
  • 2
    There are some interesting approaches one could take. Spread the labels along the height of the diagram. Or place them to the left of the tick when there's not another label in the way, then place them directly below or … Commented Jun 12, 2023 at 15:01
  • 1
    Absolutely phenomenal!
    – BlueIris
    Commented Jun 12, 2023 at 18:16
  • So sorry, how do I decrease the node distance in the second example @Qrrbrbirlbel?
    – BlueIris
    Commented Jun 13, 2023 at 5:01
  • 3
    @BlueIris The vertical distance is determined by the 3.5em in /tikz/cal date/.style={below left=3.5em and 0pt of entry text-end.south west}. Commented Jun 13, 2023 at 7:35
  • 1
    +1: Shoudl/could be it's own package :). Commented Jun 13, 2023 at 20:28
3

I tried and get my comments into some actual code, so here is the result. Unfortunately the chronology package sucks in terms of customization, and the only alternative seems to be raw tikz which I don't have experience in, so I suppose just call this part a "rough draft".

Nevertheless I find that the general approach of separating timeline and table is much more readable and less convoluted than the vertical all-in-one timeline.

\documentclass{article}
\usepackage{chronology}
\usepackage{tabularx}
\usepackage{booktabs}

\begin{document}

\begin{figure}
\begin{chronology}[1]{1938}{1941}{\textwidth}
\event{\decimaldate{17}{9}{1938}}{1}
\event{\decimaldate{30}{9}{1938}}{2}
\event{\decimaldate{6}{7}{1939}}{3}
\event{\decimaldate{1}{12}{1939}}{4}
\event{\decimaldate{14}{6}{1940}}{5}
\event{\decimaldate{17}{9}{1938}}{6}
\event{\decimaldate{27}{9}{1941}}{7}
\event{\decimaldate{16}{10}{1941}}{8}
\event{\decimaldate{24}{11}{1941}}{9}
\end{chronology}
\caption{Timeline. Details in table \ref{tab}.}
\label{fig}
\end{figure}

\newcommand{\mycmidrulegroup}{
\cmidrule(r){1-1}\cmidrule(lr){2-2}\cmidrule(lr){3-3}
\cmidrule(lr){4-4}\cmidrule(l){5-5}}

\begin{table}
\begin{tabularx}{\linewidth}{
@{}rrrrX@{}}
\toprule
{N} & {Y} & {M} & {D} & Event \\
\midrule
1 & 1938 & 9 & 17 &
Sudeten German Freikorps and other German paramilitary groups attacked Czechoslovak troops and institutions in the border areas \\
\mycmidrulegroup
2 &1938 & 9 & 30 &
Signing of the Betrayal of Munich by Hitler, Chamberlain, Daladier, and Mussolini: annexation of Sudetenland by Germany \\
\mycmidrulegroup
3 &1939 & 3 & 15 &
German army occupies Prague and establishes the Protectorate of Bohemia and Moravia, a puppet state with Konstantin Freiherr von Neurath at the helm \\
\mycmidrulegroup
4 &1939 & 7 & 6 &
Zentralstelle für jüdische Auswanderung in Böhmen und Mähren [Center for Jewish Emigration in Bohemia and Moravia] is established following the one in Vienna \\
\mycmidrulegroup
5 &1939 & 12 & 1 &
Jewish children are excluded from state schools \\
\mycmidrulegroup
6 &1940 & 6 & 14 &
Auschwitz extermination camp is established \\
\mycmidrulegroup
7 &1941 & 9 & 27 &
Reinhard Heydrich becomes the new Reichsprotektor and orders mass deportation of Jewish people and the reorganization of Terezín into a ghetto \\
\mycmidrulegroup
8 &1941 & 10 & 16 &
First transport from Prague to the Lodz ghetto \\
\mycmidrulegroup
9 &1941 & 11 & 24 &
First transport of Jewish people arrives in Terezín \\
\bottomrule
\end{tabularx}
\caption{Detailed events. Overview in figure \ref{fig}.}
\label{tab}
\end{table}

\end{document}

screenshot

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .