Built with blockbuilder.org
forked from sxywu's block: DS Aug, Code 1
forked from sxywu's block: DS Aug, Code 1
forked from sxywu's block: DS Aug, Code 3
license: mit |
Built with blockbuilder.org
forked from sxywu's block: DS Aug, Code 1
forked from sxywu's block: DS Aug, Code 1
forked from sxywu's block: DS Aug, Code 3
<!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> |