block by jeremycflin 3bc504c0ba2f797fed81326876ecb6ca

Lakeland 50 Splits

Full Screen

A parallel coordinates plot of the time taken for each of the 723 athletes who ran the Lakeland 50 between the 8 checkpoints.

Click and drag on an checkpoint ‘y axis’ to filter the results to just include those that reached the checkpoint at that time.

forked from ColinEberhardt‘s block: Lakeland 50 Splits

script.js

'use strict';

function parseTime(time) {
  if (!time) {
    return undefined;
  }
  var parts = time.split(':');
  if (parts.length !== 3) {
    return undefined;
  }
  parts = parts.map(Number);
  return parts[0] * 60 + parts[1] + parts[0] / 60;
}

d3.csv('results.csv', function (row, _, columns) {
  // parse the times into seconds
  var checkpoints = columns.slice(3);
  checkpoints.forEach(function (checkpoint) {
    row[checkpoint] = parseTime(row[checkpoint]);
  });
  return row;
}, csvLoaded);

var keyValueToObject = function keyValueToObject(keyValues) {
  var obj = {};
  keyValues.forEach(function (k) {
    obj[k.key] = k.value;
  });
  return obj;
};

function csvLoaded(error, data) {
  if (error) {
    console.error(error);
  }

  // compute the checkpoint deltas
  var checkpoints = data.columns.slice(3);
  data.forEach(function (row) {
    checkpoints.forEach(function (checkpoint, index) {
      if (index > 1) {
        row[checkpoint + '-Delta'] = row[checkpoint] - row[checkpoints[index - 1]];
      } else {
        row[checkpoint + '-Delta'] = row[checkpoint];
      }
    });
  });
  var checkpointFilter = {
    checkpoint: checkpoints[0],
    values: [0, 0],
    valuesDomain: [0, 0]
  };

  // compute the scale domians
  var yExtent = fc.extentLinear().pad([0.1, 0.1]);
  var yScales = checkpoints.map(function (checkpoint) {
    return d3.scaleLinear().domain(yExtent(data.map(function (d) {
      return d[checkpoint + '-Delta'];
    })));
  });

  var xScale = d3.scalePoint().domain(checkpoints);

  var lineData = d3.line().defined(function (d) {
    return d.value;
  }).x(function (d) {
    return xScale(d.checkpoint);
  }).y(function (d, i) {
    return yScales[i](d.value);
  });

  var rowToLine = function rowToLine(row) {
    var arr = checkpoints.map(function (checkpoint) {
      return {
        value: row[checkpoint + '-Delta'],
        checkpoint: checkpoint
      };
    });
    return lineData(arr);
  };

  var brush = d3.brushY().on('brush', function (d, i) {
    if (d3.event.sourceEvent && d3.event.sourceEvent.type === 'draw') return;
    checkpointFilter = {
      checkpoint: d,
      values: d3.event.selection,
      valuesDomain: d3.event.selection.map(yScales[i].invert)
    };
    d3.select('#chart').node().requestRedraw();
  });

  var xScaleLocation;

  d3.select('#chart').on('measure', function (d, i, nodes) {
    yScales.forEach(function (scale) {
      return scale.range([event.detail.height, 0]);
    });
    xScale.range([0, event.detail.width]);
    brush.extent([[-8, 0], [8, event.detail.height]]);
    xScaleLocation = event.detail.height;
  }).on('draw', function (d, i, nodes) {
    var svg = d3.select(nodes[i]).select('svg');

    var pathJoin = fc.dataJoin('g', 'run');
    var join = pathJoin(svg, data);

    join.enter().append('path');
    join.select('path').attr('d', rowToLine);
    join.classed('highlight', function (d) {
      return d[checkpointFilter.checkpoint + '-Delta'] > checkpointFilter.valuesDomain[1] && d[checkpointFilter.checkpoint + '-Delta'] < checkpointFilter.valuesDomain[0];
    });

    join.enter().append('text').text(function (d) {
      return d.Name;
    });
    join.select('text').attr('transform', function (d) {
      return 'translate(-5, ' + yScales[0](d[checkpoints[0]]) + ')';
    }

    // render the y-axes
    );var axisJoin = fc.dataJoin('g', 'y-axis');
    axisJoin(svg, yScales).each(function (d, index, group) {
      var axis = d3.axisRight().scale(d);
      d3.select(group[index]).attr('transform', 'translate(' + xScale(checkpoints[index]) + ', 0)').call(axis);
    });

    // render the x-axis
    var xAxisJoin = fc.dataJoin('g', 'x-axis');
    xAxisJoin(svg, [0]).classed('x-scale', true).call(d3.axisBottom().scale(xScale)).attr('transform', 'translate(0, ' + xScaleLocation + ')');

    // render the brushes
    var brushJoin = fc.dataJoin('g', 'brush');

    brushJoin(svg, checkpoints).attr('transform', function (d, i) {
      return 'translate(' + xScale(checkpoints[i]) + ', 0)';
    }).call(brush).call(brush.move, function (d) {
      if (checkpointFilter.checkpoint === d) {
        return checkpointFilter.values;
      } else {
        return undefined;
      }
    }).selectAll("rect").attr("x", -8).attr("width", 16);
  }).node().requestRedraw();
}

index.html

<!DOCTYPE html>
<html>
  <script src="https://unpkg.com/d3@4.6.0"></script>
  <script src="https://unpkg.com/d3fc@13.0.1"></script>
  <style>
    body {
      font-family: sans-serif;
    }
    g.run path {
      fill: none;
      stroke: black;
      stroke-width: 0.02;
    }
    g.run text {
      text-anchor: end;
      font-size: 10px;
      opacity: 0.00;
    }
    g.run.highlight text {
      opacity: 0.5;
    }
    g.run.highlight path {
      stroke: steelblue;
      stroke-width: 1;
      opacity: 1;
    }
    .x-scale path {
      display: none;
    }
  </style>
  <body>
    <d3fc-group auto-resize>
      <d3fc-svg id='chart' style='width: 85%; height: 400px; display: block; margin: 50px; margin-left: 100px'/>
    </d3fc-group>
    <script src="script.js"></script>
  </body>
</html>