Skip to content

Instantly share code, notes, and snippets.

@dsebastien
Created February 13, 2025 07:42
Show Gist options
  • Save dsebastien/026ae53e891eab3d28c9f00e1ff9b358 to your computer and use it in GitHub Desktop.
Save dsebastien/026ae53e891eab3d28c9f00e1ff9b358 to your computer and use it in GitHub Desktop.
// Import required Node.js modules
const fs = require("fs");
const path = require("path");
// Function to format bytes
function formatBytes(bytes, decimals = 2) {
if (!bytes) 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 to generate HTML content
function generateHtml(stats, outputFilePath) {
const htmlContent = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Knowledge Base Stats</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100 text-gray-800">
<div class="container mx-auto p-4">
<h1 class="text-3xl font-bold text-center mb-8">Knowledge Base Statistics</h1>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Overall Statistics -->
<div class="bg-white p-6 shadow-lg rounded-lg">
<h2 class="text-2xl font-semibold mb-4 text-gray-700 border-b pb-2">Overall Metrics</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
${[
['Total Notes', stats.overall.total_notes],
['Average Note Age', stats.overall.content_age.average_days.formatted],
['Oldest Note', stats.overall.content_age.oldest_note.formatted, null, stats.overall.content_age.oldest_note.date],
['Newest Note', '0 days', null, stats.overall.content_age.newest_note.date],
['Total Words', stats.overall.total_words, null, stats.overall.reading_time.formatted],
['Books Equivalent', stats.overall.books_equivalent.toFixed(1)],
['Internal Links', stats.overall.links.internal.count, null, `${stats.overall.links.internal.average} per note`],
['External Links', stats.overall.links.external.count, null, `${stats.overall.links.external.average} per note`],
['Maps of Content', stats.overall.mocs.count, null, `${stats.overall.mocs.average_links} links/MOC`],
['Voice Notes', stats.overall.voice_notes],
['Total Tasks', stats.overall.total_tasks],
['Total Folders', stats.overall.folders.count]
].map(([label, value, type, subtext]) => `
<div class="p-4 bg-gray-50 rounded-lg">
<h3 class="text-center text-lg font-semibold">${label}</h3>
<p class="text-center text-4xl font-bold text-blue-500 mt-2">${value}</p>
${subtext ? `<p class="text-center text-sm text-gray-600 mt-1">${subtext}</p>` : ''}
</div>
`).join('')}
</div>
</div>
<!-- Content Updates -->
<div class="bg-white p-6 shadow-lg rounded-lg">
<h2 class="text-2xl font-semibold mb-4 text-gray-700 border-b pb-2">Content Updates</h2>
<div class="grid grid-cols-2 gap-4">
${['this_year', 'last_week', 'last_month', 'last_quarter', 'last_year'].map(period => `
<div class="p-4 bg-gray-50 rounded-lg">
<h3 class="text-center text-lg font-semibold">${period === 'this_year' ? 'This Year' :
'Last ' + period.split('_')[1].charAt(0).toUpperCase() + period.split('_')[1].slice(1)}</h3>
<p class="text-center text-4xl font-bold text-blue-500 mt-2">${stats.overall.content_age.updates[period].count}</p>
<p class="text-center text-sm text-gray-600 mt-1">${stats.overall.content_age.updates[period].percentage}%</p>
</div>
`).join('')}
</div>
</div>
<!-- Notes by Type -->
<div class="bg-white p-6 shadow-lg rounded-lg">
<h2 class="text-2xl font-semibold mb-4 text-gray-700 border-b pb-2">Notes by Type</h2>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-4">
${Object.entries(stats.overall.note_types).map(([type, data]) => `
<div class="p-4 bg-gray-50 rounded-lg">
<h3 class="text-center text-lg font-semibold">${data.formatted_name}</h3>
<p class="text-center text-4xl font-bold text-blue-500 mt-2">${data.count}</p>
</div>
`).join('')}
</div>
</div>
<!-- Tag Cloud -->
<div class="bg-white p-6 shadow-lg rounded-lg">
<h2 class="text-2xl font-semibold mb-4 text-gray-700 border-b pb-2">Tag Cloud</h2>
<div class="flex flex-wrap gap-2 justify-center">
${[
...Object.entries(stats.overall.note_types).map(([type, data]) => ({
name: data.formatted_name,
count: data.count,
isType: true
})),
...Object.entries(stats.publication.topics).map(([topic, data]) => ({
name: data.formatted_name,
count: data.count,
isType: false
}))
]
.sort((a, b) => b.count - a.count)
.map((tag, _, arr) => {
// Get max count for normalization
const maxCount = arr[0].count;
// Normalize count between 0 and 1
const normalizedCount = tag.count / maxCount;
// Scale font size between 0.8rem and 2.5rem using a logarithmic scale
const fontSize = 0.8 + (1.7 * Math.log1p(normalizedCount));
// Scale opacity between 0.6 and 1
const opacity = 0.6 + (0.4 * normalizedCount);
return `
<span class="px-2 py-1 rounded ${tag.isType ? 'bg-blue-100' : 'bg-green-100'}"
style="font-size: ${fontSize}rem; opacity: ${opacity};">
${tag.name}
<span class="text-xs ml-1 text-gray-500">${tag.count}</span>
</span>
`;
}).join('')}
</div>
</div>
<!-- Topics -->
<div class="bg-white p-6 shadow-lg rounded-lg col-span-1 lg:col-span-2">
<h2 class="text-2xl font-semibold mb-4 text-gray-700 border-b pb-2">Topics</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
${Object.entries(stats.publication.topics).map(([topic, data]) => `
<div class="p-4 bg-gray-50 rounded-lg">
<h3 class="text-lg font-semibold mb-2 text-center">${data.formatted_name}</h3>
<p class="text-center text-3xl font-bold text-blue-500">${data.count}</p>
<p class="text-center text-sm text-gray-600 mt-1">${data.growth_rate}/month</p>
</div>
`).join('')}
</div>
</div>
<!-- Folders -->
<div class="bg-white p-6 shadow-lg rounded-lg col-span-1 lg:col-span-2">
<h2 class="text-2xl font-semibold mb-4 text-gray-700 border-b pb-2">Top Folders</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
${Object.entries(stats.overall.folders.top_folders).map(([folder, data]) => `
<div class="p-4 bg-gray-50 rounded-lg">
<h3 class="text-lg font-semibold mb-2 text-center">${folder}</h3>
<div class="grid grid-cols-3 gap-2">
<div class="text-center">
<p class="text-gray-600 text-sm">Notes</p>
<p class="text-xl font-bold text-blue-500">${data.note_count}</p>
</div>
<div class="text-center">
<p class="text-gray-600 text-sm">Words</p>
<p class="text-xl font-bold text-blue-500">${data.words}</p>
</div>
<div class="text-center">
<p class="text-gray-600 text-sm">Links</p>
<p class="text-xl font-bold text-blue-500">${data.internal_links}</p>
</div>
</div>
<div class="mt-2 text-sm text-gray-600 text-center">
Reading: ${data.reading_time.formatted}<br>
Books: ${data.books_equivalent}<br>
Growth: ${data.growth_rate}/month
</div>
</div>
`).join('')}
</div>
</div>
<!-- Tags -->
<div class="bg-white p-6 shadow-lg rounded-lg">
<h2 class="text-2xl font-semibold mb-4 text-gray-700 border-b pb-2">Tags</h2>
<div class="grid grid-cols-2 gap-4">
<div class="p-4 bg-gray-50 rounded-lg">
<h3 class="text-center text-lg font-semibold">Total Unique Tags</h3>
<p class="text-center text-4xl font-bold text-blue-500 mt-2">${stats.overall.tags.total}</p>
</div>
<div class="p-4 bg-gray-50 rounded-lg">
<h3 class="text-center text-lg font-semibold">Notes Under Areas</h3>
<p class="text-center text-4xl font-bold text-blue-500 mt-2">${stats.overall.tags.total_area_notes}</p>
</div>
<div class="p-4 bg-gray-50 rounded-lg">
<h3 class="text-center text-lg font-semibold">Notes Without Tags</h3>
<p class="text-center text-4xl font-bold text-blue-500 mt-2">${stats.overall.tags.notes_without_tags.count}</p>
<p class="text-center text-sm text-gray-600 mt-1">${stats.overall.tags.notes_without_tags.percentage}%</p>
</div>
<div class="p-4 bg-gray-50 rounded-lg">
<h3 class="text-center text-lg font-semibold">Average Tags per Note</h3>
<p class="text-center text-4xl font-bold text-blue-500 mt-2">${stats.overall.tags.average_per_note}</p>
</div>
</div>
</div>
<!-- Periodic Notes -->
<div class="bg-white p-6 shadow-lg rounded-lg">
<h2 class="text-2xl font-semibold mb-4 text-gray-700 border-b pb-2">Periodic Notes</h2>
<div class="grid grid-cols-1 gap-4">
${Object.entries(stats.overall.periodic_notes).map(([period, data]) => `
<div class="p-4 bg-gray-50 rounded-lg">
<h3 class="text-xl font-semibold mb-2">${period.charAt(0).toUpperCase() + period.slice(1)} Notes</h3>
<div class="grid grid-cols-3 gap-4">
<div class="text-center">
<p class="text-gray-600">Count</p>
<p class="text-2xl font-bold text-blue-500">${data.count}</p>
</div>
<div class="text-center">
<p class="text-gray-600">Words</p>
<p class="text-2xl font-bold text-blue-500">${data.words}</p>
</div>
<div class="text-center">
<p class="text-gray-600">Avg Words</p>
<p class="text-2xl font-bold text-blue-500">${data.average_words}</p>
</div>
</div>
<div class="mt-2 text-sm text-gray-600 text-center">
Reading Time: ${data.reading_time.formatted}<br>
Books Equivalent: ${data.books_equivalent}
</div>
</div>
`).join('')}
</div>
</div>
<!-- Special Files -->
<div class="bg-white p-6 shadow-lg rounded-lg">
<h2 class="text-2xl font-semibold mb-4 text-gray-700 border-b pb-2">Special Files</h2>
<div class="grid grid-cols-2 gap-4">
<div class="p-4 bg-gray-50 rounded-lg">
<h3 class="text-center text-lg font-semibold">Canvas Files</h3>
<p class="text-center text-4xl font-bold text-blue-500 mt-2">${stats.overall.special_files.canvases}</p>
</div>
<div class="p-4 bg-gray-50 rounded-lg">
<h3 class="text-center text-lg font-semibold">Templates</h3>
<p class="text-center text-4xl font-bold text-blue-500 mt-2">${stats.overall.special_files.templates}</p>
</div>
<div class="p-4 bg-gray-50 rounded-lg">
<h3 class="text-center text-lg font-semibold">Public Attachments</h3>
<p class="text-center text-4xl font-bold text-blue-500 mt-2">${stats.overall.special_files.attachments.public}</p>
</div>
<div class="p-4 bg-gray-50 rounded-lg">
<h3 class="text-center text-lg font-semibold">Private Attachments</h3>
<p class="text-center text-4xl font-bold text-blue-500 mt-2">${stats.overall.special_files.attachments.private}</p>
</div>
</div>
</div>
<!-- Vault Size -->
<div class="bg-white p-6 shadow-lg rounded-lg">
<h2 class="text-2xl font-semibold mb-4 text-gray-700 border-b pb-2">Vault Size</h2>
<div class="grid grid-cols-2 gap-4">
<div class="p-4 bg-gray-50 rounded-lg">
<h3 class="text-center text-lg font-semibold">Total Size</h3>
<p class="text-center text-4xl font-bold text-blue-500 mt-2">${formatBytes(stats.overall.size.total_bytes)}</p>
</div>
<div class="p-4 bg-gray-50 rounded-lg">
<h3 class="text-center text-lg font-semibold">Markdown Files</h3>
<p class="text-center text-4xl font-bold text-blue-500 mt-2">${formatBytes(stats.overall.size.md_files_bytes)}</p>
</div>
<div class="p-4 bg-gray-50 rounded-lg">
<h3 class="text-center text-lg font-semibold">Attachments</h3>
<p class="text-center text-4xl font-bold text-blue-500 mt-2">${formatBytes(stats.overall.size.attachments_bytes)}</p>
</div>
<div class="p-4 bg-gray-50 rounded-lg">
<h3 class="text-center text-lg font-semibold">Git Commits</h3>
<p class="text-center text-4xl font-bold text-blue-500 mt-2">${stats.overall.size.git_commits}</p>
</div>
</div>
</div>
<!-- Public Notes Metrics -->
<div class="bg-white p-6 shadow-lg rounded-lg col-span-1 lg:col-span-2">
<h2 class="text-2xl font-semibold mb-4 text-gray-700 border-b pb-2">Public Notes Metrics</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<div class="p-4 bg-gray-50 rounded-lg">
<h3 class="text-center text-lg font-semibold">Total Published</h3>
<p class="text-center text-4xl font-bold text-blue-500 mt-2">${stats.publication.total_published}</p>
</div>
<div class="p-4 bg-gray-50 rounded-lg">
<h3 class="text-center text-lg font-semibold">Published Words</h3>
<p class="text-center text-4xl font-bold text-blue-500 mt-2">${stats.publication.published_words}</p>
<p class="text-center text-sm text-gray-600 mt-1">${stats.publication.reading_time.formatted}</p>
</div>
<div class="p-4 bg-gray-50 rounded-lg">
<h3 class="text-center text-lg font-semibold">Books Equivalent</h3>
<p class="text-center text-4xl font-bold text-blue-500 mt-2">${stats.publication.books_equivalent.toFixed(1)}</p>
</div>
${Object.entries(stats.publication.published_by_year).map(([year, count]) => `
<div class="p-4 bg-gray-50 rounded-lg">
<h3 class="text-center text-lg font-semibold">${year} Published</h3>
<p class="text-center text-4xl font-bold text-blue-500 mt-2">${count}</p>
</div>
`).join('')}
</div>
</div>
<!-- Published by Month Graph -->
<div class="bg-white p-6 shadow-lg rounded-lg col-span-1 lg:col-span-2">
<h2 class="text-2xl font-semibold mb-4 text-gray-700 border-b pb-2">Published by Month</h2>
<canvas id="publishedByMonth"></canvas>
</div>
</div>
</div>
<script>
const publishedByMonthData = Object.entries(${JSON.stringify(stats.publication.published_by_month)})
.sort(([a], [b]) => new Date(a) - new Date(b));
const ctx = document.getElementById('publishedByMonth').getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
labels: publishedByMonthData.map(([month]) => month),
datasets: [{
label: 'Published Notes',
data: publishedByMonthData.map(([, count]) => count),
borderColor: 'rgba(59, 130, 246, 0.8)',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
fill: true,
tension: 0.4
}]
},
options: {
responsive: true,
plugins: {
legend: { position: 'top' },
title: { display: false }
},
scales: {
y: {
beginAtZero: true,
ticks: { stepSize: 1 }
}
}
}
});
</script>
</body>
</html>
`;
fs.writeFileSync(outputFilePath, htmlContent);
console.log(`HTML file generated: ${outputFilePath}`);
}
// Main execution
const jsonFile = path.join(__dirname, "stats.json");
const outputHtmlFile = path.join(__dirname, "stats.html");
if (fs.existsSync(jsonFile)) {
const stats = JSON.parse(fs.readFileSync(jsonFile, "utf-8"));
generateHtml(stats, outputHtmlFile);
} else {
console.error(`JSON file not found: ${jsonFile}`);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment