Angularjs, $compile, and redrawing charts.

9 posts, 2 answers
  1. Jacob
    Jacob avatar
    6 posts
    Member since:
    Oct 2012

    Posted 23 Feb 2015 Link to this post

    I'm developing a dashboard system that uses multiple kendo charts inside of cards or tiles.  Columns in the dashboard are a fixed size and the cards may span multiple columns.  The dashboard should reflow the card layout when the size of the window changes, and when a card is too wide for the dashboard, its column span should be reduced and the chart rescaled to fit the new dimensions of it's container.  Here's the wrinkle.  Each chart's html and javascript are stored in separate html files.  When the dashboard is rendered,  the html containing the angularjs template and the supporting javascript is inserted into the containing div and then compiled using the $compile service.  So far I have had no trouble referencing scope contents inside the compiled html, but when I try to set up a name reference for the chart so that I can redraw it once the layout has changed, that reference does not exist on my scope.  I can make this work in a simple example, but not in the dashboard structure I've set up  Here are some excerpts from my code.

    Dashboard template (with some Razor markup mixed in):

    <div class="@divClass" ng-controller="@controllerName" tsv-resize-dash>
        <div ng-repeat="card in currentLayout.cards"
             class="{{cardClass}} card"
             ng-style="{top: card.top, left: card.left, width: card.width, height: card.height}">

            <i class="fa fa-info info" ng-class="card.infoOpacity == 1.0 ? 'pressed' : ''" ng-show="card.template.length > 0" ng-click="card.toggleInfo()"></i>

            <div class="title" ng-bind="card.title"></div>

            <div class="content">
                <div class="card-body" ng-style="{opacity: card.contentOpacity}" dynamic-html="card.template"></div>
                <div class="infoContent" ng-style="{opacity: card.infoOpacity}" ng-bind="card.cardInfo"></div>
            </div>
        </div>
    </div>

    Directive that inserts and compiles the template html:

    .directive('dynamicHtml', function ($compile) {
            return {
                restrict: 'A',
                link: function ($scope, $element, $attrs) {
                    $scope.$watch($attrs.dynamicHtml, function (html) {
                        $element.html(html);
                        $compile($element.contents())($scope);
                    });
                }
            };
        })

    Directive for resizing the dashboard:

    .directive('tsvResizeDash', ['$window', function ($window) {
            return {
                link: function ($scope, $element, $attrs) {
                    angular.element($window).bind('resize', function () {
                        var nColumns = Math.max(Math.floor($element.width() / $scope.currentLayout.CARD_WIDTH), 1)
                        console.log(nColumns);
                        if ($scope.currentLayout.currentNumberOfColumns != nColumns) {
                            console.log('resize!');
                            $scope.currentLayout.currentNumberOfColumns = nColumns;
                            $scope.currentLayout.reflow();
                            $($scope.currentLayout.cards).each(function () {
                                if (this.redraw)
                                    this.redraw($scope);
                            });
                            $scope.$apply();
                        }
                    });
                }
            }

        }]);

    Template example (The html is inserted and compiled; The javascript is simply rendered to the page.):

    <script type="text/html">
        <div kendo-chart="myChart"
             k-theme="'metro'"
             k-series-defaults="{
                type: 'area',
                area: {
                    line: { style: 'smooth' }
                },
        missingValues: 'interpolate'
             }"
             k-data-source="card.dataSource"
             k-series="card.model.series"
             k-legend="{
                 position: 'bottom',
                 labels: {
                    font: '11px Droid Sans'
                 }
             }" 
             k-category-axis="{ baseUnit: 'fit' }"
             ng-style="{
                width: card.width - card.padding.horizontal,
                height: card.height - card.padding.vertical
             }">
        </div>
    </script>

    <script type="text/javascript" cardtype="AreaChart">
    //AreaChart extends the Card object which all cards share.   
    var AreaChart = Card.extend({

            load: function () {
                //call the load() of the super class using a promise to ensure the correct order of operations for the async methods
                return this._super().done(function () {
                    this.dataSource = new kendo.data.DataSource({
                        //model is a member of the super class
                        data: this.model.data
                    });
                    console.log(this.model);
                });
            },

            redraw: function ($scope) {
                //undefined
                console.log($scope.myChart);
            }
        })
    </script>

    I would expect that when I get to the redraw function just above, that I would have a reference to 'myChart' inside $scope, but that isn't the case.  I've also tried referencing it inside the controller, and it does not exist at that point, either.

    So my question is: Am I doing something wrong, here, or have I found some sort of bug?

    I realize that's quite a bit to digest.  Let me know if further explanation is needed.

    Thanks!
  2. Answer
    Petyo
    Admin
    Petyo avatar
    2444 posts

    Posted 25 Feb 2015 Link to this post

    Hi Jacob,

    currently the Kendo UI directives instantiate the widgets asynchronously, which causes various inconveniences - the widgets themselves are not immediately available in the scope in certain cases. I believe that your issue is caused by the same thing.

    We are planning to fix this in future releases, but the current workaround we are suggesting is to use the widget created events.  

    Regards,
    Petyo
    Telerik
     
    Join us on our journey to create the world's most complete HTML 5 UI Framework - download Kendo UI now!
     
  3. Jacob
    Jacob avatar
    6 posts
    Member since:
    Oct 2012

    Posted 25 Feb 2015 in reply to Petyo Link to this post

    Thanks, Petyo.  I'll definitely look into that.
  4. Jacob
    Jacob avatar
    6 posts
    Member since:
    Oct 2012

    Posted 26 Feb 2015 in reply to Petyo Link to this post

    I did manage to come up with a work around using the kendoWidgetCreated event which basically consists of buffering a reference to each widget manually to the $scope as they are created. Then inside my directive, I iterate over those and redraw them.  That's less than optimal, but acceptable.  Thanks for the solution.

    I have run into a new issue though.  In my dashboard, donut charts scale their height with their width so that they maintain the same aspect ratio.  As a result, the hole size is calculated in a function based on the width of the chart.  The issue I'm seeing is that even though the function that returns the hole size returns the correct value right before the resize call, the redrawn chart does not honor that new value.  It only uses the initial value, until I refresh the page.  Is there something I'm missing, here?

    The altered directive for resizing the dashboard is below:

    .directive('tsvResizeDash', ['$window', function ($window) {
            return {
                link: function ($scope, $element, $attrs) {
                    angular.element($window).bind('resize', function () {
                        var nColumns = Math.max(Math.floor($element.width() / $scope.currentLayout.CARD_WIDTH), 1)
                        if ($scope.currentLayout.currentNumberOfColumns != nColumns) {
                            console.log('resize!');
                            $scope.currentLayout.currentNumberOfColumns = nColumns;
                            $scope.currentLayout.reflow();
                            $scope.$apply();

                            //Now resize all the kendo charts to fit their (possibly) resized containers
                            $($scope.$widgets).each(function() {
                                if(this.$angular_scope.card.holeSize)
                                    //Hole size for single column: 96.1125
                                    //Hole size for double: 212.025
                                    //Only the initial value is ever used
                                    console.log(this.$angular_scope.card.holeSize());
                                this.redraw();
                            });
                        }
                    });
                }
            }
        }]);
  5. Petyo
    Admin
    Petyo avatar
    2444 posts

    Posted 02 Mar 2015 Link to this post

    Hello Jacob,

    the chart resize method (which, is in fact common for all Kendo UI widgets) should resolve that - please give it a try.

    Regards,
    Petyo
    Telerik
     
    Join us on our journey to create the world's most complete HTML 5 UI Framework - download Kendo UI now!
     
  6. Jacob
    Jacob avatar
    6 posts
    Member since:
    Oct 2012

    Posted 02 Mar 2015 Link to this post

    I've tried redraw, resize, and refresh.  None of them solve this problem.

    Here is the template for the donut chart I'm using:

    <script type="text/html">
        <div kendo-chart
             k-data-source="card.dataSource"
             k-series="[{
                            field: 'value',
                            categoryField: 'category',
                            padding: 0
                        }]"
             k-series-defaults="{
                type: 'donut',
                startAngle: 90,
                holeSize: card.holeSize()
             }"
             k-options="card.model.chartOptions"
             ng-style="{
                width: card.width - card.padding.horizontal,
                height: card.height - card.padding.vertical
             }"
             t-donut-legend-position="center">
        </div>
    </script>

    <script type="text/javascript" cardtype="DonutChart">
        
        var DonutChart = Card.extend({

            load: function () {
                return this._super().done(function () {
                    this.dataSource = new kendo.data.DataSource({
                        data: this.model.data
                    });

                    this.model.chartOptions = {
                        theme: 'metro',
                        title: {
                            visible: false
                        },
                        legend: {
                            visible: true,
                            position: 'custom'
                        },
                        chartArea: {
                            background: "transparent",
                        },
                        series: [{
                            padding: 0,
                            data: data
                        }],
                        tooltip: {
                            visible: true,
                            backgroundcolor: '#D9D9D9',
                            padding: '6px'
                        }
                    }
                });
            },

            holeSize: function () {
                return ((this.width - this.padding.horizontal) / 2) * 0.825;
            }
        });
    </script>

    As you can see, the function holeSize() uses the dimensions of the card to calculate the proper size.  I can verify that it returns the correct value after the chart has been resized, but it doesn't get re-evaluated when $scope.$apply() is called.  Why is that?

    My current resize directive:

    .directive('tsvResizeDash', ['$window', function ($window) {
            return {
                link: function ($scope, $element, $attrs) {
                    angular.element($window).bind('resize', function () {
                        var nColumns = Math.max(Math.floor($element.width() / $scope.currentLayout.CARD_WIDTH), 1)
                        if ($scope.currentLayout.currentNumberOfColumns != nColumns) {
                            console.log('resize!');
                            $scope.currentLayout.currentNumberOfColumns = nColumns;
                            $scope.currentLayout.reflow();
                            $scope.$apply();

                            //Now resize all the kendo charts to fit their (possibly) resized containers
                            $($scope.$widgets).each(function() {
                                if(this.$angular_scope.card.redrawChart)
                                {
                                    this.resize();
                                    //this.redraw();
                                    //this.refresh();
                                }
                            });                        
                            $scope.$apply();
                        }
                    });
                }
            }
        }])

    I've attached two screen shots to illustrate the issue.
  7. Answer
    T. Tsonev
    Admin
    T. Tsonev avatar
    2817 posts

    Posted 04 Mar 2015 Link to this post

    Hi,

    Thanks for the additional information. I think I see what's going on.

    The redraw call on itself will not re-evaluate the holeSize function. What the chart sees is a simple number. It does not know where it came from.
    If we supported a syntax like "holeSize: card.holeSize" then it will have a reference to the function and will know how to evaluate it.

    So we need to do something like:
     $($scope.$widgets).each(function() {
       if(this.$angular_scope.card.redrawChart)
       {
         this.options.series[0].holeSize = his.$angular_scope.card.holeSize(); // I guess?
         this.resize();
       }
     }); 


    I hope this helps.

    Regards,
    T. Tsonev
    Telerik
     
    Join us on our journey to create the world's most complete HTML 5 UI Framework - download Kendo UI now!
     
  8. Jacob
    Jacob avatar
    6 posts
    Member since:
    Oct 2012

    Posted 04 Mar 2015 in reply to T. Tsonev Link to this post

    Thank you for the reply, T. Tsonev.

    I have tried the work-around that you posted, and it does work.  I have modified my directive as follows:

    .directive('tResizeDash', ['$window', function ($window) {
            return {
                link: function ($scope, $element, $attrs) {
                    angular.element($window).bind('resize', function () {
                        var nColumns = Math.max(Math.floor($element.width() / $scope.currentLayout.CARD_WIDTH), 1)
                        if ($scope.currentLayout.currentNumberOfColumns != nColumns) {
                            console.log('resize!');
                            $scope.currentLayout.currentNumberOfColumns = nColumns;
                            $scope.currentLayout.reflow();
                            $scope.$apply();

                            //Now resize all the kendo charts to fit their (possibly) resized containers
                            $($scope.$widgets).each(function() {
                                var card = this.$angular_scope.card;
                                if(card.redrawChart)
                                {
                                    if(card.holeSize)
                                    {
                                        this.options.series[0].holeSize = card.holeSize();
                                        this.resize();
                                    }

                                    this.redraw();
                                }
                            });
                            $scope.$apply();
                        }
                    });
                }
            }

    I'm not terribly pleased with this solution, as it seems very counter-intuitive to angular conventions.  However, I am definitely thankful to have a working solution.

    Thanks!
  9. T. Tsonev
    Admin
    T. Tsonev avatar
    2817 posts

    Posted 06 Mar 2015 Link to this post

    Hello,

    I'm glad that you've got this working for the moment.

    What we want ideally is to support functions in as many places in the chart configuration as possible.
    In addition to the great expressiveness this provides opportunity to defer evaluation or do dynamic updates.

    It will play really nice with Angular as well.

    Regards,
    T. Tsonev
    Telerik
     
    Join us on our journey to create the world's most complete HTML 5 UI Framework - download Kendo UI now!
     
Back to Top