I log every minute I spend at a computer. Ever since I became a full-time freelancer, I’ve been tracking my time, assigning each moment of my day to a project and a sub-task. It all started from a desire to make the best possible return on my time. As a freelancer who charged hourly, each and every minute had a monetary value.
I’ve since relaxed my approach, but some positive aspects of time-watching have stuck with me. The over-used Drucker quote goes something like “What’s measured improves”, and from my few years measuring my own productivity, I’d say he’s right. Time tracking is a valuable tool for measuring yourself.
So with the new year in full swing, I’ve decided the time has come to finish a side-project that’s been bugging me: visualizing my time.
Visualizing my time
I use Toggl for tracking my time, but there are many great time tracking apps. Toggl has plenty of fancy built-in reports; but typically, as a developer, I wanted more than the built-in functionality. Luckily, they happen to have an API.
My nature has always been to build systems, to hack together ideas from different sources, and to create new approaches. The mounting pile of data which I’ve produced by tracking the past few years of my life at a computer has been constantly sparking ideas, like “What if I could correlate mood and hours?”, and such questions.
As a freelancer, and an entrepreneur, I’ve become somewhat autonomous. This environment, where I set the hours, the ethic, and the focus is ideally suited to my personality. I’ve tested all sorts of routines and rituals.
In finding a workflow which produces optimal results from my time, I also inadvertently discovered some factors which affect my overall happiness. The more fractured my day, the more energy the work consumes. Bursts of long hours are fine, but if I persist for too many days in a row I’ll crash. I work best in shorter, more intense blocks.
It’s these sorts of observations which have inspired the following exploration.
Time tracking visualization code demo
In this article I will share the steps I took to build several visualizations from my time tracking data. Provided you know a little JavaScript, you will be able to follow along and visualize your own time. This tutorial is roughly broken into three parts:
- Retrieving the data from the Toggl API
- Reformatting the data
- Drawing various graphs using the D3.js library
Before we get started, a word on technology…
Technologies used
- HTML5 (our demo app is run from a html page)
- jQuery
- D3.js
Many of us who are used to writing code are familiar with back-end languages; Node.js, PHP, Ruby on Rails. What we build here could certainly be written in any of these, and arguably more succinctly. However, for the sake of a wider understanding and a more direct access to the data, I’ve written this using just JavaScript which can be run from any browser. This removes the need for any webhosts or deploying. All of these examples can be run simply by opening the .html files.
Further, I’ve used the JavaScript library jQuery here, knowing that it’s overkill for this task. You could sacrifice clarity for load time, but for now we’ll stick with the simplest route.
Drawing Time Tracking Data with the Toggl API and D3.js
Before we get started we will need a fresh html page to draw our graphs upon. When hacking new side-projects together, I typically use a HTML5 boilerplate, including libraries such as Bootstrap. You can easily create boilerplate using websites like Initializr. I’ve included a blank boilerplate in the source files if you’d like to start from scratch; this code demo was built on the same blank boilerplate.
Our boilerplate will need to reference the JavaScript libraries we intend to use included, including jQuery, and D3.js, (this is done for you in the source files).
Assuming we have our boilerplate in place we can then move on to the fun part. Retrieving the data.
1) Getting the data from the Toggl API
Retrieving data that we’ve logged with Toggl is fairly straightforward, and happily, they let you have access to all of it. There’s not a lot of sugar coated grouping done for you, but they have a solid API. Let’s get started.
1.a) Getting Our API Key and Workspace ID from Toggl
Before we can go about writing code to retrieve time tracking data, we’ll need to find our Toggl API key, and a “workspace ID”. Both are easily available from within the Toggl web-app. Load Toggl and click on to your profile, your API key will be at the bottom under the title “API token”.
We’ll copy this API Key and use it as a value in a new variable, appDetails, (which will hold all of our global details). We declare this at the top of our JavaScript file, (/js/main.js).
// Global Settings
var appDetails = {
togglAPIKey: '3df3187f2d0f4d5e79b8853f6c4e86e6'
};
At the time of publishing this article Toggl does not expose your workspace ID through its GUI. The quickest way I’ve found to find it is to click “Team” from the top menu in the Toggl web-app, your workspace ID will then be the integer at the end of the page URL in your browser address bar. e.g. 29333 from “https://www.toggl.com/app/team/29333”. Note: You do not strictly need your workspace ID for all data requests; we’re just grabbing it here to save time later.
We’ll also add this to the appDetails variable at the top of our JavaScript file.
// Global Settings
var appDetails = {
togglAPIKey: '3df3187f2d0f4d5e79b8853f6c4e86e6',
togglWorkspaceID: '281393'
};
1.b) Write a javascript GET request using jQuery, and use Authentication
Next we’ll need to write some code to connect to the Toggl API, and bring us back the data. jQuery makes this exceptionally simple, with the only challenge being to correctly authenticate with Toggl’s API.
First, we write a basic function which will make all of our Toggl API calls. This saves us rewriting the same code multiple times.
/**
* Retrieves data from the Toggl API
* @param {String} dataUrl (URL to retrieve)
* @return {Object} data (Returned Data)
*/
function retrieveTogglData(dataUrl){
// jQuery AJAX GET request
jQuery.ajax({
url: dataUrl,
success: function(data){
// Successful GET request
// Return the data
return data;
}
});
}
The above will query Toggl via an AJAX GET request, but it will return with a http error code of 401 — access denied. We need to add authorization code in order to connect to Toggl with our API Key. While we’re at it, we’ll also add error catching with a simple alert on error.
/**
* Retrieves data from the Toggl API
* @param {String} dataUrl (URL to retrieve)
* @return {Object} data (Returned Data)
*/
function retrieveTogglData(dataUrl){
// Create Base64 Encoded "Basic" Authorization string using our API Key
var authStr = 'Basic ' + window.btoa(window.appDetails.togglAPIKey + ':api_token');
// Create Authorization header
var requestHeaders = { 'Authorization': authStr };
// jQuery AJAX GET request, including Authorization headers
jQuery.ajax({
url: dataUrl,
headers: requestHeaders,
success: function(data){
// Successful GET request
// Return the data
return data;
}
}).fail(function(errorData){
// Failure to retrieve data
// Show error alert
alert('Error retrieving data from the Toggl API!' + "\r\n" + 'Error Code:' + errorData.statusText);
// Return false
return false;
});
}
Provided you use a correctly formatted API URL, and your token and workspace ID are in order, this code should now return data directly from the Toggl API, (if not it will pop up with an error code). The most common issue here is with credentials, if you are routinely getting errors double-check your API key and workspace ID.
1.c) Different Data URLS – testing and finalizing our function
To test our new data retrieval function we will need to choose some data to return. In this code demo we will be using data from the Reports API, rather than the Toggl API, (which is for modifying timers).
To get started we will retrieve the default weekly report to test our code. The following retrieves the data into the variable weeklyData as soon as the web page has loaded:
jQuery(document).ready(function(){
// Retrieve 'weekly' default report
var weeklyData = retrieveTogglData('https://toggl.com/reports/api/v2/weekly');
});
But alas, this also produces errors. We’ve missed two required parameters, which will make the Toggl API return error code 400, which isn’t all to helpful. It’s an easy fix though; all we need to do is add two parameters to the request: user_agent and workspace_id.
We can do this by adding a user_agent string our original appDetails var and tweaking our retrieval function:
// Tweak our main app details variable, adding a "user_agent" string
// A "user_agent" is a Toggl API requirement
var appDetails = {
togglAPIKey: '3df3187f2d0f4d5e79b8853f6c4e86e6',
togglWorkspaceID: '281393',
user_agent: 'Your-Project-Name_your@email.com'
};/**
* Retrieves data from the Toggl API
* @param {String} dataUrl (URL to retrieve)
* @return {Object} data (Returned Data)
*/
function retrieveTogglData(dataUrl){
// Create Base64 Encoded "Basic" Authorization string
var authStr = 'Basic ' + window.btoa(window.appDetails.togglAPIKey + ':api_token');
// Create Authorization header
var requestHeaders = { 'Authorization': authStr };
// Create parameter object, passing our required parameters
var dataObj = {
user_agent: window.appDetails.user_agent,
workspace_id: window.appDetails.togglWorkspaceID
};
// jQuery AJAX GET request, including Authorization headers
jQuery.ajax({
url: dataUrl,
data: dataObj,
headers: requestHeaders,
success: function(data){
// Successful GET request
// Return the data
return data;
}
}).fail(function(errorData){// Failure to retrieve data// Show error alert
alert('Error retrieving data from the Toggl API!' + "\r\n" + 'Error Code:' + errorData.statusText);// Return false
return false;});}
Running the above function will successfully retrieve the default weekly report data. Congratulations, you are now able to retrieve data directly from the API! All that’s left to do for the retrieval end of this code demo is to tidy up the function. Right now the function is simply returning any data it finds, but because it does so asynchronously, this isn’t much use to us. We need to draw our visualizations only after the data has been returned.
So we’ll add a parameter to the function: callback. Then when we call retrieveTogglData, we will pass a function to be executed once the data comes back from the API. This will let us reliably wait for large data-sets or slow data transit.
// Our final data retrieval function with passed callback function/**
* Retrieves data from the Toggl API
* @param {String} dataUrl (URL to retrieve)
* @return {Object} data (Returned Data)
*/
function retrieveTogglData(dataUrl,callback){// Create Base64 Encoded "Basic" Authorization string
var authStr = 'Basic ' + window.btoa(window.appDetails.togglAPIKey + ':api_token');// Create Authorization header
var requestHeaders = { 'Authorization': authStr };// Create parameter object, passing our required parameters
var dataObj = {
user_agent: window.appDetails.user_agent,
workspace_id: window.appDetails.togglWorkspaceID
};// jQuery AJAX GET request, including Authorization headers
jQuery.ajax({
url: dataUrl,
data: dataObj,
headers: requestHeaders,
success: function(data){// Successful GET request// Fire the callback function, if it was a function passed
// We also pass the data received from Toggl to the function
if (typeof callback == 'function') callback(data);}
}).fail(function(errorData){// Failure to retrieve data// Show error alert
alert('Error retrieving data from the Toggl API!' + "\r\n" + 'Error Code:' + errorData.statusText);});}
This function will now reliably return us our data, and act on it once it is returned, (via callback).
You can now test this function out with the other common report URLS:
Weekly report URL:
https://toggl.com/reports/api/v2/weeklyDetailed report URL:
https://toggl.com/reports/api/v2/detailsSummary report URL:
https://toggl.com/reports/api/v2/summary
Here’s some basic example test code:
jQuery(document).ready(function(){// Initialise the retrieval of data:
// Specifying the URL for the Weekly Data Report
// ... and a callback function which outputs the data returned into the console
retrieveTogglData('https://toggl.com/reports/api/v2/weekly',function(data){// output the data returned into the debug console
console.log('Data Returned:',data);// Alert total ms recorded this week
alert('Total milliseconds recorded this week: ' + data.total_grand);});});
2) Reformatting the data
We’ve now got a useful function for retrieving data from the Toggl API in JavaScript. Using that function we can now enter in several different URLs and retrieve all the data we need to make up some interesting graphs. Toggl’s API is useful, in that it gives us almost un-throttled access to our time data, but by default it comes in a format which will need manipulation if we are to draw graphs from it.
2.a) How the data is provided, and how we want it
Here’s an example of some data retrieved from the API. This was retrieved by using the detailed report URL above, with the two parameters since and until, which allow you to specify a specific period to retrieve data for. In this case it’s only returned two data entries:
// Example call
// .. using the 'details' end-point and two parameters: Since and Until
retrieveTogglData('https://toggl.com/reports/api/v2/details?since=2014-06-14&until=2014-06-21',function(data){
// Your Data processing / Drawing code
});// Example data returned from 'detailed' Toggl API report
{
"total_grand": 3866000,
"total_billable": null,
"total_currencies": [
{
"currency": null,
"amount": null
}
],
"total_count": 16,
"per_page": 50,
"data": [
{
"id": 142009654,
"pid": 1234586,
"tid": 3230138,
"uid": 12345,
"description": "Book Research",
"start": "2014-06-20T12:39:48+01:00",
"end": "2014-06-20T12:50:51+01:00",
"updated": "2014-06-20T12:50:51+01:00",
"dur": 663000,
"user": "Woody",
"use_stop": true,
"client": "Woody",
"project": "Sci-Fi Novel",
"project_color": "23",
"task": null,
"billable": null,
"is_billable": false,
"cur": null,
"tags": []
},
{
"id": 142009655,
"pid": 1234586,
"tid": 3230111,
"uid": 12345,
"description": "Plot Planning",
"start": "2014-06-21T11:46:21+01:00",
"end": "2014-06-21T12:39:44+01:00",
"updated": "2014-06-21T12:39:44+01:00",
"dur": 3203000,
"user": "Woody",
"use_stop": true,
"client": "Woody",
"project": "Sci-Fi Novel",
"project_color": "1",
"task": null,
"billable": null,
"is_billable": false,
"cur": null,
"tags": []
}
]
}
As you can see there is a lot of information. For the sake of this code demo we’ll ignore most of the data passed to us; but you can see that there is lots of potential for data-analysis.
From this data, all we want is the actual time entries themselves. These are passed in the property data, as an array of objects, where each object is a timer entry. Again, against each timer entry there are handfuls of information, such as who logged it (useful for teams), the project, any tags or billing details associated with the entry. All we’ll use in this code example is the count of the objects themselves, and their value for start, their start date. In the other examples below, (and in the source), we also use their duration (dur), project IDs (pid), task IDs (tid), and descriptions (description).
Each visualization you create will need data in a different format. Entries will need to be grouped, summed, sorted, or ordered to fit the overall goal of the graph. Here we’ll focus on this question:
How fractured are my days?
In other words, how many time entries am I recording per day? Am I jumping from task to task, doing many small jobs or focusing in bigger slots of time?
To solve this question I propose that we make a visualization that shows how many entries we record per day, at a glance. We’ll keep it simple, making a bar graph for the last few days of time-tracking, where each day has a bar representing the number of entries. D3.js can do incredibly complex visualizations, but it also does simple graphing very well.
2.b) Reformatting our data
To draw a bar chart showing entries per day we will first need to create a new data object, one which groups our time-entries by date, and which can be passed to D3.js to draw out our graph. Because I intend to draw several visualizations using this data, I write this grouping code into a function. This is good practice because it leads us to write more modular and re-usable code.
Below is the function we’ll use to group our entries by day. In it we create a new blank object, and then cycle through all of the entries provided, re-adding them to our new blank object, grouped by date. There are many approaches that will achieve this, but here I derive a key for each day based on the start date of the timer entry, then add the entry to the return object array assigned to that key. This is a quick, (somewhat brutal), way of grouping these entries by date.
/**
* Groups timer entries from the Toggl API into a date-based array
* @param {Array} data (Data Array returned from Toggl API)
* @return {Object} data (Returned Data, grouped into a date-valued Object)
*/
function groupDataByDay(data){// Prepare return object
var returnData = {};// Loop through all data
jQuery.each(data,function(ind,ele){// Retrieve the starting data from the object
// And form it into a javascript Date Object
var startingDate = new Date(ele.start);// Using the startingDate Date Object, form a simple key
// This key will allow us to assign entries of the same date to the same attribute in the return object
var startingDateStr = startingDate.getUTCFullYear() + '_' + (startingDate.getUTCMonth() + 1) + '_' + startingDate.getUTCDate();// Identify whether the return object already has an attribute for this date
// .. and if not, then create an array attribute with the date as the key:
if (typeof returnData[startingDateStr] == 'undefined') returnData[startingDateStr] = [];// Ultimately add this entry to the return object
// using .push() to append this entry to the array of entries for this date
returnData[startingDateStr].push(ele);});return returnData;}
Adding this function to our code, we can now retrieve our data from the Toggl API, and then group it by day. Here’s what that code now looks like:
// Example call
retrieveTogglData('https://toggl.com/reports/api/v2/details?since=2014-06-14&until=2014-06-21',function(data){
// Using our groupDataByDay function, we'll sort the data into a usable format
var dataByDay = groupDataByDay(data.data);});
… and here’s what our data looks like after it’s been through our groupDataByDay function:
// Example data grouped by day using the groupDataByDay function
{
"2014_6_20": [
{
"id": 142009654,
"pid": 1234586,
"tid": 3230138,
"uid": 12345,
"description": "Book Research",
"start": "2014-06-20T12:39:48+01:00",
"end": "2014-06-20T12:50:51+01:00",
"updated": "2014-06-20T12:50:51+01:00",
"dur": 663000,
"user": "Woody",
"use_stop": true,
"client": "Woody",
"project": "Sci-Fi Novel",
"project_color": "23",
"task": null,
"billable": null,
"is_billable": false,
"cur": null,
"tags": []
}
],
"2014_6_21": [
{
"id": 142009655,
"pid": 1234586,
"tid": 3230111,
"uid": 12345,
"description": "Plot Planning",
"start": "2014-06-21T11:46:21+01:00",
"end": "2014-06-21T12:39:44+01:00",
"updated": "2014-06-21T12:39:44+01:00",
"dur": 3203000,
"user": "Woody",
"use_stop": true,
"client": "Woody",
"project": "Sci-Fi Novel",
"project_color": "1",
"task": null,
"billable": null,
"is_billable": false,
"cur": null,
"tags": []
}
]
}
This is a great start, and we’re almost done with arranging our data. While a lot of visualizations will start with a grouping like this, for our bar chart we will want a cleaner data object, and specifically, we’ll need to count up all of the elements present, as well as perhaps sum up their durations.
Finally, then, we cycle through each of the groups of entries, (grouped by date), and build our final graphData object which we will pass to D3 to draw with. The following walks through each date and each entry for each date, creating a simpler object to pass to the graphing library.
For bonus points: In the source-code this loop also calculates the average time spent on an entry for each day. Take a look at this code and implement it in your own graphs.
// Example call
retrieveTogglData('https://toggl.com/reports/api/v2/details?since=2014-06-14&until=2014-06-21',function(data){
// Using our groupDataByDay function, we'll sort the data into a usable format
var dataByDay = groupDataByDay(data.data);// Our data will now be grouped by day, allowing us to cycle through each day and summarise
// Using our grouped data we'll create a simpler/cleaner object to use for our bar graph// Declare our Array which will hold our final bar chart data
// Each entry of this array will ultimately be drawn as a bar in the chart
var graphData = [];// Cycle through each date's data and summarise
jQuery.each(dataByDay,function(ind,ele){// First we declare our object for this bar chart entry:
var dateEntry = {};// Log the number of entries into a variable
dateEntry.numberOfEntries = ele.length;// Here we retrieve the date we stored in the key and make a new Date object with it
var dateObj = new Date(ind.replace(/_/g,'-'));// Using this Date object, we create a 'pretty string' which represents that date in simple form
// e.g. Wed 10th
// Note: here we use two helper functions which can be found in the source-files (dayName and ordinalSuffix)
dateEntry.dateStr = dayName(dateObj.getUTCDay()) + ' ' + ordinalSuffix(dateObj.getUTCDate());// Lastly we add this entry to the overall graphData array
graphData.push(dateEntry);});// Ultimately we need to reverse the array, as the jQuery.each will have done so
// This keeps the data in chronological order.
graphData.reverse();// Draw the graph:
// Our visualisation-drawing code will go here...});
There we are! It’s in a simple form, but that’s our data grouped by date, a useful first step in drawing visualizations with it. This forms the basis of our graph example below, from here on out our focus is on the fun stuff: drawing using D3.
2.c) Notes on data retrieval
Here are a few quick notes on data retrieval that might help if you’re going for a more advanced visualisation based on more data.
In this code demo we use the ‘Detailed report’, which returns all timer entries between two specified dates. While the other two endpoints (Weekly and Summary) are useful for typical reporting, my preference is to arrange the data into a usable format on our side. There may be, as time passes, better options for retrieving data as we want it, directly from the API, but for now I would recommend you go directly to the Detailed report endpoint and grab all of the data you wish to analyze, (unless you specifically want a summary output).
This code demo keeps data-retrieval and data-organizing relatively simple, but in the more complex examples in the source-code you’ll see that for some graphs you will need to do a little, what I call, data-acrobatics. For example you’ll spot a function called retrieveTogglDataMultiPage, which hints at another challenge. Toggl sends data through its API in pages. This is common best practice and it stops us accidentally making requests for thousands of entries at once.
Now, onto the graph!
3) Drawing my time
I’ve always thought drawing things is about as much fun as you can have with JavaScript. The thought of running code in a browser which draws for you is an exciting prospect, and over the years I’ve used it to build GUIs, animate games and enhance user experience. Here though, we want to learn something, so for this code demo we’ll keep it lean. Using the D3 library we are going to draw a simple, (but revealing), bar chart which will show us a new way of looking at our time-tracking data.
First up, we need to make sure there’s somewhere to output our fancy new graph. This will ultimately house your graph, for which you can set dimensions in the next step:
<div id="timeTrackingVisualWrapper"></div>
Next we pick up where we left off with the JavaScript above, adding the following directly after the code which prepares the data. We start by declaring margins and dimensions. These will define the bounds of our graph and the margins around it:
// Setup margins & dimensions for graph
// Here we set the overall width to be 960px and height 500px
var graphMargin = {top: 30, right: 40, bottom: 30, left: 40},
graphWidth = 960 - graphMargin.left - graphMargin.right,
graphHeight = 500 - graphMargin.top - graphMargin.bottom;
Then, for D3 to properly understand the context and scale of the data we want to display, we declare some Scales and Axes. By declaring a scale, and then axes, we are preparing elements to give to D3 once we initiate our graph. Additionally we can use d3.domain to provide an input domain which will tell D3 our minimum and maximum values, (and so it can scale the graphs appropriately.)
In this instance we are creating a simple bar graph, so we declare a range that encompasses the height of our graph, and one for the width. We also declare an X and a Y axis. Our input domains are simply a unique date string for the horizontal axis, and a range of 0 to the maximum number of entries in our graphData.
// Setup Scales and Ranges for graph
// These use the graph height and width from above to create a context onto which a visualisation can be drawn
var x = d3.scale.ordinal().rangeRoundBands([0, graphWidth], .1),
y = d3.scale.linear().range([graphHeight, 0]);
// Setup our X and Y Axis
var xAxis = d3.svg.axis().scale(x).orient("bottom"),
yAxis = d3.svg.axis().scale(y).orient("left").ticks(10, "");
// Set our data's input domain
// For the horizontal axis (x) this is the unique date string
// For the veritcal axis (y) this is the range, 0 - max number of entries
x.domain(graphData.map(function(d) { return d.dateStr; }));
y.domain([0, d3.max(graphData, function(d) { return d.numberOfEntries; })]);
For a deeper understanding of D3 Axes, read here.
Now for the graph creation: using d3.select we find our wrapper, in this case a <div> element with the id of timeTrackingVisualWrapper, (as we added to our html above), and then append an <svg> element to it, defining its dimensions and adding the sub <g> item.
// Add the graph (svg) to our wrapper div, apply our dimensions and add our attributes & classes
var svg = d3.select("#timeTrackingVisualWrapper").append("svg")
.attr("width", graphWidth + graphMargin.left + graphMargin.right)
.attr("height", graphHeight + graphMargin.top + graphMargin.bottom)
.append("g")
.attr("transform", "translate(" + graphMargin.left + "," + graphMargin.top + ")").attr("class","chart");
Note that the dimensions are automatically calculated using the graphMargin, graphWidth & graphHeight vars we set above. Tweaking these is the best way to make your visualization fit where you need it to fit.
Now that we have the basic container SVG created we need some axes. Here’s the code to add our X and Y axes. As we’ve already declared them above, all we really need to do is append them. These could then be further-styled using the classes we assign to them here via CSS or attributes in-line in this JavaScript.
// Add our Axis Lines:// Add X axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + graphHeight + ")")
.call(xAxis);
// Add Y axis
svg.append("g").attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Number of Entries");
Loading your HTML page now, you should see your graph, but the axes are quite ugly. Here’s an example of some CSS to make the lines cleaner and to prepare for the bars and text we are about to add:
.axis path, .axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}// Set a font and colour for all text
// Note to set a colour in svg we use "stroke" instead of "color"
.chart text {
font-family:"Open Sans", Arial;
stroke: #000;
}// We can also set font styles here,
// such as these which will style titles on each bar
.chart text.barTitle {
font-size:24px;
}
.chart text.barSubTitle {
stroke:#3F2A2A;
}
Great! Now we have a clean looking pair of axes. Let’s go ahead and add our bars. We do this by ‘selecting all’ of the existing bars (<rect> elements with the class bar). Of course, there are none as of yet, but this is how you begin the select, data, enter, and append function which D3 uses to draw visualizations. What we are essentially doing is providing a potential collection of elements to draw into, but because they do not exist, D3 will create them via the enter and append methods.
Here’s how it looks:
// Create Bars
svg.selectAll("rect.bar")
.data(graphData)
.enter().append("rect")
.attr("x", function(d) { return x(d.dateStr); })
.attr("width", x.rangeBand())
.attr("y", function(d) { return y(d.numberOfEntries); })
.attr("height", function(d) { return graphHeight - y(d.numberOfEntries); })
.attr("class","bar")
.style("fill","rgb(39, 119, 242)");// Add first line of text (x Entries)
svg.selectAll("text.barTitle")
.data(graphData)
.enter().append("text")
.attr("x", function(d) { return x(d.dateStr) + (x.rangeBand()/2); })
.attr("y", function(d) { return y(d.numberOfEntries) + (graphMargin.top/2); })
.attr("dy", ".75em")
.attr("style",'text-anchor: middle')
.attr("class",'barTitle')
.text(function(d) { return d.numberOfEntries + ' Entries'; });
So using graphData, we ask D3 to create a <rect> for each element in the array (date), and then we give that new <rect> element some attributes and styles, (for now we’ve set the bars to be blue using the “style” attribute). Thereafter we add a useful title which tells us how many entries this represents.
This should now mean that we have some bars on our graph. Now we’re getting somewhere!
Next we’ll improve the signals which this graph can give us at a glance. Firstly, the color blue doesn’t tell us anything about the underlying data. Let’s add some gradients to the bars so that we can see when we are approaching different brackets on time entries.
To use gradients on D3 SVG visualizations you first have to declare them. The following code declares three gradients, “Okay” green, “Warning” orange, and “Danger” red. Gradients in SVG get assigned to a name, much like a CSS reference. Take a look at these three gradients and perhaps tweak the colors to fit your setup.
// Defining Gradients// Define gradient: "Okay" Green
var gradient = svg.append("svg:defs")
.append("svg:linearGradient")
.attr("id", "okay")
.attr("x1", "0%")
.attr("y1", "100%")
.attr("x2", "0%")
.attr("y2", "0%")
.attr("spreadMethod", "pad");// Define the gradient colors
gradient.append("svg:stop")
.attr("offset", "0%")
.attr("stop-color", "#29D251")
.attr("stop-opacity", 1);gradient.append("svg:stop")
.attr("offset", "100%")
.attr("stop-color", "#39E570")
.attr("stop-opacity", 1);// Define gradient: "Warning" Orange
var gradient = svg.append("svg:defs")
.append("svg:linearGradient")
.attr("id", "warning")
.attr("x1", "0%")
.attr("y1", "100%")
.attr("x2", "0%")
.attr("y2", "0%")
.attr("spreadMethod", "pad");// Define the gradient colors
gradient.append("svg:stop")
.attr("offset", "0%")
.attr("stop-color", "#29D251")
.attr("stop-opacity", 1);gradient.append("svg:stop")
.attr("offset", "100%")
.attr("stop-color", "#EA7512")
.attr("stop-opacity", 1);// Define gradient: "Danger" Red
var gradient = svg.append("svg:defs")
.append("svg:linearGradient")
.attr("id", "danger")
.attr("x1", "0%")
.attr("y1", "100%")
.attr("x2", "0%")
.attr("y2", "0%")
.attr("spreadMethod", "pad");// Define the gradient colors
gradient.append("svg:stop")
.attr("offset", "0%")
.attr("stop-color", "#EA7512")
.attr("stop-opacity", 1);gradient.append("svg:stop")
.attr("offset", "100%")
.attr("stop-color", "#E81A21")
.attr("stop-opacity", 1);
To wire up these gradients we simply tweak our JavaScript for creating the bars:
// Create Bars
svg.selectAll(".bar")
.data(graphData)
.enter().append("rect")
.attr("x", function(d) { return x(d.dateStr); })
.attr("width", x.rangeBand())
.attr("y", function(d) { return y(d.numberOfEntries); })
.attr("height", function(d) { return graphHeight - y(d.numberOfEntries); })
.attr('fill', function(d){
if (d.numberOfEntries <= 6)
return 'url(#okay)';
else if (d.numberOfEntries <= 9)
return 'url(#warning)';
else
return 'url(#danger)';
});
As you can see we are setting the style attribute fill conditionally. If there are below 6 entries the bar will be assigned the gradient referenced “okay”, between 6 and 9 entries, the gradient “warning”, and bars representing above 9 entries will get the gradient “danger”. You may wish to set these numbers differently, depending on what fits your working style. Personally if I split my time into around 6 chunks it seems to work best, but any more than 9 and I’m typically fried!
Great, just one final touch left! While it’s great to be able to see this kind of information at a glance, and spot that within the last week you’ve been working on too many jobs, it’d be ideal if we could see how long the jobs are taking us. This doesn’t need to be complex, just an average time value would be useful enough.
Let’s add another text label to each bar, similar to the label with entry counts, we write another select, data, enter, and append call:
// Add second line of text (average entry time)
svg.selectAll(".bar")
.data(graphData)
.enter().append("text")
.attr("x", function(d) { return x(d.dateStr) + (x.rangeBand()/2); })
.attr("y", function(d) { return y(d.numberOfEntries) + graphMargin.top + 10; })
.attr("dy", ".75em")
.attr("style",'text-anchor: middle')
.attr("class",'barSubTitle')
.text(function(d) { return 'Avg Time: ' + d.avgEntryTimeMinutes + 'm'; });
There we have it! We’ve written a visualization which can give us a new way to see our time-tracking data. Using JavaScript and jQuery we retrieved data from the Toggl API, authenticating using custom headers. We then took our data and did some data-acrobatics, sorting and ordering it for our use. Lastly we used the wonderful D3.js library to draw us a colour-coded, clear-detailed bar graph which we can load in a second and quickly analyse our past weeks timer entries.
If you have any questions at all or would like to share your thoughts, please do in the comments below.
Meaningful Experimentation
The more we start to collect these vast stores of data about our lives, the more we must experiment with them. What is a useful function of labor can also inform us about that labor. As we seek to optimize ourselves and find meaning in the time we spend on what we call “work”, these reflective analyses become invaluable.
I hope you have enjoyed this code demo. I hope that it inspires you to pay finer attention to your time, to analyze it, (perhaps with code), and to value it. Please do share what you find with us in the comments. Ultimately, the worth of these visualizations is not in the technology. The code is great fun; but for it to be meaningful we must be able to derive some analysis from what we draw. My hope is that through better understanding the way we spend our time, we can make a step towards mastering it.