% numodel-plot.dtx % % Docstrip source for numodel-plot.sty. % Run % tex numodel-plot.ins % to extract the derived file. User-facing documentation lives % in numodel-plot-manual.tex (a stand-alone LaTeX file). % % Copyright (C) 2026 Paul Zuurbier % % This work may be distributed and/or modified under the conditions % of the LaTeX Project Public License, either version 1.3c of this % license or (at your option) any later version. The latest version % of this license is in https://www.latex-project.org/lppl.txt % % This work has the LPPL maintenance status 'maintained'. % The Current Maintainer of this work is Paul Zuurbier. % % This work consists of the files numodel-plot.dtx and numodel-plot.ins, % and the derived file numodel-plot.sty. % \section{Implementation} % % All internal helper macros use the \texttt{\textbackslash nmp@\ldots} % prefix (standard LaTeX internal convention). Because the package % may be loaded through |\input| as well as |\usepackage|, an % explicit |\makeatletter| wraps the body so that |@| is a letter % regardless of caller. % % \begin{macrocode} \NeedsTeXFormat{LaTeX2e} \ProvidesPackage{numodel-plot}[2026/09/07 v0.9.1 Auto-sizing PGFPlots engine] \makeatletter \RequirePackage{expl3} \RequirePackage{xparse} \RequirePackage{l3keys2e} % \qtyPlain drives siunitx through its code-level interface. Every % function it uses has been public since siunitx v3.0.0 (2021-05-17), % but the axis labels also rely on `print-unity-mantissa = false', % whose handling of a bare 1 was corrected in v3.3.8, and on the % empty-exponent case of `prefix-mode = combine-exponent', fixed in % v3.3.7. Hence the v3.3.8 date: with anything older LaTeX warns % rather than letting the labels come out subtly wrong. \RequirePackage{siunitx}[2023-11-06] \RequirePackage{pgfplots} \RequirePackage{pdfrender} \usepgfplotslibrary{fillbetween} \pgfplotsset{ compat=1.18, numodel/grid/.style={ grid=both, grid style={ line width=0.5pt, draw=black, line cap=round, dash pattern=on 0pt off 1mm }, major grid style={ line width=0.5pt, draw=black, line cap=round, dash pattern=on 0pt off 1mm} }, numodel/ticks/.style={ tick style={black} }, % Semi-transparent white halo behind tick labels. Only applied to an % axis that \calcplotdims draws *through the middle* of the plot, where % curves pass behind the numbers; edge-placed axes leave their labels on % the white margin and get no halo (see \calcplotdims). % % The halo follows the glyph outlines instead of boxing the label in a % rectangle: the label text is wrapped in \nmp@ticklabelhalo (defined % below), which emits it twice -- first stroke-only with a fat white % pen (the halo), then normally on top of it. \axisdefaultticklabel % is pgfplots' stock label body, so number formatting and tick % scaling stay exactly as without the halo. % % The halo has to cover the curves as well as the grid, so the haloed % labels are lifted onto the `axis descriptions' layer, which sits % above `main' (the curves) while the grid stays on `axis grid' below % them. This only takes effect when layered graphics is active; % \calcplotdims switches that on (`set layers') in the same branches % that apply these styles. numodel/xticklabel halo/.style={ xticklabel={\nmp@ticklabelhalo{\axisdefaultticklabel}}, xticklabel style={/pgfplots/on layer=axis descriptions}}, numodel/yticklabel halo/.style={ yticklabel={\nmp@ticklabelhalo{\axisdefaultticklabel}}, yticklabel style={/pgfplots/on layer=axis descriptions}}, numodel/axis/.style={ xticklabel style={/pgf/number format/.cd, fixed, precision=6, fixed zerofill=false, use comma, 1000 sep={\,}, /tikz/.cd, text depth=0.3ex, inner sep=1pt}, yticklabel style={/pgf/number format/.cd, fixed, precision=6, fixed zerofill=false, use comma, 1000 sep={\,}, /tikz/.cd, text depth=0.3ex, inner sep=1pt}, hide obscured x ticks=false, hide obscured y ticks=false, axis lines=left, xlabel near ticks, ylabel near ticks, ylabel style={rotate=-90}, axis line style={-} } } % Glyph-following halo used by the `numodel/...ticklabel halo' styles. % The label is emitted twice: first with pdfrender's stroke-only text % rendering mode -- a fat pen traces the glyph outlines, at 80% % opacity so grid and curves still shimmer through in the halo -- and % then the untouched original on top. The \rlap keeps the halo copy % out of the width, so tick-label spacing stays identical to the % halo-less labels on edge-placed axes. % % The halo colour is the named colour `numodelhalo'. It is governed % by the `halo-color' setup key: an explicit colour pins it, the % default `auto' re-resolves it at every \drawplot against the % surrounding background. LaTeX has no universal "what is behind me" % query, so `auto' probes the places where common environments % register their background, cascading from general to specific -- % each later probe overrides the earlier ones when it applies: % % white -- base fallback (LaTeX's plain \pagecolor leaves % no readable trace); % \thepagecolor -- the `pagecolor' package's record of \pagecolor; % bg -- beamer's background colour of the current % colour theme; % tcbcolback -- the current tcolorbox's background, but only % when actually inside a box: tcolorbox keeps the % (documented) counter `tcblayer' at the current % box nesting depth, and tcbcolback itself is % already defined at load time, so definedness % alone would misfire outside boxes. \colorlet{numodelhalo}{white} \newif\ifnmp@haloauto \nmp@haloautotrue \newcommand{\nmp@resolvehalocolor}{% \ifnmp@haloauto \colorlet{numodelhalo}{white}% \ifdefined\thepagecolor \colorlet{numodelhalo}{\thepagecolor}% \fi \ifcsname\string\color@bg\endcsname \colorlet{numodelhalo}{bg}% \fi \ifcsname c@tcblayer\endcsname \ifnum\value{tcblayer}>0 \colorlet{numodelhalo}{tcbcolback}% \fi \fi \fi } \newcommand{\nmp@ticklabelhalo}[1]{% \rlap{% \pgfsetstrokeopacity{0.8}% \textpdfrender{TextRenderingMode=Stroke, LineWidth=1.6pt,% LineJoinStyle=Round, LineCapStyle=Round, StrokeColor=numodelhalo}{#1}% \pgfsetstrokeopacity{1}% }% #1% } \ExplSyntaxOn \tl_new:N \g__numodelplot_axislabel_tl \tl_gset:Nn \g__numodelplot_axislabel_tl {ieee} \providecommand{\xcmmax}{12} \providecommand{\ycmmax}{10} \keys_define:nn { numodel-plot } { axis-label-format .choice:, axis-label-format / iso .code:n = \tl_gset:Nn \g__numodelplot_axislabel_tl {iso}, axis-label-format / ieee .code:n = \tl_gset:Nn \g__numodelplot_axislabel_tl {ieee}, axis-label-format / brackets .code:n = \tl_gset:Nn \g__numodelplot_axislabel_tl {brackets}, axis-label-format / qty-only .code:n = \tl_gset:Nn \g__numodelplot_axislabel_tl {qty-only}, axis-label-format / unit-only .code:n = \tl_gset:Nn \g__numodelplot_axislabel_tl {unit-only}, grid .choice:, grid / mm-dots .code:n = \pgfplotsset{ numodel/grid/.style={ grid=both, grid~style={line~width=0.5pt, draw=black, line~cap=round, dash~pattern=on~0pt~off~1mm}, major~grid~style={line~width=0.5pt, draw=black, line~cap=round, dash~pattern=on~0pt~off~1mm} } }, grid / none .code:n = \pgfplotsset{ numodel/grid/.style={grid=none} }, grid / unknown .code:n = \pgfplotsset{ numodel/grid/.style={#1} }, xcmmax .code:n = \def\xcmmax{#1}, ycmmax .code:n = \def\ycmmax{#1}, % Colour of the tick-label halo. `auto' (the default) matches the % surrounding background at each \drawplot (tcolorbox, beamer, % pagecolor -- see \nmp@resolvehalocolor); any other value is a % colour expression that pins the halo to that colour. halo-color .code:n = \str_if_eq:nnTF {#1} {auto} { \nmp@haloautotrue } { \nmp@haloautofalse \colorlet{numodelhalo}{#1} }, % How a scaled axis presents its power of ten in the axis label: % `prefix' (the default) folds it into an SI prefix on the unit -- % t (ks) instead of t (10^3 s) -- falling back to the exponent % form whenever no engineering prefix fits (see % \__numodelplot_prefix_check:nn); `exponent' always shows the % power of ten. scale-format .choice:, scale-format / prefix .code:n = \tl_gset:Nn \g__numodelplot_scaleformat_tl {prefix}, scale-format / exponent .code:n = \tl_gset:Nn \g__numodelplot_scaleformat_tl {exponent}, } \NewDocumentCommand{\numodelplotsetup}{m} { \keys_set:nn { numodel-plot } {#1} } \ExplSyntaxOff \ExplSyntaxOn % \qtyPlain[]{}{} % % Like siunitx's \qty, except that a mantissa of exactly 1 with no % exponent left over prints as the bare unit ("m", not "1 m") -- the % form an axis label wants when the scale factor has been absorbed % into the unit's own prefix. % % Up to v0.8.0 this was done by overriding two siunitx internals % (\__siunitx_number_output_integer:nnn and % \__siunitx_quantity_print_unit:n) and reading four internal % variables. The investigation behind siunitx issue #864 established % that siunitx's documented code-level interface already exposes the % whole pipeline, so nothing below reaches into siunitx internals. % % The pipeline mirrors \__siunitx_quantity_parsed:nn: parse the % number, run the stage selected by prefix-mode, then print. All % three prefix-mode values are handled, so \qtyPlain remains a % drop-in replacement for \qty rather than being tied to % extract-exponent. \tl_new:N \l__nmp_qty_number_tl \tl_new:N \l__nmp_qty_unit_tl \tl_new:N \l__nmp_qty_input_tl \tl_new:N \l__nmp_qty_rawunit_tl \fp_new:N \l__nmp_qty_exponent_fp % prefix-mode = extract-exponent: strip the SI prefixes off the unit % and fold their net power of ten into the number, then process % (rounding, exponent-mode, ...). The order matters: prefixes come % off before the exponent is adjusted, processing runs last. \cs_new_protected:cpn { __nmp_qty_prefix_extract-exponent:n } #1 { \siunitx_unit_format_extract_prefixes:nNN {#1} \l__nmp_qty_unit_tl \l__nmp_qty_exponent_fp \tl_set:Ne \l__nmp_qty_number_tl { \siunitx_number_adjust_exponent:Nn \l__nmp_qty_number_tl \l__nmp_qty_exponent_fp } \siunitx_number_process:NN \l__nmp_qty_number_tl \l__nmp_qty_number_tl } % prefix-mode = combine-exponent: process first, then lift the % exponent out of the number and hand it to the unit formatter, which % turns it into an SI prefix. \cs_new_protected:cpn { __nmp_qty_prefix_combine-exponent:n } #1 { \siunitx_number_process:NN \l__nmp_qty_number_tl \l__nmp_qty_number_tl \exp_after:wN \__nmp_qty_drop_exponent:nnnnnnn \l__nmp_qty_number_tl \siunitx_unit_format_combine_exponent:nnN {#1} \l__nmp_qty_exponent_fp \l__nmp_qty_unit_tl } \cs_new_protected:Npn \__nmp_qty_drop_exponent:nnnnnnn #1#2#3#4#5#6#7 { \fp_set:Nn \l__nmp_qty_exponent_fp { #6 0#7 } \tl_set:Nn \l__nmp_qty_number_tl { {#1} {#2} {#3} {#4} {#5} { } { } } } % prefix-mode = input: prefixes stay where the user wrote them. \cs_new_protected:Npn \__nmp_qty_prefix_input:n #1 { \siunitx_number_process:NN \l__nmp_qty_number_tl \l__nmp_qty_number_tl \siunitx_unit_format:nN {#1} \l__nmp_qty_unit_tl } % The prefix-mode currently in force, as a string. % % siunitx v3.5.0 renamed the variable holding it from % \l_siunitx_quantity_prefix_mode_tl -- a documented public variable % up to and including v3.4.14 -- to % \l_siunitx_quantity_prefix_mode_str, and left the old name % declared but no longer assigned. Reading the stale one therefore % yields an *empty* mode and no error at all, and a \cs_if_exist % guard does not catch it either: on siunitx 3.5 and newer, v0.9.0 of % this package silently degraded every \qtyPlain to a plain \qty, so % a scaled axis label read `t (1 ks)' instead of `t (ks)'. % % Prefer the str, fall back to the tl, and leave the result empty if % neither carries anything -- an empty mode matches no % \__nmp_qty_prefix_...:n and so takes the \qty branch below, which is % what an unknown mode should do anyway. \cs_new:Npn \__nmp_qty_mode: { \bool_lazy_and:nnTF { \cs_if_exist_p:N \l_siunitx_quantity_prefix_mode_str } { ! \tl_if_empty_p:N \l_siunitx_quantity_prefix_mode_str } { \tl_use:N \l_siunitx_quantity_prefix_mode_str } { \cs_if_exist:NT \l_siunitx_quantity_prefix_mode_tl { \tl_use:N \l_siunitx_quantity_prefix_mode_tl } } } % #1-#7 are the seven groups \siunitx_number_parse:nN produces -- % comparator, sign, integer, decimal, uncertainty, exponent sign, % exponent -- and #8 is the unit. A number carrying an uncertainty % is handed straight to \qty: separate-uncertainty-units has three % layouts that are not worth reproducing here, and such a number % never reduces to the bare mantissa 1 that \qtyPlain exists to % suppress. An unrecognised or unreadable prefix-mode falls back the % same way. \cs_new_protected:Npn \__nmp_qty_parsed:nnnnnnnn #1#2#3#4#5#6#7#8 { \bool_lazy_and:nnTF { \tl_if_blank_p:n {#5} } { \cs_if_exist_p:c { __nmp_qty_prefix_ \__nmp_qty_mode: :n } } { \use:c { __nmp_qty_prefix_ \__nmp_qty_mode: :n } {#8} \exp_after:wN \__nmp_qty_output:nnnnnnn \l__nmp_qty_number_tl } { \__nmp_qty_fallback: } } % The one thing \qtyPlain does differently from \qty: a number that % has boiled down to a bare 1 prints as nothing at all, leaving the % unit to stand alone -- and with it goes the quantity-product symbol % that would otherwise separate the two. \cs_new_protected:Npn \__nmp_qty_output:nnnnnnn #1#2#3#4#5#6#7 { \bool_lazy_all:nTF { { \tl_if_blank_p:n {#1} } % no comparator { \tl_if_blank_p:n {#2} } % no sign { \str_if_eq_p:nn {#3} { 1 } } % integer part is exactly 1 { \tl_if_blank_p:n {#4} } % no decimal part { \tl_if_blank_p:n {#5} } % no uncertainty { \fp_compare_p:n { #6 0#7 = 0 } } % exponent absent or zero } { \siunitx_print_unit:V \l__nmp_qty_unit_tl } { \siunitx_quantity_print:eV { \siunitx_number_output:N \l__nmp_qty_number_tl } \l__nmp_qty_unit_tl } } % Everything \qtyPlain does not special-case goes to \qty unchanged. % The options are already in force in the enclosing group, so they % need not be passed again. \cs_new_protected:Npn \__nmp_qty_fallback: { \exp_args:NVV \qty \l__nmp_qty_input_tl \l__nmp_qty_rawunit_tl } \NewDocumentCommand{\qtyPlain}{O{} m m} { \group_begin: \tl_set:Nn \l__nmp_qty_input_tl {#2} \tl_set:Nn \l__nmp_qty_rawunit_tl {#3} \keys_set:nn { siunitx } {#1} \siunitx_unit_options_apply:n {#3} \bool_lazy_and:nnTF { \l_siunitx_number_parse_bool } { ! \tl_if_blank_p:n {#2} } { \siunitx_number_parse:nN {#2} \l__nmp_qty_number_tl \tl_if_empty:NTF \l__nmp_qty_number_tl { \__nmp_qty_fallback: } { \exp_after:wN \__nmp_qty_parsed:nnnnnnnn \l__nmp_qty_number_tl {#3} } } { \__nmp_qty_fallback: } \group_end: } \ExplSyntaxOff \ExplSyntaxOn % \pzuIfUnitNonEngTF{}{}{} % % True when the unit carries a power of ten that is not a multiple of % three -- cm, dm, daN, hPa and friends. Such a unit already encodes % its own order of magnitude, so the label builders leave the axis % unscaled rather than stacking a second power of ten on top of it. % % The power of ten is the one siunitx itself extracts, i.e. the net % exponent of the whole unit expression. It therefore sees through % abbreviations (\cm), through powers (\cm\squared is 10^{-4}, still % non-engineering, while \cm\cubed is 10^{-6}, which is not) and % through cancellation (\cm\per\cs is 10^{0}). The hard-coded token % list this replaces could do none of that, and roughly half of the % control sequences it named (\cg, \dam, \hPa, ...) are not defined % by siunitx at all. % % The argument is expanded once, as it always was, so call sites pass % \xlabelunit rather than its contents. \tl_new:N \l__nmp_noneng_unit_tl \fp_new:N \l__nmp_noneng_exponent_fp \prg_new_protected_conditional:Npnn \__nmp_unit_noneng:n #1 { TF } { \group_begin: \siunitx_unit_options_apply:n {#1} \siunitx_unit_format_extract_prefixes:nNN {#1} \l__nmp_noneng_unit_tl \l__nmp_noneng_exponent_fp \fp_compare:nNnTF { \l__nmp_noneng_exponent_fp - 3 * floor ( \l__nmp_noneng_exponent_fp / 3 ) } = \c_zero_fp { \group_end: \prg_return_false: } { \group_end: \prg_return_true: } } \prg_generate_conditional_variant:Nnn \__nmp_unit_noneng:n { o } { TF } \NewDocumentCommand{\pzuIfUnitNonEngTF}{m m m} { \__nmp_unit_noneng:oTF {#1} {#2} {#3} } \ExplSyntaxOff \ExplSyntaxOn \cs_new_protected:Npn \__numodelplot_xlabel_emit_unscaled: { \str_case:Vn \g__numodelplot_axislabel_tl { {iso} { \def \xlabel { $\nmp@savedxlabelqty \, /\, ( \si{\nmp@savedxlabelunit} )$ } } {ieee} { \def \xlabel { $\nmp@savedxlabelqty \,( \si{\nmp@savedxlabelunit} )$ } } {brackets} { \def \xlabel { $\nmp@savedxlabelqty \,[ \si{\nmp@savedxlabelunit} ]$ } } {qty-only} { \def \xlabel { $\nmp@savedxlabelqty$ } } {unit-only} { \def \xlabel { $\si{\nmp@savedxlabelunit}$ } } } } \cs_new_protected:Npn \__numodelplot_xlabel_emit_scaled: { \str_case:Vn \g__numodelplot_axislabel_tl { {iso} { \edef\xlabel{\noexpand$\noexpand\nmp@savedxlabelqty \noexpand\,/\noexpand\,(\noexpand\qtyPlain [evaluate-expression=false, round-mode=none, prefix-mode=\g__numodelplot_prefixmode_tl, extract-mass-in-kilograms=true, print-zero-exponent=false, print-unity-mantissa=false]% {1e\nmp@xlabelexpuse}{\noexpand\nmp@savedxlabelunit})\noexpand$}% } {ieee} { \edef\xlabel{\noexpand$\noexpand\nmp@savedxlabelqty \noexpand\,(\noexpand\qtyPlain [evaluate-expression=false, round-mode=none, prefix-mode=\g__numodelplot_prefixmode_tl, extract-mass-in-kilograms=true, print-zero-exponent=false, print-unity-mantissa=false]% {1e\nmp@xlabelexpuse}{\noexpand\nmp@savedxlabelunit})\noexpand$}% } {brackets} { \edef\xlabel{\noexpand$\noexpand\nmp@savedxlabelqty \noexpand\,[\noexpand\qtyPlain [evaluate-expression=false, round-mode=none, prefix-mode=\g__numodelplot_prefixmode_tl, extract-mass-in-kilograms=true, print-zero-exponent=false, print-unity-mantissa=false]% {1e\nmp@xlabelexpuse}{\noexpand\nmp@savedxlabelunit}]\noexpand$}% } {qty-only} { \def \xlabel { $\nmp@savedxlabelqty$ } } {unit-only} { \edef\xlabel{\noexpand$\noexpand\qtyPlain [evaluate-expression=false, round-mode=none, prefix-mode=\g__numodelplot_prefixmode_tl, extract-mass-in-kilograms=true, print-zero-exponent=false, print-unity-mantissa=false]% {1e\nmp@xlabelexpuse}{\noexpand\nmp@savedxlabelunit}\noexpand$}% } } } \cs_new_protected:Npn \__numodelplot_ylabel_emit_unscaled: { \str_case:Vn \g__numodelplot_axislabel_tl { {iso} { \def \ylabel { $\nmp@savedylabelqty \, /\, ( \si{\nmp@savedylabelunit} )$ } } {ieee} { \def \ylabel { $\nmp@savedylabelqty \,( \si{\nmp@savedylabelunit} )$ } } {brackets} { \def \ylabel { $\nmp@savedylabelqty \,[ \si{\nmp@savedylabelunit} ]$ } } {qty-only} { \def \ylabel { $\nmp@savedylabelqty$ } } {unit-only} { \def \ylabel { $\si{\nmp@savedylabelunit}$ } } } } \cs_new_protected:Npn \__numodelplot_ylabel_emit_scaled: { \str_case:Vn \g__numodelplot_axislabel_tl { {iso} { \edef\ylabel{\noexpand$\noexpand\nmp@savedylabelqty \noexpand\,/\noexpand\,(\noexpand\qtyPlain [evaluate-expression=false, round-mode=none, prefix-mode=\g__numodelplot_prefixmode_tl, extract-mass-in-kilograms=true, print-zero-exponent=false, print-unity-mantissa=false]% {1e\nmp@ylabelexpuse}{\noexpand\nmp@savedylabelunit})\noexpand$}% } {ieee} { \edef\ylabel{\noexpand$\noexpand\nmp@savedylabelqty \noexpand\,(\noexpand\qtyPlain [evaluate-expression=false, round-mode=none, prefix-mode=\g__numodelplot_prefixmode_tl, extract-mass-in-kilograms=true, print-zero-exponent=false, print-unity-mantissa=false]% {1e\nmp@ylabelexpuse}{\noexpand\nmp@savedylabelunit})\noexpand$}% } {brackets} { \edef\ylabel{\noexpand$\noexpand\nmp@savedylabelqty \noexpand\,[\noexpand\qtyPlain [evaluate-expression=false, round-mode=none, prefix-mode=\g__numodelplot_prefixmode_tl, extract-mass-in-kilograms=true, print-zero-exponent=false, print-unity-mantissa=false]% {1e\nmp@ylabelexpuse}{\noexpand\nmp@savedylabelunit}]\noexpand$}% } {qty-only} { \def \ylabel { $\nmp@savedylabelqty$ } } {unit-only} { \edef\ylabel{\noexpand$\noexpand\qtyPlain [evaluate-expression=false, round-mode=none, prefix-mode=\g__numodelplot_prefixmode_tl, extract-mass-in-kilograms=true, print-zero-exponent=false, print-unity-mantissa=false]% {1e\nmp@ylabelexpuse}{\noexpand\nmp@savedylabelunit}\noexpand$}% } } } \ExplSyntaxOff \newcommand{\pznmpAppendXScaleTicks}[1]{% \pgfplotsset{numodel/axis/.append style={scaled x ticks=base 10:#1}}% } \newcommand{\pznmpAppendYScaleTicks}[1]{% \pgfplotsset{numodel/axis/.append style={scaled y ticks=base 10:#1}}% } \newcommand{\pznmpSuppressXScaleLabel}{% \pgfplotsset{numodel/axis/.append style={xtick scale label code/.code={}}}% } \newcommand{\pznmpSuppressYScaleLabel}{% \pgfplotsset{numodel/axis/.append style={ytick scale label code/.code={}}}% } \ExplSyntaxOn \cs_new_protected:Npn \__numodelplot_suppress_xscale_label: { \str_if_eq:VnF \g__numodelplot_axislabel_tl {qty-only} { \pznmpSuppressXScaleLabel } } \cs_new_protected:Npn \__numodelplot_suppress_yscale_label: { \str_if_eq:VnF \g__numodelplot_axislabel_tl {qty-only} { \pznmpSuppressYScaleLabel } } \ExplSyntaxOff \ExplSyntaxOn % How the power of ten of a scaled axis reaches the axis label. % \g__numodelplot_scaleformat_tl holds the user's wish (`prefix' or % `exponent', set through the scale-format key); the label builders % translate that per axis into \g__numodelplot_prefixmode_tl, the % actual siunitx prefix-mode value spliced into the \qtyPlain calls of % the emit-scaled routines: `combine-exponent' folds the exponent into % an SI prefix on the unit, `extract-exponent' keeps the 10^k form. \tl_new:N \g__numodelplot_scaleformat_tl \tl_gset:Nn \g__numodelplot_scaleformat_tl {prefix} \tl_new:N \g__numodelplot_prefixmode_tl \tl_gset:Nn \g__numodelplot_prefixmode_tl {extract-exponent} % Can 10^{#1} be folded into an SI prefix on unit #2? siunitx's % combine-exponent divides the exponent by the power of the unit's % first atom and adds any existing prefix; if the result is not in its % prefix table it raises an *error*, and results like 10^1 (deca) % would be legal SI but unwanted engineering-wise. So this check % mirrors that computation via the siunitx parser and additionally % requires a multiple of 3: 10^6 m^2 -> km^2 passes, 10^3 m^2 (k=1.5) % and 10^3 m^3 (deca) fall back to the exponent form. Everything % lives behind \cs_if_exist guards because it reaches into siunitx % internals; if a future siunitx renames them the check just returns % false and the label keeps the always-safe 10^k form. % % This is now the only place in the package that touches siunitx % internals, and it is the one thing siunitx issue #864 asks for that % is genuinely unreachable today: \siunitx_unit_format_extract_- % prefixes:nNN reports the net power of ten of the whole unit but not % *which* prefix sat on *which* atom, which is exactly what deciding % the fold needs. \l__siunitx_unit_parsed_prop holds it (prefix-n, % unit-n, power-n, ...); if siunitx ever exposes that, this routine % becomes internals-free like the rest. \bool_new:N \g__numodelplot_prefix_ok_bool \tl_new:N \l__numodelplot_power_tl \tl_new:N \l__numodelplot_prefix_tl \tl_new:N \l__numodelplot_target_tl \cs_new_protected:Npn \__numodelplot_prefix_check:nn #1#2 { \bool_gset_false:N \g__numodelplot_prefix_ok_bool \bool_lazy_all:nT { { \cs_if_exist_p:N \__siunitx_unit_parse:n } { \cs_if_exist_p:N \__siunitx_unit_if_symbolic:nTF } { \cs_if_exist_p:N \l_siunitx_unit_symbolic_seq } { \cs_if_exist_p:N \l__siunitx_unit_parsed_prop } { \cs_if_exist_p:N \l__siunitx_unit_prefixes_forward_prop } { \cs_if_exist_p:N \l__siunitx_unit_prefixes_reverse_prop } } { \__numodelplot_prefix_check_aux:nn {#1} {#2} } } \cs_new_protected:Npn \__numodelplot_prefix_check_aux:nn #1#2 { \group_begin: % unit macros only gain their parser meaning inside a % formatting group; replicate what siunitx's own % \__siunitx_unit_format:nNN sets up before parsing (the % symbolic test, too, only answers correctly after this) \seq_map_inline:Nn \l_siunitx_unit_symbolic_seq { \cs_set_eq:Nc ##1 { __siunitx_unit_ \token_to_str:N ##1 :w } } \__siunitx_unit_if_symbolic:nTF {#2} { \__siunitx_unit_parse:n {#2} } { \prop_clear:N \l__siunitx_unit_parsed_prop } \prop_if_empty:NF \l__siunitx_unit_parsed_prop { \prop_get:NnNF \l__siunitx_unit_parsed_prop { power-1 } \l__numodelplot_power_tl { \tl_set:Nn \l__numodelplot_power_tl { 1 } } \prop_get:NnNTF \l__siunitx_unit_parsed_prop { prefix-1 } \l__numodelplot_prefix_tl { \prop_get:NVNF \l__siunitx_unit_prefixes_forward_prop \l__numodelplot_prefix_tl \l__numodelplot_prefix_tl { \tl_set:Nn \l__numodelplot_prefix_tl { 0 } } } { \tl_set:Nn \l__numodelplot_prefix_tl { 0 } } \tl_set:Nx \l__numodelplot_target_tl { \fp_eval:n { #1 / \l__numodelplot_power_tl + \l__numodelplot_prefix_tl } } \fp_compare:nNnTF { \l__numodelplot_target_tl } = \c_zero_fp { \bool_gset_true:N \g__numodelplot_prefix_ok_bool } { % engineering only: target must be a multiple of 3 \fp_compare:nNnT { \l__numodelplot_target_tl - 3 * trunc( \l__numodelplot_target_tl / 3 ) } = \c_zero_fp { \prop_get:NVNT \l__siunitx_unit_prefixes_reverse_prop \l__numodelplot_target_tl \l__numodelplot_target_tl { \bool_gset_true:N \g__numodelplot_prefix_ok_bool } } } } \group_end: } % Pick the prefix-mode for one axis: combine-exponent when the user % asked for prefixes AND the fold is feasible, extract-exponent % otherwise. \cs_new_protected:Npn \__numodelplot_set_prefixmode:nn #1#2 { \tl_gset:Nn \g__numodelplot_prefixmode_tl {extract-exponent} \str_if_eq:VnT \g__numodelplot_scaleformat_tl {prefix} { \__numodelplot_prefix_check:nn {#1} {#2} \bool_if:NT \g__numodelplot_prefix_ok_bool { \tl_gset:Nn \g__numodelplot_prefixmode_tl {combine-exponent} } } } \NewDocumentCommand{\xlabelbuild}{}{ \let\nmp@savedxlabelqty\xlabelqty \let\nmp@savedxlabelunit\xlabelunit \pzuIfUnitNonEngTF{\xlabelunit}{ \__numodelplot_xlabel_emit_unscaled: \def\nmp@xlabelexp{0} }{ \edef\nmp@xmag{\fpeval{max(abs(\xmin),abs(\xmax))}} \edef\nmp@xlabelexp{\fpeval{ \nmp@xmag > 0 ? 3*floor(ln(\nmp@xmag)/ln(10)/3) : 0 }} \ifnum\nmp@xlabelexp=0\relax \__numodelplot_xlabel_emit_unscaled: \else \edef\nmp@xlabelexpuse{\nmp@xlabelexp} \exp_args:NnV \__numodelplot_set_prefixmode:nn {\nmp@xlabelexp} \nmp@savedxlabelunit \__numodelplot_xlabel_emit_scaled: \edef\nmp@xlabelexpneg{\fpeval{-\nmp@xlabelexp}} \pznmpAppendXScaleTicks{\nmp@xlabelexpneg} \__numodelplot_suppress_xscale_label: \fi } \global\let\xlabelqty\undefined \global\let\xlabelunit\undefined } \NewDocumentCommand{\ylabelbuild}{}{ \let\nmp@savedylabelqty\ylabelqty \let\nmp@savedylabelunit\ylabelunit \pzuIfUnitNonEngTF{\ylabelunit}{ \__numodelplot_ylabel_emit_unscaled: \def\nmp@ylabelexp{0} }{ \edef\nmp@ymag{\fpeval{max(abs(\ymin),abs(\ymax))}} \edef\nmp@ylabelexp{\fpeval{ \nmp@ymag > 0 ? 3*floor(ln(\nmp@ymag)/ln(10)/3) : 0 }} \ifnum\nmp@ylabelexp=0\relax \__numodelplot_ylabel_emit_unscaled: \else \edef\nmp@ylabelexpuse{\nmp@ylabelexp} \exp_args:NnV \__numodelplot_set_prefixmode:nn {\nmp@ylabelexp} \nmp@savedylabelunit \__numodelplot_ylabel_emit_scaled: \edef\nmp@ylabelexpneg{\fpeval{-\nmp@ylabelexp}} \pznmpAppendYScaleTicks{\nmp@ylabelexpneg} \__numodelplot_suppress_yscale_label: \fi } \global\let\ylabelqty\undefined \global\let\ylabelunit\undefined } \ExplSyntaxOff \newcommand{\calcplotdims}{% \edef\nmp@xlog{\fpeval{floor(ln(\xmax-\xmin)/ln(10))}}% \edef\nmp@ylog{\fpeval{floor(ln(\ymax-\ymin)/ln(10))}}% \edef\nmp@xS{\fpeval{(\xmax-\xmin) / (10^\nmp@xlog)}}% \edef\nmp@yS{\fpeval{(\ymax-\ymin) / (10^\nmp@ylog)}}% \edef\xcm{\fpeval{% \nmp@xS <= \xcmmax/10 ? 10 * \nmp@xS :% \nmp@xS <= \xcmmax/5 ? 5 * \nmp@xS :% \nmp@xS <= \xcmmax/4 ? 4 * \nmp@xS :% \nmp@xS <= \xcmmax/2 ? 2 * \nmp@xS :% \nmp@xS <= \xcmmax ? \nmp@xS :% \nmp@xS / 2% }}% \edef\ycm{\fpeval{% \nmp@yS <= \ycmmax/10 ? 10 * \nmp@yS :% \nmp@yS <= \ycmmax/5 ? 5 * \nmp@yS :% \nmp@yS <= \ycmmax/4 ? 4 * \nmp@yS :% \nmp@yS <= \ycmmax/2 ? 2 * \nmp@yS :% \nmp@yS <= \ycmmax ? \nmp@yS :% \nmp@yS / 2% }}% \edef\nmp@xtickdiv{\fpeval{\nmp@xS / \xcm}}% \edef\nmp@ytickdiv{\fpeval{\nmp@yS / \ycm}}% \edef\xtickdistance{\fpeval{\nmp@xtickdiv * 10^\nmp@xlog}}% \edef\ytickdistance{\fpeval{\nmp@ytickdiv * 10^\nmp@ylog}}% \edef\xmax{\fpeval{ceil(\xmax / \xtickdistance) * \xtickdistance}}% \edef\ymax{\fpeval{ceil(\ymax / \ytickdistance) * \ytickdistance}}% \edef\xmin{\fpeval{floor(\xmin / \xtickdistance) * \xtickdistance}}% \edef\ymin{\fpeval{floor(\ymin / \ytickdistance) * \ytickdistance}}% \edef\xcm{\fpeval{(\xmax - \xmin) / \xtickdistance}}% \edef\ycm{\fpeval{(\ymax - \ymin) / \ytickdistance}}% \edef\nmp@yStraddles{\fpeval{(\ymin<0 && \ymax>0) ? 1 : 0}}% \edef\nmp@yAllNonPos{\fpeval{\ymax<=0 ? 1 : 0}}% \edef\nmp@xStraddles{\fpeval{(\xmin<0 && \xmax>0) ? 1 : 0}}% \edef\nmp@xAllNonPos{\fpeval{\xmax<=0 ? 1 : 0}}% % The x-axis is drawn through the middle only when the data straddles % y=0; there the x tick labels sit on top of the curves and need the % white halo. Edge placements (top / default bottom) leave the % labels on the white margin, so no halo. \ifnum\nmp@yStraddles=1 \pgfplotsset{numodel/axis/.append style={axis x line*=middle, set layers, /pgfplots/numodel/xticklabel halo}}% \else \ifnum\nmp@yAllNonPos=1 \pgfplotsset{numodel/axis/.append style={axis x line*=top}}% \fi \fi % Likewise the y-axis only gets a halo when it is drawn through % the middle (data straddles x=0). \ifnum\nmp@xStraddles=1 \pgfplotsset{numodel/axis/.append style={axis y line*=middle, set layers, /pgfplots/numodel/yticklabel halo}}% \else \ifnum\nmp@xAllNonPos=1 \pgfplotsset{numodel/axis/.append style={axis y line*=right}}% \fi \fi \edef\nmp@axisMoved{\fpeval{(\nmp@yStraddles || \nmp@yAllNonPos || \nmp@xStraddles || \nmp@xAllNonPos) ? 1 : 0}}% \ifnum\nmp@axisMoved=1 \pgfplotsset{numodel/axis/.append style={% every x tick scale label/.style={at={(xticklabel cs:1.05)}, anchor=south west},% every y tick scale label/.style={at={(yticklabel cs:1.05)}, anchor=south east}% }}% \fi \edef\nmp@fourQuad{\fpeval{(\nmp@xStraddles && \nmp@yStraddles) ? 1 : 0}}% \ifnum\nmp@fourQuad=1 \pgfplotsset{numodel/axis/.append style={% xlabel style={at={(axis cs:\xmax,0)}, anchor=west, xshift=1em},% ylabel style={at={(axis cs:0,\ymax)}, anchor=south, yshift=1em, rotate=0}% }}% \fi \edef\nmp@qIandII{\fpeval{(\nmp@xStraddles && \ymin>=0) ? 1 : 0}}% \ifnum\nmp@qIandII=1 \pgfplotsset{numodel/axis/.append style={% ylabel style={at={(axis cs:0,\ymax)}, anchor=south, yshift=1em, rotate=0}% }}% \fi \edef\nmp@qIIandIII{\fpeval{(\nmp@yStraddles && \nmp@xAllNonPos) ? 1 : 0}}% \ifnum\nmp@qIIandIII=1 \pgfplotsset{numodel/axis/.append style={% xlabel style={at={(axis cs:\xmin,0)}, anchor=east, xshift=-1em}% }}% \fi \edef\nmp@qIIIandIV{\fpeval{(\nmp@xStraddles && \nmp@yAllNonPos) ? 1 : 0}}% \ifnum\nmp@qIIIandIV=1 \pgfplotsset{numodel/axis/.append style={% ylabel style={at={(axis cs:0,\ymin)}, anchor=north, yshift=-1em, rotate=0}% }}% \fi \edef\nmp@qIVandI{\fpeval{(\nmp@yStraddles && \xmin>=0) ? 1 : 0}}% \ifnum\nmp@qIVandI=1 \pgfplotsset{numodel/axis/.append style={% xlabel style={at={(axis cs:\xmax,0)}, anchor=west, xshift=1em}% }}% \fi \ifdefined\xlabelqty\ifdefined\xlabelunit\xlabelbuild\fi\fi \ifdefined\ylabelqty\ifdefined\ylabelunit\ylabelbuild\fi\fi } \newcommand{\drawplot}[1]{ \calcplotdims \begin{tikzpicture} % Resolve halo-color=auto against the *current* surroundings; the % tikzpicture group keeps the \colorlet local to this plot. \nmp@resolvehalocolor \begin{axis}[ numodel/grid, numodel/ticks, numodel/axis, xlabel=\xlabel, ylabel=\ylabel, xmin=\xmin, xmax=\xmax, ymin=\ymin, ymax=\ymax, xtick distance=\xtickdistance, ytick distance=\ytickdistance, width=\xcm cm, height=\ycm cm, scale only axis ] #1 \end{axis} \end{tikzpicture} } \makeatother % \end{macrocode} % % \Finale \endinput