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