Cell metadata

Cell Metadata

Cell Metadata

Cell metadata is the layer that sits next to a cell value, carrying formatting, styling, validation, and conditional formatting rules. Metadata never changes the computed value — it changes how the value is displayed and validated.

This is distinct from type tags (A1 as currency = 100), which are part of the formula language and live in the model source. Cell metadata is set via the HTTP API at runtime, not in .grid source.

For the HTTP endpoints, see docs/api/http_api.md §Cell Metadata.


1. The CellMetadata Shape

Each cell can carry one optional CellMetadata record:

interface CellMetadata {
  unit?: UnitAnnotation;
  numberFormat?: NumberFormat;
  dateFormat?: DateFormat;
  style?: CellStyle;
  conditionalFormats?: ConditionalFormat[];
  visualizations?: CellVisualization[];
  validation?: ValidationRule;
}

Every field is optional. Setting one field doesn't clear the others.


2. Number Formatting (numberFormat)

interface NumberFormat {
  style: "decimal" | "percent" | "currency" | "scientific";
  currency?: string;          // ISO code, e.g. "USD"
  minimumFractionDigits?: number;
  maximumFractionDigits?: number;
  useGrouping?: boolean;      // thousands separator
  locale?: string;            // BCP-47, e.g. "en-US"
}

Examples:

{ "style": "currency", "currency": "USD", "minimumFractionDigits": 2 }
{ "style": "percent", "maximumFractionDigits": 1 }
{ "style": "scientific", "maximumFractionDigits": 3 }
{ "style": "decimal", "useGrouping": true, "maximumFractionDigits": 0 }

Underlying implementation uses the JavaScript Intl.NumberFormat contract.


3. Date Formatting (dateFormat)

interface DateFormat {
  pattern: string;
}

Patterns use Excel/Sheets-style tokens (yyyy, mm, dd, hh, mm, ss, am/pm). Example:

{ "pattern": "yyyy-mm-dd" }
{ "pattern": "mmm d, yyyy hh:mm am/pm" }

The same pattern strings are accepted by the TEXT() function in formulas.


4. Unit Annotation (unit)

A free-form unit label rendered alongside the value:

interface UnitAnnotation {
  label: string;
  position: "prefix" | "suffix";
}
{ "label": "$", "position": "prefix" }
{ "label": "kg", "position": "suffix" }
{ "label": "°C", "position": "suffix" }

Unit annotations are display-only — they don't affect the value or arithmetic. For semantic typing that does affect coercion and validation, use type tags (A1 as currency = 100).


5. Style (style)

interface CellStyle {
  bold?: boolean;
  italic?: boolean;
  textColor?: string;            // CSS color, e.g. "#FF0000"
  backgroundColor?: string;
  horizontalAlign?: "left" | "center" | "right";
  borderTop?: string;            // CSS border, e.g. "1px solid #999"
  borderRight?: string;
  borderBottom?: string;
  borderLeft?: string;
}

Style fields merge — partial PATCHes preserve existing fields:

existing: [ bold: true, textColor: "#000" ]
patch:    [ italic: true ]
result:   [ bold: true, italic: true, textColor: "#000" ]

This merge happens automatically in PATCH /api/models/:id/cells/metadata.


6. Validation (validation)

interface ValidationRule {
  type: "list" | "range" | "regex" | "custom";
  params: Record<string, unknown>;
  errorMessage?: string;
  allowBlank?: boolean;
}
type params shape What it checks
list { "options": ["a", "b", "c"] } Coerced string value must be in options
range { "min": number, "max": number } (either optional) Coerced numeric value must lie in [min, max]
regex { "pattern": "..." } Coerced string value must match the JS regex
custom (any) Always valid; reserved for caller-side checks

If allowBlank is true, blank or empty values pass regardless of the rule.

errorMessage overrides the default message in ValidationResult.

Validation is not enforced on writes by default — it produces a ValidationResult { valid, message? } that callers (UI, API consumers) choose to surface or block on.


7. Conditional Formatting (conditionalFormats)

A list of conditions, each pairing a predicate with a style override:

interface ConditionalFormat {
  operator: ConditionalOperator;
  params: Record<string, unknown>;
  style: CellStyle;
}

Operators:

operator params shape Fires when
greaterThan { "value": n } numeric value > n
lessThan { "value": n } numeric value < n
greaterThanOrEqual { "value": n } numeric value ≥ n
lessThanOrEqual { "value": n } numeric value ≤ n
equal { "value": v } numeric or string equals v
notEqual { "value": v } numeric or string ≠ v
between { "min": n, "max": n } numeric value ∈ [min, max]
notBetween { "min": n, "max": n } numeric value ∉ [min, max]
isEmpty {} cell is blank
isNotEmpty {} cell is not blank
textContains { "text": "..." } string value contains text
textNotContains { "text": "..." } string value does not contain text

Multiple conditional formats may be set; the runtime applies them in order, and the last matching style wins per CSS field.

Example:

{
  "conditionalFormats": [
    { "operator": "lessThan", "params": { "value": 0 }, "style": { "textColor": "#C00000" } },
    { "operator": "greaterThanOrEqual", "params": { "value": 1000 }, "style": { "bold": true } }
  ]
}

8. Cell Visualizations (visualizations)

Cell visualizations are value-driven renderers. They change how a cell is drawn without changing the underlying value and without being predicate rules. They share the same metadata storage and API PATCH flow as other cell metadata.

type CellVisualization =
  | ColorScaleVisualization
  | ProgressBarVisualization
  | RelativeBarVisualization
  | BadgeVisualization
  | SparklineVisualization;

Supported visualization kinds:

kind Purpose
colorScale Interpolates cell background from min/mid/max colors over a range or column scope
progressBar Draws an in-cell bar for direct percentage or normalized numeric values
relativeBar Draws an in-cell bar relative to scoped range/column stats
badge Draws compact status/risk/pass-fail labels from value predicates
sparkline Draws a compact line/bar/win-loss chart from an array value or source range

Scopes are optional:

interface CellPresentationScope {
  kind: "cell" | "range" | "column";
  range?: string;  // e.g. "A1:A20" for kind: "range"
  column?: number; // zero-based column index for kind: "column"
}

Example:

{
  "visualizations": [
    {
      "kind": "relativeBar",
      "scope": { "kind": "column" },
      "fillColor": "#3b82f6",
      "negativeFillColor": "#ef4444",
      "zeroBaseline": true
    },
    {
      "kind": "badge",
      "rules": [
        { "operator": "greaterThanOrEqual", "params": { "value": 1 }, "label": "PASS" },
        { "operator": "lessThan", "params": { "value": 1 }, "label": "RISK" }
      ]
    }
  ]
}

Visualizations are presentation-only. CSV/export surfaces raw values, not rendered bars, badges, or icons. UI renderers should provide readable labels/tooltips when a visual effect conveys information not visible in plain text.


9. Comments

Comments aren't part of CellMetadata — they live in their own threaded structure that can attach to a cell, row, column, or whole sheet.

interface CommentThread {
  key: string;
  target: CommentTarget;
  comments: CommentEntry[];
}
 
type CommentTarget =
  | { kind: "sheet"; sheet?: string }
  | { kind: "row"; row: number; sheet?: string }
  | { kind: "column"; column: number; sheet?: string }
  | { kind: "cell"; symbol: string };
 
interface CommentEntry {
  id: string;
  commenter: string;
  message: string;
  createdAt: string;     // ISO-8601
}

A ModelSnapshot includes all comment threads keyed by their CommentThread.key. Add comments via POST /api/models/:id/comments. Updates are broadcast over the WebSocket as comment:update events.


10. Setting Metadata From The API

# Single-cell write (replaces all metadata for the cell)
curl -X PUT http://localhost:3000/api/models/portfolio/cells/B1/metadata \
  -H "Content-Type: application/json" \
  -d '{
    "metadata": {
      "numberFormat": { "style": "currency", "currency": "USD" },
      "style": { "bold": true, "horizontalAlign": "right" }
    }
  }'
 
# Bulk patch (merges into existing metadata)
curl -X PATCH http://localhost:3000/api/models/portfolio/cells/metadata \
  -H "Content-Type: application/json" \
  -d '{
    "metadata": {
      "A1": { "validation": { "type": "range", "params": { "min": 0 } } },
      "B1": { "conditionalFormats": [
        { "operator": "lessThan", "params": { "value": 0 }, "style": { "textColor": "#C00000" } }
      ]},
      "C1": { "visualizations": [
        { "kind": "progressBar", "min": 0, "max": 1, "fillColor": "#3b82f6", "label": "percent" }
      ]}
    }
  }'

11. Relationship To Type Tags

Type tags (in formula source) and cell metadata (set via the API) serve overlapping but distinct purposes:

Concern Type tag Cell metadata
Stored where .grid source Redis (next to value)
Affects computation Yes (validation, coercion) No
Affects display Implicit defaults (currency tag → currency format) Explicit fields
Versioned with model Yes No (per-cell, mutable at runtime)
Set by Author of the model UI / API consumers

The recommended pattern:

  • Use type tags to declare what a value is (currency, percentage, date) — durable in source.
  • Use cell metadata to declare how it should be displayed and validated on a particular deployment (currency code, decimal places, color thresholds) — mutable at runtime.

Many UIs use the type tag as a default and let users override via metadata.


12. Display Hooks For Domain Type Tags

Domain typeTag values give the renderer enough information to default to a sensible display without any extra metadata, but the schema reserves slots so a UI may override per-cell:

typeTag Default display Override metadata fields
complex re ± im·i canonical (e.g. 3+4i, -2.5i) numberFormat for both parts
url hyperlink with the host as label displayHint.urlLabel
molecule rendered formula (subscript digits) displayHint.useSubscripts
dna monospace sequence, optional 10-char grouping displayHint.dnaGroup
color swatch + hex displayHint.colorFormat (hex, rgb, hsl)
note scientific pitch (A4) + optional Hz tooltip displayHint.noteShowHz
bytes length + first 4 bytes hex (e.g. 4 B DEADBEEF) displayHint.bytesEncoding (hex, base64)

displayHint is an optional bag on CellMetadata; renderers that don't recognize a hint key simply fall back to the type's canonical default. The canonical defaults match formatComplexCanonical / format<Domain> in src/core/model/value.ts and src/core/types/domain/*.ts, so server-side stringification stays in lockstep with what the client renders.


13. See Also

  • docs/api/http_api.md — endpoints to read/write metadata.
  • reference.md §5.2 — type tags.
  • src/core/model/cellMetadata.ts — schema source of truth.
  • src/core/model/comments.ts — comment-thread schema.
  • src/core/model/validation.ts — validation evaluator.