block by jonsadka d29662f900a3c1a590d17b9cb5261c02

Catan Ranking

Full Screen

A bump chart for wins in Catan wins by player

forked from puripant‘s block: Gold Medal Ranking in SEA Games 1959-2017

index.html

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

main.css

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

main.js

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);
});

medals.csv

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