cells
The cell data model, storage pattern, values, formulas, inline styles, and cell types.
Cell Storage
Cells are stored in a nested Y.Map structure within each sheet:
rows: Y.Map<Y.Map<Cell>>
^ ^
| └── Inner map: colId → Cell
└── Outer map: rowId → inner mapThis two-level nesting provides efficient row-level operations:
- Insert/delete row: Add or remove a single entry in the outer map
- Move row: Reorder in
rowOrderarray; the row’s cell data stays intact - Access cell: Two map lookups:
rows.get(rowId)?.get(colId)
// Read a cell
const rowMap = rows.get(rowId) as Y.Map<Cell> | undefined
const cell = rowMap?.get(colId) as Cell | undefined
// Write a cell
let rowMap = rows.get(rowId) as Y.Map<Cell>
if (!rowMap) {
rowMap = new Y.Map()
rows.set(rowId, rowMap)
}
rowMap.set(colId, cellData)Sparse storage
Only cells with actual content or formatting are stored. Empty cells have no entry in the map. When all cells in a row are cleared, the empty inner Y.Map can be garbage collected by deleting the outer map entry.
Cell Data Model
The Cell interface defines all possible fields on a cell. All fields are optional – an empty object {} represents a blank cell with all defaults.
Value Fields
| Field | Type | Description |
|---|---|---|
v |
string | number | boolean |
Display value (the computed/entered value) |
f |
string |
Formula string (starts with =, e.g. "=SUM(A1:A10)") |
Cell Type and Format
| Field | Type | Description |
|---|---|---|
ct |
CellType |
Cell type descriptor (see below) |
The ct object describes how the cell value should be interpreted and formatted:
interface CellType {
t?: 't' | 'n' | 's' | 'b' | 'g' // Type code
fa?: string // Format code
s?: Array<{ v: string }> // Rich text segments
}Type Codes (ct.t)
| Code | Meaning | Example Values |
|---|---|---|
g |
General | Auto-detected type |
n |
Number | 42, 3.14, -100 |
s |
String | "Hello", "ABC" |
t |
Time/Date | "2026-01-15", "14:30:00" |
b |
Boolean | true, false |
Format Codes (ct.fa)
The fa field contains a format pattern string compatible with spreadsheet number formatting:
| Pattern | Description | Example Output |
|---|---|---|
"General" |
Auto-format (default) | 1234.5 |
"0" |
Integer | 1235 |
"0.00" |
Two decimal places | 1234.50 |
"#,##0" |
Thousands separator | 1,235 |
"#,##0.00" |
Thousands + decimals | 1,234.50 |
"0%" |
Percentage | 12% |
"0.00%" |
Percentage with decimals | 12.35% |
"$#,##0.00" |
Currency | $1,234.50 |
"yyyy-mm-dd" |
Date | 2026-01-15 |
"h:mm:ss" |
Time | 14:30:00 |
Rich Text Segments (ct.s)
When a cell contains rich text (mixed formatting within a single cell), the ct.s array holds text segments:
{
"ct": {
"s": [
{ "v": "Bold text" },
{ "v": " and normal text" }
]
}
}Each segment contains a v field with the text content. Additional formatting fields may appear on individual segments.
Font Styling
All styling is inline on the cell – there is no named style or style inheritance system.
| Field | Type | Default | Description |
|---|---|---|---|
bl |
number |
0 |
Bold: 0 = off, 1 = on |
it |
number |
0 |
Italic: 0 = off, 1 = on |
un |
number |
0 |
Underline: 0 = off, 1 = on |
cl |
number |
0 |
Strikethrough: 0 = off, 1 = on |
ff |
number |
0 |
Font family code (0 = default) |
fs |
number |
10 |
Font size in points |
fc |
string |
– | Font color (hex, e.g. "#333333") |
Alignment
| Field | Type | Default | Description |
|---|---|---|---|
ht |
number |
– | Horizontal: 1 = left, 2 = center, 3 = right, 4 = justify |
vt |
number |
– | Vertical: 0 = top, 1 = middle, 2 = bottom |
Cell Appearance
| Field | Type | Default | Description |
|---|---|---|---|
bg |
string |
– | Background color (hex, e.g. "#ffeb3b") |
tb |
number |
0 |
Text wrap: 0 = off, 1 = on |
tr |
number |
0 |
Text rotation in degrees |
Formula Storage
Formulas are stored as plain strings in the f field:
{
"v": 150,
"f": "=SUM(A1:A10)"
}- The
ffield contains the formula string as entered by the user (always starts with=) - The
vfield contains the last computed result - Formula references use A1-style notation (e.g.
A1,B2:D10,Sheet2!A1) - The CRDT stores only the formula string – no ID-based cell references
- Formula evaluation is delegated to the FortuneSheet calculation engine at runtime
A1 references vs ID-based addressing
Internally, Calcillo uses random IDs for rows and columns in the CRDT. However, formulas use traditional A1-style references. The conversion between column IDs (from colOrder) and letter codes (A, B, C…) happens at the application layer. This means formulas may need adjustment when rows or columns are inserted or deleted – this is handled by the formula engine, not the CRDT.
Default Stripping
To minimize CRDT storage overhead, fields that match their default values are stripped when saving a cell. The following fields are omitted when they match these defaults:
| Field | Default Value |
|---|---|
bl |
0 |
it |
0 |
un |
0 |
cl |
0 |
ff |
0 |
fs |
10 |
tb |
0 |
tr |
0 |
Fields with no default (like v, f, fc, bg, ht, vt) are stored whenever present.
Cell Addressing
Cells are addressed internally by their (rowId, colId) pair. For display, the column position in colOrder is converted to a letter code:
| Position | Letter Code |
|---|---|
| 0 | A |
| 1 | B |
| … | … |
| 25 | Z |
| 26 | AA |
| 27 | AB |
Row position in rowOrder is converted to a 1-based number. For example, the cell at position (row index 2, col index 0) displays as A3.