TikZ-PGF – Determining TikZ Bend Direction Automatically

node-connectionstikz-calctikz-pgf

Consider the following example:

\documentclass{article}
\usepackage{tikz}
\usetikzlibrary{positioning}
\begin{document}
\begin{tikzpicture}
\node (A) {A};
\node (B) [right=2cm of A] {B};
\draw[->,bend left=20] (A) to (B);
\draw[->,bend left=20,red] (B) to (A);
\draw[->,bend right=40,blue] (B) to (A);
\end{tikzpicture}
\end{document}

output of code

If I draw the arrow from A to B, then bend left curves above the height of A and B as expected (the black arrow). But if I reverse the order of the nodes in the path, bend left curves below A and B (the red arrow).

What I want to do is to always have the arrow above, (like the blue arrow) and not have to specify manually the bend direction. So intuitively, I would like to be able to do the following:

if A is left of B then bend left
else
if B is left of A then bend right

How would I do that?

Best Answer

The actual math it simple, we calculate the difference between start and end point and choose the appropriate bend direction depending on the sign of \pgf@x or \pgf@y. (Though, the special cases for the difference being zero are not considered here.)

This also only uses the center of the nodes, if the nodes have different size this might not lead to the correct result.

The implementation below uses PGF to calculate the difference and also a faster approach to using the keys. With the calc library, the math can be done with the let operation:

\tikzset{
  bend north/.style={
    to path={
      let \p0=($(\tikztostart)-(\tikztotarget)$) in
      [bend \ifdim\x0<0pt left\else right\fi={#1}]
      \tikz@to@curve@path}}}

The direct usage of \tikz@to@curve@path allows us to avoid nesting to paths which needs some special expansion control otherwise to avoid recursive definitions like \def\tikztotarget{\tikztotarget} and to nodes to be lost or not applies correctly:

\expanded{
   to[bend <left or right>=<angle>]
   \unexpanded\expandafter{\tikz@tonodes}
   (\tikztotarget)
}

Code

\documentclass[tikz]{standalone}
\usepackage{tikz}
\usetikzlibrary{positioning}
\makeatletter
\tikzset{
  % “execute at begin to” can't be used inside “to[bend north]”
  % which is why we do this simply at the start of “to path”
  auto bend/north/.code={{{%
    \pgf@process{%
      \pgfpointdiff{\tikz@scan@one@point\pgfutil@firstofone(\tikztostart)}
                   {\tikz@scan@one@point\pgfutil@firstofone(\tikztotarget)}}%
    \pgfkeysvalueof{/tikz/bend \ifdim\pgf@x<0pt right\else left\fi/.@cmd}#1\pgfeov}}},
  auto bend/south/.code={{{%
    \pgf@process{%
      \pgfpointdiff{\tikz@scan@one@point\pgfutil@firstofone(\tikztostart)}
                   {\tikz@scan@one@point\pgfutil@firstofone(\tikztotarget)}}%
    \pgfkeysvalueof{/tikz/bend \ifdim\pgf@x<0pt left\else right\fi/.@cmd}#1\pgfeov}}},
  auto bend/west/.code={{{%
    \pgf@process{%
      \pgfpointdiff{\tikz@scan@one@point\pgfutil@firstofone(\tikztostart)}
                   {\tikz@scan@one@point\pgfutil@firstofone(\tikztotarget)}}%
    \pgfkeysvalueof{/tikz/bend \ifdim\pgf@y<0pt right\else left\fi/.@cmd}#1\pgfeov}}},
  auto bend/east/.code={{{%
    \pgf@process{%
      \pgfpointdiff{\tikz@scan@one@point\pgfutil@firstofone(\tikztostart)}
                   {\tikz@scan@one@point\pgfutil@firstofone(\tikztotarget)}}%
    \pgfkeysvalueof{/tikz/bend \ifdim\pgf@y<0pt left\else right\fi/.@cmd}#1\pgfeov}}},
  /utils/temp/.style={
    bend #1/.style={
      to path={% this should be faster than “[auto bend/#1={##1}]”
        \pgfextra{\pgfkeysvalueof{/tikz/auto bend/#1/.@cmd}##1\pgfeov}
        \tikz@to@curve@path}}},
  /utils/temp/.list={north, south, west, east}}
\makeatother
\begin{document}
\begin{tikzpicture}
\node (A) {A};
\node (B) [right=2cm of A] {B};
\draw[->, bend north=20      ] (A) to (B);
\draw[->, bend north=30,  red] (B) to (A);
\draw[->, bend north=40, blue] (B) to (A);
\end{tikzpicture}

\foreach \ang in {0, 10, ..., 359}{%
  \begin{tikzpicture}[nodes={sloped, fill=white, inner ysep=+.1em,
                             fill opacity=.8, text opacity=1, scale=.5}]
    \useasboundingbox (-3,-3) (3,3);
    \foreach[count=\i] \c/\d in {black/north, red/south,
      green!50!black/west, yellow!50!black/east}
      \draw[\c] (0,0) to[bend \d=\i0] node{\d} (\ang:3);
  \end{tikzpicture}}
\end{document}

Output

enter image description here