block by gabrielflorit 70bace15a196959075ccc2effbeb6064

Speedy Racer

Full Screen

Made with blockup

This simulates cars going around the Melbourne, Australia racing track. The circles are moving about three times faster than their real-life counterparts.

The car-circles change speed randomly, as displayed on the line chart.

See my previous work:

script.js

import drawTrack from './drawTrack.js'
import drawCars from './drawCars.js'
import constants from './constants.js'
import drawBoard from './drawBoard.js'
import chart from './chart.js'
import moveCar from './moveCar.js'
import calculateMetrics from './calculateMetrics.js'

// Draw the track.
const { g: gCircuit, getCoords } = drawTrack({
  container: d3.select('.circuit'),
  track: circuitTracks[0]
})

// Setup the chart.
const chartBits = chart.setup(d3.select('.chart'))

// Create the state.
const dx = 0.015
let state = {
  cars: _(drivers)
    .sortBy('standing')
    .slice(0, 10)
    .map((d, i, array) => ({
      number: d.number,
      name: d.name.split(' ')[1].slice(0, 3),
      measures: [
        {
          speed: constants.AVERAGE,
          pct: 0
        },
        {
          speed: constants.AVERAGE,
          pct: dx * array.length - (i + 1) * dx
        }
      ],
      elapsed: 0,
      laps: [],
      status: 'yellow',
      position: i + 1
    }))
    .value()
}

const draw = setup => {
  // Draw the cars.
  drawCars({ g: gCircuit, getCoords, cars: state.cars })

  // Draw the board.
  drawBoard({ cars: state.cars, setup })

  // Update the chart.
  chart.update({ cars: state.cars, ...chartBits })
}

draw(true)

let now = 0
const timer = d3.interval(elapsed => {
  const delta = elapsed - now

  // Move the cars (and also keep them sorted by position).
  state = {
    ...state,
    cars: _(state.cars)
      .map(car => moveCar({ car, delta }))
      .sortBy(d => -_.last(d.measures).pct)
      .value()
  }

  // Calculate metrics.
  state = {
    ...state,
    cars: state.cars.map((car, index, cars) =>
      calculateMetrics({ car, raceElapsed: elapsed, index, cars })
    )
  }

  // Draw everything.
  draw()

  now = elapsed

  // Stop when the leader gets to 10 laps.
  if (_.last(state.cars[0].measures).pct >= 10) {
    timer.stop()
  }
}, 0)

// document.querySelector('button.slow').addEventListener('click', () => {
//   state.cars[0].speed = state.cars[0].speed * 0.9
// })

// document.querySelector('button.fast').addEventListener('click', () => {
//   state.cars[0].speed = state.cars[0].speed * 1.01
// })

document.querySelector('button.stop').addEventListener('click', () => {
  timer.stop()
})

index.html

<!DOCTYPE html>
<title>blockup</title>
<link href='https://fonts.googleapis.com/css?family=VT323' rel='stylesheet'>
<link href='dist.css' rel='stylesheet' />
<body>
  <h1>speedy racer</h1>
  <div class='circuit'></div>
  <table class='board'>
    <thead>
      <tr>
        <td></td>
        <td></td>
        <td></td>
        <td class='cyan'>GAP</td>
        <td class='cyan'>INT</td>
        <td></td>
        <td></td>
      </tr>
    </thead>
    <tbody>
    </tbody>
  </table>
  <div class='chart'></div>
  <button class='slow'>slow</button>
  <button class='fast'>fast</button>
  <button class='stop'>stop</button>
  <script src='lodash.min.js'></script>
	<script src='d3.v4.min.js'></script>
  <script src='drivers.js'></script>
  <script src='circuitTracks.js'></script>
	<script src='dist.js'></script>
</body>

calculateMetrics.js

// We want:
// DONE: position
// DONE: status
// DONE: GAP
// DONE: INT
// DONE: recent lap time
// DONE: lap count
//     : is better lap
//     : is better GAP
//     : is better INT

const formatGap = x => (x / 1000).toFixed(1)

const calculateMetrics = ({ car, raceElapsed, cars, index }) => {
  const { elapsed, isNewLap, laps, measures, number, gap, status, int } = car
  const leader = cars[0]

  // Set defaults.
  let previousLap = null
  let newElapsed = elapsed
  let newGap = gap
  let newStatus = status
  let newInt = int

  // If the leader started a new lap,
  // set this car's status to yellow.
  if (leader.isNewLap) {
    newStatus = 'yellow'
  }

  // If we started a new lap,
  if (isNewLap) {
    // do a number of things.

    // Set our status to white.
    newStatus = 'white'

    // Calculate the previous lap time.
    // To do this, do race elapsed - previous elapsed.
    previousLap = raceElapsed - elapsed

    // And set car's elapsed to race elapsed.
    newElapsed = raceElapsed

    // Every time you start a new lap,
    // calculate GAP and INT.
    // GAP = distance to the leader.
    // INT = distance to the previous car.
    // If we're the leader, ignore.
    if (number === leader.number) {
      newGap = 'LAP'
      newInt = '--'
    } else {
      // If we're not the leader,
      // calculate the lap count differences to leader and previous car.
      const leaderLapDelta =
        Math.floor(_.last(leader.measures).pct) -
        Math.floor(_.last(measures).pct)
      const previousCarLapDelta =
        Math.floor(_.last(cars[index - 1].measures).pct) -
        Math.floor(_.last(measures).pct)

      // If we're on the same lap as leader, show time gap.
      if (leaderLapDelta === 0) {
        newGap = formatGap(newElapsed - leader.elapsed)
      } else {
        // If we're NOT on the same lap, just show the lap count delta.
        newGap = `${leaderLapDelta}L`
      }

      // If we're on the same lap as previous, show time gap.
      if (previousCarLapDelta === 0) {
        newInt = formatGap(newElapsed - cars[index - 1].elapsed)
      } else {
        // If we're NOT on the same lap, just show the lap count delta.
        newInt = `${previousCarLapDelta}L`
      }
    }
  }

  return {
    ...car,
    elapsed: newElapsed,
    laps: [...laps, previousLap].filter(d => d),
    gap: newGap,
    isNewLap,
    status: newStatus,
    int: newInt,
    position: index + 1
  }
}

export default calculateMetrics

chart.js

import constants from './constants.js'

const setup = container => {
  const margins = { top: 45, right: 5, bottom: 20, left: 40 }
  const width = container.node().offsetWidth - margins.right - margins.left
  const height = width / 3.5

  const svg = container
    .append('svg')
    .attr('width', width + margins.right + margins.left)
    .attr('height', height + margins.top + margins.bottom)

  const g = svg
    .append('g')
    .attr('transform', `translate(${margins.left}, ${margins.top})`)

  const x = d3
    .scaleLinear()
    .range([0, width])
    .domain([0, 1])

  const y = d3
    .scaleLinear()
    .range([height, 0])
    .domain([0, constants.MAX])
    .nice()

  const line = d3
    .line()
    .curve(d3.curveBasis)
    .x(d => x(d.pct % 1))
    .y(d => y(d.speed))

  const xAxis = g
    .append('g')
    .attr('class', 'axis axis--x')
    .attr('transform', `translate(0, ${height})`)
    .call(
      d3
        .axisBottom(x)
        .tickSize(0)
        .ticks([])
    )

  xAxis
    .append('text')
    .text('Start')
    .attr('x', x(0))
    .attr('text-anchor', 'start')
    .attr('dy', 14)

  xAxis
    .append('text')
    .text('Finish')
    .attr('x', x(1))
    .attr('text-anchor', 'end')
    .attr('dy', 14)

  const yAxis = g
    .append('g')
    .attr('class', 'axis axis--y')
    .call(
      d3
        .axisLeft(y)
        .tickSize(0)
        .ticks(5)
    )

  yAxis
    .append('text')
    .text('(MPH)')
    .attr('dx', -3)
    .attr('dy', 6 - 18)
    .attr('text-anchor', 'end')

  yAxis
    .append('text')
    .text('Speed')
    .attr('dx', -3)
    .attr('dy', 6 - 18 - 18)
    .attr('text-anchor', 'end')

  return {
    g,
    x,
    y,
    line
  }
}

const clean = measures => {
  const lastTwo = measures.slice(-2).map(d => d.pct % 1)

  return lastTwo[1] < lastTwo[0]
    ? measures.slice(0, measures.length - 1)
    : measures
}
// _.last(measures).pct % 1 <
// measures.length === 2 ? measures.slice(-1) : measures

const getTransform = ({ d, x, y }) => {
  const last = _.last(d.measures)
  return `translate(${x(last.pct % 1)}, ${y(last.speed)})`
}

const update = ({ g, x, y, line, cars }) => {
  // join
  const paths = g.selectAll('path.car').data(cars, d => d.number)

  // update
  paths.attr('d', d => line(clean(d.measures)))

  // enter
  paths
    .enter()
    .append('path')
    .attr('class', 'car')
    .attr('d', d => line(clean(d.measures)))

  // remove
  paths.exit().remove()

  // join
  const gJoin = g.selectAll('g.car').data(cars, d => d.number)

  // update
  gJoin
    .attr('class', d => (d.position === 1 ? 'car leader' : 'car'))
    .attr('transform', d => getTransform({ d, x, y }))

  // enter
  const gEnter = gJoin
    .enter()
    .append('g')
    .attr('class', d => (d.position === 1 ? 'car leader' : 'car'))
    .attr('transform', d => getTransform({ d, x, y }))

  gEnter
    .append('circle')
    .attr('cx', 0)
    .attr('cy', 0)
    .attr('r', 9)

  gEnter
    .append('text')
    .text(d => d.number)
    .attr('dy', 4)

  // remove
  gJoin.exit().remove()
}

const chart = {
  setup,
  update
}

export default chart

constants.js

const multiplier = 1

const constants = {
  MIN: 60 * multiplier,
  AVERAGE: 140 * multiplier,
  MAX: 194 * multiplier
}

export default constants

dist.css

*{box-sizing:border-box}html{background:rgba(1,11,20,.5);padding:0;margin:0}body{background:#010b14;font-family:VT323,monospace;overflow:hidden;margin:0 auto;padding:0;position:relative;width:960px;height:500px}svg{display:block}h1{color:#dacbed;text-shadow:0 0 14px rgba(218,203,237,.75);text-transform:uppercase;font-weight:400;font-style:italic;position:absolute;width:100%;text-align:center;padding:0;margin:0;font-size:2.5em}.chart{width:50%;position:absolute;bottom:1em;right:.75em}.chart svg{shape-rendering:crispEdges;-webkit-filter:drop-shadow(0 0 14px rgba(15,208,254,.35));filter:drop-shadow(0 0 14px rgba(15,208,254,.35))}.chart svg path.car{fill:none;stroke:#0fd0fe}.chart svg g.car.leader circle{stroke:#fa6354}.chart svg g.car.leader text{fill:#fa6354}.chart svg g.car circle{fill:none;fill:#010b14;stroke:#dacbed}.chart svg g.car text{font-size:.9em;text-anchor:middle;fill:#dacbed}.chart svg .axis path{stroke:#018dae;stroke-dasharray:1 10}.chart svg .axis text{font-size:1.75em;font-family:VT323,monospace;fill:#018dae}.circuit{width:50%;position:relative;margin:0;padding:.5em 0 0 0}.circuit svg{margin:0 auto;shape-rendering:crispEdges;-webkit-filter:drop-shadow(0 0 14px rgba(15,208,254,.35));filter:drop-shadow(0 0 14px rgba(15,208,254,.35))}.circuit svg path.track{fill:none;stroke:#0fd0fe}.circuit svg .finish-line{stroke:#0fd0fe;stroke-width:2px}.circuit svg g.car.leader path{stroke:#fa6354}.circuit svg g.car.leader text{fill:#fa6354}.circuit svg g.car path{fill:#010b14;stroke:#dacbed}.circuit svg g.car text{font-size:1.125em;text-anchor:middle;fill:#dacbed}button{display:none}table{position:absolute;table-layout:fixed;font-size:1.125em;top:1.5em;right:1em;z-index:1;color:#0fd0fe;text-align:right;padding:.5em}table td,table th{padding:0 .75em}table th{font-weight:400}table td{color:#f6df83;text-shadow:0 0 14px rgba(246,223,131,.75)}table .leader{color:#fa6354;text-shadow:0 0 14px rgba(250,99,84,.75)}table .cyan{color:#018dae;text-shadow:0 0 14px rgba(1,141,174,.75)}table .white{color:#dacbed;text-shadow:0 0 14px rgba(218,203,237,.75)}table tbody tr td:nth-child(1){color:#018dae;text-shadow:0 0 14px rgba(1,141,174,.75);width:2.5em}table tbody tr td:nth-child(2){width:2.5em}table tbody tr td:nth-child(3){text-align:left;text-transform:uppercase;width:3.5em}table tbody tr td:nth-child(4){width:4em}table tbody tr td:nth-child(5){width:4em}table tbody tr td:nth-child(6){width:6em}table tbody tr td:nth-child(7){width:2.5em}

drawBoard.js

const formatTime = d3.timeFormat('%-M:%S.%L')

const rowCells = d => [
  ['', d.position],
  [d.position === 1 ? 'leader' : d.status, d.number],
  [d.position === 1 ? 'leader' : d.status, d.name],
  ['', d.gap || ''],
  ['', d.int || ''],
  ['', _.last(d.laps) ? formatTime(_.last(d.laps)) : ''],
  ['', Math.floor(_.last(d.measures).pct) || '']
]

const drawBoard = ({ cars, setup }) => {
  // console.log(JSON.stringify(cars, null, 2))

  // Only draw the board when a car starts a new lap.
  if (cars.filter(d => d.isNewLap).length || setup) {
    const tbody = d3.select('table.board tbody')

    // join
    const trJoin = tbody.selectAll('tr').data(cars)

    // update
    trJoin
      .selectAll('td')
      .data(rowCells)
      .attr('class', d => d[0])

    // immediately display non-new laps
    trJoin
      .transition()
      .duration(0)
      .filter(d => !d.isNewLap)
      .selectAll('td')
      .text(d => d[1])
      .style('visibility', 'visible')

    // stagger display new laps
    trJoin
      .transition()
      .duration(0)
      .filter(d => d.isNewLap)
      .selectAll('td')
      .text(d => d[1])
      .style('visibility', 'hidden')
      .transition()
      .delay((d, i) => 125 + i * 125)
      .style('visibility', 'visible')

    // enter
    const trEnter = trJoin.enter().append('tr')

    trEnter
      .selectAll('td')
      .data(rowCells)
      .enter()
      .append('td')
      .attr('class', d => d[0])
      .text(d => d[1])
  }
}

export default drawBoard

drawCars.js

const getTriangleTransform = d =>
  `rotate(${-d.coords.angle + 90} 0 0) scale(1 1.2)`

const getGTransform = d => `translate(${d.coords})`

const drawCars = ({ g, cars, getCoords }) => {
  const data = cars.map(car => ({
    ...car,
    coords: getCoords(_.last(car.measures).pct)
  }))

  // join
  const gJoin = g.selectAll('g.car').data(data, d => d.number)

  // update
  gJoin
    .attr('class', d => (d.position === 1 ? 'car leader' : 'car'))
    .attr('transform', getGTransform)
    .select('path')
    .attr('transform', getTriangleTransform)

  // enter
  const gEnter = gJoin
    .enter()
    .append('g')
    .attr('class', d => (d.position === 1 ? 'car leader' : 'car'))
    .attr('transform', getGTransform)

  gEnter
    .append('path')
    .attr('d', d =>
      d3
        .symbol()
        .type(d3.symbolTriangle)
        .size(400)()
    )
    .attr('transform', getTriangleTransform)

  gEnter
    .append('text')
    .text(d => d.number)
    .attr('dy', 5)

  // remove
  gJoin.exit().remove()
}

export default drawCars

drawTrack.js

import getAngle from './getAngle.js'

const drawTrack = ({ container, track }) => {
  const margin = 20
  const dimension = container.node().offsetWidth - margin * 2

  const xExtent = d3.extent(track, d => d.x)
  const x = d3.scaleLinear().domain(xExtent)

  const yExtent = d3.extent(track, d => d.y)
  const y = d3.scaleLinear().domain(yExtent)

  const aspect = (xExtent[1] - xExtent[0]) / (yExtent[1] - yExtent[0])

  const width = Math.min(dimension * aspect, dimension)
  const height = Math.min(dimension / aspect, dimension)

  x.range([0, width])
  y.range([0, height])

  const svg = container
    .append('svg')
    .attr('width', width + 2 * margin)
    .attr('height', height + 2 * margin)

  const g = svg.append('g').attr('transform', `translate(${margin}, ${margin})`)

  const line = d3
    .line()
    // .curve(d3.curveBasis)
    .x(d => x(d.x))
    .y(d => y(d.y))

  const path = g
    .append('path')
    .attr('class', 'track')
    .datum(track)
    .attr('d', line)
    .node()

  const totalLength = path.getTotalLength()

  const getCoords = pct => {
    const length = totalLength * (pct % 1)
    const delta = 1
    const offset = length + 1 > totalLength ? -delta : delta
    const axy = path.getPointAtLength(length)
    const bxy = path.getPointAtLength(length + offset)
    const a = [axy.x, axy.y]
    const b = [bxy.x, bxy.y]

    const angle = getAngle([a, b])

    const array = [...a]
    array.angle = angle

    return array
  }

  const delta = 0.01
  const before = getCoords(1 - delta)
  const zero = getCoords(0)
  const after = getCoords(delta)

  g
    .append('line')
    .attr('class', 'finish-line')
    .attr('x1', before[0])
    .attr('y1', before[1])
    .attr('x2', after[0])
    .attr('y2', after[1])
    .attr('transform', `rotate(90, ${zero})`)

  return { g, getCoords }
}

export default drawTrack

drivers.js

var drivers = [
  {
    entrant: 'Scuderia Ferrari',
    constructor: 'Ferrari',
    chassis: 'TBA',
    power: 'Ferrari',
    tyres: 'P',
    number: '5',
    standing: 2,
    name: 'Sebastian Vettel'
  },
  {
    entrant: 'Scuderia Ferrari',
    constructor: 'Ferrari',
    chassis: 'TBA',
    power: 'Ferrari',
    tyres: 'P',
    number: '7',
    standing: 4,
    name: 'Kimi Räikkönen'
  },
  {
    entrant: 'Sahara Force India F1 Team',
    constructor: 'Force India-Mercedes',
    chassis: 'TBA',
    power: 'Mercedes',
    tyres: 'P',
    number: '11',
    standing: 7,
    name: 'Sergio Pérez'
  },
  {
    entrant: 'Sahara Force India F1 Team',
    constructor: 'Force India-Mercedes',
    chassis: 'TBA',
    power: 'Mercedes',
    tyres: 'P',
    number: '31',
    standing: 8,
    name: 'Esteban Ocon'
  },
  {
    entrant: 'Haas F1 Team',
    constructor: 'Haas-Ferrari',
    chassis: 'TBA',
    power: 'Ferrari',
    tyres: 'P',
    number: '8',
    standing: 13,
    name: 'Romain Grosjean'
  },
  {
    entrant: 'Haas F1 Team',
    constructor: 'Haas-Ferrari',
    chassis: 'TBA',
    power: 'Ferrari',
    tyres: 'P',
    number: '20',
    standing: 14,
    name: 'Kevin Magnussen'
  },
  {
    entrant: 'McLaren F1 Team',
    constructor: 'McLaren-Renault',
    chassis: 'TBA',
    power: 'Renault',
    tyres: 'P',
    number: '2',
    standing: 16,
    name: 'Stoffel Vandoorne'
  },
  {
    entrant: 'McLaren F1 Team',
    constructor: 'McLaren-Renault',
    chassis: 'TBA',
    power: 'Renault',
    tyres: 'P',
    number: '14',
    standing: 15,
    name: 'Fernando Alonso'
  },
  {
    entrant: 'Mercedes AMG Petronas Motorsport',
    constructor: 'Mercedes',
    chassis: 'TBA',
    power: 'Mercedes',
    tyres: 'P',
    number: '44',
    standing: 1,
    name: 'Lewis Hamilton'
  },
  {
    entrant: 'Mercedes AMG Petronas Motorsport',
    constructor: 'Mercedes',
    chassis: 'TBA',
    power: 'Mercedes',
    tyres: 'P',
    number: '77',
    standing: 3,
    name: 'Valtteri Bottas'
  },
  {
    entrant: 'Aston Martin Red Bull Racing',
    constructor: 'Red Bull Racing-TAG Heuer',
    chassis: 'RB14',
    power: 'TAG Heuer',
    tyres: 'P',
    number: '3',
    standing: 5,
    name: 'Daniel Ricciardo'
  },
  {
    entrant: 'Aston Martin Red Bull Racing',
    constructor: 'Red Bull Racing-TAG Heuer',
    chassis: 'RB14',
    power: 'TAG Heuer',
    tyres: 'P',
    number: '33',
    standing: 6,
    name: 'Max Verstappen'
  },
  {
    entrant: 'Renault Sport Formula One Team',
    constructor: 'Renault',
    chassis: 'TBA',
    power: 'Renault',
    tyres: 'P',
    number: '27',
    standing: 10,
    name: 'Nico Hülkenberg'
  },
  {
    entrant: 'Renault Sport Formula One Team',
    constructor: 'Renault',
    chassis: 'TBA',
    power: 'Renault',
    tyres: 'P',
    number: '55',
    standing: 9,
    name: 'Carlos Sainz Jr.'
  },
  {
    entrant: 'Alfa Romeo Sauber F1 Team',
    constructor: 'Sauber-Ferrari',
    chassis: 'TBA',
    power: 'Ferrari',
    tyres: 'P',
    number: '9',
    standing: 20,
    name: 'Marcus Ericsson'
  },
  {
    entrant: 'Alfa Romeo Sauber F1 Team',
    constructor: 'Sauber-Ferrari',
    chassis: 'TBA',
    power: 'Ferrari',
    tyres: 'P',
    number: '16',
    standing: 26,
    name: 'Charles Leclerc'
  },
  {
    entrant: 'Red Bull Toro Rosso Honda',
    constructor: 'Scuderia Toro Rosso-Honda',
    chassis: 'TBA',
    power: 'Honda',
    tyres: 'P',
    number: '10',
    standing: 21,
    name: 'Pierre Gasly'
  },
  {
    entrant: 'Red Bull Toro Rosso Honda',
    constructor: 'Scuderia Toro Rosso-Honda',
    chassis: 'TBA',
    power: 'Honda',
    tyres: 'P',
    number: '28',
    standing: 23,
    name: 'Brendon Hartley'
  },
  {
    entrant: 'Williams Martini Racing',
    constructor: 'Williams-Mercedes',
    chassis: 'TBA',
    power: 'Mercedes',
    tyres: 'P',
    number: '18',
    standing: 12,
    name: 'Lance Stroll'
  }
]

getAngle.js

const getAngle = ([a, b]) => {
  const x = b[0] - a[0]
  const y = -(b[1] - a[1])
  const angle = Math.atan2(y, x) * 180 / Math.PI
  return angle
}

export default getAngle

getRandomArbitrary.js

const getRandomArbitrary = (min, max) => Math.random() * (max - min) + min

export default getRandomArbitrary

moveCar.js

import constants from './constants.js'
import getRandomArbitrary from './getRandomArbitrary.js'

const moveCar = ({ car, delta }) => {
  const { measures } = car
  const random = 0.02

  const lastTwo = measures.slice(-2)

  // Figure out if we started a new lap.
  const isNewLap = Math.floor(lastTwo[0].pct) !== Math.floor(lastTwo[1].pct)

  // Calculate new speed.
  const newSpeed = _.clamp(
    lastTwo[1].speed * getRandomArbitrary(1 - random, 1 + random),
    constants.MIN,
    constants.MAX
  )

  // Calculate new pct.
  const newPct = lastTwo[1].pct + delta * lastTwo[1].speed / 3600000

  return {
    ...car,
    isNewLap,
    measures: [
      ...measures,
      {
        pct: newPct,
        speed: newSpeed
      }
    ].slice(isNewLap ? -2 : 0)
  }
}

export default moveCar

package.json

{
  "standard": {
    "globals": [
      "drivers",
      "circuitTracks",
      "d3",
      "_"
    ]
  }
}

style.styl

$black = #010b14
$white = #dacbed
$blue = #0fd0fe
$yellow = #f6df83
$lightblue = darken($blue, 35%)
$red = lighten(#f11c07, 33%)

$text-blur-radius = 14px
$text-blur-opacity = 0.75
$svg-blur-opacity = 0.35
$font-family = 'VT323', monospace

*
  box-sizing border-box
  
html
  background alpha($black, 0.5)
  padding 0
  margin 0
  
body
  background $black
  font-family $font-family
  overflow hidden
  margin 0 auto
  padding 0
  position relative
  width 960px
  height 500px
  // border solid $white 1px

svg
  display block
  
h1
  // visibility hidden
  color $white
  text-shadow 0 0 $text-blur-radius alpha($white, $text-blur-opacity)
  text-transform uppercase
  font-weight normal
  font-style italic
  position absolute
  width 100%
  text-align center
  padding 0
  margin 0
  font-size 2.5em

.chart
  width 50%
  position absolute
  bottom 1em
  right 0.75em
  
  svg
    shape-rendering crispEdges
    filter drop-shadow(0 0 $text-blur-radius alpha($blue, $svg-blur-opacity))
    
    path.car
      fill none
      stroke $blue

    g.car
      
      &.leader
        circle
          stroke $red
        text
          fill $red
      
      circle
        fill none
        fill $black
        stroke $white

      text
        font-size 0.9em
        text-anchor middle
        fill $white
      
    .axis
      path
        stroke $lightblue
        stroke-dasharray 1 10
      text
        font-size 1.75em
        font-family $font-family
        fill $lightblue

.circuit
  width 50%
  position relative
  margin 0
  padding 0.5em 0 0 0

  svg
    margin 0 auto
    shape-rendering crispEdges
    filter drop-shadow(0 0 $text-blur-radius alpha($blue, $svg-blur-opacity))
    
    path.track
      fill none
      stroke $blue
            
    .finish-line
      stroke $blue
      stroke-width 2px
      
    g.car
      &.leader
        path
          stroke $red
        text
          fill $red
      
      path
        fill $black
        stroke $white
          
      text
        font-size 1.125em
        text-anchor middle
        fill $white
        
button
  display none

table
  // visibility hidden
  position absolute
  table-layout fixed
  font-size 1.125em
  top 1.5em
  right 1em
  z-index 1
  color $blue
  text-align right
  padding 0.5em
  td, th
    padding 0 0.75em
  th
    font-weight normal
  td
    color $yellow
    text-shadow 0 0 $text-blur-radius alpha($yellow, $text-blur-opacity)
  .leader
    color $red
    text-shadow 0 0 $text-blur-radius alpha($red, $text-blur-opacity)
  .cyan
    color $lightblue
    text-shadow 0 0 $text-blur-radius alpha($lightblue, $text-blur-opacity)
  .white
    color $white
    text-shadow 0 0 $text-blur-radius alpha($white, $text-blur-opacity)
  tbody
    tr
      td:nth-child(1)
        color $lightblue
        text-shadow 0 0 $text-blur-radius alpha($lightblue, $text-blur-opacity)
        width 2.5em
      td:nth-child(2)
        width 2.5em
      td:nth-child(3)
        text-align left
        text-transform uppercase
        width 3.5em
      td:nth-child(4)
        width 4em
      td:nth-child(5)
        width 4em
      td:nth-child(6)
        width 6em
      td:nth-child(7)
        width 2.5em