Skip to content

Instantly share code, notes, and snippets.

@dsebastien
Created February 13, 2025 07:41
Show Gist options
  • Save dsebastien/99c5fe34a954e2f65a89022e5221328e to your computer and use it in GitHub Desktop.
Save dsebastien/99c5fe34a954e2f65a89022e5221328e to your computer and use it in GitHub Desktop.

<%* 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("⚠️ ERROR updating stats! Check console.", 0); }

// 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("⚠️ Error generating JSON stats! Check console."); }

%>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment