1

Related: Horizontal space before TikZ picture

Consider the following MWE

\documentclass{article}
\usepackage{tikz}
\usepackage[showframe]{geometry}
\begin{document}
\noindent
\begin{tikzpicture}
  \draw [red] (0pt,0pt) -- ++(\linewidth,0pt);
  \node [anchor=south,font=\sffamily\footnotesize] at (current bounding box.north) {\textcolor{red}{\the\pgflinewidth} too wide};
\end{tikzpicture}

\noindent \begin{tikzpicture} \draw [red,line width=1pt] (0pt,0pt) -- ++(\linewidth,0pt); \node [anchor=south,font=\sffamily\footnotesize] at (current bounding box.north) {\textcolor{red}{1pt} too wide}; \end{tikzpicture}

\noindent \begin{tikzpicture} \draw [red,line width=10pt] (0pt,0pt) -- ++(\linewidth,0pt); \node [anchor=south,font=\sffamily\footnotesize] at (current bounding box.north) {\textcolor{red}{10pt} too wide}; \end{tikzpicture}

\noindent \begin{tikzpicture} \draw [red,line width=100pt] (0pt,0pt) -- ++(\linewidth,0pt); \node [anchor=south,font=\sffamily\footnotesize] at (current bounding box.north) {\textcolor{red}{100pt} too wide}; \end{tikzpicture} \end{document}

Naively, I would not expect this to produce overfull boxes, yet it does

Overfull \hbox (0.4pt too wide) in paragraph at lines 246--251
[]

Overfull \hbox (1.0pt too wide) in paragraph at lines 252--257 []

Overfull \hbox (10.0pt too wide) in paragraph at lines 258--263 []

Overfull \hbox (100.0pt too wide) in paragraph at lines 264--269 []

The output shows the displacement of the lines quite clearly

displaced lines

What causes this and how can I prevent it?

  • Clearly, one option to prevent an overfull box is to simply reduce the length of the line by \pgflinewidth. This is noticeable even at smaller widths, but it is clearly not viable as lines get thicker.
  • Another option would be to draw the line while not accounting for the bounding box and then repeat the path, without drawing it, to set the bounding box.
  • Gonzalo Medina recommends using overlay, but this means the bounding box is wrong.

[Edited to remove misleading example.]

Is there a recommended way to address this issue?

cfr
  • 198,882
  • 1
    Why: to be on the safe side. If you draw a vertical line \draw[line width=1cm] (0,0) -- (0,2); the resulting picture has a (visual) width of 1cm. If PGF wouldn't add .5\pgflinewidth on both sides the diagram would bleed 5mm into your document. In this case, you could do [line cap=rect] (0,0) -- +(\linewidth-\pgflinewidth,0); as a workaround but I'm sure you're not just drawing a horizontal line. In the past, I've opted on using trim left=0pt, trim right=\linewidth in case of a diagram that's supposed to cover the whole \linewidth. – Qrrbrbirlbel Nov 16 '23 at 20:57
  • 1
    But that means non-horizontal things will bleed into the margin. I don't think there is a good quick solution that covers all usecases. For repeating a path, the preaction/postaction system can be used to not have to actually repeat the path. For example \path[line width=100pt, postaction={draw=red, overlay}] (0,0) -- (\linewidth,0); but then you will also lose the vertical bounding box – that's important, too! – Qrrbrbirlbel Nov 16 '23 at 21:02
  • Add \tikzset{every picture/.style={line cap=round}} or \tikzset{every picture/.style={line cap=rect}} in you preamble to understand why your must reduce the size of your line by \pgflinewidth. – Paul Gaborit Nov 16 '23 at 21:16
  • An extreme example: for a thick slant line \draw[line width=.5cm] (0,0) -- (1,1);, if its bounding box is exactly the rectangle (0,0) to (1,1), then line cap setting is only partially visible. Depending on how complex the complete tikzpicture will be, one can use \fill to replace \draw or set bounding box manually (\useasboundingbox). – muzimuzhi Z Nov 16 '23 at 21:17
  • IMO, line cap only covers a small part why PGF/TikZ adds .5\pgflinewidth. (PGF knows which cap is installed and could adjust the bounding box accordingly.) For non-trivial diagrams, the line width is really a problem. Take for instance \tikz[line width=1cm]\draw (0,0) [radius=5cm]; Yes, the diameter of the circle is 10cm but visually the circle covers a rectangular area of (11cm)². This is independent of any line cap. (The author of) PGF doesn't want its pictures to bleed into the surrounding document so it adds a safe .5\pgflinewidth on all sides. (The miter join can break this.) – Qrrbrbirlbel Nov 16 '23 at 21:54
  • @Qrrbrbirlbel What is \tikz[line width=1cm]\draw (0,0) [radius=5cm]; meant to produce? I get no visible output. – cfr Nov 17 '23 at 00:33
  • 2
    @cfr Whoops. That should be \tikz[line width=1cm]\draw (0,0) circle[radius=5cm];. – Qrrbrbirlbel Nov 17 '23 at 01:03
  • @Qrrbrbirlbel If the line is neither vertical nor horizontal, taking line cap (or line join) into account is no longer trivial. For circles or Bézier curves, read my answer or my answer. – Paul Gaborit Nov 17 '23 at 07:42
  • 1
    @PaulGaborit I'm not even tarking about the control points of Bézier curves. That has been discussed many times on this site already. My circle example was about a closed path (so line cap is irrelevant) and even line join doesn't matter here because all joins are tangential or whatever the right word is. The circle example is purely about the line width. A circle of diameter 10cm with line width 1cm will put ink on an area of (11cm)². That is why PGF adds half the line width. That will always be relevant. – Qrrbrbirlbel Nov 17 '23 at 10:36
  • @PaulGaborit And yes, it's no longer trivial. I'm aware. Technically, all the informations are present. You just need to parse them out of the softpath. The decorations module even does this already. – Qrrbrbirlbel Nov 17 '23 at 10:41
  • @Qrrbrbirlbel I know that you know all of this as well or even better than me and that all of this has already been discussed on this site. I just wanted to point out that this is not trivial... and that TikZ/PGF's choice to only approximate the bounding box is for performance reasons. – Paul Gaborit Nov 17 '23 at 10:48

1 Answers1

4

What causes this and how can I prevent it?

PGF adds .5\pgflinewidth on all four sides to the bounding box (if the at this point established bounding box is smaller) when a path is stroked/drawn.

It does this to cover not only the line cap rect and round but also the line width itself.

Even in your examples, a simply horizontal line of width 100pt should result in a picture that's 100pt high, otherwise it will cover up text:

\documentclass{article}
\usepackage{tikz}
\usepackage[showframe, pass]{geometry}
\setlength\parindent{0pt}
\begin{document}
I'm covered by a red bar.

\tikz[line width=100pt, opacity=.5] \useasboundingbox[postaction={draw=red, overlay}] (0pt,0pt) -- +(\linewidth,0pt);

I cover a red bar.

\vspace{50pt}

I'm covered by a red bar. \tikz[line width=100pt, opacity=.5] \useasboundingbox[postaction={draw=red, overlay}] (0pt,0pt) -- +(0pt,2cm); I cover a red bar. \end{document}

Not all miter line joins will be covered by this.

Clearly, one option to prevent an overfull box is to simply reduce the length of the line by \pgflinewidth. This is noticeable even at smaller widths, but it is clearly not viable as lines get thicker.

In combination with line cap=rect maybe:

\tikz[line width=100pt]
  \draw[red, line cap=rect]
    (0pt, 0pt) -- +(\linewidth-\pgflinewidth,0pt);

Another option would be to draw the line while not accounting for the bounding box and then repeat the path, without drawing it, to set the bounding box.

Not really, this still doesn't cover the actual line width in the vertical dimension. (Though, for a line width of 0.4pt, it's not going to be noticable really.) We can at least use postaction to not have to literally repeat the path again (and also not to parse the path and built the softpath).

Still covered.

\tikz[line width=100pt, opacity=.5] \path[postaction={draw=red, overlay}] (0pt, 0pt) -- +(\linewidth,0pt);

Still covering.


If all you want is to draw horizontal lines from 0pt to \linewidth don't use TikZ but \rule use trim left=0pt, trim right=\linewidth:

\tikz[line width=100pt, trim left=+0pt, trim right=+\linewidth]
  \draw[red] (0pt, 0pt) -- +(\linewidth,0pt);

These keys are useful if you know the width of your paths without the line width (or can determine it by two coordinates since it also allows trim left=(left coordinate of the picture), trim right=(right coordinate of the picture).

I've used these options before, especially when it comes to pictures spanning the whole \linewidth.


For now, let's ignore the round line cap (the bounding box will always be perfect then anyway) and the butt line cap (I believe only horizontal and vertical lines would be good uses for it).

For a round line join the bounding box will be perfect as well, The

What determines the “real” bounding box?

For each path segment, we just need to calculate a path that is .5\pgflinewidth parallel to the segment. And for each connection between two path segments we need to calculate

That's a lot of parsing a math I don't want to do, but for butt line caps and miter line joins, the nfold library provides the algorithm. We use it to offset the original path half the line width to both sides and use that as the bounding box of the path.

Code

\documentclass{article}
\usepackage{tikz}
\usetikzlibrary{nfold}
\makeatletter
\tikzset{
  real bb/.style={% this is not perfect because it messes with the original
                  % postaction and softpath system
    overlay,      % ignore the original's path bb
    postaction={overlay=false, path only, qoffset=+1},
    postaction={overlay=false, path only, qoffset=-1}},
  qoffset/.code=\tikz@addoption{\pgfgetpath\tikz@temp\pgfsetpath\pgfutil@empty
                   \pgfoffsetpathqfraction\tikz@temp{.5\pgflinewidth}{#1}}}
\makeatother
\usepackage[showframe, pass]{geometry}
\setlength\parindent{0pt}
\tikzset{every picture/.append style={execute at end picture={
  \draw[help lines] ([xshift=+.5\pgflinewidth,yshift=+.5\pgflinewidth]
    current bounding box.south west) rectangle
    ([xshift=+-.5\pgflinewidth,yshift=+-.5\pgflinewidth]
    current bounding box.north east);}}}
\begin{document}

\section{Using only the path as bounding box} Using only the path for the bounding box does not work because a horizontal line still has a height and a vertical line still has a width.

\vspace{35pt}

I'm covered by a red bar.

\tikz[line width=100pt, opacity=.5] \useasboundingbox[postaction={draw=red, overlay}] (0pt,0pt) -- +(\linewidth,0pt);

I cover a red bar.

\vspace{50pt}

I'm covered by a red bar. \tikz[line width=100pt, opacity=.5] \useasboundingbox[postaction={draw=red, overlay}] (0pt,0pt) -- +(0pt,2cm); I cover a red bar. \bigskip

\section{Trimming left and right and \texttt{real bb}} For a simple horizontal line, I'd use \verb|trim left| and \verb|trim right|:

\tikz[line width=100pt, trim left=+0pt, trim right=+\linewidth] \draw (0pt,0pt) --+(\linewidth,0pt);

Otherwise, we'll try \verb|real bb|:

\tikz[line width=100pt] \draw[real bb] (0pt,0pt) --+(\linewidth,0pt);\bigskip

\pagebreak

Another example where the default approach fails:\medskip

\tikz[line width=25pt, baseline=(current bounding box)] \draw [ rotate=45] (0,-1) rectangle (2,1);\qquad\qquad \tikz[line width=25pt, baseline=(current bounding box)] \draw [yscale=.5, rotate=45] (0,-1) rectangle (2,1);\bigskip

And with \verb|real bb|:

\tikz[line width=25pt, baseline=(current bounding box)] \draw [ rotate=45, real bb] (0,-1) rectangle (2,1); \tikz[line width=25pt, baseline=(current bounding box)] \draw [yscale=.5, rotate=45, real bb] (0,-1) rectangle (2,1);

\begingroup \tikzset{every picture/.append style={line width=20pt, text=white, sloped}}

\section{Line Caps} Only a round line cap will always fit perfectly.

\foreach \linecap in {butt, rect, round} \tikz[line cap=\linecap] \draw (0,0) -- node{\linecap} +(30:2);\medskip

The \verb|real bb| key can be used to “fix” the default butt line cap. The round cap does not need fixing. Good luck with the rect line cap.\medskip

\foreach \linecap in {butt, rect, round} \tikz[line cap=\linecap] \draw[real bb] (0,0) -- node{\linecap} +(30:2);

\pagebreak \section{Line Joins} Once again, only a round line join will fit perfectly.

\foreach \linejoin in {miter, bevel, round} \tikz[line join=\linejoin] \draw (0,0) -- node{\linejoin} ++(30:1.5) -- +(-30:1.5);

With \verb|real bb| again: Miter line join looks good. The round line join didn't need it. And the bevel does its own thing. (And there's also the \verb|miter limit| changing things.)

\foreach \linejoin in {miter, bevel, round} \tikz[line join=\linejoin] \draw[real bb] (0,0) -- node{\linejoin} ++(30:1.5) -- +(-30:1.5); \endgroup

\section{Hmm…} \tikz[line width=1cm, trim left=+-.5\linewidth, trim right=+.5\linewidth] \draw (0,0) node {What about this?} circle[radius=.5\linewidth]; \end{document}

Qrrbrbirlbel
  • 119,821
  • Thanks. This is really very helpful in a negative sort of way. I think my best bet is to draw it without changing the bounding box or to use your internal patch. But, frankly, I'm not sure I understand that well enough. line cap isn't an option because I don't know there isn't a line cap or an arrow tip. trim seems not to work in a local scope. And, yes, rule is a bit underpowered for my real case. I considered fill but it is complicated with the possibility of arrow tips and such. Perhaps resetting the bounding box would work better .... – cfr Nov 17 '23 at 01:18
  • Resetting seems to work. – cfr Nov 17 '23 at 03:06
  • Do you want to mention this in your answer? (Unless there's some reason not to do it.) – cfr Nov 17 '23 at 03:14
  • Yes, but I don't want to override a requested line cap. I could avoid that by ordering, but I'd rather the options given behave more like they would in a regular tikzpicture where possible. Or maybe I mean closer to how people expect them to. I think it is reasonable to assume a user won't account for the addition of .5\pgflinewidth space around the line, but somebody adding a cap or arrow can reasonably be expected to take that into account. It would be different if \linewidth was fixed, but it isn't. Changing the bounding box is one thing; changing the width of the line another. – cfr Nov 17 '23 at 04:21
  • Moreover, the user can't easily remove that additional space, whereas they can easily adjust the width of the line and its endings. And it is easy to add space, if desired, but difficult to remove, if not. But my concern about trim isn't that it is too local, but that it is not local enough. – cfr Nov 17 '23 at 04:24
  • Actually, nothing works in my real case :(. – cfr Nov 17 '23 at 05:55
  • @cfr Probably an XY problem... TikZ/pgf only calculates an approximation of the bounding box (see p.129 of the pgfmanual). – Paul Gaborit Nov 17 '23 at 07:29
  • @cfr I've updated my answer. The nfold library provides an interface to find the true bounding box of a path with butt line cap and miter line join. My code isn't a good hook to apply this because it uses the postaction system. It would be better to do this in \pgfusepath or \tikz@finish. I'll need to check where's a good hook for this. I realize now, that this does not work well will arrow tips … I believe your problems aren't easily solved with a small fix but needs checks and patches at multiple places. Arrow tips bring their own bounding box and then real bb is pointless … – Qrrbrbirlbel Nov 18 '23 at 14:33
  • @PaulGaborit Likely, I admit. So far, resetting is working better than anything else I've tried. It isn't ideal by any means. It seems to do OK with and without arrow tips. (If it weren't for arrow tips/line caps, I'd use fill across the board rather than for only a subset of cases.) – cfr Nov 18 '23 at 17:55
  • @Qrrbrbirlbel Thank-you! This looks really useful, though I don't necessarily need it in this particular case. For current purposes, I know I'm dealing with a single horizontal line. (Or three filled rectangles, but there's no problem in that case.) – cfr Nov 18 '23 at 18:09