A toy example to demonstrate the scrolling with virtual rendering implemented in zipline.
Last active
August 29, 2015 14:10
Zipline-style virtual rendering on scroll
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 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> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment