Building Fitness Tracker using D3 (Part-1)…

Yudhajit Adhikary
12 min readDec 15, 2020

In this article we will discuss about an amazing JS library for designing dynamic data visualization that is D3. We will be building fitness tracker application using D3.So we will be going through all the basic concept of D3 and the cool features provided by D3 for designing dynamic svg like pie charts , bar charts , graphs etc. Let’s start…

  1. What is D3 ?

D3 is a amazing JS library to create pretty any of data visualization, we can imagine in svg format.D3 create graphs in svg format. Svg stands Scalable Vector Graphics which basically means we can scale those up or down without any distortion of quality.

2. What is SVG ?

SVG stands for Scalable Vector Graphics. Unlike PNG and jpg formats we can make svg bigger or smaller without losing quality and svg are also very smaller in size, so it loads quickly. SVG are useful to create diagram, logo etc. There are several ways to create SVG:

  1. Using Photo Shop
  2. SVG container using width and height
  3. Using D3. D3 make it much easier to create different complex diagram, Using D3 we can create dynamic and interactive svg using the power of JavaScript.

3. Basic concept of D3 and SVG ?

Now we will discuss few concept of how to create Svg .For a basic 2D svg there are 2 axis X axis and Y axis, X axis runs from left to right , where as Y axis run from top to bottom. For creating Svg like Circle we will be requiring two parameters center x and center y, center x positions the center of the circle along x axis and center y positions the center of the circle along the y axis. We can also create lines ,lines runs from one point of svg canvas to another point . so we need to define a starting position and ending position. starting point is made of 2 quadrants X1and Y1 and ending point is made of X2 and Y2.The whole idea of D3 is that we don’t have to manually create all of these quadrants.

What if we want to create a shape that is bit more complex, in that case we will be using path to do that.

<path d= “M150 0 L75 200 L225 200Z”/>[This will create a Triangle]

let drive deep and analyze the value which is passed on d parameter of path , M150 0 ->initiate drawing from the position X=150 and Y =0. L75 200-> draw a line to a svg point , 75 to X-axis and 200 to Y-axis. L225 200 ->now draw a line from the last point to another point , 225 to X-axis and 200 to Y-axis. Z->close the path means a line will be drawn from the last point to the starting point. We can also add H instead to L to draw horizontal line , V to draw vertical line, C to draw curve, and S to draw a smooth curve.

Now let’s see how to create curves :

<path d= “C225 200 150 150 150 50”/>

When we define curves, we are actually setting 3 sets of x and y quadrants. So (225,200) is the actual ending point of the curve , (150,150) is the reflection of the Y axis and (150,50) is the origin of the graph. Instead of doing that we will be calling D3 to create svg for us .D3 actually wraps the div, and returns object to us, we used to define properties stored in D3 wrapper, which gives us access to different methods and properties which we will ultimately use.D3 actually joins the data with the element or the div.

For defining charts , we have to define several parameters:

  1. Linear Scale
  2. Band Scale
  3. Ordinal Scale
  4. Time Scale

When we are define a graph , in X-axis we used to have the parameter and in the Y-axis we have the actual value . So to define a proper graph we have to define or specify two parameter domain and range ,The domain is the complete set of values, let’s say the value of our graph varies from [0,3000]. The range is the set of resulting values of a function, let’s say the resulting values of scaling our scale from 0 to 500 in our graph. So if there is a graph of let’s say student vs year ,And the number of students varies from 0 to 3000(domain), but the scale in which we have to represent the student count on y-axis is from 0 to 500(range).So student count 0 will be denoted as 0 scale in y axis, student count 1500 will be denoted as 250 scale in y-axis and student count 3000 will be denoted as 500 scale in y-axis. So this scaling is called Linear Scale , So Linear scale actually determines the domain and range of the Y-axis scale in our graph.

By Band scaling we actually determine the bandwidth between the each bar in the x-axis(in case of bar charts).Band Scales splits the data , in the band of equal width, depending on how many different elements we have in our domain, and how much horizontal room is available. So If we pass a array of year[or any parameters which will be displayed in X-axis] , by that band scale we can know how many years will be there in the x -axis, it will associate each different bands with x-coordinate and will provide us with a bandwidth for each bar on our chart.

Ordinal Scale is actually used in Pie chart or donut chart , it actually determine the color representation of each parameter of the graph. Let assume there is a expense pie chart having expense ratio of several category like rent, bills , food etc. We want to represent rent , bills and food with red, blue and pink color in our graph respectively. So the domain of the ordinal scale will be [“rent”, “bills”, “food”] and range will be [“red”, “blue”, “pink”].

Time Scale is actually used for any time graph, where the bandwidth of each bar in x-axis are determined according to range we provide and the number of data needs to be display in the graph.

There are few more thinks like grouping, min , max, extent. Those concepts we will discuss once we start building our fitness application.

4. Fitness Tracker using D3

Now let’s start building our fitness application, we will be using Firestore for storing data. We will not focus much on the firestore part. So our application will be having line graph which will track day to day fitness report like walking, swimming, running, cycling according to the distance covered data added by our user for each day. So let’s start coding

So we will start with index.html:

So Index.html will be the main html file having the complete DOM structure of our application, So first we have added meta data, title ,link of materialized css for style and our local styling file style.css inside head tag. Then inside body tag we have added header having text Fitness Tracker, after that we added a sub title . Then comes the main container having class name “container section”. It’s having two div having class row, the first one is having the buttons and the canvas where the actual graph will form. Those buttons are actually for toggling between the fitness options like walking, running, swimming and cycling . We will be adding event Listener to those buttons in index.js, we will also be adding graph in canvas div in graph.js. Now another row is having a dynamic text “How much cycling have you done today ?”and form having an input field for the user to insert distance covered on that day and finally there is a p tag to show error if there are any .So we are done with the basic dom structure of our application .Now we have added the configuration for firestore connection. These code we can get from our firestore embedded code for connection, some little changes we have added , like added <script src=”https://www.gstatic.com/firebasejs/8.1.1/firebase-firestore.js"></script>

as we are using the firestore for data storage and

const db=firebase.firestore();

created a firestore instance for data storage. Lastly we have added our script file for the functionality, which includes D3 js extension , and our two local js files index.js and graph.js. Now let’s see the index.js file:

,

Now in index.js we have normal javascript to define the toggling and submitting functionality of our application. We have used document.querySelector and querySelectorAll to get the reference of the buttons, form ,input field ,div having class error and span inside our form element. We have declared a variable activity with value cycling. Now we are iterating through the buttons array and for each button we are adding click eventListener, there we are fetching the dataset.activity ,whatever we passed on data-activity parameter in the button we can access those using e.target.dataset.activity. We are storing that value in our activity variable. Then for each button we are removing the active class means we are removing the select style from the previously selected button and adding the style to the clicked button.

btns.forEach(btn=>btn.classList.remove(‘active’))

e.target.classList.add(‘active’);

then we are setting the id of our input field with the activity variable so that the user can insert distance for the particular category selected

input.setAttribute(‘id’,activity);

then we are changing the text of the span with the particular selected category.

formAct.textContent=activity;

then we are calling the update data function for backend update.

Now we will be adding submit functionality to our form , So in the form reference we are adding submit eventListener, there we are parsing the input value , means whatever value inserted inside in the input field and storing in the distance variable then we are adding the input field value , activity category and current date to the data ,it’s a asynchronous call so we are adding then and setting the error value and input value to empty string but if it fails means user have pressed enter without inserting any value then we are setting the error textContent with our error message, which will be displayed in p tag having class name error . Now let’s see the graph.js file:

So graph.js is the main file responsible for creating dynamic line graph according to the backend data coming from firestore. So we declared a variable margin which contains all the margin parameter which we want to be applied to our graph . We have used those value in the entire code. So now we are defining the graphWidth and graphHeight, these variable is actually required when we will be define the domain of the y-axis and x-axis of our line graph. Now we are selecting the div with class= ‘canvas’ and appending with svg. We discussed earlier that d3 used to wrap the div and used the send several properties to it, so we wrapped the canvas div :

const svg=d3.select(‘.canvas’)

and we will be sending attribute to the canvas div by .attr() function, which takes two parameter attribute name and value.

.attr(‘width’,graphWidth+margin.left+margin.right)

by this we are defining the width and height of the svg. Now we will discuss about another concept called group , by grouping we are defining certain properties which will be applied to all the elements which are under a same group. So we are defining a group called graph.

const graph=svg.append(‘g’)

then we are appending width, height and transform from margin.left to margin.top. After that we are defining the Time Scale range from 0 to graphWidth and Linear Scale range from graphHeight to 0.So there is a important thing which we want to discuss, So Time Scale is the scale which are applied to time graphs where the bandwidth of the time axis(X-axis ) is determined by the range of the x-axis. For Time Scale the domain of X-axis get automatically set from earlier Dates to latest date and range are defined by us . So In our case we have set the range from 0 to graphWidth.

const x=d3.scaleTime().range([0,graphWidth]);

for linear scale , we are defining range from graphHeight to 0 , now actually y axis starts from top to bottom, So if we give the range value from 0 to graphHeight we will see the line graph will get inverted, so to make it proper we have to determine the range from graphHeight to 0. Then we create another group called xAxisGroup and added class x-axis and transform translate from 0 to graphHeight. yAxisGroup is another group having class y-axis. Now we are defining the d3 line path , we are passing x, a function which returns date value of the data passed and for y, a function which returns distance value of the data passed.

const line=d3.line()

.x(function(d){return x(new Date(d.date))})

.y(function(d){return y(d.distance)});

Now in graph group we are appending path element.

const path=graph.append(‘path’);

We are creating dotted line group and appending to graph.

const dottedLines=graph.append(‘g’)

.attr(‘class’,’lines’)

.style(‘opacity’,0);

Now we are creating x dotted and y dotted and appending those in dottedLines group , Those line will be visible when we hover on the point quadrant of the graph.

//create x dotted lune and append to dotted line group

const xDottedLine=dottedLines.append(‘line’)

.attr(‘stroke’,’#aaa’)

.attr(‘stroke-width’,1)

.attr(‘stroke-dasharray’,4);

//create y dotted line and append to dotted line group

const yDottedLine=dottedLines.append(‘line’)

.attr(‘stroke’,’#aaa’)

.attr(‘stroke-width’,1)

.attr(‘stroke-dasharray’,4)

The update function will be called whenever the data got modified on the database. So inside update function we are first filtering only the selected category data from our database,

data = data.filter(item=>item.activity==activity);

then we are sorting the data according to the date means the early dates should come first and recent one should come at last.

data.sort((a,b)=>new Date(a.date)-new Date(b.date));

Now we are defining domain of x -axis as the data.date and y-axis domain from 0 to maximum distance covered.

x.domain(d3.extent(data,d=> new Date(d.date)))

y.domain([0,d3.max(data,d=>d.distance)])

Now we are passing the data to our path element and sending attributes like few styles and d as line which will return data.date for x parameter and data.distance for y parameter.

path.data([data])

.attr(‘fill’,’none’)

.attr(‘stroke’,’#00bfa5')

.attr(‘stroke-width’,2)

.attr(‘d’,line);

Now we will create circles which points the actual points on the graph and passing data from backend to them .

const circles=graph.selectAll(‘circle’)

.data(data)

We should always define exit() function which are used to remove unwanted points which are not having any data .

circles.exit().remove()

Now we are passing circles attributes like radius of 4, center X as data.date and center Y as data.distance.

circles.attr(‘r’,4)

.attr(‘cx’,d=>x(new Date(d.date)))

.attr(‘cy’,d=>y(d.distance));

So as we have defined exit() point of circles, we should also define enter() points of circles by this enter() points we are creating circles element according to the data entering from database.

circles.enter()

.append(‘circle’)

.attr(‘r’,4)

.attr(‘cx’,d=>x(new Date(d.date)))

.attr(‘cy’,d=>y(d.distance))

.attr(‘fill’,’#ccc’);

For the circle created we want to add some hovering effect so we are defining a mouseover event , we are having three variable d, i and n , d determines the data, i determines the index of the data and n determines the exact dom element created by the data , so we are selecting that particular dom of that particular data and added few parameter like transition of duration 1 second, radius 8,fill #fff. Now we want that xDottedLine and yDottedLine to be appeared on mouseover to indicate the exact point of contact in x and y axis, for that we are defining x1 , x2 , y1 and y2 parameters for them, and setting opacity to 1.

graph.selectAll(‘circle’)

.on(‘mouseover’,(d,i,n)=>{

d3.select(n[i])

.transition().duration(100)

.attr(‘r’,8)

.attr(‘fill’,’#fff’);

//set x dotted line coords (x1,x2,y1,y2)

xDottedLine.attr(‘x1’,x(new Date(d.date)))

.attr(‘x2’,x(new Date(d.date)))

.attr(‘y1’,graphHeight)

.attr(‘y2’,y(d.distance));

//set y dotted line coords(x1,x2,y1,y2)

yDottedLine.attr(‘x1’,0)

.attr(‘x2’,x(new Date(d.date)))

.attr(‘y1’,y(d.distance))

.attr(‘y2’,y(d.distance));

//show the dotted line group(.style,opacity)

dottedLines.style(‘opacity’,1);

})

On mouseleave we want to disappear the line so we are setting opacity to 0.

.on(‘mouseleave’,(d,i,n)=>{

d3.select(n[i])

.transition().duration(100)

.attr(‘r’,4)

.attr(‘fill’,’#ccc’);

//hide the dotted line group(.style,opacity)

dottedLines.style(‘opacity’,0);

})

Now let create axes , so we are creating the horizontal line as x-axis and left vertical line as y-axis.

const xAxis=d3.axisBottom(x)

.ticks(4)

.tickFormat(d3.timeFormat(‘%b %d’));

const yAxis=d3.axisLeft(y)

Now we are calling the axes and finally rotating the text content of xAxisGroup by -40 degree .

xAxisGroup.call(xAxis);

yAxisGroup.call(yAxis);

//rotate axis text

xAxisGroup.selectAll(‘text’)

.attr(‘transform’, ’rotate(-40)’)

.attr(‘text-anchor’,’end’)

Now let’s define the function which is responsible for adding, updating and removing data. So our collection name is activities , whenever a document inside a collection gets changed .onSnapshot() function gets triggered , which return a response . Response. docChanges() contains the array of changed document , now we are immutably storing the each changed document by de-structuring in doc variable. change.type contains whether the change is added , modified or removed type and according to that by applying switch case we are handling the update functionality of our database. In case of ‘added’ we are pushing the new doc, ‘modified’ we are we are finding the index of existing document which matches with our changed document id and updating the data of that particular index and for ‘removed’ we are filter out the document which matches with the removed document id. and Lastly we are calling the update function for updating our database.

So we are almost done with our application , the only thing which is remaining is the styling part , so let’s see the style.css file:

So the css file is self explanatory .So we are done with our application . it’s quite a long article, but congratulation our application is now up and running.

It’s just a simple implementation of creating line graph using D3.There are several and more complex visualization we can create using D3. On part -2 , We will be creating donut chart using D3.Till then we can actually go through the official documentation of D3 . It’s really nice and helpful to those who want to explore more about D3.I will be attaching my github code link as the reference of this article and the official documentation link of D3. Please keep in mind if you want to run the code please replace the config part of the firestore with your own credentials if you are not much comfortable with firestore please check official documentation of Firestore (https://firebase.google.com/docs/web/setup)

CODE:

DOCUMENTATION:

HAPPY CODING..:)

--

--

Yudhajit Adhikary

Web developer by profession,Photographer,Blog Writer,Singer,Pianist,Seeker of Solution