Shulhi Sapli Shulhi Sapli - 1 month ago 12
Ajax Question

Highchart angular directive doesn't redraw from dynamic (ajax) data table

I have an AngularJs app that retrieves data from API and populates them into table. Then, I'm using Highcharts to draw chart based on that table. It is working fine if I'm using static data in the table, but it doesn't work when I update the table through AJAX. It doesn't get redraw.

Directive:

app.directive('highChart', function ($parse) {
return {
link: function(scope, element, attrs) {
scope.$watch('chartConfig', function (newVal) {
if (newVal) {
var props = $parse(attrs.highChart)(scope);
props.chart.renderTo = element[0];
new Highcharts.Chart(props);
}
});
}
};
});


Controller:

appControllers.controller('SummaryController', ['$scope', '$timeout', 'Summaries',
function ($scope, $timeout, Summaries) {
// Request from API
Summaries.get({ table: 'level'}).$promise.then(
// success callback
function(data) {
$scope.levels = data.level[0][today];
$scope.chartConfig = {
data: {
table: document.getElementById('daily-level')
},
chart: {
type: 'column'
},
title: {
text: 'Data extracted from a HTML table in the page'
},
yAxis: {
allowDecimals: false,
title: {
text: 'Units'
}
},
tooltip: {
formatter: function () {
return '<b>' + this.series.name + '</b><br/>' +
this.y + ' ' + this.x;
}
}
};
}
}]);


HTML

<div id="top-chart" high-chart="chartConfig"></div>


It just displays an empty chart. My table does get updated by the AJAX request. I even did check for
console.log(props)
inside the directive and
props.data.table
does show the updated table, but somehow the chart doesn't get redraw. I've tried multiple ways to do this, but all come down to the same result. Currently, I'm just using jQuery timeout to refresh the chart, it works but that means I'm using DOM manipulation inside controller. I'm trying to achieve this using directive.

Answer

Ok. I managed to get it to work. I think the problem is that, Highcharts does not aware about the changes or if it knows about the changes, the DOM at that moment hasn't finish rendering. This is the working version of the code.

Directive:

app.directive('highchart', function($parse, $timeout) {
    return {
        restrict: 'E',
        template: '<div></div>',
        replace: true,
        link: function(scope, element, attrs) {
            var config = $parse(attrs.config)(scope);
            $(element[0]).highcharts(config);

            scope.$watch(attrs.watch, function(newVal) {
                if (newVal) {
                    var complete = function (options) {
                        var chart = $(element[0]).highcharts();
                        // capture all available series
                        var allSeries = chart.series;
                        for (var i = 0; i < allSeries.length; i++) {
                            allSeries[i].setData(options.series[i].data, false);
                        }

                        chart.redraw();
                    };

                    // doesn't work without the timeout 
                    $timeout(function() {
                        Highcharts.data({
                            table: config.data.table,
                            complete: complete
                        });   
                    }, 0);
                }
            });
        }
    };
});

In controller, we can setup the config.

    $scope.chartConfig = {
         // config here
    };

To use it:

<highchart config="chartConfig" watch="levels"></highchart>

where the attribute config is bind to the $scope.chartConfig and watch is the trigger for watch.$scope() in the directive. When $scope.levels changes in controller, it would re-render the chart.

I don't actually like the dependency of the directive to the watch attribute that I have right now, but I'm not sure what is the best or other way to do this. If anyone have a better idea, do let me know.

Please note that this will only work to convert table to highchart. For others, you might need to use other angularjs highcharts directive.