43

Motivation

In the answer Highlight a group of nodes in a tikz tree, Jake suggested combining the convex hull approach from padded boundary of convex hull with the hobby path and I was really intrigued by the possibility.

Preliminary work

At first I tried to modify at least as possible the \convexpath:

\documentclass[a4paper,11pt]{article}
\usepackage{tikz}
\usetikzlibrary{hobby,backgrounds,calc,trees}

\newcommand{\myconvexpath}[2]{
[   
    create hobbyhullnodes/.code={
        \global\edef\namelist{#1}
        \foreach [count=\counter] \nodename in \namelist {
            \global\edef\numberofnodes{\counter}
            \node at (\nodename) [draw=none,name=hobbyhullnode\counter] {};
        }
        \node at (hobbyhullnode\numberofnodes) [name=hobbyhullnode0,draw=none] {};
        \pgfmathtruncatemacro\lastnumber{\numberofnodes+1}
        \node at (hobbyhullnode1) [name=hobbyhullnode\lastnumber,draw=none] {};
    },
    create hobbyhullnodes
]
($(hobbyhullnode1)!#2!-90:(hobbyhullnode0)$)
\foreach [
    evaluate=\currentnode as \previousnode using \currentnode-1,
    evaluate=\currentnode as \nextnode using \currentnode+1
    ] \currentnode in {1,...,\numberofnodes} {
   let \p1 = ($(hobbyhullnode\currentnode)!#2!-90:(hobbyhullnode\previousnode) - (hobbyhullnode\currentnode)$),
    \n1 = {atan2(\x1,\y1)},
    \p2 = ($(hobbyhullnode\currentnode)!#2!90:(hobbyhullnode\nextnode) - (hobbyhullnode\currentnode)$),
    \n2 = {atan2(\x2,\y2)},
    \n{delta} = {-Mod(\n1-\n2,360)}
  in 
    {arc [start angle=\n1, delta angle=\n{delta}, radius=#2]}
     ..($(hobbyhullnode\nextnode)!0.5!(hobbyhullnode\currentnode)$)
     ..($(hobbyhullnode\nextnode)!#2!-90:(hobbyhullnode\currentnode)$)
}
--cycle
}

\begin{document}
\begin{tikzpicture}[use Hobby shortcut]
\node (f) {f}
    child { node (g) {g}
      child { node (a) {a}
    }
      child { node (b) {b}
    }
  }
    child { node (h) {h}
      child { node (c) {c}
    }
  };
  \begin{pgfonlayer}{background}
  \fill[draw,blue, opacity=0.3] \myconvexpath{f,h,c,g}{12pt};
  \fill[draw,red, opacity=0.3] \myconvexpath{g,b,a}{12pt};
  \end{pgfonlayer}
  \end{tikzpicture}

\end{document}

which leads to:

enter image description here

I suspected the combination of arcs with the hobby path was the cause of cusps, so in another example I tried with:

\documentclass[a4paper,11pt]{article}
\usepackage{tikz}
\usetikzlibrary{hobby,backgrounds,calc,trees}

\newcommand{\myconvexpath}[2]{
[   
    create hobbyhullnodes/.code={
        \global\edef\namelist{#1}
        \foreach [count=\counter] \nodename in \namelist {
            \global\edef\numberofnodes{\counter}
            \node at (\nodename) [draw=none,name=hobbyhullnode\counter] {};
        }
        \node at (hobbyhullnode\numberofnodes) [name=hobbyhullnode0,draw=none] {};
        \pgfmathtruncatemacro\lastnumber{\numberofnodes+1}
        \node at (hobbyhullnode1) [name=hobbyhullnode\lastnumber,draw=none] {};
    },
    create hobbyhullnodes
]
($(hobbyhullnode1)!#2!-90:(hobbyhullnode0)$)
\foreach [
    evaluate=\currentnode as \previousnode using \currentnode-1,
    evaluate=\currentnode as \nextnode using \currentnode+1
    ] \currentnode in {1,...,\numberofnodes} {
   let \p1 = ($(hobbyhullnode\currentnode)!#2!-90:(hobbyhullnode\previousnode)$),
    \n1 = {atan2(\x1,\y1)},
    \p2 = ($(hobbyhullnode\currentnode)!#2!90:(hobbyhullnode\nextnode)$),
    \n2 = {atan2(\x2,\y2)},
    \n{delta} = {-Mod(\n1-\n2,360)},
    \n{end}={add(\n1,\n{delta})}
  in 
    {..([in angle=\n1]$(hobbyhullnode\currentnode)!#2!-90:(hobbyhullnode\previousnode)$)..([out angle=\n{end}]$(hobbyhullnode\currentnode)!#2!90:(hobbyhullnode\nextnode)$)}
     ..($(hobbyhullnode\nextnode)!0.5!(hobbyhullnode\currentnode)$)
     ..($(hobbyhullnode\nextnode)!#2!-90:(hobbyhullnode\currentnode)$)
}
--cycle
}

\begin{document}
\begin{tikzpicture}[use Hobby shortcut]
\node (f) {f}
    child { node (g) {g}
      child { node (a) {a}
    }
      child { node (b) {b}
    }
  }
    child { node (h) {h}
      child { node (c) {c}
    }
  };
  \begin{pgfonlayer}{background}
  \fill[draw,blue, opacity=0.3] \myconvexpath{f,h,c,g}{12pt};
  \fill[draw,red, opacity=0.3] \myconvexpath{g,b,a}{12pt};
  \end{pgfonlayer}
  \end{tikzpicture}

\end{document}

that gives a not promising result:

enter image description here

Question

Is there a way to automatically recognize the node angle a path will fall when arrives near it? Doing things by hand, one can force a path to follow the desired direction, for example, h.north -> h.east -> h.south, but how is it possible to do it automatically without the arc syntax?

Notice that, for some shapes, one could proceed as follows:

\documentclass[a4paper,11pt]{article}
\usepackage{tikz}
\usetikzlibrary{hobby,backgrounds,calc,trees}

\newcommand{\hobbyconvexpath}[2]{
[   
    create hobbyhullnodes/.code={
        \global\edef\namelist{#1}
        \foreach [count=\counter] \nodename in \namelist {
            \global\edef\numberofnodes{\counter}
            \node at (\nodename) [draw=none,name=hobbyhullnode\counter] {};
        }
        \node at (hobbyhullnode\numberofnodes) [name=hobbyhullnode0,draw=none] {};
        \pgfmathtruncatemacro\lastnumber{\numberofnodes+1}
        \node at (hobbyhullnode1) [name=hobbyhullnode\lastnumber,draw=none] {};
    },
    create hobbyhullnodes
]
($(hobbyhullnode1)!#2!-40:(hobbyhullnode0)$)
\foreach [
    evaluate=\currentnode as \previousnode using \currentnode-1,
    evaluate=\currentnode as \nextnode using \currentnode+1
    ] \currentnode in {1,...,\numberofnodes} {
  let \p1 = ($(hobbyhullnode\currentnode)!#2!-90:(hobbyhullnode\previousnode) $),
    \n1 = {atan2(\x1,\y1)},
    \p2 = ($(hobbyhullnode\currentnode)!#2!-90:(hobbyhullnode\nextnode)$),
    \n2 = {atan2(\x2,\y2)},
    \n{delta} = {-Mod(\n1-\n2,360)},
    \n{fin}={add(\n1,\n{delta})}
  in 
      {..($(hobbyhullnode\currentnode)!#2!-220:(hobbyhullnode\previousnode)$)..($(hobbyhullnode\currentnode)!#2!40:(hobbyhullnode\nextnode)$)}
   %{arc [start angle=\n1, end angle=\n{fin}, radius=#2]}
     ..($(hobbyhullnode\nextnode)!0.5!(hobbyhullnode\currentnode)$)
     ..($(hobbyhullnode\nextnode)!#2!-40:(hobbyhullnode\currentnode)$)
}
--cycle
}

\begin{document}

\begin{tikzpicture}[use Hobby shortcut]
  \foreach \place/\text in {{(1,0)/a},{(0,-1)/b},{(-1,0)/c},{(0,1)/d}}
  \node[name=\text] at \place {\text};
  \begin{pgfonlayer}{background}
  \fill[draw,green, opacity=0.3] \hobbyconvexpath{a,b,c,d}{10pt};
  \end{pgfonlayer}
\end{tikzpicture}

\end{document}

enter image description here

but in general is not a valid approach and it is still to improve, to get at least the same result of Highlight a group of nodes in a tikz tree.

4
  • I'm not clear as to the role of Hobby's algorithm here. That produces a passably smooth curve through a given set of points. It is designed for when you don't know much about the curve other than those points. Here, it would seem that you know too much about the curve for Hobby's algorithm to be of particular use. Sep 17, 2012 at 12:37
  • @AndrewStacey: in your opinion a classical curve path construction (pos)..controls (controll point)and(controll point).. would give similar results? Actually, when I answered in Highlight a group of nodes in a tikz tree I didn't considered the possibility... Sep 17, 2012 at 17:51
  • 3
    What I suspect is that you'll want to put various constraints on the path and those will mean that the full power of Hobby's algorithm isn't used (there might be some use for computing the control points given the angles). So I'd want to start by knowing what makes a "good" diagram and what characteristics it has. If you'd like to discuss it, perhaps the "From Answers to Packages" chat room would be a good place. Sep 17, 2012 at 19:37
  • Found some part of the solution here: tex.stackexchange.com/questions/121286/…
    – Markus W.
    Nov 22, 2016 at 23:30

1 Answer 1

45

I'm answering because I think there's a quite good solution on the problem. First of all I'd like to thank Andrew Stacey because without the discussion in chat I wouldn't be able to solve this and, more important, he founds the major issue in the code.


What I learnt

The answer to the question:

Is there a way to automatically recognize the node angle a path will fall when arrives near it?

is actually: TikZ already does it automatically. Notice indeed that:

\documentclass{article}
\usepackage{tikz}
\begin{document}
\begin{tikzpicture}
\node[circle,fill=red] (a) at (1,1) {}; 
\node[circle,fill=blue] (b) at  (3,2) {};
\draw[color=black] (a) -- (b);
\end{tikzpicture}
\end{document}

correctly leads to:

enter image description here

and the user should not have to care about the angle the line arrives near the blue node.

The major issue

Building on that, however, was not sufficient. Andrew recognized that the paths drawn were not correctly realized due to the fact that the hobby path shortcut construct separately each piece; this was the cause of the bizarre behaviour as per the second figure displayed in the question.

The intermediate result

With his help it was possible to get (code here):

enter image description here

and notice how this solved the problems mentioned before.

A final result

Starting from the intermediate result I noticed that actually the non-perfect fit around some nodes was basically due to the out angle, that is the the path from hypothetically ($(hobbyhullnode1)!10pt!-90:(hobbyhullnode0)$)..($(hobbyhullnode1)!10pt!90:(hobbyhullnode0)$). With a bit of care, the right angle should have been set to 180 rather than 90.

Here is a MWE:

\documentclass[tikz,border=2bp]{standalone}
\usetikzlibrary{backgrounds,calc,trees,hobby}

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

\newcommand{\hobbyconvexpath}[2]{
[   
    create hobbyhullnodes/.code={
        \global\edef\namelist{#1}
        \foreach [count=\counter] \nodename in \namelist {
            \global\edef\numberofnodes{\counter}
            \node at (\nodename)
[draw=none,name=hobbyhullnode\counter] {};
        }
        \node at (hobbyhullnode\numberofnodes)
[name=hobbyhullnode0,draw=none] {};
        \pgfmathtruncatemacro\lastnumber{\numberofnodes+1}
        \node at (hobbyhullnode1)
[name=hobbyhullnode\lastnumber,draw=none] {};
    },
    create hobbyhullnodes
]
($(hobbyhullnode1)!#2!-90:(hobbyhullnode0)$)
\pgfextra{
  \gdef\hullpath{}
\foreach [
    evaluate=\currentnode as \previousnode using \currentnode-1,
    evaluate=\currentnode as \nextnode using \currentnode+1
    ] \currentnode in {1,...,\numberofnodes} {
    \pgfmathtruncatemacro\thecurrentnode\currentnode
    \pgfmathtruncatemacro\thepreviousnode\previousnode
    \pgfmathtruncatemacro\thenextnode\nextnode
    \xdef\hullpath{\hullpath    
  ..($(hobbyhullnode\thecurrentnode)!#2!180:(hobbyhullnode\thepreviousnode)$)
  ..($(hobbyhullnode\thenextnode)!0.5!(hobbyhullnode\thecurrentnode)$)}
    \ifx\currentnode\numberofnodes
    \xdef\hullpath{\hullpath .. cycle}
    \else
    \xdef\hullpath{\hullpath
  ..($(hobbyhullnode\thenextnode)!#2!-90:(hobbyhullnode\thecurrentnode)$)}
    \fi
}
}
\hullpath
}

\begin{document}
\begin{tikzpicture}[use Hobby shortcut,scale=3,transform shape]
\node (f) {f}
    child { node (g) {g} 
      child { node (a) {a}
    }
      child { node (b) {b}
    }
  }
    child { node (h) {h}
      child { node (c) {c}
      }
  };

\begin{pgfonlayer}{background}
\fill[red,opacity=0.3] \hobbyconvexpath{a,g,b}{10pt};
\end{pgfonlayer}

\draw[blue,dashed]($(hobbyhullnode1)!10pt!-90:(hobbyhullnode0)$)--
($(hobbyhullnode1)!10pt!-90:(hobbyhullnode0)$)..($(hobbyhullnode1)!10pt!180:(hobbyhullnode0)$)..
($(hobbyhullnode2)!0.5!(hobbyhullnode1)$) ..
($(hobbyhullnode2)!10pt!-90:(hobbyhullnode1)$)..($(hobbyhullnode2)!10pt!180:(hobbyhullnode1)$)..($(hobbyhullnode3)!0.5!(hobbyhullnode2)$)..
($(hobbyhullnode3)!10pt!-90:(hobbyhullnode2)$)..($(hobbyhullnode3)!10pt!180:(hobbyhullnode2)$)
..($(hobbyhullnode4)!0.5!(hobbyhullnode3)$) .. cycle;

\begin{pgfonlayer}{background}
\fill[green!50!lime,opacity=0.4] \hobbyconvexpath{g,f,h,c}{10pt};
\end{pgfonlayer}

\draw[blue,dashed]($(hobbyhullnode1)!10pt!-90:(hobbyhullnode0)$)--
($(hobbyhullnode1)!10pt!-90:(hobbyhullnode0)$)..($(hobbyhullnode1)!10pt!180:(hobbyhullnode0)$)..
($(hobbyhullnode2)!0.5!(hobbyhullnode1)$) ..
($(hobbyhullnode2)!10pt!-90:(hobbyhullnode1)$)..($(hobbyhullnode2)!10pt!180:(hobbyhullnode1)$)..($(hobbyhullnode3)!0.5!(hobbyhullnode2)$)..
($(hobbyhullnode3)!10pt!-90:(hobbyhullnode2)$)..($(hobbyhullnode3)!10pt!180:(hobbyhullnode2)$)
..($(hobbyhullnode4)!0.5!(hobbyhullnode3)$) ..
($(hobbyhullnode4)!10pt!-90:(hobbyhullnode3)$)..($(hobbyhullnode4)!10pt!180:(hobbyhullnode3)$)
..($(hobbyhullnode5)!0.5!(hobbyhullnode4)$) .. cycle;
\end{tikzpicture}

\begin{tikzpicture}[use Hobby shortcut,scale=3,transform shape]
\node (f) {f}
    child { node (g) {g} 
      child { node (a) {a}
    }
      child { node (b) {b}
    }
  }
    child { node (h) {h}
      child { node (c) {c}
      }
  };

\begin{pgfonlayer}{background}
\fill[orange,opacity=0.4] \hobbyconvexpath{a,g,h,b}{13pt};
\end{pgfonlayer}
\end{tikzpicture}

\begin{tikzpicture}[use Hobby shortcut,scale=3,transform shape]
\node (f) {f}
    child { node (g) {g} 
      child { node (a) {a}
    }
      child { node (b) {b}
    }
  }
    child { node (h) {h}
      child { node (c) {c}
      }
  };

\begin{pgfonlayer}{background}
\fill[cyan,opacity=0.4] \hobbyconvexpath{b,g,f,h}{10pt};
\end{pgfonlayer}
\end{tikzpicture}
\end{document}

with the following result:

enter image description here

Another example (since I noticed that the shape highlighted is actually the same in all the three previous figures):

\begin{tikzpicture}[use Hobby shortcut,scale=3,transform shape]
\node (f) {f}
    child { node (g) {g} 
      child { node (a) {a}
        child { node (i) {i}}
    }
      child { node (b) {b}
            child { node (d) {d}}
            child { node (e) {e}}
    }
  }
    child { node (h) {h}
      child { node (c) {c}
      }
  };

\begin{pgfonlayer}{background}
\fill[cyan,opacity=0.4] \hobbyconvexpath{a,g,f,b,e,d}{10pt};
\end{pgfonlayer}
\end{tikzpicture}

enter image description here

9
  • 2
    Really nice!... Oct 12, 2012 at 16:19
  • What a nice surprise! Oct 16, 2012 at 20:19
  • The issue that lead to a non perfect alignment has been solved: I leave to the curious to check the code to understand what was the problem. :) Nov 9, 2012 at 18:33
  • 1
    If the edit was the culprit you can instead use evaluate=\currentnode as \previousnode using int(\currentnode-1) so that the result is truncated. I think that might be the same issue that was in tex.stackexchange.com/questions/52961/…
    – percusse
    Nov 9, 2012 at 20:45
  • 4
    Note: see tex.stackexchange.com/q/121286/86 if using the latest version of the hobby package as the cycle path specification is (at time of writing) no longer valid. Jun 27, 2013 at 9:00

You must log in to answer this question.

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