Values and presentation¶
A cell that contains 0.75 could mean a quantity, a ratio or part of a monetary amount. Caxton keeps that meaning
separate from its appearance. A column declares the semantic value first; a display format controls how that value is
printed; styles and themes control the surrounding cell.
That separation lets you change the workbook's appearance without changing its schema or row logic.
Give each value a meaning¶
The column factories attach a built-in semantic type to each value:
| Value family | Semantic types | Typical Python values |
|---|---|---|
| Text | Text, Link |
str |
| Numbers | Integer, Decimal |
int, float, decimal.Decimal |
| Quantities | Money, Percentage |
Numeric values; percentages are ratios such as 0.75 |
| Time | Date, Time, DateTime, Duration |
Values from datetime |
| Logical | Boolean |
bool |
Here the raw values happen to be numbers and dates, but the columns preserve what those values mean:
import datetime as dt
from decimal import Decimal
from caxton import date, money, percentage, text
books = (
{
"title": "Kindred",
"published": dt.date(1979, 6, 1),
"price": Decimal("14.50"),
"progress": Decimal("1.0"),
},
{
"title": "Piranesi",
"published": dt.date(2020, 9, 15),
"price": Decimal("12.00"),
"progress": Decimal("0.35"),
},
)
book_columns = (
text(source="title", title="Title"),
date(source="published", title="Published"),
money(source="price", title="Price", currency="USD"),
percentage(source="progress", title="Read"),
)
Money(currency="USD") carries its currency even when no explicit display format is present. Percentage records that
the source is a ratio: 0.35 means 35 percent. The renderer receives those semantics and chooses an appropriate
physical representation for the output format.
Semantic types also drive other behavior. The numeric flag controls which columns Totals() selects automatically
when no explicit items are supplied. Grouping and matrix dimensions retain the identity of their Python values: True,
1 and Decimal("1") do not collapse into the same key.
Choose how a value is displayed¶
A display format changes the visible representation, not the value or its semantic type. Attach one with the generative
.format() method:
from caxton import date_format, money_format, percentage_format
formatted_columns = (
text(source="title", title="Title"),
date(source="published", title="Published").format(
date_format(variant="long"),
),
money(source="price", title="Price", currency="USD").format(
money_format(places=0, grouping=True),
),
percentage(source="progress", title="Read").format(
percentage_format(places=0),
),
)
The original columns remain unchanged; each call returns a new immutable column. The standard formats cover decimal,
money, percentage, date and time values. Use custom_format() when the standard vocabulary cannot express a required
representation:
from caxton import custom_format, integer
page_count = integer(source="pages", title="Length").format(
custom_format("page-count", '#,##0 "pages"'),
)
A custom format has a semantic name and an XLSX-compatible fallback pattern. Keep the semantic type honest: a custom pattern should change only the display. It should not turn an integer column into a date or give a unitless decimal the meaning of money.
Currency is a stricter case. It belongs to Money, so Caxton rejects a format that would silently discard a declared
currency. money_format(currency="EUR") may deliberately override the displayed currency; decimal_format() may not.
Compose cell styles¶
Style collects backend-neutral presentation such as fonts, fills, borders, alignment and display formats. It accepts
structured values where they matter and short forms for common cases:
from caxton import CellAlignment, FontStyle, Style
heading = Style(
font=FontStyle(name="Arial", size=16, bold=True, color="#24324A"),
fill="#E8EDF5",
border_bottom="thin",
)
wrapped_text = Style(
alignment=CellAlignment(vertical="top", wrap_text=True),
)
centered = Style(align="center")
Colors use #RRGGBB notation. Border shorthands accept thin, medium, thick, dashed, dotted and double. Use
BorderLine when one side also needs its own color.
An inline style is useful for a single declaration. Give repeated styles names at the document boundary:
from caxton import DocumentTheme
library_theme = DocumentTheme(
default=Style(font=FontStyle(name="Arial", size=10)),
header=Style(
font=FontStyle(bold=True, color="#FFFFFF"),
fill="#405A7A",
),
total=Style(font=FontStyle(bold=True), border_top="double"),
)
library_styles = {
"section-title": heading,
"body": Style(border_bottom="thin"),
"currency": Style(
align="right",
display_format=money_format(places=2, grouping=True),
),
"finished": Style(fill="#DDEEDD", font_color="#245B35"),
}
Passing the mapping as styles= to spreadsheet() normalizes it into an immutable StyleSheet. Blocks and columns can
then refer to a name instead of copying the value. Keep these names role-based: currency, section-title and
finished say what the style is for without tying it to a cell coordinate or renderer.
Apply defaults at the right scope¶
A theme supplies workbook-wide defaults. Named styles describe reusable roles within that workbook. Inline styles are best reserved for a genuine one-off.
Caxton merges styles component by component, so a column that changes only its display format still inherits the document font and the table border. The effective order depends on the kind of cell:
| Cells | Resolution order, from general to specific |
|---|---|
| Table data | Theme default → table style → column style → column .align() or .format() |
| Table header | Theme default → table style → theme header → table header style |
| Totals row | Theme default → table style → theme total → totals-row style |
| Title block | Theme default → title-level defaults → title style |
Later values override only the fields they set. For example, a table header style that sets align="center" keeps the
theme header's font and fill.
Here is the earlier reading list with those scopes applied:
from caxton import col, sheet, spreadsheet, table, title, when, write
reading_list = spreadsheet(
sheet(
"Reading",
title("Reading list", span=4, style="section-title"),
table(
source=books,
columns=(
text(source="title", title="Title"),
date(source="published", title="Published").format(
date_format(variant="short"),
),
money(
source="price",
title="Price",
currency="USD",
style="currency",
),
percentage(source="progress", title="Read").format(
percentage_format(places=0),
),
),
name="books",
style="body",
rules=(when(col("progress") >= 1, style="finished"),),
auto_width=True,
),
),
styles=library_styles,
theme=library_theme,
)
write(reading_list, "reading-list.xlsx")
The conditional rule remains live in the workbook. The spreadsheet evaluates it for each data row and overlays the
finished style when the progress reaches 100 percent. Its formula refers to the semantic column id progress; moving
that column does not break the rule.
Add an application-specific value¶
The built-in semantic set is extensible. Add a type when the application has a value with stable meaning that should be visible to renderers, totals or inspection. A rating is one example:
from typing import ClassVar
from caxton import Column, CustomFormat, SemanticType
class Rating(SemanticType):
name: ClassVar[str] = "rating"
numeric: ClassVar[bool] = True
def default_format(self) -> CustomFormat:
return CustomFormat(name="rating", pattern='0.0 "stars"')
rating = Column(
semantic_type=Rating(),
source="rating",
title="Rating",
)
The type owns its default format and whether Totals() selects it automatically. Explicit Total(...) items are
validated by column identity; numeric is not a rejection rule for an explicitly named item. A renderer that supports
semantic:extension does not need a hard-coded Rating branch; both bundled XLSX renderers use the format declared by
the type. Keep custom business rules in the application rather than adding presentation state or backend objects to the
semantic type.
For the complete constructor surface, see
caxton.core.types and
caxton.core.formatting.