`
124 | per data point, *but only for the new points since the last data join*. Since this
125 | is the first data binding (there are no rects currently), everything is new,
126 | it's straightforward to add new points. It's important to keep in mind that for
127 | the next selection, things will be more complex since there will already be
128 | rects.
129 |
130 | The part of a D3 selection that represents these element-less data-points
131 | is passed to the first argument in `selection.join`. The elements don't add themselves, we have to
132 | create the elements that will match the selection ourselves. We use the same
133 | attribute editing helpers to configure each circle per its data point.
134 |
135 |
136 |
137 | {% highlight javascript %}
138 | // recall that scales are functions that map from
139 | // data space to screen space
140 | var maxCount = d3.max(sales, (d, i) => d.count);
141 | var x = d3.scaleLinear()
142 | .range([0, 300])
143 | .domain([0, maxCount]);
144 | var y = d3.scaleOrdinal()
145 | .rangeRoundBands([0, 75])
146 | .domain(sales.map((d, i) => d.product));
147 |
148 | rects.join(
149 | // NEW - handle data points w/o rectangles
150 | newRects => {
151 | newRects.append('rect')
152 | .attr('x', x(0))
153 | .attr('y', (d, i) => y(d.product))
154 | .attr('height', y.rangeBand())
155 | .attr('width', (d, i) => x(d.count));
156 | },
157 | );
158 | {% endhighlight %}
159 |
160 |
161 |
162 |
163 | We're getting a little sneaky here! We're introducing an ordinal
164 | scale, one that's discrete instead of continuous.
165 |
166 |
167 |
168 | The d3.scaleOrdinal() helps us create buckets for each
169 | element. In this case, that's one per product.
170 |
171 |
172 | The domain is the 3 product names. The range is a little different,
173 | rangeRoundBands is a helper function that sets the range, but
174 | tells D3 to pick buckets that are whole pixel widths (no fractions).
175 |
176 |
177 |
178 |
179 | So how does it turn out? Let's take a look:
180 |
181 |
182 |
183 | {% highlight html %}
184 |
188 |
189 |
190 |
191 |
192 | {% include examples/binding.svg %}
193 |
194 |
195 |
196 |
197 |
198 |
199 | Check out how these attribute helpers can take immediate values as well
200 | as callbacks. Just like with d3.min, these callbacks use the same
201 | style of (d, i) parameters to represent the element and its index.
202 |
203 |
204 | ## Removing Elements
205 |
206 | Whereas "enter" selects elements that have added since the last data
207 | join, "exit" is the opposite, it applies to elements that have been
208 | removed.
209 |
210 | Suppose we drop the first point from our source array, we can find and operate
211 | on the corresponding element in the DOM via the exit selection.
212 |
213 | We can use the `remove()` method to immediately delete matched elements; it's
214 | the opposite of `append()`.
215 |
216 | If you only want to delete matched elements, you may omit the argument entirely from `selection.join()` since calling `remove()` is the default behavior.
217 |
218 |
219 |
220 | {% highlight javascript %}
221 | // define new logic for handling joins
222 | rects.join(
223 | newRects => {
224 | newRects.append('rect')
225 | .attr('x', x(0))
226 | .attr('y', (d, i) => y(d.product))
227 | .attr('height', y.rangeBand())
228 | .attr('width', (d, i) => x(d.count));
229 | },
230 | rects => {},
231 | // NEW - delete elements whose data has been removed
232 | rectsToRemove => {
233 | rectsToRemove.remove();
234 | }
235 | );
236 |
237 | sales.pop(); // drops the last element
238 | rects.data(sales); // join the data again
239 | {% endhighlight %}
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 | ## Identity and the Key Function
253 |
254 | As a quick aside: Javascript object equality is very shallow. Objects are only
255 | equal if they are actually the same object (identity), not if they have the same
256 | values:
257 |
258 |
259 |
260 | {% highlight javascript %}
261 | var obj1 = { value: 1 };
262 | // true -- identity
263 | obj1 == obj1;
264 |
265 | var obj2 = { value: 2 };
266 | var obj3 = { value: 2 };
267 | // false -- huh? they have the same values!
268 | obj2 == obj3;
269 | {% endhighlight %}
270 |
271 |
272 |
273 | But the example above works! It only removed one element
274 | from the DOM because we only removed one element from the array, and all the
275 | rest of the objects were the exact same.
276 |
277 | What if we get a new page of data, with some overlap, but we no longer have the
278 | exact same object instances? Well, we will have to find some way to
279 | match objects to each other, and with D3, that's where a key function comes in.
280 |
281 | When we introduced `selection.data()` earlier, we left out the hidden second
282 | parameter, the key function. It's another `(d, i)` callback.
283 |
284 | This example keys objects on their date, so we can match elements across
285 | separate arrays.
286 |
287 |
288 |
289 | {% highlight javascript %}
290 | var sales1 = [
291 | { product: 'Hoodie', count: 7 },
292 | { product: 'Jacket', count: 6 }
293 | ];
294 |
295 | var sales2 = [
296 | { product: 'Jacket', count: 6 }, // same
297 | { product: 'Snuggie', count: 9 } // new
298 | ];
299 |
300 | var rects = svg.selectAll('rect')
301 | .data(sales1, (d, i) => d.product)
302 | .join(enter => enter.append("rect"));
303 |
304 | rects.size(); // 2 -- first join adds two new elements
305 |
306 | // removes 1 element, adds 1 element
307 | rects.data(sales2, (d, i) => d.product);
308 | {% endhighlight %}
309 |
310 |
311 |
312 | ## Transitions `selection.transition()`
313 |
314 | The key function is also important in case parts of our objects change -- if we
315 | change a count, then we can update the appropriate element without having to
316 | delete and re-add the element, we can update it in place.
317 |
318 | One of D3's most visually pleasing features is its ability to help with
319 | transitions. The key function is critical here for object permanence.
320 |
321 | Suppose we have per-product sales we want to update as more products are sold?
322 | We can use transitions to demonstrate this update.
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 | Day 1
331 | |
332 |
333 |
334 | Product |
335 | Sales (Cumulative) |
336 |
337 |
338 |
339 |
340 | Hoodie |
341 | 10 |
342 |
343 |
344 | Jacket |
345 | 3 |
346 |
347 |
348 | Snuggie |
349 | 2 |
350 |
351 |
352 |
353 |
354 |
355 |
356 |
357 |
358 |
359 |
360 | Day 2
361 | |
362 |
363 |
364 | Product |
365 | Sales (Cumulative) |
366 |
367 |
368 |
369 |
370 | Hoodie |
371 | 16 |
372 |
373 |
374 | Jacket |
375 | 7 |
376 |
377 |
378 | Snuggie |
379 | 8 |
380 |
381 |
382 |
383 |
384 |
385 |
386 |
387 |
388 | {% highlight javascript %}
389 | function toggle() {
390 | sales = (sales == days[0]) ? days[1] : days[0];
391 | update();
392 | }
393 |
394 | function update() {
395 | svg.selectAll('rect')
396 | .data(sales, (d, i) => d.product)
397 | .join(
398 | enter => {
399 | enter.append('rect')
400 | .attr('x', x(0))
401 | .attr('y', (d, i) => y(d.product))
402 | .attr('height', y.bandwidth())
403 | .attr('width', (d, i) => x(d.count));
404 | },
405 | update => {
406 | update.attr('width', (d, i) => x(d.count));
407 | },
408 | );
409 | };
410 | {% endhighlight %}
411 |
412 |
413 |
414 |
415 |
416 |
417 |
418 |
419 |
420 |
421 | Ok, but now time to make it pretty. That's where `selection.transition()`
422 | comes in. In the above example, we were just using the plain update
423 | selection to change the values. Here, we'll use `transition()` to make our transition much slicker.
424 |
425 | `transition()` selections can have custom timing attributes like `.duration()`
426 | and `.delay()` and even a custom easing function `.ease()`, but the defaults
427 | are pretty nice.
428 |
429 |
430 |
431 | {% highlight javascript %}
432 | function toggle() {
433 | sales = (sales == days[0]) ? days[1] : days[0];
434 | update();
435 | }
436 |
437 | function update() {
438 | svg.selectAll('rect')
439 | .data(sales, (d, i) => d.product)
440 | .join(
441 | enter => {
442 | enter.append('rect')
443 | .attr('x', x(0))
444 | .attr('y', (d, i) => y(d.product))
445 | .attr('height', y.bandwidth())
446 | .attr('width', (d, i) => x(d.count));
447 | },
448 | update => {
449 | // NEW!
450 | update.transition().duration(1000)
451 | .attr('width', (d, i) => x(d.count));
452 | },
453 | );
454 | };
455 | {% endhighlight %}
456 |
457 |
458 |
459 |
460 |
461 |
462 |
463 |
464 |
465 |
466 | Ok! That was the basics of D3! We've got a few more complex examples, but they
467 | mostly build on what we've already shown.
468 |
469 |
470 | Next
471 |
472 |
473 |
474 |
475 |
--------------------------------------------------------------------------------
/z04-examples.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: Examples
4 | permalink: /examples/
5 | ---
6 |
7 | There's a lot more to D3! This is just a quick tour of some other stuff D3
8 | has to offer.
9 |
10 | - [Layouts and SVG Helpers](#layouts-and-svg-helpers)
11 | - [A Pie Chart](#a-pie-chart)
12 | - [Stacked Bars](#stacked-bars)
13 | - [Onward!](#onward)
14 |
15 | ## Layouts and SVG Helpers
16 |
17 | Some of these examples make use of D3's layout helpers.
18 |
19 | Some layouts convert our original data into descriptions of the shapes we want
20 | to draw. For example, the pie layout converts numbers into arcs (start and end
21 | angles for pie slices). This keeps our drawing code simple. Other
22 | layouts help us group our data so we can draw useful shapes like stacked stacked
23 | or trees.
24 |
25 | D3 also provides helpers to make some of the more complex SVG shapes easier to
26 | draw. The path helper can build curves that interolate between data points. The
27 | arc helper can take the angles generated by the pie layout and draw arcs (pie slices).
28 |
29 | ## A Pie Chart
30 |
31 | Let's start out by walking through using D3 to draw a simple pie chart.
32 |
33 | First we get our source data:
34 |
35 |
36 |
37 | {% highlight javascript %}
38 | var sales = [
39 | { product: 'Hoodie', count: 12 },
40 | { product: 'Jacket', count: 7 },
41 | { product: 'Snuggie', count: 6 },
42 | ];
43 | {% endhighlight %}
44 |
45 |
46 |
47 | We want each product to be represented as a pie slice in our pie chart, which
48 | involves calculating the associated angles. We'll use the `d3.pie` helper
49 | for that:
50 |
51 |
52 |
53 | {% highlight javascript %}
54 | var pie = d3.pie()
55 | .value(d => d.count)
56 |
57 | var slices = pie(sales);
58 | // the result looks roughly like this:
59 | [
60 | {
61 | data: sales[0],
62 | endAngle: 3.0159289474462017,
63 | startAngle: 0,
64 | value: 12
65 | },
66 | {
67 | data: sales[1],
68 | startAngle: 3.0159289474462017,
69 | endAngle: 4.775220833456486,
70 | value: 7
71 | },
72 | {
73 | data: sales[2],
74 | startAngle: 4.775220833456486,
75 | endAngle: 6.283185307179587,
76 | value: 6
77 | }
78 | ]
79 | {% endhighlight %}
80 |
81 |
82 |
83 | Now we have our data in angles (radians), so we can turn them into something
84 | visual. The next tool D3 gives us is the `d3.arc` which helps to create
85 | SVG `` tags for arcs. This is where we provide all the information relevant
86 | to actually drawing, such as the radius size.
87 |
88 |
89 |
90 | {% highlight javascript %}
91 | var arc = d3.arc()
92 | .innerRadius(0)
93 | .outerRadius(50);
94 |
95 | // helper that returns a color based on an ID
96 | var color = d3.scaleOrdinal(d3.schemeCategory10);
97 |
98 | var svg = d3.select('svg.pie');
99 | var g = svg.append('g')
100 | .attr('transform', 'translate(200, 50)')
101 |
102 | g.selectAll('path.slice')
103 | .data(slices)
104 | .join(
105 | enter => {
106 | enter.append('path')
107 | .attr('class', 'slice')
108 | .attr('d', arc)
109 | .attr('fill', d => color(d.data.product))
110 | }
111 | );
112 |
113 | // building a legend is as simple as binding
114 | // more elements to the same data. in this case,
115 | // tags
116 | svg.append('g')
117 | .attr('class', 'legend')
118 | .selectAll('text')
119 | .data(slices)
120 | .join(
121 | enter => {
122 | enter.append('text')
123 | .text(d => '• ' + d.data.product)
124 | .attr('fill', d => color(d.data.product))
125 | .attr('y', (d, i) => 20 * (i + 1));
126 | }
127 | );
128 | {% endhighlight %}
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 | Again, we snuck in a new helper, and it's another type of ordinal scale.
138 |
139 |
140 | The
d3.scaleOrdinal helper gives us back a function. This function
141 | takes in values (typically IDs) and gives back a value in its domain. The same ID gets the
142 | same color, and it will rotate through its domain.
143 | apart. We initalize it with
d3.schemeCategory10 which is a list of10 colors that are pretty easy to tell apart.
144 |
145 |
146 |
147 | ## Stacked Bars
148 |
149 | One of the most common charts to draw is some variation of stacked bars.
150 | These are deceptively complex, because after the first layer, each new layer
151 | of bars depends on layout of the previous one.
152 |
153 | The data requirements are also different because stacked bars need to have
154 | dense data sources. In most graphs, we could omit an empty value because it
155 | won't be drawn, but in a stacked layout, that still could affect the layout
156 | of the next layer.
157 |
158 | Let's start we have sales of our products over multiple days.
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 | Sales
167 | |
168 |
169 |
170 | Date |
171 | Hoodie |
172 | Jacket |
173 | Snuggie |
174 |
175 |
176 |
177 |
178 | 2014-01-01 |
179 | 6 |
180 | 2 |
181 | 3 |
182 |
183 |
184 | 2014-01-02 |
185 | 7 |
186 | 5 |
187 | 2 |
188 |
189 |
190 | 2014-01-03 |
191 | 8 |
192 | 7 |
193 | 3 |
194 |
195 |
196 |
197 |
198 |
199 |
200 | Transformed into a dense array, our data looks like this:
201 |
202 |
203 |
204 | {% highlight javascript %}
205 | var sales = [
206 | { date: "2014-01-01", hoodies: 6, jackets: 2, snuggies: 3 },
207 | { date: "2014-01-02", hoodies: 7, jackets: 5, snuggies: 2 },
208 | { date: "2014-01-03", hoodies: 8, jackets: 7, snuggies: 3 }
209 | ];
210 | {% endhighlight %}
211 |
212 |
213 |
214 | Now we can take advantage of the `d3.stack` to do the work of stacking
215 | our layers on top each other. While normally a bar graph would have one `y`
216 | value, a stacked one has two:
217 |
218 | - where a segment starts ("baseline")
219 | - where the segment ends
220 |
221 | For the first layer stacked bar chart (at the bottom), the baseline is typically 0.
222 | It can be other values for things like [streamgraphs][streamgraph], which are
223 | a whole other topic.
224 |
225 | [streamgraph]: http://bl.ocks.org/mbostock/4060954
226 |
227 |
228 |
229 | {% highlight javascript %}
230 | var stack = d3.stack()
231 | .keys(["hoodies", "jackets", "snuggies"]);
232 |
233 | var stacked = stack(sales);
234 | {% endhighlight %}
235 |
236 |
237 |
238 | Now, `stacked` will be a set of nested arrays containing the hights of the data in sales, stacked, which will come in handy when it's time to draw these. For examples, the stacked data now looks like this:
239 |
240 |
241 |
242 | {% highlight javascript %}
243 | stacked
244 | [
245 | [[0, 6], [0, 7], [0, 8 ]],
246 | [[6, 8], [7, 12], [8, 15 ]],
247 | [[8, 11], [12, 14], [15 18 ]]
248 | ]
249 | {% endhighlight %}
250 |
251 |
252 |
253 | But the data is not a plain array! It also has a few useful properties. The "rows" have `key` and `index` and the computed start/end arrays also have `data` -- the original data.
254 |
255 |
256 |
257 | {% highlight javascript %}
258 | stacked
259 | // [Array[3], Array[3], Array[3]]
260 | stacked[0]
261 | // [Array[2], Array[2], Array[2]]
262 | Object.keys(stacked[0])
263 | // ["0", "1", "2", "key", "index"]
264 | stacked[0].key
265 | // "hoodies"
266 | stacked[0][0].data
267 | // {date: "2014-01-01", hoodies: 6, jackets: 2, snuggies: 3}
268 | {% endhighlight %}
269 |
270 |
271 |
272 |
273 | Ok so let's get to drawing! We'll bring back our good friends `d3.scaleLinear` and
274 | `d3.scaleTime`.
275 |
276 |
277 |
278 | {% highlight javascript %}
279 | var height = 200;
280 | var width = 200;
281 |
282 | // we need to calculate the maximum y-value
283 | // across all our layers, so we find the biggest
284 | // end value
285 | var maxY = d3.max(stacked, d => d3.max(d, d => d[1]));
286 |
287 | var y = d3.scaleLinear()
288 | .range([height, 0])
289 | .domain([0, maxY]);
290 |
291 | var x = d3.scaleTime()
292 | .range([0, width])
293 | .domain(d3.extent(sales, d => new Date(Date.parse(d.date))))
294 | .nice(4);
295 |
296 | var svg = d3.select('svg.stack');
297 | var color = d3.scaleOrdinal(d3.schemeCategory10);
298 |
299 | // bind a tag for each layer
300 | var layers = svg.selectAll('g.layer')
301 | .data(stacked, d => d.key)
302 | .join(
303 | enter => {
304 | enter.append('g')
305 | .attr('class', 'layer')
306 | .attr('fill', d => color(d.key));
307 | }
308 | );
309 |
310 | // bind a to each value inside the layer
311 | layers.selectAll('rect')
312 | .data(d => d)
313 | .join(
314 | enter => {
315 | enter.append('rect')
316 | .attr('x', d => x(new Date(Date.parse(d.data.date))))
317 | .attr('width', width / 3)
318 | // remember that SVG is y-down while our graph is y-up!
319 | // here, we set the top-left of this bar segment to the
320 | // larger value of the pair
321 | .attr('y', d => y(d[1]))
322 | // since we are drawing our bar from the top downwards,
323 | // the length of the bar is the distance between our points
324 | .attr('height', d => y(d[0]) - y(d[1]));
325 | }
326 | );
327 | {% endhighlight %}
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
There are a few things that make this graph a little more complex. One of
336 | the hardest parts is realizing that D3 is really only going to hint at how
337 | we should stack the bars: D3 gives us stacked results in our data space, but
338 | not in SVG's coordinate system. We have to deal with the same confusing
339 | Y-axis
340 | coordinate flip.
341 |
342 |
343 |
344 | ## Onward!
345 |
346 | D3 has a lot to offer, and our goal here was to give a brief tour and cover some
347 | core concepts! There's much more to learn about D3, but hopefully this tutorial
348 | has given you enough so that you can teach yourself the rest.
349 |
350 | There are lot of great resources for learning D3 out there:
351 |
352 | 1. First and foremost, [D3's own wiki][d3-wiki]. This is a great starting point
353 | for any D3-related exploration
354 |
355 | 2. Nestled inside that wiki, the [D3 API Reference][d3-api-reference]
356 | is great for remembering what APIs there are and what the various parameters
357 | mean.
358 |
359 | 3. For more examples of what is possible with D3 check out the
360 | [D3 examples][d3-examples] by creator of D3, Mike Bostock.
361 |
362 | 4. The [D3 graph gallery](d3-graph-gallery) is a collection of examples for dozens of different types of charts made with D3. It is maintained by Yan Holtz.
363 |
364 | But don't stop there! Google searches are a great way to discover things too.
365 | Happy visualizing!
366 |
367 | [d3-wiki]: https://github.com/mbostock/d3/wiki
368 | [d3-api-reference]: https://github.com/mbostock/d3/wiki/API-Reference
369 | [d3-examples]: http://bl.ocks.org/mbostock
370 | [d3-graph-gallery]: https://www.d3-graph-gallery.com/
371 |
372 |
373 |
--------------------------------------------------------------------------------