<%* const dv = app.plugins.plugins["dataview"].api;
const statsFilePath = "50 Resources/56 Obsidian Publish/Stats";
// Constants for calculations const WORDS_PER_MINUTE = 250; // Average reading speed const WORDS_PER_BOOK = 90000; // Average book length
// Important tags to track
const noteTypes = [
'permanent_notes', // Permanent notes
'literature_notes', // Literature notes
'contact', // Contact notes
'meeting_notes', // Meeting notes
'articles/published', // Published articles
'articles/drafts', // Article drafts
'mocs', // Maps of Content
'newsletters', // Newsletters
'people', // People notes
'expressions', // Expressions notes
'my_quotes', // My quotes notes
'quotes', // Quotes notes
'book_notes', // Book notes
'projects', // Projects notes
];
// Tags to ignore (generally people job roles) const EXCLUDED_TOPICS = [ ...noteTypes, 'author', 'writer', 'professor', 'salesman', 'motivational_speaker', 'memorist', 'statesman', 'poet', 'businessman', 'entrepreneur', 'philosopher', 'writer', 'journalist', 'indie_hacker', 'solopreneur', ];
const EXCLUDED_FOLDERS = [ '.git', '.obsidian', '.smart-chats', '.smart-env', '.smtcmp_chat_histories', '.stfolder', '.stversions', '.vaultexplorer', '30 Areas/35 Contacts', '30 Areas/37 Meeting notes', '30 Areas/39 Readwise highlights', '40 Journal', '50 Resources/54 Templates', 'copilot-custom-prompts', 'textgenerator', 'TO IMPORT', 'textgenerator', ];
// ------------------------------------------------------------------ // Helper functions // ------------------------------------------------------------------
// Check if a folder should be excluded function isExcludedFolder(folderPath) { return EXCLUDED_FOLDERS.some(excluded => folderPath.startsWith(excluded)); }
// Add this helper function at the top level function isUnderAreas(file) { return file.path.startsWith('30 Areas/'); }
function isPeriodicNote(file) { const filename = file.basename; // Match patterns for different types of periodic notes const patterns = { daily: /^\d{4}-\d{2}-\d{2}$/, weekly: /^\d{4}-W\d{2}$/, monthly: /^\d{4}-\d{2}$/, yearly: /^\d{4}$/ };
// Check which pattern matches
for (const [type, pattern] of Object.entries(patterns)) {
if (pattern.test(filename)) return type;
}
return null;
}
// Add this helper function after isPeriodicNote function isSubtagOf(tag, parentTag) { try { if (!tag || !parentTag) { return false; } if (typeof tag !== 'string' || typeof parentTag !== 'string') { return false; } // Split the tag into parts and check if parentTag matches the first part const parts = tag.split('/'); return parts.length > 1 && parts[0] === parentTag; } catch (error) { return false; } }
// Add this helper function to count notes with a specific tag and its subtags function countNotesWithTag(tag, publishedTopics) { if (!tag || !publishedTopics) { return 0; }
let count = 0;
for (const [topic, topicCount] of Object.entries(publishedTopics)) {
if (topic === tag || (topic && isSubtagOf(topic, tag))) {
count += topicCount || 0;
}
}
return count;
}
function countWords(text) { return text.match(/\b\w+\b/g)?.length || 0; }
function extractYAMLTags(content) { const yamlRegex = /^---\n(.?)\n---/s; const match = content.match(yamlRegex); if (match) { const yaml = match[1]; const tagMatch = yaml.match(/tags:\s\n((?:\s*-\s*.\n))/); if (tagMatch) { return tagMatch[1].match(/-\s*([\w-]+)/g)?.map(tag => tag.replace('-', '').trim()) || []; } } return []; }
async function countGitCommits() { try { const child_process = require('child_process'); const util = require('util'); const exec = util.promisify(child_process.exec);
const { stdout } = await exec('git rev-list --count HEAD', {
cwd: app.vault.adapter.basePath
});
return parseInt(stdout.trim()) || 0;
} catch (error) {
console.error('Error counting git commits:', error);
return 0;
}
}
function formatBytes(bytes, decimals = 2) { if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
}
function formatReadingTime(minutes) {
if (!minutes) return '0 minutes';
const hours = Math.floor(minutes / 60);
const remainingMinutes = Math.round(minutes % 60);
return hours > 0 ?
${hours} hour${hours !== 1 ? 's' : ''}, ${remainingMinutes} minute${remainingMinutes !== 1 ? 's' : ''}
:
${remainingMinutes} minute${remainingMinutes !== 1 ? 's' : ''}
;
}
function formatTagName(tag) { if (!tag) return ''; return tag.split('/') .reverse() .map(part => part.replace(/_/g, ' ').replace(/#/g, '')) .map(part => part.replace(/\b\w/g, l => l.toUpperCase())) .join(' '); }
// Format age in days to a readable format function formatAge(days) { const years = Math.floor(days / 365); const months = Math.floor((days % 365) / 30); const remainingDays = Math.floor(days % 30);
const parts = [];
if (years > 0) parts.push(`${years} year${years !== 1 ? 's' : ''}`);
if (months > 0) parts.push(`${months} month${months !== 1 ? 's' : ''}`);
if (remainingDays > 0) parts.push(`${remainingDays} day${remainingDays !== 1 ? 's' : ''}`);
return parts.join(', ');
}
// ------------------------------------------------------------------
// Gather metrics
// ------------------------------------------------------------------
async function gatherMetrics(publishedFiles, allVaultFiles, publishedFilesByPathMap) {
new Notice(Gathering metrics...
);
const metrics = {
publication: {
total_published: publishedFilesByPathMap.size,
published_by_year: {},
published_by_month: {},
published_words: 0,
published_topics: {},
published_read_time_minutes: 0,
published_books_equivalent: 0,
concept_notes: 0,
topic_counts: {},
parent_tags: []
},
overall: {
total_notes: allVaultFiles.length,
total_words: 0,
total_read_time_minutes: 0,
books_equivalent: 0,
size: {
total_bytes: 0,
attachments_bytes: 0,
md_files_bytes: 0,
git_commits: 0
},
note_types: {},
notes_edited_this_year: 0,
periodic_notes: {
daily: {
count: 0,
words: 0,
average_words: 0,
internal_links: 0,
average_internal_links: 0
},
weekly: {
count: 0,
words: 0,
average_words: 0,
internal_links: 0,
average_internal_links: 0
},
monthly: {
count: 0,
words: 0,
average_words: 0,
internal_links: 0,
average_internal_links: 0
},
yearly: {
count: 0,
words: 0,
average_words: 0,
internal_links: 0,
average_internal_links: 0
}
},
attachments: {
public: 0,
private: 0
},
templates: 0,
canvases: 0,
links: {
internal: 0,
external: 0,
average_internal: 0,
average_external: 0,
},
folders: {
count: 0,
by_path: {},
growth_rates: {},
stats: {} // Will store additional stats for each folder
},
topics: {
growth_rates: {} // Will store growth rates for each topic
},
total_tags: 0,
mocs: {
count: 0,
total_internal_links: 0,
average_internal_links: 0
},
voice_notes: 0,
total_tasks: 0,
people_notes: {
count: 0,
internal_links: 0,
average_internal_links: 0
},
content_age: {
average_days: 0,
total_days: 0,
oldest_note: {
days: 0,
date: null
},
newest_note: {
days: Infinity,
date: null
},
updates: {
last_week: 0,
last_month: 0,
last_quarter: 0,
last_year: 0
}
},
tags: {
total: 0,
notes_without_tags: 0,
total_tag_usages: 0,
average_per_note: 0,
total_area_notes: 0 // Add counter for notes under Areas
},
updates: {
last_week: 0,
last_month: 0,
last_quarter: 0,
last_year: 0
}
}
};
// Add git commit counting
metrics.overall.size.git_commits = await countGitCommits();
// Load all vault files (including non-markdown)
const vaultFiles = app.vault.getAllLoadedFiles();
// Initialize folders Set and process files for size and type counts
const folders = new Set();
for (const file of vaultFiles) {
// Get folder path and add to folders Set if valid
const folderPath = file.parent?.path;
if (folderPath && !isExcludedFolder(folderPath)) {
folders.add(folderPath);
}
// Process file size and type
const fileSize = file.stat?.size || 0;
metrics.overall.size.total_bytes += fileSize;
const ext = file.extension || '';
const basename = file.basename || '';
if (ext === 'canvas') {
metrics.overall.canvases++;
} else if (basename && basename.startsWith('TPL ')) {
metrics.overall.templates++;
} else if (ext === 'md') {
metrics.overall.size.md_files_bytes += fileSize;
} else if (ext) { // Any other file with extension is considered an attachment
metrics.overall.size.attachments_bytes += fileSize;
const path = file.path || '';
if (publishedFilesByPathMap.has(path)) {
metrics.overall.attachments.public++;
} else {
metrics.overall.attachments.private++;
}
}
}
// Set the folder count after processing all files
metrics.overall.folders.count = folders.size;
// Create a Set to store unique tags
const uniqueTags = new Set();
// Track creation dates for folders and topics
const folderDates = {};
const topicDates = {};
// Process markdown files
for (const file of allVaultFiles) {
if (file.deleted) continue;
// Read file content
const content = await app.vault.read(file);
const wordCount = countWords(content);
metrics.overall.total_words += wordCount;
// Get file metadata
const metadata = app.metadataCache.getFileCache(file);
const frontmatter = metadata?.frontmatter;
const tags = Array.isArray(frontmatter?.tags) ? frontmatter.tags : []; // Ensure tags is always an array
const folderPath = file.parent?.path;
// Check if published
const isPublished = publishedFilesByPathMap.has(file.path);
if (isPublished) {
metrics.publication.published_words += wordCount;
// Extract creation date
const createdDate = frontmatter?.created ? moment(frontmatter.created) : moment(file.stat.ctime);
const year = createdDate.year();
const month = createdDate.format('YYYY-MM');
metrics.publication.published_by_year[year] = (metrics.publication.published_by_year[year] || 0) + 1;
metrics.publication.published_by_month[month] = (metrics.publication.published_by_month[month] || 0) + 1;
// Process tags for published notes
tags.forEach(tag => {
metrics.publication.published_topics[tag] = (metrics.publication.published_topics[tag] || 0) + 1;
});
}
// Count notes edited this year
const lastModified = moment(file.stat.mtime);
if (lastModified.year() === moment().year()) {
metrics.overall.notes_edited_this_year++;
}
// Process periodic notes
const periodicNoteType = isPeriodicNote(file);
if (periodicNoteType) {
const stats = metrics.overall.periodic_notes[periodicNoteType];
stats.count++;
stats.words += wordCount;
// Track internal links for all periodic notes
const internalLinks = (metadata?.links?.length || 0) + (metadata?.embeds?.length || 0) -
(metadata?.links?.filter(l => l.link.startsWith('http'))?.length || 0);
stats.internal_links += internalLinks;
}
// Process note types from tags
noteTypes.forEach(type => {
if (tags.includes(type)) {
metrics.overall.note_types[type] = (metrics.overall.note_types[type] || 0) + 1;
}
});
// Count links
const links = metadata?.links || [];
const embeds = metadata?.embeds || [];
// Count external links (both markdown links and raw URLs)
const markdownExternalLinks = links?.filter(l => l.link.startsWith('http')) || [];
const rawUrlMatches = content.match(/(?<![\[\(])(https?:\/\/[^\s\)]+)/g) || [];
const totalExternalLinks = markdownExternalLinks.length + rawUrlMatches.length;
metrics.overall.links.internal += (links.length + embeds.length - markdownExternalLinks.length);
metrics.overall.links.external += totalExternalLinks;
// Process tags for total tags count
tags.forEach(tag => uniqueTags.add(tag));
// Count MOCs and their internal links
if (tags.includes('mocs')) {
metrics.overall.mocs.count++;
const internalLinks = (metadata?.links?.length || 0) + (metadata?.embeds?.length || 0);
metrics.overall.mocs.total_internal_links += internalLinks;
}
// Count voice notes
if (file.path.startsWith('30 Areas/38 Voice notes/')) {
metrics.overall.voice_notes++;
}
// Count tasks
const tasks = metadata?.listItems?.filter(item => item.task)?.length || 0;
metrics.overall.total_tasks += tasks;
// Track internal links and quotes for people notes
if (tags.includes('people')) {
metrics.overall.people_notes.count++;
// Count internal links
const internalLinks = (metadata?.links?.length || 0) + (metadata?.embeds?.length || 0) -
(metadata?.links?.filter(l => l.link.startsWith('http'))?.length || 0);
metrics.overall.people_notes.internal_links += internalLinks;
}
// Get creation date once and reuse it
const createdDate = moment(frontmatter?.created || file.stat.ctime);
const modifiedDate = moment(file.stat.mtime);
const now = moment();
// Add to total age for average calculation
const ageDays = now.diff(createdDate, 'days');
metrics.overall.content_age.total_days += ageDays;
// Track oldest and newest notes
if (ageDays > metrics.overall.content_age.oldest_note.days) {
metrics.overall.content_age.oldest_note.days = ageDays;
metrics.overall.content_age.oldest_note.date = createdDate;
}
if (ageDays < metrics.overall.content_age.newest_note.days) {
metrics.overall.content_age.newest_note.days = ageDays;
metrics.overall.content_age.newest_note.date = createdDate;
}
// Handle all folder-related metrics in one place
if (folderPath && !isExcludedFolder(folderPath)) {
// Count notes per folder
metrics.overall.folders.by_path[folderPath] = (metrics.overall.folders.by_path[folderPath] || 0) + 1;
// Track folder dates for growth rates
folderDates[folderPath] = folderDates[folderPath] || [];
folderDates[folderPath].push(createdDate);
// Initialize folder stats if not exists
if (!metrics.overall.folders.stats[folderPath]) {
metrics.overall.folders.stats[folderPath] = {
words: 0,
internal_links: 0,
note_count: 0
};
}
// Update folder stats
metrics.overall.folders.stats[folderPath].words += wordCount;
metrics.overall.folders.stats[folderPath].note_count++;
// Count internal links for this note
const internalLinks = (metadata?.links?.length || 0) + (metadata?.embeds?.length || 0) -
(metadata?.links?.filter(l => l.link.startsWith('http'))?.length || 0);
metrics.overall.folders.stats[folderPath].internal_links += internalLinks;
}
// Track topic dates
tags.forEach(tag => {
topicDates[tag] = topicDates[tag] || [];
topicDates[tag].push(createdDate);
});
// Track content freshness based on last modification
const daysSinceModified = now.diff(modifiedDate, 'days');
if (daysSinceModified <= 7) {
metrics.overall.content_age.updates.last_week++;
}
if (daysSinceModified <= 30) {
metrics.overall.content_age.updates.last_month++;
}
if (daysSinceModified <= 90) {
metrics.overall.content_age.updates.last_quarter++;
}
if (daysSinceModified <= 365) {
metrics.overall.content_age.updates.last_year++;
}
// Process tags statistics - update this section
if (isUnderAreas(file)) {
metrics.overall.tags.total_area_notes++;
if (tags.length === 0) {
metrics.overall.tags.notes_without_tags++;
}
metrics.overall.tags.total_tag_usages += tags.length;
}
// Track topic dates
tags.forEach(tag => {
topicDates[tag] = topicDates[tag] || [];
topicDates[tag].push(createdDate);
});
}
metrics.overall.total_tags = uniqueTags.size;
// Calculate derived metrics
metrics.overall.total_read_time_minutes = metrics.overall.total_words / WORDS_PER_MINUTE;
metrics.overall.books_equivalent = metrics.overall.total_words / WORDS_PER_BOOK;
metrics.publication.published_read_time_minutes = metrics.publication.published_words / WORDS_PER_MINUTE;
metrics.publication.published_books_equivalent = metrics.publication.published_words / WORDS_PER_BOOK;
// Calculate averages
metrics.overall.links.average_internal = metrics.overall.links.internal / metrics.overall.total_notes;
metrics.overall.links.average_external = metrics.overall.links.external / metrics.overall.total_notes;
// After processing all files, calculate averages
Object.keys(metrics.overall.periodic_notes).forEach(type => {
const notes = metrics.overall.periodic_notes[type];
notes.average_words = notes.count > 0 ? Math.round(notes.words / notes.count) : 0;
notes.average_internal_links = notes.count > 0 ? notes.internal_links / notes.count : 0;
});
// Calculate daily/monthly/yearly averages
const dailyStats = metrics.overall.periodic_notes.daily;
dailyStats.books = dailyStats.words / WORDS_PER_BOOK;
dailyStats.reading_minutes = dailyStats.words / WORDS_PER_MINUTE;
dailyStats.reading_hours = dailyStats.reading_minutes / 60;
const weeklyStats = metrics.overall.periodic_notes.weekly;
weeklyStats.books = weeklyStats.words / WORDS_PER_BOOK;
weeklyStats.reading_minutes = weeklyStats.words / WORDS_PER_MINUTE;
weeklyStats.reading_hours = weeklyStats.reading_minutes / 60;
const monthlyStats = metrics.overall.periodic_notes.monthly;
monthlyStats.books = monthlyStats.words / WORDS_PER_BOOK;
monthlyStats.reading_minutes = monthlyStats.words / WORDS_PER_MINUTE;
monthlyStats.reading_hours = monthlyStats.reading_minutes / 60;
const yearlyStats = metrics.overall.periodic_notes.yearly;
yearlyStats.books = yearlyStats.words / WORDS_PER_BOOK;
yearlyStats.reading_minutes = yearlyStats.words / WORDS_PER_MINUTE;
yearlyStats.reading_hours = yearlyStats.reading_minutes / 60;
// After processing all files, detect parent tags
metrics.publication.parent_tags = Object.keys(metrics.publication.published_topics)
.filter(topic => topic && typeof topic === 'string' && topic.includes('/'))
.map(topic => topic.split('/')[0])
.filter((tag, index, self) => tag && self.indexOf(tag) === index); // Get unique parent tags
// Calculate counts for each parent tag
metrics.publication.parent_tags.forEach(tag => {
metrics.publication.topic_counts[tag] = countNotesWithTag(tag, metrics.publication.published_topics);
});
// Calculate MOC averages
if (metrics.overall.mocs.count > 0) {
metrics.overall.mocs.average_internal_links = metrics.overall.mocs.total_internal_links / metrics.overall.mocs.count;
}
// Calculate averages for people notes
if (metrics.overall.people_notes.count > 0) {
metrics.overall.people_notes.average_internal_links =
metrics.overall.people_notes.internal_links / metrics.overall.people_notes.count;
}
// Calculate average age
metrics.overall.content_age.average_days = Math.round(metrics.overall.content_age.total_days / metrics.overall.total_notes);
// Calculate tag averages
metrics.overall.tags.total = uniqueTags.size;
metrics.overall.tags.average_per_note = metrics.overall.tags.total_area_notes > 0 ?
metrics.overall.tags.total_tag_usages / metrics.overall.tags.total_area_notes : 0;
// Helper function to calculate growth rates
function calculateGrowthRates(dates) {
const now = moment();
const oldest = moment.min(dates);
const daysDiff = now.diff(oldest, 'days') || 1; // Avoid division by zero
return {
per_day: (dates.length / daysDiff).toFixed(2),
per_month: ((dates.length / daysDiff) * 30).toFixed(1),
per_year: ((dates.length / daysDiff) * 365).toFixed(0)
};
}
// Calculate growth rates for folders
Object.entries(folderDates).forEach(([path, dates]) => {
metrics.overall.folders.growth_rates[path] = calculateGrowthRates(dates);
});
// Calculate growth rates for topics
Object.entries(topicDates).forEach(([topic, dates]) => {
metrics.overall.topics.growth_rates[topic] = calculateGrowthRates(dates);
});
return metrics;
}
// ------------------------------------------------------------------ // Generate metrics report // ------------------------------------------------------------------ function generateMetricsReport(metrics) { const report = [ "# Knowledge Base Stats", "Generated on: " + moment().format('YYYY-MM-DD HH:mm:ss'),
"\n## Overall Metrics",
`- Total Notes: ${metrics.overall.total_notes}`,
`- Average Note Age: ${formatAge(metrics.overall.content_age.average_days)}`,
`- Oldest Note: ${formatAge(metrics.overall.content_age.oldest_note.days)} (${metrics.overall.content_age.oldest_note.date.format('YYYY-MM-DD')})`,
`- Newest Note: ${formatAge(metrics.overall.content_age.newest_note.days)} (${metrics.overall.content_age.newest_note.date.format('YYYY-MM-DD')})`,
`- Total Words: ${metrics.overall.total_words.toLocaleString()}`,
`- Total Reading Time: ${Math.round(metrics.overall.total_read_time_minutes)} minutes (${(metrics.overall.total_read_time_minutes/60).toFixed(1)} hours)`,
`- Content Equivalent to ${metrics.overall.books_equivalent.toFixed(1)} books`,
`- Maps of Content: ${metrics.overall.mocs.count} (avg. ${metrics.overall.mocs.average_internal_links.toFixed(1)} internal links)`,
`- Voice Notes to Process: ${metrics.overall.voice_notes}`,
`- Total Tasks: ${metrics.overall.total_tasks}`,
`- Total Folders: ${metrics.overall.folders.count}`,
`- Internal Links: ${metrics.overall.links.internal.toLocaleString()} (${metrics.overall.links.average_internal.toFixed(1)} per note)`,
`- External Links: ${metrics.overall.links.external.toLocaleString()} (${metrics.overall.links.average_external.toFixed(1)} per note)`,
`- People Notes Internal Links: ${metrics.overall.people_notes.internal_links.toLocaleString()} (${metrics.overall.people_notes.average_internal_links.toFixed(1)} per note)`,
"\n### Content Updates",
` - Updated this year: ${metrics.overall.notes_edited_this_year} (${((metrics.overall.notes_edited_this_year / metrics.overall.total_notes) * 100).toFixed(1)}%)`,
` - Updated in last week: ${metrics.overall.content_age.updates.last_week} notes (${((metrics.overall.content_age.updates.last_week / metrics.overall.total_notes) * 100).toFixed(1)}%)`,
` - Updated in last month: ${metrics.overall.content_age.updates.last_month} notes (${((metrics.overall.content_age.updates.last_month / metrics.overall.total_notes) * 100).toFixed(1)}%)`,
` - Updated in last quarter: ${metrics.overall.content_age.updates.last_quarter} notes (${((metrics.overall.content_age.updates.last_quarter / metrics.overall.total_notes) * 100).toFixed(1)}%)`,
` - Updated in last year: ${metrics.overall.content_age.updates.last_year} notes (${((metrics.overall.content_age.updates.last_year / metrics.overall.total_notes) * 100).toFixed(1)}%)`,
"\n### Notes by Type",
...Object.entries(metrics.overall.note_types)
.sort(([,a], [,b]) => b - a)
.map(([type, count]) => {
const displayName = formatTagName(type);
return `- ${displayName} (#${type}): ${count}`;
}),
"\n### Topics",
...([
// Parent tags with their counts
...metrics.publication.parent_tags.map(tag => ({
name: tag,
count: metrics.publication.topic_counts[tag],
growth: metrics.overall.topics.growth_rates[tag],
isParent: true
})),
// Regular tags with their counts
...Object.entries(metrics.publication.published_topics)
.filter(([topic]) => !EXCLUDED_TOPICS.includes(topic) &&
!metrics.publication.parent_tags.includes(topic) &&
!topic.includes('/'))
.map(([topic, count]) => ({
name: topic,
count: count,
growth: metrics.overall.topics.growth_rates[topic],
isParent: false
}))
])
.sort((a, b) => b.count - a.count)
.slice(0, 25)
.map(entry => {
const displayName = formatTagName(entry.name);
const growth = entry.growth;
return `- ${displayName} (#${entry.name}): ${entry.count} notes (${growth.per_month}/month)`;
}),
"\n### Tags",
`- Total Unique Tags: ${metrics.overall.tags.total}`,
`- Notes Under Areas: ${metrics.overall.tags.total_area_notes}`,
`- Notes Without Tags: ${metrics.overall.tags.notes_without_tags} (${((metrics.overall.tags.notes_without_tags / metrics.overall.tags.total_area_notes) * 100).toFixed(1)}% of Areas notes)`,
`- Average Tags per Note: ${metrics.overall.tags.average_per_note.toFixed(1)} (in Areas)`,
"\n### Folders",
...Object.entries(metrics.overall.folders.by_path)
.sort(([,a], [,b]) => b - a)
.slice(0, 15)
.map(([path, count]) => {
const growth = metrics.overall.folders.growth_rates[path];
const stats = metrics.overall.folders.stats[path];
const words = stats.words;
const readingMinutes = words / WORDS_PER_MINUTE;
const booksEquivalent = words / WORDS_PER_BOOK;
const avgInternalLinks = stats.internal_links / stats.note_count;
return [
`- ${path}: ${count} notes (${growth.per_month}/month)`,
` - Total Words: ${words.toLocaleString()}`,
` - Reading Time: ${Math.round(readingMinutes)} minutes (${(readingMinutes/60).toFixed(1)} hours)`,
` - Books Equivalent: ${booksEquivalent.toFixed(2)}`,
` - Average Internal Links: ${avgInternalLinks.toFixed(1)} per note`
].join('\n');
}),
"\n### Periodic Notes",
`- Daily Notes (#daily_notes): ${metrics.overall.periodic_notes.daily.count} (avg. ${metrics.overall.periodic_notes.daily.average_words.toLocaleString()} words)`,
` - Total Words: ${metrics.overall.periodic_notes.daily.words.toLocaleString()}`,
` - Reading Time: ${Math.round(metrics.overall.periodic_notes.daily.reading_minutes)} minutes (${metrics.overall.periodic_notes.daily.reading_hours.toFixed(1)} hours)`,
` - Books Equivalent: ${metrics.overall.periodic_notes.daily.books.toFixed(2)}`,
` - Average Internal Links: ${metrics.overall.periodic_notes.daily.average_internal_links.toFixed(1)} per note`,
`- Weekly Notes (#weekly_notes): ${metrics.overall.periodic_notes.weekly.count} (avg. ${metrics.overall.periodic_notes.weekly.average_words.toLocaleString()} words)`,
` - Total Words: ${metrics.overall.periodic_notes.weekly.words.toLocaleString()}`,
` - Reading Time: ${Math.round(metrics.overall.periodic_notes.weekly.reading_minutes)} minutes (${metrics.overall.periodic_notes.weekly.reading_hours.toFixed(1)} hours)`,
` - Books Equivalent: ${metrics.overall.periodic_notes.weekly.books.toFixed(2)}`,
` - Average Internal Links: ${metrics.overall.periodic_notes.weekly.average_internal_links.toFixed(1)} per note`,
`- Monthly Notes (#monthly_notes): ${metrics.overall.periodic_notes.monthly.count} (avg. ${metrics.overall.periodic_notes.monthly.average_words.toLocaleString()} words)`,
` - Total Words: ${metrics.overall.periodic_notes.monthly.words.toLocaleString()}`,
` - Reading Time: ${Math.round(metrics.overall.periodic_notes.monthly.reading_minutes)} minutes (${metrics.overall.periodic_notes.monthly.reading_hours.toFixed(1)} hours)`,
` - Books Equivalent: ${metrics.overall.periodic_notes.monthly.books.toFixed(2)}`,
` - Average Internal Links: ${metrics.overall.periodic_notes.monthly.average_internal_links.toFixed(1)} per note`,
`- Yearly Notes (#yearly_notes): ${metrics.overall.periodic_notes.yearly.count} (avg. ${metrics.overall.periodic_notes.yearly.average_words.toLocaleString()} words)`,
` - Total Words: ${metrics.overall.periodic_notes.yearly.words.toLocaleString()}`,
` - Reading Time: ${Math.round(metrics.overall.periodic_notes.yearly.reading_minutes)} minutes (${metrics.overall.periodic_notes.yearly.reading_hours.toFixed(1)} hours)`,
` - Books Equivalent: ${metrics.overall.periodic_notes.yearly.books.toFixed(2)}`,
` - Average Internal Links: ${metrics.overall.periodic_notes.yearly.average_internal_links.toFixed(1)} per note`,
"\n### Special Files",
`- Canvas Files: ${metrics.overall.canvases}`,
`- Templates: ${metrics.overall.templates}`,
`- Public Attachments: ${metrics.overall.attachments.public}`,
`- Private Attachments: ${metrics.overall.attachments.private}`,
"\n### Vault Size",
`- Total Vault Size: ${formatBytes(metrics.overall.size.total_bytes)}`,
`- Markdown Files: ${formatBytes(metrics.overall.size.md_files_bytes)}`,
`- Attachments: ${formatBytes(metrics.overall.size.attachments_bytes)}`,
`- Git Commits: ${metrics.overall.size.git_commits.toLocaleString()}`,
"\n## Public Notes Metrics",
`- Total Published Notes: ${metrics.publication.total_published}`,
`- Total Words Published: ${metrics.publication.published_words.toLocaleString()}`,
`- Published Notes Reading Time: ${Math.round(metrics.publication.published_read_time_minutes)} minutes (${(metrics.publication.published_read_time_minutes/60).toFixed(1)} hours)`,
`- Published Content Equivalent to ${metrics.publication.published_books_equivalent.toFixed(1)} books`,
"\n### Public Notes Growth by Year",
...Object.entries(metrics.publication.published_by_year)
.sort(([a], [b]) => b.localeCompare(a))
.map(([year, count]) => `- ${year}: ${count} notes`),
];
return report.join('\n');
}
let publishedFiles = null;
// Load the list of currently published files try { const publishedFilesWrapper = await app.internalPlugins.plugins.publish.instance.apiList(); if(publishedFilesWrapper !== null && publishedFilesWrapper.files) { publishedFiles = publishedFilesWrapper.files; } } catch(e) { console.log("Error while fetching published files list", e); return; }
const publishedFilesByPathMap = new Map(publishedFiles.map((publishedFile) => [publishedFile.path, publishedFile]));
// Load all the Markdown files of the vault const allVaultFiles = app.vault.getMarkdownFiles(); const allVaultFilesByPathMap = new Map(allVaultFiles.map((vaultFile) => { const vaultFileCache = app.metadataCache.getFileCache(vaultFile); return [vaultFile.path, { vaultFile, vaultFileCache, }]; }));
// ------------------------------------------------------------------ // Generate and update metrics // ------------------------------------------------------------------ const metrics = await gatherMetrics(publishedFiles, allVaultFiles, publishedFilesByPathMap); const metricsReport = generateMetricsReport(metrics);
// Create or update stats file
if (!tp.file.find_tfile(statsFilePath)) {
await tp.file.create_new("", statsFilePath);
new Notice(Created ${statsFilePath}.
);
}
const statsFile = tp.file.find_tfile(statsFilePath);
try {
await app.vault.modify(statsFile, metricsReport);
new Notice(Updated ${statsFile.basename}.
);
} catch (error) {
console.log("Error while updating stats file", error);
new Notice("
// Prepare clean data structure for JSON export const jsonStats = { overall: { total_notes: metrics.overall.total_notes, total_words: metrics.overall.total_words, reading_time: { minutes: metrics.overall.total_read_time_minutes, formatted: formatReadingTime(metrics.overall.total_read_time_minutes) }, books_equivalent: metrics.overall.books_equivalent, content_age: { average_days: { days: metrics.overall.content_age.average_days, formatted: formatAge(metrics.overall.content_age.average_days) }, oldest_note: { days: metrics.overall.content_age.oldest_note.days, formatted: formatAge(metrics.overall.content_age.oldest_note.days), date: metrics.overall.content_age.oldest_note.date.format('YYYY-MM-DD') }, newest_note: { days: metrics.overall.content_age.newest_note.days, formatted: formatAge(metrics.overall.content_age.newest_note.days), date: metrics.overall.content_age.newest_note.date.format('YYYY-MM-DD') }, updates: { last_week: { count: metrics.overall.content_age.updates.last_week, percentage: (metrics.overall.content_age.updates.last_week / metrics.overall.total_notes * 100).toFixed(1) }, last_month: { count: metrics.overall.content_age.updates.last_month, percentage: (metrics.overall.content_age.updates.last_month / metrics.overall.total_notes * 100).toFixed(1) }, last_quarter: { count: metrics.overall.content_age.updates.last_quarter, percentage: (metrics.overall.content_age.updates.last_quarter / metrics.overall.total_notes * 100).toFixed(1) }, last_year: { count: metrics.overall.content_age.updates.last_year, percentage: (metrics.overall.content_age.updates.last_year / metrics.overall.total_notes * 100).toFixed(1) }, this_year: { count: metrics.overall.notes_edited_this_year, percentage: (metrics.overall.notes_edited_this_year / metrics.overall.total_notes * 100).toFixed(1) } } }, links: { internal: { count: metrics.overall.links.internal, average: metrics.overall.links.average_internal.toFixed(1) }, external: { count: metrics.overall.links.external, average: metrics.overall.links.average_external.toFixed(1) } }, note_types: Object.entries(metrics.overall.note_types) .sort(([,a], [,b]) => b - a) .reduce((acc, [type, count]) => ({ ...acc, [type]: { count, formatted_name: formatTagName(type) } }), {}), periodic_notes: Object.entries(metrics.overall.periodic_notes) .reduce((acc, [period, data]) => ({ ...acc, [period]: { ...data, reading_time: { minutes: data.reading_minutes, formatted: formatReadingTime(data.reading_minutes) }, books_equivalent: (data.words / WORDS_PER_BOOK).toFixed(2) } }), {}), folders: { count: metrics.overall.folders.count, top_folders: Object.entries(metrics.overall.folders.stats) .sort(([,a], [,b]) => b.note_count - a.note_count) .slice(0, 15) .reduce((acc, [folder, data]) => ({ ...acc, [folder]: { ...data, reading_time: { minutes: Math.round(data.words / WORDS_PER_MINUTE), formatted: formatReadingTime(Math.round(data.words / WORDS_PER_MINUTE)) }, books_equivalent: (data.words / WORDS_PER_BOOK).toFixed(2), growth_rate: metrics.overall.folders.growth_rates[folder]?.per_month || '0' } }), {}) }, tags: { total: metrics.overall.tags.total, total_area_notes: metrics.overall.tags.total_area_notes, notes_without_tags: { count: metrics.overall.tags.notes_without_tags, percentage: ((metrics.overall.tags.notes_without_tags / metrics.overall.tags.total_area_notes) * 100).toFixed(1) }, average_per_note: metrics.overall.tags.average_per_note.toFixed(1), top_tags: Object.entries(metrics.overall.tags.top_tags || {}) .sort(([,a], [,b]) => b - a) .slice(0, 12) .reduce((acc, [tag, count]) => ({ ...acc, [tag]: { count, formatted_name: formatTagName(tag) } }), {}) }, mocs: { count: metrics.overall.mocs.count, average_links: metrics.overall.mocs.average_internal_links.toFixed(1) }, voice_notes: metrics.overall.voice_notes, total_tasks: metrics.overall.total_tasks, special_files: { canvases: metrics.overall.canvases, templates: metrics.overall.templates, attachments: { public: metrics.overall.attachments.public, private: metrics.overall.attachments.private } }, size: { total_bytes: metrics.overall.size.total_bytes, md_files_bytes: metrics.overall.size.md_files_bytes, attachments_bytes: metrics.overall.size.attachments_bytes, git_commits: metrics.overall.size.git_commits } }, publication: { total_published: metrics.publication.total_published, published_words: metrics.publication.published_words, reading_time: { minutes: metrics.publication.published_read_time_minutes, formatted: formatReadingTime(metrics.publication.published_read_time_minutes) }, books_equivalent: metrics.publication.published_books_equivalent, topics: Object.entries(metrics.publication.published_topics) .filter(([topic]) => !EXCLUDED_TOPICS.includes(topic) && !topic.includes('/') ) .sort(([,a], [,b]) => b - a) .slice(0, 25) .reduce((acc, [topic, count]) => ({ ...acc, [topic]: { count, formatted_name: formatTagName(topic), growth_rate: metrics.overall.topics.growth_rates[topic]?.per_month || '0' } }), {}), published_by_year: Object.entries(metrics.publication.published_by_year) .sort(([a], [b]) => b.localeCompare(a)) .reduce((acc, [year, count]) => ({...acc, [year]: count}), {}), published_by_month: metrics.publication.published_by_month } };
// Write the JSON file
try {
await app.vault.adapter.write("stats.json", JSON.stringify(jsonStats, null, 2));
new Notice("Updated stats.json");
} catch (error) {
console.error("Error generating JSON stats:", error);
new Notice("
%>