Skip to content

Instantly share code, notes, and snippets.

@enjalot
Forked from milroc/README.md
Last active December 22, 2015 10:18
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save enjalot/6457608 to your computer and use it in GitHub Desktop.
Save enjalot/6457608 to your computer and use it in GitHub Desktop.

Sparkline Directive for Angular with d3.js

The reusable chart pattern makes it easy to make components responsive to changes in data as well as dimension.

Integrating with Angular based on code by @milr0c: Angular.js + d3.js

Miles' talk on reusable charts with MV* Frameworks:Video

Shirley discusses a very similar pattern with Backbone: Video

<!DOCTYPE html>
<head>
<meta charset="utf-8">
<link type="text/css" rel="stylesheet" href="style.css"/>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.6/angular.min.js"></script>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="perlin.js"></script>
<script src="sparkline.js"></script>
</head>
<body>
<div ng-app="main">
<div ng-controller="MainController">
<div class="span2">
<button class="btn btn-success" ng-click="update()">update</button>
</div>
<chart-sparkline data="data"></chart-sparkline>
</div>
</div>
<script type="text/javascript">
var main = angular.module('main', [])
.config(['$routeProvider', function($routeProvider) {
$routeProvider.when('/', { controller: MainController });
}]);
// Controllers
var MainController = ['$scope', function($scope) {
// create random data
$scope.randomize = function(n, y) {
if (arguments.length < 2) y = 1;
if (!arguments.length) n = 30;
return d3.range(n).map(function(d) { return Math.random() })
};
$scope.update = function() {
//TODO: make this come from API
$scope.updates++;
$scope.data = $scope.randomize();
};
// Models
//initial values
$scope.updates = 0;
$scope.data = $scope.randomize();
}];
// Views
main.directive('chartSparkline', function() {
var sparkline = charts.sparkline();
return {
restrict: 'E',
replace: true,
template: '<div class="chart"></div>',
scope: {
data: '=',
},
link: function($scope, $element, $attr) {
//we select the element of this directive
var div = d3.select($element[0]);
//we calculate it's dimensions so we can be responsive
var bbox = div.node().getBoundingClientRect();
sparkline.width(bbox.width || 900);
sparkline.height(bbox.height || 400 - 50);
window.onresize = function() {
bbox = div.node().getBoundingClientRect();
sparkline.width(bbox.width || 900);
sparkline.height(bbox.height || 400 - 50);
//this is how you update the chart
div.call(sparkline);
}
//we update the chart when the data get's updated
$scope.$watch('data', function(newVal, oldVal) {
if(newVal) div.datum(newVal).call(sparkline);
});
}
}
});
</script>
</body>
</html>
charts = {};
charts.sparkline = function() {
// basic data
var margin = {top: 0, bottom: 50, left: 0, right: 0},
width = 900,
height = 400,
//accessors
xValue = function(d, i) { return i; },
yValue = function(d) { return d; },
// chart underpinnings
x = d3.scale.ordinal(),
y = d3.scale.linear(),
// chart enhancements
elastic = {
x: true,
y: true
},
convertData = true,
duration = 500,
formatNumber = d3.format(',d');
function render(selection) {
selection.each(function(data) {
// setup the basics
var w = width - margin.left - margin.right,
h = height - margin.top - margin.bottom;
if (convertData) {
data = data.map(function(d, i) {
return {
x: xValue.call(data, d, i),
y: yValue.call(data, d, i)
};
});
}
// set scales
if (elastic.x) x.domain(data.map(function(d) { return d.x; }));
if (elastic.y) y.domain([d3.min(data, function(d) { return d.y}), d3.max(data, function(d) { return d.y; })]);
console.log("data", data)
console.log("bounds", x.domain(), y.domain())
x.rangeRoundBands([0, w], .1);
y.range([h, margin.bottom]);
var line = d3.svg.line()
.x(function(d) { return x(d.x) })
.y(function(d) { return y(d.y) })
.interpolate("basis")
var svg = selection.selectAll('svg').data([data]);
var chartEnter = svg.enter()
.append('svg')
.attr('width', w)
.attr('height', h)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
.classed('chart', true);
var chart = svg.select('.chart');
path = chart.selectAll('.path').data([data]);
path.enter()
.append('path')
.classed('path', true)
.attr('d', line)
path.transition()
.duration(duration)
.attr('d', line)
path.exit()
.transition()
.duration(duration)
.style('opacity', 0)
.remove();
});
}
// basic data
render.margin = function(_) {
if (!arguments.length) return margin;
margin = _;
return render;
};
render.width = function(_) {
if (!arguments.length) return width;
width = _;
return render;
};
render.height = function(_) {
if (!arguments.length) return height;
height = _;
return render;
};
// accessors
render.xValue = function(_) {
if (!arguments.length) return xValue;
xValue = _;
return render;
};
render.yValue = function(_) {
if (!arguments.length) return yValue;
yValue = _;
return render;
};
render.x = function(_) {
if (!arguments.length) return x;
x = _;
return render;
};
render.y = function(_) {
if (!arguments.length) return y;
y = _;
return render;
};
// chart enhancements
render.elastic = function(_) {
if (!arguments.length) return elastic;
elastic = _;
return render;
};
render.convertData = function(_) {
if (!arguments.length) return convertData;
convertData = _;
return render;
};
render.duration = function(_) {
if (!arguments.length) return duration;
duration = _;
return render;
};
render.formatNumber = function(_) {
if (!arguments.length) return formatNumber;
formatNumber = _;
return render;
};
return render;
};
body {
font: 14px helvetica;
width: 100%;
height: 100%;
}
.chart {
width: 100%;
height: 100%;
}
.path {
fill: none;
stroke: #000;
stroke-width: 3px;
}
.selected {
stroke: #78C656;
}
.btn {
font-size: 40px;
background-color: #efefef;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment