Published
Edited
Nov 13, 2021
1 star
Insert cell
# Chess Elo Over Time
Insert cell
import {aq, op} from "@uwdata/arquero"
Insert cell
import {Scrubber} from "@mbostock/scrubber"
Insert cell
Insert cell
elodata = FileAttachment("data@15.json").json()
Insert cell
years = d3.extent(elodata.map(d => d[0]), d => d.year)
Insert cell
height = 500
Insert cell
barHeight = 30
Insert cell
margin = ({top: 90, right: 20, bottom: 30, left: 50})
Insert cell
max_elo = 2880
Insert cell
min_elo = 2660
Insert cell
x = d3.scaleLinear()
.domain([min_elo, max_elo])
.range([margin.left, width - margin.right])
.nice()
Insert cell
y = d3.scaleLinear()
.domain([10, 1])
.range([height - margin.bottom, margin.top])
.nice()
Insert cell
color = d3.scaleOrdinal()
.domain(elodata.map(d => d.country))
.range(["#9edae5", "#17becf", "#dbdb8d", "#bcbd22", "#c7c7c7", "#7f7f7f", "#f7b6d2", "#e377c2", "#c49c94", "#8c564b", "#c5b0d5", "#9467bd", "#ff9896", "#d62728", "#98df8a", "#2ca02c", "#ffbb78", "#ff7f0e", "#aec7e8", "#1f77b4",]) // try other schemes, too!
Insert cell
viewof yearAnimate = Scrubber(
d3.range(parseInt(years[0]), parseInt(years[1]) + 1, 1), // min to max years in 1 year increments
{ autoplay: false, delay: 1500, loop: false } // experiment with these settings!
)
Insert cell
chartAnimate = {
function getFilteredDataByYear(year) {
const filteredData = elodata.map(d => d[0])
.filter(d => parseInt(d.year) === parseInt(year))
.filter(d => parseInt(d.rating) >= min_elo)
.sort((a, b) => parseInt(b.rating) - parseInt(a.rating))
.filter((d, i) => i < 10);
return filteredData;
}

// chart
const svg = d3.create('svg')
.attr('width', width)
.attr('height', height);
// title
svg.append("text")
.attr("x", width / 2)
.attr("y", margin.top / 3)
.attr("text-anchor", "middle")
.style("font-size", "20px")
.style("text-decoration", "bold")
.text("Chess Elo Over The Past 20 Years");

// x-axis
svg.append('g')
.attr('transform', `translate(0, ${margin.top})`)
.call(d3.axisTop(x))
.append('text')
.attr('text-anchor', 'middle')
.attr('fill', 'black')
.attr('font-size', '16px')
.attr('x', width / 2)
.attr('y', -30)
.text('Elo Rating');

// y-axis
svg.append('g')
.attr('transform', `translate(${margin.left}, ${barHeight / 2})`)
.call(d3.axisLeft(y))
.append('text')
.attr('transform', `translate(-30, ${height / 2}) rotate(-90)`)
.attr('text-anchor', 'end')
.attr('fill', 'black')
.attr('font-size', '16px')
.text('Player Ranking');
svg.append('line') // janky fix for y-axis style
.style("stroke", "black")
.style("stroke-width", 1)
.attr("x1", margin.left)
.attr("y1", margin.top)
.attr("x2", margin.left)
.attr("y2", height);

// year label - bottom right
const yearLabel = svg.append('text')
.attr('class', 'year')
.attr('x', width - 200)
.attr('y', height - margin.bottom)
.attr('fill', '#ccc')
.attr('font-family', 'Helvetica Neue, Arial')
.attr('font-weight', 500)
.attr('font-size', 80)
.text(years[0]);

// bars
let players = svg
.selectAll('rect')
.data(getFilteredDataByYear(years[0]))
.enter().append('rect')
.attr('class', 'player')
.attr('opacity', 0.85)
.attr('fill', d => color(d.country))
.attr('y', (d, i) => y(i + 1))
.attr('x', margin.left)
.attr('width', d => x(parseInt(d.rating) - 8))
.attr('height', barHeight)
.style('font-size', 'small');

// player name label for each bar chart
let names = svg
.selectAll(".names")
.data(getFilteredDataByYear(years[0]))
.join("text")
.attr('text-anchor', 'end')
.attr('fill', '#ffffff')
.attr('font-size', 16)
.attr('font-family', 'Helvetica Neue, Arial')
.attr('y', (d, i) => y(i + 1) + 20)
.attr('x', d => margin.left + x(parseInt(d.rating) - 8) - 3)
.text(d => d.name);

// elo rating label for each bar chart
let ratings = svg
.selectAll(".ratings")
.data(getFilteredDataByYear(years[0]))
.join("text")
.attr('text-anchor', 'start')
.attr('fill', '#aaaaaa')
.attr('font-size', 16)
.attr('font-family', 'Helvetica Neue, Arial')
.attr('y', (d, i) => y(i + 1) + 20)
.attr('x', d => margin.left + x(parseInt(d.rating) - 8) + 3)
.text(d => d.rating);
updateInfo(years[0]);
function updateInfo(year){
players.selectAll('title').remove();
players.data(getFilteredDataByYear(year))
.append('title')
.text((d) => "Name: " + d.name + "\nCountry : " + d.country + "\nRating: " + d.rating);
players
.on('mouseover', function() {
d3.select(this).attr('stroke', '#333').attr('stroke-width', 2);
})
.on('mouseout', function() {
d3.select(this).attr('stroke', null);
});
}
function setYear(year) {
yearLabel.text(year);
players = players.data(getFilteredDataByYear(year), d => d.name)
.join(
enter => enter.append('rect')
.attr('class', 'player')
.attr('opacity', 0.0)
.attr('y', (d, i) => y(i + 1))
.attr('x', margin.left)
.attr('width', d => x(parseInt(d.rating) - 8))
.attr('height', barHeight),
update => update,
exit => exit.remove()
);
players.transition().duration(1000).ease(d3.easeCubic)
.attr('opacity', 0.75)
.attr('fill', d => color(d.country))
.attr('y', (d, i) => y(i + 1))
.attr('x', margin.left)
.attr('width', d => x(parseInt(d.rating) - 8))
.attr('height', barHeight)
.style('font-size', 'small');

names = names.data(getFilteredDataByYear(year), d => d.name)
.join(
enter => enter.append("text")
.attr('text-anchor', 'end')
.attr('fill', '#ffffff')
.attr('font-size', 16)
.attr('font-family', 'Helvetica Neue, Arial')
.attr('y', (d, i) => y(i + 1) + 20)
.attr('x', d => margin.left + x(parseInt(d.rating) - 8) - 3)
.text(d => d.name),
update => update,
exit => exit.remove()
);

names.transition().duration(1000).ease(d3.easeCubic)
.attr('text-anchor', 'end')
.attr('fill', '#ffffff')
.attr('font-size', 16)
.attr('font-family', 'Helvetica Neue, Arial')
.attr('y', (d, i) => y(i + 1) + 20)
.attr('x', d => margin.left + x(parseInt(d.rating) - 8) - 3)
.text(d => d.name);

ratings = ratings.data(getFilteredDataByYear(year), d => d.name)
.join(
enter => enter.append("text")
.attr('text-anchor', 'start')
.attr('fill', '#aaaaaa')
.attr('font-size', 16)
.attr('font-family', 'Helvetica Neue, Arial')
.attr('y', (d, i) => y(i + 1) + 20)
.attr('x', d => margin.left + x(parseInt(d.rating) - 8) + 3)
.text(d => d.rating),
update => update,
exit => exit.remove()
);

ratings.transition().duration(1000).ease(d3.easeCubic)
.attr('text-anchor', 'start')
.attr('fill', '#aaaaaa')
.attr('font-size', 16)
.attr('font-family', 'Helvetica Neue, Arial')
.attr('y', (d, i) => y(i + 1) + 20)
.attr('x', d => margin.left + x(parseInt(d.rating) - 8) + 3)
.text(d => d.rating);
updateInfo(year);
}
return Object.assign(svg.node(), { setYear });
}
Insert cell
chartAnimate.setYear(yearAnimate)
Insert cell
md`
# WRITE UP
Your deployed webpage should also include a write-up with the following components:

**Design Decision: ** Add a rationale for your design decisions.
- How did you choose your particular visual encodings and interaction techniques?
- inspiration from videos showing chess players rankings overtime moving up and down
- keeping it simple with a bar chart
- year filter using scrubber
- What alternatives did you consider and how did you arrive at your ultimate choices?
- Plotting the top ~1000 player's elo on a line chart with some low opacity+an opaque mean line and then support filtering based on nationality/gender for interactivity.
- We could also render a kernel density estimate or histogram when mousing over the graphic for a specific time slice.
- instead of barchart, a line chart that maps player elo ratings and rankings overtime
- not enough time to explore these options

**Development Process: ** Describe how the work was split among the team members.
- John worked on data wranglin. Connor, Kevin, Phuong contributed on coding up d3.
- Include a commentary on the development process, including answers to the following questions:
- Roughly how much time did you spend developing your application (in people-hours)?
- 7 hours on first day, 3 on second day, 4 on third day
- What aspects took the most time?
- data wrangling to make sure data was accurate
- debugging d3 whenever trying to add new chart elements, animations and other functionalities

You can either include your write-up on the same page as your visualization or link to a separate file (also hosted on GitHub pages from your A3 repo).

Things to consider:
- The data that we used does not remove retired players, i.e. retired players' elos remain the same even after they retire. This means that our top 10 list is the current top 10 FIDE rated players in the given year including retired players. In the future we might want to update this visual to only include top 10 active players.
- Also, the data we used was "dirty" and had many mistakes that we needed to clean up. This is unavoidable in most cases, but should be noted in case inaccuracies arise in the visualization.
- A feature that is not outright visible in the visualization is a hovering tooltip for each player/bar in the graph. When the user hovers over a bar, a tooltip with the name, country, and exact rating of the player will pop up.

## V1
**Design Decision: ** Add a rationale for your design decisions.

Inspired by videos showing the rankings and elos of the best chess players overtime, we decided we want to create an interactive visualization that aim to do the same. In our exploration phase, we considered several interactive visualization designs. One design would plot the top ~1000 player's elo on a line chart with a low opacity line for each player's elo rating overtime, with an opaque mean (elo rating) line included, and in which we would add interactive elements that would support filtering on year, player's gender, age, and nationality. Another design would include a rendering of a kernel density estimate of elo ratings on mouseover events for a specific time slice. However, we could not find quality datasets to support all of these desired functionalities. Many datasets were limited in the time period covered, or just included very barebone data attributes such as name, elo rating, and nationality. We also faced many roadblocks when dealing with d3 due to our inexperience working with the library, thus there was a large time constraint that forced us to scale down the scope of our visualization.

We ended up deciding to make a simple bar chart sorted by player elo ratings, showing the top ten player (there was a cap on file size that can be imported into observable notebook), and the interactive elements include filtering by year, and mouseover events for more details shown in the tooltip. We also decided to forgo a legend that would support filtering by nationality since our visualization already have color encodings tied to nationalities. We also included animations (up and down transitions) to show the change of players rankings year over year.

If we had more time and experience with d3, we would have certainly wanted to implement previously mentioned ideas or change this current visualization to be a line chart that plots players' elo ratings to a changing x-axis marking year that is updated overtime to show the rise and fall or retirement of players. This would make it easier to compare career or elo rating trajectories of players overtime, which cannot be encoded by the current bar chart visualization.

**Development Process: ** Describe how the work was split among the team members.

We first explored many different datasets to use and decided to use the International Chess Federation's (known as FIDE) provided dataset with data going back to 2000. John Li handled the data wrangling, aggregating many datasets into one, validating data accuracy, reformatting data into JSON formats, performed cleanup of data whenever we encountered problems with the provided data. There are some notable flaws of the dataset such as the inclusion of retired players' elo ratings, i.e., retired players' elos remain the same even after they retire. This means that our top 10 list is the current top 10 FIDE rated players in the given year including retired players. In the future we might want to update this visual to only include top 10 active players by manually filtering out retired players for data in years after their retirement. We also encountered many problems such as missing data over many periods of months, as well as inconsistencies in player names and elo ratings.

We then utilized the provided code in the D3 demo notebook to form the foundation for our code for the visualization. Connor, Kevin, and Phuong worked together to adapt the base code to form our visualization for our dataset, and added time/elo filters to data, text labels for each bar or data points, tooltip on mouseover events, animations for transitions of player rankings year over year (as well as other text label elements for consistency). We worked together to debug our D3 code, as well as validate data inaccuracies whenever we spot any.

We then all worked together to complete this write up.

We worked approximately 7 hours on first day, 3 on second day, 4 on third day, totaling 14 hours.

We all agreed that the two most time-consuming parts was data wrangling and debugging D3 code to fix minor visual alignment issues. It is incredibly time consuming to wrangle data and validate data when dealing with less than subpar datasets. It is also very time consuming to learn to use a new technology and use it well. The gulf of execution is much larger when working with a visualization grammar like D3, and it is frustrating at times when it is difficult to realize a simple conceptual model. However, we do realize that there is power that comes with finer specificity controls when working with D3.
`
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more