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, then
App.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
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')
});
<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')
});
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,330