Every visualization is made and changed in one editor, SeriesEditor (apps/renderer/src/components/series/SeriesEditor.tsx), at /visualization, by looking at it while it changes. Its left rail is the series. With one whole trackable it edits that trackable’s tabs; with two or more it is a visualization of several, a question asked across what you track: does Water go with Weight, does Mood move over a month, does a workout show up in the next night’s sleep. It is an open builder rather than a fixed set of charts: the value types of the series decide what can be drawn and which statistics are honest for them. A fixed set would put every series on a scatter, which either leaves out what is not a number or draws a scale’s codes on a regression line as if they were amounts.

Addresses

addressopens
/visualization?edit=<trackable>&kind=<kind>that trackable’s tab of that kind; a card’s or a page’s settings button sends you here
/visualization?id=<id>a saved visualization of several
/visualization?series=a,ba new one on those series
/visualizationa new one on the first two series that are not derived, picked once when the trackables load

Clearing the series leaves the editor empty rather than putting the first two back, and opening a saved visualization never seeds over it. One made from a trackable’s tab, by adding a second series to it, is shown on that trackable unless unticked. The list of saved ones is at /visualizations (Visualization Persistence).

One trackable

With one whole trackable selected, one you log or a derived one read its own way, the middle and right are that trackable’s (OneTrackablePanes, components/visualizations/VisualizationEditor.tsx). The open tab is drawn from a draft; the Statistics panel shows its headline, the period read as one value (none for a Table); beside them are the open tab’s settings (Period, Headline, Compared with, the Reading it is read as, Chart type, a calendar’s Shaded by and Darker means, a timeline’s Day Boundary and Layout, a scatter’s axes, Color, and Shown on Home), then the trackable’s tabs as a short ordered list, with the visualizations of several placed on it listed as links. Save writes the draft to the trackable and says so (“Saved Weight’s Chart”); Revert drops it; “not saved” shows while the draft differs. What each kind of tab is and which ones a trackable can have is in Visualizations.

Adding a second series turns it into a visualization of several in place. Taking a visualization of several back to one trackable shows that trackable’s tab, and a saved one cut to one trackable is left as saved, so adding the other series back returns to it. A saved visualization that holds a single series opens as itself, not as its trackable’s tab.

Series

A series is a day → value map, and anything that reads as one can be picked: a trackable you log, a derived trackable (a tag’s total is a derived trackable over its members), a field of a record, a trackable read another way (a Reading, such as Journal read as Any), and Logged anything, whether a day has any entry at all. A record is offered through its fields rather than whole, each number or reference field a series of its own.

Each series has one id, written dotted (packages/core/src/reading-id.ts): a trackable read its own way is <id>, read by a reduce <id>.any, a record’s field by a reduce <id>.weight.max, a text read for a word <id>.any?contains=headache, a series narrowed to some of its entries <id>.distance.sum?select=… (its narrowings as JSON, written in one order so one narrowing has one id), and Logged anything *.any. The editor writes these as you choose: each series’ Read as picks its reduce (a number field takes Mean, Median, Sum, Max or Min, Mean unless changed, so Reps can be read as its Sum and Weight as its Max, a day’s top set), its Narrow reveals the narrowings a derived trackable’s Reading has (a time of day, items, a Category value, a Routine), and typing in the word box rewrites the first text series as read for that word. A series reads every entry until it is narrowed; the same trackable can be a series twice, narrowed differently, so runs against walks is Cardio narrowed to Run and to Walk, named “Cardio · Distance · Sum · Run” and “… · Walk” (Chad’s Runs and walks). A saved visualization stores these ids, so what it reads is in the id rather than in settings beside it.

Series resolve through one primitive (useDailySeries over useReadingSeriesMap in hooks/useTrackableSeriesMap.ts): a trackable you log folds its day’s entries by its aggregation, in memory, and a derived one is evaluated on read. Values are converted to the display unit before anything is drawn or computed.

The steps, in order

seriesSelection (components/series/engine.ts) reads a selection in one order, shared by the editor and by a saved visualization drawn on a card:

  1. the day values in the visualization’s colours, with the days on hold left out when asked;
  2. the contains query, which turns its text series into a yes/no a day (effectiveSelection);
  3. the chart, chosen from which series are picked and their kinds, never from their values;
  4. only then the days grouped into weeks or months, if that chart reads values over time;
  5. what that chart draws besides its series (chartDataOf): a distribution’s categories, a grouping’s means, a histogram’s bins or a heatmap’s table.

The transform rail, shown for a visualization of several, holds what changes the series:

  • Align by day, week or month, shown only for a line, a scatter, a matrix or a histogram. A distribution, a heatmap and a grouping always count days, since a choice averaged over a week is no choice. A week starts on the person’s first day of the week, Monday unless set to Sunday (Calendar Drill-down; periodKey).
  • How a period’s days fold, beside it once a week or a month is chosen: Sum, Mean, Median, Min or Max of the days (DAY_FOLDS in packages/core/src/day-folds.ts). Unset, a series whose days add up (read as Sum, Count or Any, or a text read for a word) is summed and the rest are averaged (groupSeries): a week of sleep is its hours, a week of Weight its mean.
  • Join on the date: every series is aligned by day.
  • Days on hold, left out or kept, offered when the profile has any (see Disruptions).
  • Rollup, a trailing 7-day mean drawn over a line, on by default, only while days are days.
  • Lag, a stepper from −14 to +14 days, on a scatter while days are days.
  • Control for, toggles over the third series onward, on a scatter (see Statistics Across Trackables).
  • Contains, a search box shown when a text series is picked, and Common words, dropped or kept, for a single text series’ word counts.

Lag and smoothing are counted in days, so neither applies to weeks or months. The rail’s foot says how many days (or weeks or months) are aligned and how many series there are.

What the value types allow

The kinds of the selected series, and how many there are, decide everything: the mode, the charts it offers, what grouping means and which statistics show. seriesModel in the engine makes that decision as data, with no rendering, so the editor and a saved visualization cannot read one selection two ways; the editor keeps only the icon for each chart (VIZ_ICON).

One classifier, seriesKind, sorts a trackable by whether its values carry a magnitude:

seriesKindvalue types
numerica number, an interval, a clock time, a yes/no, an ordered scale, a derived trackable, anything with an interval field
categoricalan unordered scale, an enum, a reference
textfree text
recorda record, which is read through its fields instead

An ordered scale (Mood, Low to Great) and a yes/no carry a magnitude, so they correlate and trend like numbers. An unordered scale, an enum or a reference is an identity: it is counted, never averaged.

selectionmodecharts offeredstatistics
one series with a distribution (a scale, a yes/no, an enum, a reference, text)distributiondistribution; and line and grouped too for an ordered scale or a yes/nomost common, the breakdown, how many
one numeric series without onetrendline, histogram, groupedmean, median, spread and σ
two or more, every one numericcorrelatescatter, line, matrix; with exactly two, grouped where one is a number and the other a scale, heatmap where both are scalescorrelation, regression, lag, partials
exactly two, not both numeric: a number with an unordered scale, or two scales of which one is unordered (a yes/no is a scale)categoricalgrouped for a number by a scale, heatmap for two scalesthe grouping’s ANOVA or the heatmap’s chi-square
anything else: an enum, a reference or text beside another seriesunsupportednonea note that these do not pair

An enum, a reference or text does not pair because it has no value a day to pair on, which is also why its distribution counts entries. A text read for a word does pair, as a yes/no a day.

A timeline is added to any of them when a series has an interval or logs moments, drawing each series’ entries as a lane on one 24-hour axis (SeriesTimeline). The chart chosen is clamped to what the mode offers (effViz), so changing the selection cannot leave the toggle on a chart that no longer applies.

The distinctions behind the table:

  • Magnitude or identity. That split is the whole of seriesKind. A scatter axis, a line and a correlation need a magnitude, so a pairing with an unordered partner gets only the grouped bars and the heatmap, which need none.
  • Days or entries. A scale or a yes/no has one value a day, so its distribution re-reads its day series through its options, keeping every declared option in code order so a gap in the scale shows. An enum, a reference or text has no value a day, so its distribution counts entries, resolves labels (a reference through its catalog), ranks them and folds everything past the top 12 into one “Other” bar.
  • Two bar shapes. A scale or a yes/no, a few fixed options whose shape is the point, is a vertical bar per option in its colours; a ranked frequency of many long labels (an enum, a reference, text) is a horizontal list (rankedDistribution).
  • Grouping is one idea twice. “Mood by weekday” and “Weight by Mood” both bucket a numeric series by a per-day key and compare the buckets’ means: by weekday or month for one series (bucketMeans), by a scale partner’s value that day for two (categoryPair, groupByCategory). Two scales cross-tabulate instead (scalePair, contingency).
  • A text read for a word is a yes/no. Its kind is overlaid as a yes/no for the mode (effectiveSelection), so it distributes as has/hasn’t, correlates with a number, splits a number into groups, or fills a heatmap like any yes/no, while the rail keeps reading its real kind. Only the first text series takes the word, and its name carries it (Symptom · “headache”), so it reads on an axis and in a new visualization’s title.
  • A record opens by field. Each number or reference field is a series of its own (“Strength · weight”), classified by the field’s type and read from the record’s entries.

The charts

  • Line: each series over the aligned days, with the 7-day mean when Rollup is on.
  • Scatter: the first two series paired on the day, or at the lag, with the least-squares line.
  • Matrix: every pair’s correlation as a grid.
  • Distribution: a scale’s days by option, or a ranked list of an enum’s, a reference’s or a text’s top values.
  • Grouped: the mean per weekday or month, or per value of a scale partner.
  • Histogram: a single number’s spread in equal-width bins, Sturges’ count clamped to 5 to 12 so the labels stay legible, the maximum in the last bin (histogram).
  • Heatmap: two scales’ shared days counted per pair of values, every declared value kept as a row or column.
  • Timeline: entries with a start and an end, a lane per series.

A text series’ word counts split on anything that is not a letter or a digit, so accented and non-Latin words survive, drop one-letter scraps and, unless Common words is kept, English stopwords (textFrequency).

Every value axis reads the series’ format, never the stored number. Values are kept in base units, an interval in seconds, and Series.format turns 25200 into “7h”. axisFormat in the engine is the one decision: one series lends its format to the axis; two or more have mixed units, so the ticks stay plain numbers. The line chart calls it itself, so no caller can forget it. The scatter takes bare points rather than series, so it takes formatX and formatY from its caller for its ticks and its tooltip, and components/series/axis-format.test.tsx fails if the editor or a saved visualization’s renderer stops passing them. The rule: derive the format where the component holds the series, and guard it at the boundary where it does not.

Statistics

The Statistics panel opens on Simple, a plain verdict: which way the two go together and whether there is enough data to say so. Advanced adds the figures behind it. What each shows, and how every verdict is gated on significance and corrected for the comparisons made, is in Statistics Across Trackables. The statistics are a reading, not a finding: they say “here is a relationship”, never what caused it, and never medical advice.

Keeping it

The rail’s foot holds Shown on (Home, and each trackable the series read, where it becomes a tab), Save and Save as new. Save updates the visualization that was opened and Save as new makes another; what is stored and where it shows is in Visualization Persistence. A new one’s title comes from its selection (“Weight vs Water”, “Weight by Mood”, “Mood × Headache”); a saved one keeps its name.

How it computes

The engine, components/series/engine.ts, renders nothing and calls no repository: series in, numbers out, everything composed from the one day → value map, so joins, lags, correlations and matrices are functions over it. It takes its least-squares fit and Pearson’s p from @datler/core’s stats.ts (leastSquares, pearsonP), which the scatter’s fit line and db:check use too. It lives in the renderer rather than in @datler/core because it works in display units: it imports the renderer’s units and formatting (toDisplay and unitLabel from lib/units, the day and scale formatters from lib/format and lib/series), and moving it would take that layer into core with it. The special functions the tests need, a Lanczos lgamma, the regularized incomplete beta and gamma, Student’s t, Pearson’s p and its Fisher interval, and Benjamini–Hochberg, are in @datler/core (packages/core/src/stats.ts), free of any domain.

Everything is computed on read, and what is kept is the question, never its answer: a saved visualization is its spec, drawn again from the entries each time it is opened. The engine runs in the renderer over every entry held in memory, not in SQL, so a large profile pays for it on first paint; the formula of a derived trackable is kept expressible in SQL (Derived Trackables), and the engine’s results are what any SQL path would have to match.

Where a derived series sums foods with missing or guessed figures, a note under the statistics says how many days are minimums (≥) or include guesses (~).

Statistics Across Trackables · Visualization Persistence · Visualizations · Readings · Derived Trackables · Trackable · Page Specs