A current recommended way to visualize data with AngularJS and D3.js - angularjs

I develop a web application in which I need to visualize data.
I am using AngularJS for a modular design and separation of view, data and logic.
I am using D3.js to create visualizations of data with SVG elements.
Each of AngularJS and D3.js works very well, but I feel like I'm doing something wrong by using them together in such a decoupled way.
My web application consists of both HTML elements and SVG elements, both are being generated from the same data sets. And yet, I am using two different tools to create and manipulate the UI (Angular to dynamically build the interface and D3 to dynamically build the graphics).
Both the HTML and the SVG elements need to allow interaction with the user.
I'm currently wrapping my SVG graphics with Angular controllers. Inside the controller I am directly building and manipulating the SVGs with D3. When there are many interactions, animations and data changes - this quickly becomes painfully cumbersome and tedious.
I created this simple fiddle as a simplified example of how I currently do things.
See the itemsGraphCtrl controller as an example for using D3 to create graphics from within an Angular controller.
/* ... */
.controller("itemsGraphCtrl", function($scope, dataStore) {
$scope.data = dataStore.data;
var canvas = d3.select(".canvas");
//enter:
canvas.selectAll('circle').data($scope.data)
.enter().append('circle')
.attr('cx', function(d, i) {
return i * 30 + 10;
})
.attr('cy', 50)
.attr('r', 5)
.style('fill', 'white')
.style('stroke', 1)
.on('click', function(d) {
$scope.$apply(function() {
d.selected = !d.selected;
});
});
function update() {
canvas.selectAll('circle').data($scope.data)
.style('fill', function(d) {
return d.selected ? 'black' : 'white';
});
}
$scope.$watch('data', update, true);
});
This hardly seems like the "Angular way" to do things.
Any suggestions / guidelines / improvements / libraries / other solutions will be greatly appreciated.

I would suggest first making your D3 charts into reusable objects, using the pattern suggested by this article or a library like d3.chart.
Once your D3 charts are encapsulated in their own objects, you can wrap them in directives. As you discover how your charts will interact with the rest of your application, you will start to build up an API on the chart objects themselves; for example, the chart object may trigger an event when a circle has been clicked. You can then hook into this API in your directive wrapper. This way, you can integrate your D3 objects into your Angular code, just like you would any other GUI component or plugin.
Once you have your chart, your directive may look like this:
.directive('bubbleChart', function() {
var chart = d3.charts.bubble();
return {
restrict: 'E',
scope: {
data: '=',
emptyMessage: '#'
},
link: function(scope, element, attrs) {
chart.emptyMessage(scope.emptyMessage);
scope.$watch('data', function(data) {
d3.select(element[0])
.datum(data)
.call(chart);
});
}
};
})
You can add additional options to your chart object, or let users set options via attributes.

Related

Same element id's on different pages of AngularJS app

I'm very new to AngularJS, and am working to convert an existing web application written using JQuery and Bootstrap to one using AngularJS and Ionic.
I have 3 pages in my web application, each with a html5 canvas and a button. The canvas layout and styling are exactly the same on each page, however the button draws a different thing to each canvas, e.g. on page 1 draw a dog, page 2 a cat, and page 3 a chicken.
Because of this, I use the same element ID's between the three pages, e.g. '#myCanvas' for the canvas. I have a Javascript object called 'Drawer', which draws the relevant thing to '#myCanvas' when the relevant button is pressed.
When converting to AngularJS, I placed each page into a template, and converted the 'Drawer' object into a factory called DrawFactory. Each page is linked to a different controller that uses DrawFactory.
My problem is that, say for example I'm on page 2 and click the draw cat button, it draws it to the #myCanvas on page 1. Previously this wasn't a problem as each page loaded separately and therefore all ids were unique.
How can I achieve what I want without renaming each of the canvases to (for example) #myCanvas1, #myCanvas2, #myCanvas3- and creating 3 separate DrawFactories that individually draw to one of them?
I'm sure I'm just missing some key AngularJS concept. Thanks.
Without having your code posted, I suspect you may still be using jQuery selectors to find elements (e.g. $('#myCanvas')). You may wish to restructure your code to behave like this at a high level.
<div ng-controller="PageOneCtrl">
<drawing source="animal"></drawing>
<button ng-click="drawAnimal()">Draw</button>
</div>
The controller would be defined similar to the following:
angular.module('app').controller('PageOneCtrl', function ($scope) {
$scope.animal = null;
$scope.drawAnimal = function () {
$scope.animal = 'dog.png';
};
});
And the drawing directive like so:
angular.module('app').directive('drawing', function () {
return {
scope: {
source: '='
},
template: '<canvas width="100" height="100"></canvas>',
link: function (scope, element) {
scope.$watch('source', function (newSource) {
if (!newSource) return;
var context = element.getContext('2d'),
image = new Image(100, 100);
image.src = newSource;
image.onload = function () {
context.drawImage(image, 0, 0);
};
});
}
};
});

OO-layout / formatting (like Wicket) in AngularJS?

I'm a pretty experienced Wicket developer, but curious about AngularJS, so I decided to give it a try. Most things are pretty straightforward when coming from Java (DI etc).
However, I was not able to find a (Object Oriented) strategy for layout of components in my page. In Wicket, you can pack view and behaviour together in a Component, for example a Panel. You can create multiple subclasses for a certain component, and decide which subclass to use in Java.
In my case, I develop a (abstract) Game which has a Board component. Based on the type of game, a different Board will be renderend. Other components, like the title bar, scoring, end-of-game animation etc. will be the same for all games. In Wicket, this would look something like this:
Board b = myGame.newBoardPanel(id);
add(b);
Where the game can provide an abstract method to provide a Board.
I tried looking at views, but it seems hard to combine multiple different dynamic components in them. In my case, next to the Board, I have an AnswerPanel which can also differ from game to game, but for some games, it will be the same.
My next move was to use directives; but it seems AngularJS is not designed to choose layout from the controller, as it is not easy to change (in my case) the templateURL to implement different layout.
TL;DR
What is the Angular way to implement different components in a OO-style?
In angular you would use directives to reuse elements in the game.
Directives and element can have a controller associated with them. The controllers in angular are expected to be functions, which internally are instantiated using the new keyword, so you can use classes. And if you use classes you can use inheritance to share logic between your controllers.
You can modify the contents of a directive HTML node ($element), but you need to load and compile the template manually.
<!-- Use directives to reuse elements -->
<div ng-controller="GameController as gameCtrl">
<title-bar title="gameCtrl.title"></title-bar>
<answer-panel answers="gameCtrl.answers"></answer-panel>
<!-- Pass data into your directives via attributes, from the controller -->
<board type="gameCtrl.boardType"></board>
</div>
ES6
// controllers can be classes, as such they can be extended via mixins
// or OO extend.
class GameController {
constructor($location, gameRepository) {
var ctrl = this;
var params = $location.search();
gameRepository.get(params.id).then(function (game) {
// store the data in the controller instead of the $scope,
// this gives you context to where your data is coming from in
// the templates
ctrl.title = game.title;
ctrl.answers = game.answers
ctrl.boardType = game.boardType;
});
}
}
The title-bar directive would be very simple:
function titleBarDirective() {
return {
restrict: 'E',
// this is a convenience for angular to associate the evaluation of the
// element attribute 'title' to the $scope.title
scope: {
title: '='
},
template: '<span ng-bind="title"></span>'
};
}
For the board, you can include your custom $compiled template, this might not work exactly but I hope it gives you a good starting point.
function boardDirective() {
return {
restrict: 'E',
scope: {
type: '='
},
controller: BoardController,
controllerAs: 'boardCtrl'
};
}
class BoardController {
constructor($scope, $element, $compile, $templateRequest) {
var ctrl = this;
ctrl.$scope = $scope;
ctrl.$element = $element;
ctrl.$compile = $compile;
ctrl.$templateRequest = $templateRequest;
// check for changes to the "type", because in this example the
// type is coming from the gameRepository, so this directive could
// be initialized before a request with the game type comes
// from the server.
$scope.$watch('type', function (newType) {
if (!newType) return;
ctrl._loadBoard(newType);
});
}
_loadBoard(type) {
var ctrl = this;
// manually request the template
ctrl.$templateRequest('/templates/board/' + type + '.html')
.then(function (template) {
// manually compile the template
var boardTpl = ctrl.$compile(template):
// manually add the link of the template with a scope to
// the directive element
ctrl.$element.append(boardTpl(ctrl.$scope));
});
}
}

Angular-ui + D3: how to implement contextual menu (popover vs modal)?

Given the following use-case:
I use D3js to render objects which are managed by AngularJS. I would like to add interactivity to the D3 chart. When clicking on a svg element I would like to have a kind of popup menu allowing to modify the object properties. These properties are required by AngularJS but are not rendered by D3.
The D3-Angular integration is derived from http://bl.ocks.org/biovisualize/5372077 which uses a closure.
Current implementation:
As of today I am using the $modal service from angular-ui bootstrap to create the popup menu. From a functionnality point of view it works pretty well:
When clicking on a svg element, D3 dispatches an event
That event is catched by Angular which calls the $modal service
Within the modal window I modify the object properties
However I am not satisfied with the rendering. I would like the popup menu to look like a popover. It should be placed close to the svg element which was clicked.
As far as I understand, I have two options:
Continue to use the $modal service and modify its look. What approach should be taken? Using the windowClass option?
Stop using the $modal service and start hacking on the popover directive. The problem is that I do not think it is possible
to add such a directive to an svg element. The solution would be to
create a popover service close to the $modal service.
Which option should be chosen? and how to implement it?
EDIT:
Working plunker using a custom my-popover directive:
http://plnkr.co/edit/5KYvxi?p=preview
It is possible to add a directives to code generated by d3, only thing you need to ensure is that you call the $compile service on the content after it has been rendered.
For the given example, it would look something like this:
.directive('barChart', function($compile){ // inject $compile
var chart = d3.custom.barChart();
return {
restrict: 'E',
replace: true,
template: '<div class="chart"></div>',
scope:{
height: '=height',
data: '=data',
hovered: '&hovered'
},
link: function(scope, element, attrs) {
var chartEl = d3.select(element[0]);
chart.on('customHover', function(d, i){
scope.hovered({args:d});
});
scope.$watch('data', function (newVal, oldVal) {
chartEl.datum(newVal).call(chart);
$compile(element.contents())(scope); // <-- call to $compile
});
scope.$watch('height', function(d, i){
chartEl.call(chart.height(scope.height));
$compile(element.contents())(scope); // <-- call to $compile
})
}
}
And in the d3's drawing function:
bars.enter().append('rect')
.classed('bar', true)
.attr('myPopover', 'Text to show') // <-- Adding an attribute here.
.attr({x: chartW,
width: barW,
y: function(d, i) { return y1(d); },
height: function(d, i) { return chartH - y1(d); }
})
.on('mouseover', dispatch.customHover);
Demo

Algorithm to generate Metro UI style Tiles based on Menu Items?

I am making an Angular App. I want the main menu to look like the tiles in Windows 8.
Now I want an algorithm that will automatically generate a pattern for tiles where some tiles are bigger than others.
So basically I would like to create a component to whichI will provide a dataSource as an array of Json objects containg menu-items.
and then the component should layout tiles in a fashion that it accommodates all of them.
How should I go about it?
I am learning angularjs. Can I avoid using jQuery plugins and do without it ?
I'm not sure you can handle this without an external JS library (mansory, isotope). But there is another trap you may step into. Angular is going to populate your DOM with promises, not tiles. Trouble is that Mansory/Isotope is trying to (re)calculate the new layout without having all data (width, height) necessary. You should use $timeout to sync Angular scope with external library.
isotopeApp.directive('isotope', function ($timeout) {
return {
link: function (scope, element, attrs) {
scope.$on('postAdded', function () {
$timeout(function () {
element.isotope('reloadItems').isotope({
sortBy: 'original-order'
});
})
})
}
};
});
http://jsfiddle.net/8Qmry/
Coming late to the party here, but if you don't want the DOM littered with a whole bunch of unnecessary digest cycles and spped up the reactiveness of the effect, adding 'false' to the timeout stops the digest cycles from going nuts and with larger datasets, speeds things up exponentially:
isotopeApp.directive('isotope', function ($timeout) {
return {
link: function (scope, element, attrs) {
scope.$on('postAdded', function () {
$timeout(function () {
element.isotope('reloadItems').isotope({
sortBy: 'original-order'
});
}, false)
})
}
};
});
Not changing much but a big performance boost.

Loading with ngInclude into multiple DOM locations

I'm building an app in AngularJS where the user creates some app state, then there are a number of different ways to render it. (For a lame example, let's say they're going to enter three numbers, and then they can choose to render them into a line chart or a pie chart.)
Each rendering type lives in a different HTML file, and I'm letting the user choose a renderer with a <select> which drives the src for an ng-include to load the different renderers. This works fine, so far.
Now, some rendering modes have additional controls; for example, the pie chart might have a "3D" checkbox. Different renderers will have entirely distinct controls. So I want these included files to also create controls for customizing their presentation.
Here's my question: how do I load a single file and let it create its additional control in one spot in the DOM while putting its main content into another spot in the DOM? The additional control cannot appear adjacent to the rendered content in the DOM: it goes in an entirely different location.
Here is an example: http://plnkr.co/edit/1RXVVu?p=preview. I would like, in a.html and b.html, to be able to instantiate their popup controls in the DOM above the <hr>, while having their textual content below the <hr>. (While it would be possible in the example to just put an hr tag in each of a.html and b.html, the real DOM structure is much more complex and doesn't afford that.)
Thanks!
I created a directive to move the element to another container. Here is what I changed from your plunker.
I added jQuery and the directive:
<script type="text/javascript">
(function() {
var app = angular.module('myapp', []);
app.directive('myContainer', [function() {
return {
restrict:'A',
link: function(scope, elem, attrs) {
angular.element(attrs.myContainer).replaceWith(elem);
//angular.element(attrs.myContainer).html(elem);
//angular.element(attrs.myContainer).appendTo(elem);
//etc...
}
};
}]);
})();
</script>
I added a container above the hr:
<div id="#container"></div>
I specified the directive attribute with the container id in a.html and b.html:
<select ng-model="color" ng-options="c for c in ['red', 'blue']" data-my-container="#container"></select>
The container id (or selector) could come from the model dynamically.
I worked the Plunker into a complete solution here: http://tdierks.github.io/angular-element-mover/, code is here: https://github.com/tdierks/angular-element-mover.
The critical directive is:
(function() {
var app = angular.module('controlMover', []);
app.directive('moveTo', [function() {
return {
restrict: 'A',
link: function(scope, elem, attrs) {
elem.appendTo(angular.element(attrs.moveTo));
scope.$on("$destroy", function() {
elem.remove();
});
}
};
}]);
})();
One note: I had to listen for $destroy events on the scope to remove the moved DOM element when the pane changes. I haven't yet checked to be sure there's no leakage here, but it's a concern.
Random other things I ran into, for the education of others:
JQuery needs to be loaded before Angular.
This app extension with the directive needs to be named on the ng-app directive in the HTML.

Resources