├── .editorconfig
├── .eslintrc.js
├── .gitignore
├── README.md
├── code
├── ch01
│ └── code-listings.js
├── ch02
│ ├── data-loading.js
│ ├── data.tsv
│ ├── original-bar-chart.html
│ ├── original-bar-chart.js
│ └── updated-bar-chart.html
├── ch03
│ ├── index.html
│ ├── listing-3-10-d3fc.js
│ ├── listing-3-11-britecharts.js
│ ├── listing-3-5-dataset.js
│ ├── listing-3-6-plottable.js
│ ├── listing-3-7-billboard.js
│ ├── listing-3-8-vega.json
│ ├── listing-3-9-vega.js
│ └── listings-3-1to4.js
├── ch04
│ ├── listing-4-1.js
│ ├── listing-4-2.js
│ ├── listing-4-3.js
│ ├── listing-4-4.js
│ ├── listing-4-5.js
│ ├── listing-4-6.js
│ ├── listing-4-7.js
│ └── reusable-chart-api.js
├── ch05
│ ├── bars-and-accessors.js
│ ├── events.js
│ ├── listing-5-1-core-pattern.js
│ ├── listing-5-2-building-svg.js
│ ├── listing-5-20-final-chart.js
│ ├── listing-5-21-using-bar-chart.js
│ ├── listing-5-6-containers.js
│ ├── listings.js
│ ├── original-bar-chart.js
│ └── scales-and-axes.js
├── ch07
│ ├── line-chart-with-brush.html
│ ├── line-chart-with-custom-colors.html
│ ├── line-chart-with-legend.html
│ ├── listings.js
│ └── simple-line-chart.html
├── ch08
│ ├── listing-8-1-chart-accessor-test.js
│ ├── listing-8-2-animation-duration-accessor.js
│ ├── listing-8-3-accessor-with-comments.js
│ └── listing-8-4-add-configuration-to-demo.js
├── ch09
│ ├── bar-chart-code.js
│ ├── bar-chart-test-code.js
│ └── listings.js
├── ch10
│ └── listings.js
├── ch11
│ └── listings.js
└── ch12
│ ├── barChart.js
│ ├── listing-12-7-BarChart.js
│ ├── listing-12-8-D3Bar.js
│ ├── listing-12-9-App.js
│ └── listings.js
└── package.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 4
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "extends": "airbnb",
3 | "plugins": [
4 | "import"
5 | ],
6 | "env": {
7 | "es6": true,
8 | "browser": true,
9 | "jasmine": true,
10 | "amd": true
11 | },
12 | "rules": {
13 | "block-scoped-var": 1,
14 | "comma-style": [2, "last"],
15 | "complexity": 1,
16 | "consistent-this": [0, "self"],
17 | "default-case": 1,
18 | "dot-notation": 0,
19 | "guard-for-in": 1,
20 | "indent": ["warning", 4],
21 | "keyword-spacing": 1,
22 | "newline-after-var": 1,
23 | "no-alert": 2,
24 | "no-console": 2,
25 | "no-debugger": 2,
26 | "no-div-regex": 1,
27 | "no-eq-null": 1,
28 | "no-floating-decimal": 1,
29 | "no-multiple-empty-lines": [2, {"max": 2}],
30 | "no-nested-ternary": 1,
31 | "no-param-reassign": 0,
32 | "no-self-compare": 1,
33 | "no-throw-literal": 1,
34 | "no-underscore-dangle": 0,
35 | "no-unused-vars": [1, {"varsIgnorePattern": "[d3Transition]"}],
36 | "no-void": 1,
37 | "one-var": [1, {"var": "always", "const": "never"}],
38 | "quotes": [2, "single"],
39 | "radix": 1,
40 | "vars-on-top" : 1,
41 | "wrap-iife": [2, "inside"]
42 | },
43 | "parserOptions": {
44 | "sourceType": "module",
45 | "ecmaFeatures": {
46 | "experimentalObjectRestSpread": true
47 | }
48 | }
49 | };
50 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Node rules:
2 |
3 | ## Dependency directory
4 | ## Commenting this out is preferred by some people, see
5 | ## https://docs.npmjs.com/misc/faq#should-i-check-my-node_modules-folder-into-git
6 | node_modules
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # pro-d3-source-code
2 | > Source code for the book Pro D3.js by APress
3 |
4 | ## What’s in This Repository?
5 | We divided this book into 12 chapters. Here they are:
6 | * ch01 - Introduction to data visualizations with D3.js – describes why D3.js and ES2015+ are the best options for creating data visualizations for the web.
7 | * ch02 - An Archetypal D3.js Chart – analyses a typical D3.js chart example, walking through its code and reviewing the benefits and drawbacks of this approach.
8 | * ch03 - D3.js Code Encapsulation and APIs – introduces different strategies developers use to encapsulate D3.js code, illustrating them with real-world library examples, and advising how to pick one of them.
9 | * ch04 - The reusable API – presents this code pattern, which allows composable, extendable, configurable, and testable D3.js code encapsulation. It also discusses its advantages and drawbacks and how to overcome them.
10 | * ch05 - Making the Bar Chart Production-ready – walks through the steps necessary to take the initial archetypal example and transform it into a professional and reusable chart.
11 | * ch06 - Britecharts – introduces Eventbrite’s charting library, a set of charts and support components that follow the previous principles to help developers explore and interpret data.
12 | * ch07 - Using and Customizing Britecharts – goes deep in the day by day use of Britecharts, showing how to compose the library components together to create compelling data visualizations.
13 | * ch08 - Extending a Chart – extends the previous chapter by teaching how to extend Britecharts, modifying a chart, its documentation, and sending a pull request to contribute to the project.
14 | * ch09 - Testing Your Charts – reviews on how to leverage the Reusable API pattern to test-drive a chart, using the initial bar chart as an example.
15 | * ch10 - Building Your Library – illustrates how to build and publish an open-source charting library using Webpack, Babel, and npm.
16 | * ch11 - Creating Documentation – demonstrates the generation of documentation from source code comments, using GitHub pages to host the docs and ESLint to enforce annotations.
17 | * ch12 - Using Your Library with React – explores how to use D3.js within React.js applications, exploring different strategies and putting into practice one of them.
18 |
19 |
--------------------------------------------------------------------------------
/code/ch01/code-listings.js:
--------------------------------------------------------------------------------
1 | // Listing 1-1. D3.js Selections
2 | const svg = d3.select('body')
3 | .append('svg')
4 | .attr('width', 400)
5 | .attr('height', 200)
6 | .style('background-color', 'purple');
7 |
8 |
9 | // Listing 1-2. ES2015 destructuring and default parameters
10 | // Before
11 | function (object) {
12 | var radiant = object.radiant || '',
13 | luminous = object.luminous || '';
14 |
15 | // Use values
16 | }
17 |
18 | // With ES2015
19 | function ({radiant = '', luminous = ''}) {
20 | // Use values
21 | }
22 |
23 |
24 | // Listing 1-3. Simpler code with ES2015 arrow functions
25 | // Before
26 | someArray.map(function(value) {
27 | return value + 1;
28 | });
29 |
30 | // With ES2015
31 | someArray.map((value) => value + 1);
32 |
33 |
34 | // Listing 1-4. String concatenation with ES2015 template literals
35 | // Before
36 | var newLight = 'The new luminosity is ' + light;
37 |
38 | // With ES2015
39 | let newLight = `The new luminosity is ${light}`;
40 |
41 |
42 | // Listing 1-5. Simple array and object combinations with the spread operator
43 | // Before
44 | var lightArray = [ 'radiant', 'vivid' ];
45 | var newLightArray = lightArray.concat([ 'shiny' ]);
46 |
47 | var baseLightObject = {
48 | a: 'radiant',
49 | b: 'vivid'
50 | };
51 | var extraObject = { b: 'silvery' };
52 | var merged = _.extend({}, baseLightObject, extraObject);
53 | // using underscore.js or lodash
54 | var merged = $.extend({}, baseLightObject, extraObject);
55 | // using jquery
56 |
57 |
58 | // With ES2015
59 | let newLightArray = [ ...lightArray, 'shiny' ];
60 |
61 | let merged = { ...baseLightObject, ...extraObject };
62 |
63 |
64 | // Listing 1-6. No variable re-assignments with const
65 | const light = 'radiant';
66 |
67 | light = 'silvery';
68 | // Throws TypeError: Assignment to constant variable.
69 |
70 |
71 | // Listing 1-7. ES2015 destructuring on function signature
72 | // Before
73 | function (object) {
74 | var vivid = object.vivid,
75 | luminous = object.luminous;
76 |
77 | // Use values
78 | }
79 |
80 | // With ES2015
81 | function ({vivid, luminous}) {
82 | // Use values
83 | }
84 |
85 | // Listing 1-8. ES2015 rest parameters avoid the use of arguments
86 | // Before
87 | function (a, b) {
88 | // Transform it into a real array
89 | var arrayOfArguments = [].slice.call(arguments);
90 |
91 | // Use arguments
92 | }
93 |
94 | // With ES2015
95 | function (...args) {
96 | // Use args
97 | }
98 |
99 | // Listing 1-9. ES2015 block scoped variables
100 | // Before
101 | function wrapperFunction(light) {
102 | // The var declaration gets hoisted at this level
103 |
104 | if (light) {
105 | var newLight = light;
106 |
107 | return newLight;
108 | } else {
109 | // newLight here is 'undefined'
110 |
111 | return 'vivid';
112 | }
113 | // newLight here is 'undefined' too!
114 | }
115 |
116 | // With ES2015
117 | function wrapperFunction(light) {
118 | // newLight doesn't exist here
119 |
120 | if (light) {
121 | let newLight = light;
122 | // or
123 | const newLight = light;
124 |
125 | return newLight;
126 | } else {
127 | // newLight doesn't exist here either
128 |
129 | return 'vivid';
130 | }
131 | // newLight doesn't exist here
132 | }
--------------------------------------------------------------------------------
/code/ch02/data-loading.js:
--------------------------------------------------------------------------------
1 | // Listing 2-6. Bar Chart Data Loading (version 5)
2 | d3.tsv("data.tsv")
3 | .then((data) => {
4 | return data.map((d) => {
5 | d.frequency = +d.frequency;
6 |
7 | return d;
8 | });
9 | })
10 | .then((data) => {
11 | // Rest of code here
12 | })
13 | .catch((error) => {
14 | throw error;
15 | });
--------------------------------------------------------------------------------
/code/ch02/data.tsv:
--------------------------------------------------------------------------------
1 | letter frequency
2 | A .08167
3 | B .01492
4 | C .02782
5 | D .04253
6 | E .12702
7 | F .02288
8 | G .02015
9 | H .06094
10 | I .06966
11 | J .00153
12 | K .00772
13 | L .04025
14 | M .02406
15 | N .06749
16 | O .07507
17 | P .01929
18 | Q .00095
19 | R .05987
20 | S .06327
21 | T .09056
22 | U .02758
23 | V .00978
24 | W .02360
25 | X .00150
26 | Y .01974
27 | Z .00074
--------------------------------------------------------------------------------
/code/ch02/original-bar-chart.html:
--------------------------------------------------------------------------------
1 | // Ref: https://blockbuilder.org/Golodhros/01d07c5ad3d92ba008dffd276bd69cbf
2 |
3 |
4 |
21 |
22 |
23 |
24 |
72 |
--------------------------------------------------------------------------------
/code/ch02/original-bar-chart.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | // Listing 2-1. Original Bar Chart by Mike Bostock
4 | var svg = d3.select("svg"),
5 | margin = {top: 20, right: 20, bottom: 30, left: 40},
6 | width = +svg.attr("width") - margin.left - margin.right,
7 | height = +svg.attr("height") - margin.top - margin.bottom;
8 |
9 | var x = d3.scaleBand().rangeRound([0, width]).padding(0.1),
10 | y = d3.scaleLinear().rangeRound([height, 0]);
11 |
12 | var g = svg.append("g")
13 | .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
14 |
15 | d3.tsv("data.tsv", function(d) {
16 | d.frequency = +d.frequency;
17 | return d;
18 | }, function(error, data) {
19 | if (error) throw error;
20 |
21 | x.domain(data.map(function(d) { return d.letter; }));
22 | y.domain([0, d3.max(data, function(d) { return d.frequency; })]);
23 |
24 | g.append("g")
25 | .attr("class", "axis axis--x")
26 | .attr("transform", "translate(0," + height + ")")
27 | .call(d3.axisBottom(x));
28 |
29 | g.append("g")
30 | .attr("class", "axis axis--y")
31 | .call(d3.axisLeft(y).ticks(10, "%"))
32 | .append("text")
33 | .attr("transform", "rotate(-90)")
34 | .attr("y", 6)
35 | .attr("dy", "0.71em")
36 | .attr("text-anchor", "end")
37 | .text("Frequency");
38 |
39 | g.selectAll(".bar")
40 | .data(data)
41 | .enter()
42 | .append("rect")
43 | .attr("class", "bar")
44 | .attr("x", function(d) { return x(d.letter); })
45 | .attr("y", function(d) { return y(d.frequency); })
46 | .attr("width", x.bandwidth())
47 | .attr("height", function(d) { return height - y(d.frequency); });
48 | });
49 |
--------------------------------------------------------------------------------
/code/ch02/updated-bar-chart.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
21 |
22 |
23 |
24 |
77 |
--------------------------------------------------------------------------------
/code/ch03/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | pro-d3-library-demo
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/code/ch03/listing-3-10-d3fc.js:
--------------------------------------------------------------------------------
1 | // Listing 3-10. Bar Chart with D3FC
2 | import {letterFrequency} from './dataset';
3 | import * as fc from 'd3fc';
4 | import * as d3 from 'd3';
5 |
6 |
7 | export const d3fcBarChart = function() {
8 | const barSeries = fc.autoBandwidth(fc.seriesSvgBar())
9 | .crossValue(d => d.letter)
10 | .align('left')
11 | .mainValue(d => d.frequency)
12 | .decorate((selection) => {
13 | selection.select('path')
14 | .style('fill', 'steelblue');
15 | });
16 |
17 | const yExtent = fc.extentLinear()
18 | .accessors([d => d.frequency])
19 | .pad([0, 0.1])
20 | .include([0]);
21 |
22 | const chart = fc.chartSvgCartesian(
23 | d3.scaleBand(),
24 | d3.scaleLinear()
25 | )
26 | .xDomain(letterFrequency.map(d => d.letter))
27 | .xPadding(0.2)
28 | .yDomain(yExtent(letterFrequency))
29 | .yTicks(10, '%')
30 | .yOrient('left')
31 | .plotArea(barSeries);
32 |
33 | d3.select('#d3fc-container')
34 | .datum(letterFrequency)
35 | .call(chart);
36 | }
37 |
--------------------------------------------------------------------------------
/code/ch03/listing-3-11-britecharts.js:
--------------------------------------------------------------------------------
1 | // Listing 3-11. Bar Chart with Britecharts
2 | import {letterFrequency} from './dataset';
3 | import {bar} from 'britecharts';
4 | import {select} from 'd3-selection';
5 |
6 | require('britecharts/dist/css/britecharts.css');
7 |
8 | export const britechartsBarChart = function() {
9 | const barChart = bar();
10 | const barContainer = select('#britecharts-container');
11 |
12 | barChart
13 | .valueLabel('frequency')
14 | .nameLabel('letter')
15 | .width(800)
16 | .height(400);
17 |
18 | barContainer.datum(letterFrequency).call(barChart);
19 | }
20 |
--------------------------------------------------------------------------------
/code/ch03/listing-3-5-dataset.js:
--------------------------------------------------------------------------------
1 | export const letterFrequency = [
2 | {
3 | "letter": "A",
4 | "frequency": 0.08167
5 | },
6 | {
7 | "letter": "B",
8 | "frequency": 0.01492
9 | },
10 | {
11 | "letter": "C",
12 | "frequency": 0.02782
13 | },
14 | {
15 | "letter": "D",
16 | "frequency": 0.04253
17 | },
18 | {
19 | "letter": "E",
20 | "frequency": 0.12702
21 | },
22 | {
23 | "letter": "F",
24 | "frequency": 0.02288
25 | },
26 | {
27 | "letter": "G",
28 | "frequency": 0.02015
29 | },
30 | {
31 | "letter": "H",
32 | "frequency": 0.06094
33 | },
34 | {
35 | "letter": "I",
36 | "frequency": 0.06966
37 | },
38 | {
39 | "letter": "J",
40 | "frequency": 0.00153
41 | },
42 | {
43 | "letter": "K",
44 | "frequency": 0.00772
45 | },
46 | {
47 | "letter": "L",
48 | "frequency": 0.04025
49 | },
50 | {
51 | "letter": "M",
52 | "frequency": 0.02406
53 | },
54 | {
55 | "letter": "N",
56 | "frequency": 0.06749
57 | },
58 | {
59 | "letter": "O",
60 | "frequency": 0.07507
61 | },
62 | {
63 | "letter": "P",
64 | "frequency": 0.01929
65 | },
66 | {
67 | "letter": "Q",
68 | "frequency": 0.00095
69 | },
70 | {
71 | "letter": "R",
72 | "frequency": 0.05987
73 | },
74 | {
75 | "letter": "S",
76 | "frequency": 0.06327
77 | },
78 | {
79 | "letter": "T",
80 | "frequency": 0.09056
81 | },
82 | {
83 | "letter": "U",
84 | "frequency": 0.02758
85 | },
86 | {
87 | "letter": "V",
88 | "frequency": 0.00978
89 | },
90 | {
91 | "letter": "W",
92 | "frequency": 0.0236
93 | },
94 | {
95 | "letter": "X",
96 | "frequency": 0.0015
97 | },
98 | {
99 | "letter": "Y",
100 | "frequency": 0.01974
101 | },
102 | {
103 | "letter": "Z",
104 | "frequency": 0.00074
105 | }
106 | ];
107 |
--------------------------------------------------------------------------------
/code/ch03/listing-3-6-plottable.js:
--------------------------------------------------------------------------------
1 | // Listing 3-6. Bar Chart with Plottable
2 | import {letterFrequency} from './dataset';
3 | import {Scales, Axes, Plots, Dataset, Components} from 'plottable';
4 |
5 | require('plottable/plottable.css');
6 |
7 | export const plottableBarChart = function() {
8 | const xScale = new Scales.Category();
9 | const yScale = new Scales.Linear();
10 |
11 | const xAxis = new Axes.Category(xScale, "bottom");
12 | const yAxis = new Axes.Numeric(yScale, "left");
13 |
14 | const plot = new Plots.Bar();
15 | plot.x(function(d) { return d.letter; }, xScale);
16 | plot.y(function(d) { return d.frequency; }, yScale);
17 |
18 | const dataset = new Dataset(letterFrequency);
19 | plot.addDataset(dataset);
20 |
21 | const chart = new Components.Table([[yAxis, plot], [null, xAxis]]);
22 |
23 | chart.renderTo("#plottable-container");
24 | }
25 |
--------------------------------------------------------------------------------
/code/ch03/listing-3-7-billboard.js:
--------------------------------------------------------------------------------
1 | // Listing 3-7. Bar Chart with Billboard
2 | import {letterFrequency} from './dataset';
3 | import {bb} from 'billboard.js';
4 |
5 | require('billboard.js/dist/billboard.css');
6 |
7 | export const billboardBarChart = function() {
8 |
9 | // Data formatting
10 | const categories = letterFrequency.map(({letter}) => letter);
11 | const frequencies = letterFrequency.map(({frequency}) => frequency);
12 |
13 | bb.generate({
14 | data: {
15 | x: "x",
16 | columns: [
17 | ["x", ...categories],
18 | ["frequency", ...frequencies]
19 | ],
20 | type: "bar"
21 | },
22 | axis: {
23 | x: {
24 | type: "category"
25 | }
26 | },
27 | bindto: "#billboard-container"
28 | });
29 | }
30 |
--------------------------------------------------------------------------------
/code/ch03/listing-3-8-vega.json:
--------------------------------------------------------------------------------
1 | // Listing 3-8. Bar Chart Specification in Vega
2 | {
3 | "$schema": "https://vega.github.io/schema/vega/v4.json",
4 | "width": 800,
5 | "height": 400,
6 | "padding": 10,
7 |
8 | "data": [
9 | {
10 | "name": "table",
11 | "values": [
12 | {"letter": "A", "frequency": 0.08167 },
13 | {"letter": "B", "frequency": 0.01492 },
14 | {"letter": "C", "frequency": 0.02782 },
15 | {"letter": "D", "frequency": 0.04253 },
16 | {"letter": "E", "frequency": 0.12702 },
17 | {"letter": "F", "frequency": 0.02288 },
18 | {"letter": "G", "frequency": 0.02015 },
19 | {"letter": "H", "frequency": 0.06094 },
20 | {"letter": "I", "frequency": 0.06966 },
21 | {"letter": "J", "frequency": 0.00153 },
22 | {"letter": "K", "frequency": 0.00772 },
23 | {"letter": "L", "frequency": 0.04025 },
24 | {"letter": "M", "frequency": 0.02406 },
25 | {"letter": "N", "frequency": 0.06749 },
26 | {"letter": "O", "frequency": 0.07507 },
27 | {"letter": "P", "frequency": 0.01929 },
28 | {"letter": "Q", "frequency": 0.00095 },
29 | {"letter": "R", "frequency": 0.05987 },
30 | {"letter": "S", "frequency": 0.06327 },
31 | {"letter": "T", "frequency": 0.09056 },
32 | {"letter": "U", "frequency": 0.02758 },
33 | {"letter": "V", "frequency": 0.00978 },
34 | {"letter": "W", "frequency": 0.0236 },
35 | {"letter": "X", "frequency": 0.0015 },
36 | {"letter": "Y", "frequency": 0.01974 },
37 | {"letter": "Z", "frequency": 0.00074 }
38 | ]
39 | }
40 | ],
41 |
42 | "signals": [
43 | {
44 | "name": "tooltip",
45 | "value": {},
46 | "on": [
47 | {"events": "rect:mouseover", "update": "datum"},
48 | {"events": "rect:mouseout", "update": "{}"}
49 | ]
50 | }
51 | ],
52 |
53 | "scales": [
54 | {
55 | "name": "xscale",
56 | "type": "band",
57 | "domain": {"data": "table", "field": "letter"},
58 | "range": "width",
59 | "padding": 0.1,
60 | "round": true
61 | },
62 | {
63 | "name": "yscale",
64 | "domain": {"data": "table", "field": "frequency"},
65 | "nice": true,
66 | "range": "height"
67 | }
68 | ],
69 |
70 | "axes": [
71 | { "orient": "bottom", "scale": "xscale" },
72 | { "orient": "left", "scale": "yscale" }
73 | ],
74 |
75 | "marks": [
76 | {
77 | "type": "rect",
78 | "from": {"data":"table"},
79 | "encode": {
80 | "enter": {
81 | "x": {"scale": "xscale", "field": "letter"},
82 | "width": {"scale": "xscale", "band": 1},
83 | "y": {"scale": "yscale", "field": "frequency"},
84 | "y2": {"scale": "yscale", "value": 0}
85 | },
86 | "update": {
87 | "fill": {"value": "steelblue"}
88 | },
89 | "hover": {
90 | "fill": {"value": "red"}
91 | }
92 | }
93 | },
94 | {
95 | "type": "text",
96 | "encode": {
97 | "enter": {
98 | "align": {"value": "center"},
99 | "baseline": {"value": "bottom"},
100 | "fill": {"value": "#333"}
101 | },
102 | "update": {
103 | "x": {"scale": "xscale", "signal": "tooltip.letter", "band": 0.5},
104 | "y": {"scale": "yscale", "signal": "tooltip.frequency", "offset": -2},
105 | "text": {"signal": "tooltip.frequency"},
106 | "fillOpacity": [
107 | {"test": "datum === tooltip", "value": 0},
108 | {"value": 1}
109 | ]
110 | }
111 | }
112 | }
113 | ]
114 | }
115 |
--------------------------------------------------------------------------------
/code/ch03/listing-3-9-vega.js:
--------------------------------------------------------------------------------
1 | // Listing 3-9. Bar Chart Loading in Vega
2 | import {parse, View} from 'vega';
3 |
4 | import * as data from './vega.json';
5 |
6 | export const vegaBarChart = function() {
7 | let view;
8 |
9 | render(data);
10 |
11 | function render(spec) {
12 | view = new View(parse(spec))
13 | .renderer('svg') // set renderer (canvas or svg)
14 | .initialize('#vega-container') // initialize view within parent DOM container
15 | .hover() // enable hover encode set processing
16 | .run();
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/code/ch03/listings-3-1to4.js:
--------------------------------------------------------------------------------
1 | // Listing 3-1. Object Oriented Programming Example
2 | let chart = new Chart({ type: ‘Bar’, color: ‘blue’, data: […]});
3 | chart.render();
4 |
5 | // Listing 3-2. Declarative Programming Example
6 | let chart = Chart.create({
7 | type: 'Bar',
8 | container: '.container',
9 | bar: {
10 | color: 'blue',
11 | padding: 5
12 | },
13 | data: [...]
14 | });
15 |
16 |
17 | // Listing 3-3. Functional Programming Example
18 | let dimensions = {width: '400', height: '300'};
19 | let xAxis = Library.categoricalDataAxis(x, dimensions, data);
20 | let yAxis = Library.numericalDataAxis(y, dimensions, data);
21 | let representation = Library.bars(dimensions, data);
22 |
23 | let chart = Library.compose(xAxis, yAxis, representation);
24 |
25 |
26 | // Listing 3-4. Chained Example
27 | d3.selectAll('p')
28 | .attr('class', 'graf')
29 | .style('color', 'red');
30 |
31 | // equivalent to
32 | var p = d3.selectAll('p');
33 | p.attr('class', 'graf');
34 | p.style('color', 'red');
--------------------------------------------------------------------------------
/code/ch04/listing-4-1.js:
--------------------------------------------------------------------------------
1 | // Listing 4-1. Reusable Chart API core pattern
2 | import * as d3 from 'd3';
3 |
4 | function chart() {
5 | // Private Attributes declaration
6 |
7 | function exports(_selection) {
8 |
9 | _selection.each(function(_data) {
10 | chartWidth = width - margin.left - margin.right;
11 | chartHeight = height - margin.top - margin.bottom;
12 | data = _data;
13 |
14 | buildScales();
15 | buildAxis();
16 | buildSVG(this);
17 | // .. Rest of building blocks
18 |
19 | });
20 | }
21 |
22 | // API
23 |
24 | return exports;
25 | };
26 |
27 | export default chart;
28 |
--------------------------------------------------------------------------------
/code/ch04/listing-4-2.js:
--------------------------------------------------------------------------------
1 | // Listing 4-2. Reusable Chart API accessors
2 | exports.height = function(_x) {
3 | if (!arguments.length) {
4 | return height;
5 | }
6 | height = _x;
7 |
8 | return this;
9 | };
10 |
--------------------------------------------------------------------------------
/code/ch04/listing-4-3.js:
--------------------------------------------------------------------------------
1 | // Listing 4-3. Margin Accessor
2 | exports.margin = function(_x) {
3 | if (!arguments.length) {
4 | return margin;
5 | }
6 | margin = {
7 | ...margin,
8 | ..._x
9 | };
10 |
11 | return this;
12 | };
13 |
--------------------------------------------------------------------------------
/code/ch04/listing-4-4.js:
--------------------------------------------------------------------------------
1 | // Listing 4-4. Using the Reusable Chart API
2 | import * as d3 from 'd3';
3 | import chart from 'chart';
4 |
5 | const data = [...];
6 | const myChart = chart();
7 | const container = d3.select('.container');
8 |
9 | myChart
10 | .height(300)
11 | .width(600)
12 | .margin({
13 | top: 20,
14 | bottom: 20,
15 | });
16 |
17 | container.datum(data).call(myChart);
18 |
--------------------------------------------------------------------------------
/code/ch04/listing-4-5.js:
--------------------------------------------------------------------------------
1 | // Listing 4-5. Creating Multiple Charts with the Reusable Chart API
2 | import * as d3 from 'd3';
3 | import chart from 'chart';
4 |
5 | const myChart = chart();
6 |
7 | const data = [...];
8 | const container = d3.select('.container');
9 |
10 | const alternativeData = [...];
11 | const alternativeContainer = d3.select('.container-alt');
12 |
13 | myChart
14 | .height(300)
15 | .width(600)
16 | .margin({
17 | top: 20,
18 | bottom: 20,
19 | });
20 |
21 | container.datum(data).call(myChart);
22 | alternativeContainer.datum(alternativeData).call(myChart);
23 |
--------------------------------------------------------------------------------
/code/ch04/listing-4-6.js:
--------------------------------------------------------------------------------
1 | // Listing 4-6. Updating Properties
2 | import * as d3 from 'd3';
3 | import _ from 'underscore';
4 |
5 | import chart from 'chart';
6 |
7 | const data = [...];
8 | const myChart = chart();
9 | const container = d3.select('.container');
10 |
11 | myChart
12 | .height(300)
13 | .width(600)
14 | .margin({
15 | top: 20,
16 | bottom: 20,
17 | });
18 |
19 | container.datum(data).call(myChart);
20 |
21 | const redrawChart = function(){
22 | const containerWidth = container.node()
23 | .getBoundingClientRect()
24 | .width;
25 |
26 | myChart.width(containerWidth);
27 |
28 | container.call(myChart);
29 | };
30 |
31 | // Redraw chart on window resize
32 | const waitTime = 200;
33 | window.addEventListener(
34 | 'resize',
35 | _.throttle(redrawChart, waitTime)
36 | );
37 |
--------------------------------------------------------------------------------
/code/ch04/listing-4-7.js:
--------------------------------------------------------------------------------
1 | // Listing 4-7. Accessor Generation
2 | import * as d3 from 'd3';
3 |
4 | function chart() {
5 | // Private Attributes declaration
6 | const privateAttribute1 = 'value';
7 | const privateAttribute2 = 'value2';
8 | //...
9 |
10 | // Public Attributes declaration
11 | const publicAttributes = {
12 | margin: {
13 | top: 10,
14 | right: 10,
15 | bottom: 10,
16 | left: 10,
17 | },
18 | width: 960,
19 | height: 500,
20 | };
21 |
22 | function exports(_selection) {
23 |
24 | _selection.each(function(_data) {
25 | //...
26 |
27 | buildScales();
28 | buildAxis();
29 | // .. Rest of building blocks
30 | });
31 | }
32 |
33 | // Building blocks functions
34 |
35 | function generateAccessor(attr) {
36 | function accessor(value) {
37 | if (!arguments.length) {
38 | return publicAttributes[attr];
39 | }
40 | publicAttributes[attr] = value;
41 |
42 | return chart;
43 | }
44 |
45 | return accessor;
46 | }
47 |
48 | // API Generation
49 | for (let attr in publicAttributes) {
50 | if (
51 | (!chart[attr]) &&
52 | (publicAttributes.hasOwnProperty(attr))
53 | ) {
54 | chart[attr] = generateAccessor(attr);
55 | }
56 | }
57 |
58 | return exports;
59 | };
60 |
61 | export default chart;
62 |
63 |
--------------------------------------------------------------------------------
/code/ch04/reusable-chart-api.js:
--------------------------------------------------------------------------------
1 | import * as d3 from 'd3';
2 |
3 | function chart() {
4 | let svg;
5 | let data;
6 |
7 | let chartHeight;
8 | let chartWidth;
9 | let height = 400;
10 | let width = 800;
11 | let margin = {
12 | bottom: 10,
13 | left: 10,
14 | right: 10,
15 | top: 10,
16 | };
17 |
18 | function exports(_selection) {
19 |
20 | _selection.each(function(_data) {
21 | chartWidth = width - margin.left - margin.right;
22 | chartHeight = height - margin.top - margin.bottom;
23 | data = _data;
24 |
25 | buildScales();
26 | buildAxis();
27 | buildSVG(this);
28 | // .. Rest of building blocks
29 |
30 | });
31 | }
32 |
33 | // API
34 | exports.height = function(_x) {
35 | if (!arguments.length) {
36 | return height;
37 | }
38 | height = _x;
39 |
40 | return this;
41 | };
42 |
43 | exports.margin = function(_x) {
44 | if (!arguments.length) {
45 | return margin;
46 | }
47 | margin = _x;
48 |
49 | return this;
50 | };
51 |
52 | exports.width = function(_x) {
53 | if (!arguments.length) {
54 | return width;
55 | }
56 | width = _x;
57 |
58 | return this;
59 | };
60 |
61 | return exports;
62 | };
63 |
64 | export default chart;
65 |
--------------------------------------------------------------------------------
/code/ch05/bars-and-accessors.js:
--------------------------------------------------------------------------------
1 | import * as d3 from 'd3';
2 |
3 | function bar() {
4 | let data;
5 | let svg;
6 | let margin = {
7 | top: 20,
8 | bottom: 40,
9 | left: 40,
10 | right: 20
11 | };
12 | let width = 600;
13 | let height = 400;
14 | let chartWidth;
15 | let chartHeight;
16 | let xScale;
17 | let yScale;
18 | let xAxis;
19 | let yAxis;
20 |
21 | const getFrequency = ({frequency}) => frequency;
22 | const getLetter = ({letter}) => letter;
23 |
24 |
25 | function exports(_selection) {
26 | _selection.each(function(_data) {
27 | data = _data;
28 | chartHeight = height - margin.bottom - margin.top;
29 | chartWidth = width - margin.left - margin.right;
30 |
31 | // Main sequence here
32 | buildScales();
33 | buildAxes();
34 | buildSVG(this);
35 | drawAxes();
36 | drawBars();
37 | });
38 | }
39 |
40 | /**
41 | * Creates the d3 x and y axes, setting orientations
42 | * @private
43 | */
44 | function buildAxes(){
45 | xAxis = d3.axisBottom(xScale);
46 |
47 | yAxis = d3.axisLeft(yScale)
48 | .ticks(10, '%');
49 | }
50 |
51 | /**
52 | * Builds containers for the chart, the axis and a wrapper for all of them
53 | * Also applies the Margin convention
54 | * @private
55 | */
56 | function buildContainerGroups(){
57 | let container = svg
58 | .append('g')
59 | .classed('container-group', true)
60 | .attr(
61 | 'transform',
62 | `translate(${margin.left},${margin.top})`
63 | );
64 |
65 | container
66 | .append('g')
67 | .classed('chart-group', true);
68 | container
69 | .append('g')
70 | .classed('x-axis-group axis', true);
71 | container
72 | .append('g')
73 | .classed('y-axis-group axis', true);
74 | }
75 |
76 | /**
77 | * Creates the x and y scales of the graph
78 | * @private
79 | */
80 | function buildScales(){
81 | xScale = d3.scaleBand()
82 | .rangeRound([0, chartWidth])
83 | .padding(0.1)
84 | .domain(data.map(getLetter));
85 |
86 | yScale = d3.scaleLinear()
87 | .rangeRound([chartHeight, 0])
88 | .domain([0, d3.max(data, getFrequency)]);
89 | }
90 |
91 | /**
92 | * Builds the root SVG and gives it dimensions
93 | * @param {HTMLElement} container DOM element that will work as the container of the graph
94 | * @private
95 | */
96 | function buildSVG(container){
97 | if (!svg) {
98 | svg = d3.select(container)
99 | .append('svg')
100 | .classed('bar-chart', true);
101 |
102 | buildContainerGroups()
103 | }
104 |
105 | svg
106 | .attr('width', width)
107 | .attr('height', height);
108 | }
109 |
110 | /**
111 | * Draws the x and y axis on the svg object within their
112 | * respective groups
113 | * @private
114 | */
115 | function drawAxes(){
116 | svg.select('.x-axis-group.axis')
117 | .attr('transform', `translate(0,${chartHeight})`)
118 | .call(xAxis);
119 |
120 | svg.select('.y-axis-group.axis')
121 | .call(yAxis)
122 | .append('text')
123 | .attr('transform', 'rotate(-90)')
124 | .attr('y', 6)
125 | .attr('dy', '0.71em')
126 | .attr('text-anchor', 'end')
127 | .text('Frequency');
128 | }
129 |
130 | /**
131 | * Draws the bar elements within the chart group
132 | * @private
133 | */
134 | function drawBars(){
135 | // Select the bars, and bind the data to the .bar elements
136 | let bars = svg.select('.chart-group').selectAll('.bar')
137 | .data(data);
138 |
139 | // Enter
140 | // Create bars for the new elements
141 | bars.enter()
142 | .append('rect')
143 | .classed('bar', true)
144 | .attr('x', ({letter}) => xScale(letter))
145 | .attr('y', ({frequency}) => yScale(frequency))
146 | .attr('width', xScale.bandwidth())
147 | .attr('height', ({frequency}) => chartHeight - yScale(frequency));
148 |
149 | // Exit
150 | // Remove old elements by first fading them
151 | bars.exit()
152 | .style('opacity', 0)
153 | .remove();
154 | }
155 |
156 | // API
157 | /**
158 | * Gets or Sets the height of the chart
159 | * @param {number} _x Desired width for the graph
160 | * @return {height | module} Current height or Bar Char module to chain calls
161 | * @public
162 | */
163 | exports.height = function(_x) {
164 | if (!arguments.length) {
165 | return height;
166 | }
167 | height = _x;
168 |
169 | return this;
170 | };
171 |
172 | /**
173 | * Gets or Sets the margin of the chart
174 | * @param {object} _x Margin object to get/set
175 | * @return {margin | module} Current margin or Bar Chart module to chain calls
176 | * @public
177 | */
178 | exports.margin = function(_x) {
179 | if (!arguments.length) {
180 | return margin;
181 | }
182 | margin = {
183 | ...margin,
184 | ..._x
185 | };
186 |
187 | return this;
188 | };
189 |
190 | /**
191 | * Gets or Sets the width of the chart
192 | * @param {number} _x Desired width for the graph
193 | * @return {width | module} Current width or Bar Chart module to chain calls
194 | * @public
195 | */
196 | exports.width = function(_x) {
197 | if (!arguments.length) {
198 | return width;
199 | }
200 | width = _x;
201 |
202 | return this;
203 | };
204 |
205 | return exports;
206 | };
207 |
208 | export default bar;
209 |
--------------------------------------------------------------------------------
/code/ch05/events.js:
--------------------------------------------------------------------------------
1 | import * as d3 from 'd3';
2 |
3 | function bar() {
4 | let data;
5 | let svg;
6 | let margin = {
7 | top: 20,
8 | bottom: 40,
9 | left: 40,
10 | right: 20
11 | };
12 | let width = 600;
13 | let height = 400;
14 | let chartWidth;
15 | let chartHeight;
16 | let xScale;
17 | let yScale;
18 | let xAxis;
19 | let yAxis;
20 |
21 | // Dispatcher object to broadcast the 'customHover' event
22 | const dispatcher = d3.dispatch('customMouseOver');
23 |
24 | const getFrequency = ({frequency}) => frequency;
25 | const getLetter = ({letter}) => letter;
26 |
27 |
28 | function exports(_selection) {
29 | _selection.each(function(_data) {
30 | data = _data;
31 | chartHeight = height - margin.bottom - margin.top;
32 | chartWidth = width - margin.left - margin.right;
33 |
34 | // Main sequence here
35 | buildScales();
36 | buildAxes();
37 | buildSVG(this);
38 | drawAxes();
39 | drawBars();
40 | });
41 | }
42 |
43 | /**
44 | * Creates the d3 x and y axes, setting orientations
45 | * @private
46 | */
47 | function buildAxes(){
48 | xAxis = d3.axisBottom(xScale);
49 |
50 | yAxis = d3.axisLeft(yScale)
51 | .ticks(10, '%');
52 | }
53 |
54 | /**
55 | * Builds containers for the chart, the axis and a wrapper for all of them
56 | * Also applies the Margin convention
57 | * @private
58 | */
59 | function buildContainerGroups(){
60 | let container = svg
61 | .append('g')
62 | .classed('container-group', true)
63 | .attr(
64 | 'transform',
65 | `translate(${margin.left},${margin.top})`
66 | );
67 |
68 | container
69 | .append('g')
70 | .classed('chart-group', true);
71 | container
72 | .append('g')
73 | .classed('x-axis-group axis', true);
74 | container
75 | .append('g')
76 | .classed('y-axis-group axis', true);
77 | }
78 |
79 | /**
80 | * Creates the x and y scales of the graph
81 | * @private
82 | */
83 | function buildScales(){
84 | xScale = d3.scaleBand()
85 | .rangeRound([0, chartWidth])
86 | .padding(0.1)
87 | .domain(data.map(getLetter));
88 |
89 | yScale = d3.scaleLinear()
90 | .rangeRound([chartHeight, 0])
91 | .domain([0, d3.max(data, getFrequency)]);
92 | }
93 |
94 | /**
95 | * Builds the root SVG and gives it dimensions
96 | * @param {HTMLElement} container DOM element that will work as the container of the graph
97 | * @private
98 | */
99 | function buildSVG(container){
100 | if (!svg) {
101 | svg = d3.select(container)
102 | .append('svg')
103 | .classed('bar-chart', true);
104 |
105 | buildContainerGroups()
106 | }
107 |
108 | svg
109 | .attr('width', width)
110 | .attr('height', height);
111 | }
112 |
113 | /**
114 | * Draws the x and y axis on the svg object within their
115 | * respective groups
116 | * @private
117 | */
118 | function drawAxes(){
119 | svg.select('.x-axis-group.axis')
120 | .attr('transform', `translate(0,${chartHeight})`)
121 | .call(xAxis);
122 |
123 | svg.select('.y-axis-group.axis')
124 | .call(yAxis)
125 | .append('text')
126 | .attr('transform', 'rotate(-90)')
127 | .attr('y', 6)
128 | .attr('dy', '0.71em')
129 | .attr('text-anchor', 'end')
130 | .text('Frequency');
131 | }
132 |
133 | /**
134 | * Draws the bar elements within the chart group
135 | * @private
136 | */
137 | function drawBars(){
138 | // Select the bars, and bind the data to the .bar elements
139 | let bars = svg.select('.chart-group').selectAll('.bar')
140 | .data(data);
141 |
142 | // Enter
143 | // Create bars for the new elements
144 | bars.enter()
145 | .append('rect')
146 | .classed('bar', true)
147 | .attr('x', ({letter}) => xScale(letter))
148 | .attr('y', ({frequency}) => yScale(frequency))
149 | .attr('width', xScale.bandwidth())
150 | .attr('height', ({frequency}) => chartHeight - yScale(frequency))
151 | .on('mouseover', function(d) {
152 | dispatcher.call('customMouseOver', this, d);
153 | });
154 |
155 | // Exit
156 | // Remove old elements by first fading them
157 | bars.exit()
158 | .style('opacity', 0)
159 | .remove();
160 | }
161 |
162 | // API
163 | /**
164 | * Gets or Sets the height of the chart
165 | * @param {number} _x Desired width for the graph
166 | * @return {height | module} Current height or Bar Char module to chain calls
167 | * @public
168 | */
169 | exports.height = function(_x) {
170 | if (!arguments.length) return height;
171 | height = _x;
172 |
173 | return this;
174 | };
175 |
176 | /**
177 | * Gets or Sets the margin of the chart
178 | * @param {object} _x Margin object to get/set
179 | * @return {margin | module} Current margin or Bar Chart module to chain calls
180 | * @public
181 | */
182 | exports.margin = function(_x) {
183 | if (!arguments.length) {
184 | return margin;
185 | }
186 | margin = {
187 | ...margin,
188 | ..._x
189 | };
190 |
191 | return this;
192 | };
193 |
194 | /**
195 | * Exposes an 'on' method that acts as a bridge with the event dispatcher
196 | * We are going to expose the customMouseOver event
197 | *
198 | * @return {module} Bar Chart
199 | * @public
200 | * @example
201 | * barChart.on('customMouseOver', function(event, data) {...});
202 | */
203 | exports.on = function() {
204 | let value = dispatcher.on.apply(dispatcher, arguments);
205 |
206 | return value === dispatcher ? exports : value;
207 | };
208 |
209 | /**
210 | * Gets or Sets the width of the chart
211 | * @param {number} _x Desired width for the graph
212 | * @return {width | module} Current width or Bar Chart module to chain calls
213 | * @public
214 | */
215 | exports.width = function(_x) {
216 | if (!arguments.length) return width;
217 | width = _x;
218 |
219 | return this;
220 | };
221 |
222 | return exports;
223 | };
224 |
225 | export default bar;
226 |
--------------------------------------------------------------------------------
/code/ch05/listing-5-1-core-pattern.js:
--------------------------------------------------------------------------------
1 | // Listing 5-1. Core Pattern
2 | import * as d3 from 'd3';
3 |
4 | function bar() {
5 | let data;
6 |
7 | function exports(_selection) {
8 | _selection.each(function(_data) {
9 | data = _data;
10 |
11 | // Main sequence here
12 | });
13 | }
14 |
15 | return exports;
16 | };
17 |
18 | export default bar;
19 |
--------------------------------------------------------------------------------
/code/ch05/listing-5-2-building-svg.js:
--------------------------------------------------------------------------------
1 | // Listing 5-2. Building SVG
2 | import * as d3 from 'd3';
3 |
4 |
5 | function bar() {
6 | let data;
7 | let svg;
8 | let margin = {
9 | top: 20,
10 | bottom: 40,
11 | left: 40,
12 | right: 20
13 | };
14 | let width = 600;
15 | let height = 400;
16 | let chartWidth;
17 | let chartHeight;
18 |
19 | function exports(_selection) {
20 | _selection.each(function(_data) {
21 | data = _data;
22 | chartHeight = height - margin.bottom - margin.top;
23 | chartWidth = width - margin.left - margin.right;
24 |
25 | // Main sequence here
26 | buildSVG(this);
27 | });
28 | }
29 |
30 | /**
31 | * Builds the root SVG and gives it dimensions
32 | * @param {HTMLElement} container DOM element that will work as the container of the graph
33 | * @private
34 | */
35 | function buildSVG(container){
36 | if (!svg) {
37 | svg = d3.select(container)
38 | .append('svg')
39 | .classed('bar-chart', true)
40 | .append('g')
41 | .attr(
42 | 'transform',
43 | `translate(${margin.left},${margin.top})`
44 | );
45 | }
46 |
47 | svg
48 | .attr('width', width)
49 | .attr('height', height);
50 | }
51 |
52 | return exports;
53 | };
54 |
55 | export default bar;
56 |
--------------------------------------------------------------------------------
/code/ch05/listing-5-20-final-chart.js:
--------------------------------------------------------------------------------
1 | // Listing 5-20. Final Bar Chart
2 | import * as d3 from 'd3';
3 |
4 | function bar() {
5 | // Attributes
6 | let data;
7 | let svg;
8 | let margin = {
9 | top: 20,
10 | right: 20,
11 | bottom: 30,
12 | left: 40
13 | };
14 | let width = 960;
15 | let height = 500;
16 | let chartWidth;
17 | let chartHeight;
18 | let xScale;
19 | let yScale;
20 | let xAxis;
21 | let yAxis;
22 |
23 | // Dispatcher object to broadcast the 'customHover' event
24 | const dispatcher = d3.dispatch('customMouseOver');
25 |
26 | // extractors
27 | const getFrequency = ({frequency}) => frequency;
28 | const getLetter = ({letter}) => letter;
29 |
30 |
31 | function exports(_selection){
32 | _selection.each(function(_data){
33 | data = _data;
34 | chartHeight = height - margin.top - margin.bottom;
35 | chartWidth = width - margin.left - margin.right;
36 |
37 | buildScales();
38 | buildAxes();
39 | buildSVG(this);
40 | drawAxes();
41 | drawBars();
42 | });
43 | }
44 |
45 | function buildAxes(){
46 | xAxis = d3.axisBottom(xScale);
47 |
48 | yAxis = d3.axisLeft(yScale)
49 | .ticks(10, '%');
50 | }
51 |
52 | function buildContainerGroups(){
53 | let container = svg
54 | .append('g')
55 | .classed('container-group', true)
56 | .attr(
57 | 'transform',
58 | `translate(${margin.left},${margin.top})`
59 | );
60 |
61 | container
62 | .append('g')
63 | .classed('chart-group', true);
64 | container
65 | .append('g')
66 | .classed('x-axis-group axis', true);
67 | container
68 | .append('g')
69 | .classed('y-axis-group axis', true);
70 | }
71 |
72 | function buildScales(){
73 | xScale = d3.scaleBand()
74 | .rangeRound([0, chartWidth])
75 | .padding(0.1)
76 | .domain(data.map(getLetter));
77 |
78 | yScale = d3.scaleLinear()
79 | .rangeRound([chartHeight, 0])
80 | .domain([0, d3.max(data, getFrequency)]);
81 | }
82 |
83 | function buildSVG(container){
84 | if (!svg) {
85 | svg = d3.select(container)
86 | .append('svg')
87 | .classed('bar-chart', true);
88 |
89 | buildContainerGroups();
90 | }
91 | svg
92 | .attr('width', width)
93 | .attr('height', height);
94 | }
95 |
96 | function drawAxes(){
97 | svg.select('.x-axis-group.axis')
98 | .attr('transform', `translate(0,${chartHeight})`)
99 | .call(xAxis);
100 |
101 | svg.select('.y-axis-group.axis')
102 | .call(yAxis)
103 | .append('text')
104 | .attr('transform', 'rotate(-90)')
105 | .attr('y', 6)
106 | .attr('dy', '0.71em')
107 | .attr('text-anchor', 'end')
108 | .text('Frequency');
109 | }
110 |
111 | function drawBars(){
112 | let bars = svg.select('.chart-group').selectAll('.bar')
113 | .data(data);
114 |
115 | // Enter
116 | bars.enter()
117 | .append('rect')
118 | .classed('bar', true)
119 | .attr('x', ({letter}) => xScale(letter))
120 | .attr('y', ({frequency}) => yScale(frequency))
121 | .attr('width', xScale.bandwidth())
122 | .attr('height', ({frequency}) => chartHeight - yScale(frequency))
123 | .on('mouseover', function(d) {
124 | dispatcher.call('customMouseOver', this, d);
125 | });
126 |
127 | // Exit
128 | bars.exit()
129 | .style('opacity', 0)
130 | .remove();
131 | }
132 |
133 | exports.height = function(_x) {
134 | if (!arguments.length) {
135 | return height;
136 | }
137 | height = _x;
138 |
139 | return this;
140 | };
141 |
142 | exports.margin = function(_x) {
143 | if (!arguments.length) {
144 | return margin;
145 | }
146 | margin = {
147 | ...margin,
148 | ..._x
149 | };
150 |
151 | return this;
152 | };
153 |
154 | exports.on = function() {
155 | let value = dispatcher.on.apply(dispatcher, arguments);
156 |
157 | return value === dispatcher ? exports : value;
158 | };
159 |
160 | exports.width = function(_x) {
161 | if (!arguments.length) {
162 | return width;
163 | }
164 | width = _x;
165 |
166 | return this;
167 | };
168 |
169 | return exports;
170 | };
171 |
172 | export default bar;
173 |
--------------------------------------------------------------------------------
/code/ch05/listing-5-21-using-bar-chart.js:
--------------------------------------------------------------------------------
1 | import bar from './final-chart.js';
2 | import * as d3 from 'd3';
3 |
4 | let container = d3.select('.chart-container');
5 | let barChart = bar();
6 | let dataset = [...];
7 |
8 | barChart
9 | .width(300)
10 | .height(200)
11 | .margin({
12 | left: 50,
13 | bottom: 30
14 | })
15 | .on('customMouseOver', function(event, data) {
16 | console.log('data', data);
17 | });
18 |
19 | container.datum(dataset).call(barChart);
20 |
--------------------------------------------------------------------------------
/code/ch05/listing-5-6-containers.js:
--------------------------------------------------------------------------------
1 | // Listing 5-6. Building Containers
2 | import * as d3 from 'd3';
3 |
4 | function bar() {
5 | let data;
6 | let svg;
7 | let margin = {
8 | top: 20,
9 | bottom: 40,
10 | left: 40,
11 | right: 20
12 | };
13 | let width = 600;
14 | let height = 400;
15 | let chartWidth;
16 | let chartHeight;
17 |
18 | function exports(_selection) {
19 | _selection.each(function(_data) {
20 | data = _data;
21 | chartHeight = height - margin.bottom - margin.top;
22 | chartWidth = width - margin.left - margin.right;
23 |
24 | // Main sequence here
25 | buildSVG(this);
26 | });
27 | }
28 |
29 | /**
30 | * Builds containers for the chart, the axis and a wrapper for all of them
31 | * Also applies the Margin convention
32 | * @private
33 | */
34 | function buildContainerGroups(){
35 | let container = svg
36 | .append('g')
37 | .classed('container-group', true)
38 | .attr(
39 | 'transform',
40 | `translate(${margin.left},${margin.top})`
41 | );
42 |
43 | container
44 | .append('g')
45 | .classed('chart-group', true);
46 | container
47 | .append('g')
48 | .classed('x-axis-group axis', true);
49 | container
50 | .append('g')
51 | .classed('y-axis-group axis', true);
52 | }
53 |
54 | /**
55 | * Builds the root SVG and gives it dimensions
56 | * @param {HTMLElement} container DOM element that will work as the container of the graph
57 | * @private
58 | */
59 | function buildSVG(container){
60 | if (!svg) {
61 | svg = d3.select(container)
62 | .append('svg')
63 | .classed('bar-chart', true);
64 |
65 | buildContainerGroups();
66 | }
67 |
68 | svg
69 | .attr('width', width)
70 | .attr('height', height);
71 | }
72 |
73 | return exports;
74 | };
75 |
76 | export default bar;
77 |
--------------------------------------------------------------------------------
/code/ch05/listings.js:
--------------------------------------------------------------------------------
1 | // Listing 5-3. Declarting variables
2 | let margin = {
3 | top: 20,
4 | bottom: 40,
5 | left: 40,
6 | right: 20
7 | };
8 | let width = 600;
9 | let height = 400;
10 |
11 |
12 | // Listing 5-4. Setting height and width
13 | ...
14 | let chartWidth;
15 | let chartHeight;
16 |
17 | function exports(_selection) {
18 | _selection.each(function(_data) {
19 | data = _data;
20 | chartHeight = height - margin.bottom - margin.top;
21 | chartWidth = width - margin.left - margin.right;
22 |
23 | // Main sequence here
24 | buildSVG(this);
25 | });
26 | }
27 | ...
28 |
29 |
30 | // Listing 5-5. Building the root SVG
31 | function buildSVG(container){
32 | if (!svg) {
33 | svg = d3.select(container)
34 | .append('svg')
35 | .classed('bar-chart', true)
36 | .append('g')
37 | .attr(
38 | 'transform',
39 | `translate(${margin.left},${margin.top})`
40 | );
41 | }
42 |
43 | svg
44 | .attr('width', width)
45 | .attr('height', height);
46 | }
47 |
48 | // Listing 5-7. Building containers
49 | function buildSVG(container){
50 | if (!svg) {
51 | svg = d3.select(container)
52 | .append('svg')
53 | .classed('bar-chart', true);
54 |
55 | buildContainerGroups();
56 | }
57 |
58 | svg
59 | .attr('width', width)
60 | .attr('height', height);
61 | }
62 |
63 | // Listing 5-8. Scales and axes
64 | function buildScales(){
65 | xScale = d3.scaleBand()
66 | .rangeRound([0, chartWidth])
67 | .padding(0.1)
68 | .domain(data.map(getLetter));
69 |
70 | yScale = d3.scaleLinear()
71 | .rangeRound([chartHeight, 0])
72 | .domain([0, d3.max(data, getFrequency)]);
73 | }
74 |
75 | // Listing 5-9. Extractor functions
76 | const getFrequency = ({frequency}) => frequency;
77 | const getLetter = ({letter}) => letter;
78 |
79 | // Listing 5-10. Scales and Axes
80 | function buildAxes(){
81 | xAxis = d3.axisBottom(xScale);
82 |
83 | yAxis = d3.axisLeft(yScale)
84 | .ticks(10, '%');
85 | }
86 |
87 | // Listing 5-11. Scales and Axes
88 | function exports(_selection) {
89 | _selection.each(function(_data) {
90 | data = _data;
91 | chartHeight = height - margin.bottom - margin.top;
92 | chartWidth = width - margin.left - margin.right;
93 |
94 | // Main sequence here
95 | buildScales();
96 | buildAxes();
97 | buildSVG(this);
98 | });
99 | }
100 |
101 | // Listing 5-12. Scales and axes
102 | function drawAxes(){
103 | svg.select('.x-axis-group.axis')
104 | .attr('transform', `translate(0,${chartHeight})`)
105 | .call(xAxis);
106 |
107 | svg.select('.y-axis-group.axis')
108 | .call(yAxis)
109 | .append('text')
110 | .attr('transform', 'rotate(-90)')
111 | .attr('y', 6)
112 | .attr('dy', '0.71em')
113 | .attr('text-anchor', 'end')
114 | .text('Frequency');
115 | }
116 |
117 | // Listing 5-13. Bars and accessors
118 | function drawBars(){
119 | // Select the bars, and bind the data to the .bar elements
120 | let bars = svg.select('.chart-group').selectAll('.bar')
121 | .data(data);
122 |
123 | // Enter
124 | // Create bars for the new elements
125 | bars.enter()
126 | .append('rect')
127 | .classed('bar', true)
128 | .attr('x', ({letter}) => xScale(letter))
129 | .attr('y', ({frequency}) => yScale(frequency))
130 | .attr('width', xScale.bandwidth())
131 | .attr('height', ({frequency}) => chartHeight - yScale(frequency));
132 |
133 | // Exit
134 | // Remove old elements by first fading them
135 | bars.exit()
136 | .style('opacity', 0)
137 | .remove();
138 | }
139 |
140 | // Listing 5-14. Bars and accessors
141 | exports.height = function(_x) {
142 | if (!arguments.length) {
143 | return height;
144 | }
145 | height = _x;
146 |
147 | return this;
148 | };
149 |
150 | // Listing 5-16. Events
151 | // Dispatcher object that broadcast the 'customMouseOver' event
152 | const dispatcher = d3.dispatch('customMouseOver');
153 |
154 | // Listing 5-17. Wiring events
155 | function drawBars(){
156 | // Select the bars, and bind the data to the .bar elements
157 | let bars = svg.select('.chart-group').selectAll('.bar')
158 | .data(data);
159 |
160 | // Enter
161 | // Create bars for the new elements
162 | bars.enter()
163 | .append('rect')
164 | .classed('bar', true)
165 | .attr('x', ({letter}) => xScale(letter))
166 | .attr('y', ({frequency}) => yScale(frequency))
167 | .attr('width', xScale.bandwidth())
168 | .attr('height', ({frequency}) => chartHeight - yScale(frequency))
169 | .on('mouseover', function(d) {
170 | dispatcher.call('customMouseOver', this, d);
171 | });
172 |
173 | // Exit
174 | // Remove old elements by first fading them
175 | bars.exit()
176 | .style('opacity', 0)
177 | .remove();
178 | }
179 |
180 | // Listing 5-18. Events
181 | exports.on = function() {
182 | let value = dispatcher.on.apply(dispatcher, arguments);
183 |
184 | return value === dispatcher ? exports : value;
185 | };
186 |
187 |
188 | // Listing 5-19. Events
189 | barChart.on('customMouseOver', function(event, data) {
190 | console.log('data', data);
191 | });
192 |
--------------------------------------------------------------------------------
/code/ch05/original-bar-chart.js:
--------------------------------------------------------------------------------
1 |
2 | var svg = d3.select("svg"),
3 | margin = {top: 20, right: 20, bottom: 30, left: 40},
4 | width = +svg.attr("width") - margin.left - margin.right,
5 | height = +svg.attr("height") - margin.top - margin.bottom;
6 |
7 | var x = d3.scaleBand().rangeRound([0, width]).padding(0.1),
8 | y = d3.scaleLinear().rangeRound([height, 0]);
9 |
10 | var g = svg.append("g")
11 | .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
12 |
13 | d3.tsv("data.tsv", function(d) {
14 | d.frequency = +d.frequency;
15 | return d;
16 | }, function(error, data) {
17 | if (error) throw error;
18 |
19 | x.domain(data.map(function(d) { return d.letter; }));
20 | y.domain([0, d3.max(data, function(d) { return d.frequency; })]);
21 |
22 | g.append("g")
23 | .attr("class", "axis axis--x")
24 | .attr("transform", "translate(0," + height + ")")
25 | .call(d3.axisBottom(x));
26 |
27 | g.append("g")
28 | .attr("class", "axis axis--y")
29 | .call(d3.axisLeft(y).ticks(10, "%"))
30 | .append("text")
31 | .attr("transform", "rotate(-90)")
32 | .attr("y", 6)
33 | .attr("dy", "0.71em")
34 | .attr("text-anchor", "end")
35 | .text("Frequency");
36 |
37 | g.selectAll(".bar")
38 | .data(data)
39 | .enter()
40 | .append("rect")
41 | .attr("class", "bar")
42 | .attr("x", function(d) { return x(d.letter); })
43 | .attr("y", function(d) { return y(d.frequency); })
44 | .attr("width", x.bandwidth())
45 | .attr("height", function(d) { return height - y(d.frequency); });
46 | });
47 |
--------------------------------------------------------------------------------
/code/ch05/scales-and-axes.js:
--------------------------------------------------------------------------------
1 | import * as d3 from 'd3';
2 |
3 | function bar() {
4 | let data;
5 | let svg;
6 | let margin = {
7 | top: 20,
8 | bottom: 40,
9 | left: 40,
10 | right: 20
11 | };
12 | let width = 600;
13 | let height = 400;
14 | let chartWidth;
15 | let chartHeight;
16 | let xScale;
17 | let yScale;
18 | let xAxis;
19 | let yAxis;
20 |
21 | const getFrequency = ({frequency}) => frequency;
22 | const getLetter = ({letter}) => letter;
23 |
24 | function exports(_selection) {
25 | _selection.each(function(_data) {
26 | data = _data;
27 | chartHeight = height - margin.bottom - margin.top;
28 | chartWidth = width - margin.left - margin.right;
29 |
30 | // Main sequence here
31 | buildScales();
32 | buildAxes();
33 | buildSVG(this);
34 | drawAxes();
35 | });
36 | }
37 |
38 | /**
39 | * Creates the d3 x and y axes, setting orientations
40 | * @private
41 | */
42 | function buildAxes(){
43 | xAxis = d3.axisBottom(xScale);
44 |
45 | yAxis = d3.axisLeft(yScale)
46 | .ticks(10, '%');
47 | }
48 |
49 | /**
50 | * Builds containers for the chart, the axis and a wrapper for all of them
51 | * Also applies the Margin convention
52 | * @private
53 | */
54 | function buildContainerGroups(){
55 | let container = svg
56 | .append('g')
57 | .classed('container-group', true)
58 | .attr(
59 | 'transform',
60 | `translate(${margin.left},${margin.top})`
61 | );
62 |
63 | container
64 | .append('g')
65 | .classed('chart-group', true);
66 | container
67 | .append('g')
68 | .classed('x-axis-group axis', true);
69 | container
70 | .append('g')
71 | .classed('y-axis-group axis', true);
72 | }
73 |
74 | /**
75 | * Creates the x and y scales of the graph
76 | * @private
77 | */
78 | function buildScales(){
79 | xScale = d3.scaleBand()
80 | .rangeRound([0, chartWidth])
81 | .padding(0.1)
82 | .domain(data.map(getLetter));
83 |
84 | yScale = d3.scaleLinear()
85 | .rangeRound([chartHeight, 0])
86 | .domain([0, d3.max(data, getFrequency)]);
87 | }
88 |
89 | /**
90 | * Builds the root SVG and gives it dimensions
91 | * @param {HTMLElement} container DOM element that will work as the container of the graph
92 | * @private
93 | */
94 | function buildSVG(container){
95 | if (!svg) {
96 | svg = d3.select(container)
97 | .append('svg')
98 | .classed('bar-chart', true);
99 |
100 | buildContainerGroups()
101 | }
102 |
103 | svg
104 | .attr('width', width)
105 | .attr('height', height);
106 | }
107 |
108 | /**
109 | * Draws the x and y axis on the svg object within their
110 | * respective groups
111 | * @private
112 | */
113 | function drawAxes(){
114 | svg.select('.x-axis-group.axis')
115 | .attr('transform', `translate(0,${chartHeight})`)
116 | .call(xAxis);
117 |
118 | svg.select('.y-axis-group.axis')
119 | .call(yAxis)
120 | .append('text')
121 | .attr('transform', 'rotate(-90)')
122 | .attr('y', 6)
123 | .attr('dy', '0.71em')
124 | .attr('text-anchor', 'end')
125 | .text('Frequency');
126 | }
127 |
128 | return exports;
129 | };
130 |
131 | export default bar;
132 |
--------------------------------------------------------------------------------
/code/ch07/line-chart-with-brush.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Tutorial - Composing Data Visualizations with Britecharts
6 |
7 |
8 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | Chapter 6: Using and Customizing Britecharts
23 |
24 |
25 |
26 |
27 |
28 |
480 |
481 |
482 |
--------------------------------------------------------------------------------
/code/ch07/line-chart-with-custom-colors.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Tutorial - Composing Data Visualizations with Britecharts
6 |
7 |
8 |
9 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | Chapter 6: Using and Customizing Britecharts
31 |
32 |
33 |
34 |
35 |
36 |
493 |
494 |
495 |
--------------------------------------------------------------------------------
/code/ch07/line-chart-with-legend.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Tutorial - Composing Data Visualizations with Britecharts
6 |
7 |
8 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | Chapter 6: Using and Customizing Britecharts
23 |
24 |
25 |
26 |
27 |
414 |
415 |
416 |
--------------------------------------------------------------------------------
/code/ch07/listings.js:
--------------------------------------------------------------------------------
1 | // Listing 7-1. Downloading Britecharts
2 |
3 |
4 |
5 |
6 |
7 |
8 | // Listing 7-2. Setting up the container, data set and chart instance
9 |
10 |
11 |
12 |
17 |
18 |
19 | // Listing 7-3. Line chart required data schema
20 | const lineData = {
21 | data: [
22 | {
23 | topicName: 'San Francisco',
24 | name: 1,
25 | date: '2017-01-16T16:00:00-08:00',
26 | value: 1
27 | },
28 | {
29 | topicName: 'San Francisco',
30 | name: 1,
31 | date: '2017-01-17T16:00:00-08:00',
32 | value: 2
33 | },
34 | {
35 | topicName: 'Oakland',
36 | name: 2,
37 | date: '2017-01-16T16:00:00-08:00',
38 | value: 3
39 | },
40 | {
41 | topicName: 'Oakland',
42 | name: 2,
43 | date: '2017-01-17T16:00:00-08:00',
44 | value: 7
45 | }
46 | ]
47 | };
48 |
49 | // Listing 7-4. Finding out the container’s width
50 | const containerWidth = container.node().getBoundingClientRect().width;
51 |
52 | // Listing 7-5. Configuring and rendering a simple line chart
53 | lineChart
54 | .margin({bottom: 50})
55 | .height(400)
56 | .width(containerWidth);
57 |
58 | container.datum(lineData).call(lineChart);
59 |
60 | // Listing 7-6. Final code of the simple line chart
61 |
62 |
63 |
80 |
81 | // Listing 7-7. Responding to viewport width changes
82 | const redrawChart = () => {
83 | const newContainerWidth = container.node() ? container.node().getBoundingClientRect().width : false;
84 |
85 | // Setting the new width on the chart
86 | lineChart.width(newContainerWidth);
87 |
88 | // Rendering the chart again
89 | container.call(lineChart);
90 | };
91 | // Create a throttled redrawChart function
92 | const throttledRedraw = _.throttle(redrawChart, 200);
93 |
94 | window.addEventListener("resize", throttledRedraw);
95 |
96 | // Listing 7-8. Loading the Lodash library via CDN
97 |
98 |
99 |
100 | // Listing 7-9. Instantiating and wiring the tooltip
101 | const chartTooltip = tooltip();
102 | //…
103 |
104 | lineChart
105 | .margin({bottom: 50})
106 | .height(400)
107 | .width(containerWidth)
108 | .on('customMouseOver', chartTooltip.show)
109 | .on('customMouseMove', chartTooltip.update)
110 | .on('customMouseOut', chartTooltip.hide);
111 |
112 |
113 | // Listing 7-10. Rendering the tooltip
114 | const tooltipContainer = d3.select('.line-container .metadata-group .hover-marker');
115 |
116 | tooltipContainer.call(chartTooltip);
117 |
118 | // Listing 7-11. Instantiating the legend
119 |
120 |
121 | const chartLegend = britecharts.legend();
122 | const legendContainer = d3.select('.legend-container');
123 |
124 | // Listing 7-12. Legend data schema
125 | [
126 | {
127 | id: 1,
128 | quantity: 2,
129 | name: 'glittering'
130 | },
131 | {
132 | id: 2,
133 | quantity: 3,
134 | name: 'luminous'
135 | }
136 | ]
137 |
138 | // Listing 7-13. Creating the legend data
139 | const legendData = lineData.data.reduce(
140 | (accum, item) => {
141 | let found = accum.find((element) => element.id === item.name);
142 | if (found) { return accum; }
143 |
144 | return [
145 | {
146 | id: item.name,
147 | name: item.topicName
148 | },
149 | ...accum
150 | ];
151 | },
152 | []
153 | );
154 |
155 | // Listing 7-14. Configuring and drawing the legend
156 | chartLegend
157 | .width(containerWidth)
158 | .height(60)
159 | .isHorizontal(true);
160 |
161 | legendContainer.datum(legendData).call(chartLegend);
162 |
163 | // Listing 7-15. Instantiating the brush
164 |
165 |
166 | const chartBrush = britecharts.brush();
167 | const brushContainer = d3.select('.brush-container');
168 |
169 | // Listing 7-16. Brush data schema
170 | [
171 | {
172 | value: 1,
173 | date: "2011-01-06T00:00:00Z"
174 | },
175 | {
176 | value: 2,
177 | date: "2011-01-07T00:00:00Z"
178 | }
179 | ]
180 |
181 | // Listing 7-17. Generating the brush data
182 | const lineDataCopy = JSON.parse(JSON.stringify(lineData));
183 | const brushData = lineDataCopy.data.reduce(
184 | (accum, d) => {
185 | let found;
186 |
187 | accum.forEach((item) => {
188 | if (item.date === d.date) {
189 | item.value = item.value + d.value;
190 | found = true;
191 |
192 | return;
193 | }
194 | });
195 |
196 | if (found) {
197 | return accum;
198 | }
199 |
200 | return [d, ...accum];
201 | },
202 | []
203 | );
204 |
205 | // Listing 7-18. Configuring and drawing the brush
206 | chartBrush
207 | .width(containerWidth)
208 | .height(100)
209 | .xAxisFormat(chartBrush.axisTimeCombinations.DAY_MONTH)
210 | .margin({top:0, bottom: 40, left: 50, right: 30});
211 |
212 | brushContainer.datum(brushData).call(chartBrush);
213 |
214 | // Listing 7-19. Wiring the brush event with the line chart
215 | chartBrush
216 | .width(containerWidth)
217 | .height(100)
218 | .xAxisFormat(chartBrush.axisTimeCombinations.DAY_MONTH)
219 | .margin({ top: 0, bottom: 40, left: 50, right: 30 })
220 | .on('customBrushEnd', ([brushStart, brushEnd]) => {
221 | if (brushStart && brushEnd) {
222 | let filteredLineData = filterData(brushStart, brushEnd);
223 |
224 | container.datum(filteredLineData).call(lineChart);
225 | }
226 | });
227 |
228 | // Listing 7-20. Data filtering logic
229 | const isInRange = (startDate, endDate, {date}) => new Date(date) >= startDate && new Date(date) <= endDate;
230 |
231 | const filterData = (brushStart, brushEnd) => {
232 | // Copy the data
233 | let lineDataCopy = JSON.parse(JSON.stringify(lineData));
234 |
235 | lineDataCopy.data = lineDataCopy.data.reduce(
236 | (accum, item) => {
237 | if (!isInRange(brushStart, brushEnd, item)) {
238 | return accum;
239 | }
240 |
241 | return [...accum, item];
242 | },
243 | []
244 | );
245 |
246 | return lineDataCopy;
247 | };
248 |
249 | // Listing 7-21. Applying color schemas to line and legend
250 | const colorSchemas = britecharts.colors.colorSchemas;
251 |
252 | ...
253 |
254 | lineChart
255 | .margin({ bottom: 50 })
256 | .height(400)
257 | .width(containerWidth)
258 | .colorSchema(colorSchemas.orange)
259 | .on('customMouseOver', chartTooltip.show)
260 | .on('customMouseMove', chartTooltip.update)
261 | .on('customMouseOut', chartTooltip.hide);
262 |
263 | ...
264 |
265 | chartLegend
266 | .height(60)
267 | .width(containerWidth)
268 | .colorSchema(colorSchemas.orange)
269 | .isHorizontal(true);
270 |
271 | // Listing 7-22. Color palette example
272 | // Britecharts palette
273 | ["#6aedc7", "#39c2c9", "#ffce00", "#ffa71a", "#f866b9", "#998ce3"]
274 |
275 | // Listing 7-23. Loading a Google font
276 |
277 |
278 | // Listing 7-24. Overriding the font styles
279 |
288 |
289 | // Listing 7-25. Styling the brush
290 | // Configure and draw the brush chart
291 | chartBrush
292 | .width(containerWidth)
293 | .height(100)
294 | .gradient(colorSchemas.orange.slice(0, 2))
295 | .xAxisFormat(chartBrush.axisTimeCombinations.DAY_MONTH)
296 | .margin({ top: 0, bottom: 40, left: 50, right: 30 })
297 | .on('customBrushEnd', function ([brushStart, brushEnd]) {
298 | if (brushStart && brushEnd) {
299 | let filteredLineData = filterData(brushStart, brushEnd);
300 |
301 | container.datum(filteredLineData).call(lineChart);
302 | }
303 | });
304 |
--------------------------------------------------------------------------------
/code/ch07/simple-line-chart.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Tutorial - Composing Data Visualizations with Britecharts
6 |
7 |
8 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | Chapter 6: Using and Customizing Britecharts
23 |
24 |
25 |
26 |
220 |
221 |
222 |
--------------------------------------------------------------------------------
/code/ch08/listing-8-1-chart-accessor-test.js:
--------------------------------------------------------------------------------
1 | // Listing 8-1. Grouped Bar Chart accessor test
2 | it('should provide an animationDuration getter and setter', () => {
3 | let previous = groupedBarChart.animationDuration(),
4 | expected = 600,
5 | actual;
6 |
7 | groupedBarChart.animationDuration(expected);
8 | actual = groupedBarChart.animationDuration();
9 |
10 | expect(previous).not.toBe(expected);
11 | expect(actual).toBe(expected);
12 | });
13 |
--------------------------------------------------------------------------------
/code/ch08/listing-8-2-animation-duration-accessor.js:
--------------------------------------------------------------------------------
1 | // Listing 8-2. Grouped Bar Chart animationDuration accessor
2 | exports.animationDuration = function (_x) {
3 | if (!arguments.length) {
4 | return animationDuration;
5 | }
6 | animationDuration = _x;
7 |
8 | return this;
9 | };
10 |
--------------------------------------------------------------------------------
/code/ch08/listing-8-3-accessor-with-comments.js:
--------------------------------------------------------------------------------
1 | // Listing 8-3. Grouped Bar Chart animationDuration accessor with comments
2 | /**
3 | * Gets or Sets the duration of the bar grow animation
4 | * @param {Number} _x=1000 Desired animationDuration for chart
5 | * @return {Number | module} Current animationDuration or chart module to chain calls
6 | * @public
7 | */
8 | exports.animationDuration = function (_x) {
9 | if (!arguments.length) {
10 | return animationDuration;
11 | }
12 | animationDuration = _x;
13 |
14 | return this;
15 | };
16 |
--------------------------------------------------------------------------------
/code/ch08/listing-8-4-add-configuration-to-demo.js:
--------------------------------------------------------------------------------
1 | // Listing 8-4. Grouped Bar Chart demo with animationDuration
2 | // GroupedAreChart Setup and start
3 | groupedBar
4 | .tooltipThreshold(600)
5 | .animationDuration(2000)
6 | .width(containerWidth)
7 | .grid('horizontal')
8 | .isAnimated(true)
9 | .groupLabel('stack')
10 | .nameLabel('date')
11 | .valueLabel('views')
12 | .on('customMouseOver', function() {
13 | chartTooltip.show();
14 | })
15 | .on('customMouseMove', function(dataPoint, topicColorMap, x,y) {
16 | chartTooltip.update(dataPoint, topicColorMap, x, y);
17 | })
18 | .on('customMouseOut', function() {
19 | chartTooltip.hide();
20 | });
21 |
--------------------------------------------------------------------------------
/code/ch09/bar-chart-code.js:
--------------------------------------------------------------------------------
1 | import * as d3 from 'd3';
2 |
3 | function bar() {
4 | let data;
5 | let svg;
6 | let margin = {
7 | top: 20,
8 | right: 20,
9 | bottom: 30,
10 | left: 40
11 | };
12 | let width = 960;
13 | let height = 500;
14 | let chartWidth;
15 | let chartHeight;
16 | let xScale;
17 | let yScale;
18 | let xAxis;
19 | let yAxis;
20 |
21 | // Dispatcher object to broadcast the mouse events
22 | const dispatcher = d3.dispatch('customMouseOver', 'customMouseMove', 'customMouseOut', 'customMouseClick');
23 |
24 | // extractors
25 | const getFrequency = ({frequency}) => frequency;
26 | const getLetter = ({letter}) => letter;
27 |
28 |
29 | function exports(_selection){
30 | _selection.each(function(_data){
31 | data = _data;
32 | chartHeight = height - margin.top - margin.bottom;
33 | chartWidth = width - margin.left - margin.right;
34 |
35 | buildScales();
36 | buildAxes();
37 | buildSVG(this);
38 | drawAxes();
39 | drawBars();
40 | });
41 | }
42 |
43 | // Building Blocks
44 | function buildAxes(){
45 | xAxis = d3.axisBottom(xScale);
46 |
47 | yAxis = d3.axisLeft(yScale)
48 | .ticks(10, '%');
49 | }
50 |
51 | function buildContainerGroups(){
52 | let container = svg
53 | .append('g')
54 | .classed('container-group', true)
55 | .attr(
56 | 'transform',
57 | `translate(${margin.left},${margin.top})`
58 | );
59 |
60 | container
61 | .append('g')
62 | .classed('chart-group', true);
63 | container
64 | .append('g')
65 | .classed('x-axis-group axis', true);
66 | container
67 | .append('g')
68 | .classed('y-axis-group axis', true);
69 | }
70 |
71 | function buildScales(){
72 | xScale = d3.scaleBand()
73 | .rangeRound([0, chartWidth])
74 | .padding(0.1)
75 | .domain(data.map(getLetter));
76 |
77 | yScale = d3.scaleLinear()
78 | .rangeRound([chartHeight, 0])
79 | .domain([0, d3.max(data, getFrequency)]);
80 | }
81 |
82 | function buildSVG(container){
83 | if (!svg) {
84 | svg = d3.select(container)
85 | .append('svg')
86 | .classed('bar-chart', true);
87 |
88 | buildContainerGroups();
89 | }
90 | svg
91 | .attr('width', width)
92 | .attr('height', height);
93 | }
94 |
95 | function drawAxes(){
96 | svg.select('.x-axis-group.axis')
97 | .attr('transform', `translate(0,${chartHeight})`)
98 | .call(xAxis);
99 |
100 | svg.select('.y-axis-group.axis')
101 | .call(yAxis)
102 | .append('text')
103 | .attr('transform', 'rotate(-90)')
104 | .attr('y', 6)
105 | .attr('dy', '0.71em')
106 | .attr('text-anchor', 'end')
107 | .text('Frequency');
108 | }
109 |
110 | function drawBars(){
111 | let bars = svg.select('.chart-group').selectAll('.bar')
112 | .data(data);
113 |
114 | // Enter
115 | bars.enter()
116 | .append('rect')
117 | .classed('bar', true)
118 | .attr('x', ({letter}) => xScale(letter))
119 | .attr('y', ({frequency}) => yScale(frequency))
120 | .attr('width', xScale.bandwidth())
121 | .attr('height', ({frequency}) => chartHeight - yScale(frequency))
122 | .on('mouseover', function(d) {
123 | dispatcher.call('customMouseOver', this, d);
124 | })
125 | .on('mousemove', function(d) {
126 | dispatcher.call('customMouseMove', this, d);
127 | })
128 | .on('mouseout', function(d) {
129 | dispatcher.call('customMouseOut', this, d);
130 | })
131 | .on('click', function(d) {
132 | dispatcher.call('customMouseClick', this, d);
133 | });
134 |
135 | // Exit
136 | bars.exit()
137 | .style('opacity', 0)
138 | .remove();
139 | }
140 |
141 | // API
142 | exports.height = function(_x) {
143 | if (!arguments.length) {
144 | return height;
145 | }
146 | height = _x;
147 |
148 | return this;
149 | };
150 |
151 | exports.margin = function(_x) {
152 | if (!arguments.length) {
153 | return margin;
154 | }
155 | margin = {
156 | ...margin,
157 | ..._x
158 | };
159 |
160 | return this;
161 | };
162 |
163 | exports.on = function() {
164 | let value = dispatcher.on.apply(dispatcher, arguments);
165 |
166 | return value === dispatcher ? exports : value;
167 | };
168 |
169 | exports.width = function(_x) {
170 | if (!arguments.length) {
171 | return width;
172 | }
173 | width = _x;
174 |
175 | return this;
176 | };
177 |
178 | return exports;
179 | };
180 |
181 | export default bar;
182 |
--------------------------------------------------------------------------------
/code/ch09/bar-chart-test-code.js:
--------------------------------------------------------------------------------
1 | import * as d3 from 'd3'
2 | import bar from './barChart';
3 |
4 | const data = [
5 | {
6 | "letter": "A",
7 | "frequency": 0.08167
8 | },
9 | {
10 | "letter": "B",
11 | "frequency": 0.01492
12 | },
13 | {
14 | "letter": "C",
15 | "frequency": 0.02782
16 | },
17 | {
18 | "letter": "D",
19 | "frequency": 0.04253
20 | },
21 | {
22 | "letter": "E",
23 | "frequency": 0.12702
24 | },
25 | {
26 | "letter": "F",
27 | "frequency": 0.02288
28 | },
29 | {
30 | "letter": "G",
31 | "frequency": 0.02015
32 | },
33 | {
34 | "letter": "H",
35 | "frequency": 0.06094
36 | },
37 | {
38 | "letter": "I",
39 | "frequency": 0.06966
40 | },
41 | {
42 | "letter": "J",
43 | "frequency": 0.00153
44 | },
45 | {
46 | "letter": "K",
47 | "frequency": 0.00772
48 | },
49 | {
50 | "letter": "L",
51 | "frequency": 0.04025
52 | },
53 | {
54 | "letter": "M",
55 | "frequency": 0.02406
56 | },
57 | {
58 | "letter": "N",
59 | "frequency": 0.06749
60 | },
61 | {
62 | "letter": "O",
63 | "frequency": 0.07507
64 | },
65 | {
66 | "letter": "P",
67 | "frequency": 0.01929
68 | },
69 | {
70 | "letter": "Q",
71 | "frequency": 0.00095
72 | },
73 | {
74 | "letter": "R",
75 | "frequency": 0.05987
76 | },
77 | {
78 | "letter": "S",
79 | "frequency": 0.06327
80 | },
81 | {
82 | "letter": "T",
83 | "frequency": 0.09056
84 | },
85 | {
86 | "letter": "U",
87 | "frequency": 0.02758
88 | },
89 | {
90 | "letter": "V",
91 | "frequency": 0.00978
92 | },
93 | {
94 | "letter": "W",
95 | "frequency": 0.0236
96 | },
97 | {
98 | "letter": "X",
99 | "frequency": 0.0015
100 | },
101 | {
102 | "letter": "Y",
103 | "frequency": 0.01974
104 | },
105 | {
106 | "letter": "Z",
107 | "frequency": 0.00074
108 | }
109 | ];
110 | const alternativeData = [
111 | {
112 | "letter": "E",
113 | "frequency": 0.12702
114 | },
115 | {
116 | "letter": "F",
117 | "frequency": 0.02288
118 | },
119 | {
120 | "letter": "G",
121 | "frequency": 0.02015
122 | },
123 | {
124 | "letter": "H",
125 | "frequency": 0.06094
126 | },
127 | {
128 | "letter": "I",
129 | "frequency": 0.06966
130 | },
131 | {
132 | "letter": "J",
133 | "frequency": 0.00153
134 | }
135 | ];
136 |
137 | describe('Bar Chart', () => {
138 | let barChart;
139 | let container;
140 |
141 | beforeEach(() => {
142 | const fixture = '';
143 |
144 | // adds an html fixture to the DOM
145 | document.body.insertAdjacentHTML('afterbegin', fixture);
146 | });
147 |
148 | // remove the html fixture from the DOM
149 | afterEach(function() {
150 | document.body.removeChild(document.getElementById('fixture'));
151 | });
152 |
153 | describe('Render', () => {
154 |
155 | beforeEach(() => {
156 | barChart = bar();
157 | container = d3.select('.container');
158 |
159 | container.datum(data).call(barChart);
160 | });
161 |
162 | afterEach(() => {
163 | container.remove();
164 | });
165 |
166 | it('should render a basic bar chart', () => {
167 | const expected = 1;
168 | const actual = container.select('.bar-chart').size();
169 |
170 | expect(actual).toEqual(expected);
171 | });
172 |
173 | describe('groups', () => {
174 | it('should create a container-group', () => {
175 | const expected = 1;
176 | const actual = container.select('g.container-group').size();
177 |
178 | expect(actual).toEqual(expected);
179 | });
180 |
181 | it('should create a chart-group', () => {
182 | const expected = 1;
183 | const actual = container.select('g.chart-group').size();
184 |
185 | expect(actual).toEqual(expected);
186 | });
187 |
188 | it('should create a x-axis-group', () => {
189 | const expected = 1;
190 | const actual = container.select('g.x-axis-group').size();
191 |
192 | expect(actual).toEqual(expected);
193 | });
194 |
195 | it('should create a y-axis-group', () => {
196 | const expected = 1;
197 | const actual = container.select('g.y-axis-group').size();
198 |
199 | expect(actual).toEqual(expected);
200 | });
201 | });
202 |
203 | describe('axis', () => {
204 | it('should draw an X axis', () => {
205 | const expected = 1;
206 | const actual = container.select('.x-axis-group.axis').size();
207 |
208 | expect(actual).toEqual(expected);
209 | });
210 |
211 | it('should draw an Y axis', () => {
212 | const expected = 1;
213 | const actual = container.select('.y-axis-group.axis').size();
214 |
215 | expect(actual).toEqual(expected);
216 | });
217 | });
218 |
219 | it('should draw a bar for each data entry', () => {
220 | const expected = data.length;
221 | const actual = container.selectAll('.bar').size();
222 |
223 | expect(actual).toEqual(expected);
224 | });
225 |
226 | describe('when reloading with a different dataset', () => {
227 |
228 | it('should render in the same svg', () => {
229 | const expected = 1;
230 | const newDataset = alternativeData;
231 | let actual;
232 |
233 | container.datum(newDataset).call(barChart);
234 | actual = container.selectAll('.bar-chart').size();
235 |
236 | expect(actual).toEqual(expected);
237 | });
238 |
239 | it('should render six bars', () => {
240 | const expected = 6;
241 | const newDataset = alternativeData;
242 | let actual;
243 |
244 | container.datum(newDataset).call(barChart);
245 | actual = container.selectAll('.bar-chart .bar').size();
246 |
247 | expect(actual).toEqual(expected);
248 | });
249 | });
250 | });
251 |
252 | describe('Lifecycle', () => {
253 |
254 | beforeEach(() => {
255 | barChart = bar();
256 | container = d3.select('.container');
257 |
258 | container.datum(data).call(barChart);
259 | });
260 |
261 | afterEach(() => {
262 | container.remove();
263 | });
264 |
265 | describe('when hovering a bar', () => {
266 |
267 | it('should trigger a callback once on mouse over', () => {
268 | const expected = 1;
269 | const firstBar = container.selectAll('.bar:nth-child(1)');
270 | const callbackSpy = jasmine.createSpy('callback');
271 | let actual;
272 |
273 | barChart.on('customMouseOver', callbackSpy);
274 | firstBar.dispatch('mouseover');
275 | actual = callbackSpy.calls.count();
276 |
277 | expect(actual).toEqual(expected);
278 | });
279 |
280 | it('should trigger the callback with the data entry as argument', () => {
281 | const expected = data[0];
282 | const firstBar = container.selectAll('.bar:nth-child(1)');
283 | const callbackSpy = jasmine.createSpy('callback');
284 | let actual;
285 |
286 | barChart.on('customMouseOver', callbackSpy);
287 | firstBar.dispatch('mouseover');
288 | actual = callbackSpy.calls.first().args[0];
289 |
290 | expect(actual).toEqual(expected);
291 | });
292 | });
293 |
294 | describe('when moving over a bar', () => {
295 |
296 | it('should trigger a callback once on mouse over', () => {
297 | const expected = 1;
298 | const firstBar = container.selectAll('.bar:nth-child(1)');
299 | const callbackSpy = jasmine.createSpy('callback');
300 | let actual;
301 |
302 | barChart.on('customMouseMove', callbackSpy);
303 | firstBar.dispatch('mousemove');
304 | actual = callbackSpy.calls.count();
305 |
306 | expect(actual).toEqual(expected);
307 | });
308 |
309 | it('should trigger the callback with the data entry as argument', () => {
310 | const expected = data[0];
311 | const firstBar = container.selectAll('.bar:nth-child(1)');
312 | const callbackSpy = jasmine.createSpy('callback');
313 | let actual;
314 |
315 | barChart.on('customMouseMove', callbackSpy);
316 | firstBar.dispatch('mousemove');
317 | actual = callbackSpy.calls.first().args[0];
318 |
319 | expect(actual).toEqual(expected);
320 | });
321 | });
322 |
323 | describe('when moving out of a bar', () => {
324 |
325 | it('should trigger a callback once on mouse out', () => {
326 | const expected = 1;
327 | const firstBar = container.selectAll('.bar:nth-child(1)');
328 | const callbackSpy = jasmine.createSpy('callback');
329 | let actual;
330 |
331 | barChart.on('customMouseOut', callbackSpy);
332 | firstBar.dispatch('mouseout');
333 | actual = callbackSpy.calls.count();
334 |
335 | expect(actual).toEqual(expected);
336 | });
337 |
338 | it('should trigger the callback with the data entry as argument', () => {
339 | const expected = data[0];
340 | const firstBar = container.selectAll('.bar:nth-child(1)');
341 | const callbackSpy = jasmine.createSpy('callback');
342 | let actual;
343 |
344 | barChart.on('customMouseOut', callbackSpy);
345 | firstBar.dispatch('mouseout');
346 | actual = callbackSpy.calls.first().args[0];
347 |
348 | expect(actual).toEqual(expected);
349 | });
350 | });
351 |
352 | describe('when clicking a bar', () => {
353 |
354 | it('should trigger a callback once on mouse click', () => {
355 | const expected = 1;
356 | const firstBar = container.selectAll('.bar:nth-child(1)');
357 | const callbackSpy = jasmine.createSpy('callback');
358 | let actual;
359 |
360 | barChart.on('customMouseClick', callbackSpy);
361 | firstBar.dispatch('click');
362 | actual = callbackSpy.calls.count();
363 |
364 | expect(actual).toEqual(expected);
365 | });
366 |
367 | it('should trigger the callback with the data entry as argument', () => {
368 | const expected = data[0];
369 | const firstBar = container.selectAll('.bar:nth-child(1)');
370 | const callbackSpy = jasmine.createSpy('callback');
371 | let actual;
372 |
373 | barChart.on('customMouseClick', callbackSpy);
374 | firstBar.dispatch('click');
375 | actual = callbackSpy.calls.first().args[0];
376 |
377 | expect(actual).toEqual(expected);
378 | });
379 | });
380 | });
381 |
382 | describe('API', () => {
383 |
384 | beforeEach(() => {
385 | barChart = bar();
386 | container = d3.select('.container');
387 |
388 | container.datum(data).call(barChart);
389 | });
390 |
391 | afterEach(() => {
392 | container.remove();
393 | });
394 |
395 | it('should provide height getter and setter', () => {
396 | const previous = barChart.height();
397 | const expected = 300;
398 | let actual;
399 |
400 | barChart.height(expected);
401 | actual = barChart.height();
402 |
403 | expect(previous).not.toEqual(actual);
404 | expect(actual).toEqual(expected);
405 | });
406 |
407 | it('should provide a event "on" getter and setter', () => {
408 | const callback = () => {};
409 | const expected = callback;
410 | let actual;
411 |
412 | barChart.on('customMouseClick', callback);
413 | actual = barChart.on('customMouseClick');
414 |
415 | expect(actual).toEqual(expected);
416 | });
417 |
418 | describe('margin', () => {
419 |
420 | it('should provide margin getter and setter', () => {
421 | const previous = barChart.margin();
422 | const expected = {top: 4, right: 4, bottom: 4, left: 4};
423 | let actual;
424 |
425 | barChart.margin(expected);
426 | actual = barChart.margin();
427 |
428 | expect(previous).not.toEqual(actual);
429 | expect(actual).toEqual(expected);
430 | });
431 |
432 | describe('when margins are set partially', () => {
433 |
434 | it('should override the default values', () => {
435 | const previous = barChart.margin();
436 | const expected = {
437 | ...previous,
438 | top: 10,
439 | right: 20
440 | };
441 | let actual;
442 |
443 | barChart.margin({
444 | top: 10,
445 | right: 20
446 | });
447 | actual = barChart.margin();
448 |
449 | expect(previous).not.toEqual(actual);
450 | expect(actual).toEqual(expected);
451 | })
452 | });
453 | });
454 |
455 | it('should provide width getter and setter', () => {
456 | const previous = barChart.width();
457 | const expected = 200;
458 | let actual;
459 |
460 | barChart.width(expected);
461 | actual = barChart.width();
462 |
463 | expect(previous).not.toEqual(actual);
464 | expect(actual).toEqual(expected);
465 | });
466 | });
467 | });
468 |
--------------------------------------------------------------------------------
/code/ch09/listings.js:
--------------------------------------------------------------------------------
1 | // Listing 9-1. Test script in package.json
2 | "scripts": {
3 | "test": "karmatic"
4 | }
5 |
6 | // Listing 9-2. Dumb test to make sure testing works
7 | describe('Dumb Test', () => {
8 | it('should pass', () => {
9 | expect(true).toEqual(true);
10 | });
11 | });
12 |
13 | // Listing 9-3. Running the dumb test in the command line
14 |
15 | Dumb Test
16 | ✓ should pass
17 | Executed 1 of 1 SUCCESS (0.002 secs / 0.002 secs)
18 |
19 |
20 | =============================== Coverage summary ===============================
21 | Statements : 100% ( 0/0 )
22 | Branches : 100% ( 0/0 )
23 | Functions : 100% ( 0/0 )
24 | Lines : 100% ( 0/0 )
25 | ================================================================================
26 | ✨ Done in 4.50s.
27 |
28 | // Listing 9-4. DOM fixture with Karmatic
29 | const data = [
30 | {
31 | "letter": "A",
32 | "frequency": 0.08167
33 | },
34 | ...
35 | ];
36 |
37 | describe('Bar Chart', () => {
38 | let barChart;
39 | let container;
40 |
41 | beforeEach(() => {
42 | const fixture = '';
43 |
44 | // adds an html fixture to the DOM
45 | document.body.insertAdjacentHTML('afterbegin', fixture);
46 | });
47 |
48 | // remove the html fixture from the DOM
49 | afterEach(function() {
50 | document.body.removeChild(document.getElementById('fixture'));
51 | });
52 | });
53 |
54 | // Listing 9-5. Render describe clause
55 | describe('Render', () => {
56 |
57 | beforeEach(() => {
58 | barChart = bar();
59 | container = d3.select('.container');
60 |
61 | container.datum(data).call(barChart);
62 | });
63 |
64 | afterEach(() => {
65 | container.remove();
66 | });
67 | });
68 |
69 | // Listing 9-6. Test for the root element existence
70 | it('should render a basic bar chart', () => {
71 | const expected = 1;
72 | const actual = container.select('.bar-chart').size();
73 |
74 | expect(actual).toEqual(expected);
75 | });
76 |
77 | // Listing 9-7. Creating the container
78 | import * as d3 from 'd3';
79 |
80 | function bar() {
81 | // Variable creation
82 |
83 | function exports(_selection){
84 | _selection.each(function(_data){
85 | data = _data;
86 | chartHeight = height - margin.top - margin.bottom;
87 | chartWidth = width - margin.left - margin.right;
88 |
89 | buildSVG(this);
90 | });
91 | }
92 |
93 | function buildSVG(container){
94 | if (!svg) {
95 | svg = d3.select(container)
96 | .append('svg')
97 | .classed('bar-chart', true);
98 | }
99 | svg
100 | .attr('width', width)
101 | .attr('height', height);
102 | }
103 |
104 | return exports;
105 | };
106 |
107 | export default bar;
108 |
109 | // Listing 9-8. Container group tests
110 | describe('groups', () => {
111 |
112 | it('should create a container-group', () => {
113 | const expected = 1;
114 | const actual = container.select('g.container-group').size();
115 |
116 | expect(actual).toEqual(expected);
117 | });
118 |
119 | it('should create a chart-group', () => {
120 | const expected = 1;
121 | const actual = container.select('g.chart-group').size();
122 |
123 | expect(actual).toEqual(expected);
124 | });
125 |
126 | it('should create a x-axis-group', () => {
127 | const expected = 1;
128 | const actual = container.select('g.x-axis-group').size();
129 |
130 | expect(actual).toEqual(expected);
131 | });
132 |
133 | it('should create a y-axis-group', () => {
134 | const expected = 1;
135 | const actual = container.select('g.y-axis-group').size();
136 |
137 | expect(actual).toEqual(expected);
138 | });
139 | });
140 |
141 | // Listing 9-9. Rendering the container groups
142 | function buildSVG(container){
143 | if (!svg) {
144 | svg = d3.select(container)
145 | .append('svg')
146 | .classed('bar-chart', true);
147 |
148 | buildContainerGroups();
149 | }
150 | svg
151 | .attr('width', width)
152 | .attr('height', height);
153 | }
154 |
155 | function buildContainerGroups(){
156 | let container = svg
157 | .append('g')
158 | .classed('container-group', true)
159 | .attr(
160 | 'transform',
161 | `translate(${margin.left},${margin.top})`
162 | );
163 |
164 | container
165 | .append('g')
166 | .classed('chart-group', true);
167 | container
168 | .append('g')
169 | .classed('x-axis-group axis', true);
170 | container
171 | .append('g')
172 | .classed('y-axis-group axis', true);
173 | }
174 |
175 | // Listing 9-10. Testing the axes drawing
176 | describe('axis', () => {
177 | it('should draw an X axis', () => {
178 | const expected = 1;
179 | const actual = container.select('.x-axis-group.axis').size();
180 |
181 | expect(actual).toEqual(expected);
182 | });
183 |
184 | it('should draw an Y axis', () => {
185 | const expected = 1;
186 | const actual = container.select('.y-axis-group.axis').size();
187 |
188 | expect(actual).toEqual(expected);
189 | });
190 | });
191 |
192 | // Listing 9-11. Code to draw the axes
193 | const getFrequency = ({frequency}) => frequency;
194 | const getLetter = ({letter}) => letter;
195 |
196 | function exports(_selection){
197 | _selection.each(function(_data){
198 | data = _data;
199 | chartHeight = height - margin.top - margin.bottom;
200 | chartWidth = width - margin.left - margin.right;
201 |
202 | buildScales();
203 | buildAxes();
204 | buildSVG(this);
205 | drawAxes();
206 | });
207 | }
208 |
209 | function buildAxes(){
210 | xAxis = d3.axisBottom(xScale);
211 |
212 | yAxis = d3.axisLeft(yScale)
213 | .ticks(10, '%');
214 | }
215 |
216 | function buildScales(){
217 | xScale = d3.scaleBand()
218 | .rangeRound([0, chartWidth])
219 | .padding(0.1)
220 | .domain(data.map(getLetter));
221 |
222 | yScale = d3.scaleLinear()
223 | .rangeRound([chartHeight, 0])
224 | .domain([0, d3.max(data, getFrequency)]);
225 | }
226 |
227 | function drawAxes(){
228 | svg.select('.x-axis-group.axis')
229 | .attr('transform', `translate(0,${chartHeight})`)
230 | .call(xAxis);
231 |
232 | svg.select('.y-axis-group.axis')
233 | .call(yAxis)
234 | .append('text')
235 | .attr('transform', 'rotate(-90)')
236 | .attr('y', 6)
237 | .attr('dy', '0.71em')
238 | .attr('text-anchor', 'end')
239 | .text('Frequency');
240 | }
241 |
242 | // Listing 9-12. Test for the bars
243 | it('should draw a bar for each data entry', () => {
244 | const expected = data.length;
245 | const actual = container.selectAll('.bar').size();
246 |
247 | expect(actual).toEqual(expected);
248 | });
249 |
250 | // Listing 9-13. Code for drawing the bars
251 | function exports(_selection){
252 | _selection.each(function(_data){
253 | data = _data;
254 | chartHeight = height - margin.top - margin.bottom;
255 | chartWidth = width - margin.left - margin.right;
256 |
257 | buildScales();
258 | buildAxes();
259 | buildSVG(this);
260 | drawAxes();
261 | drawBars();
262 | });
263 | }
264 |
265 | function drawBars(){
266 | let bars = svg.select('.chart-group').selectAll('.bar')
267 | .data(data);
268 |
269 | // Enter
270 | bars.enter()
271 | .append('rect')
272 | .classed('bar', true)
273 | .attr('x', ({letter}) => xScale(letter))
274 | .attr('y', ({frequency}) => yScale(frequency))
275 | .attr('width', xScale.bandwidth())
276 | .attr('height', ({frequency}) => chartHeight - yScale(frequency));
277 |
278 | // Exit
279 | bars.exit()
280 | .style('opacity', 0)
281 | .remove();
282 | }
283 |
284 | // Listing 9-14. Checking for data reload
285 | describe('when reloading with a different dataset', () => {
286 |
287 | it('should render in the same svg', () => {
288 | const expected = 1;
289 | const newDataset = alternativeData;
290 | let actual;
291 |
292 | container.datum(newDataset).call(barChart);
293 | actual = container.selectAll('.bar-chart').size();
294 |
295 | expect(actual).toEqual(expected);
296 | });
297 |
298 | it('should render six bars', () => {
299 | const expected = 6;
300 | const newDataset = alternativeData;
301 | let actual;
302 |
303 | container.datum(newDataset).call(barChart);
304 | actual = container.selectAll('.bar-chart .bar').size();
305 |
306 | expect(actual).toEqual(expected);
307 | });
308 | });
309 |
310 | // Listing 9-15. Testing the mouse over event
311 | describe('Lifecycle', () => {
312 |
313 | beforeEach(() => {
314 | barChart = bar();
315 | container = d3.select('.container');
316 |
317 | container.datum(data).call(barChart);
318 | });
319 |
320 | afterEach(() => {
321 | container.remove();
322 | });
323 |
324 | describe('when hovering a bar', () => {
325 |
326 | it('should trigger a callback once on mouse over', () => {
327 | const expected = 1;
328 | const firstBar = container.selectAll('.bar:nth-child(1)');
329 | const callbackSpy = jasmine.createSpy('callback');
330 | let actual;
331 |
332 | barChart.on('customMouseOver', callbackSpy);
333 | firstBar.dispatch('mouseover');
334 | actual = callbackSpy.calls.count();
335 |
336 | expect(actual).toEqual(expected);
337 | });
338 | });
339 | });
340 |
341 | // Listing 9-16. Enabling event triggering on the bar chart
342 | const dispatcher = d3.dispatch('customMouseOver');
343 |
344 | //...
345 |
346 | function drawBars(){
347 | let bars = svg.select('.chart-group').selectAll('.bar')
348 | .data(data);
349 |
350 | // Enter
351 | bars.enter()
352 | .append('rect')
353 | .classed('bar', true)
354 | .attr('x', ({letter}) => xScale(letter))
355 | .attr('y', ({frequency}) => yScale(frequency))
356 | .attr('width', xScale.bandwidth())
357 | .attr('height', ({frequency}) => chartHeight - yScale(frequency))
358 | .on('mouseover', function(d) {
359 | dispatcher.call('customMouseOver', this);
360 | });
361 |
362 | // Exit
363 | bars.exit()
364 | .style('opacity', 0)
365 | .remove();
366 | }
367 |
368 | //...
369 |
370 | exports.on = function() {
371 | let value = dispatcher.on.apply(dispatcher, arguments);
372 |
373 | return value === dispatcher ? exports : value;
374 | };
375 |
376 | // Listing 9-17. Test looking for the datapoint information
377 | it('should trigger the callback with the data entry as argument', () => {
378 | const expected = data[0];
379 | const firstBar = container.selectAll('.bar:nth-child(1)');
380 | const callbackSpy = jasmine.createSpy('callback');
381 | let actual;
382 |
383 | barChart.on('customMouseOver', callbackSpy);
384 | firstBar.dispatch('mouseover');
385 | actual = callbackSpy.calls.first().args[0];
386 |
387 | expect(actual).toEqual(expected);
388 | });
389 |
390 | // Listing 9-18. Passing down the data entry
391 | //...
392 | // Enter
393 | bars.enter()
394 | .append('rect')
395 | .classed('bar', true)
396 | .attr('x', ({letter}) => xScale(letter))
397 | .attr('y', ({frequency}) => yScale(frequency))
398 | .attr('width', xScale.bandwidth())
399 | .attr('height', ({frequency}) => chartHeight - yScale(frequency))
400 | .on('mouseover', function(d) {
401 | dispatcher.call('customMouseOver', this, d);
402 | });
403 |
404 | // Listing 9-19. Tests for mouse over, out and click events
405 | describe('when moving over a bar', () => {
406 |
407 | it('should trigger a callback once on mouse over', () => {
408 | const expected = 1;
409 | const firstBar = container.selectAll('.bar:nth-child(1)');
410 | const callbackSpy = jasmine.createSpy('callback');
411 | let actual;
412 |
413 | barChart.on('customMouseMove', callbackSpy);
414 | firstBar.dispatch('mousemove');
415 | actual = callbackSpy.calls.count();
416 |
417 | expect(actual).toEqual(expected);
418 | });
419 |
420 | it('should trigger the callback with the data entry as argument', () => {
421 | const expected = data[0];
422 | const firstBar = container.selectAll('.bar:nth-child(1)');
423 | const callbackSpy = jasmine.createSpy('callback');
424 | let actual;
425 |
426 | barChart.on('customMouseMove', callbackSpy);
427 | firstBar.dispatch('mousemove');
428 | actual = callbackSpy.calls.first().args[0];
429 |
430 | expect(actual).toEqual(expected);
431 | });
432 | });
433 |
434 | describe('when moving out of a bar', () => {
435 |
436 | it('should trigger a callback once on mouse out', () => {
437 | const expected = 1;
438 | const firstBar = container.selectAll('.bar:nth-child(1)');
439 | const callbackSpy = jasmine.createSpy('callback');
440 | let actual;
441 |
442 | barChart.on('customMouseOut', callbackSpy);
443 | firstBar.dispatch('mouseout');
444 | actual = callbackSpy.calls.count();
445 |
446 | expect(actual).toEqual(expected);
447 | });
448 |
449 | it('should trigger the callback with the data entry as argument', () => {
450 | const expected = data[0];
451 | const firstBar = container.selectAll('.bar:nth-child(1)');
452 | const callbackSpy = jasmine.createSpy('callback');
453 | let actual;
454 |
455 | barChart.on('customMouseOut', callbackSpy);
456 | firstBar.dispatch('mouseout');
457 | actual = callbackSpy.calls.first().args[0];
458 |
459 | expect(actual).toEqual(expected);
460 | });
461 | });
462 |
463 | describe('when clicking a bar', () => {
464 |
465 | it('should trigger a callback once on mouse click', () => {
466 | const expected = 1;
467 | const firstBar = container.selectAll('.bar:nth-child(1)');
468 | const callbackSpy = jasmine.createSpy('callback');
469 | let actual;
470 |
471 | barChart.on('customMouseClick', callbackSpy);
472 | firstBar.dispatch('click');
473 | actual = callbackSpy.calls.count();
474 |
475 | expect(actual).toEqual(expected);
476 | });
477 |
478 | it('should trigger the callback with the data entry as argument', () => {
479 | const expected = data[0];
480 | const firstBar = container.selectAll('.bar:nth-child(1)');
481 | const callbackSpy = jasmine.createSpy('callback');
482 | let actual;
483 |
484 | barChart.on('customMouseClick', callbackSpy);
485 | firstBar.dispatch('click');
486 | actual = callbackSpy.calls.first().args[0];
487 |
488 | expect(actual).toEqual(expected);
489 | });
490 | });
491 |
492 | // Listing 9-20. Code for the rest of events
493 | //...
494 | const dispatcher = d3.dispatch('customMouseOver', 'customMouseMove', 'customMouseOut', 'customMouseClick');
495 | //...
496 | // Enter
497 | bars.enter()
498 | .append('rect')
499 | .classed('bar', true)
500 | .attr('x', ({letter}) => xScale(letter))
501 | .attr('y', ({frequency}) => yScale(frequency))
502 | .attr('width', xScale.bandwidth())
503 | .attr('height', ({frequency}) => chartHeight - yScale(frequency))
504 | .on('mouseover', function(d) {
505 | dispatcher.call('customMouseOver', this, d);
506 | })
507 | .on('mousemove', function(d) {
508 | dispatcher.call('customMouseMove', this, d);
509 | })
510 | .on('mouseout', function(d) {
511 | dispatcher.call('customMouseOut', this, d);
512 | })
513 | .on('click', function(d) {
514 | dispatcher.call('customMouseClick', this, d);
515 | });
516 |
517 | // Listing 9-21. Test for the height accessor
518 | describe('API', () => {
519 |
520 | beforeEach(() => {
521 | barChart = bar();
522 | container = d3.select('.container');
523 |
524 | container.datum(data).call(barChart);
525 | });
526 |
527 | afterEach(() => {
528 | container.remove();
529 | });
530 |
531 | it('should provide height getter and setter', () => {
532 | const previous = barChart.height();
533 | const expected = 300;
534 | let actual;
535 |
536 | barChart.height(expected);
537 | actual = barChart.height();
538 |
539 | expect(previous).not.toEqual(actual);
540 | expect(actual).toEqual(expected);
541 | });
542 | });
543 |
544 | // Listing 9-22. Code for the height accessor
545 | exports.height = function(_x) {
546 | if (!arguments.length) {
547 | return height;
548 | }
549 | height = _x;
550 |
551 | return this;
552 | };
553 |
554 | // Listing 9-23. Test for the "on" accessor
555 | it('should provide a event "on" getter and setter', () => {
556 | const callback = () => {};
557 | const expected = callback;
558 | let actual;
559 |
560 | barChart.on('customMouseClick', callback);
561 | actual = barChart.on('customMouseClick');
562 |
563 | expect(actual).toEqual(expected);
564 | });
565 |
566 | // Listing 9-24. Testing the margin object accessor
567 | describe('margin', () => {
568 |
569 | it('should provide margin getter and setter', () => {
570 | const previous = barChart.margin();
571 | const expected = {top: 4, right: 4, bottom: 4, left: 4};
572 | let actual;
573 |
574 | barChart.margin(expected);
575 | actual = barChart.margin();
576 |
577 | expect(previous).not.toEqual(actual);
578 | expect(actual).toEqual(expected);
579 | });
580 |
581 | describe('when margins are set partially', () => {
582 |
583 | it('should override the default values', () => {
584 | const previous = barChart.margin();
585 | const expected = {
586 | ...previous,
587 | top: 10,
588 | right: 20
589 | };
590 | let actual;
591 |
592 | barChart.margin({
593 | top: 10,
594 | right: 20
595 | });
596 | actual = barChart.margin();
597 |
598 | expect(previous).not.toEqual(actual);
599 | expect(actual).toEqual(expected);
600 | })
601 | });
602 | });
603 |
604 | // Listing 9-25. Code for the margin accessor
605 | exports.margin = function(_x) {
606 | if (!arguments.length) {
607 | return margin;
608 | }
609 | margin = {
610 | ...margin,
611 | ..._x
612 | };
613 |
614 | return this;
615 | };
616 |
617 | // Listing 9-26. Code coverage summary
618 | Executed 23 of 23 SUCCESS (0.319 secs / 0.264 secs)
619 |
620 |
621 | =============================== Coverage summary ===============================
622 | Statements : 100% ( 60/60 )
623 | Branches : 100% ( 10/10 )
624 | Functions : 100% ( 22/22 )
625 | Lines : 100% ( 58/58 )
626 | ================================================================================
627 | ✨ Done in 8.03s.
628 |
--------------------------------------------------------------------------------
/code/ch10/listings.js:
--------------------------------------------------------------------------------
1 | // Listing 10-1. Running npm init in our demo project
2 | $ npm init
3 | This utility will walk you through creating a package.json file.
4 | It only covers the most common items, and tries to guess sensible defaults.
5 |
6 | See `npm help json` for definitive documentation on these fields
7 | and exactly what they do.
8 |
9 | Use `npm install ` afterwards to install a package and
10 | save it as a dependency in the package.json file.
11 |
12 | Press ^C at any time to quit.
13 | package name: (pro-d3-building)
14 | version: (1.0.0)
15 | description: Demo package for the package building chapter on Pro D3.js
16 | entry point: (index.js)
17 | test command: yarn test
18 | git repository: (https://github.com/Golodhros/pro-d3-building.git)
19 | keywords: d3.js, build, package, npm
20 | author: Marcos Iglesias Valle
21 | license: (ISC)
22 | About to write to /Users/miglesias/Sites/a-d3/pro-d3-building/package.json:
23 |
24 | {
25 | "name": "pro-d3-building",
26 | "version": "1.0.0",
27 | "description": "Demo package for the package building chapter on Pro D3.js",
28 | "main": "index.js",
29 | "scripts": {
30 | "test": "yarn test"
31 | },
32 | "repository": {
33 | "type": "git",
34 | "url": "git+https://github.com/Golodhros/pro-d3-building.git"
35 | },
36 | "keywords": [
37 | "d3.js",
38 | "build",
39 | "package",
40 | "npm"
41 | ],
42 | "author": "Marcos Iglesias Valle",
43 | "license": "ISC",
44 | "bugs": {
45 | "url": "https://github.com/Golodhros/pro-d3-building/issues"
46 | },
47 | "homepage": "https://github.com/Golodhros/pro-d3-building#readme"
48 | }
49 |
50 | Is this OK? (yes)
51 |
52 | // Listing 10-2. The initial scripts object in our package.json
53 | "scripts": {
54 | "test": "yarn test",
55 | "build": "webpack --config webpack.config.js"
56 | },
57 |
58 | // Listing 10-3. A CSS loader example
59 | module: {
60 | rules: [
61 | {
62 | test:/\.scss$/,
63 | use: [
64 | 'style-loader',
65 | 'css-loader',
66 | ],
67 | exclude: /node_modules/,
68 | },
69 | ],
70 | },
71 |
72 | // Listing 10-4. Adding plugins to the bundling pipeline
73 | plugins: [
74 | new DashboardPlugin(),
75 | new BundleAnalyzerPlugin({
76 | analyzerPort: 123
77 | }),
78 | ],
79 |
80 | // Listing 10-5. Production bundle Webpack configuration
81 | const path = require('path');
82 | const merge = require('webpack-merge');
83 |
84 | const parts = require('./webpack.parts');
85 | const constants = require('./webpack.constants');
86 |
87 | const prodBundleConfig = merge([
88 | {
89 | mode: 'production',
90 | devtool: 'source-map',
91 | entry: {
92 | proD3Building: constants.PATHS.bundleIndex
93 | },
94 | output: {
95 | path: path.resolve(__dirname, 'dist/'),
96 | filename: 'proD3Building.min.js',
97 | library: ['proD3Building'],
98 | libraryTarget: 'umd'
99 | },
100 | },
101 | parts.babelLoader(),
102 | parts.cssLoader(),
103 | parts.externals(),
104 | ]);
105 |
106 | module.exports = (env) => {
107 | if (env === 'production') {
108 | return prodBundleConfig;
109 | }
110 | };
111 |
112 | // Listing 10-5. Index file
113 | export {default as bar} from './charts/barChart.js';
114 |
115 |
116 | // Listing 10-6. Webpack parts file with externals
117 | exports.externals = () => ({
118 | externals: {
119 | commonjs: 'd3',
120 | amd: 'd3',
121 | root: 'd3'
122 | },
123 | });
124 |
125 | // Loaders
126 | exports.cssLoader = () => ({
127 | module: {
128 | rules: [
129 | {
130 | test: /\.css$/,
131 | use: [
132 | 'style-loader',
133 | 'css-loader',
134 | ]
135 | },
136 | ],
137 | },
138 | });
139 | exports.babelLoader = () => ({});
140 |
141 | // Listing 10-7. Our first run log of 'yarn build'
142 | $ yarn build
143 | yarn run v1.16.0
144 | $ webpack --config webpack.config.js --env=production
145 | Hash: 5977d9e3a04ecd337815
146 | Version: webpack 4.35.2
147 | Time: 5256ms
148 | Built at: 07/07/2019 2:18:20 PM
149 | Asset Size Chunks Chunk Names
150 | proD3Building.min.js 137 KiB 0 [emitted] proD3Building
151 | proD3Building.min.js.map 617 KiB 0 [emitted] proD3Building
152 | Entrypoint proD3Building = proD3Building.min.js proD3Building.min.js.map
153 | [0] ./src/charts/barChart.css 1.08 KiB {0} [built]
154 | [1] ./node_modules/css-loader/dist/cjs.js!./src/charts/barChart.css 292 bytes {0} [built]
155 | [5] ./src/index.js + 517 modules 536 KiB {0} [built]
156 | | ./src/index.js 53 bytes [built]
157 | | ./src/charts/barChart.js 4.4 KiB [built]
158 | | + 516 hidden modules
159 | + 3 hidden modules
160 | ✨ Done in 6.99s.
161 |
162 | // Listing 10-8. Development configuration
163 | const path = require('path');
164 | const merge = require('webpack-merge');
165 |
166 | const HtmlWebpackPlugin = require('html-webpack-plugin');
167 |
168 | const parts = require('./webpack.parts');
169 | const constants = require('./webpack.constants');
170 |
171 | //... Production Configuration
172 |
173 | const devConfig = merge([
174 | {
175 | mode: 'development',
176 | devtool: 'cheap-eval-source-map',
177 | entry: constants.DEMOS,
178 | output: {
179 | path: path.resolve(__dirname, 'demos/build'),
180 | filename: '[name].js'
181 | },
182 | devServer: {
183 | contentBase: './demos/build',
184 | port: 8001,
185 | inline: true,
186 | hot: true,
187 | open: true,
188 | },
189 | plugins: [
190 | new HtmlWebpackPlugin({
191 | title: 'Development',
192 | template: 'src/demos/index.html'
193 | })
194 | ],
195 | },
196 | parts.cssLoader(),
197 | parts.babelLoader(),
198 | ]);
199 |
200 | module.exports = (env) => {
201 |
202 | if (env === 'dev') {
203 | return devConfig;
204 | }
205 |
206 | if (env === 'production') {
207 | return prodBundleConfig;
208 | }
209 | };
210 |
211 | // Listing 10-9. The tests.webpack.js file
212 | const context = require.context('./charts', true, /\.test\.js$/);
213 |
214 | context.keys().forEach(context);
215 |
216 | // Listing 10-10. The test configuration
217 | //... Dependencies imports
218 | //... Production and Development configurations
219 |
220 | const testConfig = merge([
221 | {
222 | mode: 'development',
223 | devtool: 'inline-source-map',
224 | resolve: {
225 | modules: [
226 | path.resolve(__dirname, 'src/charts'),
227 | 'node_modules',
228 | ],
229 | },
230 | },
231 | parts.cssLoader(),
232 | parts.babelLoader(),
233 | ]);
234 |
235 | module.exports = (env) => {
236 | if (env === 'dev') {
237 | return devConfig;
238 | }
239 | if (env === 'test') {
240 | return testConfig;
241 | }
242 | if (env === 'production') {
243 | return prodBundleConfig;
244 | }
245 | };
246 |
247 | // Listing 10-11. The karma.conf.js file
248 | // Karma configuration
249 | const webpack = require('webpack');
250 | const webpackConfig = require('./webpack.config');
251 |
252 | module.exports = function(config) {
253 | config.set({
254 |
255 | // base path that will be used to resolve all patterns (eg. files, exclude)
256 | basePath: '',
257 |
258 | // frameworks to use
259 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
260 | frameworks: ['jasmine'],
261 |
262 | // list of files / patterns to load in the browser
263 | files: [
264 | 'src/tests.webpack.js'
265 | ],
266 |
267 | // preprocess matching files before serving them to the browser
268 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
269 | preprocessors: {
270 | 'src/tests.webpack.js': [ 'webpack', 'sourcemap' ]
271 | },
272 |
273 | webpack: webpackConfig('test'),
274 |
275 | //... Code ommitted for brevity
276 | })
277 | }
278 |
279 | // Listing 10-12. Our tests running
280 | $ yarn test
281 | yarn run v1.16.0
282 | $ karma start --env=test
283 | // ...
284 | 07 07 2019 20:48:17.068:WARN [karma]: No captured browser, open http://localhost:9876/
285 | 07 07 2019 20:48:17.075:INFO [karma-server]: Karma v4.1.0 server started at http://0.0.0.0:9876/
286 | 07 07 2019 20:48:17.076:INFO [launcher]: Launching browsers Chrome with concurrency unlimited
287 | 07 07 2019 20:48:17.091:INFO [launcher]: Starting browser Chrome
288 | 07 07 2019 20:48:18.692:INFO [Chrome 75.0.3770 (Mac OS X 10.12.6)]: Connected on socket RXE75EE2UAkJ98GwAAAA with id 94795783
289 | .......................
290 | Chrome 75.0.3770 (Mac OS X 10.12.6): Executed 23 of 23 SUCCESS (0.164 secs / 0.124 secs)
291 |
292 | // Listing 10-13. The Istanbul code coverage loader
293 | exports.istanbulLoader = () => ({
294 | module: {
295 | rules: [
296 | {
297 | test: /\.js?$/,
298 | include: /src/,
299 | exclude: /(node_modules|tests.webpack.js)/,
300 | use: [{
301 | loader: 'istanbul-instrumenter-loader',
302 | query: {
303 | esModules: true
304 | }
305 | }],
306 | }
307 | ]
308 | }
309 | });
310 |
311 | // Listing 10-14. Code coverage report in the command line
312 | .......................
313 | Chrome 75.0.3770 (Mac OS X 10.12.6): Executed 23 of 23 SUCCESS (0.155 secs / 0.121 secs)
314 | -------------------|----------|----------|----------|----------|----------------|
315 | File | % Stmts | % Branch | % Funcs | % Lines |Uncovered Lines |
316 | -------------------|----------|----------|----------|----------|----------------|
317 | charts/ | 100 | 100 | 98.51 | 100 | |
318 | barChart.js | 100 | 100 | 100 | 100 | |
319 | barChart.test.js | 100 | 100 | 97.78 | 100 | |
320 | -------------------|----------|----------|----------|----------|----------------|
321 | All files | 100 | 100 | 98.51 | 100 | |
322 | -------------------|----------|----------|----------|----------|----------------|
323 |
324 | // Listing 10-15. Our babel configuration in package.json
325 | ...
326 | "browserslist": "defaults, IE 10",
327 | "babel": {
328 | "presets": [
329 | ["@babel/preset-env", {
330 | "debug":true
331 | }]
332 | ]
333 | }
334 |
335 | // Listing 10-16. Babel Loader in webpack.parts.js
336 | exports.babelLoader = () => ({
337 | module: {
338 | rules: [
339 | {
340 | test: /\.js$/,
341 | exclude: /node_modules/,
342 | use: ['babel-loader'],
343 | },
344 | ],
345 | },
346 | });
347 |
348 | // Listing 10-17. npm publish log
349 | npm publish
350 | npm notice
351 | npm notice 📦 pro-d3-building@1.0.0
352 | npm notice === Tarball Contents ===
353 | npm notice 1.5kB package.json
354 | npm notice 763B README.md
355 | npm notice 141.3kB dist/proD3Building.min.js
356 | npm notice 631.7kB dist/proD3Building.min.js.map
357 | npm notice 137B src/charts/barChart.css
358 | npm notice 4.5kB src/charts/barChart.js
359 | npm notice 12.5kB src/charts/barChart.test.js
360 | npm notice 1.6kB src/demos/demo-bar.js
361 | npm notice 453B src/demos/index.html
362 | npm notice 53B src/index.js
363 | npm notice 269B src/tests.webpack.js
364 | npm notice === Tarball Details ===
365 | npm notice name: pro-d3-building
366 | npm notice version: 1.0.0
367 | npm notice package size: 207.7 kB
368 | npm notice unpacked size: 794.9 kB
369 | npm notice shasum: 30529ff408379bfafcc6c04f8484bb17c5fcfba0
370 | npm notice integrity: sha512-gpj5lhvMOW7BV[...]k2oJszEWwdPew==
371 | npm notice total files: 11
372 | npm notice
373 | + pro-d3-building@1.0.0
374 |
--------------------------------------------------------------------------------
/code/ch11/listings.js:
--------------------------------------------------------------------------------
1 | // Listing 11-1. Private function comments
2 | /**
3 | * Builds the SVG element that will contain the chart
4 | * @param {HTMLElement} container DOM element that will work as the container of the graph
5 | * @private
6 | */
7 | function buildSVG(container){
8 | if (!svg) {
9 | svg = d3.select(container)
10 | .append('svg')
11 | .classed('bar-chart', true);
12 |
13 | buildContainerGroups();
14 | }
15 | svg
16 | .attr('width', width)
17 | .attr('height', height);
18 | }
19 |
20 | // Listing 11-2. Public accessor function comments
21 | /**
22 | * Gets or Sets the height of the chart
23 | * @param {Number} [_x=500] Desired height for the chart
24 | * @return {Number | Module} Current height or Chart module to chain calls
25 | * @public
26 | */
27 | exports.height = function(_x) {
28 | if (!arguments.length) {
29 | return height;
30 | }
31 | height = _x;
32 |
33 | return this;
34 | };
35 |
36 |
37 | // Listing 11-3. Comments for the bar chart module.
38 | /**
39 | * Bar Chart Reusable API component that renders a
40 | * simple and configurable bar chart.
41 | *
42 | * @module Bar
43 | * @tutorial bar
44 | * @requires d3
45 | *
46 | * @example
47 | * const barChart = bar();
48 | *
49 | * barChart
50 | * .height(500)
51 | * .width(800);
52 | *
53 | * d3Selection.select('.css-selector')
54 | * .datum(dataset)
55 | * .call(barChart);
56 | *
57 | */
58 | function bar() {
59 | //...
60 | }
61 |
62 | // Listing 11-4. Defining a complex data type.
63 | /**
64 | * @typedef BarData
65 | * @type {Object[]}
66 | * @property {String} letter Name of the letter (required)
67 | * @property {Number} frequency Value of its frequency (required)
68 | *
69 | * @example
70 | * [
71 | * {
72 | * letter: 'A',
73 | * frequency: 0.08167
74 | * },
75 | * {
76 | * letter: 'B',
77 | * frequency: 0.01492
78 | * }
79 | * ]
80 | */
81 |
82 | // Listing 11-5. JSDoc-to-markdown output excerpt
83 | ## Bar
84 | Bar Chart Reusable API component that renders a
85 | simple and configurable bar chart.
86 |
87 | **Requires**: module:d3
88 | **Example**
89 | ```js
90 | const barChart = bar();
91 |
92 | barChart
93 | .height(500)
94 | .width(800);
95 |
96 | d3Selection.select('.css-selector')
97 | .datum(dataset)
98 | .call(barChart);
99 | ```
100 | * [Bar](#module_Bar)
101 | * [exports(_selection, _data)](#exp_module_Bar--exports)
102 | * [.height([_x])](#module_Bar--exports.height) ⇒ Number
\| Module
103 | * [.margin(_x)](#module_Bar--exports.margin) ⇒ Object
\| Module
104 | * [.on()](#module_Bar--exports.on) ⇒ Module
105 | * [.width([_x])](#module_Bar--exports.width) ⇒ Number
\| Module
106 |
107 |
108 |
109 | ### exports(_selection, _data) ⏏
110 | This function creates the chart using the selection as container
111 |
112 | **Kind**: Exported function
113 |
114 | | Param | Type | Description |
115 | | --- | --- | --- |
116 | | _selection | D3Selection
| A d3 selection that represents the container(s) where the chart(s) will be rendered |
117 | | _data | [BarData
](#BarData) | The data to attach and generate the chart |
118 |
119 |
120 |
121 | #### exports.height([_x]) ⇒ Number
\| Module
122 | Gets or Sets the height of the chart
123 |
124 | **Kind**: static method of [exports
](#exp_module_Bar--exports)
125 | **Returns**: Number
\| Module
- Current height or Chart module to chain calls
126 | **Access**: public
127 |
128 | | Param | Type | Default | Description |
129 | | --- | --- | --- | --- |
130 | | [_x] | Number
| 500
| Desired height for the chart |
131 |
132 | // Listing 11-6. documentation.yml configuration with grouped sections and Readme
133 | toc:
134 | - name: Homepage
135 | file: README.md
136 | - name: Charts
137 | children:
138 | - Bar
139 | - name: Data Schemas
140 | children:
141 | - bar
142 |
143 | // Listing 11-7. Calling Documentation.js with a configuration file
144 | "docs:serve": "documentation serve src/charts/** -f html --config documentation.yml",
145 |
146 | // Listing 11-8. Calling Documentation.js with a theme
147 | "docs:serve": "documentation serve src/charts/** -f html --config documentation.yml --theme src/docs/theme",
148 |
149 | // Listing 11-9. The ESLint loader
150 | exports.ESLintLoader = () => ({
151 | module: {
152 | rules: [
153 | {
154 | enforce: 'pre',
155 | test: /\.js$/,
156 | include: /src/,
157 | exclude: /node_modules/,
158 | use: ['eslint-loader'],
159 | options: {
160 | failOnError: true
161 | }
162 | }
163 | ]
164 | }
165 | });
166 |
167 | // Listing 11-10. ESLint configuration on package.json
168 | "eslintConfig": {
169 | "parser": "babel-eslint",
170 | "parserOptions": {
171 | "ecmaVersion": 8,
172 | "sourceType": "module"
173 | },
174 | "plugins": [
175 | "jsdoc"
176 | ],
177 | "env": {
178 | "browser": true,
179 | "es6": true
180 | },
181 | "rules": {
182 | "jsdoc/require-jsdoc": ["error"],
183 | "jsdoc/require-param": ["error"],
184 | "jsdoc/require-param-name": ["error"],
185 | "jsdoc/require-param-description": ["error"]
186 | }
187 | },
188 |
--------------------------------------------------------------------------------
/code/ch12/barChart.js:
--------------------------------------------------------------------------------
1 | import * as d3 from "d3";
2 |
3 | import "./barChart.scss";
4 |
5 | function bar() {
6 | let data;
7 | let svg;
8 | let margin = {
9 | top: 20,
10 | right: 20,
11 | bottom: 30,
12 | left: 40
13 | };
14 | let width = 960;
15 | let height = 500;
16 | let chartWidth;
17 | let chartHeight;
18 | let xScale;
19 | let yScale;
20 | let xAxis;
21 | let yAxis;
22 |
23 | // Dispatcher object to broadcast the mouse events
24 | const dispatcher = d3.dispatch(
25 | "customMouseOver",
26 | "customMouseMove",
27 | "customMouseOut",
28 | "customMouseClick"
29 | );
30 |
31 | // extractors
32 | const getFrequency = ({ frequency }) => frequency;
33 | const getLetter = ({ letter }) => letter;
34 |
35 | function exports(_selection) {
36 | _selection.each(function(_data) {
37 | data = _data;
38 | chartHeight = height - margin.top - margin.bottom;
39 | chartWidth = width - margin.left - margin.right;
40 | debugger;
41 | buildScales();
42 | buildAxes();
43 | buildSVG(this);
44 | drawAxes();
45 | drawBars();
46 | });
47 | }
48 |
49 | // Building Blocks
50 | function buildAxes() {
51 | xAxis = d3.axisBottom(xScale);
52 |
53 | yAxis = d3.axisLeft(yScale).ticks(10, "%");
54 | }
55 |
56 | function buildContainerGroups() {
57 | let container = svg
58 | .append("g")
59 | .classed("container-group", true)
60 | .attr("transform", `translate(${margin.left},${margin.top})`);
61 |
62 | container.append("g").classed("chart-group", true);
63 | container.append("g").classed("x-axis-group axis", true);
64 | container.append("g").classed("y-axis-group axis", true);
65 | }
66 |
67 | function buildScales() {
68 | xScale = d3
69 | .scaleBand()
70 | .rangeRound([0, chartWidth])
71 | .padding(0.1)
72 | .domain(data.map(getLetter));
73 |
74 | yScale = d3
75 | .scaleLinear()
76 | .rangeRound([chartHeight, 0])
77 | .domain([0, d3.max(data, getFrequency)]);
78 | }
79 |
80 | function buildSVG(container) {
81 | if (!svg) {
82 | svg = d3
83 | .select(container)
84 | .append("svg")
85 | .classed("bar-chart", true);
86 |
87 | buildContainerGroups();
88 | }
89 | svg.attr("width", width).attr("height", height);
90 | }
91 |
92 | function drawAxes() {
93 | svg
94 | .select(".x-axis-group.axis")
95 | .attr("transform", `translate(0,${chartHeight})`)
96 | .call(xAxis);
97 |
98 | svg
99 | .select(".y-axis-group.axis")
100 | .call(yAxis)
101 | .append("text")
102 | .attr("transform", "rotate(-90)")
103 | .attr("y", 6)
104 | .attr("dy", "0.71em")
105 | .attr("text-anchor", "end")
106 | .text("Frequency");
107 | }
108 |
109 | function drawBars() {
110 | let bars = svg
111 | .select(".chart-group")
112 | .selectAll(".bar")
113 | .data(data);
114 |
115 | // Enter
116 | bars
117 | .enter()
118 | .append("rect")
119 | .classed("bar", true)
120 | .attr("x", ({ letter }) => xScale(letter))
121 | .attr("y", ({ frequency }) => yScale(frequency))
122 | .attr("width", xScale.bandwidth())
123 | .attr("height", ({ frequency }) => chartHeight - yScale(frequency))
124 | .on("mouseover", function(d) {
125 | dispatcher.call("customMouseOver", this, d);
126 | })
127 | .on("mousemove", function(d) {
128 | dispatcher.call("customMouseMove", this, d);
129 | })
130 | .on("mouseout", function(d) {
131 | dispatcher.call("customMouseOut", this, d);
132 | })
133 | .on("click", function(d) {
134 | dispatcher.call("customMouseClick", this, d);
135 | });
136 |
137 | // Exit
138 | bars
139 | .exit()
140 | .style("opacity", 0)
141 | .remove();
142 | }
143 |
144 | // API
145 | exports.height = function(_x) {
146 | if (!arguments.length) {
147 | return height;
148 | }
149 | height = _x;
150 |
151 | return this;
152 | };
153 |
154 | exports.margin = function(_x) {
155 | if (!arguments.length) {
156 | return margin;
157 | }
158 | margin = {
159 | ...margin,
160 | ..._x
161 | };
162 |
163 | return this;
164 | };
165 |
166 | exports.on = function() {
167 | let value = dispatcher.on.apply(dispatcher, arguments);
168 |
169 | return value === dispatcher ? exports : value;
170 | };
171 |
172 | exports.width = function(_x) {
173 | if (!arguments.length) {
174 | return width;
175 | }
176 | width = _x;
177 |
178 | return this;
179 | };
180 |
181 | return exports;
182 | }
183 |
184 | export default bar;
185 |
--------------------------------------------------------------------------------
/code/ch12/listing-12-7-BarChart.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import D3Bar from "./D3Bar";
5 |
6 | export default class BarChart extends React.Component {
7 | static propTypes = {
8 | /**
9 | * Internally used, do not overwrite.
10 | */
11 | data: PropTypes.arrayOf(PropTypes.any),
12 |
13 | /**
14 | * Gets or Sets the height of the chart
15 | */
16 | height: PropTypes.number,
17 |
18 | /**
19 | * Gets or Sets the margin of the chart
20 | */
21 | margin: PropTypes.shape({
22 | top: PropTypes.number,
23 | bottom: PropTypes.number,
24 | left: PropTypes.number,
25 | right: PropTypes.number
26 | }),
27 |
28 | /**
29 | * Gets or Sets the width of the chart
30 | */
31 | width: PropTypes.number,
32 |
33 | /**
34 | * Internally used, do not overwrite.
35 | *
36 | * @ignore
37 | */
38 | chart: PropTypes.object
39 | };
40 |
41 | static defaultProps = {
42 | chart: D3Bar
43 | };
44 |
45 | componentDidMount() {
46 | const { height, width, margin } = this.props;
47 | const configuration = { height, width, margin };
48 |
49 | this._chart = this.props.chart.create(
50 | this._rootNode,
51 | this.props.data,
52 | configuration
53 | );
54 | }
55 |
56 | componentDidUpdate() {
57 | const { height, width, margin } = this.props;
58 | const configuration = { height, width, margin };
59 |
60 | this.props.chart.update(
61 | this._rootNode,
62 | this.props.data,
63 | configuration,
64 | this._chart
65 | );
66 | }
67 |
68 | componentWillUnmount() {
69 | this.props.chart.destroy(this._rootNode);
70 | }
71 |
72 | _setRef(componentNode) {
73 | this._rootNode = componentNode;
74 | }
75 |
76 | render() {
77 | return ;
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/code/ch12/listing-12-8-D3Bar.js:
--------------------------------------------------------------------------------
1 | import * as d3 from "d3";
2 | import bar from "./barChart";
3 |
4 | const setChartProperty = (chart, configuration, key) => {
5 | if (configuration[key] || typeof configuration[key] === "string") {
6 | chart[key](configuration[key]);
7 | }
8 | };
9 |
10 | const applyConfiguration = (chart, configuration) => {
11 | Object.keys(configuration).forEach(
12 | setChartProperty.bind(null, chart, configuration)
13 | );
14 |
15 | return chart;
16 | };
17 |
18 | const D3Bar = {};
19 |
20 | D3Bar.create = (el, data, configuration = {}) => {
21 | const container = d3.select(el);
22 | const chart = bar();
23 |
24 | container.datum(data).call(applyConfiguration(chart, configuration));
25 |
26 | return chart;
27 | };
28 |
29 | D3Bar.update = (el, data, configuration = {}, chart) => {
30 | const container = d3.select(el);
31 |
32 | // Calls the chart with the container and dataset
33 | if (data) {
34 | container.datum(data).call(applyConfiguration(chart, configuration));
35 | } else {
36 | container.call(applyConfiguration(chart, configuration));
37 | }
38 |
39 | return chart;
40 | };
41 |
42 | D3Bar.destroy = () => {};
43 |
44 | export default D3Bar;
45 |
--------------------------------------------------------------------------------
/code/ch12/listing-12-9-App.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import BarChart from "./BarChart";
4 |
5 | const fixtureData = [
6 | {
7 | letter: "A",
8 | frequency: 0.08167
9 | },
10 | {
11 | letter: "B",
12 | frequency: 0.01492
13 | },
14 | {
15 | letter: "C",
16 | frequency: 0.02782
17 | },
18 | {
19 | letter: "D",
20 | frequency: 0.04253
21 | },
22 | {
23 | letter: "E",
24 | frequency: 0.12702
25 | },
26 | {
27 | letter: "F",
28 | frequency: 0.02288
29 | },
30 | {
31 | letter: "G",
32 | frequency: 0.02015
33 | },
34 | {
35 | letter: "H",
36 | frequency: 0.06094
37 | },
38 | {
39 | letter: "I",
40 | frequency: 0.06966
41 | },
42 | {
43 | letter: "J",
44 | frequency: 0.00153
45 | },
46 | {
47 | letter: "K",
48 | frequency: 0.00772
49 | },
50 | {
51 | letter: "L",
52 | frequency: 0.04025
53 | },
54 | {
55 | letter: "M",
56 | frequency: 0.02406
57 | },
58 | {
59 | letter: "N",
60 | frequency: 0.06749
61 | },
62 | {
63 | letter: "O",
64 | frequency: 0.07507
65 | },
66 | {
67 | letter: "P",
68 | frequency: 0.01929
69 | },
70 | {
71 | letter: "Q",
72 | frequency: 0.00095
73 | },
74 | {
75 | letter: "R",
76 | frequency: 0.05987
77 | },
78 | {
79 | letter: "S",
80 | frequency: 0.06327
81 | },
82 | {
83 | letter: "T",
84 | frequency: 0.09056
85 | },
86 | {
87 | letter: "U",
88 | frequency: 0.02758
89 | },
90 | {
91 | letter: "V",
92 | frequency: 0.00978
93 | },
94 | {
95 | letter: "W",
96 | frequency: 0.0236
97 | },
98 | {
99 | letter: "X",
100 | frequency: 0.0015
101 | },
102 | {
103 | letter: "Y",
104 | frequency: 0.01974
105 | },
106 | {
107 | letter: "Z",
108 | frequency: 0.00074
109 | }
110 | ];
111 |
112 | function App() {
113 | return (
114 |
115 |
126 |
127 | );
128 | }
129 |
130 | const rootElement = document.getElementById("root");
131 | ReactDOM.render(, rootElement);
132 |
--------------------------------------------------------------------------------
/code/ch12/listings.js:
--------------------------------------------------------------------------------
1 | // Listing 12-1. A data join example from our bar chart
2 | g.selectAll(".bar")
3 | .data(data)
4 | .enter()
5 | .append("rect")
6 | .attr("class", "bar")
7 | .attr("x", function(d) { return x(d.letter); })
8 | .attr("y", function(d) { return y(d.frequency); })
9 | .attr("width", x.bandwidth())
10 | .attr("height", function(d) { return height - y(d.frequency); });
11 |
12 | // Listing 12-2. D3.js within React example
13 | import React from 'react';
14 | import * as d3 from 'd3';
15 |
16 | class Line extends React.Component {
17 | componentDidMount() {
18 | // D3 Code to create the chart
19 | // using this._rootNode as container
20 | }
21 |
22 | shouldComponentUpdate() {
23 | // Prevents component re-rendering
24 | return false;
25 | }
26 |
27 | render() {
28 | return(
29 |
33 | )
34 | }
35 | }
36 |
37 | // Listing 12-3. React Faux DOM example
38 | import React from 'react';
39 | import * as d3 from 'd3';
40 | import {withFauxDOM} from 'react-faux-dom';
41 |
42 | class Line extends React.Component {
43 | componentDidMount() {
44 | // Creates a fake div and stores its virtual DOM inside the 'chart' prop
45 | const faux = this.props.connectFauxDOM('div', 'chart');
46 |
47 | // D3 Code to create the chart
48 | // using faux as container
49 | d3.select(faux)
50 | .append('svg')
51 | {...}
52 |
53 | this.props.animateFauxDOM(800);
54 | }
55 |
56 | render() {
57 |
58 | {this.props.chart}
59 |
60 | }
61 | }
62 |
63 | export default withFauxDOM(Line);
64 |
65 | // Listing 12-4. Lifecycle methods wrapper example
66 | import React from 'react';
67 | import D3Line from './D3Line';
68 |
69 | class Line extends React.Component {
70 | componentDidMount() {
71 | // D3 Code to create the chart
72 | this._chart = D3Line.create(
73 | this._rootNode,
74 | this.props.data,
75 | this.props.config
76 | );
77 | }
78 |
79 | componentDidUpdate() {
80 | // D3 Code to update the chart
81 | D3Line.update(
82 | this._rootNode,
83 | this.props.data,
84 | this.props.config,
85 | this._chart
86 | );
87 | }
88 |
89 | componentWillUnmount() {
90 | D3Line.destroy(this._rootNode);
91 | }
92 |
93 | _setRef(componentNode) {
94 | this._rootNode = componentNode;
95 | }
96 |
97 | render() {
98 |
102 | }
103 | }
104 |
105 | // Listing 12-5. Lifecycle methods chart facade example
106 | const D3Line = {};
107 |
108 | D3Line.create = (el, data, configuration) => {
109 | // D3.js Code to create the chart
110 | };
111 |
112 | D3Line.update = (el, data, configuration, chart) => {
113 | // D3.js Code to update the chart
114 | };
115 |
116 | D3Line.destroy = () => {
117 | // Cleaning code here
118 | };
119 |
120 | export default D3Line;
121 |
122 | // Listing 12-6. D3.js for the Math, React for the DOM example
123 | import React from 'react';
124 | import * as d3 from 'd3';
125 |
126 | class Line extends React.Component {
127 | drawLine() {
128 | let xScale = d3.scaleTime()
129 | .domain(d3.extent(
130 | this.props.data,
131 | ({date}) => date
132 | ))
133 | .rangeRound([0, this.props.width]);
134 |
135 | let yScale = d3.scaleLinear()
136 | .domain(d3.extent(
137 | this.props.data,
138 | ({value}) => value
139 | ))
140 | .rangeRound([this.props.height, 0]);
141 |
142 | let line = d3.line()
143 | .x((d) => xScale(d.date))
144 | .y((d) => yScale(d.value));
145 |
146 | return (
147 |
151 | );
152 | }
153 |
154 | render() {
155 |
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pro-d3-source-code",
3 | "version": "0.0.1",
4 | "description": "Code for the Pro D3.js book for APress",
5 | "main": "index.js",
6 | "scripts": {
7 | },
8 | "repository": {
9 | "type": "git",
10 | "url": "git+https://github.com/golodhros/pro-d3-source-code.git"
11 | },
12 | "keywords": [
13 | "d3.js",
14 | "charts",
15 | "testing",
16 | "tdd"
17 | ],
18 | "author": "golodhros@gmail.com",
19 | "license": "ISC",
20 | "bugs": {
21 | "url": "https://github.com/golodhros/pro-d3-source-code/issues"
22 | },
23 | "homepage": "https://github.com/golodhros/pro-d3-source-code#readme",
24 | "devDependencies": {
25 | "eslint": "^5.4.0",
26 | "eslint-config-airbnb": "^17.1.0",
27 | "eslint-plugin-import": "^2.14.0",
28 | "eslint-plugin-jsx-a11y": "^6.1.1",
29 | "eslint-plugin-react": "^7.11.1"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------