I'm using Highcharts with AngularJS. I have a directive which generates the chart. In the link function I specify a default object chartOptions. After setting the default options, I append that object to another object passed to the directive which persists between routes and refreshes, passedObject. Thus any changes to chartOptions after being appended successfully persist. Then the chart generates with the call to Highcharts.chart();
return {
restrict: 'E',
scope: {
passedObject: "="
},
link: function (scope, element) {
var chartOptions = {
title : {
text: 'sample report'
}
...
}
scope.passedObject.chartOptions = scope.passedObject.chartOptions || chartOptions;
var chart = Highcharts.chart(element[0], scope.passedObject.chartOptions);
}
// successfully add new series
// series rendered immediately but does not persist though view changes and page reload.
scope.addSeries = function(seriesObject){
scope.elementObject.chart.addSeries(seriesObject);
};
// successfully pushes object to array
// does not immediately render because Highchart doesn't see change
// but does render after reloading because Highchart checks options again
// Does not render with animation like addSeries() API does.
scope.addSeriesNoAPI = function(seriesObject){
scope.elementObject.chart.series.push(seriesObject);
};
}
There is no issue generating the chart the first time, and if I return to this view, the chart generates again with the same default options.
However, if I use the Highcharts API to programatically add a new data series chart.addSeries(newSeriesObject) the series is added and renders on the chart, but will not persist between routes and refreshes.
If I manually add the series object to my chartOptions via straight JS scope.passedObject.chartOptions.series.push(newSeriesObject) the object is successfully pushed to the array and will persist, but the chart will not automatically update because it doesn't know the series array changed, which is clearly why the API exists.
Upon closer inspection, it appears that Highchart's addSeries() function is pushing the new object to some array other than the one I am persisting, but I can't figure out how to get this to work.
I assumed that this line of code: var chart = Highcharts.chart(element[0], scope.passedObject.chartOptions); would bind the chart to the scope.passedObject.chartOptions object, such that when something like the addSeries() function added a new series, it would be added into scope.passedObject.chartOptions. After logging and watching objects, however that does not appear to be the case.
One way is to hold that data in the root scope (but that is not the best way).
Another is to use angular service to store this data. And include this service where you need it.
An angular service unlike controllers will not get reset when changing views/pages.
I would suggest that you create an angular service that stores the chart options, which has a chart options object and a getter and a setter. You could also hold the functions that make changes to the chartobject in this service.
In your case you seem to do manipulations to the chart object within the controller (after you are getting it from a service perhaps?), if so you will have to set the new object back to the service.
Also when I say service I mean something like this:
app.service('highChartService', function() {
var chartOptions;
var highChartServiceObj = {};
highChartServiceObj.getChartOptions = function() {
return chartOptions;
};
highChartServiceObj.setChartOptions = function (options) {
chartOptions = options;
};
highChartServiceObj.computeChartObject = function () {
//do some computation
}
return highChartServiceObj;
});
Add the above service to your directive and use that to update your highchart object when any changes are made.
Also if I am not wrong what you would like to do is add newseriesobject like this chart.addSeries(newSeriesObject) which solves your highchart dilemma. Why not then update your chart object as the next step: scope.passedObject.chartOptions.series.push(newSeriesObject)? Though I would rather that be part of the service too if I was doing this, all of the highchart manipulations would just be a part of the service, where I would have a function like this in above service:
highChartServiceObj.updateChart(newSeriesObject, chart) {
chartOptions.series.push(newSeriesObject);
chart.addSeries(newSeriesObject);
}
Related
I'm using highmaps to create a vector map that can change based on the data values loaded into the series[0].data array. The initial chart is created with my default values, and inserted into the page using a custom directive. When a select dropdown changes value, I fire off a function using ng-change to update the chart values.
My ng-change function fires and runs as expected, until I attempt to access the series containing the data used to populate highmaps. It seems that after the initial chart is created, the series array is given a value of null. This persists even when I attempt to load in a fresh chartOptions object. I've noticed that when the $scope object containing the chartOptions isn't bound to the directive, then the series is populated with the correct values. The array is only empty when it is bound to the directive.
This only happens with my highmaps. All of the other highcharts on the page have their series arrays visible for editing after being two-way bound to the directive. Also all other elements of my highmap chartOptions object can be viewed and edited, just not the part with the actual data.
My directive
.directive('mapChart', function(){
return {
restrict: "E",
replace: true,
template: "<div id='map' class='div_content-holder'></div>",
scope:{
options:"=",
},
link: function(scope, element){
Highcharts.mapChart(element[0], scope.options);
});
}
};
});
My html
<map-chart options="region_options"></map-chart>
my JavaScript
$scope.region_options = {};
var setRegionMap = function(jsonData){
var chartOptions = selectChart('map'); // grabs my premade options object
// grabs data from somewhere else and adds it to chart options
chartOptions.series[0].data.forEach(function (region, index) {
region["value"] = $scope.region_map.one_month[index];
console.log(chartOptions.series[0].data); //says that series exists
});
$scope.region_options = chartOptions;
console.log($scope.region_options); //says that .series is null, everything else is still there
//$scope.$apply(); //i do this elsewhere in my ajax call
};
Is this an issue with two-way binding the array with highmaps, or angular, or both?
You can change your directive in the following way, adding a new parameter for the current data.
Adding the two watches you will ensure that every time you change data from outside, the chart will be refreshed setting up the correct data and with the first watch on the option you will ensure to instantiate the chart when your options have really been loaded:
.directive('mapChart', function(){
return {
restrict: "E",
replace: true,
template: "<div id='map' class='div_content-holder'></div>",
scope:{
options:"=",
current: "=" // your actual data
},
link: function(scope, element){
var chartexample = {};
scope.$watch('options', function() {
chartexample = Highcharts.mapChart(element[0], scope.options);
});
scope.$watch('current', function(){
chartexample.series[0].setData(scope.current);
});
}
}
});
You can get rid of both the watchers.
About the init, you just have to be sure to init your directive once you have already all your options, so you don't need anymore the watch on the options.
About the current data, you can add a callback parameter & in the directive which will update the values of the chart, and call this callback of the directive inside the function associated to the ngChange of the select inside the controller.
This will make the code better and cleaner.
But for now you can enjoy your chart in this way :)
My best luck for your app!
Cheers :)
I am using external pagination in AngularJS's ui-grid as described here:
http://ui-grid.info/docs/#/tutorial/314_external_pagination
That updates a special field of totalItems when new page is arrived:
var getPage = function() {
// skipped some part
$http.get(url)
.success(function (data) {
$scope.gridOptions.totalItems = 100;
// updating grid object with data
});
};
My only difference to the standard example is that I have gridOptions inside an object that is passed to my directive, that has a grid inside it:
// Line is called right after I receive a page of data from server
$scope.gridObj.gridOptions.totalItems= response.TotalItems;
If I initialize this field before grid is shown on screen (together with configuration) then it remains at the value that I set back then (and obviously, it is not adequate to set of data I receive later, during runtime). But when I alter it later after I call my loading method (that returns current "page" as well as value for totalItems), the change is not picked up by ui-grid. If it is not initialized at startup, again, I could not set this to any value.
How am I to successfully modify the totalItems parameter for the ui-grid during runtime? Can I possibly re-trigger some grid update manually?
I've found that assigning totalItems within a $timeout works nicely for now. This was necessary because ui-grid was updating the value, just not to what I was expecting.
Update:
In my case I need to show all filtered items across all pages, which there is no API for. So for now I've implemented an angular filter, included below. GridRow objects have a visible flag set to true when filters have not filtered them out from the result set.
Example:
angular.filter('renderableRows', function() {
return function(allRows) {
var renderables = [];
angular.forEach(allRows, function(row) {
if(row.visible) {
renderables.push(row);
}
});
return renderables;
};
});
$timeout(function() {
$scope.gridOptions.totalItems = $filter('renderableRows')($scope.gridApi.grid.rows).length;
});
I've an ExtJS 5 ViewModel which contains a store, with a JSON reader using an AJAX proxy.
The URL of the proxy however relies on binding, which substitutes in ViewModel data properties. However it appears that the evaluation of the URL property is done only the first time the ViewModel loads, and not when the data property changes.
This code should illustrate what I'm trying to do, even though its missing parts (extends statements, etc.) for brevity in this post.
I define my view model with a store:
Ext.define('sample.ViewModel',{
extend:'Ext.app.ViewModel',
data:{
departmentId:0,
categoryId:0
}
stores:{
myItems:{
autoLoad:false,
fields:['id','name','price'],
proxy:{
type:'ajax',
url:'/items/department/{departmentId}/category/{categoryId}',
reader:{
rootProperty:'data',
successProperty:'success'
}
}
}
}
}
Then a view with a grid:
(columns, etc. removed for brevity)
Ext.define('sample.View',{
// ....
items:[{
xtype:grid,
bind:'{myItems}'
}]
}
And a controller who changes the view model data properties and reloads the store:
Ext.define('sample.ViewController',{
// ...
handleSomeEvent:function(){
var viewModel = this.getViewModel();
var store = viewModel.getStore('myItems');
viewModel.set('departmentId', 3);
viewModel.set('categoryId', 4);
// Desired behavior
store.load();
// Required workaround
viewModel.bind('/items/department/{departmentId}/category/{categoryId}', function(newUrl){
store.load({url: newUrl})
});
}
});
Without the "required workaround" in the controller logic the store / grid always tries to pull data from /items/department/0/category/0 (i.e. the ViewModel data values at the time that the ViewModel is instantiated).
I'd like to know how to force ExtJS to re-evaluate config properties (i.e. the proxy URL) when the ViewModel data properties change.
Much thanks in advance!
The problem you're having is that the ViewModel ticks on a timer. By the time you call load, it's not evaluated the calls to set. If it didn't, you'd have the binding trigger twice, once when you set the departmentId, then a second time when you set the categoryId.
You can force the ViewModel to tick by calling notify:
viewModel.set('departmentId', 3);
viewModel.set('categoryId', 4);
viewModel.notify();
store.load();
Here is my github gist which describes my problem:
https://gist.github.com/saxena-gaurav/516fb24d2d11d1243adf
In the initialization phase, I am setting the dataPoints array to empty. Upon the success of ajax call, I hard-code the dataPoints array to some values. I added the watcher which then calls the helper function to update the 'layers overlays data' object. Inspite of this, I do not see the data getting rendered on the map.
Any suggestions please to see what I am doing wrong ?
I am using the below link as a reference example:
https://github.com/tombatossals/angular-leaflet-directive/blob/master/examples/heatmap-example.html
Try this after your data updates.
leafletData.getMap().then(function (map) {
map.eachLayer(function (layer) {
if (layer.options.layerName == 'heatmap') {
layer.setLatLngs(newHeatmapData);
}
})
})
In my Controller, I'm quering data from a $resource object bundled in a caching service.
$scope.data = myService.query(); //myService caches the response.
In the same controller I have the configuration for a chart (for the GoogleChartingDirectve).
$scope.chart = {
'type': 'AreaChart',
'cssStyle': 'height:400px; width:600px;',
....
'rows' : convert($scope.data),
...
}
convert is just a function which takes the data from myService and returns an array for the chart.
This is only working, if the response of the query has already been cached (After changing the route, I can see the graph, but if I call the route directly it is empty).
Perhaps the problem is my caching?
angular.module('webApp')
.service('myService', function myService($cacheFactory,res) {
var cache = $cacheFactory('resCache');
return {
query: function() {
var data = cache.get('query');
if (!data) {
data = res.query();
cache.put('query', data);
}
return data;
}
};
});
I've tried the $scope.$watch in my Controller. It is fired only once with no data. If I change to a different route and back again it is fired again; this time with data.
$scope.$watch('data',function(newValue){
console.log("Watch called " + newValue);
}
I'm not sure what the problem actually is.
It looks like the $watch event is missing.
If I call the refresh with a button, $watch is fired and data is shown.
$scope.refresh = function(){
$scope.data = $scope.data.concat([]);
}
I'm just an angular newbie and playing around in a small demo project.
So I'm looking for the best way to solve some common problems.
Any idea how to solve my problem?
I guess the problem is that you are not aware that res.query() is an asynchronous call. The $resource service works as follow: the query call returns immediatly an empty array for your data. If the data are return from the server the array is populated with your data. Your $watch is called if the empty array is assigned to your $scope.data variable. You can solve your problem if you are using the $watchCollection function (http://docs.angularjs.org/api/ng.$rootScope.Scope):
$scope.$watchCollection('data', function(newNames, oldNames) {
var convertedData = convert($scope.data);
...
});
Your cache is working right. If you make your server call later again you got the array with all populated data. Btw. the $ressource service has a cache option (http://docs.angularjs.org/api/ngResource.$resource) - so you don't need to implement your own cache.
You may ask why the $ressource service works in that way - e.g. returning an empty array and populating the data later? It's the angular way. If you would use your data in a ng-repeat the data are presented to the view automatically because ng-repeat watches your collection. I guess you chart is a typical javascript (for example jQuery) that doesn't watch your data.