Reports and documents#
Recipes for getting calculations into a deliverable: a PDF, a Word file, a web page or a Quarto report, with equation numbers, a title block and your colors.
Write a calculation straight to a file#
Asked in #177, #138, #128 and #203.
You don’t need Jupyter or a notebook export. HandCalcs renders a .py file (or a
string) with any renderer, and destination= writes the result to a file:
import pathlib
from handcalcs import HandCalcs
from handcalcs.renderers import LatexRenderer, HTMLRenderer, MarkdownRenderer
calc = pathlib.Path("calcs/beam.py")
HandCalcs(LatexRenderer())(calc, destination="report/beam.tex")
HandCalcs(HTMLRenderer())(calc, destination="report/beam.html")
HandCalcs(MarkdownRenderer())(calc, destination="report/beam.md")
The .tex file is a complete document that compiles with pdflatex, and the .html
file is a complete page that needs no MathJax or other JavaScript.
See Writing a file.
A PDF through LaTeX#
For a PDF of one calculation, write it with destination="beam.tex" (above) and run
pdflatex beam.tex.
To put calculations into a larger report, render each one without a destination,
save the fragment, and \input it. The fragments need amsmath, plus amssymb and
xcolor for --check marks:
pathlib.Path("report/beam.tex").write_text(HandCalcs(LatexRenderer())(calc))
\documentclass{article}
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\begin{document}
\input{beam.tex}
\end{document}
See LaTeX renderer.
A Word document#
The Word renderer writes a .docx file with native, editable Word equations:
import pathlib
from handcalcs import HandCalcs
from handcalcs.renderers import WordRenderer
HandCalcs(WordRenderer())(pathlib.Path("calcs/beam.py"), destination="beam.docx")
Install it with pip install "handcalcs[word]". It can add to an existing document
(document=) or start from your company template (template="company.docx"). See
Word renderer.
A Quarto or MyST report#
The Markdown output works in Quarto (.qmd) and MyST documents as it is. Render a
calculation in a code cell with %%render, or write the Markdown to a file and include
it. See Where it works.
Start a new page#
Asked in #233.
# hc: --pagebreak starts a new page in printed and exported output: \newpage in
LaTeX, a CSS page break in HTML and Markdown, a real page break in Word and a print page
break in Excel. On its own line it breaks before the next line; at the end of a line it
breaks after it:
## Loads
w = 5 # kN/m
L = 6 # m
# hc: --pagebreak
## Design
M = w * L**2 / 8
V = w * L / 2 # hc: --pagebreak
See Page breaks.
Number the equations#
A calc_line:post rule can append a number to each calculation. This one reads two
context fields: eq_numbers = True numbers every calculated line in order, and
label = "..." at the end of a line gives that line its own label:
"""Equation numbers on calculation lines (#218)."""
from handcalcs.renderers import LatexRenderer, MarkdownRenderer
def number_equations(renderer, rendered, node, base_context):
"""
'# hc: eq_numbers = True' numbers every calculated line (1), (2), ...
'x = ... # hc: label = "4.2"' gives one line its own number.
"""
commands = getattr(node.comment, "commands", None) or {}
label = commands.get("label")
# A parameter (target = value) has one assign symbol; a calculation has two or more.
is_param = isinstance(rendered, list) and rendered.count(base_context.current.assign) < 2
if label is None and getattr(base_context.current, "eq_numbers", False) and not is_param:
count = getattr(base_context.global_context, "eq_count", 0) + 1
base_context.global_context.eq_count = count
label = count
if label is None:
return rendered
tag = f"\\qquad ({label})"
if isinstance(rendered, list):
# Tag the result, which is right after the last assign symbol (a comment may follow it).
assign = base_context.current.assign
result = max(i for i, part in enumerate(rendered) if part == assign) + 1
rendered[result] = rendered[result] + tag
return rendered
return rendered + tag # a multi-line (# hc: -m) calc is one string
class NumberedMarkdownRenderer(MarkdownRenderer):
name = "numbered_markdown"
class NumberedLatexRenderer(LatexRenderer):
name = "numbered_latex"
NumberedMarkdownRenderer.register("calc_line:post")(number_equations)
NumberedLatexRenderer.register("calc_line:post")(number_equations)
# hc: eq_numbers = True
w = 5 # kN/m
L = 6 # m
M = w * L**2 / 8 # kN m
V = w * L / 2 # kN
R_A = V # hc: label = "B.1"
$$\begin{aligned} w &= 5 \quad\text{(kN/m)} \\ L &= 6 \quad\text{(m)} \\ M &= \frac{w \cdot L^{2}}{8} = \frac{5 \cdot 6^{2}}{8} = 22.5\qquad (1) \\ &\quad\text{(kN m)} \\ V &= \frac{w \cdot L}{2} = \frac{5 \cdot 6}{2} = 15\qquad (2) \\ &\quad\text{(kN)} \\ R_{A} &= V = 15\qquad (B.1) \end{aligned}$$
The number is placed in the math as \qquad (n), so it works in Jupyter, KaTeX and
LaTeX alike. For LaTeX cross-references, emit \tag{...}\label{...} instead and use an
align environment in your own subclass.
A title block on every calculation#
Asked in #137.
Override complete(), which assembles the final output, and add a header built from
context fields. Set the fields once per session with handcalcs.set_option("project", "Footbridge"), or per calculation with a comment command:
"""A title block at the top of every calculation (#137)."""
from handcalcs.renderers import LatexRenderer, MarkdownRenderer
FIELDS = (("project", "Project"), ("calc_by", "By"), ("checked_by", "Checked"), ("date", "Date"))
def title_rows(base_context):
"""(label, value) for each title field that is set, from config or '# hc:' commands."""
context = base_context.global_context
return [(label, getattr(context, name)) for name, label in FIELDS if hasattr(context, name)]
class TitleMarkdownRenderer(MarkdownRenderer):
name = "title_markdown"
def complete(self, tree, base_context):
body = super().complete(tree, base_context)
rows = title_rows(base_context)
if not rows:
return body
table = "| " + " | ".join(label for label, _ in rows) + " |\n"
table += "|" + "---|" * len(rows) + "\n"
table += "| " + " | ".join(str(value) for _, value in rows) + " |\n\n"
return table + body
class TitleLatexRenderer(LatexRenderer):
name = "title_latex"
def complete(self, tree, base_context):
body = super().complete(tree, base_context)
rows = title_rows(base_context)
if not rows:
return body
cells = " & ".join(f"\\textbf{{{label}:}} {value}" for label, value in rows)
header = (
"\\noindent\\begin{tabular}{" + "l" * len(rows) + "}\n"
+ cells + "\n\\end{tabular}\n\n\\noindent\\hrulefill\n\n"
)
return header + body
# hc: project = "Footbridge"; calc_by = "CF"; date = "2026-10-20"
w = 5 # kN/m
L = 6 # m
M = w * L**2 / 8 # kN m
Project |
By |
Date |
|---|---|---|
Footbridge |
CF |
2026-10-20 |
| Project | By | Date |
|---|---|---|
| Footbridge | CF | 2026-10-20 |
$$\begin{aligned} w &= 5 \quad\text{(kN/m)} \\ L &= 6 \quad\text{(m)} \\ M &= \frac{w \cdot L^{2}}{8} = \frac{5 \cdot 6^{2}}{8} = 22.5 \\ &\quad\text{(kN m)} \end{aligned}$$
TitleLatexRenderer writes the same block as a tabular for LaTeX output.
Left-aligned or centred equations#
Asked in #201, #186, #133, #116 and #91.
The LaTeX renderer sets its math flush left. To centre it instead, override the math delimiters:
"""A LatexRenderer that sets each calculation group as a centred display."""
from handcalcs.renderers import LatexRenderer
class DisplayLatexRenderer(LatexRenderer):
name = "display_latex"
MATH_OPEN = "\\[\n\\begin{aligned}"
MATH_CLOSE = "\\end{aligned}\n\\]"
MATH_SKIP = ""
OPERATOR_SYMBOLS = LatexRenderer.OPERATOR_SYMBOLS | {"mult_op": "\\times"}
The Markdown renderer’s $$...$$ blocks are placed by your notebook or site; Jupyter
centres them. The HTML renderer is always flush left.
Colors#
Asked in #293.
HTML: Styling the HTML output, including a theme with dark mode and highlighted results.
Markdown and LaTeX: Colors in Markdown math.
Wrap the whole output#
Override complete(), call the parent’s, and add to the result. For example, a framed
LaTeX box:
class FramedLatexRenderer(LatexRenderer):
def complete(self, tree, base_context):
body = super().complete(tree, base_context)
return "\\begin{framed}\n" + body + "\\end{framed}\n"
Next: Apps and workflows.