Skip to content

Instantly share code, notes, and snippets.

@jonsadka
Last active October 10, 2019 06:11
Show Gist options
  • Save jonsadka/d29662f900a3c1a590d17b9cb5261c02 to your computer and use it in GitHub Desktop.
Save jonsadka/d29662f900a3c1a590d17b9cb5261c02 to your computer and use it in GitHub Desktop.
Catan Ranking
license: gpl-3.0
<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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment