AngularJS Grouped Bar Chart and Line Chart using D3

In one of my ealrier posts, Custom AngularJS directive to plot chart using D3.js, i talked about how to go about creating custom directive for simple chart. In this post I am going to post some code that I have put together as building blocks for reusable charts using D3.js. The sample shows how to build grouped bar charts with line charts.

d3 grouped bar chart

Data source

The data structure this chart is as below.


[
{id=112, supply=20000, demand=18000, price=1100},
{id=113, supply=25000, demand=22000, price=1000}
]

The data is grouped on id. The values for supply and demand properties are combined to create series data for each "id" group. The line charts super imposed on bar charts uses data from price property value.

Chart configuration

The implementation shows how to use some of D3 concepts to configure padding, offsets etc. for data groups and series.


var defaultSettings = {
  showValues: true,
  dimensions: { width: 250, height: 500, useContainer: true },
  margin: { top: 20, right: 40, bottom: 30, left: 40 },
  axis: { showX: false, showY: true },
  padding: { left:10, right:10, series: 0, group: 10 }
};

  • ordinal.rangeBands(interval[, padding[, outerPadding]]): Padding configuration supples values as % of each group's width.
  • ordinal.rangeBand(): Width of each column drawn is calculated by using this method. It accounts for padding and outerpadding values used to calculated width of each data point rectangle.

The code also shows how to draw markers on line chart.

AngularJS service and directive

Following code shows how reusable angularjs service has been built.


    angular.module('ui.byteblocks.charts', []).
    controller('ChartController', 
    ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) {
        var width, height;
        var chartContainer;
        var defaultSettings = {
            showValues: true,
            dimensions: { width: 250, height: 500, useContainer: true },
            margin: { top: 20, right: 40, bottom: 30, left: 40 },
            axis: { showX: false, showY: true },
            padding: { left:10, right:10, series: 0, group: 10 }
        };
    $scope.colors = d3.scale.category20();
    $scope.container = function() { return chartContainer; };
    $scope.dimensions = function() { return { width: width, height: height }; };
    $scope.getSettings = function(settings) {
        return $.extend(true, defaultSettings, settings ? settings : {});
    };
    $scope.initialize = function(selector) {
        calculateDimensions();
        createChart(selector);
    };
    $scope.renderXAxis = function(scale, orientation, position) {
        var xAxis = d3.svg.axis()
                        .scale(scale)
                        .orient(orientation);
        $scope.container().append("g")
                        .attr("class", "x axis")
                        .attr("transform", "translate(0," + position + ")")
                        .call(xAxis);
    };
    $scope.renderYAxis = function(label, scale, orientation, position, labelPosition) {
        var yAxis = d3.svg.axis()
            .scale(scale)
            .orient(orientation)
            .tickFormat(d3.format(".2s"));
        $scope.container().append("g")
            .attr("class", "y axis")
            .call(yAxis)
            .attr("transform", "translate(" + position + ")")
            .append("text")
            .attr("transform", "rotate(-90)")
            .attr("y", labelPosition)
            .attr("dy", ".71em")
            .style("text-anchor", "end")
            .text(label);
    };
    var createChart = function(selector) {
        chartContainer = d3.select(selector)
            .attr("width", 
                width + $scope.chartConfig.margin.left + $scope.chartConfig.margin.right)
            .attr("height", 
                height + $scope.chartConfig.margin.top + $scope.chartConfig.margin.bottom)
            .append("g")
            .attr("transform", 
                "translate(" + $scope.chartConfig.margin.left + "," + $scope.chartConfig.margin.top + ")");
        return chartContainer;
    };
    var calculateDimensions = function() {
        if ($scope.chartConfig.dimensions.useContainer) {
            width = $($element).width();
        } else {
            width = $scope.chartConfig.dimensions.width;
        }
        width = width - $scope.chartConfig.margin.left - $scope.chartConfig.margin.right;
        height = $scope.chartConfig.dimensions.height - 
                   $scope.chartConfig.margin.top - $scope.chartConfig.margin.bottom;
    };
}]);
WebPortal.controller('ReportsController',
[
    '$scope', 'reportsApi', 'diagnostics',
    function ($scope, reportsApi, diagnostics) {
        $scope.supplyDemand = {};
        var getSupplyDemandData = function() {
            reportsApi.getSupplyDemand().
                then(
                    function(response) {
                        $scope.supplyDemand = response.data.Data;
                    },
                    function(response) {
                    }
                );
        };
        getSupplyDemandData();
    }
]);
WebPortal.directive('supplyDemandChart', [
    function () {
        var directiveObj = {
            restrict: 'E',
            scope: {
                supplyDemand: '=',
                config: '='
            },
            controller: 'ChartController',
            transclude: true,
            template: "<svg id='supplyDemandReportChart' class='chart'></svg>",
            link: function (scope, element) {
                scope.chartConfig = scope.getSettings(scope.config);
                var width = 0;
                var height = 0;
                var columnWidth = 0;
                var sY, sX, sX1, pY, pX;
                var testScale;
                scope.$watch('supplyDemand', function (newVal, oldVal) {
                    update();
                });
                var update = function () {
                    if (!scope.supplyDemand || !angular.isArray(scope.supplyDemand)) return;
                    width = scope.dimensions().width;
                    height = scope.dimensions().height;
                    calculateXYScales();
                    calculateBarWidths();
                    var line = d3.svg.line()
                        .x(function (d) {
                            return sX(d.Number) + columnWidth;
                        })
                        .y(function (d) {
                            return pY(d.Price);
                        });
                    if (scope.chartConfig.axis.showX) {
                        scope.renderXAxis(sX, "bottom", height);
                    }
                    if (scope.chartConfig.axis.showY) {
                        scope.renderYAxis("Demand/Supply", sY, "left", 0, -35);
                    }
                    teGroups = scope.container().selectAll(".group").data(scope.supplyDemand)
                        .enter().append("g")
                        .attr("class", "g")
                        .attr("transform", function (d, i) {
                            return "translate(" + sX(d.Number) + ",0)";
                        });
                    teGroups.selectAll("rect")
                        .data(function (d) { return [d.Supply, d.Demand]; })
                        .enter().append("rect")
                        .attr("x", function (d, i) {
                            return sX1(i);
                        })
                        .attr("y", function (d) {
                            return sY(d);
                        })
                        .attr("width", sX1.rangeBand())
                        .attr("height", function (d, i) {
                            return height - sY(d);
                        })
                        .style("fill", function (d, i) { return scope.colors(i); });
                    if (scope.chartConfig.axis.showY) {
                        scope.renderYAxis("Price", pY, "right", width, 30);
                    }
                    scope.container().append("path")
                        .datum(scope.supplyDemand)
                        .attr("class", "line")
                        .attr("d", line);
                    scope.container().selectAll("circle").data(scope.supplyDemand).enter().append("circle")
                        .attr("r", 5).attr("cx", function (d) {
                            return sX(d.Number) + columnWidth;
                        }).attr("cy", function (d) {
                            return pY(d.Price);
                        });
                };
                
                var calculateBarWidths = function () {
                    columnWidth = sX1.rangeBand();
                };
                var calculateXYScales = function () {
                    sY = d3.scale.linear().range([height, 0]);
                    pY = d3.scale.linear().range([height, 0]);
                    sY.domain([0, d3.max(scope.supplyDemand, function (d) {
                        return d.Supply;
                    })]);
                    pY.domain([0, d3.max(scope.supplyDemand, function (d) {
                        return d.Price;
                    })]);
                    
                    sX = d3.scale.ordinal();
                    sX.rangeRoundBands([0, width], scope.chartConfig.padding.group / 100, scope.chartConfig.padding.left / 100);
                    sX.domain(scope.supplyDemand.map(function (d) {
                        return d.Number;
                    }));
                    
                    sX1 = d3.scale.ordinal();
                    sX1.domain([0, 1]).rangeRoundBands([0, sX.rangeBand()]);
                    pX = d3.scale.ordinal().rangeRoundBands([0, width]);
                    pX.domain(scope.supplyDemand.map(function (d) {
                        return d.Number;
                    }));
                };
                scope.initialize("#supplyDemandReportChart");
                update();
            }
        };
        return directiveObj;
    }
]);

How to use it?

Include the service in your application's module definition.


    var WebPortal = angular.module("AngularWebPortal",
    ['ngRoute', 'ui.byteblocks.qtip', 'ui.byteblocks.charts']);

HTML code for this sample looks as below.


<div ng-controller="ReportsController">
    <div class="row">
    <div class="col-xs-12 text-center">
    <h3>Supply Demand</h3>
        </div>
    </div>
    <div class="row" style="border:1px solid #eee;">
    <div class="col-xs-12" style="height:500px;">
    <supply-demand-chart data-supply-demand="supplyDemand" 
    config="{}"></supply-demand-chart>
        </div>
    </div>
</div>

comments powered by Disqus

Search

Social

Weather

Monthly Posts