Last active
August 4, 2023 21:14
-
-
Save billdwhite/36d15bc6126e6f6365d0 to your computer and use it in GitHub Desktop.
d3 Virtual Scrolling Plugin
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> | |
<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> |
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
{"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"} | |
]} |
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
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