Skip to content

Instantly share code, notes, and snippets.

@biovisualize
Last active November 3, 2015 13:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save biovisualize/549bfceb0938859fea34 to your computer and use it in GitHub Desktop.
Save biovisualize/549bfceb0938859fea34 to your computer and use it in GitHub Desktop.
streaming line chart with animation and long-scrolling
<!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