Ember.js: rendering google chart in template (AKA target DOM element only when present)

ember.js, google-visualization, javascript, model-view-controller

Solution

It is best to place logic related to ui and layout in `views`. So for this task i would suggest to use a `view` or a `component` . As you already realised the `didInsertElement` event is the appropriate place to call this kind of logic, since you need to make sure that the view has been inserted into the DOM and all required elements such as `div#chart` are available.

I'm providing a solution which is a bit rough but gives a starting point.

The idea is to create, a reusable view `GoogleChartView` with an associated template `google-chart` that can be included in any other view. I assumed that dashboard view will eventually include other charts as well that will be using its model to present data in charts, tables etc. This chart related view has been included with the assistance of view helper `{{view "googleChart"}}` . Another important point is that `google.load` needs to be called in head.

http://jsbin.com/oCIcexOB/1/edit

JS

App = Ember.Application.create();


/******** VIEWS ********/
App.DashboardView = Ember.View.extend({
  name: "",
  init: function(){
    //google.load("visualization", "1", {packages:["corechart"]});
    //google.setOnLoadCallback(drawBarGraph);
    this.set('name', 'ted');
    return this._super();
  }
});

App.GoogleChartView = Ember.View.extend({
  templateName:"google-chart",
  drawChart:function(){

    drawChart();

  }.on("didInsertElement")
});

/******** ROUTES ********/
App.Router.map(function() {
  this.resource('items');
  this.resource('dashboard');
});
App.IndexRoute = Ember.Route.extend({
  redirect: function() {
    this.transitionTo('items');
  }
});
App.ItemsRoute = Ember.Route.extend({
  model: function() {
    var a = Em.A();
    a.pushObject( App.Item.create({title: 'A', cost: '100', options: buildMockOptions('A')}));
    a.pushObject( App.Item.create({title: 'B', cost: '200', options: buildMockOptions('B')}));
    a.pushObject( App.Item.create({title: 'C', cost: '300', options: buildMockOptions('C')}));
    return a;
  }
});

buildMockOptions = function(someVar){
  var arr = [];
  for(var i = 0;i<3;i++){
    var opt = App.Option.create({someOption: 'Option for ' + someVar + ': ' + i});
    opt.cost = 5;
    arr.pushObject(opt);
  }
  return arr;
};

/******** MODELS ********/
App.Item = Ember.Object.extend({
  title: '',
  cost: '',
  quantity: '',
  options: null,

  totalOptionsCost: function(){
    var j = this.get('options').reduce(
      function(prevValue, currentValue, index, array){ 
        return prevValue + parseInt(currentValue.get('cost'), 10); }, 0);
    return j;
  }.property('options.@each.cost')
});

App.Option = Ember.Object.extend({
  someOption: '',
  cost: ''
});


/******** CONTROLLERS ********/
App.ItemsController = Ember.ArrayController.extend({
  len: function(){
    return this.get('length');
  }.property('length'),

  totalCost: function() {
    return this.reduce( function(prevCost, cost){
      return parseInt(cost.get('cost'),10) + prevCost;
    }, 0);
  }.property('@each.cost'),

  totalOptions: function(){
    var opts = this.mapBy('options');
    return [].concat.apply([], opts).length;
  }.property(),

  totalOptionCost: function(){
    var sum = this.reduce(function(prev, curr){ 
      return prev + curr.get('totalOptionsCost');},0);
    return sum;
  }.property('@each.totalOptionsCost')
});

App.DashboardController = Em.Controller.extend({
  needs: ['items'],
  itemsLength: Ember.computed.alias('controllers.items.len'),
  itemsTotalCost: Ember.computed.alias('controllers.items.totalCost'),
  optionsTotal: Ember.computed.alias('controllers.items.totalOptions'),
  optionsCost: Ember.computed.alias('controllers.items.totalOptionCost')
});

// GOOGLE CHART FUNCTION
function drawChart() {

  // Create and populate the data table.
  var options = {
      title:"Yearly Coffee Consumption",
      width:600, 
      height:400,
      animation: {duration: 1000, easing: 'out'},
      vAxis: {title: "Cups", minValue:0, maxValue:500},
      hAxis: {title: "Year"}
  };

  var data = new google.visualization.DataTable();    
  data.addColumn('string', 'N');
  data.addColumn('number', 'Value');
  data.addRow(['2003', 0]);
  data.addRow(['2004', 0]);
  data.addRow(['2005', 0]);

  // Create and draw the visualization.
  var chart = new google.visualization.ColumnChart(document.getElementById('chart'));
  chart.draw(data, options);
  data.setValue(0, 1, 400);
  data.setValue(1, 1, 300);
  data.setValue(2, 1, 400);
  chart.draw(data, options);

    var button = document.getElementById('b1');
    button.onclick = function() {

      if (data.getNumberOfRows() > 5) {
        data.removeRow(Math.floor(Math.random() * data.getNumberOfRows()));
      }
      // Generating a random x, y pair and inserting it so rows are sorted.
      var x = Math.floor(Math.random() * 10000);
      var y = Math.floor(Math.random() * 1000);
      var row = 0;
      while (row < data.getNumberOfRows() && parseInt(data.getValue(row, 0),10) < x) {
        row++;
      }
      data.insertRows(row, [[x.toString(), y]]);
      chart.draw(data, options);
    };
}

function drawChart() {

  // Create and populate the data table.
  var options = {
      title:"Yearly Coffee Consumption",
      width:600, 
      height:400,
      animation: {duration: 1000, easing: 'out'},
      vAxis: {title: "Cups", minValue:0, maxValue:500},
      hAxis: {title: "Year"}
  };

  var data = new google.visualization.DataTable();    
  data.addColumn('string', 'N');
  data.addColumn('number', 'Value');
  data.addRow(['2003', 0]);
  data.addRow(['2004', 0]);
  data.addRow(['2005', 0]);

  // Create and draw the visualization.
  var chart = new google.visualization.ColumnChart(document.getElementById('chart'));
  chart.draw(data, options);
  data.setValue(0, 1, 400);
  data.setValue(1, 1, 300);
  data.setValue(2, 1, 400);
  chart.draw(data, options);

    var button = document.getElementById('b1');
    button.onclick = function() {

      if (data.getNumberOfRows() > 5) {
        data.removeRow(Math.floor(Math.random() * data.getNumberOfRows()));
      }
      // Generating a random x, y pair and inserting it so rows are sorted.
      var x = Math.floor(Math.random() * 10000);
      var y = Math.floor(Math.random() * 1000);
      var row = 0;
      while (row < data.getNumberOfRows() && parseInt(data.getValue(row, 0),10) < x) {
        row++;
      }
      data.insertRows(row, [[x.toString(), y]]);
      chart.draw(data, options);
    };

}

HTML

<!DOCTYPE html>
<html>
<head>
  <meta name="description" content="Ember.js example: Trying to display a google chart in a template." />
<meta charset=utf-8 />
<title>JS Bin</title>
  <script src="http://www.google.com/jsapi?ext.js"></script>
  <script src="http://code.jquery.com/jquery-2.0.2.js"></script>
  <script src="http://builds.emberjs.com/handlebars-1.0.0.js"></script>
  <script src="http://builds.emberjs.com/ember-latest.js"></script>
  <script src="http://www.google.com/jsapi?ext.js"></script>
  <script>google.load("visualization", "1", {packages:["corechart"]});</script>
</head>
<body>

  <script type="text/x-handlebars" data-template-name="application">
  <p><strong>Ember.js example:</strong><br> Trying to display a google chart in a template.</p>
  <p>This jsbin represents a dashboard that analyzes a hierarchy of items and child 'options' and visualizes that info in a 'dashboard' section'. Currently, I am trying to integrate a google chart into the dashboard right above the 'Change Value' button.</p>

  <p>Google chart example: <a href='http://jsfiddle.net/doub1ejack/h7mSQ/96/'>http://jsfiddle.net/doub1ejack/h7mSQ/96/</a><br>
  Stackoverflow post: <a href='http://stackoverflow.com/questions/20198317/ember-js-rendering-google-chart-in-template-aka-target-dom-element-only-when-p'>http://stackoverflow.com/questions/20198317/...</a></p>

    {{render dashboard}}
    {{outlet}}
  </script>

  <script type="text/x-handlebars" data-template-name="items">
    <h2>Items:</h2>
    <dl>
      {{#each}}
        <dt>Item: {{title}}</dt>
        <dd>Cost: {{cost}}</dd>

        <dd class='options'>{{render 'options' options}}</dd>
      {{/each}}
    </dl>
  </script>

   <script type="text/x-handlebars" data-template-name="options">
    {{log controller}}
    <dl>
      {{#each option in controller }}
        <dt>{{option.someOption}}</dt>
        <dd>Option cost: {{option.cost}}</dd>
      {{/each}}
    </dl>
  </script>


  <script type="text/x-handlebars" data-template-name="dashboard">
    <h2>Dashboard:</h2>

    {{view "googleChart"}}
    <div id="chart"></div>

    {{! ITEM/OPTION ANALYSIS }}
    {{#if controllers.items}}
    <h3>Item Overview:</h3>
    Total number of items (expect 3): {{itemsLength}}<br>
    Total cost of items (expect 600): {{itemsTotalCost}}
    <h3>Option Overview:</h3>
    Total number of options (expect 30): {{optionsTotal}}<br>
    Total cost of options (expect 45): {{optionsCost}}
    {{/if}}
  </script>

  <script type="text/x-handlebars" data-template-name="google-chart">
    <div id="chart"></div>
<form><input id="b1" type="button" value="Change Value"/></form>
  </script>

</body>
</html>

EDIT

This is another example where the code with the properties and data related to drawing the chart are accomodated in the `GoogleChartView` class, allowing for binding of properties, execution of actions etc. This is also a draft example just to demonstrate the idea. The change value action executes the old code but it could certainly do modifications to the data property of GoogleChartView. The data property of GoogleChartView could certainly take better advantage of the model. Interesting points are the retrieval of the view's DOM via `this.$().find`, the access to the controller (DashboardController) of the context where this view in included, since at this point any data could be retrieved,parsed and drawn.

http://jsbin.com/UHajoX/1/edit

App.GoogleChartView = Ember.View.extend({
  templateName:"google-chart",
  chart:null,
  options:{
      title:"Yearly Coffee Consumption",
      width:600, 
      height:400,
      animation: {duration: 1000, easing: 'out'},
      vAxis: {title: "Cups", minValue:0, maxValue:500},
      hAxis: {title: "Year"}
  },
  data:function(){

    var items = this.get("controller").get("items");

    var chartData = new google.visualization.DataTable();
    chartData.addColumn('string', 'N');
    chartData.addColumn('number', 'Value');
    chartData.addRow(['2003', 0]);
    chartData.addRow(['2004', 0]);
    chartData.addRow(['2005', 0]);

    items.forEach(function(item,index){
      chartData.setValue(index,1,parseInt(item.get("cost"),10));

    });
   /*chartData.setValue(0, 1, 400);
    chartData.setValue(1, 1, 300);
    chartData.setValue(2, 1, 400);*/



    return chartData;
  }.property(),
  drawChart:function(){

    this.drawChart2();

  }.on("didInsertElement"),
  drawChart2:function(){

  var data = this.get("data");

  // Create and draw the visualization.
  this.set("chart", new google.visualization.ColumnChart(this.$().find('#chart').get(0)));
  this.get("chart").draw(data, this.get("options"));


  },
    actions:{
      changeValue:function(){
        var data = this.get("data");
      if (data.getNumberOfRows() > 5) {
        data.removeRow(Math.floor(Math.random() * data.getNumberOfRows()));
      }
      // Generating a random x, y pair and inserting it so rows are sorted.
      var x = Math.floor(Math.random() * 10000);
      var y = Math.floor(Math.random() * 1000);
      var row = 0;
      while (row < data.getNumberOfRows() && parseInt(data.getValue(row, 0),10) < x) {
        row++;
      }
      data.insertRows(row, [[x.toString(), y]]);
      this.set("data",data);

      this.get("chart").draw(data, this.get("options"));   
      }
    }
});

caveat: I guess in the future the data may originate from the ember app itself, but now when calling google load there might be a possibility where the view tries to call drawChart and the data is not been available yet. To overcome this you may need to check that the data have been loaded before calling drawChart.

Problem

I am trying to integrate a working google chart example into my ember.js app. The problem is that I'm not sure where to put the `drawChart()` function so that it is only run when it's target DOM element is on the page. - Ember app example: http://jsbin.com/oqeSabI/4/edit - Working google chart example: http://jsfiddle.net/doub1ejack/h7mSQ/96/ I'm thinking that the proper location for the load function is in a view (I don't see any reason to put it in a model/controller/route). But I need to be sure that `drawChart()` is not called until after the template has finished rendering and the `<div id='chart'>` element exists. Apparently `View.init()` is too early. Where should I call my build function? Does it even belong in a view (maybe I should use a control or handlebars helper?) ``` App.DashboardView = Ember.View.extend({ name: "", init: function(){ // this breaks the app... //google.load("visualization", "1", {packages:["corechart"]}); //google.setOnLoadCallback(drawBarGraph); this.set('name', 'ted'); return this._super(); } }); ```

Original source

Related problems