Zipline-style virtual rendering on scroll
A toy example to demonstrate the scrolling with virtual rendering implemented in zipline.
<!DOCTYPE html> | |
<html lang='en'> | |
<head> | |
<meta charset='utf-8'> | |
<script src='http://d3js.org/d3.v3.min.js' charset='utf-8'></script> | |
<script src='http://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js'></script> | |
<script src='http://cdnjs.cloudflare.com/ajax/libs/crossfilter/1.3.11/crossfilter.min.js'></script> | |
<link rel="stylesheet" type="text/css" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css"> | |
<style type="text/css"> | |
#main { | |
width: 800px; | |
height: 480px; | |
margin: 0 auto; | |
background-color: #f5f5f5; | |
overflow: hidden; | |
} | |
#nav { | |
width: 800px; | |
height: 60px; | |
margin: 0 auto; | |
background-color: #d6d6d6; | |
border-bottom: 5px white solid; | |
display: flex; | |
flex-direction: row; | |
justify-content: space-between; | |
} | |
#svg { | |
height: 420px; | |
overflow-x: scroll; | |
} | |
.button-holder { | |
margin-top: 8px; | |
} | |
button { | |
margin-left: 5px; | |
margin-right: 5px; | |
} | |
#axisGroup path { | |
fill: none; | |
stroke: none; | |
} | |
#axisGroup line { | |
fill: none; | |
stroke: black; | |
shape-rendering: crispEdges; | |
} | |
#axisGroup text { | |
font-family: sans-serif; | |
text-transform: lowercase; | |
} | |
</style> | |
<title>Zipline scrolling</title> | |
</head> | |
<body> | |
<div id="main"> | |
<div id="nav"> | |
<div class="button-holder"> | |
<button id="backbtn" type="button" class="btn btn-default">Back</button> | |
</div> | |
<div class="button-holder"> | |
<button id="forwardbtn" type="button" class="btn btn-default">Forward</button> | |
</div> | |
</div> | |
<div id="svg"></div> | |
</div> | |
<script type="text/javascript"> | |
// some variables | |
var w = 800, h = 410, axis = 40; | |
var barWidth = 25; | |
// let's make some data! | |
// first, dates for our x-plotting | |
var now = new Date(); | |
var s = d3.time.year.floor(now), e = d3.time.year.ceil(now); | |
var dates = d3.time.hour.range(s, e); | |
// drop these in along with random numbers from 1-100 for the y-plotting | |
var data = _.map(dates, function(date) { | |
return { | |
x: date.valueOf(), | |
y: Math.ceil(Math.random() * 100) | |
}; | |
}); | |
// set up crossfilter for data filtering | |
var filterData = crossfilter(data); | |
var dataByDate = filterData.dimension(function(d) { return d.x; }); | |
// we gotta make scales before even creating the SVG | |
// because the width will depend on the domain of the xScale | |
// set up scales | |
var xScale = d3.time.scale() | |
.domain([s, d3.time.day.offset(s, 1)]) | |
.range([0, w]); | |
var extent = d3.extent(data, function(d) { return d.y; }); | |
var yScale = d3.scale.linear() | |
.domain(extent) | |
.range([h, axis]); | |
// change bar color depending on height, just for funsies | |
var funScale = d3.scale.linear() | |
.domain(extent) | |
.range(['#302e86', '#b6211d']); | |
// add the SVG | |
var svg = d3.select('#svg').append('svg') | |
.attr({ | |
width: xScale(e), | |
height: h | |
}); | |
var mainGroup = svg.append('g') | |
.attr('id', 'scrollGroup'); | |
var barsGroup = mainGroup.append('g') | |
.attr('id', 'barsGroup'); | |
// initial plot with 1-day buffer on each side | |
plotBars(dataByDate.filter([d3.time.day.offset(s, -1).valueOf(), d3.time.day.offset(s, 2).valueOf()]).top(Infinity)); | |
// factor out bar plotting into a function that binds data | |
// and creates or deletes bars as necessary given the current data | |
function plotBars(data) { | |
var currBars = barsGroup.selectAll('rect') | |
.data(data, function(d) { return d.x; }); | |
currBars.enter() | |
.append('rect') | |
.attr({ | |
x: function(d) { | |
return xScale(d.x) - barWidth/2; | |
}, | |
y: function(d) { | |
return yScale(d.y); | |
}, | |
width: barWidth, | |
height: function(d) { | |
return h - yScale(d.y); | |
}, | |
fill: function(d) { | |
return funScale(d.y); | |
} | |
}); | |
currBars.exit().remove(); | |
} | |
// use a dispatcher to handle and trigger refiltering data and plotting bars | |
var dispatcher = d3.dispatch('plotBars'); | |
dispatcher.on('plotBars', function(date) { | |
// refilter data based on current position and plot | |
plotBars(dataByDate.filter([d3.time.day.offset(date, -1).valueOf(), d3.time.day.offset(date, 2).valueOf()]).top(Infinity)); | |
}); | |
// add an axis | |
// customize the time formatting because zero-padding bugs the stuffing out of me... | |
// i.e., this is not necessary ;) | |
var format = d3.time.format.multi([ | |
[".%L", function(d) { return d.getMilliseconds(); }], | |
[":%S", function(d) { return d.getSeconds(); }], | |
["%-I:%M", function(d) { return d.getMinutes(); }], | |
["%-I %p", function(d) { return d.getHours(); }], | |
["%a %-d", function(d) { return d.getDay() && d.getDate() != 1; }], | |
["%b %-d", function(d) { return d.getDate() != 1; }], | |
["%b", function(d) { return true; }] | |
]); | |
// here's the good stuff | |
// the xScale for the axis needs to use the entire domain | |
// and the entire range of our reallllly wide SVG | |
// which we can find the end of using the xScale | |
var axisScale = d3.time.scale() | |
.domain([s,e]) | |
.range([0, xScale(e)]); | |
var xAxis = d3.svg.axis() | |
.scale(axisScale) | |
.ticks(d3.time.hours, 6) | |
.tickFormat(format) | |
.innerTickSize(6) | |
.outerTickSize(0); | |
mainGroup.append('g') | |
.attr('id', 'axisGroup') | |
.call(xAxis); | |
// now we add programmatic panning by a day backwards and forwards | |
// to move one day per button press, we find the width that equals scrolling a day | |
// this is equal to the xScale domain, which is the width | |
var scrollWidth = w, scrollContainer = d3.select('#svg'); | |
function panBack() { | |
var currentScrollLeft = scrollContainer.property('scrollLeft'); | |
scrollContainer.transition() | |
.duration(500) | |
.tween('pan', function() { | |
var ix = d3.interpolate(currentScrollLeft, currentScrollLeft - scrollWidth); | |
return function(t) { | |
scrollContainer.property('scrollLeft', ix(t)); | |
}; | |
}); | |
} | |
d3.select('#backbtn').on('click', panBack); | |
function panForward() { | |
var currentScrollLeft = scrollContainer.property('scrollLeft'); | |
scrollContainer.transition() | |
.duration(500) | |
.tween('pan', function() { | |
var ix = d3.interpolate(currentScrollLeft, currentScrollLeft + scrollWidth); | |
return function(t) { | |
scrollContainer.property('scrollLeft', ix(t)); | |
}; | |
}); | |
} | |
d3.select('#forwardbtn').on('click', panForward); | |
scrollContainer.on('scroll', function() { | |
// trigger refilter and plot of data based on current date | |
var date = xScale.invert(scrollContainer.property('scrollLeft')); | |
dispatcher.plotBars(date); | |
}); | |
</script> | |
</body> | |
</html> |