Last active
November 3, 2015 13:43
-
-
Save biovisualize/549bfceb0938859fea34 to your computer and use it in GitHub Desktop.
streaming line chart with animation and long-scrolling
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE HTML> | |
<html> | |
<head> | |
<meta http-equiv="content-type" content="text/html; charset=UTF-8"> | |
<script type='text/javascript' src="http://d3js.org/d3.v3.min.js"></script> | |
<style> | |
div{ | |
box-sizing: border-box; | |
} | |
.gallery-container{ | |
position: absolute; | |
height: 100px; | |
width: 1000px; | |
overflow:hidden; | |
} | |
.gallery-slide{ | |
height: 125px; | |
width: 1000px; | |
overflow:auto; | |
white-space: nowrap; | |
/*font-size: 0;*/ | |
} | |
.handle{ | |
top: 0px; | |
width: 50px; | |
height: 100px; | |
position: absolute; | |
background: url(handle.png); | |
background-repeat: no-repeat; | |
background-size: 50px 100%; | |
} | |
.handle.right{ | |
right: 0px; | |
-moz-transform:rotate(180deg); | |
-webkit-transform:rotate(180deg); | |
-o-transform:rotate(180deg); | |
transform:rotate(180deg); | |
-ms-transform:rotate(180deg); | |
} | |
.panel{ | |
position: relative; | |
top: 0; | |
width: 1000px; | |
height: 100px; | |
z-index: -1; | |
display: inline-block; | |
text-align: center; | |
font-size: 50px; | |
margin: -2px; /*hack*/ | |
} | |
.before{ | |
background-color: orange; | |
} | |
.current{ | |
background: | |
radial-gradient(circle at 0% 50%, rgba(96, 16, 48, 0) 9px, #613 10px, rgba(96, 16, 48, 0) 11px) 0px 10px, | |
radial-gradient(at 100% 100%, rgba(96, 16, 48, 0) 9px, #613 10px, rgba(96, 16, 48, 0) 11px), | |
#8a3; | |
background-size: 20px 20px; | |
} | |
.after{ | |
background-color: skyblue; | |
} | |
</style> | |
<script src="../lib/pixi.dev.js"></script> | |
</head> | |
<body> | |
<div class="gallery-container"> | |
<div class="gallery-slide"> | |
<div class="handle left"></div> | |
<div class="handle right"></div> | |
<div class="panel before"><canvas></canvas></div> | |
<div class="panel current"><canvas></canvas></div> | |
<div class="panel after"><canvas></canvas></div> | |
</div> | |
</div> | |
<script> | |
/* | |
super cool features: | |
-scroll without redraw | |
-long scrolling with lazy loading | |
-adjustable speed | |
-requestAnimationFrame: smooth, stops when hidden | |
-could be adjusted for more granular partial loading []|[][][][]|[] | |
-webgl with canvas fallback | |
*/ | |
// array of data arrays | |
var DataGenerator = function(){ | |
var config = {datacount: 100, min: 0, max: 1000}; | |
var lastDatum; | |
var exports = {}; | |
exports.generate = function(){ | |
var data = d3.range(config.datacount).map(function(d, i){ | |
if(i === 0 && typeof lastDatum !== 'undefined') return {y: lastDatum.y, x: i}; | |
return {y: ~~((Math.random()*(config.max - config.min) - config.min) * 100) / 100, x: i}; | |
}); | |
lastDatum = data[data.length-1]; | |
return data; | |
}; | |
return exports; | |
}; | |
var dataGenerator = DataGenerator(); | |
// 3 canvases | |
var chartW = 1000, chartH = 100; | |
var scaleX = d3.scale.linear().domain([0, 100]).range([0, chartW]); | |
var scaleY = d3.scale.linear().domain([0, 1000]).range([chartH, 0]); | |
var canvases = []; | |
function renderCanvas(_selector){ | |
var data = dataGenerator.generate(); | |
var canvasSelection = d3.select(_selector).attr({width: chartW, height: chartH}); | |
var canvasNode = canvasSelection.node(); | |
var ctx = canvasNode.getContext('2d'); | |
ctx.globalCompositeOperation = "source-over"; | |
ctx.fillStyle = 'aliceblue'; | |
ctx.strokeStyle = 'orange'; | |
ctx.clearRect(0, 0, canvasNode.width, canvasNode.height); | |
ctx.fillRect(0, 0, chartW, chartH); | |
ctx.strokeRect(0, 0, chartW, chartH); | |
ctx.strokeStyle = 'skyblue'; | |
ctx.beginPath(); | |
var x, y, i; | |
for(i=0; i<data.length; i++){ | |
x = ~~scaleX(data[i].x); | |
y = ~~scaleY(data[i].y); | |
ctx.lineTo(x, y); | |
} | |
ctx.stroke(); | |
canvases.push(canvasNode); | |
} | |
renderCanvas('.before canvas'); | |
renderCanvas('.current canvas'); | |
renderCanvas('.after canvas'); | |
// plot/cloneNode before/current/after canvases on jump | |
var containerW = d3.select('.gallery-container').node().getBoundingClientRect().width; | |
var slideSelection = d3.select('.gallery-slide'); | |
var slideEl = slideSelection.node(); | |
d3.select('.gallery-slide').node().scrollLeft = containerW; | |
function scrollLeftTween(scrollLeft) { | |
return function() { | |
var i = d3.interpolateNumber(this.scrollLeft, scrollLeft); | |
return function(t) { | |
this.scrollLeft = i(t); | |
}; | |
}; | |
} | |
var shiftStepCount = 100; | |
var scrollDuration = 10000; | |
var shiftStepSize = containerW/shiftStepCount; | |
var shiftDuration = scrollDuration/shiftStepCount; | |
var canvasIndices = [0, 1, 2]; | |
function shiftPanel(_isGoingRight, _duration, _easingFuncName, _stepNum){ | |
if(typeof _isGoingRight === 'undefined') _isGoingRight = true; | |
if(typeof _stepNum === 'undefined') _stepNum = 1; | |
var directionSign = (_isGoingRight) ? 1 : -1; | |
if(slideEl.scrollLeft >= containerW * 2 - 2 || slideEl.scrollLeft <= 2){ | |
slideSelection.interrupt(); | |
(directionSign === 1) ? canvasIndices.push(canvasIndices.shift()) : canvasIndices.unshift(canvasIndices.pop()); | |
slideEl.scrollLeft = containerW; | |
document.querySelector('.before').appendChild(canvases[canvasIndices[0]]); | |
document.querySelector('.current').appendChild(canvases[canvasIndices[1]]); | |
document.querySelector('.after').appendChild(canvases[canvasIndices[2]]); | |
} | |
else{ | |
slideSelection.transition().duration(_duration) | |
.ease(_easingFuncName) | |
.tween("uniquetweenname", scrollLeftTween(slideEl.scrollLeft + (shiftStepSize *_stepNum) * directionSign)) | |
} | |
} | |
var timer, isGoingRight = true; | |
function setTimer(_isGoingRight, _duration, ease, stepNum){ | |
clearInterval(timer); | |
shiftPanel(_isGoingRight, _duration, ease, stepNum); | |
return setInterval(function(){ | |
shiftPanel(_isGoingRight, _duration, ease, stepNum); | |
}, _duration); | |
} | |
timer = setTimer(true, 500, 'cubic-in-out'); | |
d3.selectAll('.handle').on('mouseenter', function(d, i){ | |
isGoingRight = this.className.split(' ').indexOf('right') != -1; | |
timer = setTimer(isGoingRight, 20, 'linear', 2); | |
}) | |
.on('mouseout', function(d, i){ | |
if (d3.event.target === this){ | |
if(d3.event.toElement.className === 'gallery-slide') clearInterval(timer); | |
else timer = setTimer(true, 500, 'cubic-in-out'); | |
} | |
}) | |
.on('mousemove', function(d, i){ | |
var isRight = this.className.split(' ').indexOf('right') != -1; | |
var handleWidth = this.getBoundingClientRect().width; | |
var mouseX = d3.mouse(this)[0]; | |
var speed = !isRight ? mouseX / handleWidth : (1 - mouseX / handleWidth); | |
timer = setTimer(isGoingRight, speed*100, 'linear', 2); | |
}); | |
slideSelection.on('mouseenter', function(d, i){ | |
clearInterval(timer); | |
}) | |
.on('mouseout', function(d, i){ | |
if (d3.event.target === this){ | |
timer = setTimer(true, 500, 'cubic-in-out'); | |
} | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment