Ember and d3

interactive dashboards



Audience

  • Beginner to intermediate D3
  • Little to no experience with JS MV*
  • Want to incorporate D3 into applications (e.g., a  dashboard)
  • Curious about Ember!






never heard of d3?

You should check it out!



So, You're leveling up in D3

Why use a framework?

Like in everything...


Separation of concerns

  • Store your data somewhere
  • Handle events
  • Will you want routing?
  • Reusable components

Why reinvent the wheel?


why ember?

  • Datavis is about data. Ember gives you
    • data bindings
    • computed properties
    • observers
  • Persistent controllers
  • Easy routing
  • Fixtures (in ember-data)
  • Mixins



Ok, i'm convinced

But I don't want to tie down my D3 work to Ember



We'll add a loose layer 

around your existing D3 work


  • Not building an Ember charting library
    • Actually, if we were, would probably suggest something like Backbone
  • Also, there's tons of D3 code + examples out there. We should be able to incorporate these into an Ember project




So lets get started!

MOCKUp

Mockup



  • Show company revenues per month
  • Datepicker
  • 2 charts
  • Some filters

Dependencies



Ember starter kit, and

  • d3.js
  • bootstrap
  • ember-data
  • moment.js
  • bootstrap-datepicker
  • my d3 charts


scaffolding

(part 1)


Scaffolding


  • Application template is persistent
    • Datepicker will be here
    • Center panel contains {{outlet}}

Scaffolding

Application template


 <script type="text/x-handlebars">
    <div class="title">
      <h1>Ember and D3</h1>
      <h2>Scaffolding</h2>
    </div>

    <div class="content">
      <div id="left-panel" class="text-center">
        <h3>The datepicker</h3>
      </div>
      
      <div id="center-panel" class="container">
        {{outlet}}
      </div>
    </div>
  </script>

scaffolding

Index template


 <script type="text/x-handlebars" data-template-name="index">
    <h3 class='text-center muted thin'>Select a month</h3>
  </script>

App

 <script>
    App = Ember.Application.create();
  </script>



datepicker

(part 2)

datepicker

Month resource
  • Each month can be identfied by a URL

App.Router.map(function() {
    this.resource('month', { path: '/:month_id' });
});

datepicker

Models
App.Store = DS.Store.extend({
  adapter: DS.FixtureAdapter.create({ latency: 400 })
});

App.Month = DS.Model.extend({
  test: DS.attr('number'),
  companies: DS.hasMany('App.Company')
});

App.Company = DS.Model.extend({
  month: DS.belongsTo('App.Month'),
  name: DS.attr('string'),
  newContracts: DS.attr('number'),
  feeIncreases: DS.attr('number'),
  attritions: DS.attr('number'),
});
// Generate some random test data, thenApp.Month.FIXTURES = months; App.Company.FIXTURES = companies;

datepicker


Our datepicker will need to be able to find months.

Since the datepicker lives in the ApplicationTemplate, we need to teach the ApplicationController where to look.

App.ApplicationController = Ember.Controller.extend({
    needs: ['month']
});

Datepicker

 App.DatepickerView = Ember.View.extend({
      classNames: ['dp'],
      didInsertElement: function() {
        var _this = this;

        this.$().datepicker({'format': 'M yyyy','minViewMode': 'months'})
          .on('changeDate', function(e) {
            var id = $(this).datepicker().data('date').replace(" ", "-");
            _this.get('controller').transitionToRoute(               'month', App.Month.find(id)            );
          });
        this.$('.month.active').removeClass('active');

        if (this.get('controller.controllers.month.content')) {
          this.update();
        }

      },

datepicker

Need to ensure two-way binding
In the future some other component could change the date

       update: function() {
         var month = moment(             this.get('controller.controllers.month.id') );         this.$().datepicker('hide');
         this.$().datepicker('setDate', month.toDate());
         this.$().datepicker('show');
       }.observes('controller.controllers.month.content')
    });

datepicker

Computed property on month

 App.MonthController = Ember.ObjectController.extend({
    title: function() {
        return moment(this.get('id')).format('MMMM YYYY');
    }.property('model')
 });
In our template

 <script type="text/x-handlebars" data-template-name="month">
    <h1>{{ title }}</h1>
 </script>



data from routes

(part 3)

data from routes

First of all, ajax loading
 App.LoadingRoute = Ember.Route.extend({
            renderTemplate: function() {
        if (this.controllerFor('application').get('currentPath')) {
          this.render('loading', {            into: 'application',            outlet: 'loading'          });
        } 
      }
 });

data from routes

Ajax loading
 <script type="text/x-handlebars">   ...      <div id="left-panel" class="text-center">
        <div class="loading">
          {{ outlet loading }}
        </div>
        {{view App.DatepickerView}}
      </div>
... </script>
 <script type="text/x-handlebars" data-template-name="loading">
    <img src="img/loading.gif">
  </script>

Data from routes

Next, the data.
Where does it come from?
CompaniesController.

App.CompaniesController = Ember.ArrayController.extend({
  data: function() {
    if (this.get('model.isLoaded')) {
      var data = this.map(function(company) {
        return {
          category: company.get('name'),
          count: company.get('newContracts'),
        };
      });
    }
    return data;
  }.property('model.isLoaded')
});

data from routes

Don't forget to teach months about companies!
(template context)

App.MonthController = Ember.ObjectController.extend({
  needs: ['companies'],
  title: function() {
    return moment(this.get('id')).format('MMMM YYYY');
  }.property('this.model')
});

data from routes

The bar graph
App.BarGraph = Ember.View.extend({
  classNames: ['chart'],

  chart: BarChart()
    .margin({left: 40, top: 40, bottom: 80, right: 40})
    .oneColor('#BE3600')
    .rotateAxisLabels(true)
    // .hideAxisLabels(true)
    // .noTicks(true)
    // .staticDataLabels(true)
    ,

  didInsertElement: function() {
    Ember.run.once(this, 'updateChart');
  },

  updateChart: function() {
    if (this.get('isLoaded')) {
      d3.select(this.$()[0])
        .data([ this.get('data') ])
        .call(this.get('chart'));
    }
  }.observes('data')

});
Where do 'isLoaded' and 'data' come from?

data from routes

The context of the template

{{view App.BarGraph 
  isLoadedBinding="controller.controllers.companies.model.isLoaded" 
  dataBinding="controller.controllers.companies.data"
}}

And now we have a reusable bar graph!

With a chart I made before I knew Ember!



data within routes

(part 4)

data within routes

First, the data
App.CompaniesController = Ember.ArrayController.extend({
  filter: 'newContracts',

  data: function() {
    if (this.get('model.isLoaded')) {
      var _this = this;

      var data = this.map(function(company) {
        return {
          category: company.get('name'),
          count: company.get( _this.get('filter') ),
        };
      });
    }
    return data;
  }.property('model.isLoaded', 'filter')
});

data within routes

Now the buttons
<ul class="nav nav-pills filters">

  {{#view App.FilterView value="newContracts"}}
  	<a>New Contracts</a>
  {{/view}}

  {{#view App.FilterView value="feeIncreases"}}
  	<a>Fee Increases</a>
  {{/view}}
  
  {{#view App.FilterView value="attritions"}}
  	<a>Attritions</a>
  {{/view}}
</ul>

data within routes

The filter view
App.FilterView = Ember.View.extend({
  tagName: 'li',
  classNameBindings: ['active'],

  active: function() {
    return this.get('controller.controllers.companies.filter') == this.get('value');
  }.property('controller.controllers.companies.filter'),

  click: function() {
    this.get('controller.controllers.companies').set('filter', this.get('value'));
  }
});

data within routes

That's it! Everything else works!
With Ember, your app scales - complexity doesn't

Some considerations:
  • We added the filters separate from the chart
    • Could be together under one parent view
  • All data driven from controller
    • Promotes data sharing + performance gains
    • ..but if too many views from one controller, could make a case that some of the data manipulation should be on the view



Flexibility

(part 5)

flexibility

Create the pie chart
 App.PieGraph = Ember.View.extend({
  classNames: ['chart'],

  chart: PieChart()
    .oneColor('#BE3600')
    .labelColor('white')
    .labelSize('11px')
    // .margin({left: 40, top: 40, bottom: 50, right: 40})
    // .hideAxisLabels(true)
    // .noTicks(true)
    // .staticDataLabels(true)
    ,

  didInsertElement: function() {
    Ember.run.once(this, 'updateChart');
  },

  updateChart: function() {
    if (this.get('isLoaded')) {

      d3.select(this.$()[0])
        .data([ this.get('data') ])
        .call(this.get('chart'));
    }
  }.observes('data')
});

flexibility

Add it to the template

{{view App.PieGraph 
  isLoadedBinding="controller.controllers.companies.model.isLoaded" 
  dataBinding="controller.controllers.companies.data"
}}

All done!


Other things to explore


  • Mixins - ways to add properties to classes
    • resizable
    • draggable
    • sortable
  • Components
    • isolated view elements
  • Base/generic views
    • E.g., I see a base class for the bar/pie charts


That's all for now!


questions?


SAM SELIKOFF

www.samselikoff.com
@samselikoff

Ember and D3 - Aug 2013

By Sam Selikoff

Ember and D3 - Aug 2013

  • 10,322