Files
cs5890_data_visualization/hw5/js/table.js
2019-10-15 22:08:23 -06:00

278 lines
9.2 KiB
JavaScript

/** Class implementing the table. */
class Table {
/**
* Creates a Table Object
*/
constructor(teamData, treeObject) {
// Maintain reference to the tree Object;
console.log(treeObject);
this.tree = treeObject;
// Create list of all elements that will populate the table
// Initially, the tableElements will be identical to the teamData
this.tableElements = teamData.slice(0); //
// Store all match data for the 2014 Fifa cup
this.teamData = teamData;
// Default values for the Table Headers
this.tableHeaders = ["Delta Goals", "Result", "Wins", "Losses", "TotalGames"];
// To be used when sizing the svgs in the table cells.
this.cell = {
"width": 70,
"height": 20,
"buffer": 15
};
this.bar = {
"height": 20
};
// Set variables for commonly accessed data columns
this.goalsMadeHeader = 'Goals Made';
this.goalsConcededHeader = 'Goals Conceded';
// Setup the scales
this.goalScale = null;
// Used for games/wins/losses
this.gameScale = null;
// Color scales
// For aggregate columns Use colors '#ece2f0', '#016450' for the range.
this.aggregateColorScale = null;
// For goal Column. Use colors '#cb181d', '#034e7b' for the range.
this.goalColorScale = null;
}
/**
* Creates a table skeleton including headers that when clicked allow
* you to sort the table by the chosen attribute.
* Also calculates aggregate values of goals, wins, losses and total
* games as a function of country.
*/
createTable() {
let teamData = this.teamData;
// ******* TODO: PART II *******
// Update Scale Domains
this.goalScale = d3.scaleLinear()
.domain([d3.min(teamData, d => d.value['Goals Made']), d3.max(teamData, d => d.value['Goals Made'])])
.range([0,2*this.cell.width])
.nice();
this.aggregateColorScale = d3.scaleLinear()
.domain([0, d3.max(teamData, d => d.value['TotalGames'])])
.range(["#ece2f0","#016450"])
this.gameScale = d3.scaleLinear()
.domain([0, d3.max(teamData, d => d.value['TotalGames'])])
.range([0,this.cell.width]);
// Create the x axes for the goalScale.
let goalAxis = d3.axisTop();
goalAxis.scale(this.goalScale);
// Add GoalAxis to header of col 1.
d3.select("#goalHeader").append('svg')
.attr('width', 2 * this.cell.width + 2 * this.cell.buffer)
.attr('height', this.cell.height)
.append('g')
.attr("transform", "translate(10,16)")
.call(goalAxis)
// ******* TODO: PART V *******
// Set sorting callback for clicking on headers
// Clicking on headers should also trigger collapseList() and
// updateTable().
}
/**
* Updates the table contents with a row for each element in the global
* variable tableElements.
*/
updateTable() {
// ******* TODO: PART III *******
// Create table rows
let tableRow = d3.select('#matchTable').select('tbody')
.selectAll('tr')
.data(this.tableElements);
tableRow.exit()
.remove();
tableRow = tableRow.enter()
.append('tr')
.on('click', (d,i) => {
this.updateList(i);
this.updateTable();
})
.on('mouseover', (d,i) => {
this.tree.updateTree(this.tableElements[i]);
})
.on('mouseout', () => {
this.tree.clearTree();
})
.merge(tableRow);
//Append th elements for the Team Names
let tableHeader = tableRow.selectAll("th")
.data(d => { return[{'type': d.value['type'], 'view': 'text', 'value': d.key}] });
tableHeader.exit().remove();
tableHeader = tableHeader.enter()
.append('th')
.merge(tableHeader);
//Append td elements for the remaining columns.
let tableData = tableRow.selectAll("td")
.data(d => {
return[{'type': d.value['type'], 'view': 'goals', 'value': {'goalsMade':d.value['Goals Made'], 'goalsConceded' : d.value['Goals Conceded']}},
{'type': d.value['type'], 'view': 'text', 'value': d.value['Result']},
{'type': d.value['type'], 'view': 'bar', 'value': d.value['Wins']},
{'type': d.value['type'], 'view': 'bar', 'value': d.value['Losses']},
{'type': d.value['type'], 'view': 'bar', 'value': d.value['TotalGames']}
] });
tableData.exit()
.remove();
tableData = tableData.enter()
.append('td')
.merge(tableData);
tableHeader.text(d => {
return d.type==='aggregate' ? d.value : 'x'+d.value;})
.style('color', d => { return d.type==='aggregate' ? 'black' : 'gray';});
//Populate cells (do one type of cell at a time )
let textColumn = tableData.filter(d => { return d.view === 'text'});
textColumn.select('svg').remove();
let textBody = textColumn.append('svg')
.attr("width", this.cell.width*1.6)
.attr("height", this.cell.height);
textBody.append("text")
.attr("x", 0)
.attr("y", this.cell.height/2)
.text(d => {
return d.value['label']
});
let goalsColumn = tableData.filter(d => { return d.view === 'goals'});
goalsColumn.select('svg').remove();
let goalsBody = goalsColumn.append('svg')
.attr("width", this.cell.width * 2 + this.cell.buffer)
.attr("height", this.cell.height)
goalsBody.append('rect')
.classed('goalBar',true)
.attr('fill', d => { return d.value['goalsMade'] > d.value['goalsConceded'] ? 'blue' : 'red';})
.attr('width', d => {
if(d.type==='aggregate') {
return Math.abs(this.goalScale(d.value['goalsMade'] - d.value['goalsConceded']));
}
else {
return Math.abs(this.goalScale(d.value['goalsMade'] - d.value['goalsConceded']))-10;
}
})
.attr('height', d => { return d.type==='aggregate' ? 10 : 5;})
.attr('x', d => {
return d.type === 'aggregate'
? d.value['goalsMade'] > d.value['goalsConceded']
? this.goalScale(d.value['goalsConceded'])+6
:this.goalScale(d.value['goalsMade'])+6
: d.value['goalsMade'] > d.value['goalsConceded']
? this.goalScale(d.value['goalsConceded'])+12
: this.goalScale(d.value['goalsMade'])+12;
})
.attr('y', d => { return d.type === 'aggregate' ? 5 : 7.5
});
goalsBody.append('circle')
.classed("goalCircle", true)
.attr('fill', d => { return d.type === 'aggregate' ? 'blue' : 'none';})
.attr('stroke', d => {
return d.type === 'aggregate' ? 'none' : d.value['goalsConceded']===d.value['goalsMade'] ? 'gray' : 'blue';})
.attr('cy', 10)
.attr('cx', d => { return this.goalScale(d.value['goalsMade'])+6})
.attr('r',5);
goalsBody.append('circle')
.classed("goalCircle", true)
.attr('fill', d => {return d.type === 'aggregate' ? 'red' : 'none';})
.attr('stroke', d => {
return d.type === 'aggregate' ? 'none' : d.value['goalsConceded'] === d.value['goalsMade'] ? "gray" : "red";
})
.attr('cy', 10)
.attr('cx', d => { return this.goalScale(d.value['goalsConceded'])+6})
.attr('r',5);
let barColumns = tableData.filter(d => {return d.view === 'bar'});
barColumns.style("padding-left", "0px")
barColumns.select('svg').remove();
let barBody = barColumns.append('svg').attr("width", this.cell.width).attr("height", this.cell.height);
barBody.append('rect')
.attr('width', d => { return this.gameScale(d.value)})
.attr('height', this.bar.height)
//
.attr('x', 0)
.attr('fill', d => {
return this.aggregateColorScale(d.value);});
barBody.append('text')
.text(d => {return d.value})
.classed("label", true)
.attr("x", d => { return this.gameScale(d.value)-10})
.attr("y",15);
};
/**
* Updates the global tableElements variable, with a row for each row
* to be rendered in the table.
*/
updateList(i) {
// ******* TODO: PART IV *******
let teamList = this.tableElements[i].value.games;
if(i != this.tableElements.length-1 ) {
if(this.tableElements[i+1].value['type']=='aggregate') {
for (let j=0; j<teamList.length; j++) {
this.tableElements.splice(j+i+1, 0, teamList[j])
}
}
else {
this.tableElements.splice(i+1, teamList.length)
}
}
else if(this.tableElements[i].value['type']=='aggregate'){
for (let j=0; j<teamList.length; j++) {
this.tableElements.splice(j+i+1, 0, teamList[j])
}
}
// Only update list for aggregate clicks, not game clicks
}
/**
* Collapses all expanded countries, leaving only rows for aggregate
* values per country.
*/
collapseList() {
// ******* TODO: PART IV *******
}
}