Passing in data
Learn how to transform application data from our controller into a form suitable for our D3 visualization.
Transcript
Now that we have our bar chart rendering responsively and scaling our data, it's time for it to render our actual blog posts.
If we look at our bar-chart component right now, we're using an authors
property to store our dataset; and we're also referencing author
throughout our code while performing our calculations.
Since our bar chart component is going to be working with authors, categories and comments, let's change this variable to be more generic. We'll rename authors
everywhere we're using it to data
; and we'll also come here and change author
everywhere else it's used to data
; let's change dataCounts
to just counts
; and finally, our dataset is using a name
property right now to identify each bar, but let's just change this to something generic like label
.
Ok let's save this and make sure our chart is still working. Ok - looks good.
Our next step is to pass this dummy dataset in from the controller. Let's cut it from our component, open our posts controller, and paste it in as a property. Let's call this property authorData
, then open our template and set the first bar chart's data to our authorData
.
{{bar-chart data=authorData}}
If we save and look at our app, we can see that the first bar chart is correctly rendering data passed in from the controller!
Now if we open the console we'll see an error - cannot read property map of undefined. We can click on the line of code here, and we'll see the error comes from trying to map over data in our component's didInsertElement
hook. We're rendering two charts and not passing in any data right now, so let's open our bar chart component and give it a default data property of an empty array.
data: []
Now empty bar charts won't throw an error anymore.
Ok, back in the controller, let's comment out our dummy authorData
, and start writing a computed property to calculate the real author data from the underlying model.
Now, we know we want to start with the posts
array, and we want to turn this into something that looks like this data right here:
authorData: [
{ label: 'Mark Twain', count: 15 },
{ label: 'Virginia Woolf', count: 42 },
{ label: 'John Steinbeck', count: 23 },
{ label: 'Ralph Ellison', count: 27 }
]
Now, each post model in this array has an author
property, so if we could group the array by author
, we'd have a good start on transforming our data correctly. Fortunately there's actually an addon called ember-group-by
that gives us a computed macro that does exactly what we want.
So let's come to our command line, stop our server, and run
ember install ember-group-by
and then restart our server. And let's come check out the repo page to see how it works.
So we import groupBy
at the top of our file, and then we use it to define a new property passing in an array as the first argument and the property to group by as the second.
So let's grab this import, come back to our code, we'll paste this in, and then we'll write a new property called postsByAuthor
, which will groupBy all of our posts by each author:
postsByAuthor: groupBy('posts', 'author')
Now this will return an array that looks something like this
[
{ property: 'author', value: 'John Smith', items: [post1, post3] },
{ property: 'author', value: 'Jane Doe', items: [post2, post4, post5] },
]
It gives us an array of groups, each group has the property we're grouping by, the value of that property - which in our case will be the different author names - and then a collection of all the items in our group - in our case, arrays of posts for each author.
Now, if we look at our dummy data, which is our target format, we just want an array of objects with label
and count
properties. So, we need to write one more computed property to transform our grouped data.
We can use use Ember.computed.map
on our postsByAuthor
, and this will let us take each group and transform it into the format we need.
computed.map
will pass in each element of this array as an argument to the function, and in our case we're passing in this array of groups. So let's call the parameter group, and then we can come in here and just return a simple object, with a label property that is the group's value, and a count property which is going to be the group's items
array, and we'll just grab the length off of that array.
authorData: Ember.computed.map('postsByAuthor', function(group) {
return {
label: group.value,
count: group.items.length
}
})
Let's save this, and remember this is the authorData
property, which we're already passing into our first bar chart, so if we come back to our app, now we see our actual data in the chart!
Now we don't have any labels on the bars yet, so these are just a bunch of black bars, but we'll be taking care of that in the next video. For now let's just get our other two charts working.
Let's come back to the controller, we can delete this dummy data now, and we'll work on the category data. Remember our second chart will show posts by category, and if we open up the posts model, we can see that it has a category
attribute, just like the author
attribute.
So, back in the controller, we'll start just by copying these author properties; we'll change this to category
, and we'll group our posts by category; we'll change this to categoryData
; and the rest should stay the same.
Let's save this, come to our template, and we'll pass in categoryData
to our second chart.
postsByCategory: groupBy('posts', 'category'),
categoryData: Ember.computed.map('postsByCategory', function(group) {
return {
label: group.value,
count: group.items.length
}
}),
{{bar-chart data=categoryData}}
And looking at our app, our blog posts are in three different categories, so we see three bars in our middle chart!
Finally, in our third chart, we want to show each post's comment count. Now if we look the post
model, we'll see that commentCount
is actually an attribute. So, we'll be showing each individual post in the third chart, and we'll be using that post's commentCount
for its height.
So coming back to our controller, we don't actully need to use groupBy
, we can just use computed.map
property, change this to commentsData
, and we'll come here, and we're going to be mapping each post to a label
and count
. The label will just be the title of the post, and the
count, we'll get the post's
commentCount` property.
commentsData: Ember.computed.map('posts', function(post) {
return {
label: post.get('title'),
count: post.get('commentCount')
};
})
Let's save this, come back to our template, and we'll pass in data=commentsData
to our third chart.
{{bar-chart data=commentsData}}
If we come back to our app, now we see all of our blog posts here with their comment counts! So all three charts are correctly rendering data from our actual blog posts.
Now, before moving on, let's add some titles to our actual charts here. We'll come to our template, and below our first chart, we'll add an h2
tag for Authors, and let's give it a font of scale size 5, a margin top of scale size 3 and a margin bottom of 4.
<h2 class='f5 mt3 mb4'>
Authors
</h2>
And we can see it here. Let's also come back to our parent, and add a text-center class, so all our titles are centered.
<div class="flex tc">
Ok, looking good! Let's copy and add labels for Categories and Comments.
<h2 class='f5 mt3 mb4'>
Categories
</h2>
<h2 class='f5 mt3 mb4'>
Comments
</h2>
Alright, now things are starting to shape up!
Transforming data is a necessary part of writing data visualizations, and sometimes it can be quite tricky. In this video, we started off with a data format that our bar chart could work with - an array of objects with label
and count
properties. Then, we worked backwards, massaging our data from the underlying model into that target format.
Now, in general this is a nice way to work, and because we're writing our own bar chart, we get to decide what the data API looks like. Arrays of objects are pretty standard and we only to require consumers to pass in the bare minimum for each object - in our case, a label to identify each bar, and a count which would be used for the height of each bar.
Now when working with more complex visualizations, the data requirements will be different, and in these cases it's good to take time and work with your teammates to come up with an API that meets other developer expectations, especially if you plan on sharing these chart components with others. Just know that cleaning, formatting and massaging data is unavoidable when doing data visualization, and you should expect it to be a non-trivial part of coding your visualizations.
In the next video we'll add a tooltip to our bar chart component.