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 |
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 | 
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 | 
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 | 
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 | 
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 | 
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 |
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 | 
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 |
--------------------------------------------------------------------------------