Skip to content

Instantly share code, notes, and snippets.

@eweitnauer
Last active February 4, 2021 13:20
Show Gist options
  • Save eweitnauer/d07ebcb90b4106df2564 to your computer and use it in GitHub Desktop.
Save eweitnauer/d07ebcb90b4106df2564 to your computer and use it in GitHub Desktop.
Fast Scrolling using D3

Scrolling through big amounts of time series data.

In fast mode, scrolling is achieved by moving a parent "g" element to the left or right. Entering and exiting of data is done at the end of dragging. In slow mode, entering, exiting and updating of data is done during the dragging.

<!doctype html>
<meta charset="utf-8">
<title>Fast Plot Scrolling</title>
<script src="http://d3js.org/d3.v3.min.js"></script>
<style>
body { padding: 0; margin: 0; font-family: Lato, sans-serif; color: #444; }
div#options { margin: 10px 20px }
div#options form { display: inline-block; margin-left: 10px;}
.axis text {
font: 10px sans-serif;
fill: gray;
}
.axis line, .axis path, rect.axis {
shape-rendering: crispEdges;
fill: none;
stroke: silver;
}
</style>
<body>
<div id="options">
<span>Scrolling Mode: </span>
<form action="">
<input type="radio" name="mode" value="fast" checked='checked' onclick='set_mode("fast")'>fast
<input type="radio" name="mode" value="slow" onclick='set_mode("slow")'>slow
</form>
<div>
<script>
var MODE = 'fast'; // fast or slow
function set_mode(val) {
MODE = val;
}
var margin = {top: 0, right: 30, bottom: 30, left: 20 }
,width = 960 - margin.left - margin.right
,height = 459 - margin.top - margin.bottom;
var y_val = 0;
var N = 20000, view_N = 5000, view_x = N/2;
var data = d3.range(N).map(function(x, i, arr) {
y_val += Math.random() - 0.5;
return {x: x, y: y_val, id: i}
});
var svg = d3.select('body')
.append('svg')
.attr({ width: width+margin.left+margin.right
, height: height+margin.top+margin.bottom })
.append('g')
.classed('data-area', true)
.attr('transform', 'translate(' + [margin.left, margin.top] + ')');
svg.append("clipPath").attr("id", "clip_123")
.append("rect").attr({width: width, height: height});
svg.append('rect')
.classed('axis', true)
.attr({ width: width, height: height, fill: 'none', stroke: 'silver' });
var x = d3.scale.linear()
.range([0,width])
.domain([view_x-view_N, view_x]);
var x_axis = d3.svg.axis()
.scale(x)
.orient("bottom")
.outerTickSize(0);
var x_axis_el = svg.append('g')
.classed('x axis', true)
.attr('transform', 'translate(0,' + height +')')
.call(x_axis);
var y = d3.scale.linear()
.range([0, height])
.domain(d3.extent(data, function(d) { return d.y }));
var y_axis = d3.svg.axis()
.scale(y)
.tickSize(-width)
.outerTickSize(0)
.orient("right");
var y_axis_el = svg.append('g')
.classed('y axis', true)
.attr('transform', 'translate(' + width + ',0)')
.call(y_axis);
var clipped = svg.append('g')
.attr('clip-path', 'url(#clip_123)')
var data_area = clipped.append('g');
var sel = data_area.selectAll('circle');
function update(data) {
sel = sel.data(data, function(d) { return d.id });
sel.exit().remove();
sel.enter()
.append('circle')
.attr('r', 4)
.attr('fill', 'steelblue');
sel
.attr('cx', function(d) { return x(d.x) })
.attr('cy', function(d) { return y(d.y) });
}
function enter_exit(data) {
sel = sel.data(data, function(d) { return d.id });
sel.exit().remove();
sel.enter()
.append('circle')
.attr('r', 4)
.attr('fill', 'steelblue')
.attr('cx', function(d) { return x(d.x) })
.attr('cy', function(d) { return y(d.y) });
}
update(data.slice(view_x-view_N, view_x));
var drag = d3.behavior.drag()
.origin(function(d) { return {x: 0, y: 0} })
.on("drag", dragmove)
.on("dragstart", dragstart)
.on("dragend", dragend);
clipped.append('rect')
.attr({ width: width, height: height, fill: 'none', stroke: 'none' })
.attr('pointer-events', 'all')
.call(drag);
function shift_slow(dx) {
view_x = view_x+(x.invert(-dx)-x.invert(0));
if (view_x > N-1) view_x = N-1;
if (view_x < view_N) view_x = view_N;
var view = data.slice(view_x-view_N, view_x);
x.domain([view_x-view_N, view_x]);
x_axis_el.call(x_axis);
update(view);
}
var tx=0;
function shift_fast(dx) {
var dt = x.invert(dx)-x.invert(0);
var domain = x.domain();
var range = x.range();
if (domain[0]-dt < 0) dt = domain[0];
if (domain[1]-dt > N-1) dt = domain[1]-N+1;
dx = x(dt)-x(0);
tx += dx;
view_x = Math.round(domain[1]-dt);
x.range([x(domain[0]-dt), x(domain[1]-dt)]);
x.domain([domain[0]-dt, domain[1]-dt]);
data_area.attr('transform', 'translate('+tx+',0)');
x_axis_el.attr('transform', 'translate('+tx+','+height+')');
x_axis_el.call(x_axis);
}
function dragstart() {
if (MODE === 'fast') {
data_area.selectAll('.grayed')
.data([{x: x(0), width: x(view_x-view_N)-x(0)}
,{x: x(view_x), width: x(N)-x(view_x)}])
.enter()
.append('rect').classed('grayed', true)
.attr({x: function(d) { return d.x }})
.attr('width', function(d) { return d.width })
.attr({y: 0, height: height })
.attr('fill', 'gray')
.attr('fill-opacity', 0.25);
}
}
function dragmove() {
if (!d3.event) return;
if (MODE === 'fast') shift_fast(d3.event.dx);
else shift_slow(d3.event.dx);
}
function dragend() {
console.log('dragend');
data_area.selectAll('.grayed').remove();
update(data.slice(view_x-view_N, view_x));
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment