Data Visualization with D3.js

I was tasked with redesigning the household record view for MortarStone. MortarStone is a SaaS platform that allows nonprofit organizations and churches to assess and manage their donations, and the household record view is where users inspect individual donors. A big part of this view was, not surprisingly, showing how much each household gave, when they gave and what fund the giving belongs to.

The previous version of MortarStone showed this information in two different tables, but there were a lot of weaknesses to displaying it like that. It was easy to look at individual instances of giving, but the tabular format made it difficult to gauge growth over time or compare giving amounts by fund. This was a problem because one of our product’s main value propositions is that we allow users to easily spot giving trends and not only view individual gift items as you can in a spreadsheet, but to clearly show you the big picture as well.

I started by wireframing a solution. After a few iterations, we arrived on this layout, which shows the most important general data about the household at the top and shows some stats at the bottom. In the middle, we wanted to include a giving chart to inspect giving at both a macro and micro level. I purposely did not spend time dwelling about how the chart would look or behave too much, because I didn’t want to sell a design idea to our stakeholders and then find out that it was not technically feasible to pull it off. So in the design stage, I laid out the main elements and trusted the developer in me to find a chart solution that would be relatively easy to implement, flexible enough to be customized to our desires and scalable.

I had created charts previously with several popular javascript libraries, but I wasn’t quite satisfied with what I had tried before from neither a functional or aesthetic standpoint–I was in the market for a new library, and the search began.

After some shopping around for a good chart library, I decided to go with the amazing C3.js. C3.js is a simple, pure javascript library with only D3.js as a dependency. I had some experience using D3.js to make custom visualizations from scratch, and as awesome as D3.js can be, I thought it would be overkill to be creating my own charts from scratch. C3.js was a great solution because it offers a robust collection of pre-configured chart types and a convenient API to do some common customization, but it is simply using D3.js under the hood, so you still get the flexibility to do whatever you would want to directly via D3. The charts were also very minimalist, included tasteful animations out of the box and overall looked great!

Setting up the charts was a breeze. I installed both C3 and D3 using bower, and I was ready to start putting our data into a charts that easy to understand. I created an AngularJS service called msCharts that handles all of the general chart setup. This includes making sure that the chart legend and labels are visible on mobile devices, generating beautiful descriptive tooltips and essentially taking data from a regular array and feeding it to C3.js in the proper syntax.

'use strict';
angular.module('frontendCompanyApp')
.service('msCharts', function ($window) {
var self = this;
// rotate and cull legend on small layouts
var rotateDegrees = 0;
var culling = false;
var clientWidth = Math.max(document.documentElement.clientWidth, $window.innerWidth || 0);
if (clientWidth < 960) {
rotateDegrees = 60;
culling = true;
}
this.create = function (id, format, xAxisLabels, segments) {
function getFormat () {
var formatSet;
if (format === undefined) {
formatSet = d3.format(',');
} else if (format === 'days') {
formatSet = function (data) { return data.toFixed() + ' days'; };
} else if (format === 'dollars') {
formatSet = d3.format('$,.0f');
} else {
formatSet = d3.format(format);
}
return formatSet;
}
var chart = c3.generate({
bindto: '#givingChart',
data: {
columns: [],
groups: [segments],
order: 'desc',
},
legend: {
item: {
onclick: function (id) {
chart.toggle(id);
}
}
},
axis: {
x: {
type: 'category',
categories: xAxisLabels,
tick: {
culling: culling,
rotate: rotateDegrees
}
},
y: {
tick: {
format: getFormat()
}
}
},
grid: {
focus: {
show: false
}
},
tooltip: {
grouped: true,
format: getFormat(),
contents: function (data, defaultTitleFormat, defaultValueFormat, color) {
var template = self.givingTooltipTemplate(data, xAxisLabels, color);
return template;
}
}
});
return chart;
};
this.givingTooltipTemplate = function (data, xAxisLabels, color) {
var total = 0;
data.forEach(function (dataPoint) {total += dataPoint.value;});
var template = '' +
'<md-card class="chart__tooltip">' +
'<h3 class="chart__tooltip__header" layout="row" layout-align="start">' +
'<div flex>' + xAxisLabels[data[0].index] + ' total:</div>' +
'<div flex class="align-right"> $' + total.toLocaleString() + '</div>' +
'</h3>' +
'<div class="chart__tooltip__body" layout="column">';
data.forEach(function (dataPoint) {
template += '' +
'<div layout="row" layout-align="space-between start">' +
'<div flex>' + '<div class="chart__tooltip__swatch" style="background-color: '+ color(dataPoint.id) + '"></div>' + dataPoint.name + '</div>' +
'<div flex class="align-right"> $' + dataPoint.value.toLocaleString() + '</div>' +
'</div>';
});
template += '</div></md-card>';
return template;
};
});

Once that was set up, all I needed to do was get the data from the server and call my handy service method msCharts.create().

var app = angular.module('frontendCompanyApp');
app.controller('HouseholdRecordCtrl', function (
$scope, $http, msCharts, $cookies
) {
var self = this; 
this.getGiftsGroup = function (range) {
// Save range preference via cookies
$cookies.put('givingRange', range.key);
self.givingChart.loading = true;
self.givingChart.error = false;
self.givingRange = range.label;
$http.get('/api/v1/donors_service/donor_unit/donations/grouped', { params: { donor_unit_id: donorUnitId, type: range.key } })
.then(function (response) {
self.givingChart.loading = false;
createGiftsChart(response.data.grouped);
}, function (error) {
self.givingChart.loading = false;
self.givingChart.error = true;
console.log('Gifts grouped request error:', error);
});
};
)};

I opted for a stacked area chart. This type of chart represents quantity along the y-axis by area size and allows you to see how different data types make up the total sum. This was a great solution because it allows our users to view the giving as a whole and also see what that funds the giving was comprised of. Our users were very happy with the way the charts turned out. Now they can…

  • See a household’s giving across time
  • Select to view the giving by the last trailing 12 months, current year, month or quarter
  • Chose to show or hide individual funds
  • Hover over datapoints to reveal details
  • View their charts on mobile devices

Originally published at blog.cloudboost.io on August 15, 2017.