A bump chart for wins in Catan wins by player
forked from puripant‘s block: Gold Medal Ranking in SEA Games 1959-2017
<html>
<head>
<meta charset="utf-8">
<title>Catan Ranking</title>
<link href="main.css" rel="stylesheet">
</head>
<body>
<canvas></canvas>
<svg></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<!-- <script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script> -->
<script src="main.js"></script>
</body>
</html>
body {
background: #fff;
font: 12px sans-serif;
}
svg,
canvas {
position: absolute;
}
.axis path,
.axis line {
fill: none;
stroke: #d0d0d0;
}
.x.axis .tick text {
font: 10px sans-serif;
fill: #555;
}
.guide {
stroke: #555;
}
const numberOfPlayers = 4;
const colorLeadersCount = 3;
const margin = {top: 35, right: 70, bottom: 30, left: 70};
const width = 950,
height = 500;
const devicePixelRatio = window.devicePixelRatio || 1;
const canvas = d3.select("canvas")
.attr("width", width * devicePixelRatio)
.attr("height", height * devicePixelRatio)
.style("width", width + "px")
.style("height", height + "px");
const svg = d3.select("svg")
.style("width", width + "px")
.style("height", height + "px");
const color = d3.scaleOrdinal()
.range(["#DB7F85", "#50AB84", "#4C6C86", "#C47DCB", "#B59248", "#DD6CA7", "#E15E5A", "#5DA5B3", "#725D82", "#54AF52", "#954D56"]);
var xScale = d3.scaleOrdinal()
var xAxisLeft = d3.axisBottom()
.tickFormat(d3.timeFormat("%b %e"));
var xAxisRight = d3.axisTop()
.tickFormat(d3.timeFormat("%b %e"));
var yScale = d3.scaleLinear()
.domain([0 - 0.2, numberOfPlayers - 0.5])
.range([margin.top, height-margin.bottom]);
var radius = d3.scaleSqrt()
.domain([0, 0.1])
.range([0, 4]);
d3.csv("medals.csv", (error, data) => {
const hostHouse = {}; // Find host countries by date
data.forEach(d => {
d.points = +d.points;
d.date = +d.date;
if (d.host === "y") {
hostHouse[d.date] = d.name;
}
});
// nest by name and rank by total popularity
const nested = d3.nest()
.key(d => d.name)
.rollup(leaves => ({
data: leaves,
sum: d3.sum(leaves, d => d.points)
}))
.entries(data)
.sort((a, b) => d3.descending(a.value.sum, b.value.sum))
const topnames = nested.slice(0, numberOfPlayers).map(d => d.key);
data = data.filter(d => topnames.indexOf(d.name) > -1);
// nest by name and rank by total popularity
window.byDate = {}
d3.nest()
.key(d => d.date)
.key(d => d.name)
// .sortValues(function(a, b) { return a.points - b.points; })
.rollup((leaves, i) => leaves[0].points)
.entries(data)
.forEach(date => {
byDate[date.key] = {};
date.values
.sort((a, b) => d3.descending(a.value, b.value))
.forEach((name, i) => {byDate[date.key][name.key] = i});
});
const dates = Object.keys(hostHouse).map(d => +d);
xScale
.domain(Object.keys(hostHouse))
.range(new Array(dates.length).fill('').map((d, idx) =>
idx * width / (dates.length + 1) + margin.left
))
xAxisLeft.scale(xScale).tickValues(dates);
xAxisRight.scale(xScale).tickValues(dates);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (height - margin.bottom) + ")")
.call(xAxisLeft);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (margin.top - 10) + ")")
.call(xAxisRight);
// Vertical guide line
const hiddenMargin = 100;
let highlightedYear;
var verticalGuide = svg.append("line")
.attr("class", "guide")
.attr("x1", -hiddenMargin)
.attr("y1", margin.top - 10)
.attr("x2", -hiddenMargin)
.attr("y2", height - margin.bottom)
.style("stroke-width", () => xScale(2) - xScale(0)) //two date interval
.style("opacity", 0);
const mouseTrap = svg.append("rect")
.attr("width", width)
.attr("height", height)
.style("opacity", 0)
.on("mouseover", () => { verticalGuide.style("opacity", 0.1); })
.on("mouseout", () => { verticalGuide.style("opacity", 0); })
.on("mousemove", () => {
const mousex = d3.mouse(this)[0]
const x = xScale.invert(mousex);
let found = false;
for (let i = 0; i < dates.length; i++) {
if (Math.abs(dates[i] - x) <= 1) { // game interval (2 dates) in half
highlightedYear = dates[i];
found = true;
break;
}
}
if (!found) {
highlightedYear = null;
}
mouseTrap.style("cursor", highlightedYear? "pointer" : "auto");
verticalGuide.attr("transform", "translate(" + (xScale(highlightedYear)+hiddenMargin) + ", 0)");
});
var ctx = canvas.node().getContext("2d");
ctx.scale(devicePixelRatio, devicePixelRatio);
// Draw a circle for each host country
const countrySumRank = nested.map(d => d.key);
for (var date in hostHouse) {
if (countrySumRank.indexOf(hostHouse[date]) < colorLeadersCount) {
ctx.fillStyle = color(hostHouse[date]);
} else {
ctx.fillStyle = "#888";
}
ctx.beginPath();
ctx.arc(xScale(date), yScale(byDate[date][hostHouse[date]]), 5, 0, 2 * Math.PI);
ctx.fill();
ctx.closePath();
}
nested.slice(0, numberOfPlayers).reverse().forEach((name, idx) => {
var datespopular = name.value.data;
if (idx >= numberOfPlayers - colorLeadersCount) {
ctx.globalAlpha = 0.85;
ctx.strokeStyle = color(name.key);
ctx.lineWidth = 2.5;
} else {
ctx.globalAlpha = 0.55;
ctx.strokeStyle = "#888";
ctx.lineWidth = 1;
}
// bump line
ctx.globalCompositeOperation = "darken";
ctx.lineCap = "round";
datespopular.forEach((d, jdx) => {
if (jdx > 0) {
const previousDate = datespopular[jdx-1].date;
ctx.beginPath();
const missedLastGame = false
if (missedLastGame) { //skipping games
ctx.setLineDash([5, 10]);
} else {
ctx.setLineDash([]);
}
ctx.moveTo(xScale(previousDate), yScale(byDate[previousDate][name.key]))
// ctx.lineTo(xScale(d.date), yScale(byDate[d.date][name.key]));
ctx.bezierCurveTo(
xScale(previousDate)+15, yScale(byDate[previousDate][name.key]),
xScale(d.date)-15, yScale(byDate[d.date][name.key]),
xScale(d.date), yScale(byDate[d.date][name.key]));
// ctx.closePath();
ctx.stroke();
}
});
});
ctx.textAlign = "right";
ctx.textBaseline = "middle";
ctx.font = "10px sans-serif";
nested.slice(0, numberOfPlayers).reverse().forEach((name, i) => {
const datespopular = name.value.data;
if (i >= numberOfPlayers - colorLeadersCount) {
ctx.fillStyle = color(name.key);
} else {
ctx.fillStyle = "#555";
}
ctx.globalCompositeOperation = "source-over";
ctx.globalAlpha = 0.9;
// start names
ctx.save();
ctx.textAlign = "end";
const start = datespopular[0].date;
const x = xScale(start)-10;
const y = yScale(byDate[start][name.key]);
ctx.fillText(name.key, x, y);
ctx.restore();
// end names
ctx.textAlign = "start";
const end= datespopular[datespopular.length-1].date;
ctx.fillText(name.key, xScale(end)+10, yScale(byDate[end][name.key]));
});
// legend
var legendPos = {x: width*0.12, y: height*0.78};
ctx.fillStyle = "#888";
ctx.beginPath();
ctx.arc(legendPos.x, legendPos.y, 5, 0, 2*Math.PI);
ctx.fill();
ctx.closePath();
ctx.textAlign = "start";
ctx.fillText("marks the day when that player hosts.", legendPos.x + 10, legendPos.y - 1);
});
date,name,points,host
1563692400000,Jon,10,y
1563692400000,DCA,6,n
1563692400000,Adrian,5,n
1563692400000,Myrna,8,n
1564815600000,Myrna,10,n
1564815600000,Jon,9,n
1564815600000,DCA,7,y
1564815600000,Adrian,6,n
1564902000000,Myrna,10,y
1564902000000,Jon,0,n
1564902000000,DCA,0,n
1564902000000,Adrian,0,y
1564902000001,Myrna,0,y
1564902000001,Jon,0,n
1564902000001,DCA,0,n
1564902000001,Adrian,10,y
1564902000002,Myrna,0,y
1564902000002,Jon,10,n
1564902000002,DCA,0,n
1564902000002,Adrian,0,y
1569049200000,Myrna,6,n
1569049200000,Jon,10,n
1569049200000,DCA,4,y
1569049200000,Adrian,6,n
1569049200001,Myrna,6,n
1569049200001,Jon,10,n
1569049200001,DCA,5,y
1569049200001,Adrian,5,n
1570345200000,Myrna,10,n
1570345200000,Jon,7,y
1570345200000,DCA,4,n
1570345200000,Adrian,4,n
1570687552847,Myrna,8,y
1570687552847,Jon,10,n
1570687552847,DCA,7,n
1570687552847,Adrian,7,y