What an insight is
A name and a function. The function gets that year's entries and returns a string; the plugin prints it in the Statistics tab under the heatmap. That is the whole API — there is no reporting language to learn, because it is just JavaScript over an array.
Insights live in a dataviewjs block rather than the
heatmap-tracker codeblock, for the boring reason that
YAML cannot hold a function. Everything else about the heatmap works
the same way:
```dataviewjs
const trackerData = {
entries: [],
insights: [
{
name: "Days logged",
calculate: ({ yearEntries }) => yearEntries.length.toString(),
},
],
};
for (let page of dv.pages('"daily notes"').where((p) => p.steps)) {
trackerData.entries.push({
date: page.file.name,
filePath: page.file.path,
intensity: page.steps,
});
}
renderHeatmapTracker(this.container, trackerData);
```
Each entry in yearEntries carries
date (YYYY-MM-DD), value (the
number read from your note), intensity (the level it
was mapped to on the color scale) and filePath. Every
recipe below is one object you drop into that
insights array — several at once is fine, each becomes
its own line.
Totals and averages
The numbers you'd reach for a calculator to get.
🧮 Total for the year
Total hours slept, pages read, kilometres run — one line, any unit.
{
name: "Total",
calculate: ({ yearEntries }) =>
yearEntries
.reduce((sum, e) => sum + (e.value || 0), 0)
.toString(),
}
📊 Average per logged day
The mean across days that have an entry — blanks don't drag it down.
{
name: "Average per day",
calculate: ({ yearEntries }) => {
if (!yearEntries.length) return "No data";
const total = yearEntries.reduce((s, e) => s + (e.value || 0), 0);
return (total / yearEntries.length).toFixed(2);
},
}
🏆 Best day of the year
The single date you scored highest — the one worth remembering.
{
name: "Best day",
calculate: ({ yearEntries }) => {
if (!yearEntries.length) return "No data";
const best = yearEntries.reduce((max, e) =>
(e.value || 0) > (max.value || 0) ? e : max);
return `${best.value} on ${best.date}`;
},
}
Streaks and goals
Did you keep it up, and how often did you clear the bar.
🔥 Longest streak above a goal
Consecutive logged days that met your target. Change the number, change the goal.
{
name: "Longest streak",
calculate: ({ yearEntries }) => {
const goal = 8000;
let streak = 0, max = 0;
yearEntries.forEach((e) => {
streak = e.value >= goal ? streak + 1 : 0;
max = Math.max(max, streak);
});
return max.toString();
},
}
🎯 Days you hit the goal
How many days cleared the bar — the number most people actually want.
{
name: "Days at goal",
calculate: ({ yearEntries }) =>
yearEntries.filter((e) => e.value >= 8000).length.toString(),
}
🕳️ Longest gap
The longest run of days with nothing logged — where the habit broke.
{
name: "Longest gap",
calculate: ({ yearEntries }) => {
const dates = yearEntries
.map((e) => new Date(e.date).getTime())
.sort((a, b) => a - b);
let max = 0;
for (let i = 1; i < dates.length; i++) {
const gap = (dates[i] - dates[i - 1]) / 86400000 - 1;
max = Math.max(max, gap);
}
return `${max} days`;
},
}
Patterns in time
The grid hints at these; an insight states them.
📅 Most active weekday
Which day of the week you log most often. Usually not the one you'd guess.
{
name: "Most active weekday",
calculate: ({ yearEntries }) => {
const counts = {};
if (!yearEntries.length) return "No data";
yearEntries.forEach((e) => {
const day = new Date(e.date)
.toLocaleDateString("en-US", { weekday: "long" });
counts[day] = (counts[day] || 0) + 1;
});
return Object.entries(counts)
.reduce((a, b) => (b[1] > a[1] ? b : a))[0];
},
}
🥇 Most active month
The month you showed up most — seasonality you can't see in the grid alone.
{
name: "Most active month",
calculate: ({ yearEntries }) => {
const months = {};
if (!yearEntries.length) return "No data";
yearEntries.forEach((e) => {
const m = new Date(e.date)
.toLocaleDateString("en-US", { month: "long" });
months[m] = (months[m] || 0) + (e.value || 0);
});
return Object.entries(months)
.reduce((a, b) => (b[1] > a[1] ? b : a))[0];
},
}
🏖️ Weekend vs weekday
Whether your habit survives Saturday. Averages, side by side.
{
name: "Weekend vs weekday",
calculate: ({ yearEntries }) => {
const avg = (list) =>
list.length
? list.reduce((s, e) => s + (e.value || 0), 0) / list.length
: 0;
const isWeekend = (e) => [0, 6].includes(new Date(e.date).getDay());
const we = avg(yearEntries.filter(isWeekend));
const wd = avg(yearEntries.filter((e) => !isWeekend(e)));
return `${wd.toFixed(1)} weekday · ${we.toFixed(1)} weekend`;
},
}
Progress and consistency
Whether this year is going anywhere.
📈 Second half vs first
Are you trending up or coasting? Compares the two halves of the year.
{
name: "Trend",
calculate: ({ yearEntries }) => {
const mid = new Date(yearEntries[0]?.date).getFullYear() + "-07-01";
const avg = (list) =>
list.length
? list.reduce((s, e) => s + (e.value || 0), 0) / list.length
: 0;
const h1 = avg(yearEntries.filter((e) => e.date < mid));
const h2 = avg(yearEntries.filter((e) => e.date >= mid));
const diff = h1 ? ((h2 - h1) / h1) * 100 : 0;
return `${diff >= 0 ? "+" : ""}${diff.toFixed(0)}%`;
},
}
🗓️ Coverage of the year
What share of days you actually logged — the honest adherence number.
{
name: "Days logged",
calculate: ({ yearEntries }) => {
if (!yearEntries.length) return "No data";
const year = new Date(yearEntries[0].date).getFullYear();
const days = new Date(year, 1, 29).getDate() === 29 ? 366 : 365;
const pct = (yearEntries.length / days) * 100;
return `${yearEntries.length} / ${days} (${pct.toFixed(0)}%)`;
},
}
📉 Intensity distribution
How many days land on each color of your scale, as one line.
{
name: "Distribution",
calculate: ({ yearEntries }) => {
const dist = {};
yearEntries.forEach((e) => {
const i = Number(e.intensity) || 0;
dist[i] = (dist[i] || 0) + 1;
});
return Object.entries(dist)
.map(([i, count]) => `L${i}: ${count}`)
.join(" · ");
},
}
Rules worth knowing
-
Return a string. A number renders as nothing. End with
.toString(),.toFixed(1)or a template literal. -
Handle the empty year. Switch to a year with no data and
yearEntriesis[]—reducewithout an initial value throws there. Return"No data"early. -
valuefor totals,intensityfor buckets.valueis your raw number;intensityis the color level it landed on. - Only the displayed year. Paging to another year recalculates every insight against that year's entries — the numbers follow the grid.
- Missing days are absent, not zero. Entries exist only for days you logged, so an average is over logged days unless you divide by the calendar yourself.
- Keep it cheap. It runs on every render. A pass or two over a few hundred entries is nothing; anything heavier is felt.
FAQ
What is an insight in Heatmap Tracker?
An insight is a named function you add to trackerData.insights. It receives that year's entries and returns a string, which the plugin renders in the Statistics tab next to the built-in totals and streaks.
Do I need Dataview to use insights?
You need a dataviewjs block, because an insight is a JavaScript function and a heatmap-tracker codeblock can only hold YAML values. The heatmap itself works without any script.
What data does an insight get?
One argument, { yearEntries }: the entries for the currently displayed year. Each entry has date (YYYY-MM-DD), value (the number read from your note), intensity (the mapped color level) and filePath.
Why does my insight show nothing or break the heatmap?
An insight must return a string. Returning a number or undefined shows nothing, and a throw during calculation stops that insight rendering — guard the empty case, for example by returning "No data" when yearEntries is empty, and call toString() or toFixed() on numbers.
Can an insight use value or does it have to use intensity?
Both are available. value is the raw number from your note, intensity is the level it was mapped to on the color scale, so totals and averages normally use value while distribution-style insights use intensity.
How many insights can one heatmap have?
There is no limit; insights is an array and each entry becomes its own line in the Statistics tab. They are recalculated when the heatmap renders and when you switch years.