Datler draws days as calendars in three shapes. The Calendar page, /calendar, shows every trackable at once as a Month, a Week, 3 days or a Day. A trackable’s Calendar tab (Visualizations) is a contribution grid of its days over a period, on its page, its Home card and in the editor, and a tag’s counts the entries under it. A day in the month grid opens Day Detail, the day’s entries to read, change and add to.
Month, Week, 3 days and Day
One header, four ranges. The Calendar page (apps/renderer/src/routes/calendar.index.tsx) shows a Month, a Week, 3 days or a Day, chosen in its header beside previous, Today, next and the title of what is shown (“Oct 5 – 11, 2026”). Which of the four and the day are in the address (view, date), so Back returns to them, and the last one chosen is kept per profile (calendar:view); with none, Week. Switching between them keeps the days in sight. A week starts on the day the person picks in Settings, Calendar: Monday unless set to Sunday (useWeekStart, the setting calendar:week-start), in the month grid as well, which takes its month from the page. The setting is a weekday, Sunday 0, so another start needs only an option; Saturday, the start in parts of the Middle East, is the only other in common use and waits for someone who needs it. One rule places a day in its week (dayOfWeek and weekStartOf in packages/core/src/day.ts), and every view that lays days out by week reads it: these calendars, the contribution grid’s columns, a visualization aligned by week, the weekday means and the Rundown’s every-so-many-weeks rule. A swipe drags the shown days aside for the next or previous ones, and a pinch zooms the hours (half to three times) about its middle. A day’s heading in the Week or 3 days opens its Day. Which trackables are drawn is a filter over them (TrackablesLegend, on the library’s filter state), kept per profile (calendar:filter).
Week, 3 days and Day (components/calendar/WeekGrid.tsx) draw every trackable at once, each day a column from midnight to midnight with the hours running down, an hour 44 px tall in a Week, 56 in 3 days and 72 in a Day before any pinch, opening scrolled to 6 AM, the headings and bands staying visible as it scrolls:
- an entry with an interval is a block from its start to its end, tinted in its trackable’s colour, cut at midnight as a night’s sleep is; one still running is outlined dashed;
- a moment is a short pill with a dot at its time;
- everything is drawn at least 18 px tall, so a mark or a short entry has room for its name;
- entries that overlap sit side by side as a calendar app lays them (
placeDay): each in the first free column at its start, a run of overlapping entries sharing the width, each spreading into free columns beside it; - anything a day or longer (a Disruption, a trip) is a band across the top, one row per set of bands that would overlap (
bandsOf); - an entry holding others (a routine’s entry, or a Breakfast standing in for its egg and cheese,
holderOfinpackages/core/src/containers.ts) is one block holding its parts, which are not drawn again on their own; - the time now is a line across today, and a day on hold is notched in its heading.
A Day is one column of the same, wide enough for each block to say its value; in a Day and in a band, the value is shown with the times taken off it (withoutWhen).
The Week and the Day are also where a day’s time can be seen going: every interval in its trackable’s colour, with time two of them share drawn side by side rather than added up. For a few trackables over many days, a visualization of several draws each as a lane on one 24-hour axis (Editor for Several Trackables).
A block opens a popover, as a chart’s marks do: its trackable, linking to its page; when it was, naming the day it started (a span of days names its days); the value and the note; and Edit, which opens the entry over the page (see Inspect Without Leaving).
The page fits a phone’s width with no sideways scroll.
The month grid
EntryCalendar (components/calendar/EntryCalendar.tsx) is a month of day cells, its entries read by one SQL query for the month (Data Table). Each cell (CalendarDay) carries its day number, a mark per entry no other entry that month holds (the query’s nested, so a holder is one mark), and its state: today outlined in the accent, a day with nothing logged anywhere (Untracked) outlined dashed, a day on hold notched in its corner. Each mark is a dot, shaded as the contribution grid shades the day. A click opens the day’s detail. A day outside the month shows its number, faded, with nothing to open and no marks. The month grid is the Calendar page’s Month; a trackable’s page draws the contribution grid.
The grid draws a day only once its entries and their trackables have both loaded, since an entry whose trackable is not known yet would draw as no mark. Until then it shows MonthSkeleton, the grid’s weekdays and six weeks of empty cells, so the month arrives where it will stand; the Calendar page’s Month shows it too while its trackables load. A wait longer than LOADING_INDICATOR_DELAY_MS (in hooks/useShownAfter.ts, 400 ms) adds the loading indicator, so a quick load never flashes one. A new month keeps the last one’s entries while it loads, so only a first load waits.
Day Detail
Clicking a day opens Day Detail: every entry that day, grouped by trackable, earliest first. DayDetail is the component and DayDetailSheet its host, and the split between desktop and phone is CSS, not two renderings: the library’s Sheet, full width below md and a 440 px sheet from the right at md and up, closed by its backdrop or Escape.
- An entry holding others that day is one row saying how many it holds, opening to them beneath it, a part of another trackable named by it (
entryRows); a part whose holder is not in the list is a row of its own. - Each entry shows its time, its value, its issues and what it is inside, where that is not shown around it. Editing one opens the entry editor in place, with a way back to the list, rather than a dialog over the sheet. Deleting asks first and offers Undo (
useEntryDelete). - A trackable’s name heads its entries and links to its page, closing the sheet as it goes.
- Log something opens the entry panel (Entry Panel) for any trackable you log, on that day rather than the app’s moment; opened from a trackable’s calendar it is that trackable’s. When it happened is on that day, and when it was recorded stays the real now.
- Entries can be ticked and put into a routine together (Events).
A trackable’s calendar passes its id, which both filters the day’s entries and pins the add form to it; a tag’s passes its subtree, and the add form asks which trackable. /calendar/day?date=…&trackable_id=… renders the same component as a full page, reached by its address: no screen links to it, and the sheet, not this page, is what a phone sees.
The contribution grid
TrackableCalendar (components/charts/TrackableCalendar.tsx) is a grid of every day in the calendar visualization’s period, 13, 26 or 52 weeks (26 by default), a cell a day, the shape a Home card and the editor draw. A cell’s shade encodes one of three things, the calendar’s scale:
- value: the day’s value on a ramp, or its zone, or its option’s colour;
- count: how many entries that day, on a fewer-to-more ramp;
- presence: logged or not, one flat shade.
Unset, it shades by value. The choice is offered only where the three differ, on a number, a length or a time, and Count only where there are entries to count (scalesFor). An unlogged day is empty, and an Untracked one hollow. A cell can be pinned to Inspect its day (Inspect Without Leaving).
Its legend samples the same colour rule, so it cannot drift from its cells. It names each zone the data falls in, and shows the ramp only when some day falls outside every zone, sampled without zones so a sample point does not take the colour of the zone it lands in.
One colour rule
Where a cell’s colour depends on its value, one function decides it, heatCellColor(trackable, value) (apps/renderer/src/lib/colors.ts):
- a number, an interval or a time is a lightness ramp across the trackable’s own colour, low dark and high light; the calendar visualization’s
directioninverts it; - a declared zone wins over the ramp;
- a scale or an enum takes the matching option’s colour;
- a yes/no, or anything read as Any, is the trackable’s colour for yes and a faint wash of it for no;
- anything else logged is the trackable’s colour;
- a null return is an unlogged day, so a logged 0 or No stays a real, faint cell while an empty day stays empty.
The contribution grid calls it with the trackable’s options and its range. The month grid calls it through getColor, which unwraps an entry’s value, converts it and the trackable’s minimum and maximum to the unit the person reads it in, and passes those, their unit preferences and the trackable’s options, so a weigh-in falls in the zone it reads in and a scale’s dots and chips take their option’s colour. Its chips name an item and an option as everywhere else.
The return is a CSS colour, not always a hex: a No is a color-mix(…). Callers treat it as opaque, and the one sanctioned reader is outlineFor, a darker mix for a mark’s outline.
This note owns the rule, which value becomes which colour and that one function decides it. The settings it reads, a calendar’s scale, direction and period, are the calendar visualization’s own, set in the editor (Visualizations).
Derived trackables and tags
A derived trackable has no entries of its own, so it puts no marks on the month grid and no rows in a Day Detail, and the Calendar page’s trackables are the ones you log. Its Calendar tab is the contribution grid of its computed days, shaded by value or presence, the same as its Home card; nothing opens on a day, since no entry is behind it. A tag’s Calendar tab, and a derived trackable’s with members, count the entries under it each day, and a day opens their Day Detail. The month grid stays the entry log: shading its cells from a computed series would put two models in one surface, a day you can add entries to coloured by something no entry produced.
Related
Visualizations · Entry · Data Table · Inspect Without Leaving · Presentation Layer · Disruptions