Skip to content

Instantly share code, notes, and snippets.

@sxywu
Last active October 3, 2016 07:24
Show Gist options
  • Save sxywu/0909fba07ffd7ec9a67a619d6cb5772d to your computer and use it in GitHub Desktop.
Save sxywu/0909fba07ffd7ec9a67a619d6cb5772d to your computer and use it in GitHub Desktop.
DS Aug, Code 4
license: mit
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.15.0/lodash.min.js"></script>
<style>
</style>
</head>
<body>
<canvas id='canvas'></canvas>
<script>
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
// data from olympics diving
var data = [{
"country": "China",
"athletes": ["A. Chen", "Y. Lin"],
"total": 496.98,
"breakdown": [
[2, 56.4, [9.5,9.5,9.5,9,9.5,9]],
[2, 52.20, [9.0,9.5,10.0,7.0,7.5,8.0]],
[3.4, 85.68, [7.5,7.0,8.5,8.5,8.0,8.5]],
[3.4, 88.74, [8.0,8.0,8.0,9.0,8.5,8.5]],
[3.8, 104.88, [9.0,9.0,9.0,8.5,8.5,8.5]],
[3.3, 89.10, [8.5,8.5,8.5,9.0,9.5,8.5]]
]
}];
// properties
var padding = 25;
var width = canvas.width = window.innerWidth;
var height = canvas.height = 1200;
var colors = {'China': [[255,0,0], [255,255,0]]};
var TWO_PI = 2 * Math.PI;
var maxRadius = 50;
var radiusScale = d3.scaleLinear().range([5, maxRadius]);
var yScale = d3.scaleLinear().range([0, 1]);
function processData(data) {
_.each(data, function(d) {
// create an artifical first score
d.breakdown.unshift([1, 1, [10, 10, 10, 10, 10, 10]]);
d.processed = _.map(d.breakdown, function(score) {
return _.map(score[2], function(num) {return num * score[0]});
});
});
var difficulty = _.chain(data).map('breakdown').flatten().map(0).value();
var maxRadius = _.max(difficulty);
var scores = _.chain(data).map('processed').flattenDeep().value();
var minY = _.min(scores);
var maxY = _.max(scores);
radiusScale.domain([1, maxRadius]);
yScale.domain([minY, maxY]);
}
// generate the flow line, given one event for one team
function generateFlowData(team) {
var gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, maxRadius);
gradient.addColorStop(1, 'rgba(' + colors[team.country][0] + ',0.2)');
gradient.addColorStop(0, 'rgba(' + colors[team.country][1] + ',0.2)');
return {
centerX: padding,
centerY: (width / 2) + padding + maxRadius,
color: gradient,
// globalPhase: (team.total / 1000) * TWO_PI, // globalPhase is for yOffset
radii: _.map(team.breakdown, function(scores) {
return radiusScale(scores[0]);
}),
points: _.map(team.processed, function(scores) {
return generateCircleData(scores);
}),
length: _.map(team.breakdown, function(scores) {
return Math.round(scores[1]) * 4;
}),
rotations: _.map(team.breakdown, function(scores) {
return scores[1] / team.total;
}),
totalLength: team.total * 4,
elapsed: 0
}
}
// generate the data for just one of the circles in the flow line
// majority of this function is taken from
// Dan Gries's tutorial http://rectangleworld.com/blog/archives/462
// in particular the function setLinePoints
function generateCircleData(scores) {
var circle = {
first: {x: 0, y: 1}
};
var last = {x: 1, y: 1};
var minY = maxY = 1;
var point, nextPoint;
var dx, newX, newY;
// connect first point with the last
circle.first.next = last;
_.each(scores, function(score) {
point = circle.first;
while (point.next) {
nextPoint = point.next;
dx = nextPoint.x - point.x;
newX = 0.5 * (point.x + nextPoint.x);
newY = 0.5 * (point.y + nextPoint.y);
// vary the y-pos by the score, but subtract it
// by what is around the mid-point so that
// some are positive and others are negative
newY += dx * (yScale(score) * 2 - 1);
var newPoint = {x: newX, y: newY};
//min, max
if (newY < minY) {
minY = newY;
}
else if (newY > maxY) {
maxY = newY;
}
// insert mid-point
newPoint.next = nextPoint;
point.next = newPoint;
point = nextPoint;
}
})
// normalize to values between 0 and 1
if (maxY != minY) {
var normalizeRate = 1/(maxY - minY);
point = circle.first;
while (point != null) {
point.y = normalizeRate*(point.y - minY);
point = point.next;
}
}
return circle;
}
function tweenPoints(circle1, circle2) {
// interpolate all the points of the circles
var interpolators = _.map(circle1, function(point1, i) {
return {
x: d3.interpolate(point1.x, circle2[i].x),
y: d3.interpolate(point1.y, circle2[i].y)
};
});
return function(t) {
return _.map(interpolators, function(interpolate) {
return {x: interpolate.x(t), y: interpolate.y(t)};
});
};
}
// given set of points making up a squiggly line
// turn it into a squiggly imperfect circle
// also calculate the interpolators for them
function calculateCircles(flow) {
flow.circles = [];
flow.interpolators = [];
var prevCircle = null;
_.each(flow.points, function(points, i) {
// calculate circles
var point = points.first;
var rotation = flow.rotations[i];
var radii = flow.radii[i];
var circle = [];
var theta = TWO_PI * (point.x + rotation);
var radius = radii * point.y;
var x = radius * Math.cos(theta);
var y = radius * Math.sin(theta);
circle.push({x: x, y: y});
while (point.next) {
point = point.next;
// given its x and y, calculate its theta and radius
var theta = TWO_PI * (point.x + rotation);
var radius = radii * point.y;
var x = radius * Math.cos(theta);
var y = radius * Math.sin(theta);
circle.push({x: x, y: y});
}
flow.circles.push(circle);
// now calculate the interpolators
if (prevCircle) {
var interpolators = tweenPoints(prevCircle, circle);
flow.interpolators.push(interpolators);
}
prevCircle = circle;
});
}
function drawCircle(elapsed, flow) {
var drawCount;
elapsed = parseInt(elapsed);
elapsed = d3.easeQuad(elapsed / flow.totalLength);
elapsed = parseInt(flow.totalLength * elapsed);
if (elapsed < flow.elapsed) {
// if it's going backwards, clear the canvas
// and set everything back to 0
console.log(elapsed, flow.elapsed)
ctx.clearRect(0, 0, width, height);
flow.elapsed = 0;
}
_.times(elapsed - flow.elapsed, function(t) {
t += flow.elapsed;
drawCount = t;
_.some(flow.interpolators, function(interpolator, i) {
var length = flow.length[i + 1];
if (t > length) {
// if elapsed is more than length of section
// subtract length and move to next interpolator
t -= length;
return false;
}
// else this is the interpolator to use
ctx.strokeStyle = flow.color;
ctx.beginPath();
flow.centerX += 0.5;
var yOffset = 40 * Math.sin(drawCount/600*TWO_PI);
ctx.setTransform(1, 0, 0, 1, flow.centerY + yOffset, flow.centerX);
var points = interpolator(t / length);
_.each(points, function(pos) {
ctx.lineTo(pos.x, pos.y);
});
ctx.closePath();
ctx.stroke();
return true;
});
});
flow.elapsed = elapsed;
}
processData(data);
var flow = generateFlowData(data[0]);
calculateCircles(flow);
var t = d3.timer(function(elapsed) {
drawCircle(elapsed, flow);
if (elapsed > flow.totalLength) t.stop();
});
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment