Last active
June 27, 2018 10:45
-
-
Save goulu/a45ad9ba663e23d470dbd0cbbab1bdd7 to your computer and use it in GitHub Desktop.
Table using D3.js and clusterize.js
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>D3Table</title> | |
<link rel="stylesheet" href="https://rawgit.com/goulu/Clusterize.js/master/clusterize.css"></script> | |
<script src="https://rawgit.com/goulu/Clusterize.js/master/clusterize.js"></script> | |
<script src='http://d3js.org/d3.v3.js' type='text/javascript'></script> | |
<script src="./table.js" type='text/javascript'></script> | |
</head> | |
<body> | |
<h1>Table using D3.js and clusterize.js</h1> | |
(in fact it currently uses <a href="https://github.com/goulu/Clusterize.js">my fork of clusterize.js</a> | |
which allows to specify a function providing data for each row, | |
which makes it even faster and handier for sorting, filtering etc.) | |
<div class="table"> | |
</div> | |
<script> | |
let n=50000; | |
var d = d3.range(n).map(function(d,i){ | |
return [i,n-i,Math.sin(i/100)]; | |
}); | |
var table = new Table(d3.select('.table')) | |
.header(["i","n-i","sin"]) | |
.data(d); | |
</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
/* | |
reusable D3.js class for a table | |
* filterable thanks to D3 | |
* sortable thanks to http://bl.ocks.org/AMDS/4a61497182b8fcb05906 | |
* (TODO : make it like https://www.kryogenix.org/code/browser/sorttable/) | |
* efficient with large data thanks to https://clusterize.js.org/ | |
@author Philippe Guglielmetti https://github.com/goulu/ | |
@license LGPL 3 | |
@version 0.1 | |
@updated 2018.06.25 | |
*/ | |
/*export*/ | |
class Table extends Clusterize { | |
constructor(element) { | |
/* build DOM structure like this: | |
<table> | |
<thead> | |
<tr> | |
<th>Headers</th> | |
</tr> | |
</thead> | |
</table> | |
<div id="scrollArea" class="clusterize-scroll"> | |
<table> | |
<tbody id="contentArea" class="clusterize-content"> | |
<tr class="clusterize-no-data"> | |
<td>Loading data…</td> | |
</tr> | |
</tbody> | |
</table> | |
</div> | |
*/ | |
let thead = element.append("table").append("thead"); | |
thead.insert("th").append("tr").text("Headers"); | |
let scroll = element.append("div") | |
.attr("id", uniqueId) | |
.classed("clusterize-scroll", true) | |
.on('scroll', (function () { | |
var prevScrollLeft = 0; | |
return function () { | |
var scrollLeft = this.scrollLeft(); | |
if (scrollLeft == prevScrollLeft) return; | |
prevScrollLeft = scrollLeft; | |
this.thead.style('margin-left', -scrollLeft); | |
} | |
}) | |
); | |
let tbody = scroll.append("table").append("tbody") | |
.attr("id", uniqueId) | |
.classed("clusterize-content", true); | |
let tr = tbody.append("tr").classed("clusterize-no-data", true); | |
tr.append("td").text("Loading data..."); | |
super({ | |
// rows: [], // do not specify it here | |
scrollId: scroll.attr("id"), | |
contentId: tbody.attr("id"), | |
}); | |
this.selected = []; | |
this.element = element; | |
this.thead = thead; | |
this.tbody = element.select("tbody"); | |
this.format(function (v) { | |
return v; | |
}); | |
let table=this; | |
this.options.callbacks = { | |
clusterChanged: function () { | |
table.fitHeaderColumns(); | |
table.setHeaderWidth(); | |
} | |
}; | |
window.addEventListener("resize", this.resize.bind(this)); | |
} | |
// config | |
header(cols) { | |
this.columns = cols; | |
this.thead.selectAll("th") | |
.remove(); | |
this.thead.selectAll("th") | |
.data(this.columns) | |
.enter() | |
.append("th") | |
.text(function (column) { | |
return column; | |
}); | |
return this; | |
} | |
format(f) { | |
this._format = f; | |
return this; | |
} | |
// run | |
data(d) { | |
if (d === undefined) { | |
return this.__data__; | |
} | |
this.__data__ = d; | |
let fmt = this._format | |
this.update(function (i) { | |
return '<tr>' | |
+ d[i].map(function (cell) { | |
return '<td>' + fmt(cell) + '</td>'; | |
}).join('') | |
+ '</tr>' | |
} | |
, | |
d.length | |
); | |
return this; | |
} | |
append(newdata) { | |
// merge and sort data with current | |
let data = this.data().concat(newdata); | |
data = data.sort(function (a, b) { | |
return d3.ascending(a.datetime, b.datetime) | |
}); | |
return this.data(data); | |
} | |
indexOf(d) { | |
return this.data().indexOf(d); | |
} | |
scrollTo(d, ms = 1000) { | |
// smooth scroll to data d in ms milliseconds | |
let node = this.tbody.node(); | |
let f = node.scrollHeight / node.rows.length; | |
let nlines = node.clientHeight / f; | |
function scrollTween(offset) { | |
return function () { | |
let i = d3.interpolateNumber(node.scrollTop, offset); | |
return function (t) { | |
node.scrollTop = i(t); | |
}; | |
}; | |
} | |
let line = table.indexOf(d); | |
if (line < 0) { | |
console.log(d + "not found in table"); | |
return; | |
} | |
this.tbody.transition() | |
.duration(ms) | |
.tween("scroll", scrollTween( | |
(line - Math.round(nlines / 2)) * f | |
) | |
); | |
} | |
select(d, i) { | |
this.selected.push( | |
this.rows | |
.filter(function (d2, j) { | |
return d2 === d; | |
}) | |
.classed("highlight", true) | |
); | |
} | |
deselect() { | |
this.selected.map(function (row) { | |
row.classed("highlight", false); | |
}); | |
this.selected = []; | |
} | |
// events | |
resize() { | |
this.fitHeaderColumns(); | |
this.setHeaderWidth() | |
} | |
// Makes header columns equal width to content columns | |
fitHeaderColumns() { | |
let td = this.tbody.select('tr:not(.clusterize-extra-row)').selectAll('td'); | |
let th = this.thead.selectAll('th'); | |
let w = []; | |
td.map(function (d, i) { | |
w.push(d.clientWidth) | |
}); | |
th.attr("width", function (d, i) { | |
return w[i] | |
}); | |
} | |
setHeaderWidth() { | |
this.thead.width = this.tbody.width; | |
} | |
} | |
function uniqueId() { | |
// https://gist.github.com/gordonbrander/2230317 | |
// Math.random should be unique because of its seeding algorithm. | |
// Convert it to base 36 (numbers + letters), and grab the first 9 characters | |
// after the decimal. | |
return "_" + Math.random().toString(36).substr(2, 9); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
See it in action here : http://bl.ocks.org/goulu/a45ad9ba663e23d470dbd0cbbab1bdd7