Skip to content

Instantly share code, notes, and snippets.

@basilesimon
Created November 24, 2017 16:04
Show Gist options
  • Save basilesimon/7802f6498ca5904c92041312876ace3e to your computer and use it in GitHub Desktop.
Save basilesimon/7802f6498ca5904c92041312876ace3e to your computer and use it in GitHub Desktop.
Treemap
license: mit
{
"name": "transfers_position_in",
"children": [
{
"name": "Defender",
"children": [
{
"name": "Cohen Bramall",
"fee": "0.04",
"fromto": "Hednesford Town to Arsenal",
"position": "Defender",
"id": "transfers_position_in.Defender.Cohen Bramall"
},
{
"name": "Robbie Brady",
"fee": "13.00",
"fromto": "Norwich City to Burnley",
"position": "Defender",
"id": "transfers_position_in.Defender.Robbie Brady"
},
{
"name": "Jeffrey Schlupp",
"fee": "12.50",
"fromto": "Leicester City to Crystal Palace",
"position": "Defender",
"id": "transfers_position_in.Defender.Jeffrey Schlupp"
},
{
"name": "Patrick v. Aanholt",
"fee": "14.00",
"fromto": "Sunderland to Crystal Palace",
"position": "Defender",
"id": "transfers_position_in.Defender.Patrick v. Aanholt"
},
{
"name": "Mamadou Sakho",
"fee": "0.00",
"fromto": "Liverpool to Crystal Palace",
"position": "Defender",
"id": "transfers_position_in.Defender.Mamadou Sakho"
},
{
"name": "Omar Elabdellaoui",
"fee": "0.00",
"fromto": "Olympiacos to Hull City",
"position": "Defender",
"id": "transfers_position_in.Defender.Omar Elabdellaoui"
},
{
"name": "Andrea Ranocchia",
"fee": "0.00",
"fromto": "Inter Milan to Hull City",
"position": "Defender",
"id": "transfers_position_in.Defender.Andrea Ranocchia"
},
{
"name": "Molla Wague",
"fee": "0.00",
"fromto": "Granada CF to Leicester City",
"position": "Defender",
"id": "transfers_position_in.Defender.Molla Wague"
},
{
"name": "Joleon Lescott",
"fee": "0.00",
"fromto": "AEK Athens to Sunderland",
"position": "Defender",
"id": "transfers_position_in.Defender.Joleon Lescott"
},
{
"name": "Bryan Oviedo",
"fee": "3.75",
"fromto": "Everton to Sunderland",
"position": "Defender",
"id": "transfers_position_in.Defender.Bryan Oviedo"
},
{
"name": "Martin Olsson",
"fee": "4.50",
"fromto": "Norwich City to Swansea City",
"position": "Defender",
"id": "transfers_position_in.Defender.Martin Olsson"
},
{
"name": "Marc Wilson",
"fee": "0.00",
"fromto": "Bournemouth to West Bromwich Albion",
"position": "Defender",
"id": "transfers_position_in.Defender.Marc Wilson"
},
{
"name": "José Fonte",
"fee": "8.00",
"fromto": "Southampton to West Ham United",
"position": "Defender",
"id": "transfers_position_in.Defender.José Fonte"
},
{
"name": "Nathan Holland",
"fee": "0.00",
"fromto": "Everton to West Ham United",
"position": "Defender",
"id": "transfers_position_in.Defender.Nathan Holland"
}
],
"id": "transfers_position_in.Defender"
},
{
"name": "Goalkeeper",
"children": [
{
"name": "Aaron Ramsdale",
"fee": "0.04",
"fromto": "Sheffield United to Bournemouth",
"position": "Goalkeeper",
"id": "transfers_position_in.Goalkeeper.Aaron Ramsdale"
},
{
"name": "Mouez Hassen",
"fee": "0.00",
"fromto": "OGC Nice to Southampton",
"position": "Goalkeeper",
"id": "transfers_position_in.Goalkeeper.Mouez Hassen"
},
{
"name": "Lee Grant",
"fee": "1.30",
"fromto": "Derby County to Stoke City",
"position": "Goalkeeper",
"id": "transfers_position_in.Goalkeeper.Lee Grant"
}
],
"id": "transfers_position_in.Goalkeeper"
},
{
"name": "Midfielder",
"children": [
{
"name": "Joey Barton",
"fee": "0.00",
"fromto": "Rangers to Burnley",
"position": "Midfielder",
"id": "transfers_position_in.Midfielder.Joey Barton"
},
{
"name": "Ashley Westwood",
"fee": "5.00",
"fromto": "Aston Villa to Burnley",
"position": "Midfielder",
"id": "transfers_position_in.Midfielder.Ashley Westwood"
},
{
"name": "Luka Milivojevic",
"fee": "13.60",
"fromto": "Olympiacos to Crystal Palace",
"position": "Midfielder",
"id": "transfers_position_in.Midfielder.Luka Milivojevic"
},
{
"name": "Morgan Schneiderlin",
"fee": "24.00",
"fromto": "Manchester United to Everton",
"position": "Midfielder",
"id": "transfers_position_in.Midfielder.Morgan Schneiderlin"
},
{
"name": "Evandro Goebel",
"fee": "2.13",
"fromto": "Porto to Hull City",
"position": "Midfielder",
"id": "transfers_position_in.Midfielder.Evandro Goebel"
},
{
"name": "Markus Henriksen",
"fee": "4.25",
"fromto": "AZ Alkmaar to Hull City",
"position": "Midfielder",
"id": "transfers_position_in.Midfielder.Markus Henriksen"
},
{
"name": "Lazar Markovic",
"fee": "0.00",
"fromto": "Liverpool to Hull City",
"position": "Midfielder",
"id": "transfers_position_in.Midfielder.Lazar Markovic"
},
{
"name": "Alfred N'Diaye",
"fee": "0.00",
"fromto": "Villarreal to Hull City",
"position": "Midfielder",
"id": "transfers_position_in.Midfielder.Alfred N'Diaye"
},
{
"name": "Wilfred Ndidi",
"fee": "15.00",
"fromto": "Genk to Leicester City",
"position": "Midfielder",
"id": "transfers_position_in.Midfielder.Wilfred Ndidi"
},
{
"name": "Adlene Guedioura",
"fee": "0.00",
"fromto": "Watford to Middlesbrough",
"position": "Midfielder",
"id": "transfers_position_in.Midfielder.Adlene Guedioura"
},
{
"name": "Darron Gibson",
"fee": "3.75",
"fromto": "Everton to Sunderland",
"position": "Midfielder",
"id": "transfers_position_in.Midfielder.Darron Gibson"
},
{
"name": "Luciano Narsingh",
"fee": "4.00",
"fromto": "PSV Eindhoven to Swansea City",
"position": "Midfielder",
"id": "transfers_position_in.Midfielder.Luciano Narsingh"
},
{
"name": "Tom Carroll",
"fee": "4.50",
"fromto": "Tottenham Hotspur to Swansea City",
"position": "Midfielder",
"id": "transfers_position_in.Midfielder.Tom Carroll"
},
{
"name": "Tom Cleverley",
"fee": "0.00",
"fromto": "Everton to Watford",
"position": "Midfielder",
"id": "transfers_position_in.Midfielder.Tom Cleverley"
},
{
"name": "Jake Livermore",
"fee": "10.00",
"fromto": "Hull City to West Bromwich Albion",
"position": "Midfielder",
"id": "transfers_position_in.Midfielder.Jake Livermore"
},
{
"name": "Robert Snodgrass",
"fee": "10.20",
"fromto": "Hull City to West Ham United",
"position": "Midfielder",
"id": "transfers_position_in.Midfielder.Robert Snodgrass"
}
],
"id": "transfers_position_in.Midfielder"
},
{
"name": "Striker",
"children": [
{
"name": "Ademola Lookman",
"fee": "7.50",
"fromto": "Charlton Athletic to Everton",
"position": "Striker",
"id": "transfers_position_in.Striker.Ademola Lookman"
},
{
"name": "Oumar Niasse",
"fee": "0.00",
"fromto": "Everton to Hull City",
"position": "Striker",
"id": "transfers_position_in.Striker.Oumar Niasse"
},
{
"name": "Kamil Grosicki",
"fee": "7.00",
"fromto": "Stade Rennais to Hull City",
"position": "Striker",
"id": "transfers_position_in.Striker.Kamil Grosicki"
},
{
"name": "Gabriel Jesus",
"fee": "27.20",
"fromto": "Palmeiras to Manchester City",
"position": "Striker",
"id": "transfers_position_in.Striker.Gabriel Jesus"
},
{
"name": "Rudy Gestede",
"fee": "6.00",
"fromto": "Aston Villa to Middlesbrough",
"position": "Striker",
"id": "transfers_position_in.Striker.Rudy Gestede"
},
{
"name": "Patrick Bamford",
"fee": "10.00",
"fromto": "Chelsea to Middlesbrough",
"position": "Striker",
"id": "transfers_position_in.Striker.Patrick Bamford"
},
{
"name": "Manolo Gabbiadini",
"fee": "14.45",
"fromto": "Napoli to Southampton",
"position": "Striker",
"id": "transfers_position_in.Striker.Manolo Gabbiadini"
},
{
"name": "Saido Berahino",
"fee": "15.00",
"fromto": "West Bromwich Albion to Stoke City",
"position": "Striker",
"id": "transfers_position_in.Striker.Saido Berahino"
},
{
"name": "Jordan Ayew",
"fee": "0.00",
"fromto": "Aston Villa to Swansea City",
"position": "Striker",
"id": "transfers_position_in.Striker.Jordan Ayew"
},
{
"name": "Mauro Zárate",
"fee": "2.30",
"fromto": "Fiorentina to Watford",
"position": "Striker",
"id": "transfers_position_in.Striker.Mauro Zárate"
},
{
"name": "M'Baye Niang",
"fee": "0.00",
"fromto": "AC Milan to Watford",
"position": "Striker",
"id": "transfers_position_in.Striker.M'Baye Niang"
}
],
"id": "transfers_position_in.Striker"
}
],
"id": "transfers_position_in"
}
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://unpkg.com/d3"></script>
<script src="https://unpkg.com/d3-jetpack"></script>
<style>
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
svg {
margin: 0 auto;
display: flex;
background-color: #f8f7f1;
}
text, .text {
font: 15px arial;
fill: white;
color: white;
}
.d3-tip {
line-height: 1;
font: 15px arial;
fill: white;
color: white;
z-index: 1000;
}
.legendTitle text {
font: 18px Times New Roman;
fill: black;
}
.playerInfoTitle text {
font-size: 30px;
font-family: Times New Roman;
fill: black;
}
.playerInfo text {
font: 15px arial;
fill: #696969;
&.mobile {
font: 20px serif;
fill: #1d1d1d;
}
}
.playerNamesFee {
font-size: 15px;
fill: rgba(255,255,255, .6)
}
</style>
</head>
<body>
<svg id="chart"></svg>
<script>
// set config object
const config = { width: 700, height: 450 };
const margin = { top: 20, left: 20, right: 150 };
const width = config.width - margin.left - margin.right,
height = config.height - margin.top;
const svg = d3.select('#chart').at({
width: config.width,
height: config.height,
});
// construct an ordinal scale from our colour palette
const timesColors = ['#254251', '#E0AB26', '#F37F2F', '#3292A6', '#6c3c5e'];
const color = d3.scaleOrdinal(timesColors);
const format = d3.format(',d');
d3.json('data.json', (err, dataset) => {
if (err) {
console.log(err);
return;
}
const treemap = d3
.treemap()
.tile(d3.treemapResquarify)
.size([width, height])
.round(true)
.paddingOuter(2)
.paddingInner(1);
const root = d3
.hierarchy(dataset)
.eachBefore(
d => (d.data.id = (d.parent ? d.parent.data.id + '.' : '') + d.data.name)
)
.sum(sumBySize)
.sort((a, b) => b.height - a.height || b.value - a.value);
treemap(root);
// One cell per player
const container = svg.append('g').at({ class: 'container' });
const cell = container
.selectAll('g')
.data(root.leaves())
.enter()
.append('g')
.translate(d => [d.x0, d.y0]);
cell
.append('rect')
.at({
id: d => d.data.id,
class: d => (d.x1 - d.x0 > 120 && d.y1 - d.y0 > 40 ? 'wide' : null),
width: d => d.x1 - d.x0,
height: d => d.y1 - d.y0,
fill: d => color(d.parent.data.id),
})
.on('mouseover', function(d) {
var _this = this;
d3
.selectAll('rect')
.transition()
.duration(100)
.style('opacity', function() {
return this === _this ? 1.0 : 0.6;
});
})
.on('mouseout', function(d) {
d3.selectAll('rect').transition().duration(500).style('opacity', 1);
})
.on('click', function(d) {
appendPlayerInfo(this, d);
});
// Player names
cell
.append('text')
.attr('clip-path', d => 'url(#clip-' + d.data.id + ')')
.append('tspan')
.at({
x: 8,
y: 8,
dy: '.8em',
class: 'playerNames',
})
.text(function(d) {
// Only display text if sibling <rect> element is wide enough
const parentRect = this.parentNode.previousElementSibling;
if (d3.select(parentRect).classed('wide')) {
return d.data.name;
}
});
cell
.append('text')
.attr('clip-path', d => 'url(#clip-' + d.data.id + ')')
.append('tspan')
.at({
x: 8,
y: 20,
dy: '1.2em',
class: 'playerNamesFee',
})
.text(function(d) {
// Only display text if sibling <rect> element is wide enough
const parentRect = this.parentNode.parentNode.firstElementChild;
if (d3.select(parentRect).classed('wide')) {
return '£' + d.data.fee.split('.')[0] + 'm';
}
});
// 💩 Manual labelling
const key = [
{ name: 'Europe', color: '#254251' },
{ name: 'Britain', color: '#E0AB26' },
{ name: 'Africa', color: '#3292A6' },
{ name: 'S. America', color: '#F37F2F' },
];
let legendConfig = {
x: width + 10,
y: 10,
height: 20,
};
// Create a legend element
const legend = container
.append('g')
.at({ class: 'legendContainer' })
.selectAll('g')
.data(key)
.enter()
.append('g')
.at({ class: 'legend' })
.attr('transform', (d, i) => {
const leftmargin = 0;
const topmargin = 0;
const x = leftmargin + legendConfig.x;
const y = i * legendConfig.height + legendConfig.y + topmargin;
return 'translate(' + x + ',' + y + ')';
});
const legendTitle = container
.append('g')
.at({ class: 'legendTitle' })
.attr('transform', (d, i) => {
const height = 20;
const x = legendConfig.x;
const y = i * height + legendConfig.y;
return 'translate(' + x + ',' + y + ')';
});
legendTitle.append('text').at({ x: 0, y: 20 }).text('Key');
legend
.append('rect')
.at({
width: 10,
height: 10,
})
.translate([0, 30])
.st({
fill: d => d.color,
stroke: d => d.color,
});
legend
.append('text')
.at({
x: 20,
y: 40,
})
.st({ color: '#666', fill: '#666' })
.text(d => d.name);
const linewidth = config.width < 400 ? width - 20 : 100;
const lineheight = config.width < 400 ? 90 : 110;
legendTitle.append('line').at({
x1: 0,
x2: linewidth,
y1: 0,
y2: 0,
strokeWidth: 2,
stroke: '#ddd',
});
legendTitle.append('line').at({
x1: 0,
x2: linewidth,
y1: lineheight,
y2: lineheight,
strokeWidth: 2,
stroke: '#ddd',
});
// Create a legend element
const titleheight = height;
const titlemargin = 30;
const playerInfoContainer = svg
.append('g')
.attr('class', 'playerInfoContainer')
.selectAll('g')
.data(key)
.enter()
.append('g')
.attr('class', 'playerInfo')
.translate((d, i) => {
return [legendConfig.x, i * titleheight + 10];
});
const playerInfoTitle = svg
.append('g')
.attr('class', 'playerInfoTitle')
.translate([legendConfig.x, titleheight * 0.3 + titlemargin]);
playerInfoTitle.append('text').attr('x', 0).attr('y', 0);
const playerInfo = svg
.append('g')
.attr('class', 'playerInfo')
.translate([legendConfig.x, titleheight * 0.3 + titlemargin + 30]);
playerInfo
.append('text')
.at({
class: 'playerInfo',
x: 0,
y: 10,
})
.tspans(() => d3.wordwrap('Tap an area for more information', 20));
// Appends player info on click on a rect
const appendPlayerInfo = (obj, data) => {
playerInfoTitle.html('');
playerInfo.html('');
playerInfo.append('text').at({ x: 0, y: 0, class: 'playerInfo' });
playerInfoTitle
.append('text')
.at({ y: 0 })
.text('£' + data.data.fee.split('.')[0] + 'm');
playerInfo
.append('text')
.at({ y: -25 })
.tspans(() => {
const { name, fromto } = data.data;
return d3.wordwrap(name + ' ' + fromto, 15);
})
.attr('dy', (d, i) => i + 15);
};
});
const sumBySize = d => d.fee;
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment