% File: numodel-plot-manual.tex
% Standalone manual for the numodel-plot package.

\documentclass{ltxdoc}
\usepackage[left=3.5cm, right=2cm, top=2cm, bottom=2cm,
            marginparwidth=3.5cm, marginparsep=0.3cm]{geometry}
\usepackage{numodel-plot}
\usepackage{subcaption}
\EnableCrossrefs
\CodelineIndex

% Fonts.  fontspec + unicode-math require LuaLaTeX.
\usepackage{fontspec}
\setmainfont{Arial}
\usepackage{unicode-math}
\setmathfont{Lete Sans Math}
\setmonofont{Fira Mono}

% Breakable inline verbatim (see numodel-manual.tex for rationale).
\usepackage{fvextra}
\AtBeginDocument{%
  \MakeShortVerb{\|}%
  \DeleteShortVerb{\|}%
  \DefineShortVerb[breaklines,breakanywhere]{\|}%
}

% tcolorbox+listings for example boxes that both display and execute.
\usepackage{tcolorbox}
\tcbuselibrary{listings,skins,breakable}

\lstdefinestyle{numodelcode}{
  basicstyle=\small\ttfamily,
  breaklines=true,
  columns=fullflexible,
  keepspaces=true,
  showstringspaces=false,
}

\NewTCBListing{plotexample}{}{%
  enhanced,
  breakable,
  listing style=numodelcode,
  colback=black!3,
  colframe=black!50,
  boxrule=0.4pt,
  arc=2pt,
  before skip=10pt,
  after skip=10pt,
}

% No colour tweaks needed for the example plots: plotexample is a
% tcolorbox, so the package's halo-color=auto default picks up its
% colback for the tick-label halos.  See §"Tick label halos".

\begin{document}

\CheckSum{0}

\changes{v0.1}{2026/04/24}{Initial version, extracted from internal
  project sources.}
\changes{v0.2.0}{2026/05/16}{l3build workflow; bundle structure
  with numodel; \cs{drawplot} now invokes \cs{calcplotdims}
  internally; default axis-label-format renamed to \texttt{ieee}.}
\changes{v0.3.0}{2026/05/17}{Fix \cs{par}-token leak in
  \cs{calcplotdims} that bloated the picture bounding box inside
  horizontal boxes; fix \texttt{grid / unknown} key syntax so custom
  \texttt{grid=} values work; move \texttt{legend pos=outer north
  east} into the \texttt{numodel/axis} style so it can be overridden;
  add rendered example plots throughout the manual.}
\changes{v0.4.0}{2026/05/19}{Version-sync release with
  \textsf{numodel}~v0.4.0; no functional changes to
  \textsf{numodel-plot} itself.  Example files renamed from
  \texttt{numodel-plot-simple.tex}/\texttt{numodel-plot-scaled.tex}
  to \texttt{numodel-plot-example-basic.tex}/\texttt{numodel-plot-example-scaled.tex}.}
\changes{v0.5.0}{2026/05/23}{Version-sync release with
  \textsf{numodel}~v0.5.0; no functional changes to
  \textsf{numodel-plot} itself.}

\GetFileInfo{numodel-plot.sty}

\DoNotIndex{\newcommand,\newenvironment,\def,\edef,\let,\global,
  \RequirePackage,\usepgfplotslibrary,\pgfplotsset,\ProvidesPackage,
  \NeedsTeXFormat,\makeatletter,\makeatother,\endinput,\providecommand,
  \NewDocumentCommand,\ExplSyntaxOn,\ExplSyntaxOff,\ifnum,\ifdefined,
  \fi,\else,\relax,\undefined,\fpeval,\si,\qty,\num,\penalty,\nobreak,
  \begin,\end,\thinspace}

\title{The \textsf{numodel-plot} package\thanks{This document
  corresponds to \textsf{numodel-plot}~\fileversion, dated \today.}}
\author{Paul Zuurbier \\ \texttt{mail@paulzuurbier.nl}}
\date{\today}
\maketitle

\begin{abstract}
A PGFPlots engine that auto-sizes plots to a whole number of tick
intervals, supports configurable axis-label formats (IEEE-style
by default; ISO 80000-1 also supported), and automatically selects
label placement for 1-, 2-, and 4-quadrant graphs.  Part of the \textsf{numodel} package suite, but
can be loaded standalone as an independent PGFPlots styling layer.
\end{abstract}

\tableofcontents

\section{Introduction}

\textsf{numodel-plot} fills a gap between bare PGFPlots and the
heavy styling required for physics-teaching material: it sizes every
axis to an integer number of centimetre ticks, lays out the axis
origin according to which quadrants of the coordinate plane contain
data, and renders axis labels as either |quantity (unit)| (IEEE,
the default) or one of four alternative conventions selectable at
package level.  It was extracted from a Dutch high-school physics
test set where uniform plot appearance across hundreds of graphs is
more valuable than per-graph tweaking, and hence adopts an
opinionated default style.  Users who need one-off deviations are
expected to drop to plain PGFPlots with the variable macros
|\xmin|, |\xmax|, \ldots{} exposed by this package.

\newpage
\section{Usage}

Minimum working example (assuming |\usepackage{numodel-plot}| in
the preamble):

\begin{plotexample}
\def\xmin{-5}   \def\xmax{10}
\def\ymin{-3}   \def\ymax{5}
\def\xlabelqty{t}  \def\xlabelunit{\s}
\def\ylabelqty{v}  \def\ylabelunit{\m\per\s}
\drawplot{\addplot[domain=\xmin:\xmax]{0.5*x};}
\end{plotexample}

The user sets the data range (|\xmin|\ldots|\ymax|) and optionally a
quantity symbol plus \textsf{siunitx} unit for each axis.
|\drawplot| internally calls |\calcplotdims| to round the range to a
clean tick lattice and compute the axis size in centimetres, then
renders a full |tikzpicture|+|axis| environment whose body is the
argument (one or more |\addplot| lines).

Labels are built automatically from |\xlabelqty|+|\xlabelunit| (and
likewise for the $y$-axis).  If the data magnitude exceeds $10^{4}$
or is below $10^{-2}$, an engineering factor~$10^{n}$ (with $n$ a
multiple of~3) is split off and PGFPlots' own |scaled ticks| are
configured so that tick numbers remain small.  By default the factor
is folded into an SI prefix on the unit --- 5000~m becomes
\verb|s (km)| --- and only when no engineering prefix fits (say
$10^{3}\,$m$^{2}$, which would need $10^{1.5}$ per metre) does the
label fall back to showing the power of ten itself; the
|scale-format| key (section~\ref{sec:keys}) forces that form
globally.  In the next example the data magnitude is
$5\times10^{7}$ and the unit |\mega\joule\per\kilo\gram| already
carries a prefix on the leading unit; the injected $10^{6}$ combines
with mega into tera, giving \verb|E (TJ/kg)|:
\begin{plotexample}
\def\xmin{0}   \def\xmax{10}
\def\ymin{0}   \def\ymax{5e7}
\def\xlabelqty{m}  \def\xlabelunit{\kilo\gram}
\def\ylabelqty{E}  \def\ylabelunit{\mega\joule\per\kilo\gram}
\drawplot{\addplot[domain=\xmin:\xmax]{5e6*x};}
\end{plotexample}

Users preferring full control can omit |\xlabelqty|/|\xlabelunit|
and set |\xlabel|/|\ylabel| directly; the package will use them
verbatim.

\section{Configuration}

\DescribeMacro{\numodelplotsetup}
Configuration is set through a single key--value interface:
\begin{quote}
\begin{verbatim}
\numodelplotsetup{axis-label-format=ieee, grid=mm-dots}
\end{verbatim}
\end{quote}

\subsection{Keys}\label{sec:keys}

\begin{description}
\item[\texttt{axis-label-format}] Default |ieee|.  Determines the
  notation emitted for axis labels built from |\xlabelqty| and
  |\xlabelunit|:
  \begin{center}
  \begin{tabular}{lll}
    \texttt{ieee}      & \verb|v (m/s)|      & IEEE (default) \\
    \texttt{iso}       & \verb|v / (m/s)|    & ISO 80000-1 \\
    \texttt{brackets}  & \verb|v [m/s]|      & older physics convention \\
    \texttt{qty-only}  & \verb|v|            & quantity symbol only \\
    \texttt{unit-only} & \verb|m/s|          & unit only \\
  \end{tabular}
  \end{center}
  When scaling is applied (data exceeds $10^{4}$ or below
  $10^{-2}$), the factor is integrated into the label according to
  |scale-format|, e.g.\ \verb|v (km/s)| for IEEE.  Under |qty-only|
  the exponent remains in PGFPlots' scaled-tick label instead
  (otherwise the scale information would be lost).
\item[\texttt{scale-format}] Default |prefix|.  How a scaled axis
  presents its power of ten in the label.  |prefix| folds it into an
  SI prefix on (the first unit atom of) the unit: \verb|s (km)|,
  \verb|E (TJ/kg)|, \verb|m (Mg)|.  The fold respects unit powers
  ($10^{6}\,$m$^{2}$ becomes km$^{2}$) and existing prefixes, and
  silently falls back to the power-of-ten form whenever no
  engineering prefix fits ($10^{3}\,$m$^{2}$, or $10^{3}\,$m$^{3}$
  which would need the non-engineering deca).  |exponent| always
  shows the power of ten: \verb|s (10^3 m)|.
\item[\texttt{grid}] Default |mm-dots| (black millimetre dots,
  matching engineering millimetre paper).  Accepts |none|, or any
  PGFPlots style list which will be passed verbatim to the
  |numodel/grid| style.
\item[\texttt{xcmmax}, \texttt{ycmmax}] Maximum axis width and
  height in centimetres (defaults 12 and 10).
\end{description}

The first three axis-label formats render as follows.  Each plot
uses |\numodelplotsetup{xcmmax=3, ycmmax=3}| so the axis itself is
trimmed to a 2~cm by 3~cm tick lattice (the package's invariant 1~cm
major-grid spacing is preserved):

\begin{plotexample}
\captionsetup{type=figure}%
\begin{subfigure}{0.33\textwidth}\centering
\numodelplotsetup{xcmmax=3, ycmmax=3, axis-label-format=ieee}%
\def\xmin{0}\def\xmax{10}\def\ymin{0}\def\ymax{5}%
\def\xlabelqty{v}\def\xlabelunit{\m\per\s}%
\def\ylabelqty{F}\def\ylabelunit{\N}%
\drawplot{\addplot[domain=\xmin:\xmax,thick]{0.4*x};}
\caption*{\texttt{ieee}}
\end{subfigure}\hspace{-1cm}%
\begin{subfigure}{0.33\textwidth}\centering
\numodelplotsetup{xcmmax=3, ycmmax=3, axis-label-format=iso}%
\def\xmin{0}\def\xmax{10}\def\ymin{0}\def\ymax{5}%
\def\xlabelqty{v}\def\xlabelunit{\m\per\s}%
\def\ylabelqty{F}\def\ylabelunit{\N}%
\drawplot{\addplot[domain=\xmin:\xmax,thick]{0.4*x};}
\caption*{\texttt{iso}}
\end{subfigure}\hspace{-1cm}%
\begin{subfigure}{0.33\textwidth}\centering
\numodelplotsetup{xcmmax=3, ycmmax=3, axis-label-format=brackets}%
\def\xmin{0}\def\xmax{10}\def\ymin{0}\def\ymax{5}%
\def\xlabelqty{v}\def\xlabelunit{\m\per\s}%
\def\ylabelqty{F}\def\ylabelunit{\N}%
\drawplot{\addplot[domain=\xmin:\xmax,thick]{0.4*x};}
\caption*{\texttt{brackets}}
\end{subfigure}
\end{plotexample}
\numodelplotsetup{axis-label-format=ieee}%

Three grid variants, sized the same way:

\begin{plotexample}
\captionsetup{type=figure}%
\begin{subfigure}{0.33\textwidth}\centering
\numodelplotsetup{xcmmax=3, ycmmax=3, grid=mm-dots}%
\def\xmin{0}\def\xmax{10}\def\ymin{0}\def\ymax{5}%
\def\xlabelqty{t}\def\xlabelunit{\s}%
\def\ylabelqty{v}\def\ylabelunit{\m\per\s}%
\drawplot{\addplot[domain=\xmin:\xmax,thick]{0.5*x};}
\caption*{\texttt{mm-dots}}
\end{subfigure}\hspace{-1cm}%
\begin{subfigure}{0.33\textwidth}\centering
\numodelplotsetup{xcmmax=3, ycmmax=3, grid=none}%
\def\xmin{0}\def\xmax{10}\def\ymin{0}\def\ymax{5}%
\def\xlabelqty{t}\def\xlabelunit{\s}%
\def\ylabelqty{v}\def\ylabelunit{\m\per\s}%
\drawplot{\addplot[domain=\xmin:\xmax,thick]{0.5*x};}
\caption*{\texttt{none}}
\end{subfigure}\hspace{-1cm}%
\begin{subfigure}{0.33\textwidth}\centering
\numodelplotsetup{xcmmax=3, ycmmax=3,
  grid={grid=major, grid style={gray, very thin}}}%
\def\xmin{0}\def\xmax{10}\def\ymin{0}\def\ymax{5}%
\def\xlabelqty{t}\def\xlabelunit{\s}%
\def\ylabelqty{v}\def\ylabelunit{\m\per\s}%
\drawplot{\addplot[domain=\xmin:\xmax,thick]{0.5*x};}
\caption*{\texttt{\{major,gray\}}}
\end{subfigure}
\end{plotexample}
\numodelplotsetup{grid=mm-dots}%

\subsection{PGFPlots styles}

The package defines three PGFPlots styles applied by |\drawplot|:
|numodel/grid|, |numodel/ticks|, |numodel/axis|.  These can be
overridden wholesale through |\pgfplotsset{numodel/axis/.style={...}}|
from the calling preamble, giving projects a single choke point for
house-style customisation.  One override per style, on the same
plot:

\begin{plotexample}
\captionsetup{type=figure}%
\begin{subfigure}{0.33\textwidth}\centering
\pgfplotsset{numodel/grid/.style={grid=major,
  grid style={gray!50, very thin}}}%
\numodelplotsetup{xcmmax=3, ycmmax=3}%
\def\xmin{0}\def\xmax{10}\def\ymin{0}\def\ymax{5}%
\def\xlabelqty{t}\def\xlabelunit{\s}%
\def\ylabelqty{v}\def\ylabelunit{\m\per\s}%
\drawplot{\addplot[domain=\xmin:\xmax,thick]{0.5*x};}
\caption*{\texttt{numodel/grid}}
\end{subfigure}\hspace{-1cm}%
\begin{subfigure}{0.33\textwidth}\centering
\pgfplotsset{numodel/ticks/.style={tick style={red, thick},
  minor tick num=4}}%
\numodelplotsetup{xcmmax=3, ycmmax=3}%
\def\xmin{0}\def\xmax{10}\def\ymin{0}\def\ymax{5}%
\def\xlabelqty{t}\def\xlabelunit{\s}%
\def\ylabelqty{v}\def\ylabelunit{\m\per\s}%
\drawplot{\addplot[domain=\xmin:\xmax,thick]{0.5*x};}
\caption*{\texttt{numodel/ticks}}
\end{subfigure}\hspace{-1cm}%
\begin{subfigure}{0.33\textwidth}\centering
\pgfplotsset{numodel/axis/.append style={axis line style={->}}}%
\numodelplotsetup{xcmmax=3, ycmmax=3}%
\def\xmin{0}\def\xmax{10}\def\ymin{0}\def\ymax{5}%
\def\xlabelqty{t}\def\xlabelunit{\s}%
\def\ylabelqty{v}\def\ylabelunit{\m\per\s}%
\drawplot{\addplot[domain=\xmin:\xmax,thick]{0.5*x};}
\caption*{\texttt{numodel/axis}}
\end{subfigure}
\end{plotexample}

\subsection{Tick label halos}

When |\calcplotdims| places an axis \emph{through the middle} of the
plot (the data straddles zero), grid lines and plotted curves pass
behind the scale numbers.  To keep those numbers readable, every
tick label on such an axis is backed by a semi-transparent halo that
follows the character outlines: the label is printed twice, first
stroke-only --- a fat pen (|1.6pt|, round joins, 80\,\% opacity, via
the \textsf{pdfrender} package) traces the glyph outlines --- and
then the normal label on top.  Between and around the digits the
grid and the curves run through untouched; there is no rectangular
patch.  So that the halo covers the curves and not only the grid,
the haloed labels are lifted onto the pgfplots layer
|axis descriptions|, above the |main| layer carrying the curves
(|set layers| is activated automatically in these cases).

An axis placed on the plot edge (the default bottom/left, or the
|top|/|right| placements used for single-sign data) leaves its
labels on the white margin and gets no halo.  Tick labels also stay
visible where a crossing axis would otherwise hide them
(|hide obscured x ticks=false| and the $y$-counterpart); the halo
provides the visual separation from the crossing axis line.

The halo colour is controlled by the setup key |halo-color|.  Its
default, |halo-color=auto|, matches the halo to the surrounding
background at every |\drawplot|: inside a \textsf{tcolorbox} it takes
the box's |colback| (this manual's |plotexample| boxes rely on
exactly that), on a \textsf{beamer} slide it takes the colour theme's
|bg|, on a page coloured through the \textsf{pagecolor} package it
takes |\thepagecolor|, and otherwise it falls back to white.  (Plain
|\pagecolor| without the \textsf{pagecolor} package leaves no
readable record, so it cannot be detected.)  A colour value pins the
halo instead:
\begin{quote}
\begin{verbatim}
\numodelplotsetup{halo-color=<your-color>}  % pin
\numodelplotsetup{halo-color=auto}          % re-enable matching
\end{verbatim}
\end{quote}
The resolved colour is exposed as the named colour |numodelhalo|.
The mechanism itself lives in the styles |numodel/xticklabel halo|
and |numodel/yticklabel halo|; redefining them to be empty
(|\pgfplotsset{numodel/xticklabel halo/.style={}}| and the
$y$-counterpart) removes the halos entirely.

\section{Public API}

\DescribeMacro{\drawplot}
Renders a |tikzpicture| containing an |axis| whose body is the
single mandatory argument.  Typically a block of |\addplot| and
|\addlegendentry| lines.  Calls |\calcplotdims| internally, so the
user does not need to invoke it separately.

\DescribeMacro{\calcplotdims}
Reads |\xmin|, |\xmax|, |\ymin|, |\ymax|, and (if set)
|\xlabelqty|/|\xlabelunit|/|\ylabelqty|/|\ylabelunit|.  Writes
|\xcm|, |\ycm|, |\xtickdistance|, |\ytickdistance|, |\xlabel|,
|\ylabel|, and may rewrite |\xmin|\ldots|\ymax| to align with the
tick lattice (floor/ceil to the nearest tick).  It also appends
axis-positioning styles to |numodel/axis| based on which quadrants
the data occupies.  |\drawplot| invokes it automatically; expose for
advanced cases where dimensions are needed before rendering (overlay
TikZ, custom |axis| environment).

\DescribeMacro{\xlabelqty}\DescribeMacro{\xlabelunit}
\DescribeMacro{\ylabelqty}\DescribeMacro{\ylabelunit}
Input hooks for automatic label construction.  |\xlabelqty| is the
mathematical quantity symbol (e.g.\ |v|, |F|, |E|); the corresponding
|\xlabelunit| is a bare \textsf{siunitx} unit macro sequence
(e.g.\ |\m\per\s|, |\J|, |\N\m|) \emph{without} a surrounding |\si{}|
or |\qty{}| wrapper.

\DescribeMacro{\xcmmax}\DescribeMacro{\ycmmax}
Maximum axis dimensions in centimetres.  Can be set directly through
|\def| for backwards compatibility, or via |\numodelplotsetup|.

\DescribeMacro{\qtyPlain}
Like \textsf{siunitx}'s |\qty| but prints no numeric mantissa when
the number has come out as exactly~1 with no exponent left over: the
unit then stands alone, without the |quantity-product| symbol that
would otherwise separate the two.  Used internally to inject scale
factors into axis labels; exposed because the same need recurs in
other scaled-axis contexts.

All three \textsf{siunitx} |prefix-mode| values are supported, and
everything |\qtyPlain| does not special-case --- a number carrying an
uncertainty, a blank number, |parse-numbers=false| --- is passed to
|\qty| unchanged.  A mantissa of~1 that is preceded by a sign or a
comparator (|-1|, |<1|) keeps its digit.

\DescribeMacro{\pzuIfUnitNonEngTF}
Boolean conditional testing whether a unit macro sequence carries a
power of ten that is not a multiple of three --- |\centi|, |\deci|,
|\deca|, |\hecto| and their \textsf{siunitx} abbreviations |\cm|,
|\dm|, |\hL|, \ldots\ %
Used internally to suppress scaling on units where the user has
already encoded the order of magnitude; exposed for completeness.

The verdict is the net power of ten \textsf{siunitx} itself extracts
from the whole unit expression, so it accounts for unit powers and
for prefixes that cancel: |\centi\metre\squared| is $10^{-4}$ and
counts as non-engineering, while |\centi\metre\cubed| ($10^{-6}$)
and |\centi\metre\per\centi\second| ($10^{0}$) do not.

\section{Requirements}

\textsf{numodel-plot} requires \textsf{expl3}, \textsf{xparse},
\textsf{l3keys2e}, \textsf{siunitx} (mandatory, for quantities in
labels), and \textsf{pgfplots} (with the |fillbetween| library).
LuaLaTeX is not required at the plot layer (it is required by the
sibling \textsf{numodel} package).

\textsf{siunitx} is requested with a date of 2023-11-06 (v3.3.8).
Everything the axis labels call has been public since \textsf{siunitx}
v3.0.0, but two fixes in v3.3.7 and v3.3.8 --- the empty-exponent
case of |prefix-mode=combine-exponent| and the printing of a bare~1
under |print-unity-mantissa=false| --- are what keeps a scaled label
correct.  An older \textsf{siunitx} produces a \LaTeX{} warning
rather than an error, and the labels may come out subtly wrong.

\end{document}
