I am writing a d3.js-based real time chart directive. The structure looks like this:
myDirective.js:
app.directive('myDirective', function(socketio) {
return {
restrict: 'EA',
templateUrl: '../../views/partials/chart.html',
controller: DataController,
link: function(scope, elem, attrs, ctrl) {
scope.Watch = scope.$watch(function() {
return ctrl.data;
}, function(newVal) {
// d3 code part
});
socketio.on(event, function(newdata) {
ctrl.data.push(newdata);
// redraw chart
});
}
};
});
In the above d3 code part, I refer to http://bl.ocks.org/gniemetz/4618602. The main code is almost the same and the chart is displayed well.
Now I want to use socket.io to update the chart via code
socketio.on(event, function(newdata) {
ctrl.data.push(newdata);
// redraw chart
});
but do not know how to redraw the chart efficiently with the updated 'ctrl.data'. I know in Morris.js, we could do this by '.setData(ctrl.data)' method, but do not know how to update in d3. Any idea?
ps: I tried to copy/paste the d3 code above to this place, but there was always an error said: "TypeError: t.slice is not a function"
Thanks,
The quick and dirty way: every time you have an update, empty the <div> and redraw the entire graph.
The more elegant way: write your own update function to add new data. This is by far the most visually appealing way, since the graph will actually be animated. D3 is well built for this sort of thing, so its by no means a stretch of its capabilities. This will take a bit longer, but usually provides a much more pleasing experience, as seen on some of the graphs in D3's gallery (examples: http://square.github.io/crossfilter/ , http://bl.ocks.org/NPashaP/96447623ef4d342ee09b) Here's how I would do it using your desired graph:
Keep your code the same
Add an update function, say named Update(), that's called in the socketio.on(...) section where you commented // redraw chart
Update() will then redefine all D3 variables and animate the change
Update() will essentially do the above bullet by performing a subset of the steps used in creating the graph from scratch. Here's an overview of what it'll do: rescale both the x and y axes, graphically update the axes, translate the original points and lines to their new positions on new axes, add new points, and add any lines it may need to complete the graph
I'm working on a jsFiddle for you with a working Update to demo the above and, hopefully, implementing it in your code will be simple. I'll edit this post when its done, but I wanted to give you the "quick" answer while I work on it in an attempt to help you meanwhile. If you want some reading meanwhile, check out
http://blog.visual.ly/creating-animations-and-transitions-with-d3-js/
UPDATE
https://jsfiddle.net/npzjLng9/2/. I took the example you provided and had to modify the data to be loaded to be local, not from a text file; however, it is in the same format, and also a lot less entries for readability. Other than that, I did not make any changes to the example. Here's what I added: scroll down and find the last function, Update. Here is where the update and animation happens. Notice the flow of the function:
"update" the data as if your socket.io was the one who received it and then appended it to the dataset.
Redefine the axes
Redefine the points and paths
Specify a transition duration (feel free to play with whatever number suits you)
Actually update and animate changes on the plot
I added a button to simulate a socket.io receiving event.
For your application, ignore the data.push and console.log in the Update() function and I think that's all you'd need -- other than pointing the data to your ctrl.data array and running the Update() function in your socketio.on(...), of course.
The same basic outline applies to animating/updating most graphs.
I hope this helps you!
Related
I have started a scaled down, proof-of-concept floor plan application for my employer that is using an SVG floor plan drawing generated by Adobe Illustrator CC, HTML, CSS and AngularJS.
The idea is to have a floor plan that contains static cubicle/office numbers and to marry that floor plan with a CSV file with personnel data that updates periodically and make the information available on our intranet.
Although I am new at AngularJS/SVG, I was following an example for a map of the US and have made some pretty good progress so far, however am now at a point where I need to map the CSV data (personnel) to the SVG (cubicles) and am not sure how to proceed.
From the example in the link above, the author appears to have injected an "sg-click" directive into the SVG DOM with Angular.$compile that fires a JS Alert() of the ID of the SVG DOM when clicked:
link: function (scope, element, attrs) {
scope.elementId = element.attr("id");
scope.regionClick = function () {
alert(scope.elementId);
};
element.attr("ng-click", "regionClick()");
element.removeAttr("region");
$compile(element)(scope);
}
I am wondering if the same thing is possible with the CSV data, IE:
Outer loop over the SVG (Cubicle ID)
Inner loop over the CSV (Personnel/Cubicle ID) data
If Cubicle ID matches Personnel ID, inject the CSV data into that SVG node.
Since I am having some difficulty including the functional code here due to tag and external file restrictions, I have created a Plunk, where you can view everything in context along with external files that is operational in its current state.
Thanks.
After talking with a colleague, he suggested that I:
Convert the CSV list into an array.
Load the array into some kind of container.
When a user clicks on a cubicle.
Iterate through the array and see if there's a match based on the cubicle ID.
Since this my first attempt at Angular, my implementation may well be incorrect, but I've created a new plunk to reflect the latest code changes.
...code in plunker.
Thanks.
Plunk
I am new to angularjs, trying to create my first directive. I am creating a directive to load Charts.js2.0(beta) into my application.
I have 2 views managed by angular-route, both html view has ng-included a html page that contains only charts-element.
The problem is the first page properly draws the chart, when i go to other view the charts div is loaded but charts is not re-drawn. And now if i go back to first view its blank.
Link to Plunker
What i am doing wrong? Is there any issue with my directive?
Thanks in advance.
There appears to be an issue with the Charts library modifying the existing object on the root scope, and thereby ignoring it forever afterward. I can't really trace down what is doing it, but here's a fix for you: http://plnkr.co/edit/jDQFV62FSeXAQJ6o7jE8
Here is what you had
scope.$watch('config', function(newVal) {
if(angular.isDefined(newVal)) {
if(charts) {
charts.destroy();
}
var ctx = element[0].getContext("2d");
charts = new Chart(ctx, scope.config);
//scope.$emit('create', charts);
}
});
Above, you can see that you're passing scope.config directly into the charts method. That appears to be modifying the data somehow, and since that's passed by reference, you're actually modifying $rootScope.sales.charts. If you copy that object and use it locally like below, you don't have that problem.
Here's how I fixed it.
scope.$watch('config', function(newVal) {
var config = angular.copy(scope.config);
if(angular.isDefined(newVal)) {
if(charts) {
charts.destroy();
}
var ctx = element[0].getContext("2d");
charts = new Chart(ctx, config);
//scope.$emit('create', charts);
}
});
You can see that instead of passing that object directly in, we use angular to make a copy (angular.copy()), and that's the object we pass in.
I think it has relation with the id of the canvas where you are drawing. I've had this problem too amd it was because i was using the same id for the canvas of two graphs in different views. Be sure that those ids are different and that the javasrcipt of each graph is in the controller of each view or in each view itself.
Taking a look at your pluker I see that you are using the same html for the graph and I guess that when angular moves from one of your views to the other thinks that the graph is already drawn. Differentiating two graphs will solve the problem. I don't know of there is any other approach that allows using the same html for the canvas of the graph.
Hope it helps you solve it
I am currently building a simple drag & drop directive so that I can move some SVG stuff around on the screen. At the moment I'm still in the early stages, but I have run into a strange issue with $watch that I'm hoping someone can help me with.
I have a service that maintains my mouse state. At the moment it's just the x and y coordinates of the cursor. I also have an attribute level directive that interacts with this service in order to bind to the mouse-move event and update the service whenever someone moves the mouse around. These two items work together like a champ. The directive keeps the service up to date with the mouse's position and since my service (Factory really) is a singleton, I can pull this data in to other directives/controllers to see what's going on with the mouse.
Here's the problem: I'm trying to allow a specific SVG element to be dragged around, so I created a super simple controller with two functions: a "trackDrag" function that begins tracking and moving a specific element, and a "releaseDrag" which stops tracking/moving the element (drops it where it is, basically).
Inside of my trackDrag function, I attempt to use $scope.$watch to watch the mouse service's current x and y coordinates. Since it's a factory, these values are returned in a function and my watch looks something like this:
$scope.$watch("mouseTrackingService.get()", function(){
// do some stuff here
});
This watch DOES fire off when I first start dragging an element but it doesn't fire as I continue dragging it across the screen. In my "releaseDrag" function, I deallocate the watcher and that seems to work correctly. I'm kind of stumped about why I don't see the watch fire off continuously, even though I can console write out inside of the service and I see that IT is updating correctly.
I've included a plnkr with some sample code below:
http://plnkr.co/edit/g3WEgiQWvd9oXCpFEByn?p=preview
If I just give in and use a $interval then this code works (updating the position every 10ms for example), but really I see that as a much less "angular" way of doing things vs binding.
Ugh, I'm just being dumb. I forgot that $scope.$watch can ONLY WATCH SCOPE VARIABLES.
I fixed this issue by adding the following wrapper around the service:
$scope.currentMouse = function(){
return mouseTrackingService.get();
};
I can then watch currentMouse:
$scope.$watch(currentMouse(), function(){
updateMousePosition(target);
console.log("noticed a change");
});
Of course that gives me that awful Digest error after more than like a half second of dragging:
Uncaught Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting!
But that's a different issue entirely.
Sorry folks, nothing to see here. Move along now :-p
Having trouble with always rendering google maps in my Ionic app. When I first land on a view from a list of items on the previous view, the map always renders in its complete state. However, if I go back to the previous view and tap a different business, or even the same one, it appears as if the map is only rendering 25% of the complete map. I'm having this issue on both the emulator and on my iPhone.
Example
Code
getData.getBusinesses()
.then(function(data) {
// get businesses data from getData factory
})
.then(function(data) {
// get businesses photo from getData factory
})
.then(function(data) {
// get some other business stuff
})
.then(function() {
// get reviews for current business from separate async call in reviews factory
})
.then(function() {
// instantiate our map
var map = new GoogleMap($scope.business.name, $scope.business.addr1, $scope.business.city, $scope.business.state, $scope.business.zip, $scope.business.lat, $scope.business.long);
map.initialize();
})
.then(function() {
// okay, hide loading icon and show view now
},
function(err) {
// log an error if something goes wrong
});
What doesn't make sense to me is that I'm using this exact code for a website equivalent of the app, yet the maps fully load in the browser every time. The maps also fully load when I do an ionic serve and test the app in Chrome. I did also try returning the map and initializing it in a following promise, but to no avail.
I've also tried using angular google maps, but the same issue is occurring. I think I might want to refactor my gmaps.js (where I'm creating the Google Maps function) into a directive, but I don't know if that will actually fix anything (seeing as angular google maps had the same rendering issue).
I don't think the full code is necessary, but if you need to see more let me know.
EDIT
It seems that wrapping my map call in a setTimeout for 100ms always renders the map now. So I guess the new question is, what's the angular way of doing this?
I'm seeing similar issues with ng-map in Ionic. I have a map inside of a tab view and upon switching tabs away from the map view and back again, I would often see the poorly rendered and greyed out map as you describe above. Two things that I did that may help fix your issue:
Try using $state.go('yourStateHere', {}, {reload: true}); to get back to your view. The reload: true seemed to help re-render the map properly when the map was within the tab's template.
After wrapping the map in my own directive, I found the same thing happening again and wasn't able to fix it with the first suggestion. To fix it this time, I started with #Fernando's suggestion and added his suggested $ionicView.enter event to my directive's controller. When that didn't work, I instead added a simple ng-if="vm.displayMap" directive to the <ng-map> directive and added the following code to add it to the DOM on controller activation and remove it from the DOM right before leaving the view.
function controller($scope) {
vm.displayMap = true;
$scope.$on('$ionicView.beforeLeave', function(){
vm.displayMap = false;
});
}
Hope that helps.
don't use setTimeout on this!
You need to understand that the map is conflicting with the container size or something (example: map is loading while ionic animation is running, like swiping).
Once you understand this, you need to set map after view is completely rendered.
Try this on your controller:
$scope.$on('$ionicView.enter', function(){
var map = new GoogleMap($scope.business.name,
$scope.business.addr1, $scope.business.city,
$scope.business.state, $scope.business.zip,
$scope.business.lat, $scope.business.long);
map.initialize();
});
In the following Layout, I am adding a CollectionView to display a SELECT list within onRender. Immediately after that, I am using the ui hash to enable or disable all controls within the view. This does not work for the SELECT generated by new App.View.Categories.
Should it? Or does the UI hash not work on Regions within a Layout?
App.View.UploadFile = Backbone.Marionette.Layout.extend({
template: '#upload-file-template',
regions:{
category: 'td:nth-child(4)'
},
ui:{
inputs: 'textarea, select, .save'
},
onRender: function(){
this.category.show(
new App.View.Categories({
collection: App.collection.categories
}) // generates the SELECT list
);
console.log(this.ui.inputs); // Length 2. Missing select.
console.log(this.$('textarea, select, .save')); // Length 3
this.ui.inputs.prop(
'disabled', (this.model.get('upload_status')!='staged')
);
}
});
This should be working the way you expect it to work. The code in question in the Marionette source is here: https://github.com/marionettejs/backbone.marionette/blob/master/src/marionette.itemview.js#L49-L51
The call to bindUIElements() is what converts the ui hash in to jQuery selector objects, and it is called right before the onRender method is called.
Are you seeing errors? Or is the selector simply returning nothing, and having no affect on the elements?
Update:
Ah! Of course... I wasn't paying attention to your code close enough. You're correct in that the UI element selectors happen before you're adding the the sub-view to the region. I've never run in to this situation before... but this seems like something we would want to fix / support.
For now, the best workaround I can suggest would be to call 'this.bindUIElements();' at the very end of your onRender method. This would force the ui elements to re-bind to the selectors.
I'll also add an issue to the github issues list, to look in to a better solution for this. i don't know when i'll be able to get to this, but this will at least get it on the list of things to fix.