A Reading is a question asked of a trackable: Journal read as Any is “did I journal that day”, Water read as Sum is “how much did I drink”, Weight read as Last is “what did I weigh”. It is list work over the trackable’s entries as objects (The List Model): keep some, map each to a value, group them by day, and fold each day by a reduce. Everything that shows a trackable reads it through a Reading, so “what is this trackable’s value on this day” has one answer wherever it is asked.
Every trackable has a Reading of its own, its aggregation. Anything that shows it can read it another way. A derived trackable is a Reading saved, or several combined by a formula; its own Reading is worked out from its formula. A Reading needs saving as a derived trackable only when it has to stand on its own: on Home, under a tag, or carrying zones or achievements. A Reading picked in a visualization is kept with that visualization and makes no trackable.
The pipeline
flowchart LR e["A trackable's entries, as objects"] --> s["Whole-list steps: coalesce, filter"] s --> d["Group by day"] d --> p["A day's pick: maxBy or rest"] p --> m["Map each entry to a value"] m --> v["Each Event as one, where asked: its values summed"] v --> r["Reduce the day"] r --> f["Fold the days: a week, a month, a window, a streak"]
readingDays in packages/core/src/reading-days.ts runs a Reading with steps or a map; trackableDays in packages/core/src/rollup.ts reads a trackable by its own way or by a reduce, for Home and the derived evaluator alike. Both work over entry objects in memory. The derived evaluator reads a profile’s entries once, handed in or by one query, and works out every day from them, which is far faster than a query per trackable through the app’s SQL bridge. Nothing a Reading works out is stored.
Steps over the whole list (ListStep in packages/core/src/derived-spec.ts, run by list-steps.ts):
coalescejoins finished intervals less than N seconds apart into one, across midnight too: a night broken by being up at 3 is one sleep. The joined interval runs from its first start to its last end, counts toward the day it ended, and its length is the time in its parts, so the gap is not in it.filterkeeps the entries meeting a condition on a path: a number at least so much (a sleep of at least 3 hours), an instant’s time of day in a window (“ate before 17:00” isatfrom 04:00 to 17:00), an item’s Category holding one of some values or none of them, a text containing a word, an interval running at a clock time on the day it ended or the day before (03:00 finds the night, 14:30 the afternoon nap), or being part of an entry of a given Routine, or of none of its (Events).
A day’s pick. maxBy keeps the entry with the largest value at a path among those meeting its conditions, the first logged on a tie; rest keeps every other entry with a value there. Main sleep is the longest sleep; the sleeps besides it are its rest. Only entries with a number at the path take part, so an open interval does not.
Each Event as one (byEvent, offered as Each event as one where a value is read). The values of the entries that were part of one Event are summed, and the Event is filed under the day it began, so one that ran past midnight is not split; an entry in no Event is a session of its own. The day then reduces its Events: Max of Volume read this way is the day’s best session, and a day of two workouts keeps them apart rather than merging them, which grouping by day alone would. Count counts the day’s Events.
The map says what each entry becomes: a path into the entry object (duration, start, pages, calories, item.type) or arithmetic over paths (sets × reps × weight). An instant (at, start, end) maps to its time of day (Time of Day). Without a map, an interval reads as its length, a moment logged with no value as its time of day, and anything else as its own value.
The reduce folds a day’s values by one table, REDUCES in packages/core/src/reduces.ts. Each reduce has its label, the kinds of value it takes, what it gives back, its value over an answered day with no entries, and its one implementation (reduceValues). The Reading picker, a field’s reduce in a visualization, and a record’s summary all ask this table.
| Reduce | Takes | Gives | An answered empty day |
|---|---|---|---|
| Any | anything | yes or no | No |
| Count | anything | how many entries | 0 |
| Distinct | a ranked or unranked choice, a text, an item | how many different | 0 |
| Sum | an amount | the same kind | 0 |
| Mean, Median, Min, Max | an amount, a time of day, a ranked choice | the same kind | nothing |
| First, Last | all of those, a yes/no, a choice, a text, an item | the same kind, in recorded order | nothing |
Union, which joins comma-lists of words, is in the table for a record’s summary (the muscles worked) and is not offered as a Reading. Over times of day, Min reads as the earliest and Max as the latest. A choice without an order is never averaged. reducesFor(schema) says which reduces a trackable’s own kind takes.
Any is true for an entry that is not a No or a zero, so a moment logged at midnight counts and an entry with no value counts. A stored 0 is false for a yes/no, a number and a choice coded 0, which only a scale with an option meaning none has (Headache’s None); a scale without one codes its options from 1, so Energy’s Drained and Mood’s Low are true (zeroIsFalse). Count counts entries, not numbers, so a trackable logged with no value counts too.
When a day has an answer
A day is answered by an entry of the trackable that the Reading can read, counted before a filter removes it (an open interval, with no length yet, answers nothing), or by saying there was none: a Nothing entry (Entry), or, for a trackable whose absence setting says an empty day means none, a day on which you logged anything at all and which no Disruption touches (Capture Model, Disruptions). An entry logged for you, such as a day’s weather, does not count as you logging that day (Logged for You).
An answered day with no value left reads the reduce’s empty value where it has one, and a yes/no reads No; a day not answered stays unknown. So “ate before 17:00” reads No on a day with only dinner, and nothing on a day with no Diet entry at all. Under a maxBy, the longest of no entries is none rather than 0.
A day can carry a sign: ≥ (At least) when it can only be a minimum (an interval logged with an end and no start, an item with no figure for what was asked, an item’s value marked At least), ≤ (At most) when a value behind it is At most, and ~ (About) when one is About or the directions disagree. The signs of what goes into a day combine by one rule (combineSigns in packages/core/src/value-sign.ts), and a period or a window combines the signs of the days in it.
Tags
A tag’s members read as one list of entries: Sum adds every entry, Min and Max are the smallest and largest of them all, Count adds the members’ counts, Any is any member’s Any, and Mean is every member’s total over every member’s count, so a member logged ten times a day weighs ten times as much as one logged once. First and Last read the earliest and latest recorded entry across the members. Median and Distinct fold the list itself, since neither folds across members: the median of every entry, not of each member’s median, and how many values the entries hold between them, not a count of each member’s count. A derived member’s day value is one more item of its day. The derived member that summarises a tag is left out of what it reads (Tags).
Folding the days
The day is the unit every series pairs on, for charts and correlations alike: a finer question is a filter on the time of day, and a coarser one a fold by week, month or rolling window.
A Reading’s days fold again, by the same table:
- A week or a month. A visualization of several trackables can align by Day, Week or Month: each period is keyed by its first day (a week starts on the person’s first day, Monday unless set to Sunday) and folds its days by Sum, Mean, Median, Min or Max (
DAY_FOLDSinpackages/core/src/day-folds.ts), by default Sum where the days add up and Mean where they do not. Journal read as Any, by month, is the number of days journaled in each month. - A rolling window is the derived formula’s
windownode: each day’s value is the operand reduced over the last N calendar days (dense, answering every day in range) or the last N days that have a value (sparse). Weight’s three-day mean isWeight › Meanover a rolling three days. A window can wait until it is full, for a series that feeds a record (Rolling Windows). - A Streak (
streakOf) is a run of days in a row whose value reads as Any, by the zero rule above: a No, a 0 or a choice’s none breaks it, Energy’s Drained does not, and a run carries across days on hold rather than breaking at them (Disruptions).runningDaysreads days as they stood after each one (a running count, a running Sum, the run ending there, the day’s own value), which is what an achievement measures by (Achievements). - A summary of a span (
summarizeDays): its last day, the Mean, Min, Max and Sum of its days, how many days have a value, and its Streak as it stands today. The stat strip shows it.
Where a Reading is chosen
One ReadingPicker (apps/renderer/src/components/trackables/ReadingPicker.tsx) offers a Reading wherever one is chosen: on a trackable’s page and a Home card (Read as), for a series in the visualization editor, and for a derived formula’s reduce and its rolling window. It lists the trackable’s own reduce first, then what the source takes, the first three and the rest under More, each by its name alone. Picking the trackable’s own reduce keeps no choice of its own, so what is read follows the trackable’s own Reading. What a source takes (sourceReduces): for a clock reading, the reduces that keep a time a time; for an item’s attribute, a tag or a derived trackable, every reduce of a number; for anything else, its own kind’s. A Home card on a derived trackable offers Any besides its own value.
On a trackable’s page and a Home card, a Reading picked is written to the visualization shown there, so it is kept. Which controls on a page or a card save, and which are meant only to look, is in Visualizations.
A card read another way charts, shades and headlines that Reading; the zones and target belong to the trackable’s own Reading and stand aside.
A trackable’s own Reading is set in the trackable form, under the kind of value: a ReadingPicker over the reduces the kind takes, saved as aggregation. A trackable made there starts from startingReading (packages/core/src/value-schema.ts): a number in a level’s dimension (mass, temperature, pressure, glucose) reads Last, so two weigh-ins in a day read the later one; anything else reads by its kind (defaultAggregation: a number or an interval is a Sum, a time of day a Mean, a record a Count, anything else a Last). A trackable a module makes takes the module’s, and one saved without a Reading takes its kind’s. The form keeps the picked Reading while the kind of value takes it, and falls back to the new kind’s starting one when it does not. Reattaching a trackable to its module takes the module’s, and a stored Reading the kind does not take reads as the kind’s default. A visualization’s Read as only looks: it never changes the trackable’s own Reading.
A Reading is stored as a structure and named by a dotted id (packages/core/src/reading-id.ts): <id>.any is a trackable read as Any, <id>.weight.mean a path read by a reduce, <id>.any?contains=headache a filter, <id>.distance.sum?select=… one narrowed as a derived trackable’s Reading is, and *.any is every trackable at once, the Logged anything series.
Related
The List Model · Derived Trackables · Derived Authoring · Editor for Several Trackables · Capture Model · Time of Day · Rolling Windows