3

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.

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();
  }
});
doub1ejack
  • 10,627
  • 20
  • 66
  • 125
  • Perhaps the view's `didInsertElement` hook would be the right place for this call (see http://stackoverflow.com/questions/8881040/using-ember-js-how-do-i-run-some-js-after-a-view-is-rendered)? The app still breaks when I try that though, so there may be something wrong with the way I'm calling `drawChart()` too... – doub1ejack Nov 25 '13 at 16:51

1 Answers1

5

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.

melc
  • 11,523
  • 3
  • 36
  • 41
  • Awesome, thanks. Pulling this out into a reusable view is fantastic. Thanks for including a little background on how/why that decision was made, that's what I'm really struggling with in Ember these days. – doub1ejack Nov 25 '13 at 17:59
  • Looking at GoogleChartView in the Chrome Ember.js inspector, it doesn't look like there is a model assigned to it. I want the chart to represent the data in the 'Items' ArrayController (a collection of Item models). How would I do that? – doub1ejack Nov 25 '13 at 18:21
  • @doub1ejack indeed it was a basic example, showing how to properly use the chart within the context of ember app. The second example is bound further to the data of the app, i hope it helps. – melc Nov 25 '13 at 20:04
  • I was just looking for an explanation, but the example is way more valuable - thanks melc! – doub1ejack Nov 25 '13 at 20:10