Skip to content

Instantly share code, notes, and snippets.

@chrisgrieser
Last active May 30, 2023 10:34
  • Star 45 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
Star You must be signed in to star a gist
Embed
What would you like to do?
Word and Character Count of multiple notes in Obsidian, using dataviewjs.
// Word Count Dashboard
// a dataviewjs snippet by @pseudometa, https://gist.github.com/chrisgrieser/ac16a80cdd9e8e0e84606cc24e35ad99
// version 1.10.2
// last update: 2022-01-25
//----------------------------------------------------
// Import configuration
//----------------------------------------------------
const source = dv.current();
const sourceFolder = source.sourceFolder;
const target = source.target;
const toCount = source.toCount;
const includeFootnotes = source.includeFootnotes;
const charactersIncludeSpaces = source.charactersIncludeSpaces;
const excludeComments = source.excludeComments;
const includeBibliographyEstimate = source.includeBibliographyEstimate;
const wordsPerCitation = source.wordsPerCitation;
const charsPerCitation = source.charsPerCitation;
const thousandSeperator = source.thousandSeperator;
const useThousandSeperator = source.useThousandSeperator;
const naChar = source.naChar;
const subsectionStartChar = source.subsectionStartChar;
const wordsPerPage = source.wordsPerPage;
const charsPerPage = source.charsPerPage;
const pathToIndexFile = source.pathToIndexFile;
const cumulativeShare = source.cumulativeShare;
const groupedCount = source.groupedCount;
const mostRecentIcon = source.mostRecentIcon;
let sourceTag = source.sourceTag;
let excludeTag = source.excludeTag;
// prepend hashtags for tags
if (sourceTag) if (!sourceTag.startsWith("#")) sourceTag = "#" + sourceTag;
if (excludeTag) if (!excludeTag.startsWith("#")) excludeTag = "#" + excludeTag;
//----------------------------------------------------
// Functions
//----------------------------------------------------
function getWordCount(text) {
// Regex from BetterWordCount Plugin
const spaceDelimitedChars = /A-Za-z\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0-\u08B4\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AD\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC/
.source;
const nonSpaceDelimitedWords = /[\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u4E00-\u9FD5]{1}/
.source;
const pattern = new RegExp([
"(?:[0-9]+(?:(?:,|\\.)[0-9]+)*|[\\-" + spaceDelimitedChars + "])+",
nonSpaceDelimitedWords
].join("|"), "g");
return (text.match(pattern) || []).length;
}
function getCharacterCount(text) {
if (charactersIncludeSpaces) return text.length;
return text.replaceAll(" ", "").length;
}
function insert1000sep (num) {
let numText = String(num);
if (!useThousandSeperator) return numText;
if (num >= 10000) numText = numText.slice(0, -3) + thousandSeperator + numText.slice (-3); // eslint-disable-line no-magic-numbers
return numText;
}
String.prototype.strong = function () {
if (this === " ") return " ";
return "**" + this + "**";
};
function removeMarkdown (text) {
let plaintext = text
.replace(/`\$?=[^`]+`/g, "") // inline dataview
.replace(/^---\n.*?\n---\n/s, "") // YAML Header
.replace(/!?\[(.+)\]\(.+\)/g, "$1") // URLs & Image Captions
.replace(/\*|_|\[\[|\]\]|\||==|~~|---|#|> |`/g, ""); // Markdown Syntax
if (excludeComments) {
plaintext = plaintext
.replace(/<!--.*?-->/sg, "")
.replace(/%%.*?%%/sg, "");
}
else {
plaintext = plaintext
.replace(/%%|<!--|-->/g, ""); // remove only comment syntax
}
return plaintext;
}
function removeFootnotes (text) {
return text
.replace(/^\[\^[A-Za-z0-9-]+\]:.*$/gm, "") // footnote at the end
.replace(/\[\^[A-Za-z0-9-]+\]/g, ""); // footnote reference inline
}
function countPandocCitations (text) {
const citations = text.match(/@[A-Za-z0-9-]+(?=[,;\] ])/gi);
if (!citations) return 0;
const uniqCitations = [...new Set(citations)]; // only unique citations
return uniqCitations.length;
}
function toPercentStr (share) {
return (share * 100).toFixed(0).toString() + " %";
}
//----------------------------------------------------
// Table Construction
//----------------------------------------------------
async function getTableContents () {
const output = [];
let completeText = "";
let total = 0;
let share = 0;
let sectionCounter = 0;
let subsectionCounter = 0;
let totalTasks = 0;
// get sections via folder or via tag
let sections;
if (sourceFolder) sections = dv.pages("\"" + sourceFolder + "\"");
else sections = dv.pages(sourceTag);
// exclude certain notes
numExcludeStatus = sections.filter(t => t.status === "exclude").length;
sections = sections.filter(t => t.status !== "exclude");
if (excludeTag !== "") {
numExcludedNotes = sections.filter(t => t.file.tags.includes(excludeTag)).length;
sections = sections.filter(t => !t.file.tags.includes(excludeTag));
}
// most recent note
sections = sections.sort (s => s.file.mtime, "desc");
const mostRecentNote = sections[0].file.name;
// SORT sections
if (pathToIndexFile) {
const draftName = sourceFolder.split("/").pop();
const longformOrder =
dv.page(pathToIndexFile) // do not wrap in ", as with db.pages
.drafts
.filter(d => d.name === draftName)
.scenes;
sections = sections.sort(
s => s.file.name,
"desc",
(a, b) => longformOrder.indexOf(b) - longformOrder.indexOf(a)
);
} else {
sections = sections.sort(s => s.file.name);
}
//-------------------------------------------------
// SECTIONS LOOP
//-------------------------------------------------
for (const section of sections) {
// read page content
let content = await dv.io.load(section.file.path); // eslint-disable-line no-await-in-loop
// count markdown tasks
// need to be counted before cleanup
let tasks = content.match(/- \[ ] /g);
let taskNum = 0;
let taskStr = "";
if (tasks) {
taskNum = tasks.length;
taskStr = taskNum.toString();
}
// clean up
content = removeMarkdown (content);
if (!includeFootnotes) content = removeFootnotes (content);
content = content
.replace(/(^\s*)|(\s*$)/g, "") // remove the start and end spaces of the given string
.replace(/ {2,}/g, " "); // reduce multiple spaces to a single space
// Table Values: Count & Share
let wcCount = 0;
if (toCount === "words") wcCount = getWordCount(content);
if (toCount === "chars") wcCount = getCharacterCount(content);
if (cumulativeShare) share += (wcCount / target);
else share = (wcCount / target);
// Status
let status = section.status;
if (!status) status = " ";
// Section numbering
const isSubsection = section.file.name.startsWith(subsectionStartChar);
let sectionNumbering;
let sectionLink;
if (isSubsection) {
subsectionCounter++;
sectionNumbering = "<small>" + sectionCounter.toString() + "." + subsectionCounter.toString() + "</small>";
sectionLink =
"<small>[["
+ section.file.path
+ "|"
+ section.file.name.slice(1)
+ "]]</small>";
} else {
subsectionCounter = 0;
sectionCounter++;
sectionNumbering = sectionCounter.toString().strong();
sectionLink = "__" + section.file.link + "__";
}
// Most Recent Note
if (section.file.name === mostRecentNote) sectionLink += "&nbsp;&nbsp;&nbsp;" + mostRecentIcon;
// push table values
output.push([
sectionNumbering,
sectionLink,
wcCount,
share,
taskStr,
status
]);
// add to totals & bibliography calculation
totalTasks += taskNum;
total += wcCount;
if (includeBibliographyEstimate) completeText += content;
}
// Add Subsections counts to the sections
//-------------------------------------------------
if (groupedCount) {
let upperSectionID = -1;
for (var i = 0; i < output.length; i++) {
let isSubsection = output[i][0].includes(".");
let firstSectionFound = (upperSectionID !== -1);
if (!firstSectionFound && isSubsection) continue;
if (isSubsection) {
output[upperSectionID][2] += output[i][2]; // add count
if (!cumulativeShare) output[upperSectionID][3] += output[i][3]; // add share
}
if (!isSubsection) upperSectionID = i;
}
output.map(row => {
row[2] = insert1000sep(row[2]);
row[3] = toPercentStr(row[3]);
let isSubsection = row[0].includes(".");
if (isSubsection) {
row[2] = "<small>" + row[2] + "</small>";
if (!cumulativeShare) row[3] = "<small>" + row[3] + "</small>";
} else {
row[2] = "<u>" + row[2] + "</u>" ;
if (!cumulativeShare) row[3] = "<u>" + row[3] + "</u>";
}
return row;
});
}
//-------------------------------------------------
// OVERALL
//-------------------------------------------------
// Bibliography Estimate
if (includeBibliographyEstimate) {
const citationCount = countPandocCitations(completeText);
let wcCount = 0;
if (toCount === "words") wcCount = citationCount * wordsPerCitation;
if (toCount === "chars") wcCount = citationCount * charsPerCitation;
if (!charactersIncludeSpaces && toCount === "chars") wcCount = citationCount * (charsPerCitation - 20); // eslint-disable-line no-magic-numbers
if (cumulativeShare === "true") share += (wcCount / target);
else share = (wcCount / target);
output.push([
"",
"Bibliography (" + citationCount + " citations)",
"~" + insert1000sep(wcCount),
toPercentStr(share),
naChar,
naChar
]);
total += wcCount;
}
// Totals calculation
const totalShare = total / target;
let totalTitle = "Total";
if (wordsPerPage && toCount === "words") {
const totalPages = (total / wordsPerPage).toFixed(1);
totalTitle += "&nbsp;&nbsp;&nbsp;(~" + totalPages + " Pages)";
}
if (charsPerPage && toCount === "chars") {
const totalPages = (total / charsPerPage).toFixed(1);
totalTitle += "&nbsp;&nbsp;&nbsp;(~" + totalPages + " Pages)";
}
output.push([
"",
totalTitle.strong(),
insert1000sep(total).strong(),
toPercentStr(totalShare).strong(),
totalTasks.toString().strong(),
naChar.strong()
]);
// Target & Progress Bar
const progressBar =
"&nbsp;&nbsp;&nbsp;&nbsp;"
+ " <progress max=\"100\" value=\""
+ (totalShare * 100).toFixed().toString()
+ "\"> </progress>";
output.push([
"",
"Target".strong() + progressBar,
insert1000sep(target).strong(),
naChar.strong(),
naChar.strong(),
naChar.strong()
]);
return output;
}
//----------------------------------------------------
// Main
//----------------------------------------------------
// Print Table
let numExcludedNotes = 0;
let numExcludeStatus = 0;
let countedEntity = "Words";
if (toCount === "chars") countedEntity = "Chars";
let typeOfShare = "Share";
if (cumulativeShare === "true") countedEntity = "Target";
const tcontent = await getTableContents();
dv.table(["⟡", "Section", countedEntity, typeOfShare, "Tasks" ,"Status"], tcontent);
// Append Settings Info
let settingFt = "Footnotes excluded. ";
let settingExcludeTag = "";
let settingExcludeStatus = "";
let settingBibliography = "";
let settingCharSpaces = "Character Count includes Spaces. ";
let settingComments = "Comments included. ";
let settingPages = "";
if (includeFootnotes) settingFt = "Footnotes included. ";
if (!includeBibliographyEstimate) settingBibliography = "Bibliography excluded. ";
if (!charactersIncludeSpaces) settingCharSpaces = "Character Count without Spaces. ";
if (toCount === "words") settingCharSpaces = "";
if (excludeComments) settingComments = "Comments excluded. ";
if (wordsPerPage && toCount === "words") settingPages = "Assuming " + wordsPerPage.toString() + " words per page. ";
if (wordsPerPage && toCount === "chars") settingPages = "Assuming " + charsPerPage.toString() + " characters per page. ";
if (numExcludeStatus) {
let plural = "s";
if (numExcludeStatus === 1) plural = "";
const excludedQuery = "\"status: exclude\" path:(" + sourceFolder + ")";
settingExcludeStatus =
"[" + numExcludeStatus.toString() + " Section" + plural + "]"
+ "(obsidian://search?query=" + encodeURIComponent(excludedQuery) + ")"
+ " with the status \"exclude\" omitted. ";
}
if (numExcludedNotes) {
let plural = "s";
if (numExcludedNotes === 1) plural = "";
const excludedQuery = "tag:" + excludeTag + " path:(" + sourceFolder + ")";
settingExcludeTag =
"[" + numExcludedNotes.toString() + " Section" + plural + "]"
+ "(obsidian://search?query=" + encodeURIComponent(excludedQuery) + ")"
+ " tagged with " + excludeTag
+ " omitted. ";
}
dv.span(
"<small>"
+ "Settings: ".strong()
+ settingExcludeStatus
+ settingExcludeTag
+ settingFt
+ settingBibliography
+ settingComments
+ settingCharSpaces
+ settingPages
+ "</small>"
);

Wordcount Dashboard for Obsidian Dataview

image

Setup

  1. Install dataview
  2. Install the CSS file as CSS snippet.
  3. Create a note with the markdown note template.
  4. Insert the dataviewjs-script into the dataviewjs-codeblock
  5. Enter the configuration values in the note template. (The surrounding %% %% ensure that they are treated as comments, so the configuration will not be displayed in Preview Mode.)
  6. The status column will be populated with the value of the YAML-key status of every document, i.e., you have to use the add the following YAML-Header to every note.
  7. When also using the Longform Plugin, you can name the index file in your settings and the Dashboard will automatically change order based on the order of scenes in your Draft.

Wordcount_Dashboard

---
status: 
---

Known Issues & Troubleshooting

  • It seems that this dahsboard fails when amount of words / notes becomes too high. As far as I can tell, this is due to limitations by dataview/Obsidian, and one could only be tackled by a dedicated plugin for Word Counts. If you really want multi-note word counts, please request a plugin like that in the forum!
  • When dataview reports some error, try to use the default configuration with just the sourceFolder changed – that should work, and then try step-by-step to find out whether a certain configuration causes the dashboard to break.

Troubleshooting & Feature Requests

I am very sorry, but I unfortunately cannot really provide much support or implement feature requests. As much as I'd love to help out, the limitations of being an overly complex script running on top of dataview makes this dashboard quite brittle, and the fact that this isn't really a proper plugin make any sort of debugging extremely time-intensive (and writing a PhD and maintaining multiple plugins/themes already, my time is limited). If I do have the time, I'll maybe turn this into a proper plugin, but that would require quite some time, since it would require a lot of coding skills I do not have yet (being only a hobby coder.)

If you do want to have a nice word count dashboard in Obsidian, please make a request in the forum and/or make a feature request at one of the already existing plugins to integrate this dashboard (e.g. the longform plugin). Or, if you have coding experience yourself, feel free to take this code, customize it, and make a plugin of your own!

License

MIT License 2022

/* used to properly align the numbers of the dataviewjs wordcount snippet
https://gist.github.com/chrisgrieser/ac16a80cdd9e8e0e84606cc24e35ad99 */
.wordcountTable table.dataview.table-view-table td:first-child,
.wordcountTable table.dataview.table-view-table th:first-child {
text-align: center;
padding: 4px 7px;
border-left: none;
}
.wordcountTable table.dataview.table-view-table td:first-child {
color: var(--text-muted) !important;
}
.wordcountTable table.dataview.table-view-table td:nth-child(3),
.wordcountTable table.dataview.table-view-table td:nth-child(4) {
text-align: end;
}
.wordcountTable table.dataview.table-view-table th:nth-child(3),
.wordcountTable table.dataview.table-view-table td:nth-child(3),
.wordcountTable table.dataview.table-view-table th:nth-child(4),
.wordcountTable table.dataview.table-view-table td:nth-child(4),
.wordcountTable table.dataview.table-view-table th:nth-child(5),
.wordcountTable table.dataview.table-view-table td:nth-child(5) {
padding-right: 7px;
padding-left: 7px;
}
.wordcountTable table.dataview.table-view-table td:nth-child(2) {
text-align: start;
}
.wordcountTable table.dataview.table-view-table td:nth-child(5),
.wordcountTable table.dataview.table-view-table td:last-child {
text-align: center;
}
.wordcountTable .markdown-preview-section {
max-width: 100% !important;
}
.wordcountTable table.dataview.table-view-table th {
font-size: 1.1em;
text-align: center;
}
.wordcountTable table.dataview.table-view-table {
font-size: 0.9em;
}
.wordcountTable table.dataview.table-view-table a.internal-link {
text-decoration: none;
}
.wordcountTable progress {
margin-bottom: 3px;
}
/* target NA field */
.wordcountTable table.dataview.table-view-table tr:last-child td:nth-child(4) {
text-align: center;
}
/* Supercharged Links */
.data-link-icon[data-link-cssclass*="wordcount" i]::BEFORE { content: "🔢 " }
---
cssclass: wordcountTable
---
%%
__Notes to display__
*Gets either notes in a folder or notes with a certain tag. Leave one of them empty.*
sourceFolder:: Writing/Interdependence & Innovation/Drafts/Submission
sourceTag::
__Notes to exclude__
*Leave empty to disable. Notes with the yaml-key `status` and value `exclude` for that key are also excluded.)*
excludeTag:: #exclude
__Counting Settings__
*"chars" or "words"*
toCount:: chars
target:: 70000
*words or characters per page, depending on setting above. Set to zero to ignore.*
wordsPerPage:: 350
charsPerPage:: 2000
includeFootnotes:: true
charactersIncludeSpaces:: true
excludeComments:: true
cumulativeShare:: false
groupedCount:: true
__Bibliography Estimate for Pandoc Citations__
includeBibliographyEstimate:: true
wordsPerCitation:: 22
charsPerCitation:: 155
__Longform Plugin__
*Leave empty to sort alphabetically. Enter the path to the index file of a longform project to order sections by their order in the longform plugin. (The `sourceFolder` setting further above has to be a Longform Drafts folder. )*
pathToIndexFile::
*Begin a filename with this character and it will be treated as subsection*
subsectionStartChar:: _
__Purely visual__
useThousandSeperator:: true
thousandSeperator:: .
naChar:: —
mostRecentIcon:: 🕙
%%
```dataviewjs
<!-- put the code from above in a codeblock like this-->
```
@chrisgrieser
Copy link
Author

@ReaderGuy42 coudl you also post your configuration? like the key:: value fields from the markdown file?

@ReaderGuy42
Copy link

ReaderGuy42 commented Dec 8, 2021

The whole thing?


sourceTag:: #manuscript  

**set to 0 to ignore**

charTarget:: 157500

wordTarget:: 25000

wordsPerPage:: 300

includeFootnotes:: false

charactersIncludeSpaces:: true

excludeComments:: true

**Notes to exclude**

Leave empty to disable. (Notes with the yaml-key "status" and value "exclude" for that key are also excluded)

excludeTag:: exclude

**Bibliography estimate for Pandoc Citations**

includeBibliographyEstimate:: false

wordsPerCitation:: 22

charsPerCitation:: 155

**purely visual**

thousandSeperator:: ,

useThousandSeperator:: true

naChar:: —

maybe the word target of 25,000 is too high for this?

@chrisgrieser
Copy link
Author

The whole thing?
How else should I find out what's wrong? :P

Anyway, found the issue, use the new version of dataviewjs_wordcount_obsidian.js and it should work now! :)

@ReaderGuy42
Copy link

ReaderGuy42 commented Dec 8, 2021

How else should I find out what's wrong? :P

I wasn't sure if you just wanted the line that said wordTarget:: 2500 lol
anyways, thank you! works great :)

@jsmm
Copy link

jsmm commented Dec 8, 2021

Hi. I would also like to sort files by their wordCount, if I knew how to do it. Any help?

As of now, I am sorting the table by status . I find these sorting options useful in the first writing stages. I won't use the Sortable plugin whilst it's in beta stage.

// sections = sections.sort(s => s.file.name);
sections = sections.sort(s => s["status"]);

In later writing stages, I will use the default sorting option. Hopefully, with the Longform plugin but, if not, with numbered file names, chiefly because the target column here seems to be cumulative.

@Rainbell129
Copy link

Rainbell129 commented Dec 15, 2021

Hi, your code is of great use to me. Thanks for the good work!
But I'm still slightly confused with the target settings. Is it possible to set different targets for each file here? I'm not sure if I'm wrong but it seems the targets of all files are set the same in the current version. And all the js files are intended to track the progress of one project.

In my case, every note is a project on their own and they have different targets. Is it possible for me to use your code for such project tracking?

@ReaderGuy42
Copy link

ReaderGuy42 commented Dec 16, 2021

With the newest update I think the total word count and thus also the page count is broken. The files are all showing up correctly, but where the total should show up for total words it just says NaN and the same for Total (NaN Pages).

image
Like you wrote in Discord, I copied over the new .js, the new .css and the new markdown template.

@chrisgrieser
Copy link
Author

I find these sorting options useful in the first writing stages. I won't use the Sortable plugin whilst it's in beta stage.

@jsmm And why not? This dashboard is a side project that certainly fewer people will have reviewed than a plugin in beta. So if security or stability is your concern, in both regards the sortable plugin would be better. It feels a bit inefficient to code something sortability options when there is a plugin already does that and more?

Nevertheless, I have implemented to inherit the manual order from the longform plugin as well as section numbering and sub chapters. Maybe that's enough for you?

@Rainbell129 Yes, the way this dashboard is implemented, the target is for the overall project, since that is my use case. Per-note-targets also sound interesting, when I find the time I'll maybe add them in the future.

@ReaderGuy42 well, it didn't need to be the configuration and the whole script on top of it :D
just a typo this time, it's fixed now. Update the .js file :)

@ReaderGuy42
Copy link

Whoops, sorry, wasn't sure what you needed. I copied the new .js file, but now I'm getting another error:
image

@chrisgrieser
Copy link
Author

@ReaderGuy42 got it, fixed in the updated .js file!

@jsmm
Copy link

jsmm commented Dec 16, 2021

@chrisgrieser It works beautifully. Thanks!

@Rainbell129
Copy link

Rainbell129 commented Dec 17, 2021

@chrisgrieser thanks for the reply. I have made some modifications on your code to allow separate counting and tracking.
E2F786FB-5A5B-49E8-9201-5CB3BE093E02
I have to say it’s an excellent complement to the homepage I’m designing. Thanks for making this!

@tyf2018
Copy link

tyf2018 commented Dec 17, 2021

Hi, @Rainbell129 ,这段代码方便分享下吗?谢谢
image

@Rainbell129
Copy link

Rainbell129 commented Dec 17, 2021

Currently it’s based on the plug-in react components.

I will add all the codes to my GitHub when it’s all finished.

The basic draft code is this.


defines-react-components: true

const files = app.vault.getMarkdownFiles();
let count = 0;
let final = "小鸟倒计时 "

for (var ita=0;ita<files.length;ita++){
	let today = new Date();
	let year = today.getFullYear();
	let yearString = year.toString();
	const basename = files[ita].basename;
	
	const meta = app.metadataCache.getFileCache(files[ita]).frontmatter;
	if (typeof(meta) != "undefined"){
		const birthday = app.metadataCache.getFileCache(files[ita]).frontmatter.birthday;
		if (birthday){
			let birthdaySlice = birthday.slice(4);
			let nextBirthdayString = yearString+birthdaySlice;
			let nextBirthdayNumber = new Date(nextBirthdayString);
			let diffBetween = Math.floor((nextBirthdayNumber-today)/3600000/24);
			let N = 0;
			diffBetween>0? N = diffBetween:N = diffBetween+365;
			if (N < 60){
				count = count+1;
				final += `${basename}生日还有${N}天
				`
			}
		}
	}
}

const element = <h1>未来360天有{count}只鸟过生日</h1>;
	


<BirdCountdown></BirdCountdown>

@tyf2018
Copy link

tyf2018 commented Dec 17, 2021

@Rainbell129 Thanks!

@Rhydderch
Copy link

@Rainbell129 Your dashboard looks awesome! And great that you plan on sharing your code. I really look forward to it as I would love to have something similar.

@chrisgrieser Thank you for this code. The single note targets would be great as it could help me target a certain number of words per section and see whether I am above or under that target. Perhaps this could be an additional column ? "Note target"
This way, the total target for a project could still remain at the bottom of the table. And at the same time, the code would be flexible enough for tracking single notes without an overall project just like @Rainbell129 .

@SinglePIXL
Copy link

Hello! This is a really fantastic plugin, but I'm having some trouble setting it up. I've placed the word count note in my writing folder and pointed folderPath:: to my drafts folder, but the table is showing every note in my vault. Is there a setting that I'm missing?

@Liong1976
Copy link

Hi Chris, I got a question. Do you know why the code doesn't work when I put the relative path to the Index.md?
Here is what it shows when I put the Index file's path.
image

Here is my configuration.
image

Many thanks in advance!

@chrisgrieser
Copy link
Author

Regarding Bug Reports & Feature Requests

I am very sorry everyone, but I unfortunately cannot really provide much support or implement feature requests. As much as I'd love to help out, the limitations of being an overly complex script running on top of dataview makes this dashboard quite brittle; and the fact that this isn't a proper plugin make any sort of debugging extremely time-intensive. (And writing a PhD thesis and maintaining multiple plugins/themes already, my time is somewhat limited). If I do find the time, I'll maybe turn this into a proper plugin, but probably not in the near future, since it would require a lot of coding skills I do not have yet (being only a hobby coder).

If you do want to have a nice wordcount dashboard in Obsidian, please make a request in the forum and/or make a feature request at a related, already existing plugin to integrate this dashboard (e.g. the longform plugin). Or, if you have coding experience yourself, feel free to take this code, customize it, and make a plugin of your own! (MIT License)

@pdworkman
Copy link

Occasionally appearing bug:

Sometimes when I first open my vault, the word count dashboard gives me an error and doesn't display the table.

Evaluation Error: TypeError: Cannot read property 'strong' of undefined
at getTableContents (eval at (eval at (app://obsidian.md/app.js:1:1362602)), :245:10)
at async eval (eval at (eval at (app://obsidian.md/app.js:1:1362602)), :279:18)
at async DataviewJSRenderer.render (eval at (app://obsidian.md/app.js:1:1362602), :16267:13)

I have figured out that if I delete most of the path here:

Notes to display
Gets either notes in a folder or notes with a certain tag. Leave one of them empty.
sourceFolder:: ZG 13/Drafts/Draft 1
sourceTag::

So that my sourceFolder is just ZG 13

Then it will work for that path. Then I add the rest of the path back on again, and it goes back to working normally.

It's quick, now that I know how to fix it, to cut /Drafts/Draft 1, cmd-E, cmd-E again, and then paste the path back again. But not sure what it is refreshing when I do that.

@chrisgrieser
Copy link
Author

@pdworkman I assume that on Vault Startup, a lot of stuff get loaded (plugins, themes, etc.) already, and then loading all the data for the wordcount dashboard on that of that is simply too much. Your change of the path most likely causes dataview to re-trigger the dashboard after all the loading is already done.

This is a limitation I cannot do much about, as it's most likely related to Obsidian or to dataview, sorry :(

@tyf2018
Copy link

tyf2018 commented Feb 22, 2022

Hi, @chrisgrieser
My theme automatically adds sequence numbers to all tables, so there are duplicate sequence numbers. Is there a way to disable the serial number feature of your code?

Thanks!

image

@ReaderGuy42
Copy link

How would I go about moving the bottom line with the Total pages and word count, etc. to the top of the table? Is that even possible/easy? I was just wondering since I have a long list of files in the table and it would be quicker to have that at the top. Thanks :)

@nathanielarking
Copy link

I'm trying to set up this snippet with the longform plugin, but I can't seem to get it to work. Can someone verify my file paths are correct?

sourceFolder:: Manuscripts/Part 1/Draft 1

pathToIndexFile:: Manuscripts/Part 1/Draft 1/Index.md

It works when I have sourceFolder set, but when I add the pathToIndexFile, I get this error:

Evaluation Error: TypeError: Cannot read properties of undefined (reading 'filter') at getTableContents (eval at <anonymous> (plugin:dataview), <anonymous>:143:4) at eval (eval at <anonymous> (plugin:dataview), <anonymous>:345:24) at eval (eval at <anonymous> (plugin:dataview), <anonymous>:399:4) at DataviewInlineApi.eval (plugin:dataview:18370:16) at evalInContext (plugin:dataview:18371:7) at asyncEvalInContext (plugin:dataview:18378:16) at DataviewJSRenderer.render (plugin:dataview:18402:19) at DataviewRefreshableRenderer.maybeRefresh (plugin:dataview:17980:22) at t.e.tryTrigger (app://obsidian.md/app.js:1:1045441) at t.e.trigger (app://obsidian.md/app.js:1:1045374)

@J-FSS
Copy link

J-FSS commented Jan 31, 2023

Unfortunately, I cannot confirm the files paths are correct @nathanielarking , but I can confirm I get the same error and would be very interested in knowing how this may be resolved.

@Liong1976 , did you figure out a fix?

PS thank you @chrisgrieser for taking the time to share your (academic) work and insights here, on Notion, and the Obsidian forum, I really appreciate it!

@Liong1976
Copy link

Hi,

The new version of Longform (2.0,0 and above) has made me not using these codes anymore, for the plugin has the feature of word counting for each scene and the total project,

@ReaderGuy42
Copy link

@Liong1976 could you link me to some info on that Longform version? I'm using it but haven't seen that bit. Thanks!

@Liong1976
Copy link

Sure, @ReaderGuy42

Here is the link

@J-FSS
Copy link

J-FSS commented Feb 1, 2023

Hi @Liong1976 I understand, thank you for the quick reply!

If anyone else knows how to sort scenes using the index file, I would be very happy to learn how fix it to get a complete overview of my work.

@Liong1976
Copy link

You are welcome, @J-FSS!

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