The presentation layer is how a Trackable and its entries look. All of it is safe to edit at any time, because none of it is a stored value: an entry holds a number, a word or a code, and the label, colour and order it is shown with are looked up when it is shown.

Kept apart from the values

That safety comes from where it is stored, not from care at each call site:

  • The presentation JSON column on trackable, separate from value_schema (Presentation in packages/core/src/value-schema.ts), so writing presentation cannot touch what an entry means.
  • The trackable_option table, one row per option keyed by (trackable_id, code), the same stable code an entry stores, with its label, color, sort_order and tombstoned. Renaming Okay to Meh rewrites one row; the entries holding that code are untouched. A scale has at least two options and no two share a label, ignoring case (optionsProblem: “Two options are both called Good”), since two codes under one word cannot be told apart.
  • The trackable’s colour is a column of its own on trackable, not part of the presentation blob.

Zones and visualizations are presentation in what they mean but rows of their own tables, because something refers to them by id: an achievement names a zone, and an id inside a JSON blob is one the database cannot protect (Zones, Visualizations).

What it holds

  • Colour, per trackable and per option.
  • Option labels, per option. A retired option keeps its label so the entries holding it still read (Stranded Option Data).
  • Option order (sort_order), independent of the codes; retired options sort last.
  • The words for yes and no (true_label, false_label), Yes and No unless set, under “Words for true and false” in the trackable form when the value is a yes/no.
  • Exclusive, a tag’s setting (Tags).
  • Record summaries (summaries): for a record trackable, declared stats { label, expr, agg, unit } worked out over the entries the table is showing, all of them or one exercise’s (RecordSummary in apps/renderer/src/components/table/RecordSummary.tsx). expr is per-entry arithmetic over the record’s fields, and agg is Sum, Mean, Median, Min, Max, Count or Days (how many days have one). Strength’s strip (sessions, sets, volume, top set) is these four, declared as data rather than code. See Data Table.

A trackable’s own icon is not stored at all: it comes from its kind (iconForTrackable: a tag, an item, a number, a choice, a yes/no, a text, a time).

There is no per-trackable number format: how a number is shown (fmt in apps/renderer/src/lib/format.ts: a whole number bare, else one decimal, more where one would round a real value to 0) does not depend on the trackable, and a duration always reads as days, hours and minutes. A format field is left out of Presentation on purpose: a field nobody reads looks like a setting that works.

One value, one label

Every surface turns a stored value into words through the same functions, so a tooltip, a calendar cell, a chart axis and a stat header cannot disagree: booleanLabel in @datler/core for a yes/no, and valueLabel in the renderer for every value, which converts a number to the unit shown and looks an option up by its code. A boolean’s words have one home, the presentation blob; the value schema of a yes/no is { kind: "boolean" } and nothing else.

Why a layer

Gathering labels, colours and order into one place apart from the values makes “safe to edit” true by construction and easy to see, keeps the value types free of anything about display (Value Types), and lets a change of wording reach every entry ever logged without rewriting one.

Trackable · Value Types · Capture Model · Trackable Authoring Flow · Design Tokens