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!






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
    • observers
    • data bindings
    • computed properties
  • Persistent controllers
  • Easy routing
  • Fixtures (in ember-data)
  • Components, mixins 

One of my goals: Ember can be hard. Find lots of references.



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

Go


one
  • two
  • asdf

Dependencies



Ember starter kit (1.0.0), and

  • d3.v3.js
  • bootstrap
  • ember-data-1.0.0-beta.2
  • 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">
    <header class="title">
      <h1>Ember and D3</h1>
      <h2>Scaffolding</h2>
    </header>

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

scaffolding

Index template


 <script type="text/x-handlebars" data-template-name="index">
    <h3>Select a month</h3>
  </script>

App

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



datepicker

(part 2)

datepicker


MonthlyReport resource
  • Each report can be identified by a URL

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

datepicker


Adapter
App.ApplicationAdapter = DS.FixtureAdapter.extend({ latency: 400 });

MonthlyReport model

App.MonthlyReport = DS.Model.extend({
  companies: DS.hasMany('company', { async: true }),

date: function() { return moment( this.get('id'), 'MMM-YYYY' ); }.property('model'), });

datepicker


Company model
App.Company = DS.Model.extend({
  monthlyReport: DS.belongsTo('monthly-report'),
  name: DS.attr('string'),
  newContracts: DS.attr('number'),
  feeIncreases: DS.attr('number'),
  attritions: DS.attr('number'),
});

Fixtures
// Generate some random test data, then
App.MonthlyReport.FIXTURES = months; App.Company.FIXTURES = companies;

Datepicker


We will use a component for our datepicker.

What are components?
  • Reusable
  • Ignorant of their surroundings
  • Can communicate with your application via actions

What does ours need?
  • To know the month
  • To tell our app when the month changes

Datepicker

Our first component
App.MonthlyDatepickerComponent = Ember.Component.extend({
  classNames: ['dp'],

  didInsertElement: function() {
    var _this = this;
    this.$().datepicker({format: 'M-yyyy',minViewMode: 'months'})
      .on('changeDate', function(e) {
        _this.sendAction('action', e.format());
      });
    
    this.update();
  },
update: function() { if (this.get('month')) { this.$().datepicker('update', this.get('month').toDate()); } else { this.$('.month.active').removeClass('active'); } }.observes('month') });

datepicker


How do we get 'month' and 'action'?

The 'month' comes from the MonthlyReport controller.

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

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

Datepicker


Now, we can use our template to define these props:

{{monthly-datepicker      month=controller.controllers.monthlyReport.date      action="getMonthlyReport"}}

A reusable component!

Who handles getMonthlyReport?

datepicker


We let it bubble up to the ApplicationController:

 App.ApplicationController = Ember.Controller.extend({
  needs: ['monthlyReport'],
  actions: {
    getMonthlyReport: function(id) {           this.transitionToRoute(        'monthlyReport',        this.get('store').find('monthlyReport', id )     );
    }
  }
});

datepicker

Computed property on month

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

 <script type="text/x-handlebars" data-template-name="monthlyReport">
    <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>
The actual loading template
 <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')) {return;}

    var data = this.map(function(company) {
      return {
        category: company.get('name'),
        count: company.get('newContracts'),
      };
    });

    return data;
  }.property('model')
});

data from routes

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

App.MonthlyReportController = Ember.ObjectController.extend({
  needs: ['companies'],  ...
});

data from routes

The bar component
App.BarChartComponent = Ember.Component.extend({
  classNames: ['chart'],

  chart: BarChart()
    .margin({left: 40, top: 40, bottom: 80, right: 40})
    .manyColors(true)
    .colors(['#be3600', '#ff4b00', '#ff6100', '#ff7600', '#ff8c00']) 
    // .oneColor('#BE3600')
    .rotateAxisLabels(true)
    // .hideAxisLabels(true)
    // .noTicks(true)
    // .staticDataLabels(true)
    ,

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

  update: 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

{{bar-chart    data=controller.controllers.companies.data   isLoaded=controller.controllers.companies.model.isLoaded }}


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', 'filter')
});

data within routes

Now the filters
<ul class="nav nav-pills filters">
  {{filter-item value="newContracts" 
                label="New Contracts" 
                filter=controller.controllers.companies.filter}}
  {{filter-item value="feeIncreases" 
                label="Fee Increases" 
                filter=controller.controllers.companies.filter}}
  {{filter-item value="attritions" 
                label="Attritions" 
                filter=controller.controllers.companies.filter}}
</ul>

data within routes

The filter component
App.FilterItemComponent = Ember.Component.extend({
  tagName: 'li',
  classNameBindings: ['active'],

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

  click: function() {
    this.set('filter', this.get('value'));
  }
});
Two-way binding - we modify 'filter' directly!

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 components/views being driven by one controller, may want to move some of your data manipulation to the components/views themselves



Flexibility

(part 5)

flexibility

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

  chart: PieChart()
        .oneColor('#BE3600')
        .labelColor('white')
        .labelSize('11px'),

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

  update: 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

<div class="row">
  <div class="col-lg-7">
    {{bar-chart 
        data=controller.controllers.companies.data 
        isLoaded=controller.controllers.companies.model.isLoaded }}
  </div>
  <div class="col-lg-5">
    {{pie-chart 
        data=controller.controllers.companies.data 
        isLoaded=controller.controllers.companies.model.isLoaded}}
  </div>
</div>

All done!


Other things to explore


  • Mixins - ways to add properties to classes
    • resizable
    • draggable
    • sortable
  • Experiment with components
    • base components
    • hooking into your own chart's api


That's all for now!


questions?


SAM SELIKOFF

www.samselikoff.com
@samselikoff
Made with Slides.com