19

In my job I often draw network diagrams. Connections are usually doubled for safe redundant operation. Also, connections can be bundled for performance reasons, so there may be four connections between two devices.

I need to indicate that two edges belong to the same bundle. We say port channel or Ethernet channel for such aggregated connections between switches. Usually, they are indicated by a summarising ellipse in the middle of parallel lines.

It would be very helpful for network engineers who use TikZ instead of MS Visio to have an edge style for channels.

I started to create one:

  • parallel edges, 2 or 4 (other numbers rarely occur)
  • ellipse split into two arcs
    • either drawn as preaction and postaction
    • or on background and foreground layer
    • a border around lines is good for indicating a crossing (we could use a double style for line and arc)

Here is my code for a start:

\documentclass[tikz, border=2pt]{standalone}
\usetikzlibrary{arrows.meta,decorations.markings}
\def\bundlesep{2pt}% distance between parallel edges
\def\bundleX{0.8*\bundlesep}
\def\bundleY{1.8*\bundlesep}% x and y radius of ellipse
\tikzset{
  arc/.style = { x radius = \bundleX, y radius = \bundleY,
                 start angle = 90, delta angle = 180},
  arrow/.style = {{Bar[white, width = \bundlesep,length=0pt]}-{Bar[white,
      width = \bundlesep, length = 0pt]}},% just a workaround fixing `double` bug 
  decomark/.style = {black, thick, yshift = \bundleY, -, shorten <=-0.1pt},% arc style
  ellipse/.style = {
    preaction = {decorate, decoration = {markings,
      mark = at position 0.5 with {\draw[decomark, shorten >=-0.1pt] (0,0)
        arc [arc, delta angle = -180];}}},
    postaction = {decorate, decoration = {markings,
      mark=at position 0.5 with {\draw[decomark] (0,0)
          arc [arc];}}}},% consisting of 2 arcs
  bundle/.style = {double, line width = 0.5pt, double distance = \bundlesep,
      arrow, ellipse},
  single/.style = {white, double = black, line width = 0.4pt,
      double distance = 0.8pt, arrow, ellipse}
}
\begin{document}
\begin{tikzpicture}
  \draw [bundle] (0,0)    -- (3,0);       
  \draw [single] (0,-0.5) -- (3,-0.5);       
\end{tikzpicture}
\end{document}

bundled edges

These are two examples:

  1. I used a double path for two parallel edges. I wish there was a quad path! How can I have 4 lines?

  2. This shows how the bundling circle, drawn as ellipse, has a foreground part and a background part behind the line, gaps help to see the crossing. Also the arc should have some white space around for the other crossing.

Throwing this together and improving it to have an easy to use edge style, what would be the best way to draw a bundle of edges with a "3D" circle, that is, an ellipse around the 2 or 4 edges bundle?

3 Answers 3

15
+50

Here is a proposal that uses a decoration for the quadruple lines and the execute at begin to option to draw separate paths.

\documentclass[tikz, border=3.14mm]{standalone}
\usetikzlibrary{arrows.meta,decorations.markings,decorations}
\def\bundlesep{2pt}% distance between parallel edges
\def\bundleX{0.8*\bundlesep}
\def\bundleY{1.8*\bundlesep}% x and y radius of ellipse
\pgfdeclaredecoration{dublines}{initial}
{% 
\state{initial}[width=0pt,next state=final] {
    \pgfcoordinate{dubstart1}{\pgfpoint{0pt}{-0.5*\bundlesep}}
    \pgfcoordinate{dubstart2}{\pgfpoint{0pt}{0.5*\bundlesep}}
  }
  \state{final}[width=\pgfdecoratedpathlength]
  { 
    \pgfmoveto{\pgfpointanchor{dubstart1}{center}}
    \pgfpathlineto{\pgfpoint{\pgfdecoratedpathlength}{-0.5*\bundlesep}}
    \pgfmoveto{\pgfpointanchor{dubstart2}{center}}
    \pgfpathlineto{\pgfpoint{\pgfdecoratedpathlength}{0.5*\bundlesep}}
    \pgfmoveto{\pgfpointdecoratedpathlast}
  }
}
\pgfdeclaredecoration{quadlines}{initial}
{% 
\state{initial}[width=0pt,next state=final] {
    \pgfcoordinate{quadstart1}{\pgfpoint{0pt}{-1*\bundlesep}}
    \pgfcoordinate{quadstart2}{\pgfpoint{0pt}{-0.33*\bundlesep}}
    \pgfcoordinate{quadstart3}{\pgfpoint{0pt}{0.33*\bundlesep}}
    \pgfcoordinate{quadstart4}{\pgfpoint{0pt}{1*\bundlesep}}
  }
  \state{final}[width=\pgfdecoratedpathlength]
  { 
    \pgfmoveto{\pgfpointanchor{quadstart1}{center}}
    \pgfpathlineto{\pgfpoint{\pgfdecoratedpathlength}{-1*\bundlesep}}
    \pgfmoveto{\pgfpointanchor{quadstart2}{center}}
    \pgfpathlineto{\pgfpoint{\pgfdecoratedpathlength}{-0.33*\bundlesep}}
    \pgfmoveto{\pgfpointanchor{quadstart3}{center}}
    \pgfpathlineto{\pgfpoint{\pgfdecoratedpathlength}{0.33*\bundlesep}}
    \pgfmoveto{\pgfpointanchor{quadstart4}{center}}
    \pgfpathlineto{\pgfpoint{\pgfdecoratedpathlength}{1*\bundlesep}}
    \pgfmoveto{\pgfpointdecoratedpathlast}
  }
}
\tikzset{
  arc/.style = { x radius = \bundleX, y radius = \bundleY,
                 start angle = 90, delta angle = 180},
  decomark/.style = {double=black,white,double distance=0.8pt, yshift = \bundleY, -, shorten <=-0.1pt},% arc style
  edge bundle/.style n args={2}{execute at begin to={
  \path[decorate, decoration = {markings,
      mark=at position 0.5 with {\draw[decomark] (0,-\bundleY) circle({\bundleX} and \bundleY);
      }}] (\tikztostart)--(\tikztotarget);
  \draw[#1] (\tikztostart)--(\tikztotarget);      
  \draw[#2] (\tikztostart)--(\tikztotarget);
  \path[decorate, decoration = {markings,
      mark=at position 0.5 with {\draw[decomark] (0,0)
          arc [arc];}}] (\tikztostart)--(\tikztotarget);}},
  quad/.style={edge bundle={white,line width=2.5*\bundlesep}{decorate,decoration=quadlines}},
  single/.style={edge bundle={white,line width=1.6pt}{black,line width=.8pt}},               
  bundle/.style={edge bundle={white,line
  width=1.5*\bundlesep}{decorate,decoration=dublines}},
}
\begin{document}
\begin{tikzpicture}
  \draw [bundle] (0,0) to (3,0);       
  \draw [single] (0,-0.5) to (3,-0.5);       
  \draw [quad] (0,-1) to (3,-1);  
  \draw [bundle] (4,0) to (7,-2);
  \draw [quad] (5,-2) to (8,0);     
\end{tikzpicture}
\end{document}

enter image description here

OLD ANSWER (to be removed if the new answer is closer to what you want.)

\documentclass[tikz, border=3.14mm]{standalone}
\usetikzlibrary{arrows.meta,decorations.markings,decorations}
\def\bundlesep{2pt}% distance between parallel edges
\def\bundleX{0.8*\bundlesep}
\def\bundleY{1.8*\bundlesep}% x and y radius of ellipse
\pgfdeclaredecoration{quadlines}{initial}
{% 
\state{initial}[width=0pt,next state=final] {
    \typeout{\pgfdecoratedpathlength}
    \pgfcoordinate{quadstart1}{\pgfpoint{0pt}{-1*\bundlesep}}
    \pgfcoordinate{quadstart2}{\pgfpoint{0pt}{-0.33*\bundlesep}}
    \pgfcoordinate{quadstart3}{\pgfpoint{0pt}{0.33*\bundlesep}}
    \pgfcoordinate{quadstart4}{\pgfpoint{0pt}{1*\bundlesep}}
  }
  \state{final}[width=\pgfdecoratedpathlength]
  { 
    \pgfmoveto{\pgfpointanchor{quadstart1}{center}}
    \pgfpathlineto{\pgfpoint{\pgfdecoratedpathlength}{-1*\bundlesep}}
    \pgfmoveto{\pgfpointanchor{quadstart2}{center}}
    \pgfpathlineto{\pgfpoint{\pgfdecoratedpathlength}{-0.33*\bundlesep}}
    \pgfmoveto{\pgfpointanchor{quadstart3}{center}}
    \pgfpathlineto{\pgfpoint{\pgfdecoratedpathlength}{0.33*\bundlesep}}
    \pgfmoveto{\pgfpointanchor{quadstart4}{center}}
    \pgfpathlineto{\pgfpoint{\pgfdecoratedpathlength}{1*\bundlesep}}
    \pgfmoveto{\pgfpointdecoratedpathlast}
  }
}
\tikzset{
  arc/.style = { x radius = \bundleX, y radius = \bundleY,
                 start angle = 90, delta angle = 180},
  arrow/.style = {{Bar[white, width = \bundlesep,length=0pt]}-{Bar[white,
      width = \bundlesep, length = 0pt]}},% just a workaround fixing `double` bug 
  decomark/.style = {black, thick, yshift = \bundleY, -, shorten <=-0.1pt},% arc style
  ellipse/.style = {
    preaction = {decorate, decoration = {markings,
      mark = at position 0.5 with {\draw[decomark, shorten >=-0.1pt] (0,0)
        arc [arc, delta angle = -180];}}},
    postaction = {decorate, decoration = {markings,
      mark=at position 0.5 with {\draw[decomark] (0,0)
          arc [arc];}}}},% consisting of 2 arcs
  bundle/.style = {double, line width = 0.5pt, double distance = \bundlesep,
      arrow, ellipse},
  single/.style = {white, double = black, line width = 0.4pt,
      double distance = 0.8pt, arrow, ellipse},
  quad/.style={execute at begin to={
  \path[decorate, decoration = {markings,
      mark=at position 0.5 with {\draw[decomark] (0,-\bundleY) circle({\bundleX} and \bundleY);
      \draw[white,line width=2.5*\bundlesep] (0,0) -- (1.3*\bundleX,0);}}] (\tikztostart)--(\tikztotarget);
  \draw[decorate,decoration=quadlines] (\tikztostart)--(\tikztotarget);
  \path[decorate, decoration = {markings,
      mark=at position 0.5 with {\draw[decomark] (0,0)
          arc [arc];}}] (\tikztostart)--(\tikztotarget);}},   
}
\begin{document}
\begin{tikzpicture}
  \draw [bundle] (0,0)    -- (3,0);       
  \draw [single] (0,-0.5) -- (3,-0.5);       
  \draw [quad] (0,-1) to (3,-1);       
\end{tikzpicture}
\end{document}

enter image description here

4
  • Instead of using a decoration, one draw the parallel lines also with the calc syntax. Please let me know if you would like to see an example. I chose decorations since this may be easier to customize if you ever decide to also draw bent paths.
    – user121799
    Oct 20, 2018 at 20:14
  • Great, thanks! It also works fine when the lines are not horizontal. A white outline around the lines in example #1 and the arc in general (I guess also a decoration, like Tom showed above) would be perfect.
    – Stefan Kottwitz
    Oct 21, 2018 at 12:57
  • Yes, that makes is always better to see what is crossing above and what is below. As in Tom's image at the end. But don't worry, it's again a decoration. Thanks!
    – Stefan Kottwitz
    Oct 21, 2018 at 14:54
  • @StefanKottwitz I have rewritten the answer and made it more universal (i.e. there are some basic things stored in edge bundle which gets fed with the specifics for single, bundle and quad. Please have a look and tell me whether those go in the right direction.
    – user121799
    Oct 21, 2018 at 15:24
12

You can "stack" actions, so have a post action in a post action in a post action ... and so on. For some reason unknown to me, a double line as postaction to a double line produces not the desired result, but a way to wide line with wrong proportions. But one can stack post actions to draw lines alternating in black and white to achieve the look of four lines. As I kept doing something wrong with your nested definitions, I just copied everything into a draw command, I hope you'll be able to sort it out (Sorry!). I added two variantions for computing the line widths, absolute and relative.

Code

\documentclass[tikz, border=2pt]{standalone}
\usetikzlibrary{arrows.meta,decorations.markings}

%\pgfmathsetlengthmacro{\FirstBlack}{3.75pt}
%\pgfmathsetmacro{\LineFraction}{0.07}
%
%\pgfmathsetlengthmacro{\FirstWhite}{\FirstBlack*(1-2*\LineFraction)}
%\pgfmathsetlengthmacro{\SecondBlack}{\FirstBlack*(1-4*\LineFraction)/3+2*\FirstBlack*\LineFraction}
%\pgfmathsetlengthmacro{\SecondWhite}{\FirstBlack*(1-4*\LineFraction)/3}
%\pgfmathsetlengthmacro{\LoopX}{0.6*\FirstBlack}
%\pgfmathsetlengthmacro{\LoopY}{1.2*\FirstBlack}

\pgfmathsetlengthmacro{\FirstBlack}{4.3pt}
\pgfmathsetlengthmacro{\LineWidth}{0.4pt}

\pgfmathsetlengthmacro{\FirstWhite}{\FirstBlack-2*\LineWidth}
\pgfmathsetlengthmacro{\SecondBlack}{(\FirstBlack-4*\LineWidth)/3+2*\LineWidth}
\pgfmathsetlengthmacro{\SecondWhite}{(\FirstBlack-4*\LineWidth)/3}
\pgfmathsetlengthmacro{\LoopX}{0.6*\FirstBlack}
\pgfmathsetlengthmacro{\LoopY}{1.2*\FirstBlack}

\begin{document}
\begin{tikzpicture}
  \draw
  [ line width=\FirstBlack,
    postaction=
    {   draw, white, line width=\FirstWhite,
        postaction=
        {   draw, black, line width=\SecondBlack,
            postaction=
            {   draw, white, line width=\SecondWhite
            }
        }
    },
    preaction=
    {   decorate,
        decoration=
        {   markings, mark=at position 0.5 with
            {   \draw[  black, thick, yshift = \LoopY, -, shorten <=-0.1pt, shorten >=-0.1pt]
                    (0,0) arc 
                    [   x radius = \LoopX, y radius = \LoopY,
                 start angle = 90, delta angle = 180, delta angle = -180];}}},
        postaction=
    {   decorate,
        decoration=
        {   markings, mark=at position 0.5 with
            {   \draw[  black, thick, yshift = \LoopY, -, shorten <=-0.1pt, shorten >=-0.1pt]
                    (0,0) arc 
                    [   x radius = \LoopX, y radius = \LoopY,
                 start angle = 90, delta angle = 180];}}}
    ]
        (0,1) --(5,1);

  \node[right] {\FirstBlack, \FirstWhite, \SecondBlack, \SecondWhite};
\end{tikzpicture}
\end{document}

Output

enter image description here


Edit 1: For a white outline one can simply add another line (here the \ZerothWhite). One can also apply a post action to the arcs, so therefore they can also have a white outline:

Code

\documentclass[tikz, border=2pt]{standalone}
\usetikzlibrary{arrows.meta,decorations.markings}

%\pgfmathsetlengthmacro{\FirstBlack}{3.75pt}
%\pgfmathsetmacro{\LineFraction}{0.07}
%\pgfmathsetmacro{\WhiteFraction}{0.15}
%\pgfmathsetlengthmacro{\LoopFraction}{0.1}
%\pgfmathsetlengthmacro{\LoopSpaceFraction}{0.2}
%
%\pgfmathsetlengthmacro{\ZerothWhite}{\FirstBlack*(1+2*\WhiteFraction)}
%\pgfmathsetlengthmacro{\FirstWhite}{\FirstBlack*(1-2*\LineFraction)}
%\pgfmathsetlengthmacro{\SecondBlack}{\FirstBlack*(1-4*\LineFraction)/3+2*\FirstBlack*\LineFraction}
%\pgfmathsetlengthmacro{\SecondWhite}{\FirstBlack*(1-4*\LineFraction)/3}
%\pgfmathsetlengthmacro{\LoopX}{0.6*\FirstBlack}
%\pgfmathsetlengthmacro{\LoopY}{1.2*\FirstBlack}
%\pgfmathsetlengthmacro{\LoopWidth}{\LoopFraction*\FirstBlack}
%\pgfmathsetlengthmacro{\LoopWhite}{(\LoopFraction+2*\LoopFraction)*\FirstBlack}

\pgfmathsetlengthmacro{\FirstBlack}{4.3pt}
\pgfmathsetlengthmacro{\LineWidth}{0.4pt}
\pgfmathsetlengthmacro{\WhiteSpace}{1.2pt}
\pgfmathsetlengthmacro{\LoopWidth}{0.4pt}
\pgfmathsetlengthmacro{\LoopSpace}{1.2pt}

\pgfmathsetlengthmacro{\ZerothWhite}{\FirstBlack+2*\WhiteSpace}
\pgfmathsetlengthmacro{\FirstWhite}{\FirstBlack-2*\LineWidth}
\pgfmathsetlengthmacro{\SecondBlack}{(\FirstBlack-4*\LineWidth)/3+2*\LineWidth}
\pgfmathsetlengthmacro{\SecondWhite}{(\FirstBlack-4*\LineWidth)/3}
\pgfmathsetlengthmacro{\LoopX}{0.6*\FirstBlack}
\pgfmathsetlengthmacro{\LoopY}{1.2*\FirstBlack}
\pgfmathsetlengthmacro{\LoopWhite}{\LoopWidth+2*\LoopSpace}

\begin{document}
\begin{tikzpicture}
  \draw
  [ line width=\FirstBlack,
    postaction=
    {   draw, white, line width=\FirstWhite,
        postaction=
        {   draw, black, line width=\SecondBlack,
            postaction=
            {   draw, white, line width=\SecondWhite
            }
        }
    },
    preaction=
    {   draw, white, line width=\ZerothWhite,
        preaction=
        {   decorate,
            decoration=
            {   markings, mark=at position 0.5 with
                {   \draw[  black, line width=\LoopWidth, yshift = \LoopY, -, shorten <=-0.1pt, shorten >=-0.1pt]
                        (0,0) arc 
                        [   x radius = \LoopX, y radius = \LoopY, start angle = 90, delta angle = 180,
                            delta angle = -180];
            }
          }
        }      
    }, 
        postaction=
    {   decorate,
        decoration=
        {   markings, mark=at position 0.5 with
            {   \draw[  white, line width=\LoopWhite, yshift = \LoopY, -, shorten <=-0.1pt, shorten >=-0.1pt,
                                postaction={draw, black, line width=\LoopWidth}
                         ]
                    (0,0) arc 
                    [   x radius = \LoopX, y radius = \LoopY, start angle = 90, delta angle = 180           
                    ];}}}
    ]
        (0,1) --(5,1);

  \node[right] {\FirstBlack, \FirstWhite, \SecondBlack, \SecondWhite};
\end{tikzpicture}
\end{document}

Output

enter image description here

1
  • I think that the double works by doubling the current line width. So if you stack them, it keeps doubling them, I think.
    – cfr
    Nov 29, 2015 at 20:05
2

This combines the nfold library with a ring pic.

Unfortunately, the ring pic can't access the original path's double setup which is why I patch \tikz@normal@fig (the macro that starts the node parsing which is also used for the pic) so that it saves the current modes.

This also means that the specifying any double settings after the pic have no effect on the pic.

This works:

\tikz
  \draw[double distance=5mm] (0,0) -- pic {ring} (right:1);

This does not:

\tikz
  \draw                      (0,0) -- pic {ring} (right:1) [double distance=5mm];

Code

\documentclass[tikz]{standalone}
\usetikzlibrary{nfold}
\usepackage{etoolbox}
\makeatletter
\pretocmd{\tikz@normal@fig}{\let\tikz@pathmode\tikz@mode}{}{}
\tikzset{
  pics/ring/.style={
    /tikz/sloped, /tikz/allow upside down,
    setup code=
      \begingroup
        \tikz@mode@doublefalse % only one we care for
        \tikz@pathmode\iftikz@mode@double\tikz@double@setup\fi
        \edef\tikz@temp{\def\noexpand\tikzringlinewidth{\the\pgflinewidth}}%
        \pgfsetlinewidth{+\tikz@save@line@width}% PGF globalizes the line width :/
      \expandafter\endgroup\tikz@temp
      \pgfmathsetmacro\ringY{\pgfkeysvalueof{/tikz/ring half width}}%
      \pgfmathsetmacro\ellAngle{asin(\ringY/(\pgfkeysvalueof{/tikz/ring y radius}))},
    foreground code={
      \draw[pic actions, fill=none, shade=none,
        draw=\pgfkeysvalueof{/tikz/ring background color},
        line width/.expanded=\pgfkeysvalueof{/tikz/ring clearance}]
        (180-\ellAngle:{\pgfkeysvalueof{/tikz/ring x radius}}
                   and {\pgfkeysvalueof{/tikz/ring y radius}})
             arc[x radius=\pgfkeysvalueof{/tikz/ring x radius},
                 y radius=\pgfkeysvalueof{/tikz/ring y radius},
              start angle=180-\ellAngle, end angle=180+\ellAngle];
      \draw[pic actions]
         (\ellAngle:{\pgfkeysvalueof{/tikz/ring x radius}}
                and {\pgfkeysvalueof{/tikz/ring y radius}})
           arc[x radius=\pgfkeysvalueof{/tikz/ring x radius},
               y radius=\pgfkeysvalueof{/tikz/ring y radius},
               start angle=\ellAngle, end angle=360-\ellAngle];}},
  ring y radius/.initial = .5*\tikzringlinewidth + 3pt,
  ring x radius/.initial = (\pgfkeysvalueof{/tikz/ring y radius})/2,
  ring half width/.initial = .5*\tikzringlinewidth + 1pt,
  ring clearance/.initial = 2.5*\the\pgflinewidth + .5pt,
  ring background color/.initial=white}
\begin{document}
\begin{tikzpicture}[y=5mm]
\foreach \i in {1, ..., 4}
  \draw[style/.expanded={\ifnum\i>2 double distance=1mm\fi},
        style/.expanded={\ifnum\i>1 double, nfold = \i\fi},
        ] (0,\i.5) -- pic[thick,] {ring} +(right:1);
\draw[double distance=3mm, nfold=5, ring background color=red]
  (0,0) to[bend left=50]
    pic[near start, thick]                   {ring}
    pic[near end,   thick, green, xscale=-1] {ring} (1,0);
\end{tikzpicture}
\end{document}

Output

enter image description here

3
  • Looks great! Can it be combined with a white outline like Tom did?
    – Stefan Kottwitz
    Jun 11, 2023 at 17:39
  • @StefanKottwitz Normally, I'd just double with white as an outer color and black as the inner color but it seems like nfold's settings get inherited with the double option but can't be disabled easily. Another option would be a preaction with white and a thicker line width. I almost use the same but with a custom path so that not the whole ring needs to be painted white, just the area that crosses the path (as long as ring half width is setup correctly. The last example shows this with the color red. Jun 11, 2023 at 17:53
  • Excellent, thanks!
    – Stefan Kottwitz
    Jun 11, 2023 at 20:26

You must log in to answer this question.

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