├── .RData ├── .gitignore ├── LICENCE.txt ├── README.md ├── app.js ├── dual-masthead.svg ├── favicon.png ├── hemicycle ├── 1.png ├── index.html └── script.js ├── index.html ├── index.json ├── joyplot ├── 1.png ├── D3 │ ├── script.js │ └── style.css ├── README.md ├── data │ └── data.json └── index.html ├── page.css ├── palettes ├── index.html ├── palettes.json ├── script.js └── style.css ├── scatterplot ├── 1.png ├── D3 │ ├── script.js │ └── style.css ├── R │ ├── scatterplot-r.svg │ └── script.R ├── README.md ├── data.csv ├── data.json └── index.html ├── small-multiples ├── 1.png ├── README.md ├── index.html ├── script.js └── style.css ├── style.css ├── timeline-bubbles ├── 1.png ├── D3 │ ├── script.js │ └── style.css ├── README.md ├── data.json └── index.html ├── times-visual-vocabulary.Rproj ├── treemap ├── 1.png ├── README.md ├── data.json ├── index.html ├── script.js └── style.css ├── vertical-stepped-line ├── 1.png ├── README.md ├── data.json ├── index.html ├── script.js └── style.css └── waffle ├── 1.png ├── D3 ├── layout.js ├── script.js └── style.css ├── README.md └── index.html /.RData: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/times/dataviz-catalogue/fadceb03044acd37da4676a342a719fce8f274d7/.RData -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .Rproj.user 3 | .Rhistory 4 | .RData -------------------------------------------------------------------------------- /LICENCE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2018 Times Newspapers Limited 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Times Data Viz catalogue 2 | 3 | [Read about this project on Medium](https://medium.com/digital-times/opening-the-times-dataviz-catalogue-c6fd6e30ccb2) 4 | 5 | * [Vertical steppy line](https://github.com/times/dataviz-catalogue/tree/master/vertical-steppy-line) 6 | * [Scatterplot](https://github.com/times/dataviz-catalogue/tree/master/scatterplot) 7 | * [Treemap](https://github.com/times/dataviz-catalogue/tree/master/treemap) 8 | * [Small multiples](https://github.com/times/dataviz-catalogue/tree/master/small-multiples) 9 | * [Timeline of bubbles](https://github.com/times/dataviz-catalogue/tree/master/timeline-bubbles) 10 | * [Hemicycle](https://github.com/times/dataviz-catalogue/tree/master/hemicycle) 11 | * [Waffle chart](https://github.com/times/dataviz-catalogue/tree/master/waffle) 12 | 13 | ## Instructions 14 | 15 | Download this repo as ZIP file (or `git clone` it). 16 | 17 | Open `index.html` in your favourite web browser to display the available charts and some basic instructions. 18 | 19 | Each folder contains a working version of a chart. You can of course edit the files to tweak things. 20 | 21 | --- 22 | 23 | ## Contributing 24 | 25 | We'd be delighted to see the community contribute to this repository (or to spot mistakes we've made). To contribute to this work in progress, please either: 26 | 27 | * [File an issue](https://github.com/times/dataviz-catalogue/issues) with your Github account explaining the issue or improvement you are proposing 28 | * [Fork then open a pull request](https://github.com/times/dataviz-catalogue/pulls) so we can review your work and schedule it for release 29 | * Or simply get in touch with us at the details below! 30 | 31 | 32 | ## Contact 33 | 34 | * Basile Simon, basile.simon@thetimes.co.uk 35 | * @TimesDevelops on Twitter 36 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | d3.json('index.json', (error, imgs) => { 2 | if (error) { 3 | console.log(error); 4 | return; 5 | } 6 | 7 | const body = d3.select('#container'); 8 | const divs = body 9 | .selectAll('div') 10 | .data(imgs) 11 | .enter() 12 | .append('div') 13 | .attr('class', 'item'); 14 | 15 | divs 16 | .append('div') 17 | .text(d => d.title) 18 | .attr('class', 'chartTitle'); 19 | 20 | const links = divs 21 | .append('a') 22 | .attr('href', d => d.url) 23 | .append('img') 24 | .style('height', '200px') 25 | .attr('src', d => d.img); 26 | }); 27 | -------------------------------------------------------------------------------- /favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/times/dataviz-catalogue/fadceb03044acd37da4676a342a719fce8f274d7/favicon.png -------------------------------------------------------------------------------- /hemicycle/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/times/dataviz-catalogue/fadceb03044acd37da4676a342a719fce8f274d7/hemicycle/1.png -------------------------------------------------------------------------------- /hemicycle/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 21 | 22 |
23 |
Hemicycle layout
24 |
Number of seats won in the Italian election 2018
25 | 26 |
27 |

This layout implements an abstracted representation of the parliament itself. Built for the 2018 Italian election, this rotated matrix featured in 28 | our detailed coverage of the results. 29 |

30 |
31 | 32 | 35 | 36 | 37 | 49 |
50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /hemicycle/script.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | width: 600, 3 | height: 400, 4 | mobileWidth: 300, 5 | mobileHeight: 200, 6 | innerRadiusCoef: 0.2, 7 | }; 8 | const isMobile = window.innerWidth < 768 ? true : false; 9 | 10 | const data = [ 11 | { id: 'leu', name: 'Free and Equal', seats: 59, color: '#700000' }, 12 | { id: 'pd', name: 'Democratic Party', seats: 281, color: '#E45B5B' }, 13 | { 14 | id: 'centre-right', 15 | name: 'Centre right', 16 | seats: 23, 17 | color: '#4990E2', 18 | }, 19 | { id: 'centre', name: 'Centre', seats: 28, color: '#F6C55E' }, 20 | { id: 'others', name: 'Others', seats: 61, color: '#dbdbdb' }, 21 | { id: 'm5s', name: 'Five Star Movement', seats: 88, color: '#F3D92B' }, 22 | { id: 'forza', name: 'Forza Italia', seats: 56, color: '#4894D2' }, 23 | { id: 'nleague', name: 'Northern League', seats: 22, color: '#65BDA2' }, 24 | { 25 | id: 'brothers', 26 | name: 'Brothers of Italy', 27 | seats: 12, 28 | color: '#1d24bf', 29 | }, 30 | ]; 31 | 32 | const series = (s, n) => { 33 | let r = 0; 34 | for (let i = 0; i <= n; i++) { 35 | r += s(i); 36 | } 37 | return r; 38 | }; 39 | 40 | // shameless reimplementation of: 41 | // https://github.com/geoffreybr/d3-parliament 42 | const makeParliament = function(data, width, height, innerRadiusCoef) { 43 | const outerParliamentRadius = Math.min(width / 2, height); 44 | const innerParliementRadius = outerParliamentRadius * innerRadiusCoef; 45 | 46 | // compute number of seats and rows of the parliament 47 | let nSeats = 0; 48 | data.forEach(p => { 49 | nSeats += 50 | typeof p.seats === 'number' ? Math.floor(p.seats) : p.seats.length; 51 | }); 52 | 53 | let nRows = 0; 54 | let maxSeatNumber = 0; 55 | let b = 0.5; // was ist das 56 | 57 | (function() { 58 | const a = innerRadiusCoef / (1 - innerRadiusCoef); 59 | const calcFloor = i => Math.floor(Math.PI * (b + i)); 60 | while (maxSeatNumber < nSeats) { 61 | nRows += 1; 62 | b += a; 63 | /* NOTE: the number of seats available in each row depends on the total number 64 | of rows and floor() is needed because a row can only contain entire seats. So, 65 | it is not possible to increment the total number of seats adding a row. */ 66 | maxSeatNumber = series(calcFloor, nRows - 1); 67 | } 68 | })(); 69 | 70 | // create the seats list 71 | // compute the cartesian and polar coordinates for each seat 72 | const rowWidth = (outerParliamentRadius - innerParliementRadius) / nRows; 73 | const seats = []; 74 | (function() { 75 | const seatsToRemove = maxSeatNumber - nSeats; 76 | for (let i = 0; i < nRows; i += 1) { 77 | const rowRadius = innerParliementRadius + rowWidth * (i + 0.5); 78 | const rowSeats = 79 | Math.floor(Math.PI * (b + i)) - 80 | Math.floor(seatsToRemove / nRows) - 81 | (seatsToRemove % nRows > i ? 1 : 0); 82 | const anglePerSeat = Math.PI / rowSeats; 83 | for (let j = 0; j < rowSeats; j += 1) { 84 | const s = {}; 85 | s.polar = { 86 | r: rowRadius, 87 | teta: -Math.PI + anglePerSeat * (j + 0.5), 88 | }; 89 | s.cartesian = { 90 | x: s.polar.r * Math.cos(s.polar.teta), 91 | y: s.polar.r * Math.sin(s.polar.teta), 92 | }; 93 | seats.push(s); 94 | } 95 | } 96 | })(); 97 | 98 | // sort the seats by angle 99 | seats.sort((a, b2) => a.polar.teta - b2.polar.teta || b2.polar.r - a.polar.r); 100 | 101 | // fill the seat objects with data of its party and of itself if existing 102 | (function() { 103 | let partyIndex = 0; 104 | let seatIndex = 0; 105 | seats.forEach(s => { 106 | // get current party and go to the next one if it has all its seats filled 107 | let party = data[partyIndex]; 108 | const nSeatsInParty = 109 | typeof party.seats === 'number' ? party.seats : party.seats.length; 110 | if (seatIndex >= nSeatsInParty) { 111 | partyIndex += 1; 112 | seatIndex = 0; 113 | party = data[partyIndex]; 114 | } 115 | 116 | // set party data 117 | s.party = party; 118 | s.data = typeof party.seats === 'number' ? null : party.seats[seatIndex]; 119 | 120 | seatIndex += 1; 121 | }); 122 | })(); 123 | 124 | return { 125 | seats, 126 | rowWidth, 127 | }; 128 | }; 129 | 130 | const { seats, rowWidth } = makeParliament( 131 | data, 132 | isMobile ? config.mobileWidth : config.width, 133 | isMobile ? config.mobileHeight : config.height, 134 | config.innerRadiusCoef 135 | ); 136 | const seatRadius = d => { 137 | let r = 0.4 * rowWidth; 138 | if (d.data && typeof d.data.size === 'number') { 139 | r *= d.data.size; 140 | } 141 | return r; 142 | }; 143 | const svg = d3 144 | .select('#chart') 145 | .at({ 146 | width: isMobile ? config.mobileWidth : config.width, 147 | height: isMobile ? config.mobileHeight : config.height, 148 | }) 149 | .st({ 150 | backgroundColor: '#f8f7f3', 151 | }); 152 | 153 | svg 154 | .append('g') 155 | .translate( 156 | isMobile 157 | ? [config.mobileWidth / 2, config.mobileHeight / 2 + 80] 158 | : [config.width / 2, config.height / 2 + 150] 159 | ) 160 | .selectAll('.seat') 161 | .data(seats) 162 | .enter() 163 | .append('circle') 164 | .at({ 165 | class: d => `seat ${d.party.id}`, 166 | cx: d => d.cartesian.x, 167 | cy: d => d.cartesian.y, 168 | fill: d => d.party.color, 169 | r: seatRadius, 170 | }); 171 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | The Times | data viz catalogue 13 | 14 | 18 | 22 | 26 | 30 | 31 | 32 | 33 | 55 |
56 |

57 | Data visualisation catalogue 58 | Star 67 |

68 |

A public resource of data viz code and designs

69 | 70 |
71 |
72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /index.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "Scatterplot (d3 + R)", 4 | "img": "scatterplot/1.png", 5 | "url" : "scatterplot/" 6 | }, 7 | { 8 | "title": "Treemap (d3)", 9 | "img": "treemap/1.png", 10 | "url" : "treemap/" 11 | }, 12 | { 13 | "title": "Vertical stepped line (d3)", 14 | "img": "vertical-stepped-line/1.png", 15 | "url" : "vertical-stepped-line/" 16 | }, 17 | { 18 | "title": "Small multiples (d3)", 19 | "img": "small-multiples/1.png", 20 | "url" : "small-multiples/" 21 | }, 22 | { 23 | "title": "Hemicycle (d3)", 24 | "img": "hemicycle/1.png", 25 | "url" : "hemicycle/" 26 | }, 27 | { 28 | "title": "Timelines bubbles (d3)", 29 | "img": "timeline-bubbles/1.png", 30 | "url" : "timeline-bubbles/" 31 | }, 32 | { 33 | "title": "Waffle (d3)", 34 | "img": "waffle/1.png", 35 | "url" : "waffle/" 36 | }, 37 | { 38 | "title": "Joyplot (d3)", 39 | "img": "joyplot/1.png", 40 | "url" : "joyplot/" 41 | } 42 | 43 | ] 44 | -------------------------------------------------------------------------------- /joyplot/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/times/dataviz-catalogue/fadceb03044acd37da4676a342a719fce8f274d7/joyplot/1.png -------------------------------------------------------------------------------- /joyplot/D3/script.js: -------------------------------------------------------------------------------- 1 | // set config object 2 | const config = { 3 | width: 700, 4 | height: 800, 5 | mobileWidth: 300, 6 | mobileHeight: 800, 7 | ridgeHeight: 18, 8 | formatTime: d3.timeFormat('%Y'), 9 | }; 10 | const isMobile = window.innerWidth < 600 ? true : false; 11 | 12 | const margin = { top: 30, right: 30, bottom: 50, left: 50 }, 13 | width = 14 | (isMobile ? config.mobileWidth : config.width) - margin.left - margin.right, 15 | height = 16 | (isMobile ? config.mobileHeight : config.height) - 17 | margin.top - 18 | margin.bottom; 19 | 20 | // Y scale in each ridge 21 | const y = d => d.n; 22 | const yScale = d3.scaleLinear(); 23 | const yValue = d => yScale(y(d)); 24 | 25 | // Sections scale 26 | const section = d => d.key; 27 | const sectionScale = d3.scaleOrdinal().range([0, 260, 390, 580, 740]); 28 | const sectionValue = d => sectionScale(section(d)); 29 | const sectionAxis = d3.axisLeft(sectionScale); 30 | 31 | // Main container y scale 32 | const termScale = d3.scaleBand().range([0, 10]); 33 | 34 | // from our dataset coming out of a require(), 35 | // parse strings into dates, ints, etc. 36 | // nest groups by key, and it's just great 37 | const formatData = function(dataset) { 38 | const parseTime = d3.timeParse('%Y-%m-%d'); 39 | dataset.map(d => { 40 | return (d['time'] = parseTime(d.week)), (d['n'] = parseInt(d.n)); 41 | }); 42 | const groups = d3 43 | .nest() 44 | .key(d => d.category_label) 45 | .key(d => d.term_label) 46 | .entries(dataset); 47 | 48 | // sort, etc 49 | return groups; 50 | }; 51 | 52 | const setupXScale = function(config) { 53 | // Date scale 54 | const x = d => d.time; 55 | const xScale = d3.scaleTime().range([30, width]); 56 | const xValue = d => xScale(x(d)); 57 | const xAxis = d3 58 | .axisBottom(xScale) 59 | .tickFormat(config.formatTime) 60 | .ticks(4); 61 | 62 | return { x, xScale, xValue, xAxis }; 63 | }; 64 | 65 | const makeSection = (config, xValue) => { 66 | return function(data) { 67 | const area = d3 68 | .area() 69 | .defined(d => d) 70 | .x(xValue) 71 | .y1(yValue) 72 | .curve(d3.curveBasisOpen); 73 | const line = area.lineY1(); 74 | 75 | termScale.domain(data.values.map(d => d.key)); 76 | //const areaChartHeight = config.ridgeHeight; 77 | const areaChartHeight = 1.5 * (100 / termScale.domain().length); 78 | yScale.domain([0, 10]).range([areaChartHeight, 0]); 79 | area.y0(yScale(0)); 80 | 81 | let ridges = d3 82 | .select(this) 83 | .append('g') 84 | .selectAll('.foo') 85 | .data(d => d.values) 86 | .enter() 87 | .append('g') 88 | .at({ class: d => 'term term--' + d.key }) 89 | .translate((d, i) => { 90 | return [0, i * config.ridgeHeight]; 91 | }); 92 | 93 | ridges 94 | .append('text') 95 | .data(d => d.values) 96 | .text(d => d.key) 97 | .st({ textAnchor: 'end' }) 98 | .translate(config.width < 500 ? [-20, yScale(0) - 5] : [20, yScale(0)]); 99 | 100 | ridges 101 | .append('path') 102 | .at({ class: 'area' }) 103 | .datum(d => d.values) 104 | .at({ d: area }); 105 | 106 | ridges 107 | .append('path') 108 | .at({ class: 'line' }) 109 | .datum(d => d.values) 110 | .at({ d: line }); 111 | }; 112 | }; 113 | 114 | // Clean up before drawing 115 | // By brutally emptying all HTML from plot container div 116 | d3.select('#times-joyplot').html(''); 117 | 118 | const svg = d3 119 | .select('#times-joyplot') 120 | .at({ 121 | width: isMobile ? config.mobileWidth : config.width, 122 | height: isMobile ? config.mobileHeight : config.height, 123 | }) 124 | .st({ backgroundColor: '#F8F7F1' }) 125 | .append('g') 126 | .translate([margin.left, margin.top]); 127 | 128 | // x axis 129 | const { x, xScale, xValue, xAxis } = setupXScale(config); 130 | xScale.domain([new Date('2014-01-01'), new Date()]); 131 | svg 132 | .append('g') 133 | .at({ class: 'axis axis--x' }) 134 | .translate([0, height]) 135 | .call(xAxis); 136 | 137 | d3.json('./data/data.json', (error, dataset) => { 138 | if (error) throw error; 139 | 140 | const data = formatData(dataset); 141 | // console.log(data); 142 | 143 | sectionScale.domain(data.map(d => d.key)); 144 | const sections = svg 145 | .selectAll('.section') 146 | .data(data) 147 | .enter() 148 | .append('g') 149 | .at({ 150 | class: d => 151 | 'section section--' + 152 | d.key 153 | .split(' ') 154 | .join('') 155 | .toLowerCase(), 156 | }) 157 | .translate(d => [0, sectionValue(d)]); 158 | svg 159 | .append('g') 160 | .at({ class: 'axis axis--term' }) 161 | .st({ textAnchor: 'start' }) 162 | .translate([margin.left + margin.right, -10]) 163 | .call(sectionAxis); 164 | 165 | sections.each(makeSection(config, xValue)); 166 | 167 | const campaignDates = [new Date('2015-06-15'), new Date('2016-11-08')]; 168 | svg.append('rect').at({ 169 | x: xScale(campaignDates[0]), 170 | y: 0, 171 | width: xScale(campaignDates[1]) - xScale(campaignDates[0]), 172 | height: config.height - margin.top - margin.bottom - 10, 173 | class: 'campaign', 174 | }); 175 | }); 176 | -------------------------------------------------------------------------------- /joyplot/D3/style.css: -------------------------------------------------------------------------------- 1 | svg { 2 | margin: 0 auto; 3 | padding: 15px; 4 | background-color: #f7f8f1; 5 | display: flex; 6 | overflow: visible; 7 | } 8 | 9 | text, .text, .source { 10 | font: 10px GillSansMTStd-Medium, GillSansW01-Medium;; 11 | fill: #696969; 12 | color: #696969; 13 | } 14 | 15 | .axis--term text { 16 | font-family: TimesModern-Bold; 17 | fill: #1a1a1a; 18 | font-size: 2rem; 19 | text-anchor: end; 20 | 21 | @media only screen and (max-width: 767px) { 22 | text-anchor: middle; 23 | } 24 | } 25 | 26 | .term text { 27 | @media only screen and (max-width: 767px) { 28 | text-anchor: start !important; 29 | } 30 | } 31 | 32 | .axis .domain { 33 | display: none; 34 | } 35 | 36 | .axis--x text { 37 | fill: #999; 38 | } 39 | 40 | .axis--term .tick line { 41 | display: none; 42 | } 43 | 44 | .line { 45 | fill: none; 46 | stroke: #ddd; 47 | } 48 | 49 | .tick line { 50 | stroke-width: 1px; 51 | stroke: #ddd; 52 | stroke-dasharray: 5 5; 53 | } 54 | 55 | .area { 56 | //fill: #254251; 57 | opacity: .95; 58 | } 59 | 60 | .campaign { 61 | opacity: .05; 62 | color: #eee; 63 | } 64 | 65 | .section--people { 66 | fill: #254251; 67 | } 68 | .section--phrases { 69 | fill: #e0ab26; 70 | } 71 | .section--policy { 72 | fill: #80b1e2; 73 | } 74 | .section--nicknames { 75 | fill: #f37f2f; 76 | } 77 | .section--themedia { 78 | fill: #3292a6; 79 | } 80 | -------------------------------------------------------------------------------- /joyplot/README.md: -------------------------------------------------------------------------------- 1 | # Joyplot chart 2 | 3 | ![](1.png) 4 | 5 | An alternative to small multiple to present timeseries of frequencies. 6 | 7 | # Data format 8 | 9 | An array of objects containing, at least, a date, a frequency, and a label: 10 | 11 | ``` 12 | [ 13 | {"week":"2013-12-15","n":"10","label":"James Comey"}, 14 | {"week":"2013-12-22","n":"15","label":"James Comey"}, 15 | {"week":"2013-12-15","n":"1","label":"Hillary Clinton"}, 16 | {"week":"2013-12-22","n":"5","label":"Hillary Clinton"}, 17 | ] 18 | ``` 19 | 20 | ## Download and edit 21 | 22 | Install the [SVG Crowbar](http://nytimes.github.io/svg-crowbar/) by dragging the bookmarklet on this page to your bookmarks bar. Click the bookmarklet to download an Illustrator-ready SVG. 23 | -------------------------------------------------------------------------------- /joyplot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 21 | 22 |
23 |
Joyplot
24 |
Who and what Mr Trump tweets about
25 | 26 |
27 |

Technically a timeseries à la joyplot, this chart plots the frequency of data points over time, and provides an alternative layout to small multiples. One of the star of our Trump Vocabulary piece.

28 |
29 | 30 | 33 | 34 | 35 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /page.css: -------------------------------------------------------------------------------- 1 | .Standfirst { 2 | color: #4a4a4a; 3 | font-family: "TimesDigital-Regular"; 4 | font-size: 18px; 5 | line-height: 26px; 6 | margin-top: 5px; 7 | } 8 | 9 | svg { 10 | margin: 0 auto; 11 | padding: 0px; 12 | background-color: #f7f8f1; 13 | display: flex; 14 | } 15 | 16 | .links { 17 | margin-top: 3rem; 18 | font-size: 1.5rem; 19 | } 20 | ul { 21 | list-style: none; 22 | } 23 | li::before { 24 | content: "♥ "; 25 | position: absolute; 26 | left: -10px; 27 | } 28 | li { 29 | position: relative; 30 | } 31 | .Article-content { 32 | margin-bottom: 2rem; 33 | } 34 | .Article-content p { 35 | font-family: "TimesModern-Regular"; 36 | font-size: 1.2em; 37 | color: #333; 38 | } 39 | a { 40 | text-decoration: underline; 41 | font-family: "TimesModern-Regular" !important; 42 | } 43 | 44 | .Byline > a:hover { 45 | color: #ddd; 46 | } 47 | 48 | .Byline a { 49 | text-decoration: none; 50 | } 51 | 52 | .Byline .Icon { 53 | transform: translate(-3px, -2px); 54 | } 55 | 56 | @media screen and (max-width: 767px) { 57 | .sidebar { 58 | height: 9rem; 59 | } 60 | .wrapper { 61 | margin-bottom: 3rem; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /palettes/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 21 | 22 |
23 |
Times colour palettes
24 |
Click on a rectangle to copy the colour's hex code to your clipboard
25 | 26 |
27 |
28 | 29 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /palettes/palettes.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Times Digital main colours", 4 | "colours": [ 5 | { "code": "#254251", "label": "" }, 6 | { "code": "#e0ab26", "label": "" }, 7 | { "code": "#80b1e2", "label": "" }, 8 | { "code": "#f37f2f", "label": "" }, 9 | { "code": "#3292a6", "label": "" }, 10 | { "code": "#6c3c5e", "label": "" }, 11 | { "code": "#dacfc1", "label": "" }, 12 | { "code": "#96807a", "label": "" } 13 | ] 14 | }, 15 | { 16 | "name": "UK political parties", 17 | "colours": [ 18 | { "code": "#4093B2", "label": "con" }, 19 | { "code": "#EC5156", "label": "lab" }, 20 | { "code": "#EAAA00", "label": "libdem" }, 21 | { "code": "#71E2DA", "label": "brexit" }, 22 | { "code": "#9767AE", "label": "ukip" }, 23 | { "code": "#F6D700", "label": "snp" }, 24 | { "code": "#61A961", "label": "green" }, 25 | { "code": "#90CD7C", "label": "pc" }, 26 | { "code": "#A15252", "label": "dup" }, 27 | { "code": "#44966B", "label": "sf" }, 28 | { "code": "#7DA17D", "label": "sdlp" }, 29 | { "code": "#3F617C", "label": "uup" }, 30 | { "code": "#A0938F", "label": "others" } 31 | ] 32 | }, 33 | { 34 | "name": "German main political parties", 35 | "colours": [ 36 | { "code": "#000", "label": "cdu/csu" }, 37 | { "code": "#EB001F", "label": "spd" }, 38 | { "code": "#8C3473", "label": "left" }, 39 | { "code": "#58AB27", "label": "greens" }, 40 | { "code": "#F0D034", "label": "fdp" }, 41 | { "code": "grey", "label": "ind" }, 42 | { "code": "#4990E2", "label": "afd" } 43 | ] 44 | }, 45 | { 46 | "name": "Italy main political parties", 47 | "colours": [ 48 | { "label": "dp", "code": "#E45B5B" }, 49 | { "label": "5sm", "code": "#F3D92B" }, 50 | { "label": "forza", "code": "#4894D2" }, 51 | { "label": "nleague", "code": "#65BDA2" }, 52 | { "label": "LeU", "code": "#700000" }, 53 | { "label": "brothers", "code": "#1d24bf" } 54 | ] 55 | }, 56 | { 57 | "name": "US main political parties", 58 | "colours": [ 59 | { "code": "#015b90", "label": "D" }, 60 | { "code": "#CC0B07", "label": "R" } 61 | ] 62 | }, 63 | { 64 | "name": "Sequential blue", 65 | "colours": [ 66 | { "code": "#074467", "label": "" }, 67 | { "code": "#15618a", "label": "" }, 68 | { "code": "#2473a3", "label": "" }, 69 | { "code": "#528ab7", "label": "" }, 70 | { "code": "#74a9cf", "label": "" }, 71 | { "code": "#91b9d5", "label": "" }, 72 | { "code": "#aecce2", "label": "" }, 73 | { "code": "#c1d7ea", "label": "" }, 74 | { "code": "#e0e7f1", "label": "" }, 75 | { "code": "#ebeff5", "label": "" } 76 | ] 77 | }, 78 | { 79 | "name": "Bivariate", 80 | "colours": [ 81 | { "code": "#005415", "label": "" }, 82 | { "code": "#7C8E1D", "label": "" }, 83 | { "code": "#00594F", "label": "" }, 84 | { "code": "#E0CD24", "label": "" }, 85 | { "code": "#78966E", "label": "" }, 86 | { "code": "#016389", "label": "" }, 87 | { "code": "#DADA8B", "label": "" }, 88 | { "code": "#86A7BF", "label": "" }, 89 | { "code": "#E1E4E5", "label": "" } 90 | ] 91 | }, 92 | { 93 | "name": "Section colours", 94 | "colours": [ 95 | { "code": "#13354e", "label": "news" }, 96 | { "code": "#850029", "label": "comment" }, 97 | { "code": "#636c17", "label": "world" }, 98 | { "code": "#005b8d", "label": "business" }, 99 | { "code": "#bc3385", "label": "style" }, 100 | { "code": "#6c6c69", "label": "register" }, 101 | { "code": "#008347", "label": "sport" }, 102 | { "code": "#c74600", "label": "puzzles" }, 103 | { "code": "#622956", "label": "t2" }, 104 | { "code": "#006469", "label": "the game" }, 105 | { "code": "#40001c", "label": "bricks" }, 106 | { "code": "#a31d24", "label": "sat review" }, 107 | { "code": "#05829a", "label": "weekend" }, 108 | { "code": "#691d26", "label": "law" } 109 | ] 110 | }, 111 | { 112 | "name": "Good to bad", 113 | "colours": [ 114 | { "code": "#2AA818", "label": "Good" }, 115 | { "code": "#6BA519", "label": "" }, 116 | { "code": "#99A31A", "label": "" }, 117 | { "code": "#AFA41C", "label": "" }, 118 | { "code": "#B79730", "label": "" }, 119 | { "code": "#AA691A", "label": "" }, 120 | { "code": "#A8461B", "label": "" }, 121 | { "code": "#AF1722", "label": "Bad" } 122 | ] 123 | } 124 | ] 125 | -------------------------------------------------------------------------------- /palettes/script.js: -------------------------------------------------------------------------------- 1 | // set config object 2 | const config = { width: 700, height: 1400, blockWidth: 50 }; 3 | 4 | const margin = { top: 50, right: 40, bottom: 50, left: 60 }, 5 | width = config.width - margin.left - margin.right, 6 | height = config.height - margin.top - margin.bottom; 7 | 8 | // Clean up before drawing 9 | // By brutally emptying all HTML from plot container div 10 | d3.select("#times-palettes").html(""); 11 | 12 | const svg = d3 13 | .select("#times-palettes") 14 | .attr("width", config.width) 15 | .attr("height", config.height); 16 | 17 | function onHover(duration, type) { 18 | return function() { 19 | if (type === "mouseover") { 20 | d3.select(this) 21 | .transition() 22 | .duration(duration) 23 | .styleTween("opacity", () => d3.interpolate(1, 0.5)) 24 | .attrTween("width", () => 25 | d3.interpolate(config.blockWidth, config.blockWidth + 2) 26 | ) 27 | .attrTween("height", () => 28 | d3.interpolate(config.blockWidth, config.blockWidth + 2) 29 | ); 30 | } else if (type === "mouseout") { 31 | d3.select(this) 32 | .transition() 33 | .duration(duration) 34 | .styleTween("opacity", () => d3.interpolate(0.5, 1)) 35 | .attrTween("width", () => 36 | d3.interpolate(config.blockWidth + 2, config.blockWidth) 37 | ) 38 | .attrTween("height", () => 39 | d3.interpolate(config.blockWidth + 2, config.blockWidth) 40 | ) 41 | .attr("rx", 2) 42 | .attr("ry", 2); 43 | } else if (type === "click") { 44 | d3.select(this) 45 | .transition() 46 | .duration(duration) 47 | .style("opacity", 0.2) 48 | .transition() 49 | .duration(200) 50 | .style("opacity", 0.6); 51 | } 52 | }; 53 | } 54 | 55 | const makePalette = container => { 56 | const colour = container 57 | .selectAll("rect") 58 | .data(d => d.colours) 59 | .enter() 60 | .append("g"); 61 | 62 | colour 63 | .append("rect") 64 | .attr("x", (d, i) => i * 60) 65 | .attr("y", 10) 66 | .attr("rx", 2) 67 | .attr("ry", 2) 68 | .attr("width", config.blockWidth) 69 | .attr("height", config.blockWidth) 70 | .attr("class", "rect") 71 | .attr("dataClipboardText", d => d.code) 72 | .attr("transform", "translate(0,20)") 73 | // .translate([0, 20]) 74 | .style("fill", d => d.code) 75 | .on("mouseover", onHover(100, "mouseover")) 76 | .on("mouseout", onHover(200, "mouseout")) 77 | .on("click", onHover(20, "click")); 78 | 79 | colour 80 | .append("text") 81 | .attr("x", (d, i) => i * 60) 82 | .attr("y", 0) 83 | .attr("class", "label") 84 | .text(d => d.code.toString()) 85 | .attr("transform", `translate(0,${config.blockWidth * 2 - 5})`); 86 | // .translate([0, config.blockWidth * 2 - 5]); 87 | 88 | colour 89 | .append("text") 90 | .attr("x", (d, i) => i * 60) 91 | .attr("y", 13) 92 | .attr("class", "label name") 93 | .text(d => d.label) 94 | .attr("transform", `translate(0,${config.blockWidth * 2 - 5})`); 95 | // .translate([0, config.blockWidth * 2 - 5]); 96 | }; 97 | 98 | d3.json("palettes.json", (error, data) => { 99 | if (error) throw error; 100 | 101 | const palette = svg 102 | .selectAll("g") 103 | .data(data) 104 | .enter() 105 | .append("g") 106 | .attr("transform", (d, i) => `translate(10, ${i * 150 + 50})`); 107 | // .translate((d, i) => [10, i * 150 + 50]); 108 | 109 | palette 110 | .append("text") 111 | .attr("x", 0) 112 | .attr("y", 10) 113 | .attr("class", "Headline paletteTitle") 114 | .text(d => d.name); 115 | 116 | palette.call(makePalette(palette)); 117 | }); 118 | 119 | const clipboard = new Clipboard(".rect"); 120 | -------------------------------------------------------------------------------- /palettes/style.css: -------------------------------------------------------------------------------- 1 | svg { 2 | background-color: #fff; 3 | width: 100%; 4 | } 5 | 6 | .paletteTitle { 7 | font-size: 2.3em; 8 | } 9 | .label { 10 | font-family: "GillSansW01-Medium"; 11 | font-size: .9em; 12 | fill: #898989; 13 | } 14 | .name { 15 | font-weight: 400; 16 | fill: black; 17 | } 18 | -------------------------------------------------------------------------------- /scatterplot/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/times/dataviz-catalogue/fadceb03044acd37da4676a342a719fce8f274d7/scatterplot/1.png -------------------------------------------------------------------------------- /scatterplot/D3/script.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Annotation layer 3 | * DO uncomment further below 4 | * draggable(true) 5 | * @TODO: commenting out `path` for now, 6 | * difficult to make it work with mobile layout 7 | */ 8 | const annotations = [ 9 | { 10 | Fee: '53', 11 | Age: '25', 12 | //path: 'M51,-1L7,5', 13 | text: 'Oscar', 14 | textOffset: [0, -8], 15 | }, 16 | { 17 | Fee: '27', 18 | Age: '27', 19 | //path: 'M0,-56L0,-26', 20 | text: 'Morgan Schneiderlin', 21 | textOffset: [-80, 0], 22 | }, 23 | ]; 24 | 25 | // set config object 26 | const config = { width: 600, height: 450, mobileWidth: 300, mobileHeight: 300 }; 27 | const isMobile = window.innerWidth < 600 ? true : false; 28 | 29 | const margin = { top: 50, right: 40, bottom: 50, left: 60 }, 30 | width = 31 | (isMobile ? config.mobileWidth : config.width) - margin.left - margin.right, 32 | height = 33 | (isMobile ? config.mobileHeight : config.height) - 34 | margin.top - 35 | margin.bottom; 36 | 37 | // Clean up before drawing 38 | // By brutally emptying all HTML from plot container div 39 | d3.select('#times-scatterplot').html(''); 40 | 41 | const svg = d3.select('#times-scatterplot').at({ 42 | width: isMobile ? config.mobileWidth : config.width, 43 | height: isMobile ? config.mobileHeight : config.height, 44 | }); 45 | 46 | /* 47 | * Scales 48 | * Both scales run full height and full width 49 | * and are linear 50 | * More about scales: https://github.com/d3/d3/blob/master/API.md#scales-d3-scale 51 | */ 52 | const x = d3.scaleLinear().range([0, width]); 53 | const y = d3.scaleLinear().range([height, 0]); 54 | 55 | // g is our main container 56 | const g = svg.append('g').translate([margin.left + 20, 0]); 57 | 58 | d3.json('data.json', (err, dataset) => { 59 | if (err) throw err; 60 | /* 61 | * Constrain variables to numbers 62 | */ 63 | dataset.forEach(function(d) { 64 | d.Age = +d.Age; 65 | d.Fee = +d.Fee; 66 | }); 67 | 68 | /* 69 | * d3.extent should return a [min,max] array 70 | * We're hard-coding the y-axis extent in this case 71 | */ 72 | const xExtent = d3.extent(dataset, d => d.Age); 73 | //var yExtent = d3.extent(dataset, function(d) { return d.Fee; }); 74 | const yExtent = d3.extent([0, 65]); 75 | 76 | x.domain(xExtent); 77 | y.domain(yExtent); 78 | 79 | // X-axis 80 | g 81 | .append('g') 82 | .at({ 83 | class: 'axis axis--x', 84 | }) 85 | .translate([0, height]) 86 | .call( 87 | d3 88 | .axisBottom(x) 89 | .ticks(isMobile ? 5 : 10) 90 | .tickSizeInner(0) 91 | .tickPadding(20) 92 | ); 93 | 94 | // text label for the x axis 95 | g 96 | .append('text') 97 | .attr('class', 'label') 98 | .at({ class: 'label' }) 99 | .translate([width / 2, height + margin.top]) 100 | .style('text-anchor', 'middle') 101 | .text('Age'); 102 | 103 | // Y-axis 104 | g 105 | .append('g') 106 | .at({ class: 'axis axis--y' }) 107 | .call( 108 | d3 109 | .axisLeft(y) 110 | .ticks(5) 111 | .tickSize(-width) 112 | .tickPadding(20) 113 | .tickFormat(d => d + 'm') 114 | ); 115 | 116 | // text label for the y axis 117 | g 118 | .append('text') 119 | .at({ 120 | class: 'label', 121 | transform: 'rotate(-90)', 122 | y: 0 - margin.left, 123 | x: 0 - height / 2, 124 | dy: '0em', 125 | }) 126 | .style('text-anchor', 'middle') 127 | .text('Fee (£)'); 128 | 129 | // Add the scatterplot 130 | g 131 | .selectAll('dot') 132 | .data(dataset) 133 | .at({ class: 'dots' }) 134 | .enter() 135 | .append('circle') 136 | .at({ 137 | r: 5, 138 | cx: d => x(d.Age), 139 | cy: d => y(d.Fee), 140 | }) 141 | .style('fill', '#254251'); 142 | 143 | // Annotations 144 | const swoopy = d3 145 | .swoopyDrag() 146 | .x(d => x(d.Age)) 147 | .y(d => y(d.Fee)) 148 | //.draggable(true) 149 | .annotations(annotations); 150 | 151 | const swoopySel = g 152 | .append('g') 153 | .attr('class', 'annotations') 154 | .call(swoopy); 155 | 156 | // SVG arrow marker fix 157 | // Do not change 158 | svg 159 | .append('marker') 160 | .attr('id', 'arrow') 161 | .attr('viewBox', '-10 -10 20 20') 162 | .attr('markerWidth', 6) 163 | .attr('markerHeight', 6) 164 | .attr('orient', 'auto') 165 | .append('circle') 166 | .attr('r', '6') 167 | .style('fill', '#F37F2F'); 168 | swoopySel.selectAll('path').attr('marker-start', 'url(#arrow)'); 169 | }); 170 | -------------------------------------------------------------------------------- /scatterplot/D3/style.css: -------------------------------------------------------------------------------- 1 | svg { 2 | margin: 0 auto; 3 | padding: 25px; 4 | background-color: #f8f7f1; 5 | display: flex; 6 | } 7 | 8 | .axis, .axis text { 9 | font: 15px arial; 10 | color: #666; 11 | fill: #666; 12 | } 13 | .axis path, 14 | .axis line { 15 | fill: none; 16 | stroke: lightgrey; 17 | stroke-width: 1px; 18 | shape-rendering: crispEdges; 19 | stroke-dasharray: 5; 20 | } 21 | .axis path { 22 | display: none; 23 | } 24 | 25 | .line { 26 | fill: none; 27 | stroke: #254251; 28 | stroke-width: 3px; 29 | } 30 | 31 | .label { 32 | background-color: #fff; 33 | font: 15px arial; 34 | fill: #666; 35 | } 36 | 37 | .annotations { 38 | font: 15px arial; 39 | fill: #F37F2F; 40 | } 41 | 42 | path { 43 | stroke: #F37F2F; 44 | stroke-width: 2px; 45 | } 46 | 47 | .zero { 48 | stroke: lightgrey; 49 | stroke-width: 1px; 50 | } 51 | -------------------------------------------------------------------------------- /scatterplot/R/scatterplot-r.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | Oscar 58 | Morgan Schneiderlin 59 | 60 | 61 | 62 | 63 | 64 | 0 65 | 10 66 | 20 67 | 30 68 | 40 69 | 50 70 | 60 71 | 20 72 | 21 73 | 22 74 | 23 75 | 24 76 | 25 77 | 26 78 | 27 79 | 28 80 | 29 81 | 30 82 | 31 83 | 32 84 | 33 85 | Age 86 | Fee (m£) 87 | 88 | -------------------------------------------------------------------------------- /scatterplot/R/script.R: -------------------------------------------------------------------------------- 1 | # Load following libraries 2 | library(ggplot2) 3 | library(readr) 4 | 5 | # Load CSV file containing data 6 | # Note: the path to the file must be correct! 7 | # It can be a local file or a URL 8 | data <- read_csv("../data.csv") 9 | 10 | # Replace Age and Fee in the example by the variables you want 11 | # on x and y in your plot 12 | ggplot(data = data, aes(x = Age, 13 | y = Fee)) + 14 | 15 | # Fill colour defaults to Times blue 16 | geom_point(stat = "identity", 17 | aes(size = 2, fill = "#254251")) + 18 | 19 | # Name and label of the y axis 20 | # Limits sets up to and from where runs our axis 21 | # Breaks sets the tick placements 22 | scale_y_continuous(name = "Fee (m£)", 23 | limits = c(0,60), 24 | breaks = seq(0,60,10)) + 25 | 26 | # Breaks sets the tick placements 27 | # on our x axis 28 | scale_x_continuous(breaks = seq(20,33,1)) + 29 | 30 | # Annotation layer 31 | # Use x/y coordinates similar to the one you'd use 32 | # if you were placing a point on the plot 33 | annotate("text", 34 | x = 25.8, y = 52, 35 | label = "Oscar", color="#F37F2F") + 36 | annotate("text", 37 | x = 25.2, y = 24.2, 38 | label = "Morgan Schneiderlin", color="#F37F2F") + 39 | 40 | # Theming 41 | theme( 42 | plot.background = element_rect(fill = "#f7f8f1"), # on buff 43 | panel.background = element_rect(fill = "#f7f8f1"), # on buff 44 | panel.grid.minor = element_blank(), # no small lines 45 | panel.grid.major = element_line(colour = "lightgrey", linetype = "dashed"), 46 | panel.grid.minor.x = element_blank(), # no x axis rules 47 | panel.grid.major.x = element_blank(), # no x axis rules 48 | legend.position = "none", # no legend 49 | axis.ticks = element_blank(), # no ticks 50 | plot.margin = unit(c(1,1,1,1), "cm") 51 | ) + 52 | 53 | # Export to SVG 54 | ggsave(filename = "scatterplot-r.svg") 55 | 56 | -------------------------------------------------------------------------------- /scatterplot/README.md: -------------------------------------------------------------------------------- 1 | # Scatterplot 2 | 3 | ![](1.png) 4 | 5 | A simple bubble scatterplot. 6 | 7 | ## Data format 8 | 9 | #### for d3 10 | 11 | An array of objects containing two key/value pairs for the x-axis and y-axis. 12 | 13 | ``` 14 | [{ 15 | "Age": "22", 16 | "Fee": "12" 17 | },{ 18 | "Age": "23", 19 | "Fee": "24" 20 | }, 21 | (...) 22 | ] 23 | ``` 24 | 25 | #### for R 26 | 27 | A CSV file containing at least two variables (columns). 28 | 29 | ``` 30 | Name,Fee,Age 31 | Glenn Murray,3,33 32 | Lukas Jutkiewicz,1,27 33 | Danny Lafferty,0.43,27 34 | ``` 35 | 36 | ## Annotations 37 | 38 | At the top of `script.js`, an array of objects contains the annotation layer. 39 | 40 | Un-comment line 119 (`draggable(true)`) and refresh to manually drag and edit your annotations. Once you're happy, run `copy(annotations)` in the console and paste into `annotations` at the top of `script.js`. 41 | 42 | Annotations are added manually in R. 43 | 44 | ## Download and edit 45 | 46 | Install the [SVG Crowbar](http://nytimes.github.io/svg-crowbar/) by dragging the bookmarklet on this page to your bookmarks bar. Click the bookmarklet to download an Illustrator-ready SVG. 47 | -------------------------------------------------------------------------------- /scatterplot/data.csv: -------------------------------------------------------------------------------- 1 | Name,Fee,Age 2 | Glenn Murray,3,33 3 | Lukas Jutkiewicz,1,27 4 | Danny Lafferty,0.43,27 5 | Oscar,52,25 6 | John Obi Mikel,9.5,29 7 | Patrick Bamford,10,23 8 | Darron Gibson,3.75,29 9 | Bryan Oviedo,3.75,26 10 | Jake Livermore,10,27 11 | Robert Snodgrass,10.2,29 12 | Jeffrey Schlupp,12.5,24 13 | Luis Hernandez,1.7,27 14 | Joe Maguire,0.05,21 15 | Tiago Ilori,3.75,23 16 | George Glendon,0.09,21 17 | Ian Lawlor,0.09,22 18 | Morgan Schneiderlin,24,27 19 | Memphis Depay,21.7,22 20 | Sean Goss,0.5,21 21 | Jordan Rhodes,10,26 22 | Emilio Nsue,1.02,27 23 | José Fonte,8,33 24 | Patrick v. Aanholt,14,26 25 | Tom Carroll,4.5,24 26 | Odion Ighalo,19.8,27 27 | Saido Berahino,15,23 28 | Dimitri Payet,25,29 29 | Lewis Page,0.09,20 30 | -------------------------------------------------------------------------------- /scatterplot/data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Player Name": "Glenn Murray", 4 | "Fee": 3, 5 | "Age": 33 6 | }, 7 | { 8 | "Player Name": "Lukas Jutkiewicz", 9 | "Fee": 1, 10 | "Age": 27 11 | }, 12 | { 13 | "Player Name": "Danny Lafferty", 14 | "Fee": 0.43, 15 | "Age": 27 16 | }, 17 | { 18 | "Player Name": "Oscar", 19 | "Fee": 52, 20 | "Age": 25 21 | }, 22 | { 23 | "Player Name": "John Obi Mikel", 24 | "Fee": 9.5, 25 | "Age": 29 26 | }, 27 | { 28 | "Player Name": "Patrick Bamford", 29 | "Fee": 10, 30 | "Age": 23 31 | }, 32 | { 33 | "Player Name": "Darron Gibson", 34 | "Fee": 3.75, 35 | "Age": 29 36 | }, 37 | { 38 | "Player Name": "Bryan Oviedo", 39 | "Fee": 3.75, 40 | "Age": 26 41 | }, 42 | { 43 | "Player Name": "Jake Livermore", 44 | "Fee": 10, 45 | "Age": 27 46 | }, 47 | { 48 | "Player Name": "Robert Snodgrass", 49 | "Fee": 10.2, 50 | "Age": 29 51 | }, 52 | { 53 | "Player Name": "Jeffrey Schlupp", 54 | "Fee": 12.5, 55 | "Age": 24 56 | }, 57 | { 58 | "Player Name": "Luis Hernandez", 59 | "Fee": 1.7, 60 | "Age": 27 61 | }, 62 | { 63 | "Player Name": "Joe Maguire", 64 | "Fee": 0.05, 65 | "Age": 21 66 | }, 67 | { 68 | "Player Name": "Tiago Ilori", 69 | "Fee": 3.75, 70 | "Age": 23 71 | }, 72 | { 73 | "Player Name": "George Glendon", 74 | "Fee": 0.09, 75 | "Age": 21 76 | }, 77 | { 78 | "Player Name": "Ian Lawlor", 79 | "Fee": 0.09, 80 | "Age": 22 81 | }, 82 | { 83 | "Player Name": "Morgan Schneiderlin", 84 | "Fee": 24, 85 | "Age": 27 86 | }, 87 | { 88 | "Player Name": "Memphis Depay", 89 | "Fee": 21.7, 90 | "Age": 22 91 | }, 92 | { 93 | "Player Name": "Sean Goss", 94 | "Fee": 0.5, 95 | "Age": 21 96 | }, 97 | { 98 | "Player Name": "Jordan Rhodes", 99 | "Fee": 10, 100 | "Age": 26 101 | }, 102 | { 103 | "Player Name": "Emilio Nsue", 104 | "Fee": 1.02, 105 | "Age": 27 106 | }, 107 | { 108 | "Player Name": "José Fonte", 109 | "Fee": 8, 110 | "Age": 33 111 | }, 112 | { 113 | "Player Name": "Patrick v. Aanholt", 114 | "Fee": 14, 115 | "Age": 26 116 | }, 117 | { 118 | "Player Name": "Tom Carroll", 119 | "Fee": 4.5, 120 | "Age": 24 121 | }, 122 | { 123 | "Player Name": "Odion Ighalo", 124 | "Fee": 19.8, 125 | "Age": 27 126 | }, 127 | { 128 | "Player Name": "Saido Berahino", 129 | "Fee": 15, 130 | "Age": 23 131 | }, 132 | { 133 | "Player Name": "Dimitri Payet", 134 | "Fee": 25, 135 | "Age": 29 136 | }, 137 | { 138 | "Player Name": "Lewis Page", 139 | "Fee": 0.09, 140 | "Age": 20 141 | } 142 | ] 143 | -------------------------------------------------------------------------------- /scatterplot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 21 | 22 |
23 |
Scatterplot
24 |
25 | 26 |
27 |
28 | 29 | 32 | 33 | 34 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /small-multiples/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/times/dataviz-catalogue/fadceb03044acd37da4676a342a719fce8f274d7/small-multiples/1.png -------------------------------------------------------------------------------- /small-multiples/README.md: -------------------------------------------------------------------------------- 1 | # Small multiples 2 | 3 | ![](1.png) 4 | 5 | "series of similar graphs or charts using the same scale and axes, allowing them to be easily compared. It uses multiple views to show different partitions of a dataset." (Wikipedia) 6 | 7 | ## Data format 8 | 9 | ``` 10 | [ 11 | { 12 | date: 2008, 13 | value: 10, 14 | facet: "foo" 15 | }, 16 | { 17 | date: 2009, 18 | value: 20, 19 | facet: "foo" 20 | }, 21 | { 22 | date: 2008, 23 | value: 1, 24 | facet: "bar" 25 | }, 26 | { 27 | date: 2009, 28 | value: 10, 29 | facet: "bar" 30 | }, 31 | ] 32 | ``` 33 | 34 | ## Download and edit 35 | 36 | Install the [SVG Crowbar](http://nytimes.github.io/svg-crowbar/) by dragging the bookmarklet on this page to your bookmarks bar. Click the bookmarklet to download an Illustrator-ready SVG. 37 | -------------------------------------------------------------------------------- /small-multiples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 21 | 22 |
23 |
Small multiple
24 |
Relative change in music genres, dummy data
25 | 26 |
27 |

Allow easy comparison of different variables by plotting them in identical charts and axes, as per our analysis of festival lines-ups.

28 |
29 | 30 | 31 | 34 |
35 | 36 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /small-multiples/script.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Sample dataset 3 | */ 4 | const dataset = []; 5 | const labels = ['rock', 'pop', 'electro', 'world', 'folk', 'hip hop']; 6 | labels.map(function(symbol) { 7 | const parseDate = d3.timeParse('%Y'); 8 | for (var i = 2008; i < 2017; i++) { 9 | dataset.push({ 10 | symbol: symbol, 11 | date: parseDate(i), 12 | price: Math.floor(Math.random() * 80) + 1, 13 | }); 14 | } 15 | }); 16 | 17 | const width = document 18 | .getElementsByClassName('container')[0] 19 | .getBoundingClientRect().width; 20 | const config = { 21 | parseDate: d3.timeParse('%Y'), 22 | chartWidth: width < 450 ? width * 0.7 : width / 3, 23 | chartHeight: 120, 24 | chartMargin: { top: 5, right: 40, bottom: 30, left: 20 }, 25 | area: d3.area().curve(d3.curveStep), 26 | line: d3.line().curve(d3.curveStep), 27 | xScale: d3.scaleTime(), 28 | yScale: d3.scaleLinear(), 29 | xAxis: d3.axisBottom().tickSizeInner(10), 30 | }; 31 | 32 | const usableWidth = 33 | config.chartWidth - config.chartMargin.left - config.chartMargin.right; 34 | const usableHeight = 35 | config.chartHeight - config.chartMargin.top - config.chartMargin.bottom; 36 | 37 | const symbols = d3 38 | .nest() 39 | .key(d => d.symbol) 40 | .entries(dataset); 41 | 42 | // this function appends an SVG and a mini chart to a div 43 | function makeSmallChart(d, i) { 44 | const { values } = d; 45 | const xScale = config.xScale 46 | .domain(d3.extent(values, d => d.date)) 47 | .range([0, usableWidth]); 48 | 49 | const yScale = config.yScale 50 | .domain(d3.extent(values, d => d.price)) 51 | .range([usableHeight, 0]); 52 | 53 | const xAxis = config.xAxis.scale(config.xScale); 54 | 55 | const line = config.line.x(d => xScale(d.date)).y(d => yScale(d.price)); 56 | const area = config.area 57 | .x(d => xScale(d.date)) 58 | .y1(d => yScale(d.price)) 59 | .y0(yScale(d3.min(values, d => d.price))); 60 | 61 | const chart = d3 62 | .select(this) 63 | .append('svg') 64 | .at({ width: config.chartWidth, height: config.chartHeight }) 65 | .append('g') 66 | .translate([config.chartMargin.left, config.chartMargin.top]); 67 | 68 | chart.append('path').at({ d: area(d.values), class: 'area' }); 69 | chart.append('path').at({ 70 | d: line(d.values), 71 | class: 'line', 72 | }); 73 | chart 74 | .append('g') 75 | .at({ class: 'x axis' }) 76 | .translate([0, usableHeight]) 77 | .call(config.xAxis.ticks(3)); 78 | } 79 | 80 | d3 81 | .select('.container') 82 | .selectAll('.chart') 83 | .data(symbols) 84 | .enter() 85 | .append('div') 86 | .attr('class', 'chart') 87 | .each(makeSmallChart); 88 | -------------------------------------------------------------------------------- /small-multiples/style.css: -------------------------------------------------------------------------------- 1 | .container { 2 | margin: 0 auto; 3 | padding: 25px; 4 | padding-left: 15%; 5 | background-color: #f8f7f1; 6 | width: 700px; 7 | height: 400px; 8 | display: flex; 9 | flex-wrap: wrap; 10 | } 11 | @media screen and (max-width: 700px) { 12 | .container {width: 300px; height: 1200px} 13 | } 14 | text { 15 | font: 10px sans-serif; 16 | color: #696969; 17 | } 18 | 19 | text.title { 20 | font: 13px; 21 | } 22 | 23 | .axis path, 24 | .axis line { 25 | fill: none; 26 | stroke: #dbdbdb; 27 | shape-rendering: crispEdges; 28 | } 29 | .axis { 30 | color: lightgrey; 31 | opacity: .5 32 | } 33 | .line { 34 | fill: none; 35 | stroke: #254151; 36 | stroke-width: 2; 37 | } 38 | .area { 39 | fill: #254251; 40 | opacity:.2; 41 | } 42 | 43 | path.domain { 44 | stroke: #dbdbdb; 45 | stroke-width: 1px; 46 | } 47 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #fff; 3 | display: flex; 4 | flex-direction: row; 5 | } 6 | 7 | .sidebar { 8 | width: 25%; 9 | background-color: #225ba0; 10 | display: flex; 11 | height: 100vh; 12 | position: fixed; 13 | flex-direction: column; 14 | padding-bottom: 5rem; 15 | } 16 | 17 | .wrapper { 18 | width: 75%; 19 | margin-left: 25%; 20 | } 21 | 22 | .sidebar { 23 | padding-left: 1.5rem; 24 | padding-right: 1.5rem; 25 | padding-top: 5rem; 26 | } 27 | 28 | .wrapper { 29 | padding-left: 4rem; 30 | padding-right: 4rem; 31 | padding-top: 5rem; 32 | } 33 | 34 | @media screen and (max-width: 767px) { 35 | body { 36 | flex-direction: column; 37 | } 38 | 39 | .sidebar, 40 | .wrapper { 41 | width: 100%; 42 | } 43 | 44 | .sidebar { 45 | height: 32rem; 46 | bottom: 0; 47 | padding-top: 0; 48 | padding-bottom: 2rem; 49 | position: relative; 50 | order: 2; 51 | } 52 | 53 | .wrapper { 54 | margin-left: 0; 55 | padding-right: 1em; 56 | padding-left: 1em; 57 | padding-top: 2em; 58 | position: relative; 59 | order: 1; 60 | } 61 | } 62 | 63 | .Headline { 64 | color: #000; 65 | margin-bottom: 0; 66 | font-family: "TimesModern-Bold"; 67 | font-size: 3.3em; 68 | line-height: 1.33; 69 | width: 100%; 70 | display: flex; 71 | align-items: center; 72 | } 73 | 74 | .Headline>span { 75 | margin: 0 0 0 auto; 76 | } 77 | 78 | @media screen and (max-width: 767px) { 79 | 80 | .Headline, 81 | .Standfirst { 82 | text-align: center; 83 | } 84 | } 85 | 86 | .Headline iframe { 87 | margin-right: 0; 88 | margin-left: auto; 89 | } 90 | 91 | .Dip { 92 | font-size: 1.8em; 93 | font-family: "TimesModern-Regular"; 94 | margin-top: 0; 95 | margin-bottom: 1em; 96 | font-size: 1.4em; 97 | } 98 | 99 | @media screen and (max-width: 767px) { 100 | .Headline { 101 | font-size: 2.8em; 102 | line-height: 1em; 103 | margin-bottom: 0.2em; 104 | } 105 | 106 | .Headline iframe { 107 | display: none; 108 | } 109 | 110 | .Dip { 111 | font-size: 1.1em; 112 | } 113 | } 114 | 115 | .Article-content p { 116 | font-size: 1.4rem; 117 | margin-bottom: 1rem; 118 | line-height: 2rem; 119 | } 120 | 121 | #container { 122 | width: 100%; 123 | display: flex; 124 | flex-direction: row; 125 | flex-wrap: wrap; 126 | margin-bottom: 2em; 127 | margin-top: 3em; 128 | } 129 | 130 | img { 131 | border-radius: 4px; 132 | margin: 0 auto; 133 | margin-top: 1em; 134 | } 135 | 136 | .item { 137 | display: flex; 138 | flex-direction: column; 139 | padding: 1em; 140 | margin-bottom: 1em; 141 | background-color: #f8f7f1; 142 | width: 300px; 143 | } 144 | 145 | @media screen and (min-width: 768px) { 146 | .item { 147 | margin-right: 1em; 148 | } 149 | } 150 | 151 | .chartTitle { 152 | font-family: "GillSansW01-Medium"; 153 | font-size: 1em; 154 | } 155 | 156 | .section { 157 | padding-top: 3em; 158 | color: #fff; 159 | } 160 | 161 | .section>p { 162 | font-family: "GillSansW01-Medium"; 163 | margin: 1rem 0 1rem 0; 164 | opacity: 0.8; 165 | } 166 | 167 | .section a { 168 | text-decoration: underline; 169 | } 170 | 171 | .Byline { 172 | font-family: "TimesModern-Regular"; 173 | font-size: 1.4em; 174 | line-height: 1.1em; 175 | color: #fff; 176 | -webkit-font-smoothing: antialiased; 177 | display: flex; 178 | flex-direction: column; 179 | border-top: 1px solid #dbdbdb; 180 | padding-top: 0.5em; 181 | width: 90%; 182 | margin: auto 0 0; 183 | } 184 | 185 | .team { 186 | font-family: "GillSansW01-Medium"; 187 | font-size: 0.8em; 188 | font-weight: 300; 189 | opacity: 0.8; 190 | } 191 | 192 | @media screen and (max-width: 767px) { 193 | .Byline { 194 | display: none; 195 | } 196 | } -------------------------------------------------------------------------------- /timeline-bubbles/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/times/dataviz-catalogue/fadceb03044acd37da4676a342a719fce8f274d7/timeline-bubbles/1.png -------------------------------------------------------------------------------- /timeline-bubbles/D3/script.js: -------------------------------------------------------------------------------- 1 | // set config object 2 | const isMobile = window.innerWidth < 600 ? true : false; 3 | const config = { 4 | width: 600, 5 | height: 350, 6 | mobileWidth: 300, 7 | mobileHeight: 300, 8 | ticksCount: 12, 9 | mobileTicksCount: 3, 10 | circleRadius: 70, 11 | mobileCircleRadius: 40, 12 | parseTime: d3.timeParse('%d/%m/%Y'), 13 | area: d3.scaleSqrt().domain([0, 300]), 14 | xScale: d3.scaleLinear(), 15 | bubbleOpacity: 0.6, 16 | get yTranslation() { 17 | return isMobile ? config.height / 4 : config.height / 2; 18 | }, 19 | }; 20 | 21 | const margin = { top: 40, right: 100, bottom: 30, left: 20 }, 22 | width = 23 | (isMobile ? config.mobileWidth : config.width) - margin.left - margin.right, 24 | height = 25 | (isMobile ? config.mobileHeight : config.height) - 26 | margin.top - 27 | margin.bottom; 28 | 29 | // Clean up before drawing 30 | // By brutally emptying all HTML from plot container div 31 | d3.select('#times-timeline').html(''); 32 | 33 | const svg = d3 34 | .select('#times-timeline') 35 | .at({ 36 | width: isMobile ? config.mobileWidth : config.width, 37 | height: isMobile ? config.mobileHeight : config.height, 38 | }) 39 | .st({ backgroundColor: '#F8F7F1' }); 40 | 41 | // g is our main container 42 | const g = svg.append('g').translate([margin.left, margin.top]); 43 | 44 | d3.json('data.json', (err, dataset) => { 45 | if (err) throw err; 46 | 47 | // Map over the data to process it, return a fresh copy, rather than mutating the original data 48 | const processedData = dataset 49 | .map(d => Object.assign({}, d, { date: config.parseTime(d.Date) })) 50 | .sort((x, y) => d3.descending(x.Fee, y.Fee)); 51 | 52 | /* 53 | * Scales 54 | * note that we use give an area to d3's radius parameter 55 | */ 56 | const area = config.area.range([ 57 | 3, 58 | isMobile ? config.mobileCircleRadius : config.circleRadius, 59 | ]); 60 | const x = config.xScale 61 | .range([0, width]) 62 | .domain(d3.extent(processedData, d => d.date)); 63 | 64 | // X-axis 65 | g 66 | .append('g') 67 | .translate([0, config.yTranslation]) 68 | .call( 69 | d3 70 | .axisBottom(x) 71 | .ticks(isMobile ? config.mobileTicksCount : config.ticksCount) 72 | .tickFormat(d3.timeFormat('%d %b')) 73 | .tickSizeInner(70) 74 | ); 75 | 76 | // Annotation layer 77 | const annotations = [ 78 | { 79 | type: d3.annotation.annotationLabel, 80 | note: { 81 | title: 'Something something annotated', 82 | label: '', 83 | wrap: 130, 84 | }, 85 | x: x(new Date('2017-08-07')), 86 | y: config.yTranslation, 87 | dy: isMobile ? -30 : -90, 88 | dx: 0, 89 | }, 90 | ]; 91 | 92 | // Include annotations 93 | const makeAnnotations = d3 94 | .annotation() 95 | .type(d3.annotationLabel) 96 | .annotations(annotations); 97 | g 98 | .append('g') 99 | .attr('class', 'annotation-group') 100 | .call(makeAnnotations); 101 | 102 | /* 103 | * The little things: 104 | * by sorting this way, the largest bubbles are 105 | * always at the very back. not data is hidden. 106 | */ 107 | processedData.sort((x, y) => d3.descending(x.Fee, y.Fee)); 108 | 109 | let circles = g.selectAll('circle').data(processedData); 110 | circles 111 | .enter() 112 | .append('circle') 113 | .at({ 114 | class: 'circle', 115 | cx: d => x(d.date), 116 | cy: config.yTranslation, 117 | }) 118 | .transition() 119 | .delay((d, i) => i * 50) 120 | .attr('r', d => area(d.Fee)) 121 | .st({ opacity: config.bubbleOpacity }); 122 | }); 123 | -------------------------------------------------------------------------------- /timeline-bubbles/D3/style.css: -------------------------------------------------------------------------------- 1 | svg { 2 | margin: 0 auto; 3 | padding: 25px; 4 | background-color: #f8f7f1; 5 | display: flex; 6 | } 7 | 8 | .tick line { 9 | stroke: #dbdbdb; 10 | stroke-dasharray: 5; 11 | } 12 | 13 | path.domain { 14 | stroke: #dbdbdb; 15 | stroke-width: 1px; 16 | } 17 | 18 | circle.circle { fill: #254251; stroke: #F9F8F3; } 19 | 20 | .annotation path, .annotation-connector { 21 | stroke: #E0AB26; 22 | stroke-width: 2px; 23 | fill: none; 24 | } 25 | .annotation path.connector-arrow, 26 | .annotation path.connector-dot, 27 | .title text, .annotation text { 28 | fill: #E0AB26; 29 | font: 13px GillSansMTStd-Medium, GillSansW01-Medium;; 30 | fill: #696969; 31 | color: #696969; 32 | } 33 | .annotation-note-bg { 34 | fill: rgba(0, 0, 0, 0); 35 | } 36 | .annotation-note-title, text.title { 37 | font: 13px GillSansMTStd-Medium, GillSansW01-Medium;; 38 | fill: #696969; 39 | color: #696969; 40 | } 41 | .annotation-note-content line { dispay: none; } 42 | -------------------------------------------------------------------------------- /timeline-bubbles/README.md: -------------------------------------------------------------------------------- 1 | # Timeline of bubbles 2 | 3 | ![](1.png) 4 | 5 | A simple yet slightly original timeline 6 | 7 | ## Data format 8 | 9 | #### for d3 10 | 11 | An array of objects containing at least two key/value pairs. 12 | 13 | ``` 14 | [{ 15 | "Date": "01/07/2017", 16 | "Fee": "5.4", 17 | },{ 18 | "Date": "02/07/2017", 19 | "Fee": "12", 20 | }, 21 | (...) 22 | ] 23 | ``` 24 | 25 | ## Download and edit 26 | 27 | Install the [SVG Crowbar](http://nytimes.github.io/svg-crowbar/) by dragging the bookmarklet on this page to your bookmarks bar. Click the bookmarklet to download an Illustrator-ready SVG. 28 | -------------------------------------------------------------------------------- /timeline-bubbles/data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Date": "01/07/2017", 4 | "Player_full": "Jan Bednarek [Lech Poznan - Southampton] Undisclosed", 5 | "Fee": "5.4", 6 | "Player": "Jan Bednarek", 7 | "Transfer_1": "Lech Poznan - Southampton", 8 | "Transfer": "Lech Poznan to Southampton" 9 | }, 10 | { 11 | "Date": "15/07/2017", 12 | "Player_full": "Tiemoue Bakayoko [Monaco - Chelsea] Undisclosed", 13 | "Fee": "35.2", 14 | "Player": "Tiemoue Bakayoko", 15 | "Transfer_1": "Monaco - Chelsea", 16 | "Transfer": "Monaco to Chelsea" 17 | }, 18 | { 19 | "Date": "15/07/2017", 20 | "Player_full": "Douglas Luiz [Vasco Da Gama - Manchester City] Undisclosed", 21 | "Fee": "10", 22 | "Player": "Douglas Luiz", 23 | "Transfer_1": "Vasco Da Gama - Manchester City", 24 | "Transfer": "Vasco Da Gama to Manchester City" 25 | }, 26 | { 27 | "Date": "16/07/2017", 28 | "Player_full": "Nolito [Manchester City - Sevilla] Undisclosed (reported £7.9m)", 29 | "Fee": "7.9", 30 | "Player": "Nolito", 31 | "Transfer_1": "Manchester City - Sevilla", 32 | "Transfer": "Manchester City to Sevilla" 33 | }, 34 | { 35 | "Date": "21/07/2017", 36 | "Player_full": "Javier Manquillo [Atletico Madrid - Newcastle] Undisclosed", 37 | "Fee": "4.5", 38 | "Player": "Javier Manquillo", 39 | "Transfer_1": "Atletico Madrid - Newcastle", 40 | "Transfer": "Atletico Madrid to Newcastle" 41 | }, 42 | { 43 | "Date": "22/07/2017", 44 | "Player_full": "Marko Arnautovic [Stoke - West Ham] £20m up to £25m", 45 | "Fee": "25", 46 | "Player": "Marko Arnautovic", 47 | "Transfer_1": "Stoke - West Ham", 48 | "Transfer": "Stoke to West Ham" 49 | }, 50 | { 51 | "Date": "25/07/2017", 52 | "Player_full": "Phil Bardsley [Stoke - Burnley] Undisclosed", 53 | "Fee": "1.5", 54 | "Player": "Phil Bardsley", 55 | "Transfer_1": "Stoke - Burnley", 56 | "Transfer": "Stoke to Burnley" 57 | }, 58 | { 59 | "Date": "31/07/2017", 60 | "Player_full": "Nemanja Matic [Chelsea - Manchester United] £40m", 61 | "Fee": "80", 62 | "Player": "Nemanja Matic", 63 | "Transfer_1": "Chelsea - Manchester United", 64 | "Transfer": "Chelsea to Manchester United" 65 | }, 66 | { 67 | "Date": "03/08/2017", 68 | "Player_full": "Kelechi Iheanacho [Manchester City - Leicester] £25m", 69 | "Fee": "25", 70 | "Player": "Kelechi Iheanacho", 71 | "Transfer_1": "Manchester City - Leicester", 72 | "Transfer": "Manchester City to Leicester" 73 | }, 74 | { 75 | "Date": "07/08/2017", 76 | "Player_full": "Sead Haksabanovic [Halmstads BK - West Ham] Undisclosed (reported £2.7m)", 77 | "Fee": "2.7", 78 | "Player": "Sead Haksabanovic", 79 | "Transfer_1": "Halmstads BK - West Ham", 80 | "Transfer": "Halmstads BK to West Ham" 81 | }, 82 | { 83 | "Date": "11/08/2017", 84 | "Player_full": "Bruno Martins Indi [Porto - Stoke] £7m", 85 | "Fee": "7", 86 | "Player": "Bruno Martins Indi", 87 | "Transfer_1": "Porto - Stoke", 88 | "Transfer": "Porto to Stoke" 89 | }, 90 | { 91 | "Date": "15/08/2017", 92 | "Player_full": "Gareth Barry [Everton - West Brom] Undisclosed", 93 | "Fee": "1", 94 | "Player": "Gareth Barry", 95 | "Transfer_1": "Everton - West Brom", 96 | "Transfer": "Everton to West Brom" 97 | }, 98 | { 99 | "Date": "21/08/2017", 100 | "Player_full": "Chris Wood [Leeds - Burnley] Undisclosed (reported £15m)", 101 | "Fee": "15", 102 | "Player": "Chris Wood", 103 | "Transfer_1": "Leeds - Burnley", 104 | "Transfer": "Leeds to Burnley" 105 | }, 106 | { 107 | "Date": "25/08/2017", 108 | "Player_full": "Oliver Burke [RB Leipzig - West Brom] Undisclosed", 109 | "Fee": "30", 110 | "Player": "Oliver Burke", 111 | "Transfer_1": "RB Leipzig - West Brom", 112 | "Transfer": "RB Leipzig to West Brom" 113 | }, 114 | { 115 | "Date": "29/08/2017", 116 | "Player_full": "Kevin Wimmer [Tottenham - Stoke] £18m", 117 | "Fee": "5", 118 | "Player": "Kevin Wimmer", 119 | "Transfer_1": "Tottenham - Stoke", 120 | "Transfer": "Tottenham to Stoke" 121 | } 122 | ] 123 | -------------------------------------------------------------------------------- /timeline-bubbles/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 13 | 17 | 21 | 25 | 29 | 30 | 31 | 37 | 38 |
39 |
Timeline of bubbles
40 |
41 | When and for how much players moved during the summer 2017 Premier 42 | League transfer window (fake data) 43 |
44 | 45 |
46 |

47 | This layout proposes a different visualisation of time and importance 48 | of events, 49 | as we used in our detailed analysis of the 2017 transfer window. 53 |

54 |
55 | 56 | 59 | 60 | 61 | 82 |
83 | 84 | 88 | 89 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /times-visual-vocabulary.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: Default 4 | SaveWorkspace: Default 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | -------------------------------------------------------------------------------- /treemap/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/times/dataviz-catalogue/fadceb03044acd37da4676a342a719fce8f274d7/treemap/1.png -------------------------------------------------------------------------------- /treemap/README.md: -------------------------------------------------------------------------------- 1 | # Treemap 2 | 3 | ![](1.png) 4 | 5 | A treemap recursively subdivides area into rectangles; the area of any node in the tree corresponds to its value. 6 | 7 | Use to represent hierarchies. 8 | 9 | ## Data format 10 | 11 | The data format must be a hierarchical representation. 12 | 13 | ``` 14 | { 15 | "name": "transfers_position_in", 16 | "children": [ 17 | { 18 | "name": "Defender", 19 | "children": [ 20 | { 21 | "name": "Cohen Bramall", 22 | "fee": "0.04", 23 | "fromto": "Hednesford Town to Arsenal", 24 | "position": "Defender", 25 | "id": "transfers_position_in.Defender.Cohen Bramall" 26 | }, 27 | (..) 28 | ], 29 | "id": "transfers_position_in.Defender" 30 | }, 31 | { 32 | "name": "Goalkeeper", 33 | "children": [ 34 | { 35 | "name": "Aaron Ramsdale", 36 | "fee": "0.04", 37 | "fromto": "Sheffield United to Bournemouth", 38 | "position": "Goalkeeper", 39 | "id": "transfers_position_in.Goalkeeper.Aaron Ramsdale" 40 | }, 41 | (...) 42 | ], 43 | "id": "transfers_position_in.Goalkeeper" 44 | }, 45 | (...) 46 | ], 47 | "id": "transfers_position_in" 48 | } 49 | ``` 50 | 51 | ## Download and edit 52 | 53 | Install the [SVG Crowbar](http://nytimes.github.io/svg-crowbar/) by dragging the bookmarklet on this page to your bookmarks bar. Click the bookmarklet to download an Illustrator-ready SVG. 54 | -------------------------------------------------------------------------------- /treemap/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "transfers_position_in", 3 | "children": [ 4 | { 5 | "name": "Defender", 6 | "children": [ 7 | { 8 | "name": "Cohen Bramall", 9 | "fee": "0.04", 10 | "fromto": "Hednesford Town to Arsenal", 11 | "position": "Defender", 12 | "id": "transfers_position_in.Defender.Cohen Bramall" 13 | }, 14 | { 15 | "name": "Robbie Brady", 16 | "fee": "13.00", 17 | "fromto": "Norwich City to Burnley", 18 | "position": "Defender", 19 | "id": "transfers_position_in.Defender.Robbie Brady" 20 | }, 21 | { 22 | "name": "Jeffrey Schlupp", 23 | "fee": "12.50", 24 | "fromto": "Leicester City to Crystal Palace", 25 | "position": "Defender", 26 | "id": "transfers_position_in.Defender.Jeffrey Schlupp" 27 | }, 28 | { 29 | "name": "Patrick v. Aanholt", 30 | "fee": "14.00", 31 | "fromto": "Sunderland to Crystal Palace", 32 | "position": "Defender", 33 | "id": "transfers_position_in.Defender.Patrick v. Aanholt" 34 | }, 35 | { 36 | "name": "Mamadou Sakho", 37 | "fee": "0.00", 38 | "fromto": "Liverpool to Crystal Palace", 39 | "position": "Defender", 40 | "id": "transfers_position_in.Defender.Mamadou Sakho" 41 | }, 42 | { 43 | "name": "Omar Elabdellaoui", 44 | "fee": "0.00", 45 | "fromto": "Olympiacos to Hull City", 46 | "position": "Defender", 47 | "id": "transfers_position_in.Defender.Omar Elabdellaoui" 48 | }, 49 | { 50 | "name": "Andrea Ranocchia", 51 | "fee": "0.00", 52 | "fromto": "Inter Milan to Hull City", 53 | "position": "Defender", 54 | "id": "transfers_position_in.Defender.Andrea Ranocchia" 55 | }, 56 | { 57 | "name": "Molla Wague", 58 | "fee": "0.00", 59 | "fromto": "Granada CF to Leicester City", 60 | "position": "Defender", 61 | "id": "transfers_position_in.Defender.Molla Wague" 62 | }, 63 | { 64 | "name": "Joleon Lescott", 65 | "fee": "0.00", 66 | "fromto": "AEK Athens to Sunderland", 67 | "position": "Defender", 68 | "id": "transfers_position_in.Defender.Joleon Lescott" 69 | }, 70 | { 71 | "name": "Bryan Oviedo", 72 | "fee": "3.75", 73 | "fromto": "Everton to Sunderland", 74 | "position": "Defender", 75 | "id": "transfers_position_in.Defender.Bryan Oviedo" 76 | }, 77 | { 78 | "name": "Martin Olsson", 79 | "fee": "4.50", 80 | "fromto": "Norwich City to Swansea City", 81 | "position": "Defender", 82 | "id": "transfers_position_in.Defender.Martin Olsson" 83 | }, 84 | { 85 | "name": "Marc Wilson", 86 | "fee": "0.00", 87 | "fromto": "Bournemouth to West Bromwich Albion", 88 | "position": "Defender", 89 | "id": "transfers_position_in.Defender.Marc Wilson" 90 | }, 91 | { 92 | "name": "José Fonte", 93 | "fee": "8.00", 94 | "fromto": "Southampton to West Ham United", 95 | "position": "Defender", 96 | "id": "transfers_position_in.Defender.José Fonte" 97 | }, 98 | { 99 | "name": "Nathan Holland", 100 | "fee": "0.00", 101 | "fromto": "Everton to West Ham United", 102 | "position": "Defender", 103 | "id": "transfers_position_in.Defender.Nathan Holland" 104 | } 105 | ], 106 | "id": "transfers_position_in.Defender" 107 | }, 108 | { 109 | "name": "Goalkeeper", 110 | "children": [ 111 | { 112 | "name": "Aaron Ramsdale", 113 | "fee": "0.04", 114 | "fromto": "Sheffield United to Bournemouth", 115 | "position": "Goalkeeper", 116 | "id": "transfers_position_in.Goalkeeper.Aaron Ramsdale" 117 | }, 118 | { 119 | "name": "Mouez Hassen", 120 | "fee": "0.00", 121 | "fromto": "OGC Nice to Southampton", 122 | "position": "Goalkeeper", 123 | "id": "transfers_position_in.Goalkeeper.Mouez Hassen" 124 | }, 125 | { 126 | "name": "Lee Grant", 127 | "fee": "1.30", 128 | "fromto": "Derby County to Stoke City", 129 | "position": "Goalkeeper", 130 | "id": "transfers_position_in.Goalkeeper.Lee Grant" 131 | } 132 | ], 133 | "id": "transfers_position_in.Goalkeeper" 134 | }, 135 | { 136 | "name": "Midfielder", 137 | "children": [ 138 | { 139 | "name": "Joey Barton", 140 | "fee": "0.00", 141 | "fromto": "Rangers to Burnley", 142 | "position": "Midfielder", 143 | "id": "transfers_position_in.Midfielder.Joey Barton" 144 | }, 145 | { 146 | "name": "Ashley Westwood", 147 | "fee": "5.00", 148 | "fromto": "Aston Villa to Burnley", 149 | "position": "Midfielder", 150 | "id": "transfers_position_in.Midfielder.Ashley Westwood" 151 | }, 152 | { 153 | "name": "Luka Milivojevic", 154 | "fee": "13.60", 155 | "fromto": "Olympiacos to Crystal Palace", 156 | "position": "Midfielder", 157 | "id": "transfers_position_in.Midfielder.Luka Milivojevic" 158 | }, 159 | { 160 | "name": "Morgan Schneiderlin", 161 | "fee": "24.00", 162 | "fromto": "Manchester United to Everton", 163 | "position": "Midfielder", 164 | "id": "transfers_position_in.Midfielder.Morgan Schneiderlin" 165 | }, 166 | { 167 | "name": "Evandro Goebel", 168 | "fee": "2.13", 169 | "fromto": "Porto to Hull City", 170 | "position": "Midfielder", 171 | "id": "transfers_position_in.Midfielder.Evandro Goebel" 172 | }, 173 | { 174 | "name": "Markus Henriksen", 175 | "fee": "4.25", 176 | "fromto": "AZ Alkmaar to Hull City", 177 | "position": "Midfielder", 178 | "id": "transfers_position_in.Midfielder.Markus Henriksen" 179 | }, 180 | { 181 | "name": "Lazar Markovic", 182 | "fee": "0.00", 183 | "fromto": "Liverpool to Hull City", 184 | "position": "Midfielder", 185 | "id": "transfers_position_in.Midfielder.Lazar Markovic" 186 | }, 187 | { 188 | "name": "Alfred N'Diaye", 189 | "fee": "0.00", 190 | "fromto": "Villarreal to Hull City", 191 | "position": "Midfielder", 192 | "id": "transfers_position_in.Midfielder.Alfred N'Diaye" 193 | }, 194 | { 195 | "name": "Wilfred Ndidi", 196 | "fee": "15.00", 197 | "fromto": "Genk to Leicester City", 198 | "position": "Midfielder", 199 | "id": "transfers_position_in.Midfielder.Wilfred Ndidi" 200 | }, 201 | { 202 | "name": "Adlene Guedioura", 203 | "fee": "0.00", 204 | "fromto": "Watford to Middlesbrough", 205 | "position": "Midfielder", 206 | "id": "transfers_position_in.Midfielder.Adlene Guedioura" 207 | }, 208 | { 209 | "name": "Darron Gibson", 210 | "fee": "3.75", 211 | "fromto": "Everton to Sunderland", 212 | "position": "Midfielder", 213 | "id": "transfers_position_in.Midfielder.Darron Gibson" 214 | }, 215 | { 216 | "name": "Luciano Narsingh", 217 | "fee": "4.00", 218 | "fromto": "PSV Eindhoven to Swansea City", 219 | "position": "Midfielder", 220 | "id": "transfers_position_in.Midfielder.Luciano Narsingh" 221 | }, 222 | { 223 | "name": "Tom Carroll", 224 | "fee": "4.50", 225 | "fromto": "Tottenham Hotspur to Swansea City", 226 | "position": "Midfielder", 227 | "id": "transfers_position_in.Midfielder.Tom Carroll" 228 | }, 229 | { 230 | "name": "Tom Cleverley", 231 | "fee": "0.00", 232 | "fromto": "Everton to Watford", 233 | "position": "Midfielder", 234 | "id": "transfers_position_in.Midfielder.Tom Cleverley" 235 | }, 236 | { 237 | "name": "Jake Livermore", 238 | "fee": "10.00", 239 | "fromto": "Hull City to West Bromwich Albion", 240 | "position": "Midfielder", 241 | "id": "transfers_position_in.Midfielder.Jake Livermore" 242 | }, 243 | { 244 | "name": "Robert Snodgrass", 245 | "fee": "10.20", 246 | "fromto": "Hull City to West Ham United", 247 | "position": "Midfielder", 248 | "id": "transfers_position_in.Midfielder.Robert Snodgrass" 249 | } 250 | ], 251 | "id": "transfers_position_in.Midfielder" 252 | }, 253 | { 254 | "name": "Striker", 255 | "children": [ 256 | { 257 | "name": "Ademola Lookman", 258 | "fee": "7.50", 259 | "fromto": "Charlton Athletic to Everton", 260 | "position": "Striker", 261 | "id": "transfers_position_in.Striker.Ademola Lookman" 262 | }, 263 | { 264 | "name": "Oumar Niasse", 265 | "fee": "0.00", 266 | "fromto": "Everton to Hull City", 267 | "position": "Striker", 268 | "id": "transfers_position_in.Striker.Oumar Niasse" 269 | }, 270 | { 271 | "name": "Kamil Grosicki", 272 | "fee": "7.00", 273 | "fromto": "Stade Rennais to Hull City", 274 | "position": "Striker", 275 | "id": "transfers_position_in.Striker.Kamil Grosicki" 276 | }, 277 | { 278 | "name": "Gabriel Jesus", 279 | "fee": "27.20", 280 | "fromto": "Palmeiras to Manchester City", 281 | "position": "Striker", 282 | "id": "transfers_position_in.Striker.Gabriel Jesus" 283 | }, 284 | { 285 | "name": "Rudy Gestede", 286 | "fee": "6.00", 287 | "fromto": "Aston Villa to Middlesbrough", 288 | "position": "Striker", 289 | "id": "transfers_position_in.Striker.Rudy Gestede" 290 | }, 291 | { 292 | "name": "Patrick Bamford", 293 | "fee": "10.00", 294 | "fromto": "Chelsea to Middlesbrough", 295 | "position": "Striker", 296 | "id": "transfers_position_in.Striker.Patrick Bamford" 297 | }, 298 | { 299 | "name": "Manolo Gabbiadini", 300 | "fee": "14.45", 301 | "fromto": "Napoli to Southampton", 302 | "position": "Striker", 303 | "id": "transfers_position_in.Striker.Manolo Gabbiadini" 304 | }, 305 | { 306 | "name": "Saido Berahino", 307 | "fee": "15.00", 308 | "fromto": "West Bromwich Albion to Stoke City", 309 | "position": "Striker", 310 | "id": "transfers_position_in.Striker.Saido Berahino" 311 | }, 312 | { 313 | "name": "Jordan Ayew", 314 | "fee": "0.00", 315 | "fromto": "Aston Villa to Swansea City", 316 | "position": "Striker", 317 | "id": "transfers_position_in.Striker.Jordan Ayew" 318 | }, 319 | { 320 | "name": "Mauro Zárate", 321 | "fee": "2.30", 322 | "fromto": "Fiorentina to Watford", 323 | "position": "Striker", 324 | "id": "transfers_position_in.Striker.Mauro Zárate" 325 | }, 326 | { 327 | "name": "M'Baye Niang", 328 | "fee": "0.00", 329 | "fromto": "AC Milan to Watford", 330 | "position": "Striker", 331 | "id": "transfers_position_in.Striker.M'Baye Niang" 332 | } 333 | ], 334 | "id": "transfers_position_in.Striker" 335 | } 336 | ], 337 | "id": "transfers_position_in" 338 | } 339 | -------------------------------------------------------------------------------- /treemap/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 22 | 23 |
24 |
Treemap
25 |
Where do the Premier League transfer bought during the January 2017 transfer window come from
26 | 27 |
28 |

Displays hierarchical data ordered by importance (in this case, geographical origin and price), as we used in our January 2017 transfer window microsite 29 |

30 |
31 | 32 | 35 | 36 | 37 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /treemap/script.js: -------------------------------------------------------------------------------- 1 | // set config object 2 | const config = { width: 700, height: 450, mobileWidth: 300, mobileHeight: 450 }; 3 | const isMobile = window.innerWidth < 600 ? true : false; 4 | 5 | const margin = { 6 | top: 20, 7 | left: 20, 8 | right: 150, 9 | bottom: 0, 10 | mobileRight: 10, 11 | mobileBottom: 100, 12 | }; 13 | const width = isMobile 14 | ? config.mobileWidth - margin.left - margin.mobileRight 15 | : config.width - margin.left - margin.right, 16 | height = isMobile 17 | ? config.mobileHeight - margin.top - margin.mobileBottom 18 | : config.height - margin.top - margin.bottom; 19 | 20 | d3.select('#times-treemap').html(''); 21 | 22 | const svg = d3.select('#times-treemap').at({ 23 | width: isMobile ? config.mobileWidth : config.width, 24 | height: isMobile ? config.mobileHeight : config.height, 25 | }); 26 | 27 | // construct an ordinal scale from our colour palette 28 | const timesColors = ['#254251', '#E0AB26', '#F37F2F', '#3292A6', '#6c3c5e']; 29 | const color = d3.scaleOrdinal(timesColors); 30 | const format = d3.format(',d'); 31 | 32 | const makeLegend = (container, key, legendConfig) => { 33 | // Create a legend element 34 | const legend = container 35 | .append('g') 36 | .at({ class: 'legendContainer' }) 37 | .selectAll('g') 38 | .data(key) 39 | .enter() 40 | .append('g') 41 | .at({ class: 'legend' }) 42 | .attr('transform', (d, i) => { 43 | const leftmargin = 0; 44 | const topmargin = 0; 45 | const x = leftmargin + legendConfig.x; 46 | const y = i * legendConfig.height + legendConfig.y + topmargin; 47 | return 'translate(' + x + ',' + y + ')'; 48 | }); 49 | 50 | const legendTitle = container 51 | .append('g') 52 | .at({ class: 'legendTitle' }) 53 | .attr('transform', (d, i) => { 54 | const height = 20; 55 | const x = legendConfig.x; 56 | const y = i * height + legendConfig.y; 57 | return 'translate(' + x + ',' + y + ')'; 58 | }); 59 | 60 | legendTitle 61 | .append('text') 62 | .at({ x: 0, y: 20 }) 63 | .text('Key'); 64 | 65 | legend 66 | .append('rect') 67 | .at({ 68 | width: 10, 69 | height: 10, 70 | }) 71 | .translate([0, 30]) 72 | .st({ 73 | fill: d => d.color, 74 | stroke: d => d.color, 75 | }); 76 | 77 | legend 78 | .append('text') 79 | .at({ 80 | x: 20, 81 | y: 40, 82 | }) 83 | .st({ color: '#666', fill: '#666' }) 84 | .text(d => d.name); 85 | 86 | const linewidth = config.width < 400 ? width - 20 : 100; 87 | const lineheight = config.width < 400 ? 90 : 110; 88 | legendTitle.append('line').at({ 89 | x1: 0, 90 | x2: linewidth, 91 | y1: 0, 92 | y2: 0, 93 | strokeWidth: 2, 94 | stroke: '#ddd', 95 | }); 96 | legendTitle.append('line').at({ 97 | x1: 0, 98 | x2: linewidth, 99 | y1: lineheight, 100 | y2: lineheight, 101 | strokeWidth: 2, 102 | stroke: '#ddd', 103 | }); 104 | }; 105 | 106 | d3.json('data.json', (err, dataset) => { 107 | if (err) throw err; 108 | 109 | const treemap = d3 110 | .treemap() 111 | .tile(d3.treemapResquarify) 112 | .size([width, height]) 113 | .round(true) 114 | .paddingOuter(2) 115 | .paddingInner(1); 116 | 117 | const root = d3 118 | .hierarchy(dataset) 119 | .eachBefore( 120 | d => (d.data.id = (d.parent ? d.parent.data.id + '.' : '') + d.data.name) 121 | ) 122 | .sum(sumBySize) 123 | .sort((a, b) => b.height - a.height || b.value - a.value); 124 | 125 | treemap(root); 126 | 127 | // One cell per player 128 | const container = svg.append('g').at({ class: 'container' }); 129 | 130 | const cell = container 131 | .selectAll('g') 132 | .data(root.leaves()) 133 | .enter() 134 | .append('g') 135 | .translate(d => [d.x0, d.y0]); 136 | 137 | cell 138 | .append('rect') 139 | .at({ 140 | id: d => d.data.id, 141 | class: d => (d.x1 - d.x0 > 120 && d.y1 - d.y0 > 40 ? 'wide' : null), 142 | width: d => d.x1 - d.x0, 143 | height: d => d.y1 - d.y0, 144 | fill: d => color(d.parent.data.id), 145 | }) 146 | .on('mouseover', function(d) { 147 | var _this = this; 148 | d3 149 | .selectAll('rect') 150 | .transition() 151 | .duration(100) 152 | .style('opacity', function() { 153 | return this === _this ? 1.0 : 0.6; 154 | }); 155 | }) 156 | .on('mouseout', function(d) { 157 | d3 158 | .selectAll('rect') 159 | .transition() 160 | .duration(500) 161 | .style('opacity', 1); 162 | }) 163 | .on('click', function(d) { 164 | appendPlayerInfo(this, d); 165 | }); 166 | 167 | // Player names 168 | cell 169 | .append('text') 170 | .attr('clip-path', d => 'url(#clip-' + d.data.id + ')') 171 | .append('tspan') 172 | .at({ 173 | x: 8, 174 | y: 8, 175 | dy: '.8em', 176 | class: 'playerNames', 177 | }) 178 | .text(function(d) { 179 | // Only display text if sibling element is wide enough 180 | const parentRect = this.parentNode.previousElementSibling; 181 | if (d3.select(parentRect).classed('wide')) { 182 | return d.data.name; 183 | } 184 | }); 185 | 186 | cell 187 | .append('text') 188 | .attr('clip-path', d => 'url(#clip-' + d.data.id + ')') 189 | .append('tspan') 190 | .at({ 191 | x: 8, 192 | y: 20, 193 | dy: '1.2em', 194 | class: 'playerNamesFee', 195 | }) 196 | .text(function(d) { 197 | // Only display text if sibling element is wide enough 198 | const parentRect = this.parentNode.parentNode.firstElementChild; 199 | if (d3.select(parentRect).classed('wide')) { 200 | return '£' + d.data.fee.split('.')[0] + 'm'; 201 | } 202 | }); 203 | 204 | // 💩 Manual labelling 205 | const key = [ 206 | { name: 'Europe', color: '#254251' }, 207 | { name: 'Britain', color: '#E0AB26' }, 208 | { name: 'Africa', color: '#3292A6' }, 209 | { name: 'S. America', color: '#F37F2F' }, 210 | ]; 211 | 212 | let legendConfig = { 213 | x: isMobile ? 10 : width + 10, 214 | y: isMobile ? height : 10, 215 | height: 20, 216 | }; 217 | 218 | container.call(makeLegend(container, key, legendConfig)); 219 | 220 | // // Create a legend element 221 | // const titleheight = height; 222 | // const titlemargin = 30; 223 | // const playerInfoContainer = svg 224 | // .append('g') 225 | // .attr('class', 'playerInfoContainer') 226 | // .selectAll('g') 227 | // .data(key) 228 | // .enter() 229 | // .append('g') 230 | // .attr('class', 'playerInfo') 231 | // .translate((d, i) => { 232 | // return [legendConfig.x, i * titleheight + 10]; 233 | // }); 234 | 235 | // const playerInfoTitle = svg 236 | // .append('g') 237 | // .attr('class', 'playerInfoTitle') 238 | // .translate([legendConfig.x, titleheight * 0.3 + titlemargin]); 239 | 240 | // playerInfoTitle 241 | // .append('text') 242 | // .attr('x', 0) 243 | // .attr('y', 0); 244 | 245 | // const playerInfo = svg 246 | // .append('g') 247 | // .attr('class', 'playerInfo') 248 | // .translate([legendConfig.x, titleheight * 0.3 + titlemargin + 30]); 249 | 250 | // playerInfo 251 | // .append('text') 252 | // .at({ 253 | // class: 'playerInfo', 254 | // x: 0, 255 | // y: 10, 256 | // }) 257 | // .tspans(() => d3.wordwrap('Tap an area for more information', 20)); 258 | 259 | // // Appends player info on click on a rect 260 | // const appendPlayerInfo = (obj, data) => { 261 | // playerInfoTitle.html(''); 262 | // playerInfo.html(''); 263 | 264 | // playerInfo.append('text').at({ x: 0, y: 0, class: 'playerInfo' }); 265 | 266 | // playerInfoTitle 267 | // .append('text') 268 | // .at({ y: 0 }) 269 | // .text('£' + data.data.fee.split('.')[0] + 'm'); 270 | // playerInfo 271 | // .append('text') 272 | // .at({ y: -25 }) 273 | // .tspans(() => { 274 | // const { name, fromto } = data.data; 275 | // return d3.wordwrap(name + ' ' + fromto, 15); 276 | // }) 277 | // .attr('dy', (d, i) => i + 15); 278 | // }; 279 | }); 280 | 281 | const sumBySize = d => d.fee; 282 | -------------------------------------------------------------------------------- /treemap/style.css: -------------------------------------------------------------------------------- 1 | svg { 2 | margin: 0 auto; 3 | display: flex; 4 | background-color: #f8f7f1; 5 | } 6 | text, .text { 7 | font: 15px arial; 8 | fill: white; 9 | color: white; 10 | } 11 | .d3-tip { 12 | line-height: 1; 13 | font: 15px arial; 14 | fill: white; 15 | color: white; 16 | z-index: 1000; 17 | } 18 | 19 | .legendTitle text { 20 | font: 18px Times New Roman; 21 | fill: black; 22 | } 23 | 24 | .playerInfoTitle text { 25 | font-size: 30px; 26 | font-family: Times New Roman; 27 | fill: black; 28 | } 29 | .playerInfo text { 30 | font: 15px arial; 31 | fill: #696969; 32 | 33 | &.mobile { 34 | font: 20px serif; 35 | fill: #1d1d1d; 36 | } 37 | } 38 | 39 | .playerNamesFee { 40 | font-size: 15px; 41 | fill: rgba(255,255,255, .6) 42 | } 43 | 44 | -------------------------------------------------------------------------------- /vertical-stepped-line/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/times/dataviz-catalogue/fadceb03044acd37da4676a342a719fce8f274d7/vertical-stepped-line/1.png -------------------------------------------------------------------------------- /vertical-stepped-line/README.md: -------------------------------------------------------------------------------- 1 | # Vertical steppy line 2 | 3 | ![](1.png) 4 | 5 | A vertical line chart build for January 2017 Transfer Window, complete with Times-esque annotations. 6 | 7 | ## Data format 8 | 9 | An array of objects containing a _date_ (on the y-axis) and a _value_ (on the x-axis). 10 | 11 | ``` 12 | [{ 13 | "year": "2002/06", 14 | "value": "-0.09" 15 | },{ 16 | "year": "2003/01", 17 | "value": "1.19" 18 | }, 19 | (...) 20 | ] 21 | ``` 22 | 23 | ## Annotations 24 | 25 | At the top of `script.js`, an array of objects contains the annotation layer. 26 | 27 | Un-comment line 119 (`draggable(true)`) and refresh to manually drag and edit your annotations. Once you're happy, run `copy(annotations)` in the console and paste into `annotations` at the top of `script.js`. 28 | 29 | ## Download and edit 30 | 31 | Install the [SVG Crowbar](http://nytimes.github.io/svg-crowbar/) by dragging the bookmarklet on this page to your bookmarks bar. Click the bookmarklet to download an Illustrator-ready SVG. 32 | -------------------------------------------------------------------------------- /vertical-stepped-line/data.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "year": "2002/06", 3 | "value": "-0.09" 4 | }, 5 | { 6 | "year": "2003/01", 7 | "value": "1.19" 8 | }, 9 | { 10 | "year": "2003/06", 11 | "value": "-6.86" 12 | }, 13 | { 14 | "year": "2004/01", 15 | "value": "-16.11" 16 | }, 17 | { 18 | "year": "2004/06", 19 | "value": "-5.01" 20 | }, 21 | { 22 | "year": "2005/01", 23 | "value": "-1.87" 24 | }, 25 | { 26 | "year": "2005/06", 27 | "value": "1.06" 28 | }, 29 | { 30 | "year": "2006/01", 31 | "value": "-18.92" 32 | }, 33 | { 34 | "year": "2006/06", 35 | "value": "-3.70" 36 | }, 37 | { 38 | "year": "2007/01", 39 | "value": "1.93" 40 | }, 41 | { 42 | "year": "2007/06", 43 | "value": "15.96" 44 | }, 45 | { 46 | "year": "2008/01", 47 | "value": "6.01" 48 | }, 49 | { 50 | "year": "2008/06", 51 | "value": "0.00" 52 | }, 53 | { 54 | "year": "2009/01", 55 | "value": "-14.03" 56 | }, 57 | { 58 | "year": "2009/06", 59 | "value": "30.35" 60 | }, 61 | { 62 | "year": "2010/01", 63 | "value": "0.00" 64 | }, 65 | { 66 | "year": "2010/06", 67 | "value": "-8.33" 68 | }, 69 | { 70 | "year": "2011/01", 71 | "value": "-3.06" 72 | }, 73 | { 74 | "year": "2011/06", 75 | "value": "10.51" 76 | }, 77 | { 78 | "year": "2012/01", 79 | "value": "0.38" 80 | }, 81 | { 82 | "year": "2012/06", 83 | "value": "16.53" 84 | }, 85 | { 86 | "year": "2013/01", 87 | "value": "-8.16" 88 | }, 89 | { 90 | "year": "2013/06", 91 | "value": "-31.20" 92 | }, 93 | { 94 | "year": "2014/01", 95 | "value": "8.16" 96 | }, 97 | { 98 | "year": "2014/06", 99 | "value": "-65.05" 100 | }, 101 | { 102 | "year": "2015/01", 103 | "value": "-12.45" 104 | }] 105 | -------------------------------------------------------------------------------- /vertical-stepped-line/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 23 | 24 |
25 |
Vertical stepped line
26 |
Balance of money spent and received by Arsenal over the years
27 | 28 |
29 |

As we used in our January 2017 transfer window microsite 30 |

31 |
32 | 33 | 34 | 37 | 38 | 39 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /vertical-stepped-line/script.js: -------------------------------------------------------------------------------- 1 | const annotations = [ 2 | { 3 | value: -100, 4 | year: '2013/01', 5 | path: 'M93,-26L93,36', 6 | text: 'Arsenal buy Alexis Sánchez', 7 | textOffset: [-25, -35], 8 | }, 9 | ]; 10 | 11 | // The config object passed by draw() gives us a width and height 12 | const config = { width: 600, height: 550, mobileWidth: 300, mobileHeight: 300 }; 13 | const isMobile = window.innerWidth < 600 ? true : false; 14 | const margin = { top: 30, right: 100, bottom: 100, left: 40 }, 15 | width = 16 | (isMobile ? config.mobileWidth : config.width) - margin.left - margin.right, 17 | height = 18 | (isMobile ? config.mobileHeight : config.height) - 19 | margin.top - 20 | margin.bottom; 21 | 22 | // Clean up SVG container before drawing 23 | d3.select('#times-vertical-line').html(''); 24 | 25 | const svg = d3.select('#times-vertical-line').at({ 26 | width: isMobile ? config.mobileWidth : config.width, 27 | height: isMobile ? config.mobileHeight : config.height, 28 | }); 29 | 30 | // Date parser 31 | const parseTime = d3.timeParse('%Y/%m'); 32 | 33 | // Scales 34 | const x = d3.scaleLinear().range([0, width]); 35 | const y = d3.scaleTime().range([height, 0]); 36 | 37 | // Line declaration 38 | const line = d3 39 | .line() 40 | .x(d => x(d.value)) 41 | .y(d => y(d.year)) 42 | .curve(d3.curveStepAfter); 43 | 44 | // g is our container 45 | const g = svg.append('g').translate([margin.left, margin.top]); 46 | 47 | d3.json('data.json', (err, dataset) => { 48 | if (err) throw err; 49 | 50 | const processedData = dataset.map(d => 51 | Object.assign({}, d, { 52 | year: parseTime(d.year), 53 | value: parseInt(d.value), 54 | }) 55 | ); 56 | 57 | // Min, max values from dataset 58 | // or computed for each club 59 | const hardCordedDomain = [-140, 140]; 60 | const clubDomain = [ 61 | d3.min(processedData, d => d.value), 62 | d3.max(processedData, d => d.value), 63 | ]; 64 | 65 | // Set domains 66 | // Fixed values x-axis 67 | x.domain(hardCordedDomain); 68 | y.domain(d3.extent(processedData, d => d.year).reverse()); 69 | 70 | // X-axis 71 | g 72 | .append('g') 73 | .at({ class: 'axis axis--x' }) 74 | .translate([30, height]) 75 | .call( 76 | d3 77 | .axisBottom(x) 78 | .ticks(isMobile ? 3 : 10) 79 | .tickSize(-height, 0, 0) 80 | .tickPadding(10) 81 | ); 82 | 83 | // text label for the x axis 84 | g 85 | .append('text') 86 | .at({ class: 'label' }) 87 | .translate([x(0), height + 50]) 88 | .style('text-anchor', 'middle') 89 | .text('Season balance (£m)'); 90 | g 91 | .append('text') 92 | .at({ class: 'label' }) 93 | .translate([x(0) - 20, margin.top - 55]) 94 | .style('text-anchor', 'middle') 95 | .text('⟵ Spent'); 96 | g 97 | .append('text') 98 | .at({ class: 'label' }) 99 | .translate([x(0) + 90, margin.top - 55]) 100 | .style('text-anchor', 'middle') 101 | .text('Received ⟶'); 102 | 103 | // Y-axis 104 | g 105 | .append('g') 106 | .attr('class', 'axis axis--y') 107 | .call(d3.axisLeft(y).ticks(isMobile ? 5 : 10)); 108 | 109 | // Dashed line on the zero for reference 110 | // Created before the spending line so it"s under 111 | g 112 | .append('line') 113 | .at({ 114 | class: 'zero', 115 | x1: x(0), 116 | y1: 0, 117 | x2: x(0), 118 | y2: height, 119 | }) 120 | .translate([30, 0]) 121 | .style('stroke', '#666'); 122 | 123 | // Main spending line 124 | g 125 | .append('path') 126 | .datum(processedData) 127 | .at({ class: 'line', d: line }) 128 | .translate([30, 0]); 129 | 130 | // Annotations 131 | var swoopy = d3 132 | .swoopyDrag() 133 | .x(d => x(d.value)) 134 | .y(d => y(parseTime(d.year))) 135 | //.draggable(true) 136 | .annotations(annotations); 137 | 138 | if (!isMobile) { 139 | var swoopySel = g 140 | .append('g') 141 | .attr('class', 'swoop') 142 | .call(swoopy); 143 | } 144 | 145 | // SVG arrow marker fix 146 | svg 147 | .append('marker') 148 | .attr('id', 'arrow') 149 | .attr('viewBox', '-10 -10 20 20') 150 | .attr('markerWidth', 6) 151 | .attr('markerHeight', 6) 152 | .attr('orient', 'auto') 153 | .append('circle') 154 | .attr('r', '6') 155 | .style('fill', '#F37F2F'); 156 | swoopySel.selectAll('path').attr('marker-start', 'url(#arrow)'); 157 | }); 158 | -------------------------------------------------------------------------------- /vertical-stepped-line/style.css: -------------------------------------------------------------------------------- 1 | svg { 2 | margin: 0 auto; 3 | padding: 25px; 4 | overflow: visible; 5 | display: flex; 6 | background-color: #f7f8f1; 7 | } 8 | 9 | .axis, .axis text, text { 10 | font: 15px arial; 11 | fill: #666; 12 | color: #666; 13 | } 14 | 15 | .axis path, 16 | .axis line { 17 | fill: none; 18 | stroke: lightgrey; 19 | stroke-width: 1px; 20 | shape-rendering: crispEdges; 21 | stroke-dasharray: 5; 22 | } 23 | .axis .domain { 24 | stroke-width: 0px; 25 | } 26 | 27 | .line { 28 | fill: none; 29 | stroke: #254251; 30 | stroke-width: 2px; 31 | } 32 | 33 | .zero { 34 | stroke: lightgrey; 35 | stroke-width: 1px; 36 | stroke-dasharray: 5; 37 | } 38 | 39 | .swoop path { 40 | fill: none; 41 | stroke: #F37F2F; 42 | stroke-width: 2px; 43 | } 44 | .swoop text { 45 | color:#F37F2F; 46 | fill:#F37F2F; 47 | } 48 | -------------------------------------------------------------------------------- /waffle/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/times/dataviz-catalogue/fadceb03044acd37da4676a342a719fce8f274d7/waffle/1.png -------------------------------------------------------------------------------- /waffle/D3/layout.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | d3.grid = function() { 3 | var mode = 'equal', 4 | layout = _distributeEqually, 5 | x = d3.scaleOrdinal(), 6 | y = d3.scaleOrdinal(), 7 | size = [1, 1], 8 | actualSize = [0, 0], 9 | nodeSize = false, 10 | bands = false, 11 | padding = [0, 0], 12 | cols, 13 | rows; 14 | 15 | function grid(nodes) { 16 | return layout(nodes); 17 | } 18 | 19 | function _distributeEqually(nodes) { 20 | var i = -1, 21 | n = nodes.length, 22 | _cols = cols ? cols : 0, 23 | _rows = rows ? rows : 0, 24 | col, 25 | row; 26 | 27 | if (_rows && !_cols) { 28 | _cols = Math.ceil(n / _rows); 29 | } else { 30 | _cols || (_cols = Math.ceil(Math.sqrt(n))); 31 | _rows || (_rows = Math.ceil(n / _cols)); 32 | } 33 | 34 | if (nodeSize) { 35 | x 36 | .domain(d3.range(_cols)) 37 | .range( 38 | d3.range(0, (size[0] + padding[0]) * _cols, size[0] + padding[0]) 39 | ); 40 | y 41 | .domain(d3.range(_rows)) 42 | .range( 43 | d3.range(0, (size[1] + padding[1]) * _rows, size[1] + padding[1]) 44 | ); 45 | actualSize[0] = bands ? x(_cols - 1) + size[0] : x(_cols - 1); 46 | actualSize[1] = bands ? y(_rows - 1) + size[1] : y(_rows - 1); 47 | } else if (bands) { 48 | var x = d3.scaleBand(); 49 | var y = d3.scaleBand(); 50 | x.domain(d3.range(_cols)).range([0, size[0]], padding[0], 0); 51 | y.domain(d3.range(_rows)).range([0, size[1]], padding[1], 0); 52 | actualSize[0] = x.bandwidth() - 10; 53 | actualSize[1] = y.bandwidth() - 10; 54 | } else { 55 | var x = d3.scalePoint(); 56 | var y = d3.scalePoint(); 57 | x.domain(d3.range(_cols)).range([0, size[0]]); 58 | y.domain(d3.range(_rows)).range([0, size[1]]); 59 | actualSize[0] = x(1); 60 | actualSize[1] = y(1); 61 | } 62 | 63 | while (++i < n) { 64 | col = i % _cols; 65 | row = Math.floor(i / _cols); 66 | nodes[i].x = x(col); 67 | nodes[i].y = y(row); 68 | } 69 | 70 | return nodes; 71 | } 72 | 73 | grid.size = function(value) { 74 | if (!arguments.length) return nodeSize ? actualSize : size; 75 | actualSize = [0, 0]; 76 | nodeSize = (size = value) == null; 77 | return grid; 78 | }; 79 | 80 | grid.nodeSize = function(value) { 81 | if (!arguments.length) return nodeSize ? size : actualSize; 82 | actualSize = [0, 0]; 83 | nodeSize = (size = value) != null; 84 | return grid; 85 | }; 86 | 87 | grid.rows = function(value) { 88 | if (!arguments.length) return rows; 89 | rows = value; 90 | return grid; 91 | }; 92 | 93 | grid.cols = function(value) { 94 | if (!arguments.length) return cols; 95 | cols = value; 96 | return grid; 97 | }; 98 | 99 | grid.bands = function() { 100 | bands = true; 101 | return grid; 102 | }; 103 | 104 | grid.points = function() { 105 | bands = false; 106 | return grid; 107 | }; 108 | 109 | grid.padding = function(value) { 110 | if (!arguments.length) return padding; 111 | padding = value; 112 | return grid; 113 | }; 114 | 115 | return grid; 116 | }; 117 | })(); 118 | -------------------------------------------------------------------------------- /waffle/D3/script.js: -------------------------------------------------------------------------------- 1 | // set config object 2 | const config = { 3 | width: 500, 4 | height: 400, 5 | mobileWidth: 300, 6 | mobileHeight: 300, 7 | }; 8 | const isMobile = window.innerWidth < 600 ? true : false; 9 | 10 | const dataModel = [ 11 | { 12 | color: '#254251', 13 | number: 450, 14 | }, 15 | { 16 | color: '#e0ab26', 17 | number: 100, 18 | }, 19 | { 20 | color: '#ddd', 21 | number: 350, 22 | }, 23 | ]; 24 | 25 | const margin = { top: 50, right: 120, bottom: 50, left: 30 }, 26 | width = 27 | (isMobile ? config.mobileWidth : config.width) - margin.left - margin.right, 28 | height = 29 | (isMobile ? config.mobileHeight : config.height) - 30 | margin.top - 31 | margin.bottom; 32 | 33 | // Clean up before drawing 34 | // By brutally emptying all HTML from plot container div 35 | d3.select('#times-waffle').html(''); 36 | 37 | const svg = d3 38 | .select('#times-waffle') 39 | .at({ 40 | width: isMobile ? config.mobileWidth : config.width, 41 | height: isMobile ? config.mobileHeight : config.height, 42 | }) 43 | .st({ backgroundColor: '#F8F7F1' }); 44 | 45 | // g is our main container 46 | const g = svg.append('g').translate([margin.left, margin.top]); 47 | 48 | let seats = []; 49 | for (var i = 0; i < dataModel.length; i++) { 50 | for (var y = 0; y < dataModel[i].number; y++) { 51 | seats.push({ color: dataModel[i].color }); 52 | } 53 | } 54 | 55 | const grid = d3 56 | .grid() 57 | .points() 58 | .size([width, height]) 59 | .cols(30) 60 | .rows(30); 61 | 62 | const points = g.selectAll('.circles').data(grid(seats)); 63 | points 64 | .enter() 65 | .append('circle') 66 | .at({ 67 | class: '.circles', 68 | cx: d => d.x, 69 | cy: d => d.y, 70 | r: 1e-6, 71 | fill: d => d.color, 72 | }) 73 | .transition() 74 | .duration(100) 75 | .delay((d, i) => i * 3) 76 | .at({ r: isMobile ? 3 : 4 }) 77 | .style('opacity', 1); 78 | 79 | const yScale = d3 80 | .scalePoint() 81 | .domain(d3.range(30)) 82 | .range([height, 0]); 83 | g.append('line').at({ 84 | x1: -10, 85 | y1: yScale(10) - 5, 86 | x2: width + 10, 87 | y2: yScale(10) - 5, 88 | stroke: '#4d4d4d', 89 | strokeWidth: '1px', 90 | }); 91 | 92 | g 93 | .append('text') 94 | .at({ 95 | class: 'label', 96 | x: width + 20, 97 | y: yScale(10), 98 | }) 99 | .text('300 things') 100 | .at({ 101 | fontFamily: 'sans-serif', 102 | fill: '#4d4d4d', 103 | }) 104 | .style('opacity', '1'); 105 | -------------------------------------------------------------------------------- /waffle/D3/style.css: -------------------------------------------------------------------------------- 1 | svg { 2 | margin: 0 auto; 3 | padding: 15px; 4 | background-color: #f7f8f1; 5 | display: flex; 6 | } 7 | 8 | .tick line { 9 | stroke: #dbdbdb; 10 | stroke-dasharray: 5; 11 | } 12 | 13 | path.domain { 14 | stroke: #dbdbdb; 15 | stroke-width: 1px; 16 | } 17 | 18 | circle.circle { fill: #254251; stroke: #F9F8F3; } 19 | -------------------------------------------------------------------------------- /waffle/README.md: -------------------------------------------------------------------------------- 1 | # Waffle chart 2 | 3 | ![](1.png) 4 | 5 | An alternative to the pie chart to represent proportions and shares. 6 | 7 | ## Data format 8 | 9 | #### for d3 10 | 11 | A simple array would do and would draw `array.length` elements 12 | 13 | ``` 14 | ['one rectangle', 'two rectangles', ...] 15 | ``` 16 | 17 | In this example we pass an array of objects to store some properties, such as wether to highlight the squares or circles 18 | 19 | ``` 20 | [ 21 | { color: '#ddd' }, 22 | { color: 'blue'}, 23 | ] 24 | ``` 25 | 26 | 27 | ## Download and edit 28 | 29 | Install the [SVG Crowbar](http://nytimes.github.io/svg-crowbar/) by dragging the bookmarklet on this page to your bookmarks bar. Click the bookmarklet to download an Illustrator-ready SVG. 30 | -------------------------------------------------------------------------------- /waffle/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 21 | 22 |
23 |
Waffle
24 |
25 | 26 |
27 |

An alternative to donut and pie charts when it comes to representing shares and proportions. 28 |

29 |
30 | 31 | 34 | 35 | 36 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | --------------------------------------------------------------------------------