Robert J.
Robert J.

Reputation: 2701

Create ChartJS Dynamically

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)

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');
                    });
                }
            }
        }
    });

Upvotes: 3

Views: 2546

Answers (1)

fqhv
fqhv

Reputation: 1201

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);
            })
        }
    };
}]);

Upvotes: 2

Related Questions