Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save ataraxies/43dfec1a2ba434c8c055366cd99c0285 to your computer and use it in GitHub Desktop.

Select an option

Save ataraxies/43dfec1a2ba434c8c055366cd99c0285 to your computer and use it in GitHub Desktop.
Creates a writing goals status board for an active Obsidian note using dataview.js.

Writing Goals Status Board

writing_goals_status_board.mp4

Getting Started

  • Install Dataview
  • Install Templater
  • Install the CSS file as CSS snippet.
  • Create a note with the markdown status board template below.
  • Create a template with the markdown YAML template below.
  • Insert the dataviewjs-script into the dataviewjs-codeblock within you markdown status board note.
  • Use the YAML template for your notes and populate the fields
  • The status board will come to life so long as you have the showWritingGoals populated in your active note.
```dataviewjs
// Writing Goals Status Component (Obsidian dataviewjs snippet)
// by @chetachi, https://gist.github.com
// version 0.0.1
let file = app.workspace.activeLeaf.view.file;
let currentPage = file ? dv.page(file.path) : "";
let text = file ? file.unsafeCachedData : null;
let charTarget = currentPage.charTarget;
let wordTarget = currentPage.wordTarget;
let sentenceTarget = currentPage.sentenceTarget;
let wordsToRemove = currentPage.wordsToRemove;
let charsToRemove = currentPage.charsToRemove;
let sentencesToRemove = currentPage.sentencesToRemove;
let charactersIncludeSpaces = currentPage.charactersIncludeSpaces;
let includeFootnotes = currentPage.includeFootnotes;
let excludeComments = currentPage.excludeComments;
let fileStatus = currentPage.status;
let setWordsPerMinute = currentPage.wordsPerMinute;
let showWritingGoals = currentPage.showWritingGoals;
let fileAudio = currentPage.audio;
let fileDecription = currentPage.description;
let pageTitle = currentPage.file.name;
const componentDiv = this.container.createDiv();
componentDiv.classList += "count-div pattern-2";
text = text.replace(/(^\\s\*)|(\\s\*$)/gi,"");
text = text.replace(/\[ \]{2,}/gi," ");
text = text.replace(/\\n /,"\\n");
function setAttributes(element, attributes) {
for (let key in attributes) {
element.setAttribute(key, attributes[key]);
}
}
function getWordCount(text) {
let 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;
let nonSpaceDelimitedWords = /\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u4E00-\u9FD5/
.source;
let 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 getSentenceCount(text) {
let sentences = ((text || "").match(/[^.!?\s][^.!?]*(?:[.!?](?!['"]?\s|$)[^.!?]*)*[.!?]?['"]?(?=\s|$)/gm) || []).length;
return sentences;
}
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,"") //HTML comments
.replace(/%%.*?%%/sg,""); //Obsidian comments
else plaintext = plaintext
.replace(/%%|<!--|-->/g,""); //remove only comment syntax
return plaintext;
}
function removeFootnotes (text){
return text.replace(/^\[\^\w+\]:.*$/gm,"") //footnote at the end
.replace(/\[\^\w+\]/g,""); //footnote reference inline
}
let textContent = text;
textContent = removeMarkdown (textContent);
if (!includeFootnotes) textContent = removeFootnotes (textContent);
let wordCount = getWordCount(textContent) - wordsToRemove;
let characterCount = getCharacterCount(textContent) - charsToRemove;
let sentenceCount = getSentenceCount(textContent) - sentencesToRemove;
let wordCountPercentage = wordTarget ? wordCount/wordTarget : 0;
let characterCountPercentage = charTarget ? characterCount/charTarget : 0;
let sentenceCountPercentage = sentenceTarget ? sentenceCount/sentenceTarget : 0;
function toPercentStr (percentage){
return (percentage * 100).toFixed(1).toString() + "%";
}
// < Create Settings Info
let setting_ft = "Footnotes excluded. ";
let setting_charSpaces = "Character Count includes Spaces. ";
let setting_comments = "Comments included. ";
if (includeFootnotes) setting_ft = "Footnotes included. ";
if (!charactersIncludeSpaces) setting_charSpaces = "Character Count without Spaces. ";
if (excludeComments) setting_comments = "Comments excluded. ";
function createSettings(){
return(
`<hr/><small><strong>Settings:</strong> ${setting_ft} ${setting_comments} ${setting_charSpaces}</small>`
)
}
let numStats = (wordTarget > 0 ? 1 : 0) + (charTarget > 0 ? 1 : 0) + (sentenceTarget > 0 ? 1 : 0);
let overallPercentage = (wordCountPercentage + characterCountPercentage + sentenceCountPercentage) / numStats;
function getStat(percentage){
return(
percentage >= 1 ? `🥳 Target has been reached` :
percentage >= .8 ? `📄 Almost there` :
percentage >= .6 ? `✍🏽 Keep going` :
percentage >= .4 ? `✏️ Making progress` :
percentage >= .2 ? `🙏🏽 Let's do this!` :
percentage >= .05 ? `😪 Just getting started` :
`🔴 No Progress Yet`
)
}
let progressSvgEl = document.createElement("svg");
let progressInnerEl = this.container.createEl("svg");
let progressText = this.container.createEl("text");
let progressTrack = this.container.createEl("path");
let progressFill = this.container.createEl("path");
let percent = (overallPercentage * 100).toFixed(1);
let max = -229.99078369140625;
let strokeVal = (40.64 * 100) / 200;
setAttributes(progressInnerEl, {
"class": "progress accent noselect",
"data-progress": `${Math.round(overallPercentage * 100 / 10)}`,
"x":"0px",
"y":"0px",
"viewBox": "0 0 80 80"
})
setAttributes(progressTrack, {
"class": "track",
"d": "M5,40a35,35 0 1,0 70,0a35,35 0 1,0 -70,0",
})
setAttributes(progressFill, {
"class": "fill",
"d": "M5,40a35,35 0 1,0 70,0a35,35 0 1,0 -70,0",
"style": `stroke-dashoffset: ${(((100 - percent) / 100) * max)}; stroke-dasharray: ${(40.64) * (strokeVal) + ' 999'}`
})
setAttributes(progressText, {
"class": "value",
"x":"50%",
"y":"57%",
})
progressText.innerHTML = `${percent >= 0 ? percent : `--`}%`;
progressInnerEl.appendChild(progressTrack);
progressInnerEl.appendChild(progressFill);
progressInnerEl.appendChild(progressText);
progressSvgEl.appendChild(progressInnerEl);
let wordsPerMinute = setWordsPerMinute ? setWordsPerMinute : 250; // Average case.
let result;
let textLength = wordCount; // Split by words
if(textLength > 0){
let value = Math.ceil(textLength / wordsPerMinute);
result = `<i style="font-weight: 900; color: var(--interactive-accent);">~</i> ${value} min read`;
}else{
result = "--"
}
componentDiv.innerHTML =
`<div class="progress-wrapper"><text class="progress-item-title">INFO</text><div class="progress-content-div"><span class="progress-list-item"><h5 class="file-title">${pageTitle}</h5></span>${fileDecription ? `<span class="progress-list-item file-description">${fileDecription}</span>` : ''}</div><hr/><text class="progress-item-title">OVERALL</text><div class="progress-content-div center-text">${progressSvgEl.innerHTML}<small class="progress-list-item small-width">${wordTarget && wordCount || characterCount && charTarget || sentenceCount && sentenceTarget ? `Calculating: ` : ``}${wordTarget && wordCount? `word count` : ''} ${characterCount && charTarget? `character count` : ''} ${sentenceCount && sentenceTarget? `sentence count` : ''}</small>${result}</div><text class="progress-item-title">PROGRESS</text><div class="progress-content-div">${wordTarget ? `<span class="progress-list-item">${wordCount} / ${wordTarget} words<span class="progress-stat"><progress value=${(wordCount / wordTarget >= 1 ? 1 : wordCount / wordTarget) * 100} max="100"></progress>${toPercentStr(wordCountPercentage)}</span></span>` : ''}${charTarget ? `<span class="progress-list-item">${characterCount} / ${charTarget} characters<span class="progress-stat"><progress value=${(characterCount / charTarget >= 1 ? 1 : characterCount / charTarget) * 100} max="100"></progress>${toPercentStr(characterCountPercentage)}</span></span>` : ''}${sentenceTarget ? `<span class="progress-list-item">${sentenceCount} / ${sentenceTarget} sentence(s)<span class="progress-stat"><progress value=${(sentenceCount / sentenceTarget >= 1 ? 1 : sentenceCount / sentenceTarget) * 100} max="100"></progress> ${toPercentStr(sentenceCountPercentage)}</span></span>` : ''}<div class="progress-content-item center-text">${getStat(overallPercentage)}</div></div><text class="progress-item-title">GOALS</text><div class="progress-content-div">${wordTarget ? `<span class="progress-list-item">Word Count Goal: ${wordTarget} words</span>` : ''}${charTarget ? `<span class="progress-list-item">Char Count Goal: ${charTarget} characters</span>` : ''}${sentenceTarget ? `<span class="progress-list-item">Sentence Count Goal: ${sentenceTarget} sentences</span>` : ''}</div>
${createSettings()}
</div>`;
!file || !showWritingGoals ? this.container.removeChild(componentDiv) : this.container.appendChild(componentDiv);
```
.progress-wrapper{
width: 100%;
height: 100%;
margin: 0 auto;
padding: .86em;
}
.count-div.pattern-2{
height: fit-content;
width: 100%;
}
.progress-content-div{
width: 100%;
height: fit-content;
padding: .98em .86em;
margin: 10px auto;
border-radius: 6px;
background: var(--background-zero);
border: 1px solid var(--background-tertiary);
}
.theme-dark .progress-content-div{
background: var(--background-primary);
}
.progress-content-item.center-text,
.progress-content-div.center-text{
text-align: center;
}
.progress-list-item{
display: list-item;
list-style: none;
}
h5.file-title{
font-size: 18px;
margin: 4px auto;
margin-top: 8px;
font-weight: 500;
text-decoration-color: var(--interactive-accent-hover);
text-decoration-line: underline;
text-decoration-thickness: inherit;
text-decoration-style: double;
text-transform: capitalize;
}
.progress-list-item.file-description{
display: block;
text-overflow: ellipsis;
overflow: hidden;
max-height: 80px;
margin-top: 8px;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
color: var(--text-muted);
font-size: 16px;
line-height: 1.32em;
}
.progress-list-item.small-width{
width: 180px;
margin: 0 auto;
line-height: 1.26em;
margin-bottom: 8px;
}
.progress-stat{
display: flex;
grid-gap: 5px;
align-content: center;
align-items: center;
}
/*===== The CSS =====*/
.progress{
max-width: 320px;
height: auto;
}
.progress .track, .progress .fill{
fill: var(--background-secondary);
stroke-width: 3;
transform: rotate(90deg)translate(0px, -80px);
}
.theme-dark .progress .track, .theme-dark.progress .fill{
fill: var(--background-zero);
}
.progress .track{
stroke: var(--background-secondary);
}
.progress .fill {
stroke: rgb(255, 255, 255);
stroke-dasharray: 219.99078369140625;
stroke-dashoffset: -219.99078369140625;
transition: stroke-dashoffset 1s;
}
.progress.accent .fill {
stroke: var(--interactive-accent);
}
.progress .value, .progress .text {
fill: var(--interactive-accent);
text-anchor: middle;
}
.progress .text {
font-size: 12px;
}
.progress-item-title{
font-weight: 600;
}
.progress-item-title:before{
content: "✽ ";
font-weight: 900;
color: var(--interactive-accent);
}
.noselect {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
cursor: default;
}
/*progress bars*/
progress {
background: none;
height: .64em;
width: 100%;
}
progress[value^="0"]::-webkit-progress-value {
background: var(--red-highlighter);
}
progress[value^="1"]::-webkit-progress-value {
background: #FF558280;
}
progress[value^="2"]::-webkit-progress-value {
background: #FF5582CC;
}
progress[value^="3"]::-webkit-progress-value {
background: #FFB86CCA;
}
progress[value^="4"]::-webkit-progress-value {
background: #FFB86CCC;
}
progress[value^="5"]::-webkit-progress-value {
background: #FFE84CAC;
}
progress[value^="6"]::-webkit-progress-value {
background: #FFE84CCC;
}
progress[value^="7"]::-webkit-progress-value {
background: #95F695CC;
}
progress[value^="8"]::-webkit-progress-value {
background: #64E048AC;
}
progress[value^="9"]::-webkit-progress-value {
background: #64E048CC;
}
progress[value^="10"]::-webkit-progress-value {
background: var(--interactive-accent-hover);
}
progress::-webkit-progress-value {
border-radius: 20px;
transition: 5s width;
background: var(--interactive-accent-hover);
}
progress::-webkit-progress-bar {
border-radius: 16px;
background: var(--background-primary-alt);
}
---
tags:
aliases:
cssclass:
showWritingGoals: true
wordsPerMinute: 200
wordTarget: 250
charTarget: 4000
sentenceTarget: 28
wordsToRemove: 996
charsToRemove: 7799
sentencesToRemove: 2
excludeComments: true
charactersIncludeSpaces: false
description: "This is a component used to show your writing progression. Stats include: word count, character count, and sentence count."
---
%%
**Creates the status board for the current note. Leave empty or set to false to avoid building the board**
`showWritingGoals`
**Configures words perminute calucation. Set to 250 by default. Leave empty to avoid changing.**
`wordsPerMinute`
**Creates the target values to calculate statuses. Leave empty or set to 0 to avoid setting status**
`charTarget`
`wordTarget`
`sentenceTarget`
**Removes values from status calculations. Leave empty or set to 0 to avoid removing values**
`wordsToRemove`
`charsToRemove`
`sentencesToRemove`
**Allows you to further configure your calculations. Leave empty or set to false to avoid configuring**
`includeFootnotes`
`charactersIncludeSpaces`
`excludeComments`
**For aesthetics ❤️**
`description`
%%
```dataviewjs
<!-- put the above status board code in a codeblock like this-->
```
tags aliases cssclasses showWritingGoals wordsPerMinute wordTarget charTarget sentenceTarget wordsToRemove charsToRemove sentencesToRemove excludeComments includeFootnotes charactersIncludeSpaces description
true
1600
true
true
false
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment