3

I have a simple form, where user selects date from and date to, where afterwards the tool automatically retrieves data from a website (it returns a JSON).

Here is how my angular controller looks like:

(function () {
    angular.module("app-machines", ['ngFlatDatepicker'])
        .factory('MachinesService', ['$http', MachinesService])
        .controller('mainController', ['$scope', 'MachinesService', mainController]);

    function mainController($scope, MachinesService) {
        $scope.datepickerConfig_From = {
            allowFuture: true,
            dateFormat: 'DD.MM.YYYY',
            minDate: moment.utc('2015-09-13'),
            maxDate: moment.utc('2015-09-17')
        };

        $scope.datepickerConfig_To = {
            allowFuture: true,
            dateFormat: 'DD.MM.YYYY',
            minDate: moment.utc('2015-09-13'),
            maxDate: moment.utc('2015-09-17')
        };

        $scope.date_from = "14.09.2015";
        $scope.date_to = "15.09.2015";

        $scope.machines = [];
        $scope.errorMessage = "";

        $scope.change = function () {
            MachinesService.getMachines($scope.date_from, $scope.date_to).then(function (response) {
                angular.copy(response.data, $scope.machines);
            }, function (error) {
                $scope.errorMessage = "Failed to load data:" + error;
            });
        };

        $scope.change();
    }

Where in my getMachines I am calling a simple GET request which looks more or less like this (example):

return $http.get("/api/machine/2015-09-14_2015-09-16");

The returned JSON is a array of objects with the following structure (just informational)

  • Machine Name
    • Categories (under specific machine)
      • Days (each category contains a collection of days with data)

I can retrieve the data without a problem now (with a fantastic help from you folks). I am now trying to display a chart for each of my returned machines. This means that on my page I am trying to do something like this:

        <div class="col-md-12" ng-repeat="machine in machines">
            <h1> {{ machine.name }}</h1>

            <div class="col-md-6" ng-repeat="category in machine.categories">
                <h3> {{ category.name }}</h3>

                <div class="col-md-6" ng-repeat="day in category.days">
                    <p>{{day.date | date : 'dd.MM' }}</p>
                </div>
            </div>

        </div>

Here (just simple example) I am looping through the machines and I am displaying category with days. Instead of displaying categoreis (with the days) I simply would like to insert a bar chart with the data.

I have found ChartJs which allows me to do that. Here is my example script which displays a chart on my page:

var data = {
    labels: ["January", "February", "March", "April", "May", "June", "July"],
    datasets: [
        {
            label: "My First dataset",

            // The properties below allow an array to be specified to change the value of the item at the given index
            // String  or array - the bar color
            backgroundColor: "rgba(100,220,220,0.2)",

            // String or array - bar stroke color
            borderColor: "rgba(220,220,220,1)",

            // Number or array - bar border width
            borderWidth: 1,

            // String or array - fill color when hovered
            hoverBackgroundColor: "rgba(220,220,220,0.2)",

            // String or array - border color when hovered
            hoverBorderColor: "rgba(220,220,220,1)",

            // The actual data
            data: [65, 59, 80, 81, 56, 55, 40],

            // String - If specified, binds the dataset to a certain y-axis. If not specified, the first y-axis is used.
            yAxisID: "y-axis-0",
        },
        {
            label: "My Second dataset",
            backgroundColor: "rgba(220,220,220,0.2)",
            borderColor: "rgba(220,220,220,1)",
            borderWidth: 1,
            hoverBackgroundColor: "rgba(220,220,220,0.2)",
            hoverBorderColor: "rgba(220,220,220,1)",
            data: [28, 48, 40, 19, 86, 27, 90]
        }
    ]
};
var options = {
    scales: {
        xAxes: [{
            stacked: true
        }],
        yAxes: [{
            stacked: true
        }]
    }
};

var ctx = document.getElementById("myChart");


var myBarChart = new Chart(ctx, {
    type: 'bar',
    data: data,
    options: options
});

This works like a charm, however only for one chart - because we are targeting the context with document.getElementById("myChart").

The question is - how can I change this to create a chart from my returned data? As a backup solution I came up with pre-designing the page in advance (I know maximum number of machines returned) and simply hide the ones that should not appear....but I know that is not the right approach (it's a back-up plan). I would like to learn how to do it properly.

Any help in this matter would be more than appreciated. I am a AngularJS newbie, therefore your code samples would be more than welcome!

EDIT:

As suggested I have updated my HTML code to the following:

        <div class="col-md-12" ng-repeat="machine in machines">
            <h1> {{ machine.name }}</h1>
            <canvas id="{{'myChart_' + $index}}" width="400" height="400"></canvas>

        </div>

which names those charts without a problem. Then, under my controller I have changed the code to the following:

    $scope.change = function () {
        MachinesService.getMachines($scope.date_from, $scope.date_to).then(function (response) {
            //$scope.machines = response.data;
            angular.copy(response.data, $scope.machines);
        }, function (error) {
            $scope.errorMessage = "Failed to load data:" + error;
        }).finally(function () {

            var data = {..same as above};
            var options = {...same as above};

            //now assign those values to the representative charts
            for (var i = 0; i < $scope.machines.length -1; i++) {
                var ctx = document.getElementById("myChart_" + i);


                var myBarChart = new Chart(ctx, {
                    type: 'bar',
                    data: data,
                    options: options
                });
            }



        });
    };

The problem I have encountered here is, that my charts are rendered after my code executes. This means that I try to find my charts before they are actually created by Angular on my page. I have tried (as you can see) to add .finally clause to my code, but it did not work.

Is there a switch / code that I need to use in order for this solution to work?

EDIT2

I have also tried to add the following parameter $timeout to my controller as following:

.controller('mainController', ['$scope', 'MachinesService', '$timeout', mainController]);

then I made the finally clause an external function like this (inside same controller):

    var changeValues = function () {
        var data = {...same as before};
        var options = {...same as before};

        for (var i = 0; i < $scope.machines.length - 1; i++) {
            var ctx = document.getElementById("myChart_" + i);


            var myBarChart = new Chart(ctx, {
                type: 'bar',
                data: data,
                options: options
            });
        }
    };

and from within the finally clause I called my function like this $timeout(changeValues, 0); but it still does not work. I am quite lost right now. What am I missing?

FINAL:

Here is how I had to edit my code in the end:

angular.module("app-machines", ['ngFlatDatepicker'])
    .factory('MachinesService', ['$http', MachinesService])
    .controller('mainController', ['$scope', 'MachinesService', '$timeout', mainController])
    .directive('onFinishRender', function ($timeout) 
    {
        return {
            restrict: 'A',
            link: function (scope, element, attr) {
                if (scope.$last === true) {
                    $timeout(function () {
                        scope.$emit('ngRepeatFinished');
                    });
                }
            }
        }
    });

1 Answer 1

2

There might be a better answer, but..

You could use an angular loop to create the initial HTML elements.

<div class="col-md-12" ng-repeat="machine in machines">
  <h1> {{ machine.name }}</h1>
  <canvas id="{{'myChart_' + $index}}" width="400" height="400"></canvas>
</div>

Then in your controller pass the elements to the DOM and $broadcast an event to draw the charts.

$scope.change = function () {
        MachinesService.getMachines($scope.date_from, $scope.date_to).then(function (response) {
            angular.copy(response.data, $scope.machines);
            $scope.$broadcast('chartReady'); //broadcast the event
        }, function (error) {
            $scope.errorMessage = "Failed to load data:" + error;
    });
};

Also in your controller handle the broadcasted event. I borrowed and modified this code from here.

directive('drawCharts', ['$timeout', function ($timeout) {
    return {
        link: function ($scope, element, attrs) {
            $scope.$on('chartReady', function () {
                $timeout(function () { // You might need this timeout to be sure its run after DOM render.
                  //get chart elements and draw chart here
                  for (var i = 0; i < $scope.machines.length -1; i++) {
                      var ctx = document.getElementById("myChart_" + i);

                      var myBarChart = new Chart(ctx, {
                        type: 'bar',
                        data: data,
                        options: options
                      });
                  }  
                }, 0, false);
            })
        }
    };
}]);
Sign up to request clarification or add additional context in comments.

4 Comments

So easy! So instead of using machine.name I could use Angular $index property?
As long as the indexes are the same when getting the element then that would work yes. Referring to this portion: document.getElementById("myChart_" + machine.name)
I have encountered an error with your solution. Could you please take a look at my edit?
I think you will have to use $scope.$broadcast. I will modify my answer.. but in the mean time this is what I'm looking at: stackoverflow.com/questions/16935766/… stackoverflow.com/questions/17779745/…

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.