675 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			675 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
---
 | 
						||
cssclasses:
 | 
						||
  - wide-dashboard
 | 
						||
  - no-properties
 | 
						||
  - note-bg-blur
 | 
						||
  - styled-checkbox
 | 
						||
  - styled-hr
 | 
						||
  - small-text
 | 
						||
---
 | 
						||
<div style="
 | 
						||
  width: 100%;
 | 
						||
  height: 200px;
 | 
						||
  background-image: url('https://images8.alphacoders.com/127/thumb-1920-1275563.jpg');
 | 
						||
  background-size: cover;
 | 
						||
  background-position: center 35%;
 | 
						||
  filter: blur(2px);
 | 
						||
  border-radius: 0;
 | 
						||
  margin: 0;
 | 
						||
"></div>
 | 
						||
 | 
						||
---
 | 
						||
# Dailys
 | 
						||
## Week
 | 
						||
```dataviewjs
 | 
						||
// Force English locale for weekdays
 | 
						||
moment.locale("en");
 | 
						||
 | 
						||
const today = moment();
 | 
						||
 | 
						||
// === CONFIGURE WEEK RANGE HERE ===
 | 
						||
// For current week:
 | 
						||
const weekStart = today.clone().startOf("isoWeek");
 | 
						||
const weekEnd   = today.clone().endOf("isoWeek");
 | 
						||
 | 
						||
// For next week, uncomment these:
 | 
						||
// const weekStart = today.clone().add(1, "week").startOf("isoWeek");
 | 
						||
// const weekEnd   = today.clone().add(1, "week").endOf("isoWeek");
 | 
						||
 | 
						||
// For weekend only (Sat & Sun):
 | 
						||
// const weekStart = today.clone().startOf("isoWeek").add(5, "days"); // Saturday
 | 
						||
// const weekEnd   = today.clone().startOf("isoWeek").add(6, "days"); // Sunday
 | 
						||
 | 
						||
// All notes tagged #Calendar within the week
 | 
						||
const pages = dv.pages("#Calendar")
 | 
						||
  .where(p => {
 | 
						||
    const d = moment(p.file.name, "DD.MM.YYYY", true);
 | 
						||
    return d.isValid() && d.isBetween(weekStart, weekEnd, "day", "[]");
 | 
						||
  });
 | 
						||
 | 
						||
// Generate days array (Mon–Sun or custom)
 | 
						||
const totalDays = weekEnd.diff(weekStart, "days") + 1;
 | 
						||
const weekDays = Array.from({ length: totalDays }, (_, i) => weekStart.clone().add(i, "days"));
 | 
						||
 | 
						||
// Separate days with and without notes
 | 
						||
const daysWithNotes = [];
 | 
						||
const daysWithoutNotes = [];
 | 
						||
 | 
						||
for (const day of weekDays) {
 | 
						||
  const page = pages.find(p => p.file.name === day.format("DD.MM.YYYY"));
 | 
						||
  if (page) {
 | 
						||
    daysWithNotes.push({ date: day, page });
 | 
						||
  } else {
 | 
						||
    daysWithoutNotes.push(day);
 | 
						||
  }
 | 
						||
}
 | 
						||
 | 
						||
// Build Columns plugin Markdown
 | 
						||
let md = "````col\nheight=shortest\ntextAlign=start\n===\n";
 | 
						||
 | 
						||
// Columns for days with notes
 | 
						||
for (const { date, page } of daysWithNotes) {
 | 
						||
  const isToday = date.isSame(today, "day");
 | 
						||
  const label = `${isToday ? "📌 " : ""}${date.format("ddd")}<br>${date.format("DD.MM.YYYY")}`;
 | 
						||
 | 
						||
  md += "```col-md\n";
 | 
						||
  md += `### ${label}\n`;
 | 
						||
 | 
						||
  // Show reason from frontmatter under the title
 | 
						||
  if (page.reason) {
 | 
						||
    md += `**Reason:** ${page.reason}\n\n`;
 | 
						||
  }
 | 
						||
 | 
						||
  // Get tasks from the note
 | 
						||
  const tasks = page.file.tasks;
 | 
						||
 | 
						||
  // Filter tasks under # Tasks heading if possible
 | 
						||
  const tasksInSection = tasks.filter(t => {
 | 
						||
    if (t.heading) return t.heading.toLowerCase().includes("tasks");
 | 
						||
    return true; // fallback: include all tasks if heading not available
 | 
						||
  });
 | 
						||
 | 
						||
  if (tasksInSection.length > 0) {
 | 
						||
    for (const t of tasksInSection) {
 | 
						||
      md += `- [${t.completed ? "x" : " "}] ${t.text}\n`;
 | 
						||
    }
 | 
						||
  } else {
 | 
						||
    md += "_No tasks found_\n";
 | 
						||
  }
 | 
						||
 | 
						||
  md += "```\n\n";
 | 
						||
}
 | 
						||
 | 
						||
// Single column for days without notes
 | 
						||
if (daysWithoutNotes.length > 0) {
 | 
						||
  md += "```col-md\n";
 | 
						||
  md += "### Days without notes\n";
 | 
						||
  for (const day of daysWithoutNotes) {
 | 
						||
    md += `- ${day.format("ddd")} — ${day.format("DD.MM.YYYY")}\n`;
 | 
						||
  }
 | 
						||
  md += "```\n\n";
 | 
						||
}
 | 
						||
 | 
						||
md += "````\n";
 | 
						||
 | 
						||
// Render the dashboard
 | 
						||
dv.paragraph(md);
 | 
						||
 | 
						||
```
 | 
						||
## Weekend
 | 
						||
```dataviewjs
 | 
						||
// Force English locale for weekdays
 | 
						||
moment.locale("en");
 | 
						||
 | 
						||
const today = moment();
 | 
						||
 | 
						||
// === CONFIGURE WEEK RANGE HERE ===
 | 
						||
// For current week:
 | 
						||
//const weekStart = today.clone().startOf("isoWeek");
 | 
						||
//const weekEnd   = today.clone().endOf("isoWeek");
 | 
						||
 | 
						||
// For next week, uncomment these:
 | 
						||
// const weekStart = today.clone().add(1, "week").startOf("isoWeek");
 | 
						||
// const weekEnd   = today.clone().add(1, "week").endOf("isoWeek");
 | 
						||
 | 
						||
// For weekend only (Sat & Sun):
 | 
						||
const weekStart = today.clone().startOf("isoWeek").add(5, "days"); // Saturday
 | 
						||
const weekEnd   = today.clone().startOf("isoWeek").add(6, "days"); // Sunday
 | 
						||
 | 
						||
// All notes tagged #Calendar within the week
 | 
						||
const pages = dv.pages("#Calendar")
 | 
						||
  .where(p => {
 | 
						||
    const d = moment(p.file.name, "DD.MM.YYYY", true);
 | 
						||
    return d.isValid() && d.isBetween(weekStart, weekEnd, "day", "[]");
 | 
						||
  });
 | 
						||
 | 
						||
// Generate days array (Mon–Sun or custom)
 | 
						||
const totalDays = weekEnd.diff(weekStart, "days") + 1;
 | 
						||
const weekDays = Array.from({ length: totalDays }, (_, i) => weekStart.clone().add(i, "days"));
 | 
						||
 | 
						||
// Separate days with and without notes
 | 
						||
const daysWithNotes = [];
 | 
						||
const daysWithoutNotes = [];
 | 
						||
 | 
						||
for (const day of weekDays) {
 | 
						||
  const page = pages.find(p => p.file.name === day.format("DD.MM.YYYY"));
 | 
						||
  if (page) {
 | 
						||
    daysWithNotes.push({ date: day, page });
 | 
						||
  } else {
 | 
						||
    daysWithoutNotes.push(day);
 | 
						||
  }
 | 
						||
}
 | 
						||
 | 
						||
// Build Columns plugin Markdown
 | 
						||
let md = "````col\nheight=shortest\ntextAlign=start\n===\n";
 | 
						||
 | 
						||
// Columns for days with notes
 | 
						||
for (const { date, page } of daysWithNotes) {
 | 
						||
  const isToday = date.isSame(today, "day");
 | 
						||
  const label = `${isToday ? "📌 " : ""}${date.format("ddd")}<br>${date.format("DD.MM.YYYY")}`;
 | 
						||
 | 
						||
  md += "```col-md\n";
 | 
						||
  md += `### ${label}\n`;
 | 
						||
 | 
						||
  // Show reason from frontmatter under the title
 | 
						||
  if (page.reason) {
 | 
						||
    md += `**Reason:** ${page.reason}\n\n`;
 | 
						||
  }
 | 
						||
 | 
						||
  // Get tasks from the note
 | 
						||
  const tasks = page.file.tasks;
 | 
						||
 | 
						||
  // Filter tasks under # Tasks heading if possible
 | 
						||
  const tasksInSection = tasks.filter(t => {
 | 
						||
    if (t.heading) return t.heading.toLowerCase().includes("tasks");
 | 
						||
    return true; // fallback: include all tasks if heading not available
 | 
						||
  });
 | 
						||
 | 
						||
  if (tasksInSection.length > 0) {
 | 
						||
    for (const t of tasksInSection) {
 | 
						||
      md += `- [${t.completed ? "x" : " "}] ${t.text}\n`;
 | 
						||
    }
 | 
						||
  } else {
 | 
						||
    md += "_No tasks found_\n";
 | 
						||
  }
 | 
						||
 | 
						||
  md += "```\n\n";
 | 
						||
}
 | 
						||
 | 
						||
// Single column for days without notes
 | 
						||
if (daysWithoutNotes.length > 0) {
 | 
						||
  md += "```col-md\n";
 | 
						||
  md += "### Days without notes\n";
 | 
						||
  for (const day of daysWithoutNotes) {
 | 
						||
    md += `- ${day.format("ddd")} — ${day.format("DD.MM.YYYY")}\n`;
 | 
						||
  }
 | 
						||
  md += "```\n\n";
 | 
						||
}
 | 
						||
 | 
						||
md += "````\n";
 | 
						||
 | 
						||
// Render the dashboard
 | 
						||
dv.paragraph(md);
 | 
						||
 | 
						||
```
 | 
						||
---
 | 
						||
 | 
						||
# Next Week
 | 
						||
```dataviewjs
 | 
						||
// Force English locale for weekdays
 | 
						||
moment.locale("en");
 | 
						||
 | 
						||
const today = moment();
 | 
						||
 | 
						||
// === CONFIGURE WEEK RANGE HERE ===
 | 
						||
// For current week:
 | 
						||
//const weekStart = today.clone().startOf("isoWeek");
 | 
						||
//const weekEnd   = today.clone().endOf("isoWeek");
 | 
						||
 | 
						||
// For next week, uncomment these:
 | 
						||
const weekStart = today.clone().add(1, "week").startOf("isoWeek");
 | 
						||
const weekEnd   = today.clone().add(1, "week").endOf("isoWeek");
 | 
						||
 | 
						||
// For weekend only (Sat & Sun):
 | 
						||
// const weekStart = today.clone().startOf("isoWeek").add(5, "days"); // Saturday
 | 
						||
// const weekEnd   = today.clone().startOf("isoWeek").add(6, "days"); // Sunday
 | 
						||
 | 
						||
// All notes tagged #Calendar within the week
 | 
						||
const pages = dv.pages("#Calendar")
 | 
						||
  .where(p => {
 | 
						||
    const d = moment(p.file.name, "DD.MM.YYYY", true);
 | 
						||
    return d.isValid() && d.isBetween(weekStart, weekEnd, "day", "[]");
 | 
						||
  });
 | 
						||
 | 
						||
// Generate days array (Mon–Sun or custom)
 | 
						||
const totalDays = weekEnd.diff(weekStart, "days") + 1;
 | 
						||
const weekDays = Array.from({ length: totalDays }, (_, i) => weekStart.clone().add(i, "days"));
 | 
						||
 | 
						||
// Separate days with and without notes
 | 
						||
const daysWithNotes = [];
 | 
						||
const daysWithoutNotes = [];
 | 
						||
 | 
						||
for (const day of weekDays) {
 | 
						||
  const page = pages.find(p => p.file.name === day.format("DD.MM.YYYY"));
 | 
						||
  if (page) {
 | 
						||
    daysWithNotes.push({ date: day, page });
 | 
						||
  } else {
 | 
						||
    daysWithoutNotes.push(day);
 | 
						||
  }
 | 
						||
}
 | 
						||
 | 
						||
// Build Columns plugin Markdown
 | 
						||
let md = "````col\nheight=shortest\ntextAlign=start\n===\n";
 | 
						||
 | 
						||
// Columns for days with notes
 | 
						||
for (const { date, page } of daysWithNotes) {
 | 
						||
  const isToday = date.isSame(today, "day");
 | 
						||
  const label = `${isToday ? "📌 " : ""}${date.format("ddd")}<br>${date.format("DD.MM.YYYY")}`;
 | 
						||
 | 
						||
  md += "```col-md\n";
 | 
						||
  md += `### ${label}\n`;
 | 
						||
 | 
						||
  // Show reason from frontmatter under the title
 | 
						||
  if (page.reason) {
 | 
						||
    md += `**Reason:** ${page.reason}\n\n`;
 | 
						||
  }
 | 
						||
 | 
						||
  // Get tasks from the note
 | 
						||
  const tasks = page.file.tasks;
 | 
						||
 | 
						||
  // Filter tasks under # Tasks heading if possible
 | 
						||
  const tasksInSection = tasks.filter(t => {
 | 
						||
    if (t.heading) return t.heading.toLowerCase().includes("tasks");
 | 
						||
    return true; // fallback: include all tasks if heading not available
 | 
						||
  });
 | 
						||
 | 
						||
  if (tasksInSection.length > 0) {
 | 
						||
    for (const t of tasksInSection) {
 | 
						||
      md += `- [${t.completed ? "x" : " "}] ${t.text}\n`;
 | 
						||
    }
 | 
						||
  } else {
 | 
						||
    md += "_No tasks found_\n";
 | 
						||
  }
 | 
						||
 | 
						||
  md += "```\n\n";
 | 
						||
}
 | 
						||
 | 
						||
// Single column for days without notes
 | 
						||
if (daysWithoutNotes.length > 0) {
 | 
						||
  md += "```col-md\n";
 | 
						||
  md += "### Days without notes\n";
 | 
						||
  for (const day of daysWithoutNotes) {
 | 
						||
    md += `- ${day.format("ddd")} — ${day.format("DD.MM.YYYY")}\n`;
 | 
						||
  }
 | 
						||
  md += "```\n\n";
 | 
						||
}
 | 
						||
 | 
						||
md += "````\n";
 | 
						||
 | 
						||
// Render the dashboard
 | 
						||
dv.paragraph(md);
 | 
						||
 | 
						||
```
 | 
						||
---
 | 
						||
 | 
						||
![[Veranstaltungen - 2025]]
 | 
						||
 | 
						||
---
 | 
						||
 | 
						||
![[Wohnorte]]
 | 
						||
 | 
						||
---
 | 
						||
# Stats
 | 
						||
```dataviewjs
 | 
						||
// === Config ===
 | 
						||
const splitOnSlash = false; // true -> merge hierarchical tags (#project/obsidian -> project)
 | 
						||
const topN = 50;            // show top N tags (null for all)
 | 
						||
 | 
						||
// === Catppuccin Mocha Palette ===
 | 
						||
const catppuccin = {
 | 
						||
    rosewater: '#f5e0dc',
 | 
						||
    flamingo: '#f2cdcd',
 | 
						||
    pink: '#f5c2e7',
 | 
						||
    mauve: '#cba6f7',
 | 
						||
    red: '#f38ba8',
 | 
						||
    maroon: '#eba0ac',
 | 
						||
    peach: '#fab387',
 | 
						||
    yellow: '#f9e2af',
 | 
						||
    green: '#a6e3a1',
 | 
						||
    teal: '#94e2d5',
 | 
						||
    sky: '#89dceb',
 | 
						||
    sapphire: '#74c7ec',
 | 
						||
    blue: '#89b4fa',
 | 
						||
    lavender: '#b4befe',
 | 
						||
    text: '#cdd6f4',
 | 
						||
    subtext1: '#bac2de',
 | 
						||
    base: '#1e1e2e'
 | 
						||
};
 | 
						||
 | 
						||
const catppuccinColors = [
 | 
						||
    catppuccin.rosewater,
 | 
						||
    catppuccin.flamingo,
 | 
						||
    catppuccin.pink,
 | 
						||
    catppuccin.mauve,
 | 
						||
    catppuccin.red,
 | 
						||
    catppuccin.maroon,
 | 
						||
    catppuccin.peach,
 | 
						||
    catppuccin.yellow,
 | 
						||
    catppuccin.green,
 | 
						||
    catppuccin.teal,
 | 
						||
    catppuccin.sky,
 | 
						||
    catppuccin.sapphire,
 | 
						||
    catppuccin.blue,
 | 
						||
    catppuccin.lavender
 | 
						||
];
 | 
						||
 | 
						||
// === Helpers ===
 | 
						||
function flattenTags(input, out = []) {
 | 
						||
    if (!input) return out;
 | 
						||
    if (Array.isArray(input)) {
 | 
						||
        for (const v of input) flattenTags(v, out);
 | 
						||
    } else if (typeof input === 'string') {
 | 
						||
        out.push(input);
 | 
						||
    } else if (typeof input === 'object') {
 | 
						||
        if (typeof input.tag === 'string') out.push(input.tag);
 | 
						||
        else if (typeof input.path === 'string') out.push(input.path);
 | 
						||
        else {
 | 
						||
            const s = String(input);
 | 
						||
            if (s && s !== '[object Object]') out.push(s);
 | 
						||
        }
 | 
						||
    } else {
 | 
						||
        out.push(String(input));
 | 
						||
    }
 | 
						||
    return out;
 | 
						||
}
 | 
						||
 | 
						||
// Convert a string to Title Case (preserving slashes/hyphens)
 | 
						||
function toTitleCase(str) {
 | 
						||
    return str.replace(/[\w]+/g, w =>
 | 
						||
        w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()
 | 
						||
    );
 | 
						||
}
 | 
						||
 | 
						||
// === Collect tags from all pages ===
 | 
						||
const pages = dv.pages();
 | 
						||
let allTags = [];
 | 
						||
for (const p of pages) {
 | 
						||
    if (p.file?.tags) flattenTags(p.file.tags, allTags);
 | 
						||
    if (p.tags) flattenTags(p.tags, allTags);
 | 
						||
    if (p.tags_list) flattenTags(p.tags_list, allTags);
 | 
						||
}
 | 
						||
 | 
						||
// === Normalize and count ===
 | 
						||
const counts = {};
 | 
						||
for (let tag of allTags) {
 | 
						||
    if (!tag) continue;
 | 
						||
    tag = String(tag).replace(/^#/, '').trim().toLowerCase();
 | 
						||
    if (!tag) continue;
 | 
						||
    if (splitOnSlash && tag.includes('/')) tag = tag.split('/')[0];
 | 
						||
    counts[tag] = (counts[tag] || 0) + 1;
 | 
						||
}
 | 
						||
 | 
						||
// === Handle no tags ===
 | 
						||
const tagEntries = Object.entries(counts);
 | 
						||
if (tagEntries.length === 0) {
 | 
						||
    dv.el("p", "⚠️ No tags found in your vault.");
 | 
						||
} else {
 | 
						||
    // === Sort + limit ===
 | 
						||
    tagEntries.sort((a, b) => b[1] - a[1]);
 | 
						||
    const limited = (topN && tagEntries.length > topN)
 | 
						||
        ? tagEntries.slice(0, topN)
 | 
						||
        : tagEntries;
 | 
						||
 | 
						||
    // Convert to title case for display
 | 
						||
    const labels = limited.map(e => toTitleCase(e[0].replace(/[-_]/g, ' ')));
 | 
						||
    const dataValues = limited.map(e => e[1]);
 | 
						||
 | 
						||
    // === Color setup ===
 | 
						||
    const bgColors = [];
 | 
						||
    const borderColors = [];
 | 
						||
    for (let i = 0; i < labels.length; i++) {
 | 
						||
        const color = catppuccinColors[i % catppuccinColors.length];
 | 
						||
        bgColors.push(color + "55");
 | 
						||
        borderColors.push(color);
 | 
						||
    }
 | 
						||
 | 
						||
    // === Chart.js config ===
 | 
						||
    const chartData = {
 | 
						||
        type: 'bar',
 | 
						||
        data: {
 | 
						||
            labels: labels,
 | 
						||
            datasets: [{
 | 
						||
                label: 'Tag Usage',
 | 
						||
                data: dataValues,
 | 
						||
                backgroundColor: bgColors,
 | 
						||
                borderColor: borderColors,
 | 
						||
                borderWidth: 1
 | 
						||
            }]
 | 
						||
        },
 | 
						||
        options: {
 | 
						||
            plugins: {
 | 
						||
                legend: { display: false },
 | 
						||
                title: {
 | 
						||
                    display: true,
 | 
						||
                    text: `Tag Usage (Top ${labels.length})`,
 | 
						||
                    color: catppuccin.text,
 | 
						||
                    font: { size: 16 }
 | 
						||
                }
 | 
						||
            },
 | 
						||
            scales: {
 | 
						||
                x: {
 | 
						||
                    ticks: { color: catppuccin.subtext1, autoSkip: false, maxRotation: 60, minRotation: 30 },
 | 
						||
                    title: { display: true, text: 'Tags', color: catppuccin.text },
 | 
						||
                    grid: { color: catppuccin.base }
 | 
						||
                },
 | 
						||
                y: {
 | 
						||
                    ticks: { color: catppuccin.subtext1 },
 | 
						||
                    title: { display: true, text: 'Count', color: catppuccin.text },
 | 
						||
                    beginAtZero: true,
 | 
						||
                    grid: { color: catppuccin.base }
 | 
						||
                }
 | 
						||
            },
 | 
						||
            maintainAspectRatio: false
 | 
						||
        }
 | 
						||
    };
 | 
						||
 | 
						||
    // === Render ===
 | 
						||
    const wrapper = this.container.createEl('div');
 | 
						||
    wrapper.style.minHeight = '320px';
 | 
						||
    wrapper.style.maxHeight = '60vh';
 | 
						||
    wrapper.style.overflow = 'auto';
 | 
						||
    wrapper.style.borderRadius = '8px';
 | 
						||
    wrapper.style.padding = '8px';
 | 
						||
 | 
						||
    window.renderChart(chartData, wrapper);
 | 
						||
}
 | 
						||
```
 | 
						||
```dataviewjs
 | 
						||
// === Config ===
 | 
						||
const splitOnSlash = false; // true -> merge hierarchical tags (#project/obsidian -> project)
 | 
						||
const topN = 20;            // show top N tags (null for all)
 | 
						||
 | 
						||
// === Catppuccin Mocha Palette ===
 | 
						||
const catppuccin = {
 | 
						||
    rosewater: '#f5e0dc',
 | 
						||
    flamingo: '#f2cdcd',
 | 
						||
    pink: '#f5c2e7',
 | 
						||
    mauve: '#cba6f7',
 | 
						||
    red: '#f38ba8',
 | 
						||
    maroon: '#eba0ac',
 | 
						||
    peach: '#fab387',
 | 
						||
    yellow: '#f9e2af',
 | 
						||
    green: '#a6e3a1',
 | 
						||
    teal: '#94e2d5',
 | 
						||
    sky: '#89dceb',
 | 
						||
    sapphire: '#74c7ec',
 | 
						||
    blue: '#89b4fa',
 | 
						||
    lavender: '#b4befe',
 | 
						||
    text: '#cdd6f4',
 | 
						||
    subtext1: '#bac2de',
 | 
						||
    base: '#1e1e2e'
 | 
						||
};
 | 
						||
 | 
						||
const catppuccinColors = [
 | 
						||
    catppuccin.rosewater,
 | 
						||
    catppuccin.flamingo,
 | 
						||
    catppuccin.pink,
 | 
						||
    catppuccin.mauve,
 | 
						||
    catppuccin.red,
 | 
						||
    catppuccin.maroon,
 | 
						||
    catppuccin.peach,
 | 
						||
    catppuccin.yellow,
 | 
						||
    catppuccin.green,
 | 
						||
    catppuccin.teal,
 | 
						||
    catppuccin.sky,
 | 
						||
    catppuccin.sapphire,
 | 
						||
    catppuccin.blue,
 | 
						||
    catppuccin.lavender
 | 
						||
];
 | 
						||
 | 
						||
// === Helpers ===
 | 
						||
function flattenTags(input, out = []) {
 | 
						||
    if (!input) return out;
 | 
						||
    if (Array.isArray(input)) {
 | 
						||
        for (const v of input) flattenTags(v, out);
 | 
						||
    } else if (typeof input === 'string') {
 | 
						||
        out.push(input);
 | 
						||
    } else if (typeof input === 'object') {
 | 
						||
        if (typeof input.tag === 'string') out.push(input.tag);
 | 
						||
        else if (typeof input.path === 'string') out.push(input.path);
 | 
						||
        else {
 | 
						||
            const s = String(input);
 | 
						||
            if (s && s !== '[object Object]') out.push(s);
 | 
						||
        }
 | 
						||
    } else {
 | 
						||
        out.push(String(input));
 | 
						||
    }
 | 
						||
    return out;
 | 
						||
}
 | 
						||
 | 
						||
function toTitleCase(str) {
 | 
						||
    return str.replace(/[\w]+/g, w =>
 | 
						||
        w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()
 | 
						||
    );
 | 
						||
}
 | 
						||
 | 
						||
// === Collect tags ===
 | 
						||
const pages = dv.pages();
 | 
						||
let allTags = [];
 | 
						||
for (const p of pages) {
 | 
						||
    if (p.file?.tags) flattenTags(p.file.tags, allTags);
 | 
						||
    if (p.tags) flattenTags(p.tags, allTags);
 | 
						||
    if (p.tags_list) flattenTags(p.tags_list, allTags);
 | 
						||
}
 | 
						||
 | 
						||
// === Normalize + count ===
 | 
						||
const counts = {};
 | 
						||
for (let tag of allTags) {
 | 
						||
    if (!tag) continue;
 | 
						||
    tag = String(tag).replace(/^#/, '').trim().toLowerCase();
 | 
						||
    if (!tag) continue;
 | 
						||
    if (splitOnSlash && tag.includes('/')) tag = tag.split('/')[0];
 | 
						||
    counts[tag] = (counts[tag] || 0) + 1;
 | 
						||
}
 | 
						||
 | 
						||
const tagEntries = Object.entries(counts);
 | 
						||
if (tagEntries.length === 0) {
 | 
						||
    dv.el("p", "⚠️ No tags found in your vault.");
 | 
						||
} else {
 | 
						||
    // === Sort + limit ===
 | 
						||
    tagEntries.sort((a, b) => b[1] - a[1]);
 | 
						||
    const limited = (topN && tagEntries.length > topN)
 | 
						||
        ? tagEntries.slice(0, topN)
 | 
						||
        : tagEntries;
 | 
						||
 | 
						||
    const labels = limited.map(e => toTitleCase(e[0].replace(/[-_]/g, ' ')));
 | 
						||
    const dataValues = limited.map(e => e[1]);
 | 
						||
    const total = dataValues.reduce((a, b) => a + b, 0);
 | 
						||
 | 
						||
    // === Colors ===
 | 
						||
    const bgColors = [], borderColors = [];
 | 
						||
    for (let i = 0; i < labels.length; i++) {
 | 
						||
        const color = catppuccinColors[i % catppuccinColors.length];
 | 
						||
        bgColors.push(color + "aa");
 | 
						||
        borderColors.push(color);
 | 
						||
    }
 | 
						||
 | 
						||
    // === Create container ===
 | 
						||
    const wrapper = this.container.createEl('div');
 | 
						||
    wrapper.style.minHeight = '400px';
 | 
						||
    wrapper.style.maxHeight = '70vh';
 | 
						||
    wrapper.style.position = 'relative';
 | 
						||
    wrapper.style.borderRadius = '8px';
 | 
						||
    wrapper.style.padding = '8px';
 | 
						||
    wrapper.style.overflow = 'auto';
 | 
						||
 | 
						||
    // === Center label ===
 | 
						||
    const centerLabel = document.createElement('div');
 | 
						||
    centerLabel.style.position = 'absolute';
 | 
						||
    centerLabel.style.top = '50%';
 | 
						||
    centerLabel.style.left = '44%';
 | 
						||
    centerLabel.style.transform = 'translate(-50%, -50%)';
 | 
						||
    centerLabel.style.color = catppuccin.text;
 | 
						||
    centerLabel.style.fontSize = '14px';
 | 
						||
    centerLabel.style.fontWeight = '600';
 | 
						||
    centerLabel.style.textAlign = 'center';
 | 
						||
    centerLabel.innerText = 'Tag Usage';
 | 
						||
    wrapper.appendChild(centerLabel);
 | 
						||
 | 
						||
    // === Chart.js config ===
 | 
						||
    const chartData = {
 | 
						||
        type: 'doughnut',
 | 
						||
        data: {
 | 
						||
            labels: labels,
 | 
						||
            datasets: [{
 | 
						||
                data: dataValues,
 | 
						||
                backgroundColor: bgColors,
 | 
						||
                borderColor: borderColors,
 | 
						||
                borderWidth: 1
 | 
						||
            }]
 | 
						||
        },
 | 
						||
        options: {
 | 
						||
            cutout: '60%',
 | 
						||
            plugins: {
 | 
						||
                legend: {
 | 
						||
                    display: true,
 | 
						||
                    position: 'right',
 | 
						||
                    labels: { color: catppuccin.subtext1, font: { size: 12 } }
 | 
						||
                },
 | 
						||
                title: {
 | 
						||
                    display: true,
 | 
						||
                    text: `Tag Distribution (Top ${labels.length})`,
 | 
						||
                    color: catppuccin.text,
 | 
						||
                    font: { size: 16 }
 | 
						||
                },
 | 
						||
                tooltip: {
 | 
						||
                    callbacks: {
 | 
						||
                        label: ctx => {
 | 
						||
                            const val = ctx.raw;
 | 
						||
                            const pct = ((val / total) * 100).toFixed(1);
 | 
						||
                            return `${ctx.label}: ${val} (${pct}%)`;
 | 
						||
                        }
 | 
						||
                    }
 | 
						||
                }
 | 
						||
            },
 | 
						||
            onHover: (evt, elements) => {
 | 
						||
                if (elements.length > 0) {
 | 
						||
                    const el = elements[0];
 | 
						||
                    const label = labels[el.index];
 | 
						||
                    const value = dataValues[el.index];
 | 
						||
                    const pct = ((value / total) * 100).toFixed(1);
 | 
						||
                    centerLabel.innerText = `${label}\n${pct}%`;
 | 
						||
                } else {
 | 
						||
                    centerLabel.innerText = 'Tag Usage';
 | 
						||
                }
 | 
						||
            },
 | 
						||
            maintainAspectRatio: false
 | 
						||
        }
 | 
						||
    };
 | 
						||
 | 
						||
    window.renderChart(chartData, wrapper);
 | 
						||
}
 | 
						||
```
 | 
						||
 | 
						||
---
 |