October 17th, 2021
by Georg Ledermann

Coding time visualization

How to build a live chart using WakaTime

  • # JavaScript
  • # Tailwind CSS
  • # nginx
Photo by Jeremy Bishop on Unsplash

Photo by Jeremy Bishop on Unsplash

Coding time visualization

WakaTime is a time tracking app to build metrics about coding. From inside the code editor it sends metadata (coding time, language, project, operating system) to a WakaTime account. This way you can see how much time you spend programming.

I would like to demonstrate how to create a simple, but good-looking live (!) chart showing the languages I have been using recently. Let me show the result first:

Languages I have used in the last 12 months

As you can see, at the time of writing I mainly use Ruby, TypeScript and Vue.js. I’ve added this chart to my about page.

How does this work?

A WakaTime plugin is installed in my code editor (Visual Studio Code) and tracks the coding time for me. In a private dashboard, I can see the time I spent coding in my projects, the languages I have used and some other metrics.

To share these statistics, WakaTime already offers embedding a chart in your web page. But for me, this is not perfect:

  • I don’t like the visual design of the diagrams. There are only standard column or pie charts that show far too much detail.
  • I prefer not to send the IP address of my site’s visitors to wakatime.com

Fortunately, wakatime.com also provides a JSON endpoint that allows building my own representation.

Ok, let’s go and create a custom chart:

1. Protect visitor’s IP address by setting up a proxy

Sending requests to wakatime.com directly from within the browser would be a privacy issue because the IP address of the visitor will be sent to wakatime.com. To fix this, I want to use a proxy installed on my own host.

Thanks to nginx, which I use to serve my website, setting up a proxy is easy. I add a new endpoint named ledermann.dev/languages.json, which returns the original JSON response. The result is being cached, so once a day the data is updated.

In my nginx.conf, the most important part for doing this is the proxy_pass which contains the URL I got from WakaTime. Replace [username] and [uuid] with your credentials.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# ...
proxy_cache_path /var/cache/nginx/wakatime
                 keys_zone=wakatime:1m
                 use_temp_path=off;

server {
  # ...
  location /languages.json {
    proxy_pass https://wakatime.com/share/@[username]/[uuid].json;
    proxy_set_header Host $proxy_host;

    proxy_ignore_headers Cache-Control Set-Cookie;

    proxy_buffering on;
    proxy_cache wakatime;
    proxy_cache_valid 200 1d;
    proxy_cache_use_stale error timeout invalid_header updating
                          http_500 http_502 http_503 http_504;

    add_header Cache-Control "public, max-age=86400";
  }
}

2. Visual appearance

I want to show a simple stacked bar chart build with CSS only, based on the idea of Marco Slooten. Ok, because I’m using Tailwind CSS, there will be no CSS at all.

In the HTML code, I put a <div> as an anchor point, which will later be filled with content by JavaScript:

1
2
<div id="wakatime" class="rounded-full grid overflow-hidden">
</div>

3. Fetch and process JSON data

This is the main part. On the client side, the JSON data is fetched and for every language it contains, a child <div> is created inside the anchor point.

The original JSON response from WakaTime already includes an Other item, but there are still items with very low usage. For me, I get more than 30 languages, which is far too much to display. I want to reduce the total number of languages, so I remove items below 1% of usage.

Here is the code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
document.addEventListener("DOMContentLoaded", () => {
  const element = document.getElementById("wakatime");

  if (element)
    fetch("https://ledermann.dev/languages.json")
      .then((response) => response.json())
      .then(({ data }) => drawChart(element, fixOther(data)));
});

/* Draw chart for the given data */
function drawChart(element, data) {
  data.forEach((item) => {
    const childElement = document.createElement("div");
    childElement.className = "flex justify-center py-1";
    childElement.style.backgroundColor = item.color;
    childElement.innerText = item.name;

    element.appendChild(childElement);
  });

  // Let the CSS grid do the magic, see the blog post
  // of Marco Slooten for details:
  // https://marcoslooten.com/blog/using-css-grid-to-create-a-bar-chart/
  element.style.gridTemplateColumns = data
    .map((item) => `${item.percent}fr`)
    .join(" ");
}

/* All items below 1% should be grouped into "Other" */
function fixOther(wakatimeData) {
  // There is already an item called "Other"
  const other = wakatimeData
    .find((item) => item.name === "Other");

  // Sum up the other items with low usage (< 1 %)
  const otherSum =
    other.percent +
    wakatimeData
      .filter((item) => item.percent < 1 && item.name !== other.name)
      .reduce((total, item) => total + item.percent, 0);

  // Build new array without others
  const filteredData = wakatimeData.filter(
    (item) => item.percent >= 1 && item.name !== other.name,
  );

  // Add new "Other" item
  filteredData.push({
    percent: otherSum,
    name: other.name,
    color: other.color,
  });

  return filteredData;
}

That’s it. In my actual implementation, I added tooltips and language logos. But to keep this blog post short, I left that out.