Plotting download counts of my GitHub project using Obsidian Charts and DataviewJS

I’ve wanted to insert a dynamic plot of my plugin’s download counts in my note (x=version, y=# of downloads).

Update (older version follows)

Thanks to @AlanG, it turned out that Octokit is unecessary, and therefore I don’t have to rely on Obsidian Modules (although it’s super useful for other purposes, too). The following is the updated solution:

  1. Install Obsidian Charts and Dataview. Enable DataviewJS.

  2. Create this script.js in your vault (replace "YOUR_PERSONAL_ACCESS_TOKEN" with yours).

  3. In the note that you want to insert a plot, simply add the following dataviewjs code block. Also modify the parameters owner, repo, title and cumulative as you like.

```dataviewjs
await dv.view("path/to/script", {
    owner: "Your GitHub account",
    repo: "Your repo",
    title: '# of Downloads',
    cumulative: true,
    ctx: this.container,
});
```
  1. It’s done!

Old version

Once I get the data, I can plot it using DataviewJS and the Obsidian Charts plugin (see here for details).
However, it was not clear to me how I can import the octokit.js (required to get the count data using the GitHub API) in dataviewjs code blocks.

Recently, I finally managed to do that thanks to the awesome Obsidian Modules plugin. So let me share my solution here.

Also, let me know if there’s a better or easier way to do it!

  1. In the Obsidian Modules settings, turn on “Enable external links”.

  2. Create the following script.js in your vault (replace "YOUR_PERSONAL_ACCESS_TOKEN" with yours).

// load octokit.js using Obsidian Modules
const { Octokit } = await self.require.import("https://esm.sh/@octokit/core");

// get download count
const octokit = new Octokit({auth: "YOUR_PERSONAL_ACCESS_TOKEN"});
const releases = (await octokit.request(
    `GET /repos/${input.owner}/${input.repo}/releases`,
    {
        per_page: 100,
        page: 1,
    }
)).data;
const download_counts = releases.map((release) => { return { version: release.tag_name, download_count: release.assets.find((asset) => asset.name == 'main.js').download_count } }).sort((a, b) => a.version.localeCompare(b.version, undefined, { numeric: true }));

if (input.cumulative) {
    download_counts.forEach(
        (item, index) => {
            item.download_count += (index > 0 ? download_counts[index - 1].download_count : 0);
        }
    );
}

// prepare data for Obsidian Charts (Chart.js)
const data = {
    type: 'line',
    data: {
        labels: download_counts.map(row => row.version),
        datasets: [
            {
                data: download_counts.map(row => row.download_count),
                borderColor: 'rgb(75, 192, 192)',
                legend: false,
            }
        ]
    },
    options: {
        fill: {
            target: 'origin',
            above: 'rgba(75, 192, 192, 0.15)',
        },
        plugins: {
            title: {
                display: true,
                text: input.title,
                font: {
                    size: 18,
                }
            },
        }
    }
};
// draw line chart
window.renderChart(data, input.ctx);
  1. In the note that you want to insert a plot, simply add the following dataviewjs code block. Also modify the parameters owner, repo, title and cumulative as you like.
```dataviewjs
await dv.view("path/to/script", {
    owner: "Your GitHub account",
    repo: "Your repo",
    title: '# of Downloads',
    cumulative: true,
    ctx: this.container,
});
```
  1. It’s done!
3 Likes

I absolutely love it and I’m implementing it immediately :tada:

But there’s no reason to use Octokit, so you don’t need to bother with the extra Modules plugin.

Just use the built-in requestUrl() function:

const res = await requestUrl('https://api.github.com/repos/YOU/REPO/releases', {
  headers: {
    Accept: 'application/vnd.github+json',
    Authorization: 'Bearer YOUR_TOKEN',
    'X-GitHub-Api-Version': '2022-11-28'
  }
})

console.log(res.json)
1 Like

I was a fool…:rofl:
Every time I come to this forum, I learn something important. Thanks a lot!

Thanks to @AlanG, I was able to get rid of the dependency to Obsidian Modules (although it’s useful for other purposes, too).

Updated script.js:

// get the count data
const releases = (await requestUrl(`https://api.github.com/repos/${input.owner}/${input.repo}/releases?per_page=100&page=1`, {
    headers: {
        Accept: 'application/vnd.github+json',
        Authorization: "YOUR_PERSONAL_ACCESS_TOKEN",
        'X-GitHub-Api-Version': '2022-11-28',
    },
})).json;
const download_counts = releases.map((release) => { return { version: release.tag_name, download_count: release.assets.find((asset) => asset.name == 'main.js')?.download_count ?? 0} }).sort((a, b) => a.version.localeCompare(b.version, undefined, { numeric: true }));

if (input.cumulative) {
    download_counts.forEach(
        (item, index) => {
            item.download_count += (index > 0 ? download_counts[index - 1].download_count : 0);
        }
    );
}

// prepare data for Obsidian Charts (Chart.js)
const data = {
    type: 'line',
    data: {
        labels: download_counts.map(row => row.version),
        datasets: [
            {
                data: download_counts.map(row => row.download_count),
                borderColor: 'rgb(75, 192, 192)',
                legend: false,
            }
        ]
    },
    options: {
        fill: {
            target: 'origin',
            above: 'rgba(75, 192, 192, 0.15)',
        },
        plugins: {
            title: {
                display: true,
                text: input.title,
                font: {
                    size: 18,
                }
            },
        }
    }
};
// draw line chart
window.renderChart(data, input.ctx);

Brilliant!

If you want to fix up one small potential issue - you’ll get an error with the above code if you have any release which doesn’t include a main.js.

Change the download count part to be:

download_count: release.assets.find(asset => asset.name === 'main.js')?.download_count || 0

with the ?. and the || 0 to ensure you always end up with a number.

1 Like

Thanks, you’re right!

This is super super cool, really glad you posted this. My plugin project notes are now looking awesome :100: :heart:

1 Like

@ush I have made one small improvement. Rather than storing the access token in the note or in the Dataview script, I have set my vault up with a pseudo .env file.

You can’t use an actual .env because a dotfile isn’t supported on mobile. So what I’ve done is put an env.json file in the root of my vault with various secrets and API keys (which I can exclude from any syncing/backup):

{
  "github": {
    "accessToken": "some token"
  }
}

To read it in the charts script, you can go:

const env = JSON.parse(await app.vault.adapter.read('env.json'))
const token = env.github.accessToken

I’ve also made a view that shows your downloads by date, rather than by release version:

const releases = res.json
const downloads = releases
  .map(release => {
    return {
      version: release.tag_name,
      count: release.assets.find(asset => asset.name === 'main.js')?.download_count || 0,
      date: moment(release.published_at)
    }
  })
  .sort((a, b) => a.date - b.date)

// Shift all release dates & versions down by one so that the current version is the last two datapoints
downloads.unshift({ count: 0 })
downloads.forEach((item, index) => {
  item.version = downloads[index + 1]?.version || item.version
  item.count += downloads[index - 1]?.count || 0
  item.date = downloads[index + 1]?.date || moment()
})

// Prepare data for Obsidian Charts (Chart.js)
const data = {
  type: 'line',
  data: {
    labels: downloads.map(row => row.date.format()),
    datasets: [{
      data: downloads,
      parsing: {
        xAxisKey: 'date',
        yAxisKey: 'count'
      },
      borderColor: 'rgb(75, 192, 192)',
    }]
  },
  options: {
    fill: {
      target: 'origin',
      above: 'rgba(75, 192, 192, 0.15)',
    },
    scales: {
      x: {
        type: 'time',
        time: {
          unit: 'day'
        }
      }
    },
    plugins: {
      tooltip: {
        callbacks: {
          label (context) {
            return 'Release ' + context.raw.version
          }
        }
      },
      legend: {
        display: false
      },
      title: {
        display: true,
        text: downloads[downloads.length - 1].count + ' downloads',
        font: {
          size: 18,
        }
      }
    }
  }
};
window.renderChart(data, input.ctx)
3 Likes

Awesome! Keeping the token in a separate file makes it easy to manage it. And I love the date on the horizontal axis, it makes the plot easier to read!

1 Like

I wasn’t happy with how the chart would make the page jitter when it refreshed, so I wanted to put it into a callout which is collapsed by default.

It took me quite a while to figure out how to make this work effectively, so I’ll post it here in case anyone else finds this useful.

This is how it looks when collapsed/expanded (theme is Prism):

This is the markdown source:

> [!summary]- Download stats
> 
> <div id="github-chart"></div>
> 
> `$=dv.view("github-release-info")`

:point_up_2: you can see that there is no dataviewjs anymore, it’s just a nice inline code snippet which lives inside the callout itself.

And this is the magic to get the callout to update correctly with the chart:

At the end of the script, instead of using window.renderChart(data, input.ctx), replace that line with this:

const el = document.getElementById('github-chart')
if (el) {
  const chartEl = document.createElement('div')
  window.renderChart(options, chartEl)
  el.replaceWith(chartEl)
}

The secret sauce is to create a new Element and then replace the existing one. No other option I found would work consistently across both Live Preview and Reading mode.

1 Like

I’ve never come up with the idea of using inline HTML with ID + inline DataviewJS code. So neat!

An alternative option might be using Chart.js’s option for disabling animation, that is:

// Prepare data for Obsidian Charts (Chart.js)
const data = {
  type: 'line',
  data: {
    labels: downloads.map(row => row.date.format()),
    datasets: [{
      data: downloads,
      parsing: {
        xAxisKey: 'date',
        yAxisKey: 'count'
      },
      borderColor: 'rgb(75, 192, 192)',
    }]
  },
  options: {
    fill: {
      target: 'origin',
      above: 'rgba(75, 192, 192, 0.15)',
    },
    scales: {
      x: {
        type: 'time',
        time: {
          unit: 'day'
        }
      }
    },
    animation: false, // ADDED!!
    plugins: {
      tooltip: {
        callbacks: {
          label (context) {
            return 'Release ' + context.raw.version
          }
        }
      },
      legend: {
        display: false
      },
      title: {
        display: true,
        text: input.title ?? downloads[downloads.length - 1].count + ' downloads',
        font: {
          size: 18,
        }
      }
    }
  }
};
2 Likes