Novel-writing info panel (with dataviewjs)

I’m writing a novel in Obsidian, but what I’m really missed here is Scrivener-like info panel, where I can see all the info related to the scene or the whole project, like synopsis, words progress, list of characters, etc. So I decided to create one myself with the help of dataviewjs and share it with other people.

How to set it up: make sure you have Dataview plugin installed and javascript queries turned on. Copy this code to the separate file and pin this file to the sidebar.

```dataviewjs

const createProgressBar = (char_count, char_goal, words_count, words_goal) => {
    let percents = 0
    if (char_goal != undefined) {
      percents = Math.round(char_count / char_goal * 100)
    } else if (words_goal != undefined) {
      percents = Math.round(words_count / words_goal * 100)
    }
    dv.paragraph("<progress max=100 value=" + percents + "> </progress> " + percents + "%")
}


const getFirstSplit = (split) => {
  if (split.type == "tabs") {
    return split
  } else if (split.type == "split") {
    return getFirstSplit(split.children[0])
  }
}

let firstSplit = getFirstSplit(app.workspace.rootSplit)

let current = firstSplit.children[firstSplit.currentTab].view.file

if (current != null) {
  let page = dv.page(current.path)
  
  if (page != undefined) {
    let char_goal = page.char_goal
    let words_goal = page.words_goal
    let synopsis = page.synopsis
    let characters = page.characters
    let tags = page.tags
    let project = page.project
    let highlights


    if (project != undefined) {
      dv.header(4, "[[" + project + "]]")
    }


    if (synopsis != undefined) {
      dv.span("***")
      dv.header(5, "Synopsis")
      dv.span(synopsis)
    }



    if (page.type == "writing scene") {


      let content = await dv.io.load(current.path)
      highlights = content.match(/==.*?==/g)
      content = content.replace(/^---\n.*?\n---/ms, "").trim().replaceAll("—", "").replaceAll(/[\n]+/mg, " ").replaceAll(/[ ]+/mg, " ").replaceAll("==", "").replaceAll("*", "").replaceAll("#", "")
      let words = content.split(" ")
      
      dv.span("***")
      dv.header(5, "Scene progress")


      if (words_goal != undefined) {
        dv.paragraph(words.length + " / " + words_goal + " words")
      }

      if (char_goal != undefined) {
        dv.paragraph(content.length + " / " + char_goal + " characters")
      }
      
      createProgressBar(content.length, char_goal, words.length, words_goal)
    }




  if (page.type == "writing project") {
    let path = '"' + page.file.folder + '"'
    let scenePages = dv.pages(path).filter(p => p.type == "writing scene")
    let words_count = 0
    let char_count = 0

    let scenesData = [] 

    for (let scene of scenePages) {
      let sceneContent = await dv.io.load(scene.file.path)
      sceneContent = sceneContent.replace(/^---\n.*?\n---/ms, "").trim().replaceAll("—", "").replaceAll(/[\n]+/mg, " ").replaceAll(/[ ]+/mg, " ").replaceAll("==", "").replaceAll("*", "").replaceAll("#", "")
      let sceneWords = sceneContent.split(" ")
      words_count = words_count + sceneWords.length
      char_count = char_count + sceneContent.length
      scenesData.push({words_count: sceneWords.length, char_count: sceneContent.length, link: scene.file.link, words_goal: scene.words_goal, char_goal: scene.char_goal})
	  }

    dv.span("***")
    dv.header(5, "Novel progress")

    if (words_goal != undefined) {
      dv.paragraph(words_count + " / " + words_goal + " words")
    }
    if (char_goal != undefined) {
      dv.paragraph(char_count + " / " + char_goal + " characters")
    }
    createProgressBar(char_count, char_goal, words_count, words_goal)

    dv.span("***")
    dv.header(5, "Scenes progress")
    
    scenesData.forEach(scene => {
      dv.span(scene.link)
      createProgressBar(scene.char_count, scene.char_goal, scene.words_count, scene.words_goal)
    })
  }




   if (characters != undefined) {
      dv.span("***")
      dv.header(5, "Characters")
      dv.span(characters.map(c => " [[" + c + "]]") + "")
    }
    
   if (tags != undefined) {
      dv.span("***")
      dv.header(5, "Tags")
      dv.span(tags.map(c => " #" + c) + "")
    }
    
    if (highlights != undefined) {
      highlights = highlights.map(h => h.replaceAll("==", ""))
      dv.span("***")
      dv.header(5, "Highlights")
      dv.paragraph(highlights)
    }
  }
}

```

Next you need to set up your novel folder. Create a new folder for the novel anywhere. Inside of this folder you can create files of two types: scene notes or project notes. In scene notes you will write your text, and the project note is the place to accumulate info about the whole novel.

In scene notes you can write this metadata:

---
type: writing scene
project: My New Novel
words_goal: 2400
char_goal: 15000
synopsis: Something interesting happens in this scene.
characters:
- John
- Mary
tags:
- fantasy
- unfinished
---

where project is the name of your novel, words_goal and char_goal are the goals for the current scene (you can use both or only one of them, if you wish), synopsis, characters and tags are optional. Feel free to change anything except for the type, the type is important for the script to work.

You need only one project note. Put it in the root of your folder and give it the same name as your novel, for links to work correctly. In project note you can write this metedata:

---
type: writing project
words_goal: 48000
char_goal: 300000
synopsis: There is a novel about some characters doing some things.
characters:
- John
- Mary
- Peter
tags:
- fantasy
- unfinished
---

In this case the goals and all other metadata are related to the whole novel.

Now if you open any of your scene notes along with the pinned note in the sidebar, you will see something like this:

And if you open the project note, you will see this:

As you can see, in the project note info you can see progress for every scene and also accumulated progress for the whole novel. Everything written in the non-scene files does not count. It is also easy enough to tweak the code to add your own metadata.

It is worth mentioning that the info panel shows info for the first pane in your workspace, so if you open several files in splitted view, you will see the info for the top left one.

15 Likes

This is fantastic!! :slight_smile:
Thank you very much! :slight_smile:

1 Like

I changed the code a little bit, so now it is possible to track progress not only for the whole file, but also for the current session. For example, you can see how many words you should write before you hit your daily goal.

```dataviewjs

const createProgressBar = (char_count, char_goal, words_count, words_goal) => {
    let percents = 0
    if (char_goal != undefined) {
      percents = Math.round(char_count / char_goal * 100)
    } else if (words_goal != undefined) {
      percents = Math.round(words_count / words_goal * 100)
    }
    dv.paragraph("<progress max=100 value=" + percents + "> </progress> " + percents + "%")
}


const getFirstSplit = (split) => {
  if (split.type == "tabs") {
    return split
  } else if (split.type == "split") {
    return getFirstSplit(split.children[0])
  }
}

let firstSplit = getFirstSplit(app.workspace.rootSplit)

let current = firstSplit.children[firstSplit.currentTab].view.file

if (current != null) {
  let page = dv.page(current.path)
  
  if (page != undefined) {
    let char_goal = page.char_goal
    let words_goal = page.words_goal
    let synopsis = page.synopsis
    let characters = page.characters
    let tags = page.tags
    let project = page.project
    let highlights


    if (project != undefined) {
      dv.header(4, "[[" + project + "]]")
    }


    if (synopsis != undefined) {
      dv.span("***")
      dv.header(5, "Synopsis")
      dv.span(synopsis)
    }



    if (page.type == "writing scene") {


      let content = await dv.io.load(current.path)
      highlights = content.match(/==.*?==/g)
      content = content.replace(/^---\n.*?\n---/ms, "").trim().replaceAll("—", "").replaceAll(/[\n]+/mg, " ").replaceAll(/[ ]+/mg, " ").replaceAll("==", "").replaceAll("*", "").replaceAll("#", "")
      let words = content.split(" ")
      
      dv.span("***")
      dv.header(5, "Scene progress")


      if (words_goal != undefined) {
        dv.paragraph(words.length + " / " + words_goal + " words")
      }

      if (char_goal != undefined) {
        dv.paragraph(content.length + " / " + char_goal + " characters")
      }
      
      createProgressBar(content.length, char_goal, words.length, words_goal)

      let words_session_goal = page.words_session_goal
      let words_current = page.words_current
      
      if (words_current == undefined) {
        words_current = 0
      }


      let words_in_session = words.length - words_current
      
      if (words_session_goal != undefined) {
        dv.span("***")
        dv.header(5, "Session progress")
        dv.paragraph(words_in_session + " / " + words_session_goal + " words written")
        dv.paragraph(words_session_goal - words_in_session + " words left to write")
        createProgressBar(undefined, undefined, words_in_session, words_session_goal)
      }
    }


  if (page.type == "writing project") {
    let path = '"' + page.file.folder + '"'
    let scenePages = dv.pages(path).filter(p => p.type == "writing scene")
    let words_count = 0
    let char_count = 0

    let scenesData = [] 

    for (let scene of scenePages) {
      let sceneContent = await dv.io.load(scene.file.path)
      sceneContent = sceneContent.replace(/^---\n.*?\n---/ms, "").trim().replaceAll("—", "").replaceAll(/[\n]+/mg, " ").replaceAll(/[ ]+/mg, " ").replaceAll("==", "").replaceAll("*", "").replaceAll("#", "")
      let sceneWords = sceneContent.split(" ")
      words_count = words_count + sceneWords.length
      char_count = char_count + sceneContent.length
      scenesData.push({words_count: sceneWords.length, char_count: sceneContent.length, link: scene.file.link, words_goal: scene.words_goal, char_goal: scene.char_goal})
	  }

    dv.span("***")
    dv.header(5, "Novel progress")

    if (words_goal != undefined) {
      dv.paragraph(words_count + " / " + words_goal + " words")
    }
    if (char_goal != undefined) {
      dv.paragraph(char_count + " / " + char_goal + " characters")
    }
    createProgressBar(char_count, char_goal, words_count, words_goal)

    dv.span("***")
    dv.header(5, "Scenes progress")
    
    scenesData.forEach(scene => {
      dv.span(scene.link)
      createProgressBar(scene.char_count, scene.char_goal, scene.words_count, scene.words_goal)
    })
  }




   if (characters != undefined) {
      dv.span("***")
      dv.header(5, "Characters")
      dv.span(characters.map(c => " [[" + c + "]]") + "")
    }
    
   if (tags != undefined) {
      dv.span("***")
      dv.header(5, "Tags")
      dv.span(tags.map(c => " #" + c) + "")
    }
    
    if (highlights != undefined) {
      highlights = highlights.map(h => h.replaceAll("==", ""))
      dv.span("***")
      dv.header(5, "Highlights")
      dv.paragraph(highlights)
    }
  }
}
```

To the scene file’s frontmatter you should add metadata like this:

words_session_goal: 500
words_current: 208

where “words_session_goal” is your goal for the session and “words_current” is the word count at the beginning of the session. Unfortunately stats are not saved automatically, so you should manually update your current word count in the frontmatter at the beginning of the new session.

The result is going to look like this:

3 Likes

This is great! Now, who’s going to turn it into a plugin???

1 Like

I’m actually trying to learn how to make plugins maybe I’ll try to do this eventually.

2 Likes

I like the idea of having a panel that displays the YAML and synopsis. I almost never work to scene or project goals, so I rarely need to track their progress. Have tried and failed to limit the dvjs to just display the fields I want. Will have to keep on trying or hope that a plugin makes this user configurable. But absolutely brilliant. Useful, I think, for writing tasks beyond creative writing.

EDIT: At the moment, I use a regime similar to the one by PD Workman (@pdworkman) described below, with the ‘problem’ being that I have to change the main and side panels manually each time I select a different file — would be so much easier if the two were linked as in the solution created here by @reaty.

If you don’t care abour progress bars and just want to see some yaml fields, you can simply use something like this (just change “value1” and “value2” to whatever fields you care about):

```dataviewjs

const getFirstSplit = (split) => {
  if (split.type == "tabs") {
    return split
  } else if (split.type == "split") {
    return getFirstSplit(split.children[0])
  }
}

let firstSplit = getFirstSplit(app.workspace.rootSplit)
let current = firstSplit.children[firstSplit.currentTab].view.file

if (current != null) {
  let page = dv.page(current.path)
  
  if (page != undefined) {

    let value1 = page.value1
    let value2 = page.value2

    if (value1 != undefined) {
      dv.span("***")
      dv.header(5, "Value 1")
      dv.span(value1)
    }

    if (value2 != undefined) {
      dv.span("***")
      dv.header(5, "Value 2")
      dv.span(value2)
    }
  }
}

```

I should admit that PD Workman’s solution have it’s benefits, because it allows to edit frontmatter directly in that sideleaf, and in dataview you can see values, but not edit them. It would be really cool to have sidabar with editable metadata linked to the file. I think the Make.md plugin does something like this, but unfortunately it is extremely buggy, so I don’t use it.

1 Like

Thank you for the code. I’m trying to do that for a long time!

Unfortunately, the sidebar doesn’t update when I open a new note. It seems stuck to the first note open, right after a modification of the dataviewjs note… (Don’t know if I’m clear here).

1 Like

A million thanks for that. :man_bowing:

Will try it in a test vault to see if I can make it work … I made a complete mess of your original code. :person_facepalming:

I will probably create a layout with multiple panes open so I can incorporate your genius solution alongside my current (similar to PDW’s) working practices. Super grateful.

It’s a dataview issue, it doesn’t update in real time. You can make the refresh interval smaller in dataview settings or use the command “force refresh all views and blocks”.

2 Likes

@Alcandre @reaty For now, a simple workaround is you can make a button at the top of the note to run the command “Force refresh all views and blocks” using the either the Buttons or the Advanced URI plugin!

@reaty Thanks for this, I like where this idea is heading!

How would it be possible to tweak the tags code so that it pulls from the standard yaml tags syntax (with #prefix)?

Supporting the dataview variation would be ace as well: tags:: #tag1, #tag2

Would turn in to a great plugin…which really should eventually be a core feature I would think.

I think with the new properties feature half of this code will be not needed anymore (except for the progress counting part). I’m not insider, so I don’t have it yet, but as far as I understand, you will be able to see the tags and all of your metadata in sidebar with the core properties plugin.