Created
March 15, 2012 01:19
-
-
Save cpudney/2040990 to your computer and use it in GitHub Desktop.
D3 Lap Chart 2.0
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"lapCount": 58, | |
"laps": [ | |
{ | |
"name": "Sebastian Vettel", | |
"placing": [1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], | |
"pitstops": [9], | |
"mechanical": [25] | |
}, | |
{ | |
"name": "Mark Webber", | |
"placing": [2, 3, 3, 3, 3, 3, 2, 2, 2, 1, 1, 6, 6, 6, 6, 6, 8, 8, 8, 8, 8, 8, 7, 7, 7, 7, 6, 6, 5, 5, 5, 5, 7, 7, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 8, 9, 9], | |
"pitstops": [10, 32, 56] | |
}, | |
{ | |
"name": "Fernando Alonso", | |
"placing": [3, 18, 18, 18, 18, 15, 13, 13, 15, 13, 10, 10, 10, 9, 8, 8, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 7, 7, 7, 7, 7, 7, 6, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4], | |
"pitstops": [8], | |
"accident": [0] | |
}, | |
{ | |
"name": "Jenson Button", | |
"placing": [4, 6, 6, 6, 6, 6, 19, 19, 12, 4, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], | |
"pitstops": [6, 34], | |
"accident": [0] | |
}, | |
{ | |
"name": "Felipe Massa", | |
"placing": [5, 2, 2, 2, 2, 2, 3, 3, 9, 7, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 5, 5, 6, 6, 6, 6, 5, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3], | |
"pitstops": [8] | |
}, | |
{ | |
"name": "Nico Rosberg", | |
"placing": [6, 5, 5, 5, 5, 5, 5, 5, 7, 6, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 5, 5, 5], | |
"pitstops": [8, 33] | |
}, | |
{ | |
"name": "Michael Schumacher", | |
"placing": [7], | |
"accident": [0] | |
}, | |
{ | |
"name": "Rubens Barrichello", | |
"placing": [8, 9, 9, 9, 9, 9, 8, 7, 10, 8, 7, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 8, 8, 8, 8, 8, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 9, 9, 9, 9, 9, 9, 8, 8], | |
"pitstops": [8, 31], | |
"accident": [55] | |
}, | |
{ | |
"name": "Robert Kubica", | |
"placing": [9, 4, 4, 4, 4, 4, 4, 4, 6, 5, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2], | |
"pitstops": [8] | |
}, | |
{ | |
"name": "Adrian Sutil", | |
"placing": [10, 8, 8, 8, 8, 8, 7, 9, 5, 2], | |
"mechanical": [9] | |
}, | |
{ | |
"name": "Lewis Hamilton", | |
"placing": [11, 7, 7, 7, 7, 7, 6, 6, 11, 11, 8, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 3, 3, 3, 3, 3, 3, 3, 3, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6], | |
"pitstops": [8, 34], | |
"accident": [55] | |
}, | |
{ | |
"name": "Sebastien Buemi", | |
"placing": [12], | |
"accident": [0] | |
}, | |
{ | |
"name": "Vitantonio Liuzzi", | |
"placing": [13, 12, 12, 12, 12, 12, 11, 11, 3, 9, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 10, 10, 10, 9, 9, 9, 9, 9, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 7, 7, 7], | |
"pitstops": [9] | |
}, | |
{ | |
"name": "Pedro De La Rosa", | |
"placing": [14, 11, 11, 11, 11, 11, 10, 10, 13, 12, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 10, 10, 10, 10, 10, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 12, 12], | |
"pitstops": [8] | |
}, | |
{ | |
"name": "Nico Hulkenberg", | |
"placing": [15], | |
"accident": [0] | |
}, | |
{ | |
"name": "Kamui Kobayashi", | |
"placing": [16], | |
"accident": [0] | |
}, | |
{ | |
"name": "Jaime Alguersuari", | |
"placing": [17, 13, 13, 13, 13, 13, 12, 12, 4, 10, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, 13, 13, 12, 12, 12, 12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 11, 11], | |
"pitstops": [9, 27] | |
}, | |
{ | |
"name": "Vitaly Petrov", | |
"placing": [18, 10, 10, 10, 10, 10, 9, 8, 14, 14], | |
"pitstops": [8], | |
"accident": [9] | |
}, | |
{ | |
"name": "Heikki Kovalainen", | |
"placing": [19, 15, 15, 15, 14, 14, 14, 15, 18, 18, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13], | |
"pitstops": [8] | |
}, | |
{ | |
"name": "Jarno Trulli", | |
"placing": [20, 20, 20, 20, 20, 17, 15, 14, 17, 15, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 12, 12, 12, 11, 11, 11, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 11, 11, 11, 11, 10, 10], | |
"pitstops": [1, 8, 29], | |
"mechanical": [0] | |
}, | |
{ | |
"name": "Bruno Senna", | |
"placing": [21, 14, 14, 14, 15], | |
"mechanical": [4] | |
}, | |
{ | |
"name": "Karun Chandock", | |
"placing": [22, 16, 16, 16, 16, 19, 18, 18, 19, 19, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, 17, 16, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14], | |
"pitstops": [8, 46] | |
}, | |
{ | |
"name": "Timo Glock", | |
"placing": [23, 17, 17, 17, 17, 16, 16, 16, 8, 17, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14], | |
"pitstops": [8, 9], | |
"mechanical": [41] | |
}, | |
{ | |
"name": "Luca di Grassi", | |
"placing": [24, 19, 19, 19, 19, 18, 17, 17, 16, 16, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 16, 16, 16, 16, 16, 17, 16], | |
"pitstops": [10, 25], | |
"mechanical": [26] | |
} | |
], | |
"lapped": [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 17, 17, 16, 16, 16, 16, 16, 16, 16,15, 15, 15, 14, 13 , 13 , 13 , 13 , 13 , 13 , 13 , 13 , 13 , 13 , 13 , 13 , 13 , 13 , 13 , 13 , 13 , 13 , 13 , 13 , 13 , 13 , 13 , 13 , 13 , 13 , 13 , 13 , 13 , 13 , 13 , 13], | |
"safety": [1, 2, 3, 4] | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" | |
"http://www.w3.org/TR/html4/loose.dtd"> | |
<html> | |
<head> | |
<title>Formula 1 Lap Chart</title> | |
<meta http-equiv="X-UA-Compatible" content="chrome=1"> | |
<link href="style.css" media="screen" rel="stylesheet" type="text/css"/> | |
<script type="text/javascript" src="http://d3js.org/d3.v2.min.js"></script> | |
</head> | |
<body> | |
<span class="title">Australian Formula 1 Grand Prix, 2010 | |
<a href="http://creativecommons.org/licenses/by-sa/3.0/" target="_blank"><img align="top" | |
alt="Creative Commons License" | |
src="http://i.creativecommons.org/l/by-sa/3.0/80x15.png"/></a> | |
</span> | |
<span class="attrib">By <a href="http://vislives.com/" target="_blank">Chris Pudney</a></span> | |
<div id="chart"></div> | |
<span class="legend">M = mechanical failure | P = pit stop | X = accident | <span | |
class="safety">safety-car deployed</span> | <span class="lapped">lapped</span> | Mouse click to focus on a lap</span> | |
<script src="lap-chart.js" type="text/javascript"></script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Dimensions. | |
const DIMENSIONS = getWindowDimensions(); | |
const WIDTH = DIMENSIONS.width - 20; // 20 => padding. | |
const HEIGHT = DIMENSIONS.height - 50; // 50 => legend, title and padding. | |
// Insets. | |
const INSETS = {'left': 150, 'right': 150, 'top': 30, 'bottom': 30}; | |
// Padding. | |
const PADDING = {'left': 20, 'right': 20, 'top': 15, 'bottom': 15}; | |
// Tick-mark length. | |
const TICK_MARK_LENGTH = 8; | |
// Marker radius. | |
const MARKER_RADIUS = 12; | |
// Scales. | |
const SCALES = {}; | |
// Transition duration. | |
const TRANSITION_DURATION = 1000; | |
// Opacity of dimmed and highlighted objects. | |
const DIMMED_OPACITY = 0.3; | |
const HIGHLIGHT_OPACITY = 1.0; | |
// Zoom factors. | |
const ZOOM_PEAK = 6.0; | |
const ZOOM_SHOULDER = 3.0; | |
// Zooming. | |
var zoomed = false; | |
// Visualize when document has loaded. | |
// | |
window.onload = function() { | |
// Load data. | |
d3.json("2010au.json", function(data) { | |
// Check integrity. | |
integrityCheck(data); | |
// Sort laps on finishing order. | |
data.laps.sort(function(a, b) { | |
var aLaps = a.placing.length; | |
var bLaps = b.placing.length; | |
return aLaps == bLaps ? a.placing[aLaps - 1] - b.placing[bLaps - 1] : bLaps - aLaps; | |
}); | |
// Process lap markers.. | |
data.pitstops = processLapMarkers(data, "pitstops"); | |
data.mechanical = processLapMarkers(data, "mechanical"); | |
data.accident = processLapMarkers(data, "accident"); | |
// Visualize the data. | |
visualize(data); | |
}); | |
}; | |
// Check data. | |
// | |
// data: the data to check. | |
// | |
function integrityCheck(data) { | |
var laps = data.laps; | |
var lapCount = data.lapCount; | |
// Check lap data. | |
checkLaps(laps, lapCount); | |
// Check lapped data. | |
checkLapped(data.lapped, lapCount, laps.length); | |
// Check safety car data. | |
checkSafetyCar(data.safety, lapCount); | |
} | |
// Check lap data. | |
// | |
// laps: the lap data. | |
// lapCount: number of laps. | |
// | |
function checkLaps(laps, lapCount) { | |
for (var j = 0; | |
j < laps.length; | |
j++) { | |
// Has name? | |
var name = laps[j].name; | |
if (name == undefined || name.length == 0) { | |
alert("Warning: invalid name for element " + j); | |
} | |
// Has placings? | |
var places = laps[j].placing; | |
if (places == undefined) { | |
alert("Warning: missing placings for element " + j + " (" + name + ")"); | |
} | |
else if (places.length == 0 || places.length > lapCount + 1) { | |
alert("Warning: invalid number of placings (" + places.length + ") for element " + j + | |
" (" + name + ") - expected between 1 and " + (lapCount - 1)); | |
} | |
// Check markers. | |
var maxLaps = places.length; | |
checkMarker(laps[j].pitstops, "pitstop", maxLaps, j, name); | |
checkMarker(laps[j].mechanical, "mechanical", maxLaps, j, name); | |
checkMarker(laps[j].accident, "accident", maxLaps, j, name); | |
} | |
for (var i = 0; | |
i < lapCount; | |
i++) { | |
var positions = []; | |
for (j = 0; | |
j < laps.length; | |
j++) { | |
places = laps[j].placing; | |
if (places.length > i) { | |
// Valid placing? | |
var placing = places[i]; | |
if (isNaN(placing) || placing < 1 || placing % 1 != 0) { | |
alert("Warning: invalid placing '" + placing + "' for " + laps[j].name) | |
} | |
else { | |
var count = positions[placing]; | |
positions[placing] = isNaN(count) ? 1 : count + 1 | |
} | |
} | |
} | |
// Check for duplicate/missing positions. | |
for (j = 1; | |
j < positions.length; | |
j++) { | |
count = positions[j]; | |
if (count != 1) { | |
alert("Warning: data inconsistent: lap " + i + ", position " + j + ", count " + count); | |
} | |
} | |
} | |
} | |
// Check integrity of marker data. | |
// | |
// marker: marker data. | |
// name: driver name. | |
// type: text description of marker. | |
// max: maximum allowed lap value of marker. | |
// index: index of driver in list. | |
// | |
function checkMarker(marker, type, max, index, name) { | |
if (marker != undefined) { | |
// Check marker. | |
for (var i = 0; | |
i < marker.length; | |
i++) { | |
var stop = marker[i]; | |
if (isNaN(stop) || stop < 0 || stop >= max || stop % 1 != 0) { | |
alert("Warning: invalid " + type + " (" + stop + ") for element " + index + " (" + name + ")"); | |
} | |
} | |
} | |
} | |
// Check lapped data. | |
// | |
// lapped: the lapped data. | |
// lapCount: number of laps. | |
// driverCount: number of drivers. | |
// | |
function checkLapped(lapped, lapCount, driverCount) { | |
if (lapped != undefined) { | |
var lappedLength = lapped.length; | |
if (lappedLength != lapCount) { | |
alert("Lapped array length (" + lappedLength + ") incorrect - expected length " + lapCount); | |
} | |
for (var j = 1; | |
j < lappedLength; | |
j++) { | |
// Valid position. | |
var position = lapped[j]; | |
if (isNaN(position) || position % 1 != 0 || position < -1 || position > driverCount) { | |
alert("Invalid lapped position: element " + j + " (" + position | |
+ "); expected integer between -1 and " + driverCount); | |
} | |
} | |
} | |
} | |
// Check safety car data. | |
// | |
// safety: safety car data. | |
// lapCount: number of laps. | |
// | |
function checkSafetyCar(safety, lapCount) { | |
if (safety != undefined) { | |
for (var i = 0; | |
i < safety.length; | |
i++) { | |
// Valid lap? | |
var lap = safety[i]; | |
if (isNaN(lap) || lap < 0 || lap % 1 != 0 || lap > lapCount) { | |
alert("Invalid safety car lap: element " + i + " (" + lap | |
+ "); expected integer between 0 and " + lapCount); | |
} | |
} | |
} | |
} | |
// Process lap markers. | |
// | |
// data: lap data. | |
// key: marker key. | |
// | |
function processLapMarkers(data, key) { | |
var markers = []; | |
var p = 0; | |
for (var i = 0; | |
i < data.laps.length; | |
i++) { | |
var lapData = data.laps[i]; | |
var laps = lapData[key]; | |
if (laps != undefined) { | |
for (var j = 0; | |
j < laps.length; | |
j++) { | |
var lap = laps[j]; | |
var marker = {}; | |
marker.start = lapData.placing[0]; | |
marker.lap = lap; | |
marker.placing = lapData.placing[lap]; | |
marker.name = lapData.name; | |
markers[p++] = marker; | |
} | |
} | |
} | |
return markers; | |
} | |
// Create the visualization. | |
// | |
// data the lap data object. | |
// | |
function visualize(data) { | |
// Configure scales. | |
configureScales(data); | |
var vis = d3.select('#chart') | |
.append('svg:svg') | |
.attr('width', WIDTH) | |
.attr('height', HEIGHT) | |
.attr('class', 'zoom'); | |
// Background rect to catch zoom clicks. | |
vis.append('svg:rect') | |
.attr('class', 'zoom') | |
.attr('x', 0) | |
.attr('y', 0) | |
.attr('width', WIDTH) | |
.attr('height', HEIGHT) | |
.style('opacity', 0.0); | |
// Add safety car element. | |
addSafetyElement(vis, data.safety); | |
// Add lapped element. | |
addLappedElement(vis, data.lapped); | |
// Lap tick-lines. | |
addLapTickLines(vis, data.lapCount); | |
// Lap labels. | |
addLapLabels(vis, data.lapCount, SCALES.y.range()[0] - PADDING.bottom, '0.0em', 'top'); | |
addLapLabels(vis, data.lapCount, SCALES.y.range()[1] + PADDING.top, '0.35em', 'bottom'); | |
// Add placings poly-lines. | |
addPlacingsLines(vis, data.laps); | |
// Add name labels. | |
addDriverLabels(vis, data.laps, 'pole', SCALES.x(0) - PADDING.right, 'end') | |
.attr('y', function (d) { | |
return SCALES.y(d.placing[0] - 1); | |
}); | |
addDriverLabels(vis, data.laps, 'flag', SCALES.x(data.lapCount) + PADDING.left, 'start') | |
.attr('y', function (d, i) { | |
return SCALES.y(i); | |
}); | |
// Add markers. | |
addMarkers(vis, data.pitstops, "pitstop", "P"); | |
addMarkers(vis, data.mechanical, "mechanical", "M"); | |
addMarkers(vis, data.accident, "accident", "X"); | |
// Listen for clicks -> zoom. | |
vis.selectAll('.zoom') | |
.on("click", function() { | |
toggleZoom(vis, d3.mouse(this)[0]); | |
}); | |
} | |
// Configure the scales. | |
// | |
// data: data set. | |
// | |
function configureScales(data) { | |
SCALES.x = d3.scale.linear() | |
.domain([0, data.lapCount]) | |
.range([INSETS.left, WIDTH - INSETS.right]); | |
SCALES.y = d3.scale.linear() | |
.domain([0, data.laps.length - 1]) | |
.range([INSETS.top, HEIGHT - INSETS.bottom]); | |
SCALES.clr = d3.scale.category20(); | |
} | |
// Highlight driver. | |
// | |
// vis: the data visualization root. | |
// index: index of driver to highlight. | |
// | |
function highlight(vis, name) { | |
// Dim others. | |
vis.selectAll('polyline') | |
.style('opacity', function(d) { | |
return d.name == name ? HIGHLIGHT_OPACITY : DIMMED_OPACITY; | |
}); | |
vis.selectAll('circle') | |
.style('opacity', function(d) { | |
return d.name == name ? HIGHLIGHT_OPACITY : DIMMED_OPACITY; | |
}); | |
vis.selectAll('text.label') | |
.style('opacity', function(d) { | |
return d.name == name ? HIGHLIGHT_OPACITY : DIMMED_OPACITY; | |
}); | |
} | |
// Remove highlights. | |
// | |
// vis: the data visualization root. | |
// | |
function unhighlight(vis) { | |
// Reset opacity. | |
vis.selectAll('polyline') | |
.style('opacity', HIGHLIGHT_OPACITY); | |
vis.selectAll('circle') | |
.style('opacity', HIGHLIGHT_OPACITY); | |
vis.selectAll('text.label') | |
.style('opacity', HIGHLIGHT_OPACITY); | |
} | |
// Zoom/unzoom. | |
// | |
// vis: the data visualization root. | |
// mouseX: x-coordinate of mouse click. | |
// | |
function toggleZoom(vis, mouseX) { | |
// Get lap of mouse-click position. | |
var lap = Math.round(SCALES.x.invert(mouseX)); | |
// Clamp to domain. | |
var domain = SCALES.x.domain(); | |
lap = Math.max(domain[0], Math.min(domain[1], lap)); | |
// Specify transform. | |
var xform = zoomed ? unzoomXform : zoomXform; | |
zoomed = !zoomed; | |
// Transition tick lines. | |
vis.selectAll('line.tickLine') | |
.transition() | |
.duration(TRANSITION_DURATION) | |
.attr("x1", function(d) { | |
return SCALES.x(xform(d + 0.5, lap)) | |
}) | |
.attr("x2", function(d) { | |
return SCALES.x(xform(d + 0.5, lap)) | |
}); | |
// Transition tick labels. | |
vis.selectAll('text.lap') | |
.transition() | |
.duration(TRANSITION_DURATION) | |
.attr("x", function(d) { | |
return SCALES.x(xform(d, lap)) | |
}); | |
// Transition safety elements. | |
vis.selectAll('rect.safety') | |
.transition() | |
.duration(TRANSITION_DURATION) | |
.attr('x', function(d) { | |
return SCALES.x(xform(d - 0.5, lap)); | |
}) | |
.attr('width', function(d) { | |
return SCALES.x(xform(d + 0.5, lap)) - SCALES.x(xform(d - 0.5, lap)); | |
}); | |
// Transition lapped elements. | |
vis.selectAll('rect.lapped') | |
.transition() | |
.duration(TRANSITION_DURATION) | |
.attr('x', function(d, i) { | |
return SCALES.x(xform(i + 0.5, lap)); | |
}) | |
.attr('width', function(d, i) { | |
return SCALES.x(xform(i + 1.5, lap)) - SCALES.x(xform(i + 0.5, lap)); | |
}); | |
// Transition lapped elements. | |
vis.selectAll('polyline.placing') | |
.transition() | |
.duration(TRANSITION_DURATION) | |
.attr('points', function(d) { | |
var points = []; | |
for (var i = 0; | |
i < d.placing.length; | |
i++) { | |
points[i] = SCALES.x(xform(i, lap)) + ',' + SCALES.y(d.placing[i] - 1); | |
} | |
if (points.length > 0) { | |
points.push(SCALES.x(xform(i - 0.5, lap)) + ',' + SCALES.y(d.placing[i - 1] - 1)); | |
} | |
return points.join(' '); | |
}); | |
// Transition markers (circles). | |
vis.selectAll('circle.marker') | |
.transition() | |
.duration(TRANSITION_DURATION) | |
.attr('cx', function(d) { | |
return SCALES.x(xform(d.lap, lap)); | |
}); | |
// Transition markers (labels). | |
vis.selectAll('text.label.marker') | |
.transition() | |
.duration(TRANSITION_DURATION) | |
.attr('x', function(d) { | |
return SCALES.x(xform(d.lap, lap)); | |
}); | |
} | |
/** | |
* The zooming function is piecewise linear. It divides the x-axis into several sections each of which is zoomed by | |
* a different amount. The closer the zone is to the zoom centre, the higher the zoom factor. | |
* | |
* | NO ZOOM | ZOOM_SHOULDER | ZOOM_PEAK | ZOOM_SHOULDER | NO ZOOM | | |
* | |
* ZOOM_PEAK is applied where on the lap where the user clicked. | |
* ZOOM_SHOULDER is applied to the laps either side of this. | |
* No zoom is applied elsewhere. | |
* | |
* @param x the x-coordinate to transform using the zooming function. | |
* @param lap the lap the user clicked on. | |
*/ | |
function zoomXform(x, lap) { | |
// The x-axis domain. | |
var domain = SCALES.x.domain(); | |
var step = domain[1] - domain[0]; | |
// What is the increment between each lap after zooming. | |
var inc = lap <= domain[0] || lap >= domain[1] ? | |
step / (ZOOM_PEAK + ZOOM_SHOULDER - 2.0 + step) : | |
step / (ZOOM_PEAK + 2.0 * ZOOM_SHOULDER - 3.0 + step); | |
// The zoom centre is mid-lap. | |
lap += 0.5; | |
// The transformed version of x. | |
var z = 0.0; | |
// Beyond upper shoulder. | |
if (x > lap + 1.0) z = (x + ZOOM_PEAK + 2.0 * ZOOM_SHOULDER - 3.0) * inc; | |
// Upper shoulder. | |
else if (x > lap) z = ((x - lap + 1.0) * ZOOM_SHOULDER + lap + ZOOM_PEAK - 2.0) * inc; | |
// Peak. | |
else if (x > lap - 1.0) z = ((x - lap + 1.0) * ZOOM_PEAK + lap + ZOOM_SHOULDER - 2.0) * inc; | |
// Lower shoulder. | |
else if (x > lap - 2.0) z = ((x - lap + 2.0) * ZOOM_SHOULDER + lap - 2.0) * inc; | |
// Below lower shoulder. | |
else z = (x - domain[0]) * inc; | |
return z; | |
} | |
function unzoomXform(x) { | |
return x; | |
} | |
// Add safety car laps (rectangle elements). | |
// | |
// vis: the data visualization root. | |
// data: safety car laps. | |
// | |
function addSafetyElement(vis, data) { | |
if (data != undefined) { | |
var y = SCALES.y.range()[0]; | |
var height = SCALES.y.range()[1] - y; | |
var width = SCALES.x(1) - SCALES.x(0); | |
vis.selectAll('rect.safety') | |
.data(data) | |
.enter() | |
.append('svg:rect') | |
.attr('class', 'safety zoom') | |
.attr('x', function(d) { | |
return SCALES.x(d - 0.5); | |
}) | |
.attr('y', function() { | |
return y; | |
}) | |
.attr('height', function() { | |
return height; | |
}) | |
.attr('width', function() { | |
return width; | |
}); | |
} | |
} | |
// Add lapped rectangle elements. | |
// | |
// vis: the data visualization root. | |
// data: the lapped data. | |
// | |
function addLappedElement(vis, data) { | |
if (data != undefined) { | |
var width = SCALES.x(1) - SCALES.x(0); | |
vis.selectAll('rect.lapped') | |
.data(data) | |
.enter() | |
.append('svg:rect') | |
.attr('class', 'lapped zoom') | |
.attr('x', function(d, i) { | |
return SCALES.x(i + 0.5); | |
}) | |
.attr('y', function(d) { | |
return SCALES.y(d > 0 ? d - 1.5 : 0); | |
}) | |
.attr('height', function(d) { | |
return d > 0 ? SCALES.y.range()[1] - SCALES.y(d - 1.5) : 0; | |
}) | |
.attr('width', function(d) { | |
return d > 0 ? width : 0; | |
}); | |
} | |
} | |
// Add lap tick-lines. | |
// | |
// vis: the data visualization root. | |
// lapCount: number of laps. | |
// | |
function addLapTickLines(vis, lapCount) { | |
vis.selectAll('line.tickLine') | |
.data(SCALES.x.ticks(lapCount)) | |
.enter().append('svg:line') | |
.attr('class', 'tickLine zoom') | |
.attr('x1', function(d) { | |
return SCALES.x(d + 0.5); | |
}) | |
.attr('x2', function(d) { | |
return SCALES.x(d + 0.5); | |
}) | |
.attr('y1', SCALES.y.range()[0] - TICK_MARK_LENGTH) | |
.attr('y2', SCALES.y.range()[1] + TICK_MARK_LENGTH) | |
.attr('visibility', function(d) { | |
return d <= lapCount ? 'visible' : 'hidden' | |
}); | |
} | |
// Add lap labels. | |
// | |
// vis: the data visualization root. | |
// data: lap data. | |
// y: y position of labels. | |
// dy: y offset. | |
// cssClass: CSS class id. | |
// | |
function addLapLabels(vis, data, y, dy, cssClass) { | |
vis.selectAll('text.lap.' + cssClass) | |
.data(SCALES.x.ticks(data)) | |
.enter().append('svg:text') | |
.attr('class', 'lap ' + cssClass + ' zoom') | |
.attr('x', function(d) { | |
return SCALES.x(d); | |
}) | |
.attr('y', y) | |
.attr('dy', dy) | |
.attr('text-anchor', 'middle') | |
.text(function(d, i) { | |
return i > 0 ? i : ''; | |
}); | |
} | |
// Add placings polyline elements. | |
// | |
// vis: the visualization root. | |
// laps: lap data. | |
// | |
function addPlacingsLines(vis, laps) { | |
vis.selectAll('polyline.placing') | |
.data(laps) | |
.enter() | |
.append('svg:polyline') | |
.attr('class', 'placing zoom') | |
.attr('points', function(d) { | |
var points = []; | |
for (var i = 0; | |
i < d.placing.length; | |
i++) { | |
points[i] = SCALES.x(i) + ',' + SCALES.y(d.placing[i] - 1); | |
} | |
if (points.length > 0) | |
points.push(SCALES.x(i - 0.5) + ',' + SCALES.y(d.placing[i - 1] - 1)); | |
return points.join(' '); | |
}) | |
.style('stroke', function(d) { | |
return SCALES.clr(d.placing[0]); | |
}) | |
.on('mouseover', function(d) { | |
highlight(vis, d.name); | |
}) | |
.on('mouseout', function() { | |
unhighlight(vis); | |
}); | |
} | |
// Add driver name labels. | |
// | |
// vis: the data visualization root. | |
// laps: the lap data. | |
// cssClass: CSS class id. | |
// textAnchor: text-anchor value. | |
// | |
function addDriverLabels(vis, laps, cssClass, x, textAnchor) { | |
return vis.selectAll('text.label.' + cssClass) | |
.data(laps) | |
.enter() | |
.append('svg:text') | |
.attr('class', 'label ' + cssClass) | |
.attr('x', x) | |
.attr('dy', '0.35em') | |
.attr('text-anchor', textAnchor) | |
.text(function(d) { | |
return d.name; | |
}) | |
.style('fill', function(d) { | |
return SCALES.clr(d.placing[0]); | |
}) | |
.on('mouseover', function(d) { | |
highlight(vis, d.name); | |
}) | |
.on('mouseout', function() { | |
unhighlight(vis); | |
}); | |
} | |
// Add markers. | |
// | |
// vis: the visualization root. | |
// data: marker data. | |
// class: marker sub-class. | |
// label: marker label. | |
// | |
function addMarkers(vis, data, cssClass, label) { | |
label = label || "P"; | |
// Place circle glyph. | |
vis.selectAll("circle.marker." + cssClass) | |
.data(data) | |
.enter() | |
.append("svg:circle") | |
.attr("class", "marker " + cssClass + " zoom") | |
.attr("cx", function(d) { | |
return SCALES.x(d.lap); | |
}) | |
.attr("cy", function(d) { | |
return SCALES.y(d.placing - 1); | |
}) | |
.attr("r", MARKER_RADIUS) | |
.style("fill", function(d) { | |
return SCALES.clr(d.start); | |
}) | |
.on('mouseover', function(d) { | |
highlight(vis, d.name); | |
}) | |
.on('mouseout', function() { | |
unhighlight(vis); | |
}); | |
// Place text. | |
vis.selectAll("text.label.marker." + cssClass) | |
.data(data) | |
.enter() | |
.append("svg:text") | |
.attr("class", "label marker " + cssClass + " zoom") | |
.attr("x", function(d) { | |
return SCALES.x(d.lap); | |
}) | |
.attr("y", function(d) { | |
return SCALES.y(d.placing - 1); | |
}) | |
.attr("dy", "0.35em") | |
.attr("text-anchor", "middle") | |
.text(label) | |
.on('mouseover', function(d) { | |
highlight(vis, d.name); | |
}) | |
.on('mouseout', function() { | |
unhighlight(vis); | |
}); | |
} | |
// Gets the window dimensions. | |
// | |
function getWindowDimensions() { | |
var width = 630; | |
var height = 460; | |
if (document.body && document.body.offsetWidth) { | |
width = document.body.offsetWidth; | |
height = document.body.offsetHeight; | |
} | |
if (document.compatMode == 'CSS1Compat' && document.documentElement && document.documentElement.offsetWidth) { | |
width = document.documentElement.offsetWidth; | |
height = document.documentElement.offsetHeight; | |
} | |
if (window.innerWidth && window.innerHeight) { | |
width = window.innerWidth; | |
height = window.innerHeight; | |
} | |
return {'width': width, 'height': height}; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
body { | |
margin: 0; | |
padding: 10px; | |
background-color: #000000; | |
font-family: sans-serif; | |
font-size: 12px; | |
color: #666666; | |
overflow: hidden; | |
} | |
a:link { | |
color: #ccccff; | |
} | |
a:visited { | |
color: #cccccc; | |
} | |
a:active { | |
color: #ffccff; | |
} | |
a.hover { | |
color: #ffcccc; | |
} | |
.title { | |
float: left; | |
font-size: 14px; | |
font-weight: bold; | |
color: #ffffff; | |
} | |
.attrib { | |
float: right; | |
font-size: 14px; | |
color: #ffffff; | |
} | |
text.lap { | |
fill: #999999; | |
} | |
text.label { | |
fill: #000000; | |
} | |
text.label.marker { | |
font-weight: bold; | |
} | |
polyline.placing { | |
fill: none; | |
stroke-width: 5; | |
} | |
line.tickLine { | |
stroke: #999999; | |
} | |
.lapped { | |
background-color: #333333; | |
stroke: #333333; | |
fill: #333333; | |
} | |
.safety{ | |
background-color: #330000; | |
stroke: #330000; | |
fill: #330000; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment