Skip to content

Commit

Permalink
Merge pull request #1 from sharadbhat/alex
Browse files Browse the repository at this point in the history
d3 classes used for visualizations
  • Loading branch information
sharadbhat authored Nov 12, 2022
2 parents 7529b61 + 1486109 commit 911e09c
Show file tree
Hide file tree
Showing 6 changed files with 522 additions and 0 deletions.
106 changes: 106 additions & 0 deletions src/classes/CumulativeLaps.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
class CumulativeRecordedLaps{
constructor(divId, lapData, driverData, defaultRaceId='845', dependencies){
this.divId = divId;
this.dependencies = dependencies;

//set SVG attributes/groups
d3.select('#' + this.divId).attr("style", "padding: 0");
this.SVGWidth = parseInt(d3.select('#' + this.divId).style("width"));
this.SVGHeight = this.SVGWidth/2;
this.scaleFactor = (this.SVGWidth/759);
d3.select('#' + this.divId)
.attr("width", this.SVGWidth)
.attr("height", this.SVGHeight);
d3.select('#' + this.divId).append("g").attr("id", "cumulativeLapGroup");
d3.select('#' + this.divId).append("g").attr("id", "cumulativeLapXAxis");
d3.select('#' + this.divId).append("g").attr("id", "cumulativeLapYAxis");
d3.select('#' + this.divId).append("g").attr("id", "cumulativeLapLine");
d3.select('#' + this.divId).on("mousemove", function(pos){
this.dependencies.recordedLaps.updateSlider(this.dependencies.recordedLaps.SVGWidth / this.SVGWidth * pos.offsetX);
this.updateLine(pos.offsetX)
}.bind(this));

//set scales
this.xScale = null;
this.yScale = null;

//grouped data items
this.groupedLapData = d3.group(lapData, d => d.raceId);
this.groupedDriverData = d3.group(driverData, d => d.driverId);
this.currentData = null;

//update chart data
this.updateDataPoints(defaultRaceId);
this.updateScatterPlot();
}

updateDataPoints(raceId){
//modify currentData
this.currentData = structuredClone(this.groupedLapData.get(raceId));
this.currentData = d3.flatGroup(this.currentData, d => d.driverId);

//make cumulative
for (let driver of this.currentData){
driver[1].forEach(function (e) {
this.count = (this.count || 0) + parseInt(e.milliseconds);
e.milliseconds = parseFloat(this.count);
}, Object.create(null));
}
}

updateScatterPlot(){
let maxLap = d3.max(this.currentData, driverData => d3.max(driverData[1], d => parseFloat(d.lap)));
let minLap = d3.min(this.currentData, driverData => d3.min(driverData[1], d => parseFloat(d.lap)));
let maxLapTime = d3.max(this.currentData, driverData => d3.max(driverData[1], d => parseFloat(d.milliseconds)));
let minLapTime = d3.min(this.currentData, driverData => d3.min(driverData[1], d => parseFloat(d.milliseconds)));

//set scales
this.xScale = d3.scaleLinear()
.domain([minLap, maxLap])
.range([0, this.SVGWidth]);
this.yScale = d3.scaleLinear()
.domain([minLapTime, maxLapTime])
.range([this.SVGHeight, 0]);

//set color mapping
let colorScale = d3.scaleOrdinal()
.domain(Array.from(this.groupedDriverData.keys()))
.range(d3.schemeCategory10);

//plot data
let line = d3.line()
.x(d => this.xScale(parseInt(d.lap)))
.y(d => this.yScale(parseInt(d.milliseconds)));
d3.select("#cumulativeLapGroup").selectAll("path").data(this.currentData).join(
enter => enter
.append("path")
.attr("fill", "none")
.attr("d", d => line(d[1]))
.attr("stroke", d => colorScale(d[0]))
.on("mousemove", function(pos, data){
console.log(pos, data);
}.bind(this)),
update => update
.attr("d", d => line(d[1]))
.attr("stroke", d => colorScale(d[0])),
exit => exit
.remove()
);
}

updateLine(xValue){
let currentLap = xValue;
d3.select('#' + this.divId).select("#cumulativeLapLine").selectAll("line").data([currentLap]).join(
enter => enter
.append("line")
.attr("y1", 0)
.attr("y2", this.SVGHeight)
.attr("stroke", "black"),
update => update
.attr("x1", d => d)
.attr("x2", d => d),
exit => exit
.remove()
);
}
}
3 changes: 3 additions & 0 deletions src/classes/RacerList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/*class RacerList{
constructor(divId, )
}*/
142 changes: 142 additions & 0 deletions src/classes/RecordedLaps.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
class RecordedLaps{
constructor(divId, listId, lapData, driverData, defaultRaceId='845', dependencies){
this.divId = divId;
this.listId = listId;
this.ANIMATION_DURATION = 300;

//set SVG attributes/groups
d3.select('#' + this.divId).attr("style", "padding: 0");
this.SVGWidth = parseInt(d3.select('#' + this.divId).style("width"));
this.SVGHeight = this.SVGWidth;
this.scaleFactor = (this.SVGWidth/759);
d3.select('#' + this.divId)
.attr("width", this.SVGWidth)
.attr("height", this.SVGHeight);
d3.select('#' + this.divId).append("rect").attr("width", "100%").attr("height", "100%").attr("fill","#f5f5f5");
d3.select('#' + this.divId).append("g").attr("id", "scatterGroup");
d3.select('#' + this.divId).append("g").attr("id", "scatterXAxis");
d3.select('#' + this.divId).append("g").attr("id", "scatterYAxis");
d3.select('#' + this.divId).append("g").attr("id", "scatterSlider");
d3.select('#' + this.listId).append("g").attr("id", "currentRaceListText");

//set scales
this.xScale = null;
this.yScale = null;

//grouped data items
this.groupedLapData = d3.group(lapData, d => d.raceId);
this.groupedDriverData = d3.group(driverData, d => d.driverId);
this.currentData = null;

//update chart data
this.updateDataPoints(defaultRaceId);
this.updateScatterPlot();
}

updateDataPoints(raceId){
//modify currentData
this.currentData = structuredClone(this.groupedLapData.get(raceId));
}

updateSlider(xValue){
let currentLap = Math.ceil(this.xScale.invert(xValue));

//update slider
d3.select('#' + this.divId).select("#scatterSlider").selectAll("rect").data([currentLap]).join(
enter => enter
.append("rect")
.attr("id", "slideRect")
.attr("fill", "black")
.attr("opacity", "0.5")
.attr("x", this.xScale(currentLap) - this.scaleFactor * 5)
.attr("y", 0)
.attr("height", this.SVGHeight)
.attr("width", this.xScale(2) * this.scaleFactor),
update => update
.attr("x", this.xScale(currentLap) - this.scaleFactor * 5),
exit => exit
.remove()
);

//update list values
let racersByLap = d3.filter(this.currentData, function(d){
return parseInt(d.lap) === currentLap;
}.bind(this));
racersByLap = racersByLap.sort((a, b) => parseInt(a.position) - parseInt(b.position));
console.log(racersByLap);
d3.select("#currentRaceListText").selectAll("g").data(racersByLap).join(
function(enter){
let group = enter.append("g");
group.append("rect")
.attr("x", 0)
.attr("y", (d,i) => i * 17)
.attr("height", "17")
.attr("width", "100%")
.attr("fill", "gray"),//colorScale(d.driverId))
group.append("text")
.attr("x", 0)
.attr("y", (d,i) => i * 17 + 16)
.text(d => this.groupedDriverData.get(d.driverId)[0].driverRef + " " + d.position);
}.bind(this),
update => update
.select("text")
.text(d => this.groupedDriverData.get(d.driverId)[0].driverRef + " " + d.position),
exit => exit
.remove()
);

}

updateScatterPlot(){
let maxLap = d3.max(this.currentData, d => parseInt(d.lap));
let minLap = d3.min(this.currentData, d => parseInt(d.lap));
let maxLapTime = d3.max(this.currentData, d => parseInt(d.milliseconds));
let minLapTime = d3.min(this.currentData, d => parseInt(d.milliseconds));

//set color mapping
let colorScale = d3.scaleOrdinal()
.domain(Array.from(this.groupedDriverData.keys()))
.range(d3.schemeCategory10);

//update scales
this.xScale = d3.scaleLinear()
.domain([minLap, maxLap])
.range([5 * this.scaleFactor, this.SVGWidth - 5 * this.scaleFactor]);
this.yScale = d3.scaleLinear()
.domain([minLapTime, maxLapTime])
.range([this.SVGHeight, 0]);

//update slider
d3.select('#' + this.divId).on("mousemove", function(pos){
this.updateSlider(pos.offsetX);
}.bind(this))

//plot data
d3.select("#scatterGroup").selectAll("circle").data(this.currentData).join(
enter => enter
.append("circle")
.attr("cx", d => this.xScale(parseInt(d.lap)))
.attr("cy", d => this.yScale(parseInt(d.milliseconds)))
.attr("opacity", function(d){
let filteredData = d3.filter(this.currentData, score => score.lap === d.lap);
let lapMedian = d3.median(filteredData, d1 => d1.milliseconds);
let difference = Math.abs(lapMedian - Math.abs(d.milliseconds - parseInt(lapMedian)));

let returnVal = Math.pow(1.0 * (difference / lapMedian), 64);
return returnVal < 0.2 ? 0.2 : returnVal;
}.bind(this))
.attr("fill", d => colorScale(d.driverId))
.attr("r", this.scaleFactor * 5),
update => update
.attr("cx", d => this.xScale(parseInt(d.lap)))
.attr("cy", d => this.yScale(parseInt(d.milliseconds)))
.attr("r", 0)
.transition().duration(this.ANIMATION_DURATION)
.attr("r", this.scaleFactor * 5),
exit => exit
.transition().duration(this.ANIMATION_DURATION)
.attr("r", 0)
.remove()
);
}
}
112 changes: 112 additions & 0 deletions src/classes/WorldGrid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
class WorldGrid{
constructor(divId, worldId, worldData, raceData, circuitData, defaultYear='2000'){
//grouped data
this.raceData = d3.group(raceData, d => d.year);
this.circuitData = d3.group(circuitData, d => d.circuitId);
this.currentData = null;
this.divId = divId;
this.worldId = worldId;
this.ANIMATION_DURATION = 300;

//set SVG attributes/groups
d3.select('#' + this.divId).attr("style", "padding: 0");
this.SVGWidth = parseInt(d3.select('#' + this.divId).style("width"));
this.SVGHeight = this.SVGWidth/2;
this.scaleFactor = (this.SVGWidth/759);
d3.select('#' + this.divId)
.attr("width", this.SVGWidth)
.attr("height", this.SVGHeight)
.append("g").attr("id", "tileGroup");

//create gradient
let gradient = d3.select("#tileGroup").append("defs")
.append("linearGradient")
.attr("id", "gridGradient")
.attr("gradientTransform", "rotate(90)");
gradient.append("stop")
.attr("offset", "5%")
.attr("stop-color", "#808080")
gradient.append("stop")
.attr("offset", "95%")
.attr("stop-color", "#434343")

//initialize data
this.updateData(defaultYear);
}

updateData(currentYear){
this.updateDataPoints(currentYear);
this.updateChart();
}

updateDataPoints(currentYear){
//modify currentData
let newCurrentData = new Array();
for (let race of this.raceData.get(currentYear)){
let appendedData = structuredClone(this.circuitData.get(race.circuitId)[0]);
appendedData.raceId = race.raceId;
newCurrentData.push(appendedData);
}
this.currentData = newCurrentData;
}

updateChart(){
let newLineVal = Math.ceil(Math.sqrt(this.currentData.length));
let widthScale = d3.scaleLinear()
.domain([0, newLineVal])
.range([0, this.SVGWidth]);
let heightScale = d3.scaleLinear()
.domain([0, newLineVal])
.range([0, this.SVGHeight]);

d3.select("#tileGroup").selectAll("svg").data(this.currentData).join(
function(enter){
let SVG = enter.append("svg")
.attr("x", (d,i) => widthScale((i % newLineVal)))
.attr("y", (d,i) => heightScale((Math.floor(i / newLineVal))))
.attr("width", this.SVGWidth/newLineVal)
.attr("height", this.SVGHeight/newLineVal);
SVG.append("rect")
.attr("width", "100%")
.attr("height", "100%")
.attr("rx", 10 * this.scaleFactor)
.attr("ry", 10 * this.scaleFactor)
.attr("fill", "url('#gridGradient')")
.attr("stroke", "black")
.on("mouseover", function(event, data){
d3.select('#' + this.worldId).select("#locationGroup").selectAll("circle")
.transition().duration(this.ANIMATION_DURATION)
.attr("r", function(d){
if (d.circuitId === data.circuitId)
return 10 * this.scaleFactor;
else
return 5 * this.scaleFactor;
}.bind(this));
d3.select(event.path[0])
.attr("fill", "white");
}.bind(this))
.on("mouseout", function(event){
d3.select('#' + this.worldId).select("#locationGroup").selectAll("circle")
.transition().duration(this.ANIMATION_DURATION)
.attr("r", 5 * this.scaleFactor);
d3.select(event.path[0])
.attr("fill", "url('#gridGradient')");
}.bind(this));
SVG.append("text")
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.attr("pointer-events", "none")
.attr("font-weight", "bold")
.style("font-size", 15 * this.scaleFactor)
.attr("x", "50%")
.attr("y", "50%")
.text(d => d.location);
}.bind(this),
update => update
.select("text")
.text(d => d.location),
exit => exit
.remove()
);
}
}
Loading

0 comments on commit 911e09c

Please sign in to comment.