[Tex/LaTex] Multipart nodes with multipart subparts — style issue

nodestikz-pgftikz-treestrees

I want to typeset multipart nodes that have multipart sub parts — similar to the following diagram (from Miller and Ranum)

Tree with multipart nodes

I have tried using TikZ horizontal split rectangles inside vertical split rectangles with the following example LaTeX

\documentclass[12pt,a4paper]{article}
\usepackage[parfill]{parskip}
\usepackage{cmbright}
\renewcommand{\familydefault}{\sfdefault}
\usepackage{tikz}
\usetikzlibrary{arrows}
\usetikzlibrary{shapes.geometric}
\usetikzlibrary{shapes.multipart}
\usetikzlibrary{positioning}
\usetikzlibrary{trees}
\pgfkeys{/pgf/rectangle split parts=10}
\newlength{\MMtextNodeWidth}
\newcommand{\MMsetTextNodeWidth}[1]{%
  \settowidth{\MMtextNodeWidth}{#1}%
}
\begin{document}
  
{% \normalsize
\large
\MMsetTextNodeWidth{99}

\begin{tikzpicture}
  [myRectangleVSplit/.style={rectangle split
        ,rectangle split horizontal=false
        ,draw=black,thin,
        % ,inner sep=0pt 
        ,rectangle split ignore empty parts
       } % requires library shapes.multipart
   ,myRectangleHSplit/.style={rectangle split
        ,rectangle split horizontal
        ,draw=black,thin
        % ,inner sep=0.3333em 
        % ,outer sep=-0.3333em  % did not appear to do anything
        ,rectangle split part align={center,base} 
        ,rectangle split ignore empty parts
       } % requires library shapes.multipart
   ,edge from parent/.style={draw,thick,red,-triangle 60}
   % ,edge from parent fork down
   ,mySingleItem/.style={rectangle,draw=black,thin}
   ,level distance=6\MMtextNodeWidth
   ,level 1/.style={sibling distance=12\MMtextNodeWidth} 
   ,level 2/.style={sibling distance=6\MMtextNodeWidth} 
   ,level 3/.style={sibling distance=3\MMtextNodeWidth} 
   ,level 4/.style={sibling distance=2\MMtextNodeWidth} 
  ]
  
  \node[myRectangleVSplit,label=above:A] (nodeA)
    {rootnode\nodepart{two}\begin{tikzpicture}%
      \node[myRectangleHSplit] (nodeA2)
        {child1\nodepart{two}child2};
    \end{tikzpicture}
    }
    child {node[myRectangleVSplit,label=left:B] (nodeB)
     {node1\nodepart{two}
       \begin{tikzpicture}
        \node[myRectangleHSplit] (nodeB2)
          {child1\nodepart{two}child2\nodepart{three}child3};
       \end{tikzpicture}
     }
     child {node[mySingleItem,label=left:D] (nodeD) {node3}}
     child {node[mySingleItem,label=left:E] (nodeE) {node4}}
     child {node[mySingleItem,label=left:F] (nodeF) {node5}}
    }
    child {node[myRectangleVSplit,label=right:C] (nodeC) 
      {node2\nodepart{two}child1}
      child {node[mySingleItem,label=right:G] (nodeG) {node6}}
    };
\end{tikzpicture}
}% end size

\end{document}   

The above LaTeX results in

Result of above LaTeX

As you can see, the spacing in nodes with multipart sub parts (nodes A and B in my diagram) leaves a separation that is unwanted — I don't think you can alter the inner sep for just one part (TikZ manual page 451) and even if we got rid of that, the line thickness would be double in parts. Is there a better strategy here ? It looks like an obvious problem but I haven't seen something similar elsewhere.

Version 2

Instead of trying to use TikZ multi part shapes, here is a version using tables. The table line width \arrayrulewidth is 0.4pt which is the same as the TikZ thin line width. The nodes with the tables have inner sep set to 0pt so that the arrows meet the table boundary. Is this the right way to do this sort of diagram and could the arrows to nodes D and F be improved — could the start of those arrows be shifted to start nearer the appropriate child1 and child3 in node B

Here is the code:

\documentclass[12pt,a4paper]{article}
\usepackage[parfill]{parskip}
\usepackage{bigstrut}
\usepackage{cmbright}
\renewcommand{\familydefault}{\sfdefault}
\usepackage{tikz}
\usetikzlibrary{arrows}
\usetikzlibrary{shapes.geometric}
\usetikzlibrary{shapes.multipart}
\usetikzlibrary{positioning}
\usetikzlibrary{trees}
\pgfkeys{/pgf/rectangle split parts=10}
\newlength{\MMtextNodeWidth}
\newcommand{\MMsetTextNodeWidth}[1]{%
  \settowidth{\MMtextNodeWidth}{#1}%
}
\begin{document}
  
{% \normalsize
\large
\MMsetTextNodeWidth{99}

\begin{tikzpicture}
  [myTabularNode/.style={rectangle,inner sep=0pt}
   ,mySingleItem/.style={rectangle,draw=black,thin}
   ,edge from parent/.style={draw,thick,red,-triangle 60}
   % ,edge from parent fork down
   ,level distance=6\MMtextNodeWidth
   ,level 1/.style={sibling distance=12\MMtextNodeWidth} 
   ,level 2/.style={sibling distance=6\MMtextNodeWidth} 
   ,level 3/.style={sibling distance=3\MMtextNodeWidth} 
   ,level 4/.style={sibling distance=2\MMtextNodeWidth} 
  ]  
  \node[myTabularNode,label=above:A] (nodeA)
    {\begin{tabular}{|c|c|}\hline
      \multicolumn{2}{|c|}{rootnode\bigstrut} \\ \hline
       \bigstrut child1 & child2 \\ \hline 
    \end{tabular}
    }
    child {node[myTabularNode,label=left:B] (nodeB)
     {\begin{tabular}{|c|c|c|}\hline
       \multicolumn{3}{|c|}{node1\bigstrut} \\ \hline
     \bigstrut child1 & child2 & child3 \\ \hline 
     \end{tabular}
     }
     child {node[mySingleItem,label=left:D] (nodeD) {node3\bigstrut}}
     child {node[mySingleItem,label=left:E] (nodeE) {node4\bigstrut}}
     child {node[mySingleItem,label=left:F] (nodeF) {node5\bigstrut}}
    }
    child {node[myTabularNode,label=right:C] (nodeC) 
      {\begin{tabular}{|c|} \hline
       node2\bigstrut \\ \hline
       child1\bigstrut \\ \hline 
      \end{tabular}
      }
      child {node[mySingleItem,label=right:G] (nodeG) {node6\bigstrut}}
    };
\end{tikzpicture}
}% end size

\end{document}

and here is the resulting output:

Multipart nodes version 2

Best Answer

The posted code no longer works for me, so I played around to create a forest solution, which can also automatise things a bit (in case you have a large tree or many such trees!).

Rather than using tabular environments, this solution uses the edge path to simulate multi-part nodes/tabulars. The solution should, hopefully, work for any number of cells on the second row.

The code does make a few assumptions:

  • the 'tabulars' have exactly 2 rows;

  • the first row/upper part consists of a single cell;

  • the text in the first row/upper part is not excessively long relative to the total width of the second row/lower part;

  • the second row/lower part consists of 1+ cells of approximately the same height;

  • the elements of the 'tabulars'/multi-part nodes are entered as nodes, with one cell/part per node, and the style my tabular is applied to the node corresponding to the first row/upper part.

For example, if a tabular/multi-part node consists of an upper row/part containing upper and a lower row/part containing lower 1, lower 2 and lower 3, then, using the standard bracket syntax for trees, this would be entered as

[upper, my tabular [lower 1] [lower 2] [lower 3]]

forest will then figure out where to draw the lines.

forest in parts

Slight adjustments are required in this example to allow room for the labels applied to the inner terminal nodes. This is done by adding 10pt to the distance between their parents.

\documentclass[tikz, mult, varwidth, border=5pt]{standalone}
\usepackage{forest}
\standaloneenv{forest}
\usetikzlibrary{arrows.meta,calc}
\begin{document}
  \forestset{
    my tabular/.style={
      for children={
        if={equal(n_children("!u"),1)}{
          edge path={
            \noexpand\path [draw, \forestoption{edge}] (!u.north -| .north west) rectangle (.south east) (!u.south -| .north west) -- (!u.south -| .south east)\forestoption{edge label};
          },
        }{
          if={equal(n_children("!u"),2)}{
            edge path={
              \noexpand\path [draw, \forestoption{edge}] (!u.north -| !u1.west) -| (!u.south -| !ul.east) -| cycle (!u.south -| !ul.east) |- (!u1.south west) |- cycle (!u.south) -- (!u.south |- !ul.south)\forestoption{edge label};
            },
          }{
            if n=1{
              edge path={
                \noexpand\path [draw, \forestoption{edge}] (!u.north -| !u1.west) rectangle (!ul.south east) (!u.south -| !ul.east) -- (!u.south -| !u1.west)\forestoption{edge label} (!u.south -| {$(.east)!1/2!(!n.west)$}) |- (.south);
              },
            }{
              edge path={
                \noexpand\path [draw, \forestoption{edge}] (!u.south -| {$(.west)!1/2!(!p.east)$}) |- (.south)\forestoption{edge label};
              },
            },
          },
        },
        before computing xy={
          l=15pt,
        },
        before typesetting nodes={
          if={equal(n_children("!u"),2)}{
            if n=1{
              append={[, phantom]},
            }{
              if n'=1{
                prepend={[, phantom]},
              }{},
            },
          }{},
        },
      },
    },
  }
  \begin{forest}
    /tikz/every label/.append style={font=\sffamily\large},
    /tikz/every node/.append style={font=\sffamily\large},
    for tree={
      if n children=0{
        draw,
      }{},
      edge path={
        \noexpand\path [draw, -{Triangle[angle=60:1pt 3]}, thick, red] (!u.parent anchor) -- (.child anchor)\forestoption{edge label};
      },
      font=\sffamily\large,
    }
    [root node, my tabular, label={above:A}
      [child 1
        [node 1, my tabular, s sep+=10pt
          [child 1, edge label={node [at end, left] {B}}
            [node 3, label={left:D}
            ]
          ]
          [child 2
            [node 4, label={left:E}
            ]
          ]
          [child 3
            [node 5, label={left:F}
            ]
          ]
        ]
      ]
      [child 2
        [node 2, my tabular
          [child 1, edge label={node [at end, right] {C}}
          [node 6, label={right:G}
            ]
          ]
        ]
      ]
    ]
  \end{forest}
\end{document}