1

I have a div that will need to host a chart or table depending on an XHR response. In the chart case, I need the div contents to be replaced by a canvas element that chart.js uses to display a graph.

If I add the canvas element to the HTML code, angular-chart.js renders a graph. However, If I inject the canvas into a div using javascript, the canvas element will be in the dom but the chart will not be displayed.

How do I work around this?

HTML

<div ng-controller="ChartCtrl">
  <div>
    {{chart.name}}
    This works (doughnut):
    <canvas id="chart-{{$index}}" class="chart chart-doughnut" chart-data="chart.data" chart-labels="chart.labels"></canvas>
  </div>
  This Doesn't work ({{chart.type}}):
  <div id="chartDiv"> </div>
</div>

Javascript

var myApp = angular.module('myApp', ['chart.js']);

// Create the controller, the 'ToddlerCtrl' parameter 
// must match an ng-controller directive
myApp.controller('ChartCtrl', function ($scope) {
   var chart_div = $('#chartDiv');
   chart_div.empty();
   canvas_html = '<canvas id="chart-2" class="chart chart-doughnut" chart-data="chart.data" chart-labels="chart.labels"></canvas>';
   console.log(canvas_html)
   chart_div.append(canvas_html);
   //console.log(chart_div)
  $scope.chart =    {
     name: 'Chart 1',
     type: 'Doughnut',
     labels: ['EVSC', 'ISB'],
     data: [13, 44]
   };
});

Plunker Example: http://plnkr.co/edit/9JkANojIl8EXRj3hJNVH?p=preview

2 Answers 2

1
+50

You're altering the structure of the DOM, you can't append your newly created element like that in a "vanilla" or "jQuery" way.

You need to compile your HTML string or DOM element into a template to produce a template function that will be linked to the scope. The process will walk the DOM tree and match DOM elements to directives.

// Instantiate the app, the 'myApp' parameter must match what is in ng-app
var myApp = angular.module('myApp', ['chart.js']);

// Create the controller, the 'ToddlerCtrl' parameter must match an ng-controller directive
myApp.controller('ChartCtrl', function ($scope, $compile) {
  canvas_html = '<canvas id="chart-2" class="chart chart-doughnut" chart-data="chart.data" chart-labels="chart.labels"></canvas>';

  var element = angular.element(canvas_html);
  $compile(element)($scope);
  angular.element('#chartDiv').append(element);

  $scope.chart =    {
     name: 'Chart 1',
     type: 'Doughnut',
     labels: ['EVSC', 'ISB'],
     data: [13, 44]
   };
});

You can check out the result in this Plunker example.

Sign up to request clarification or add additional context in comments.

1 Comment

Super! I'll need to digest this. Thanks for the quick answer!
1

@HiDeo's answer is spot on, but here is a simplified Directive to do the same thing (notice the $compile still). The chartData variable attached to the scope is the presets for Chartjs' charts, any will work - the only thing of note is the $watch that I have added to the update property: if you are going to be updating your charts I would suggest doing something like this just to keep your hands clean.

.directive('replaceWithChart',function($compile){
    return {
        restrict: 'A',
        scope: {
            chartData: '=replaceWithChart',
            height: '=',
            width: '='
        },
        compile: function(element){
            var canvasModel = angular.element('<canvas></canvas>');
            return {
                post: function postLink(scope,elem,attr){
                    elem.empty();
                    var canvas = canvasModel.clone();
                    elem.append($compile(canvas)(scope));

                    !scope.height && (scope.height = elem[0].style.height || (elem[0].offsetHeight || elem[0].clientHeight));
                    !scope.width && (scope.width = elem[0].style.width || (elem[0].offsetWidth || elem[0].clientWidth));

                    var chart = new Chart(canvas[0].getContext('2d'),scope.chartData);
                    canvas.attr('height',(canvas[0].style.height = scope.height + 'px'));
                    canvas.attr('width',(canvas[0].style.width = scope.width + 'px'));

                    scope.$watch('chartData.update',function(){
                        scope.chartData.update && !(scope.chartData.update = false) && chart.update();
                    });
                    elem.on('$destroy',chart.destroy.bind(chart));
                    // Fixed some rendering issue, but I can't remember which
                    window.setTimeout(function(){
                        var padding = elem[0].style.padding || (window.getComputedStyle(elem[0],null).getPropertyValue('padding'));
                        elem[0].style.padding = '0';
                        elem[0].style.padding = padding;
                        chart.resize();
                    },300); // Magic number - I found this worked best
                }
            };
        }
    };
})

Comments

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.