A rolling window answers questions about a trailing period: good days in the last 30, the 7-day mean of a run’s distance, the mean of the last 10 weigh-ins, the best month. It is a node of a derived trackable’s formula, so a windowed series is an ordinary derived trackable: charted, given zones, read in a visualization of several trackables, and handed to an achievement rule like any other.
{ node: "window", operand, reduce, span, by: "days" | "values", requireFull? }The node is WindowNode in packages/core/src/derived-spec.ts, and windowSeries in packages/core/src/derived-evaluator.ts computes it.
Why a window is a node, not an achievement rule
“Any 30-day stretch with 20 good days, ever” and “20 good days in the last 30, now” sound alike but are different kinds of thing. The first is earned once, on a date the data fixes. The second rises and falls; it is a status, like a streak counter or a value against a zone. An achievement’s date comes from the data, so nothing has to have been watching when it happened; a rule over “the last 30 days from now” would un-earn itself as time passed.
So the window is part of the formula language, and achievements need nothing new: they already fold over whatever day series they are handed.
| Question | Built as |
|---|---|
| 20 good days in the last 30 (live) | a windowed count, read as the trackable’s value |
| ever had 30 days with 20 good ones | a windowed count, with a highest rule and a ladder |
| your best month | a windowed sum, with a highest rule and a record |
Two modes
The two modes answer different questions, so one node carries both rather than one being picked:
days: the last N calendar days. A day with no value adds nothing to the window, and the output is dense, because “how many good days in the last 30” has an answer on a day you logged nothing. A calendar window runs on through today, past its operand’s last value, and answers only on a day its operand has a value or a day you logged anything at all (entries logged for you do not count): a day with nothing logged anywhere is on hold, as it is for a streak, so an app left unopened for a month is not a month of zeros.values: the last N days that have a value, skipping the gaps. The output stays sparse, because “the mean of my last 30 weigh-ins” is only defined where there is a weigh-in.
Making the two agree would give one of the questions a wrong answer.
Partial and empty windows
requireFull is off by default. A partial window is the true reading five days in: “good days in the last 30” really is however many there have been. It is for a record, where a half-full window’s mean can beat a full one on noise alone, and a best set by not having enough data is not a best. It is written to a spec only when true.
An empty calendar window is 0 for count and sum, and no value for every other reducer. No good days is none and an empty sum is 0, while the mean or the minimum of nothing is undefined.
A window over a constant is that constant on every day.
In SQL terms
Every node of the formula language is meant to stay expressible in SQL (Derived Trackables). values is a plain ROWS BETWEEN n PRECEDING AND CURRENT ROW frame. days needs a spine of dates to join against, because SQLite has no interval RANGE frame. The evaluator computes both in memory, over the operand’s day series.
Authoring
In the trackable form a window is a modifier on a formula rather than a template of its own: “the 7-day mean of the sum of Run” is the Aggregate template with a window around it, shown as a checkbox below the formula with its reducer, span and mode (Derived Authoring). Reading a stored spec back peels the window off before matching a template, and building one wraps whatever the template built, so a spec survives the form unchanged; a window as a template of its own would multiply every template by it.
A windowed formula’s kind of value follows its reducer, as an aggregate’s does (aggregateSchema): the count of a yes/no’s days is a number, not a yes/no, while the 7-day mean of a weight is still a weight and of a bedtime still a time. The form labels the two modes days and readings.
The write’s schema and the TypeScript union of nodes are two declarations of one set; packages/core/src/schemas.test.ts keys an example per node off the union, so a new node is a type error until it has an example, and the test proves the schema accepts it.
Related
Derived Trackables · Derived Authoring · Achievements · Zones · Editor for Several Trackables