Pop open in new, full screen window to see the line get drawn as you scroll, connecting the points.
Built with blockbuilder.org
| license: mit |
Pop open in new, full screen window to see the line get drawn as you scroll, connecting the points.
Built with blockbuilder.org
| <html> | |
| <head> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.1/lodash.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.js" charset="utf-8"></script> | |
| <style> | |
| * { | |
| font-family: Helvetica, Verdana, sans-serif; | |
| } | |
| .marker { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 400px; | |
| border: 1px solid #ccc; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <svg width="1000" height="20000"></svg> | |
| <div class="marker"></div> | |
| <script> | |
| var data = {categoryX: 300, points: [ | |
| {"x":300,"y":53.46},{"x":300,"y":64.33},{"x":300,"y":81.25},{"x":200,"y":169.69},{"x":300,"y":169.81},{"x":300,"y":209.83},{"x":300,"y":258.2},{"x":100,"y":352.41},{"x":300,"y":365.26},{"x":300,"y":437.33},{"x":300,"y":556.63},{"x":300,"y":589.71},{"x":300,"y":626.93},{"x":300,"y":649.84},{"x":300,"y":666.02},{"x":300,"y":689.28},{"x":300,"y":705.31},{"x":300,"y":727.3},{"x":300,"y":744.38},{"x":300,"y":766.83},{"x":300,"y":817.81},{"x":300,"y":862.84},{"x":400,"y":882.63},{"x":300,"y":1036.55},{"x":300,"y":1192.58},{"x":300,"y":1419.56},{"x":300,"y":1566.87},{"x":300,"y":8667.14},{"x":300,"y":8780.15},{"x":300,"y":1784.1},{"x":300,"y":1827.76},{"x":300,"y":1855.53},{"x":300,"y":1890.78},{"x":300,"y":1939.6},{"x":300,"y":1997.51},{"x":300,"y":1169.01},{"x":300,"y":2314.37},{"x":300,"y":2399.17},{"x":300,"y":2594.91},{"x":500,"y":2756.56},{"x":200,"y":3109.02}]}; | |
| var svg = d3.select('svg'); | |
| var distancePath = svg.append('path') | |
| .attr('fill', 'none') | |
| .attr('stroke', 'none').node(); | |
| var source; | |
| var target; | |
| var gap = 100; | |
| var totalDistance = 0; | |
| _.each(data.points, function(target) { | |
| if (!source) { | |
| target.d = 'M' + target.x + ',' + target.y; | |
| } else { | |
| if (source.y > target.y - gap) { | |
| // if they're sufficiently close to each other | |
| if (source.x === target.x) { | |
| target.d = drawLine(target.x, target.y); | |
| setDistance(source, target); | |
| } else { | |
| target.d = drawCurve(source.x, source.y, target.x, target.y); | |
| setDistance(source, target); | |
| target.y1 = target.y - source.y; | |
| target.interpolate1 = d3.interpolate(0, target.distance); | |
| } | |
| } else { | |
| target.d = ''; | |
| if (source.x !== data.categoryX) { | |
| // if the source repo owner isn't the same as the contributor, move the line back | |
| target.d += drawCurve(source.x, source.y, data.categoryX, source.y + gap / 3); | |
| setDistance(source, target); | |
| target.y1 = gap / 3; | |
| target.interpolate1 = d3.interpolate(0, target.distance); | |
| } | |
| x = target.x; | |
| y = target.y; | |
| if (target.x !== data.categoryX) { | |
| x = data.categoryX; | |
| y = target.y - gap / 3; | |
| } | |
| target.d += drawLine(x, y); | |
| target.y2 = y - (target.y1 || 0); | |
| setDistance(source, target); | |
| if (target.x !== data.categoryX) { | |
| target.d += drawCurve(x, y, target.x, target.y); | |
| var currentDistance = target.distance; | |
| setDistance(source, target); | |
| target.y3 = gap / 3; | |
| target.interpolate2 = d3.interpolate(0, target.distance - currentDistance); | |
| } | |
| } | |
| target.totalDistance = (totalDistance += target.distance); | |
| } | |
| source = target; | |
| }); | |
| var path = svg.append('path') | |
| .attr('d', _.pluck(data.points, 'd').join(' ')) | |
| .attr('fill-opacity', 0) | |
| .attr('stroke', '#000000') | |
| .attr('stroke-width', 2) | |
| .attr('stroke-linecap', 'round') | |
| .attr('stroke-dasharray', totalDistance) | |
| .attr('stroke-dashoffset', totalDistance); | |
| var circle = svg.selectAll('circle') | |
| .data(data.points).enter().append('circle') | |
| .attr('fill', '#fff') | |
| .attr('stroke', '#000000') | |
| .attr('stroke-width', 1) | |
| .attr('cx', function(d) {return d.x}) | |
| .attr('cy', function(d) {return d.y}) | |
| .attr('r', 4); | |
| window.addEventListener('scroll', _.throttle(windowScroll, 200)); | |
| function windowScroll() { | |
| var top = scrollY + 400; | |
| var source; | |
| var target = _.find(data.points, function(point) { | |
| if (point.y >= top) { | |
| return true; | |
| } | |
| source = point; | |
| return false; | |
| }); | |
| if (source && target) { | |
| var distance = 0; | |
| var distanceFromSource = top - source.y; | |
| if (!target.interpolate1) { | |
| // if there's no interpolate1 | |
| if (!target.interpolate2) { | |
| // and there's no interpolate2, must mean it's a straight line | |
| distance = distanceFromSource + source.totalDistance; | |
| } else { | |
| // if there's a interpolate2, must mean there's a straight line | |
| // and then a curve at the end, so figure out if we're in straight line or curve part | |
| if (distanceFromSource <= target.y2) { | |
| // it's in straight line part | |
| distance = distanceFromSource + source.totalDistance; | |
| } else { | |
| // if it's in last curve part, first interpolate the curve | |
| // and then add that back to the straight part and the previous total distance | |
| var partialDistance = (distanceFromSource - target.y2) / target.y3; | |
| distance = target.interpolate2(partialDistance) + target.y2 + source.totalDistance; | |
| } | |
| } | |
| } else { | |
| // if there's interpolate1, must mean there's a first curve | |
| if (distanceFromSource <= target.y1) { | |
| // so if it's within the first curve, interpolate that and add it to total distance | |
| var partialDistance = distanceFromSource / target.y1; | |
| distance = target.interpolate1(partialDistance) + source.totalDistance; | |
| } else if (distanceFromSource <= (target.y1 + target.y2)) { | |
| // if we're in line part, add curve to it | |
| distance = target.interpolate1(1) + (distanceFromSource - target.y1) + source.totalDistance; | |
| } else if (interpolate2) { | |
| var partialDistance = (distanceFromSource - target.y2 - target.y1) / target.y3; | |
| distance = target.interpolate1(1) + target.y2 + target.interpolate2(partialDistance); | |
| } | |
| } | |
| // var partialDistance = (top - source.y) / (target.y - source.y); | |
| // var distance = target.interpolater(partialDistance) + source.totalDistance; | |
| path.transition().duration(200) | |
| .attr('stroke-dashoffset', totalDistance - (distance || 0)); | |
| } | |
| }; | |
| function drawLine(x, y) { | |
| return 'L' + x + ',' + y; | |
| } | |
| function drawCurve(x1, y1, x2, y2) { | |
| var cy = (y1 + y2) / 2; | |
| return 'C' + x1 + ',' + cy + ' ' + x2 + ',' + cy + ' ' + x2 + ',' + y2; | |
| } | |
| function setDistance(source, target) { | |
| var distancePathD = 'M' + source.x + ',' + source.y + ' ' + target.d; | |
| distancePath.setAttribute('d', distancePathD); | |
| target.distance = parseFloat(distancePath.getTotalLength().toFixed(2)); | |
| } | |
| </script> | |
| </body> | |
| </html> |