Data-Viz.png

Data Visualization with D3.js

Data Visualization with D3.js

 
Household-record-chart.png

Designing the UI

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.

The wireframe without a chart concept developed

Finding the right chart library

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!

Implementing the charts

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.

msCharts Service

'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().

Creating the data from server data

var app = angular.module('frontendCompanyApp');
app.controller('HouseholdRecordCtrl', function (
  $scope, $http, msCharts, $cookies
) {
  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);
    });
  };
)};

The final result

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