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> |