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')
});
<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')
});
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'));
}
});
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
Ember and D3 - Sep 2013
By Sam Selikoff
Ember and D3 - Sep 2013
- 13,319