Skip to content

Instantly share code, notes, and snippets.

@chriskoiak
Last active January 21, 2022 04:32
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chriskoiak/d3de95e270a4cc186046 to your computer and use it in GitHub Desktop.
Save chriskoiak/d3de95e270a4cc186046 to your computer and use it in GitHub Desktop.
d3 Virtual Horizontal Scrolling Plugin (based on http://bl.ocks.org/billdwhite/36d15bc6126e6f6365d0)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Virtual Horizontal 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 chartGroup = scrollSVG.append("g")
.attr("class", "chartGroup");
//.attr("filter", "url(#dropShadow1)"); // sometimes causes issues in chrome
chartGroup.append("rect")
.attr("fill", "#FFFFFF");
var rowEnter = function(rowSelection) {
rowSelection.append("rect")
.attr("rx", 3)
.attr("ry", 3)
.attr("width", "24")
.attr("height", "250")
.attr("fill-opacity", 0.25)
.attr("stroke", "#999999")
.attr("stroke-width", "2px");
rowSelection.append("text")
.attr("transform", "translate(10,15)rotate(90)");
};
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()
.columnWidth(60)
.enter(rowEnter)
.update(rowUpdate)
.exit(rowExit)
.svg(scrollSVG)
.totalColumns(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);
});
</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,
totalColumns = 0,
position = 0,
columnWidth = 24,
totalWidth = 0,
minWidth = 0,
viewportWidth = 0,
visibleColumns = 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
viewportWidth = parseInt(viewport.style("width"));
visibleColumns = Math.ceil(viewportWidth / columnWidth) + 1; // add 1 more row for extra overlap; avoids visible add/remove at top/bottom
}
var scrollTop = viewport.node().scrollLeft;
totalWidth = Math.max(minWidth, (totalColumns * columnWidth));
svg.style("width", totalWidth + "px") // both style and attr height values seem to be respected
.attr("width", totalWidth)
.attr("height", "300px");
var lastPosition = position;
position = Math.floor(scrollTop / columnWidth);
delta = position - lastPosition;
scrollRenderFrame(position);
}
function scrollRenderFrame(scrollPosition) {
container.attr("transform", "translate(" + (scrollPosition * columnWidth) + ",0)"); // position viewport to stay visible
var position0 = Math.max(0, Math.min(scrollPosition, totalColumns - visibleColumns + 1)), // calculate positioning (use + 1 to offset 0 position vs totalRow count diff)
position1 = position0 + visibleColumns;
container.each(function () { // slice out visible rows from data and display
var rowSelection = container.selectAll(".column")
.data(data.slice(position0, Math.min(position1, totalColumns)), dataid);
rowSelection.exit().call(exit).remove();
rowSelection.enter().append("g")
.attr("class", "column")
.call(enter);
rowSelection.order();
var rowUpdateSelection = container.selectAll(".column:not(.transitioning)"); // do not position .transitioning elements
rowUpdateSelection.call(update);
rowUpdateSelection.each(function (d, i) {
d3.select(this).attr("transform", function (d) {
return "translate(" + ((i * columnWidth)) + ",0)";
});
});
});
if (position1 > (data.length - visibleColumns)) { // dispatch events
dispatch.pageDown({
delta: delta
});
} else if (position0 < visibleColumns) {
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.totalColumns = function (_) {
if (!arguments.length) return totalColumns;
totalColumns = _;
return virtualscroller;
};
virtualscroller.columnWidth = function (_) {
if (!arguments.length) return columnWidth;
columnWidth = +_;
return virtualscroller;
};
virtualscroller.totalWidth = function (_) {
if (!arguments.length) return totalWidth;
totalWidth = +_;
return virtualscroller;
};
virtualscroller.minWidth = function (_) {
if (!arguments.length) return minWidth;
minWidth = +_;
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;
};
@chriskoiak
Copy link
Author

image

Here's an example of the rendered page

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment