├── .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 |
5 | 20 |
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 |
5 | 20 |
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 | 160 | {this.drawLine()} 161 | 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 | --------------------------------------------------------------------------------