Building Expense Tracker using D3 (Part-2)…

Yudhajit Adhikary
9 min readDec 18, 2020
Photo by Scott Graham on Unsplash

Hi Guys , this article is the continuity of Part-1 article on D3

I will recommend you please go through my first article before starting this one. So in this article we will be designing expense tracker using D3. The user will enter the expense title and expense cost and according to the inserted expense data a donut chart will be created.

So we will start coding now, We will go through each and every line of code to get a better understanding , and there are few concept which we will be discussing while doing code walk through. So let’s start with index.html:

So in index.js we are first declaring the title, styles, metadata and materialize css link (we will be using materialized css for styling) link in the head tag of our application. Now we want our entire application background color to be indigo. So we added indigo class in our body tag. Inside header we have added the title and subtitle of our application. The <div> with container section class is the wrapper which will be containing the input fields and donut chart. So inside the wrapper there will be a single row having two columns, one will contain the input fields with button and another will contain the canvas div where we will be injecting our donut chart. Then we have added the config part of firestore which is acting as our backend. Lastly we are adding cdn link for d3,materialized css,d3-legend and d3-tip (d3-legend is used to create legends for data visualization and d3-tip is used to create tool tip over our data visualization in our case inside our donut chart). We are also adding our javascript files index.js and graph.js. So this is our entire DOM structure now let’s see the index.js file.

So in index.js we are performing the expense data insertion functionality of our application. So we are importing reference of <form> for adding submit event listener , div with id name and cost to get the value typed into those input field and error for injecting error message into the dom whenever user enters some wrong inputs. So we are adding submit event listener to our form component and checking if there are any value inserted into name and cost field If those are filled we are creating an object item with those values. Then we are adding the item object into our expense collection in backend, once data are added we want our text field to be empty so we are setting all the value to empty string. If user don’t insert any details and press the submit button we are injecting the error message into our error div which is the else part of our code. Now let’s see the graph.js file:

So graph.js is the main file which actually handles the creation of donut chart for dynamic data visualization. So dims and cent are the constants which determines the position ,height and width of our donut chart , It’s totally dependence on our wish how we want to create our donut chart. For this application we have put those into a const so that we can verify and set dimension of our donut chart suitable for our application.

const svg = d3

.select(“.canvas”)

.append(“svg”)

.attr(“width”, dims.width + 150)

.attr(“height”, dims.height + 150);

Now we are creating our svg component , by d3.select we are selecting the div having classname ‘canvas’ and appending svg element inside it. As we have mentioned in our previous article D3 used to wrap our component and attributes are passed through that component using .attr() function .So here we are passing width and height attribute to our svg component.

const graph=svg.append(‘g’)

.attr(‘transform’,`translate(${cent.x},${cent.y})`);

we created a group called graph.

const pie=d3.pie()

.sort(null)

.value(d=>d.cost);

The d3.pie() function takes in a dataset and creates handy data for us to generate a pie chart in the SVG. It calculates the start angle and end angle for each wedge of the pie chart. These start and end angles can then be used to create actual paths for the wedges in the SVG. d3.pie is used to create the pie component , by setting sort to null we are telling the pie that no sorting of data is required, we want the sequence of the data inserted same while displaying in donut chart, and since the donut chart will be created according to the expense cost we are passing cost in value attribute of pie.

const arcPath=d3.arc()

.outerRadius(dims.radius)

.innerRadius(dims.radius /2);

The d3.arc() generates an arc. These are the paths that will create our pie’s wedges. Arcs need an inner radius and outer radius. If the inner radius is 0, the result will be a pie chart, otherwise the result will be a donut chart. We need to supply these generated arcs to our SVG path elements.

const colour=d3.scaleOrdinal(d3[‘schemeSet3’])

d3.scaleOrdinal() function is used to create ordinal scale of our donut chart , we can set our own array of colors but in our application we are using the default color set provided by D3 => ‘schemeSet3’. So we are creating the ordinal scale and storing into our colour variable.

const legendGroup=svg.append(‘g’)

.attr(‘transform’,`translate(${dims.width + 40}, 10)`)

const legend= d3.legendColor()

.shape(‘circle’)

.shapePadding(10)

.scale(colour)

Now we are configuring the legend of our donut chart. First we are creating a group for legends called legendGroup .Then we are defining our svg d3.legendColor() creates colored legends for d3.on that legend element we are sending attributes like shape as circle , shapePadding as 10px , and in scale attribute we are passing our colour variable .

const tip=d3.tip()

.attr(‘class’, ’tip card’)

.html(d=>{

// return `<p>Hello there</p>`

let content=`<div class=”name”>${d.data.name}</div>`;

content += `<div class=”cost”>${d.data.cost}</div>`;

content +=`<div class=”delete”>Click slice to delete</div>`;

return content;

});

d3.tip() is used to create tip element of our donut chart, there we are sending the attribute class, and html means we are defining what should appear on the tool tips we are using back ticks to add JavaScript code into it , so we are adding name, cost and a message so that user can know if we click on the particular slice of the donut chart(dataset), the data will get deleted.

graph.call(tip)

So svg.append(“g”) appends an SVG group element to the svg and returns a reference to itself in the form of a selection . When we use call on a selection we are calling the function named tip on the elements of the selection g. In this case we are running the tip function on the group, graph.

Whenever a data updates means deleted, modified or added we will be calling update function.

colour.domain(data.map(d=>d.name))

Inside update function we are first adding domain to our ordinal scale, colour, and passing name as the domain.

legendGroup.call(legend);

legendGroup.selectAll(‘text’).attr(‘fill’,’white’);

We are calling the function named legend on the group legendGroup. Then we are setting all the text inside legendGroup as color white.

const paths=graph.selectAll(‘path’)

.data(pie(data));

Now we are joining the enhanced pie data to the path elements.

paths.exit()

.transition().duration(750)

.attrTween(‘d’,arcTweenExit)

.remove()

So we should always define an exit point of our data visualization , so here we are setting exit point and adding a transition of 750 millisecond. When we want our transition to be happen more smoother , we use attrTween .You can check the link given below to see the difference between attr and attrTween with example.

So we are passing arcTweenExit as d attribute through attrTween().We will be discussing about arcTweenExit later.

paths.attr(‘d’,arcPath)

.transition().duration(750)

.attrTween(‘d’,arcTweenUpdate)

We also make sure our current Dom path is updating. For that we are adding arcPath as d attribute in paths, transition of 750 millisecond and arcTweenUpdate as d attribute through attrTween().

paths.enter()

.append(‘path’)

.attr(‘class’,’arc’)

// .attr(‘d’,arcPath)

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

.attr(‘stroke-width’,3)

.attr(‘fill’,d=>colour(d.data.name))

.each(function(d){this._current=d})

.transition().duration(750)

.attrTween(“d”,arcTweenEnter);

Now let’s define the enter point of our paths, there we are appending path, adding classname arc, stroke, stroke-width, and color of the path are filled with colour ordinal scale, for each slice we are setting this._current=d, (this is actually required to keep the scale updated with current properties) adding transition of 750 millisecond and lastly arcTweenEnter as d through attrTween() function.

graph.selectAll(‘path’)

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

tip.show(d,n[i])

handleMouseOver(d,i,n)

})

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

tip.hide();

handleMouseOut(d,i,n)})

.on (‘click’, handleClick)

}

We want our path should have mouseover effect , the tip should popup with some background color changes. d determines the data, i determines the index of the data and n determines the exact dom element created by the data , tip.show () is used to show the tooltip we have defined earlier and handleMouseOver function will take care of all other things. On mouseout we want the tooltip element to hide and calling handleMouseOut function. on Click we are calling the handleClick function, we will be looking into those functions later.

Now let’s define the function which is responsible for adding, updating and removing data. So our collection name is expenses , 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.

const arcTweenEnter=(d)=>{

var i=d3.interpolate(d.endAngle,d.startAngle)

return function(t){

d.startAngle=i(t)

return arcPath(d);

}

}

arcTweenEnter was passed in d attribute of our path (slice)of donut chart in the enter point , So in this function we are defining a interpolate. Interpolate returns an interpolator between the two arbitrary values passed to it .Values may be numbers, colors, strings, arrays, or even deeply-nested objects.

For example:
var i = d3.interpolateNumber(10, 20);
i(0.0); // 10
i(0.2); // 12
i(0.5); // 15
i(1.0); // 20
The returned function i is called an interpolator. Given a starting value a and an ending value b, it takes a parameter t in the domain [0, 1] and returns the corresponding interpolated value between a and b. An interpolator typically returns a value equivalent to a at t = 0 and a value equivalent to b at t = 1.

So in our case we are passing the endAngle to startAngle in interpolate function means our donut chart slice will enter from endAngle to startAngle. So in the return function we are setting d.startAngle from t=0 to t=1means from endAngle to startAngle and returning the arcPath.

const arcTweenExit=(d)=>{

var i=d3.interpolate(d.startAngle,d.endAngle)

return function(t){

d.startAngle=i(t)

return arcPath(d);

}

}

arcTweenExit was passed in d attribute of our path (slice)of donut chart in the exit point , So in this function we are defining a interpolate. So in our case we are passing the startAngle to endAngle means our donut chart slice will exit from startAngle to endAngle. So in the return function we are setting d.startAngle from t=0 to t=1 from startAngle to endAngle and returning the arcPath we are just doing the opposite to arcTweenEnter.

function arcTweenUpdate(d){

var i= d3.interpolate(this._current,d)

//update the current prop with new updated data

this._current=d;

return function(t){

return arcPath(i(t))

}

}

So arcTweenUpdate was passed in d attribute of our path (slice)of donut chart when data is updated, this._current stores all the properties of slice(path) before the data updates and d contains the updated properties of the slice(path).So in variable i we are creating the interpolate from older position to new updated position of the slice. Then we are updating the current property with the new updated data and returning arcPath.

Lastly we have the event handlers

const handleMouseOver=(d,i,n)=>{

// console.log(n[i])

d3.select(n[i])

.transition(‘changeSliceFill’).duration(300)

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

}

handleMouseOver is called when we hover over the slices of our donut chart, on mouse over we are selecting the particular slice of donut chart where we are hovering and filling it with White.

const handleMouseOut=(d,i,n)=>{

d3.select(n[i])

.transition(‘changeSliceFill’).duration(300)

.attr(‘fill’,colour(d.data.name))

}

handleMouseOut is called when we mouse out from the slices, so we are selected the slice and fill the slice with it’s actual color according to the ordinal scale.

const handleClick =(d)=>{

// console.log(d)

const id=d.data.id

db.collection(‘expenses’).doc(id).delete()

}

handleClick is triggered when we click on the slices, we are actually deleting the record from our backend when a user clicks on the particular slice. So Our application is now up and running.

It’s just a simple implementation of creating donut chart using D3.There are several and more complex visualization we can create using D3. On part -3 , We will be creating data hierarchy or data tree 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