Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

redraw google charts on hidden bootstrap tab pane

I am using google scatter chart in my web app and it loads on document ready. I recently did some restructuring and trying to divide the page content in different bootstrap tabs.

If I load the graph on the active tab pane it work fine and the width, height parameters of the graph is correct. However if I shift the graph to a hidden tab the graph doesn't load properly with the parameters passed on the function (specially width and height). At the moment I am calling the graph on document ready.

Here is my code

HTML

<div class="layout layout-stack-sm layout-main-right">
    <div class="col-sm-3 layout-sidebar">
        <div class="col-sm-12">
            <ul id="myTab" class="nav nav-layout-sidebar nav-stacked">
                <li class="active">
                    <a href="#quick-stats" data-toggle="tab">
                        <i class="fa fa-th"></i>
                        &nbsp;&nbsp;Quick Stats
                    </a>
                </li>
                <li>
                    <a href="#engagement-graph" data-toggle="tab">
                        <i class="fa fa-bar-chart-o"></i>
                        &nbsp;&nbsp;Engagement graph
                    </a>
                </li>
            </ul>
        </div>
    </div>  <!-- layout sidebar -->

    <div class="col-sm-9 layout-main">
        <div class="tab-content stacked-content">
            <div class="tab-pane fade in active" id="quick-stats">
                Some content   
            </div>
            <div class="tab-pane fade" id="engagement-graph">
                <div id="myMoodPage">
                <div class="graph" id="graph">
                </div>
            </div>
        </div>
    </div>
</div>

Loading graph on document ready

<script type="text/javascript" src="https://www.google.com/jsapi">
</script>
<script type="text/javascript">
    google.load("visualization", "1", {packages: ["corechart"]});

    $(document).ready(function () {
        $('#myMoodPage').myMood({
            graphDataUrl: '<?php echo $this->url('my_mood/mood_graph_data'); ?>',
            graphData: <?php echo $graphData; ?>,
            titles: ['<?php echo $this->translate('time'); ?>', '<?php echo $this->translate('mood'); ?>', '<?php echo $this->translate('avg'); ?>']
        });
    });

    function updatePageCharts() {
        $('#myMoodPage').myMood('loadGraph');
    }

    $("a[href='#engagement-graph']").on('shown.bs.tab', function (e) {
        google.load('visualization', '1', {
            packages: ['corechart'],
            callback: drawChart
        });
    });

</script>

This is my main JS file where I am defining the graph parameters and other things (which are not actually relevant for this question, but just for the sake of completeness I am posting the whole function)

(function ($) {
    $.widget("ui.myMood", {
        options: {
            graphDataUrl: "",
            graphData: {},
            titles: ["time", "mood", "avg"]
        },
        _create: function () {
            var self = this;
            this.periodSelect = this.element.find('select[name="period"]');
            this.questionSelect = this.element.find('select[name="question"]');
            this.graphContainer = this.element.find(".statistics");
            this.atStart = this.element.find("#atStart .value");
            this.highest = this.element.find("#highest .value");
            this.lowest = this.element.find("#lowest .value");
            this.current = this.element.find("#current .value");
            this.last30 = this.element.find("#last30 .value");
            this.last30Trend = this.element.find("#last30");
            this.week = this.element.find("#week .value");
            this.weekTrend = this.element.find("#week");
            this.graphId = "graph";
            this.selectedFilterName = this.options.titles[2];
            this.periodSelect.change(function () {
                self.loadGraph()
            });
            this.questionSelect.change(function () {
                self.loadGraph();
                self.selectedFilterName = self.questionSelect.find("option:selected").text()
            });
            this.setGraphData(this.options.graphData);
            return this
        },
        loadGraph: function () {
            var self = this;
            var data = {
                period: self.periodSelect.val(),
                question: self.questionSelect.val()
            };
            var success = function (data) {
                self.setGraphData(data)
            };
            $.ajax({
                type: "POST",
                url: self.options.graphDataUrl,
                data: data,
                success: success
            })
        },
        setGraphData: function (data) {
            var self = this;
            this.atStart.text(data.atStart);
            this.highest.text(data.highest);
            this.lowest.text(data.lowest);
            this.current.text(data.current);
            this.last30.text(data.last30.val);
            this.last30Trend.find(".up, .down").hide();
            this.last30Trend.removeClass("incr");
            this.last30Trend.removeClass("decr");
            if (Number(data.last30.trend) > 0) {
                this.last30Trend.find(".up").show();
                this.last30Trend.addClass("incr")
            }
            if (Number(data.last30.trend) < 0) {
                this.last30Trend.find(".down").show();
                this.last30Trend.addClass("decr")
            }
            this.week.text(data.week.val);
            this.weekTrend.find(".up, .down").hide();
            this.weekTrend.removeClass("incr");
            this.weekTrend.removeClass("decr");
            if (Number(data.week.trend) > 0) {
                this.weekTrend.find(".up").show();
                this.weekTrend.addClass("incr")
            }
            if (Number(data.week.trend) < 0) {
                this.weekTrend.find(".down").show();
                this.weekTrend.addClass("decr")
            }
            var moodData = data.mood;
            var dataArray = new Array();
            for (i in moodData) {
                if (moodData[i].avg == null) {
                    moodData[i].avg = undefined
                }
                if (moodData[i].mood == null) {
                    moodData[i].mood = undefined
                }
                if (moodData[i].team == null) {
                    moodData[i].team = undefined
                }
                var moodText = '<div style="border-radius: 2px; padding: 10px; color: #ffffff; font-weight: bold; background-color: #9c3b8a;">';
                if (moodData[i].event != null) {
                    moodText += moodData[i].event + " "
                }
                moodText += moodData[i].mood;
                if (moodData[i].comment) {
                    moodText += "<br>" + moodData[i].comment
                }
                moodText += "</div>";
                var avgText = '<div style="border-radius: 2px; padding: 10px; color: #ffffff; font-weight: bold; background-color: #15426c;">';
                if (moodData[i].event != null) {
                    avgText += moodData[i].event + " "
                }
                avgText += moodData[i].avg;
                if (moodData[i].publicComment) {
                    avgText += "<br>" + moodData[i].publicComment
                }
                avgText += "</div>";
                var row = [new Date(moodData[i].time * 1000), Number(moodData[i].mood), moodText, Number(moodData[i].avg), avgText, false];
                dataArray.push(row)
            }
            if (google) {
                drawChart()
            } else {
                google.setOnLoadCallback(drawChart)
            }

            function drawChart() {
                var dataTable = new google.visualization.DataTable();
                dataTable.addColumn("date", self.options.titles[0]);
                dataTable.addColumn("number", self.options.titles[1]);
                dataTable.addColumn({
                    type: "string",
                    role: "tooltip",
                    p: {
                        html: true
                    }
                });
                dataTable.addColumn("number", self.selectedFilterName);
                dataTable.addColumn({
                    type: "string",
                    role: "tooltip",
                    p: {
                        html: true
                    }
                });
                dataTable.addColumn({
                    type: "boolean",
                    role: "certainty"
                });
                dataTable.addRows(dataArray);
                var dataView = new google.visualization.DataView(dataTable);
                var chart = new google.visualization.ScatterChart(document.getElementById(self.graphId));
                var options = {
                    legend: {
                        position: "bottom"
                    },
                    interpolateNulls: true,
                    chartArea: {
                        left: 25,
                        top: 25,
                        width: '92%',
                        height: '80%'
                    },
                    backgroundColor: "#ffffff",
                    vAxis: {
                        minValue: 0,
                        maxValue: 6,
                        viewWindow: {
                            min: 0,
                            max: 6
                        }
                    },
                    tooltip: {
                        isHtml: true
                    },
                    series: {
                        0: {
                            color: "#9c3b8a",
                            lineWidth: 2,
                            pointSize: 6
                        },
                        1: {
                            color: "#15426c",
                            lineWidth: 1,
                            pointSize: 3
                        }
                    }
                };
                chart.draw(dataView, options)
            }
        }
    })
})(jQuery);

I tried reloading the graph like this, but it didn't work

$("a[href='#engagement-graph']").on('shown.bs.tab', function (e) {
        google.load('visualization', '1', {
            packages: ['corechart'],
            callback: drawChart
        });
    });  

Screenshot in 2 different situation

Loading on active tab pane enter image description here

Loading on hidden tab pane enter image description here

like image 255
zaq Avatar asked May 29 '14 11:05

zaq


2 Answers

Drawing charts inside hidden divs (which is usually what tab interfaces are implemented as) breaks the dimension detection algorithms in the Visualization API, which is why the chart draws oddly when drawn in any tab other than the tab open at the start.

You can load the API only once; subsequent calls to google.load for the Visualization API will be ignored, which is why your "shown.bs.tab" event handler does not do what you want.

Since delaying the tab initialization until after the charts draw is not really a viable option with Bootstrap, I would suggest replacing this code:

google.load("visualization", "1", {packages: ["corechart"]});

$("a[href='#engagement-graph']").on('shown.bs.tab', function (e) {
    google.load('visualization', '1', {
        packages: ['corechart'],
        callback: drawChart
    });
});

with this:

function init () {
    if (/* boolean expression teting if chart tab is open */) {
        drawChart();
    }
    else {
        $("a[href='#engagement-graph']").one('shown.bs.tab', drawChart);
    }
}

google.load("visualization", "1", {packages: ["corechart"], callback: init});

This should draw the chart if its container tab is open, and create a one-time event handler that draws the chart the first time the tab is opened if the tab is not open.

You need to put this code inside the setGraphData function to avoid a race condition with your data loading, and so the drawChart function will be in scope. Also, remove these lines:

if (google) {
    drawChart()
} else {
    google.setOnLoadCallback(drawChart)
}

The google object exists as soon as the script tag for the jsapi loads, which will be before this code is executed, so you will always trigger the drawChart function here. Having the google object available, however, does not mean that the Visualization API is loaded.

like image 150
asgallant Avatar answered Sep 27 '22 21:09

asgallant


Just incase other people are looking for a simpler solution.

An alternative is to implement the following:

$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
    var ev = document.createEvent('Event');
    ev.initEvent('resize', true, true);
    window.dispatchEvent(ev);
  });

This will cause the chart to re-draw as if the browser was resized.

like image 36
Tikiboy Avatar answered Sep 27 '22 23:09

Tikiboy