Top 20 Mythic Plus Run Statistics
Data Analysis
The top 20 Mythic Plus runs are fascinating to examine, if you take a closer look. For example, let's take a look at the scores vs. the rankings to start with:
Click for d3 code.
function drawBarChart(selector, xrangestart, yrangestart, yrangeend, datasource, xaxis, yaxis) { // set the dimensions and margins of the graph const margin = {top: 30, right: 30, bottom: 70, left: 60}, width = 460 - margin.left - margin.right, height = 400 - margin.top - margin.bottom; // append the svg object to the body of the page const svg = d3.select(selector) .append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", `translate(${margin.left},${margin.top})`); // Parse the Data d3.json(datasource).then( function(data) { // step down a level data = data.rankings; // X axis const x = d3.scaleBand() .range([ xrangestart, width ]) .domain(data.map(d => d[xaxis])) .padding(0.2); svg.append("g") .attr("transform", `translate(0, ${height})`) .call(d3.axisBottom(x)) .selectAll("text") .attr("transform", "translate(-10,0)rotate(-45)") .style("text-anchor", "end"); // Add Y axis const y = d3.scaleLinear() .domain([yrangestart, yrangeend]) .range([ height, 0]); svg.append("g") .call(d3.axisLeft(y)); // Bars svg.selectAll("mybar") .data(data) .join("rect") .attr("x", d => x(d[xaxis])) .attr("y", d => y(d[yaxis])) .attr("width", x.bandwidth()) .attr("height", d => height - y(d[yaxis])) .attr("fill", "#0074d9") })}
As you can see, the scores are fairly homogenous for the top 20 Mythic runs, as of 07/23/2023
Click for d3 code.
function drawBarChart(selector, xrangestart, yrangestart, yrangeend, datasource, xaxis, yaxis) { // set the dimensions and margins of the graph const margin = {top: 30, right: 30, bottom: 70, left: 60}, width = 460 - margin.left - margin.right, height = 400 - margin.top - margin.bottom; // append the svg object to the body of the page const svg = d3.select(selector) .append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", `translate(${margin.left},${margin.top})`); // Parse the Data d3.json(datasource).then( function(data) { // step down a level data = data.rankings; // X axis const x = d3.scaleBand() .range([ xrangestart, width ]) .domain(data.map(d => d[xaxis])) .padding(0.2); svg.append("g") .attr("transform", `translate(0, ${height})`) .call(d3.axisBottom(x)) .selectAll("text") .attr("transform", "translate(-10,0)rotate(-45)") .style("text-anchor", "end"); // Add Y axis const y = d3.scaleLinear() .domain([yrangestart, yrangeend]) .range([ height, 0]); svg.append("g") .call(d3.axisLeft(y)); // Bars svg.selectAll("mybar") .data(data) .join("rect") .attr("x", d => x(d[xaxis])) .attr("y", d => y(d[yaxis])) .attr("width", x.bandwidth()) .attr("height", d => height - y(d[yaxis])) .attr("fill", "#0074d9") })}
However, if we adjust the scaling a bit, the difference between the top, and the top of the top, truly stands out.
Now, let's take a look at the meta as well -- how bad is it?
Click for d3 code.
function drawClassBarChart(selector, datasource) { // Some constants for the positioning const margin = {top: 30, right: 30, bottom: 70, left: 60}, width = 460 - margin.left - margin.right, height = 400 - margin.top - margin.bottom; // Parse the Data d3.json(datasource).then( function(data) { // step down a level data = data.rankings; // pull out an array of arrays of the characters for each run roster_array = data.map(d => { return [...new Map(d.run.roster.map(item => [item.character.id, item])).values()]}) // flatten the array to 100 character records let options_2 = Array.from(roster_array.values()).flat() // pull out individual character records let options = [...new Map(options_2.map(item => [item.character.id, item])).values()]; // roll up to the class count rollup_array = d3.rollup(options, v => v.length, d => d["character"]["class"]["slug"]); xScale = d3 .scaleBand() .domain(Array.from(rollup_array.keys())) .range([margin.left, width]) .padding(0.1) x1 = d3 .scaleBand() .domain(Array.from(rollup_array.keys())) .rangeRound([0, xScale.bandwidth()]) .padding(0.05) xAxis = d3.axisBottom(xScale).tickSizeOuter(0); yScale = d3 .scaleLinear() .domain([0, d3.max(rollup_array, d => d[1])]) // in each key, look for the maximum number .rangeRound([height, margin.top]) yAxis = d3.axisLeft(yScale).tickSizeOuter(0) const svg = d3.select(selector) .append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) // draw the bars svg .append("g") .selectAll("rect") .data(rollup_array) .join('rect') .attr("x", d => xScale(d[0])) .attr("y", d => yScale(d[1])) .attr("width", xScale.bandwidth()) .attr("height", d => yScale(0) - yScale(d[1])) .attr("fill", "#0074d9") // draw the x axis // nothing new here svg .append('g') .attr('class', 'x-axis') .attr('transform', `translate(0,${height})`) .call(xAxis) .selectAll("text") .attr("transform", "translate(-10,0)rotate(-45)") .style("text-anchor", "end"); // draw the y axis // nothing new here svg .append('g') .attr('class', 'y-axis') .attr('transform', `translate(${margin.left},0)`) .call(yAxis); // render the whole chart // nothing new here return svg.node(); }) }
Whew! That's something. Let's take a look at the spec breakdown as well.
Click for d3 code.
function drawSpecBarChart(selector, datasource) { const margin = {top: 30, right: 30, bottom: 70, left: 60}, width = 460 - margin.left - margin.right, height = 400 - margin.top - margin.bottom; // Parse the Data d3.json(datasource).then( function(data) { // step down a level data = data.rankings; // pull out an array of arrays of the characters for each run roster_array = data.map(d => { return [...new Map(d.run.roster.map(item => [item.character.id, item])).values()]}) // flatten the array to 100 character records let options_2 = Array.from(roster_array.values()).flat() // pull out individual character records let options = [...new Map(options_2.map(item => [item.character.id, item])).values()]; // roll up to the class count rollup_array = d3.rollup(options, v => v.length, d => d["character"]["spec"]["slug"]); xScale = d3 .scaleBand() .domain(Array.from(rollup_array.keys())) .range([margin.left, width]) .padding(0.1) x1 = d3 .scaleBand() .domain(Array.from(rollup_array.keys())) .rangeRound([0, xScale.bandwidth()]) .padding(0.05) xAxis = d3.axisBottom(xScale).tickSizeOuter(0); yScale = d3 .scaleLinear() .domain([0, d3.max(rollup_array, d => d[1])]) .rangeRound([height, margin.top]) yAxis = d3.axisLeft(yScale).tickSizeOuter(0) const svg = d3.select(selector) .append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) // draw the bars svg .append("g") .selectAll("rect") .data(rollup_array) .join('rect') .attr("x", d => xScale(d[0])) .attr("y", d => yScale(d[1])) .attr("width", xScale.bandwidth()) .attr("height", d => yScale(0) - yScale(d[1])) .attr("fill", "#0074d9") // draw the x axis // nothing new here svg .append('g') .attr('class', 'x-axis') .attr('transform', `translate(0,${height})`) .call(xAxis) .selectAll("text") .attr("transform", "translate(-10,0)rotate(-45)") .style("text-anchor", "end"); // draw the y axis // nothing new here svg .append('g') .attr('class', 'y-axis') .attr('transform', `translate(${margin.left},0)`) .call(yAxis); // render the whole chart // nothing new here return svg.node(); })}
Whew again! So, there's not much variation at the top this season. Some seasons, there's a smidge of variation, but not this time.
Now, let's take a look at the races that are being played:
Click for d3 code.
function drawRaceBarChart(selector, datasource) { const margin = {top: 30, right: 30, bottom: 70, left: 60}, width = 460 - margin.left - margin.right, height = 400 - margin.top - margin.bottom; // Parse the Data d3.json(datasource).then( function(data) { // step down a level data = data.rankings; // pull out an array of arrays of the characters for each run roster_array = data.map(d => { return [...new Map(d.run.roster.map(item => [item.character.id, item])).values()]}) // flatten the array to 100 character records let options_2 = Array.from(roster_array.values()).flat() // pull out individual character records let options = [...new Map(options_2.map(item => [item.character.id, item])).values()]; // roll up to the class count rollup_array = d3.rollup(options, v => v.length, d => d["character"]["race"]["slug"]); xScale = d3 .scaleBand() .domain(Array.from(rollup_array.keys())) .range([margin.left, width]) .padding(0.1) x1 = d3 .scaleBand() .domain(Array.from(rollup_array.keys())) .rangeRound([0, xScale.bandwidth()]) .padding(0.05) xAxis = d3.axisBottom(xScale).tickSizeOuter(0); yScale = d3 .scaleLinear() .domain([0, d3.max(rollup_array, d => d[1])]) .rangeRound([height, margin.top]) yAxis = d3.axisLeft(yScale).tickSizeOuter(0) const svg = d3.select(selector) .append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) // draw the bars svg .append("g") .selectAll("rect") .data(rollup_array) .join('rect') .attr("x", d => xScale(d[0])) .attr("y", d => yScale(d[1])) .attr("width", xScale.bandwidth()) .attr("height", d => yScale(0) - yScale(d[1])) .attr("fill", "#0074d9") // draw the x axis // nothing new here svg .append('g') .attr('class', 'x-axis') .attr('transform', `translate(0,${height})`) .call(xAxis) .selectAll("text") .attr("transform", "translate(-10,0)rotate(-45)") .style("text-anchor", "end"); // draw the y axis // nothing new here svg .append('g') .attr('class', 'y-axis') .attr('transform', `translate(${margin.left},0)`) .call(yAxis); // render the whole chart // nothing new here return svg.node(); })}
Oh ho! A tiny bit of variation there. Dwarf is not surprisingly the highest-played race; with the all-powerful Stoneform and other race-based traits, dwarves are quite powerful overall, if a bit under-utilized by the population at large.
Finally, let's take a look at the faction and server distribution:
Click for d3 code.
function drawFactionBarChart(selector, datasource) { const margin = {top: 30, right: 30, bottom: 70, left: 60}, width = 460 - margin.left - margin.right, height = 400 - margin.top - margin.bottom; // Parse the Data d3.json(datasource).then( function(data) { // step down a level data = data.rankings; // pull out an array of arrays of the characters for each run roster_array = data.map(d => { return [...new Map(d.run.roster.map(item => [item.character.id, item])).values()]}) // flatten the array to 100 character records let options_2 = Array.from(roster_array.values()).flat() // pull out individual character records let options = [...new Map(options_2.map(item => [item.character.id, item])).values()]; // roll up to the class count rollup_array = d3.rollup(options, v => v.length, d => d["character"]["faction"]); xScale = d3 .scaleBand() .domain(Array.from(rollup_array.keys())) .range([margin.left, width]) .padding(0.1) xAxis = d3.axisBottom(xScale).tickSizeOuter(0); yScale = d3 .scaleLinear() .domain([0, d3.max(rollup_array, d => d[1])]) .rangeRound([height, margin.top]) yAxis = d3.axisLeft(yScale).tickSizeOuter(0) const svg = d3.select(selector) .append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) // draw the bars svg .append("g") .selectAll("rect") .data(rollup_array) .join('rect') .attr("x", d => xScale(d[0])) .attr("y", d => yScale(d[1])) .attr("width", xScale.bandwidth()) .attr("height", d => yScale(0) - yScale(d[1])) .attr("fill", "#0074d9") // draw the x axis // nothing new here svg .append('g') .attr('class', 'x-axis') .attr('transform', `translate(0,${height})`) .call(xAxis) .selectAll("text") .attr("transform", "translate(-10,0)rotate(-45)") .style("text-anchor", "end"); // draw the y axis // nothing new here svg .append('g') .attr('class', 'y-axis') .attr('transform', `translate(${margin.left},0)`) .call(yAxis); // render the whole chart // nothing new here return svg.node(); })}
Click for d3 code.
function drawServerBarChart(selector, datasource) { const margin = {top: 30, right: 30, bottom: 70, left: 60}, width = 460 - margin.left - margin.right, height = 400 - margin.top - margin.bottom; // Parse the Data d3.json(datasource).then( function(data) { // step down a level data = data.rankings; // pull out an array of arrays of the characters for each run roster_array = data.map(d => { return [...new Map(d.run.roster.map(item => [item.character.id, item])).values()]}) // flatten the array to 100 character records let options_2 = Array.from(roster_array.values()).flat() // pull out individual character records let options = [...new Map(options_2.map(item => [item.character.id, item])).values()]; // roll up to the class count rollup_array = d3.rollup(options, v => v.length, d => d["character"]["realm"]["slug"]); xScale = d3 .scaleBand() .domain(Array.from(rollup_array.keys())) .range([margin.left, width]) .padding(0.1) xAxis = d3.axisBottom(xScale).tickSizeOuter(0); yScale = d3 .scaleLinear() .domain([0, d3.max(rollup_array, d => d[1])]) .rangeRound([height, margin.top]) yAxis = d3.axisLeft(yScale).tickSizeOuter(0) const svg = d3.select(selector) .append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) // draw the bars svg .append("g") .selectAll("rect") .data(rollup_array) .join('rect') .attr("x", d => xScale(d[0])) .attr("y", d => yScale(d[1])) .attr("width", xScale.bandwidth()) .attr("height", d => yScale(0) - yScale(d[1])) .attr("fill", "#0074d9") // draw the x axis // nothing new here svg .append('g') .attr('class', 'x-axis') .attr('transform', `translate(0,${height})`) .call(xAxis) .selectAll("text") .attr("transform", "translate(-10,0)rotate(-45)") .style("text-anchor", "end"); // draw the y axis // nothing new here svg .append('g') .attr('class', 'y-axis') .attr('transform', `translate(${margin.left},0)`) .call(yAxis); // render the whole chart // nothing new here return svg.node(); })}
Not surprisingly, given the popularity of dwarves this season, the Alliance wins out, though there is a strong Horde showing as well. The servers are a bit mixed, which is nice to see; though this certainly isn't representative, there are only so many players at the top, so there will only be so many servers as well.
As a general note, out of the 20 runs, there were 38 unique characters; their role distribution was as follows:
Click for d3 code.
function drawRoleBarChart(selector, datasource) { const margin = {top: 30, right: 30, bottom: 70, left: 60}, width = 460 - margin.left - margin.right, height = 400 - margin.top - margin.bottom; // Parse the Data d3.json(datasource).then( function(data) { // step down a level data = data.rankings; // pull out an array of arrays of the characters for each run roster_array = data.map(d => { return [...new Map(d.run.roster.map(item => [item.character.id, item])).values()]}) // flatten the array to 100 character records let options_2 = Array.from(roster_array.values()).flat() // pull out individual character records let options = [...new Map(options_2.map(item => [item.character.id, item])).values()]; // roll up to the class count rollup_array = d3.rollup(options, v => v.length, d => d["role"]); xScale = d3 .scaleBand() .domain(Array.from(rollup_array.keys())) .range([margin.left, width]) .padding(0.1) xAxis = d3.axisBottom(xScale).tickSizeOuter(0); yScale = d3 .scaleLinear() .domain([0, d3.max(rollup_array, d => d[1])]) .rangeRound([height, margin.top]) yAxis = d3.axisLeft(yScale).tickSizeOuter(0) const svg = d3.select(selector) .append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) // draw the bars svg .append("g") .selectAll("rect") .data(rollup_array) .join('rect') .attr("x", d => xScale(d[0])) .attr("y", d => yScale(d[1])) .attr("width", xScale.bandwidth()) .attr("height", d => yScale(0) - yScale(d[1])) .attr("fill", "#0074d9") // draw the x axis // nothing new here svg .append('g') .attr('class', 'x-axis') .attr('transform', `translate(0,${height})`) .call(xAxis) .selectAll("text") .attr("transform", "translate(-10,0)rotate(-45)") .style("text-anchor", "end"); // draw the y axis // nothing new here svg .append('g') .attr('class', 'y-axis') .attr('transform', `translate(${margin.left},0)`) .call(yAxis); // render the whole chart // nothing new here return svg.node(); })}
While this was not a very inspired data analysis (though still interesting!), I hope you'll watch this space as I expand my skills and begin tackling more interesting topics! Thanks for reading!
Code Analysis
This was my first time working with d3.js! I had spent much time building a Power BI dashboard to explore the relationships in the data, so I knew the general graphs I wanted to show, but I had never worked with d3 before, and was quite a noob when it came to putting together the graphs.
With the help of a plethora of Google searches, I put together some functional code, and whaddya know! It doesn't look half bad!
My goal is to build code that's as reusable as possible; though I did not succeed here (for the most part), I know it's only a matter of improving. Building more analyses will strengthen my skills, and I will have reusable, beautiful, original code in no time.
Acknowledgements
There are a few sources I absolutely could not have done this without. They are as follows:
- Array.prototype.flat()
- This primer on Highlight.js
- This explainer on collapsible Markdown elements
- The answer here on getting unique items from an array of objects
- This incredible piece on grouped bar charts with d3, from which most of the presentation code on this page was taken
- This piece from d3 on grouping and rollup functions, which was critical to the functionality of this code
- This piece reminding me how to snag the index inside the map function
- This answer reminding me of basic JavaScript object functionality (d'oh)
- And, of course, a huge thanks to Raider.io for making a public API