var app = angular.module('plunker', []);
app.constant('_', _);
app.controller('MainCtrl', function($scope, $timeout) {
$scope.data = [{
name: "Signal1",
score: 93
}, {
name: "Signal2",
score: 90
}, {
name: 'Signal3',
score: 75
}, {
name: "Signal4",
score: 60
}, {
name: "Signal5",
score: 50
}, {
name: "Signal6",
score: 34
}, {
name: 'Signal7',
score: 23
}, {
name: "Signal8",
score: 17
}, {
name: "Signal9",
score: 13
}, {
name: "Signal10",
score: 9
}];
$scope.barConfig = {
margin: {
top: 30,
right: 30,
bottom: 40,
left: 50
},
height: 400,
gradient: {
color: ['#419BF9', '#207EE1', 'red', 'yellow'],
y2: 100
}
};
$scope.xConfig = {
field: 'name',
innerPadding: 0.1,
labels: {
mainText: 'signal name'
}
};
$scope.yConfig = {
field: 'score',
ticks: 10,
labels: {
mainText: 'scores ( in % )'
}
};
$scope.barConfigHorizontal = {
margin: {
top: 30,
right: 30,
bottom: 40,
left: 50
},
height: 400,
gradient: {
color: ['#419BF9', '#207EE1', 'red', 'yellow'],
x2: 100
}
};
$scope.xConfigHorizontal = {
field: 'score',
ticks: 10,
labels: {
mainText: 'scores ( in % )'
}
};
$scope.yConfigHorizontal = {
field: 'name',
innerPadding: 0.1,
labels: {
mainText: 'signal name'
}
};
});
<!DOCTYPE html>
<html ng-app="plunker">
<head>
<meta charset="utf-8" />
<title>AngularJS Plunker</title>
<script>
document.write('<base href="' + document.location + '" />');
</script>
<link rel="stylesheet" href="style.css" />
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<script data-require="angular.js@1.4.x" src="https://code.angularjs.org/1.4.9/angular.js" data-semver="1.4.9"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.0.0/lodash.min.js"></script>
<script src="app.js"></script>
<script src="bar-chart.directive.js"></script>
<script src="d3.service.js"></script>
<script src="chart.service.js"></script>
</head>
<body ng-controller="MainCtrl">
<!--toDo: Responsible d3 inside bootstrap -->
<h1>Vertical chart</h1>
<div class="col-xs-12">
<d-bars data="data" xconfig="xConfig" y-config="yConfig" bar-config="barConfig"></d-bars>
</div>
<h1>Horizontal chart</h1>
<div class="col-xs-12">
<d-bars data="data" xconfig="xConfigHorizontal" y-config="yConfigHorizontal" bar-config="barConfigHorizontal" horizontal="true"></d-bars>
</div>
</body>
</html>
/* Put your css in here */
/* Custome Axis: http://bl.ocks.org/mbostock/4323929 */
.begin { stop-color: #419BF9 ; }
.end { stop-color: #207EE1 ; }
.y-axis path {
display: none;
}
.y-axis line {
stroke: #777;
stroke-dasharray: 2,2;
}
.horizontal-chart .x-axis path {
display: none;
}
.horizontal-chart .x-axis line {
stroke: #777;
stroke-dasharray: 2,2;
}
.tooltip:after {
top: 100%;
left: 50%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
border-color: rgba(255, 255, 255, 0);
border-top-color: black;
border-width: 4px;
margin-left: -4px;
}
.tooltip-horizontal:after {
right: 100%;
top: 50%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
border-color: rgba(255, 255, 255, 0);
border-right-color: black;
border-width: 4px;
margin-top: -4px;
}
/* http://www.ng-newsletter.com/posts/d3-on-angular.html
By using dependecy injection, we can keep our global namespace clean and
can inject our dependencies like normal.
All the work that we’ll do with our d3 library, we’ll do on a new module with the name d3.
With this factory, we can add our custom code to our d3 element. With this in place, we can inject our d3 service into our code by adding it as a dependency to our app module, like normal.*/
angular.module('plunker')
.factory('d3Service', ['$document', '$window', '$q', '$rootScope',
function($document, $window, $q, $rootScope) {
var d = $q.defer(),
d3service = {
d3: function() {
return d.promise;
}
};
function onScriptLoad() {
// Load client in the browser
$rootScope.$apply(function() {
d.resolve($window.d3);
});
}
var scriptTag = $document[0].createElement('script');
scriptTag.type = 'text/javascript';
scriptTag.async = true;
scriptTag.src = 'https://d3js.org/d3.v4.min.js';
scriptTag.onreadystatechange = function() {
if (this.readyState == 'complete') onScriptLoad();
};
scriptTag.onload = onScriptLoad;
var s = $document[0].getElementsByTagName('body')[0];
s.appendChild(scriptTag);
return d3service;
}
]);
angular.module('plunker')
.directive('dBars', ['$window', '$timeout', 'd3Service', '$compile', 'uptakeChart', '$location',
function($window, $timeout, d3Service, $compile, uptakeChart, $location) {
return {
restrict: 'EA',
scope: {
data: '=',
yConfig: '=',
xconfig: '=',
barConfig: '=',
horizontal: '@'
},
link: function(scope, ele, attrs) {
d3Service.d3().then(function(d3) {
var chartDiv = d3.select(ele[0])
.append('div').classed("row", true)
.append('div').classed("col-xs-12", true)
.append('div').classed("chart", true);
var barChart = new uptakeChart.uptakeBarChart();
scope.barConfig.width = ele[0].querySelector('.chart').clientWidth;
console.log(scope.xconfig,scope.yConfig);
// create uptakeChart
barChart.data(scope.data)
.barConfig(scope.barConfig)
.xConfig(scope.xconfig)
.yConfig(scope.yConfig)
.initChart(chartDiv);
scope.$watch('data', function(data) {
scope.horizontal ? barChart.data(data).removeBars().buildHorizontalChart(): barChart.data(data).removeBars().buildVerticalChart();
}, true);
angular.element($window).bind('resize', function(){
scope.barConfig.width = ele[0].querySelector('.chart').clientWidth;
scope.horizontal ? barChart.barConfig(scope.barConfig).buildHorizontalChart(): barChart.barConfig(scope.barConfig).buildVerticalChart();
});
});
}
};
}
]);
'use strict';
angular.module('plunker')
.service('uptakeChart', ['d3Service', '_',
function(d3Service, _) {
var self = this;
d3Service.d3().then(function(d3) {
//self.uptake = {};
self.initializeContainer = function(svg, height, width) {
return svg.attr('width', width) // return to outer world through function getChart
.attr('height', height)
.append('g');
};
self.initializeTooltip = function(container) {
return container.append('div')
.attr('class', 'tooltip');
};
self.initializeAxes = function(svg, className) {
return svg.append('g')
.classed(className, true);
};
self.uptakeBarChart = function() {
var uptakeBarChart = this;
var svg;
var container;
var data;
var barConfig; // bar specific
var xConfig;
var yConfig;
var chart;
var vGuide;
var hGuide;
var vGuideLabel;
var hGuideLabel;
var tooltip;
var vAxis;
var hAxis;
var maxYaxis;
var maxXaxis;
// getter and setter for chart data
uptakeBarChart.data = function(value) {
if (!arguments.length) return data;
data = value;
return uptakeBarChart;
};
// getter and setter for chart margin
uptakeBarChart.barConfig = function(value) {
if (!arguments.length) return barConfig;
barConfig = value;
return uptakeBarChart;
};
// getter and setter for chart x-axis config
uptakeBarChart.xConfig = function(value) {
if (!arguments.length) return xConfig;
xConfig = value;
return uptakeBarChart;
};
// getter and setter for chart y-axis config
uptakeBarChart.yConfig = function(value) {
if (!arguments.length) return yConfig;
yConfig = value;
return uptakeBarChart;
};
var chartSizing = function() {
return {
top: _.get(barConfig, ['margin', 'top'], 0),
right: _.get(barConfig, ['margin', 'right'], 0),
bottom: _.get(barConfig, ['margin', 'bottom'], 0),
left: _.get(barConfig, ['margin', 'left'], 0),
quadHeight: _.get(barConfig, 'height', 400) - _.get(barConfig, ['margin', 'top'], 0) - _.get(barConfig, ['margin', 'bottom'], 0),
quadWidth: _.get(barConfig, 'width', 600) - _.get(barConfig, ['margin', 'left'], 0) - _.get(barConfig, ['margin', 'right'], 0),
height: _.get(barConfig, 'height', 400),
width: _.get(barConfig, 'width', 600),
color: _.get(barConfig, ['gradient', 'color'], ['#419BF9']),
gradX1: _.get(barConfig, ['gradient', 'x1'], 0),
gradX2: _.get(barConfig, ['gradient', 'x2'], 0),
gradY1: _.get(barConfig, ['gradient', 'y1'], 0),
gradY2: _.get(barConfig, ['gradient', 'y2'], 0),
gradSpreadMethod: _.get(barConfig, ['gradient', 'spreadMethod'], 'pad'),
};
};
function barFill() {
svg.select('lineargradient').remove();
if (chartSizing().color && chartSizing().color.length) {
var colorLength = chartSizing().color.length;
var gradient = svg.append("linearGradient")
.attr("id", "gradient")
.attr("x1", chartSizing().gradX1 + "%")
.attr("y1", chartSizing().gradY1 + "%")
.attr("x2", chartSizing().gradX2 + "%")
.attr("y2", chartSizing().gradY2 + "%")
.attr("x1", chartSizing().gradX1 + "%")
.attr("spreadMethod", chartSizing().gradSpreadMethod);
chartSizing().color.forEach(function(d, i) {
gradient.append("svg:stop")
.style("stop-color", d)
.attr("offset", (colorLength > 1 ? i * (100 / (colorLength - 1)) : 0) + "%");
});
chart.selectAll('rect')
// abs url needed as uptake uses <base> tag in the header which causes IRI to be relative to the base page and not the current page
.style('fill', 'url(' + location.href + '#gradient)');
}
}
uptakeBarChart.initChart = function(container) {
container = container;
svg = container.append('svg');
hGuide = self.initializeAxes(svg, "x-axis"); // entire x-axis in one group element g
vGuide = self.initializeAxes(svg, "y-axis"); // entire y-axis in one group element g
vGuideLabel = self.initializeAxes(svg, "y-axis-label").append("text");
hGuideLabel = self.initializeAxes(svg, "x-axis-label").append("text");
tooltip = self.initializeTooltip(container);
chart = self.initializeContainer(svg, chartSizing().height, chartSizing().width)
.classed("chart-data", true);
return uptakeBarChart;
};
uptakeBarChart.removeBars = function() {
svg.selectAll("rect").remove();
return uptakeBarChart;
};
function buildAxes() {
vGuide.call(vAxis); // append this vGuide to y-Axis so it is available in html
hGuide.call(hAxis); // append this hGuide to x-Axis so it is available in html
// axis css
vGuide.attr('transform', 'translate(' + chartSizing().left + ',' + chartSizing().top + ')');
hGuide.attr('transform', 'translate(' + chartSizing().left + ',' + (chartSizing().quadHeight + chartSizing().top) + ')');
// axis labels
vGuideLabel.attr("text-anchor", "middle") // this makes it easy to centre the text as the transform is applied to the anchor
.attr("transform", "translate(" + 10 + "," + ((chartSizing().height) / 2) + ")rotate(-90)") // text is drawn off the screen top left, move down and out and rotate
.text(_.get(yConfig, 'axisLabel', 'y-axis'));
hGuideLabel.attr("text-anchor", "middle") // this makes it easy to centre the text as the transform is applied to the anchor
.attr("transform", "translate(" + ((chartSizing().width) / 2) + "," + (chartSizing().height - 5) + ")") // centre below axis
.text(_.get(xConfig, 'axisLabel', 'x-axis'));
}
// main chart function
uptakeBarChart.buildVerticalChart = function() {
var xScale;
var yScale;
var yScaleDown;
// imp: add margin to the bar chart (https://bl.ocks.org/mbostock/3019563)
svg.attr('width', chartSizing().width);
// finding maxYaxis from data
maxYaxis = d3.max(data, function(d) {
return _.get(d, _.get(yConfig, 'field'));
});
// to get data remapped to fit yScale (0, height)
yScale = d3.scaleLinear()
.domain([0, maxYaxis])
.range([0, chartSizing().quadHeight]);
// to get data remapped to fit yScale (height, 0)
yScaleDown = d3.scaleLinear()
.domain([0, maxYaxis])
.range([chartSizing().quadHeight, 0]);
// to get data remapped to fit xScale (0, width)
xScale = d3.scaleBand()
.domain(data.map(function(d) { // creating new array of object with what we need on x scale(here data.name)
return _.get(d, _.get(xConfig, 'field'));
}))
.range([0, chartSizing().quadWidth])
.paddingInner([_.get(xConfig, 'innerPadding', 0.1)]);
// y -axis
vAxis = d3.axisLeft(yScaleDown) // create axis on left with scale (height, 0)
.tickSize(-chartSizing().quadWidth) // give size of tick
.tickPadding([5]) // add padding to tick
.ticks(_.get(yConfig, 'ticks', 10)); // number of ticks want to add on scale
// x -axis
hAxis = d3.axisBottom(xScale) // create axis on bottom with scale (0, width)
.tickSize(0) // give size of tick
.tickPadding([5]); // add padding to tick
//charting
chart.selectAll('rect').data(uptakeBarChart.data())
.enter().append('rect')
.attr('transform', 'translate(' + chartSizing().left + ',' + chartSizing().top + ')'); // to move graph to center of width & height
barFill();
chart.selectAll('rect')
.attr('height', function(d) {
console.log(_.get(d, _.get(yConfig, 'field')))
return yScale(_.get(d, _.get(yConfig, 'field'))); // height of each rect to fit on yScale
})
.attr('y', function(d) {
return chartSizing().quadHeight - yScale(_.get(d, _.get(yConfig, 'field'))); // y axis from where rect will start plotting
// OR return yScaleDown(_.get(d, _.get(yConfig, 'field')));
});
chart.selectAll('rect')
.transition()
.duration(400)
.attr('width', xScale.bandwidth()) // width of each rect to fit on xScale
.attr('x', function(d) {
return xScale(_.get(d, _.get(xConfig, 'field'))); // x axis from where rect will start plotting
});
buildAxes()
//tooltip
chart.selectAll('rect')
.on('mouseover', function(d) {
d3.select(this).style('opacity', 0.9); // change opactity of selected element
tooltip.transition() // add transtion to tooltip for better design change
.style('opacity', 0.9); // change opactity of tooltip to make is visible
tooltip.html(d.score)
.style('left', (xScale(d.name) + chartSizing().left + 15) + 'px') // x axis of tooltip
.style('top', (yScaleDown(d.score)) + 'px'); // y-axis of tootltip to open on top
})
.on('mouseout', function(d) {
d3.select(this).style('opacity', 1); // change back opactity of selected element
tooltip.transition()
.style('opacity', 0); // change back opactity of tooltip
});
tooltip.style('width', xScale.bandwidth() + 'px')
.style('height', '25px')
.style('line-height', '25px')
.style('position', 'absolute')
.style('background', 'black')
.style('color', 'white')
.style('text-align', 'center')
.style('opacity', 0);
return uptakeBarChart;
};
uptakeBarChart.buildHorizontalChart = function() {
var xScale;
var yScale;
svg.attr('width', chartSizing().width);
// finding maxXaxis from data
maxXaxis = d3.max(data, function(d) {
return _.get(d, _.get(xConfig, 'field'));
});
// to get data remapped to fit yScale (0, height)
yScale = d3.scaleBand()
.domain(data.map(function(d) {
return _.get(d, _.get(yConfig, 'field'));
}))
.range([0, chartSizing().quadHeight])
.paddingInner([_.get(yConfig, 'innerPadding', 0.1)]);
// to get data remapped to fit xScale (0, width)
xScale = d3.scaleLinear()
.domain([0, maxXaxis])
.range([0, chartSizing().quadWidth]);
// y -axis
vAxis = d3.axisLeft(yScale)
.tickSize(_.get(yConfig, 'tickSize', 0))
.tickPadding([_.get(yConfig, 'tickPadding', 5)]);
// x -axis
hAxis = d3.axisBottom(xScale)
.ticks(_.get(xConfig, 'ticks', 10));
//charting
chart.selectAll('rect').data(uptakeBarChart.data())
.enter().append('rect')
.attr('transform', 'translate(' + chartSizing().left + ',' + chartSizing().top + ')'); // to move graph to center of width & height
barFill();
chart.selectAll('rect')
.attr('height', yScale.bandwidth())
.attr('y', function(d) {
console.log(yScale(_.get(d, _.get(yConfig, 'field'))))
return yScale(_.get(d, _.get(yConfig, 'field'))); // y axis from where rect will start plotting
});
chart.selectAll('rect')
.transition()
.duration(400)
.attr('width', function(d) {
return xScale(_.get(d, _.get(xConfig, 'field')));
})
.attr('x', 0);
buildAxes();
return uptakeBarChart;
};
return uptakeBarChart;
};
});
}
]);