Skip to content

Instantly share code, notes, and snippets.

@billdwhite
Last active August 4, 2023 21:14
Show Gist options
  • Save billdwhite/36d15bc6126e6f6365d0 to your computer and use it in GitHub Desktop.
Save billdwhite/36d15bc6126e6f6365d0 to your computer and use it in GitHub Desktop.
d3 Virtual Scrolling Plugin
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Virtual Scrolling Demo</title>
<style>
html, body {
width: 100%;
height: 100%;
margin: 0;
font-family: sans-serif;
font-size: 12px;
}
.viewport {
position: absolute;
top: 15px;
left: 15px;
overflow-y: auto;
width: 280px;
height: 400px;
background-color: #e8e8e8;
border: 1px solid #AAAAAA;
border-radius: 4px;
box-shadow: inset 1px 1px 6px 2px rgba(0,0,0, .25);
}
.scroll-svg {
}
.longscroll .row {
font-family: Arial;
font-size: 11px;
height: 19px;
padding: 0 8px;
border-bottom: solid #eee 1px;
}
.information {
position: absolute;
top: 15px;
left: 300px;
width: 350px;
height: 400px;
}
.info-svg {
fill: #2968AA;
overflow: visible;
}
.brace {
stroke: #2968AA;
stroke-width: 2px;
fill: none;
}
.infotext {
font-size: 20px;
}
</style>
<script src="http://d3js.org/d3.v3.min.js" type="text/javascript"></script>
<script src="virtualscroller.js"></script>
</head>
<body>
<div class="viewport"></div>
<div class="information"></div>
<script type="text/javascript">
d3.json("states.json", function (states) {
var colorScale = d3.scale.category20();
var scrollSVG = d3.select(".viewport").append("svg")
.attr("class", "scroll-svg");
var defs = scrollSVG.insert("defs", ":first-child");
createFilters(defs);
var chartGroup = scrollSVG.append("g")
.attr("class", "chartGroup")
//.attr("filter", "url(#dropShadow1)"); // sometimes causes issues in chrome
chartGroup.append("rect")
.attr("fill", "#FFFFFF");
var infoSVG = d3.select(".information").append("svg")
.attr("class", "info-svg");
var braceGroup = infoSVG.append("g")
.attr("transform", "translate(0,0)");
braceGroup.append("path")
.attr("class", "brace")
.attr("d", makeCurlyBrace(10, 380, 10, 20, 30, 0.55));
var braceLabelGroup = braceGroup.append("g")
.attr("transform", "translate(45, 176)");
braceLabelGroup.append("text")
.attr("class", "infotext")
.attr("transform", "translate(0, 0)")
.text("50 data items but only ");
braceLabelGroup.append("text")
.attr("class", "infotext")
.attr("transform", "translate(-1, 30)")
.text("15 dom nodes rendered");
braceLabelGroup.append("text")
.attr("class", "infotext")
.attr("transform", "translate(0, 60)")
.text("at any given time!");
var rowEnter = function(rowSelection) {
rowSelection.append("rect")
.attr("rx", 3)
.attr("ry", 3)
.attr("width", "250")
.attr("height", "24")
.attr("fill-opacity", 0.25)
.attr("stroke", "#999999")
.attr("stroke-width", "2px");
rowSelection.append("text")
.attr("transform", "translate(10,15)");
};
var rowUpdate = function(rowSelection) {
rowSelection.select("rect")
.attr("fill", function(d) {
return colorScale(d.id);
});
rowSelection.select("text")
.text(function (d) {
return (d.index + 1) + ". " + d.label;
});
};
var rowExit = function(rowSelection) {
};
var virtualScroller = d3.VirtualScroller()
.rowHeight(30)
.enter(rowEnter)
.update(rowUpdate)
.exit(rowExit)
.svg(scrollSVG)
.totalRows(50)
.viewport(d3.select(".viewport"));
// tack on index to each data item for easy to read display
states.items.forEach(function(nextState, i) {
nextState.index = i;
});
virtualScroller.data(states.items, function(d) { return d.id; });
chartGroup.call(virtualScroller);
function createFilters(svgDefs) {
var filter = svgDefs.append("svg:filter")
.attr("id", "dropShadow1")
.attr("x", "0")
.attr("y", "0")
.attr("width", "200%")
.attr("height", "200%");
filter.append("svg:feOffset")
.attr("result", "offOut")
.attr("in", "SourceAlpha")
.attr("dx", "1")
.attr("dy", "1");
filter.append("svg:feColorMatrix")
.attr("result", "matrixOut")
.attr("in", "offOut")
.attr("type", "matrix")
.attr("values", "0.1 0 0 0 0 0 0.1 0 0 0 0 0 0.1 0 0 0 0 0 0.2 0");
filter.append("svg:feGaussianBlur")
.attr("result", "blurOut")
.attr("in", "matrixOut")
.attr("stdDeviation", "1");
filter.append("svg:feBlend")
.attr("in", "SourceGraphic")
.attr("in2", "blurOut")
.attr("mode", "normal");
}
function makeCurlyBrace(x1,y1,x2,y2,w,q)
{
//Calculate unit vector
var dx = x1-x2;
var dy = y1-y2;
var len = Math.sqrt(dx*dx + dy*dy);
dx = dx / len;
dy = dy / len;
//Calculate Control Points of path,
var qx1 = x1 + q*w*dy;
var qy1 = y1 - q*w*dx;
var qx2 = (x1 - .25*len*dx) + (1-q)*w*dy;
var qy2 = (y1 - .25*len*dy) - (1-q)*w*dx;
var tx1 = (x1 - .5*len*dx) + w*dy;
var ty1 = (y1 - .5*len*dy) - w*dx;
var qx3 = x2 + q*w*dy;
var qy3 = y2 - q*w*dx;
var qx4 = (x1 - .75*len*dx) + (1-q)*w*dy;
var qy4 = (y1 - .75*len*dy) - (1-q)*w*dx;
return ( "M " + x1 + " " + y1 +
" Q " + qx1 + " " + qy1 + " " + qx2 + " " + qy2 +
" T " + tx1 + " " + ty1 +
" M " + x2 + " " + y2 +
" Q " + qx3 + " " + qy3 + " " + qx4 + " " + qy4 +
" T " + tx1 + " " + ty1 );
}
});
</script>
</body>
</html>
{"identifier":"id",
"label": "label",
"items": [
{"name":"Alabama", "label":"Alabama","id":"AL"},
{"name":"Alaska", "label":"Alaska","id":"AK"},
{"name":"Arizona", "label":"Arizona","id":"AZ"},
{"name":"Arkansas", "label":"Arkansas","id":"AR"},
{"name":"California", "label":"California","id":"CA"},
{"name":"Colorado", "label":"Colorado","id":"CO"},
{"name":"Connecticut", "label":"Connecticut","id":"CT"},
{"name":"Delaware", "label":"Delaware","id":"DE"},
{"name":"Florida", "label":"Florida","id":"FL"},
{"name":"Georgia", "label":"Georgia","id":"GA"},
{"name":"Hawaii", "label":"Hawaii","id":"HI"},
{"name":"Idaho", "label":"Idaho","id":"ID"},
{"name":"Illinois", "label":"Illinois","id":"IL"},
{"name":"Indiana", "label":"Indiana","id":"IN"},
{"name":"Iowa", "label":"Iowa","id":"IA"},
{"name":"Kansas", "label":"Kansas","id":"KS"},
{"name":"Kentucky", "label":"Kentucky","id":"KY"},
{"name":"Louisiana", "label":"Louisiana","id":"LA"},
{"name":"Maine", "label":"Maine","id":"ME"},
{"name":"Maryland", "label":"Maryland","id":"MD"},
{"name":"Massachusetts", "label":"Massachusetts","id":"MA"},
{"name":"Michigan", "label":"Michigan","id":"MI"},
{"name":"Minnesota", "label":"Minnesota","id":"MN"},
{"name":"Mississippi", "label":"Mississippi","id":"MS"},
{"name":"Missouri", "label":"Missouri","id":"MO"},
{"name":"Montana", "label":"Montana","id":"MT"},
{"name":"Nebraska", "label":"Nebraska","id":"NE"},
{"name":"Nevada", "label":"Nevada","id":"NV"},
{"name":"New Hampshire", "label":"New Hampshire","id":"NH"},
{"name":"New Jersey", "label":"New Jersey","id":"NJ"},
{"name":"New Mexico", "label":"New Mexico","id":"NM"},
{"name":"New York", "label":"New York","id":"NY"},
{"name":"North Carolina", "label":"North Carolina","id":"NC"},
{"name":"North Dakota", "label":"North Dakota","id":"ND"},
{"name":"Ohio", "label":"Ohio","id":"OH"},
{"name":"Oklahoma", "label":"Oklahoma","id":"OK"},
{"name":"Oregon", "label":"Oregon","id":"OR"},
{"name":"Pennsylvania", "label":"Pennsylvania","id":"PA"},
{"name":"Rhode Island", "label":"Rhode Island","id":"RI"},
{"name":"South Carolina", "label":"South Carolina","id":"SC"},
{"name":"South Dakota", "label":"South Dakota","id":"SD"},
{"name":"Tennessee", "label":"Tennessee","id":"TN"},
{"name":"Texas", "label":"Texas","id":"TX"},
{"name":"Utah", "label":"Utah","id":"UT"},
{"name":"Vermont", "label":"Vermont","id":"VT"},
{"name":"Virginia", "label":"Virginia","id":"VA"},
{"name":"Washington", "label":"Washington","id":"WA"},
{"name":"West Virginia", "label":"West Virginia","id":"WV"},
{"name":"Wisconsin", "label":"Wisconsin","id":"WI"},
{"name":"Wyoming", "label":"Wyoming","id":"WY"}
]}
d3.VirtualScroller = function() {
var enter = null,
update = null,
exit = null,
data = [],
dataid = null,
svg = null,
viewport = null,
totalRows = 0,
position = 0,
rowHeight = 24,
totalHeight = 0,
minHeight = 0,
viewportHeight = 0,
visibleRows = 0,
delta = 0,
dispatch = d3.dispatch("pageDown","pageUp");
function virtualscroller(container) {
function render(resize) {
if (resize) { // re-calculate height of viewport and # of visible row
viewportHeight = parseInt(viewport.style("height"));
visibleRows = Math.ceil(viewportHeight / rowHeight) + 1; // add 1 more row for extra overlap; avoids visible add/remove at top/bottom
}
var scrollTop = viewport.node().scrollTop;
totalHeight = Math.max(minHeight, (totalRows * rowHeight));
svg.style("height", totalHeight + "px") // both style and attr height values seem to be respected
.attr("height", totalHeight);
var lastPosition = position;
position = Math.floor(scrollTop / rowHeight);
delta = position - lastPosition;
scrollRenderFrame(position);
}
function scrollRenderFrame(scrollPosition) {
container.attr("transform", "translate(0," + (scrollPosition * rowHeight) + ")"); // position viewport to stay visible
var position0 = Math.max(0, Math.min(scrollPosition, totalRows - visibleRows + 1)), // calculate positioning (use + 1 to offset 0 position vs totalRow count diff)
position1 = position0 + visibleRows;
container.each(function() { // slice out visible rows from data and display
var rowSelection = container.selectAll(".row")
.data(data.slice(position0, Math.min(position1, totalRows)), dataid);
rowSelection.exit().call(exit).remove();
rowSelection.enter().append("g")
.attr("class", "row")
.call(enter);
rowSelection.order();
var rowUpdateSelection = container.selectAll(".row:not(.transitioning)"); // do not position .transitioning elements
rowUpdateSelection.call(update);
rowUpdateSelection.each(function(d, i) {
d3.select(this).attr("transform", function(d) {
return "translate(0," + ((i * rowHeight)) + ")";
});
});
});
if (position1 > (data.length - visibleRows)) { // dispatch events
dispatch.pageDown({
delta: delta
});
} else if (position0 < visibleRows) {
dispatch.pageUp({
delta: delta
});
}
}
virtualscroller.render = render; // make render function publicly visible
viewport.on("scroll.virtualscroller", render); // call render on scrolling event
render(true); // call render() to start
}
virtualscroller.render = function(resize) { // placeholder function that is overridden at runtime
};
virtualscroller.data = function(_, __) {
if (!arguments.length) return data;
data = _;
dataid = __;
return virtualscroller;
};
virtualscroller.dataid = function(_) {
if (!arguments.length) return dataid;
dataid = _;
return virtualscroller;
};
virtualscroller.enter = function(_) {
if (!arguments.length) return enter;
enter = _;
return virtualscroller;
};
virtualscroller.update = function(_) {
if (!arguments.length) return update;
update = _;
return virtualscroller;
};
virtualscroller.exit = function(_) {
if (!arguments.length) return exit;
exit = _;
return virtualscroller;
};
virtualscroller.totalRows = function(_) {
if (!arguments.length) return totalRows;
totalRows = _;
return virtualscroller;
};
virtualscroller.rowHeight = function(_) {
if (!arguments.length) return rowHeight;
rowHeight = +_;
return virtualscroller;
};
virtualscroller.totalHeight = function(_) {
if (!arguments.length) return totalHeight;
totalHeight = +_;
return virtualscroller;
};
virtualscroller.minHeight = function(_) {
if (!arguments.length) return minHeight;
minHeight = +_;
return virtualscroller;
};
virtualscroller.position = function(_) {
if (!arguments.length) return position;
position = +_;
if (viewport) {
viewport.node().scrollTop = position;
}
return virtualscroller;
};
virtualscroller.svg = function(_) {
if (!arguments.length) return svg;
svg = _;
return virtualscroller;
};
virtualscroller.viewport = function(_) {
if (!arguments.length) return viewport;
viewport = _;
return virtualscroller;
};
virtualscroller.delta = function() {
return delta;
};
d3.rebind(virtualscroller, dispatch, "on");
return virtualscroller;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment