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}
\draw[line width=1cm] (0,0) -- (0,2);the resulting picture has a (visual) width of 1cm. If PGF wouldn't add.5\pgflinewidthon 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 usingtrim left=0pt, trim right=\linewidthin case of a diagram that's supposed to cover the whole\linewidth. – Qrrbrbirlbel Nov 16 '23 at 20:57preaction/postactionsystem 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\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\draw[line width=.5cm] (0,0) -- (1,1);, if its bounding box is exactly the rectangle (0,0) to (1,1), thenline capsetting is only partially visible. Depending on how complex the completetikzpicturewill be, one can use\fillto replace\drawor set bounding box manually (\useasboundingbox). – muzimuzhi Z Nov 16 '23 at 21:17line caponly 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 anyline cap. (The author of) PGF doesn't want its pictures to bleed into the surrounding document so it adds a safe.5\pgflinewidthon all sides. (Themiterjoin can break this.) – Qrrbrbirlbel Nov 16 '23 at 21:54\tikz[line width=1cm]\draw (0,0) [radius=5cm];meant to produce? I get no visible output. – cfr Nov 17 '23 at 00:33\tikz[line width=1cm]\draw (0,0) circle[radius=5cm];. – Qrrbrbirlbel Nov 17 '23 at 01:03line cap(orline 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:42decorationsmodule even does this already. – Qrrbrbirlbel Nov 17 '23 at 10:41