Published
Edited
Jan 21, 2021
Insert cell
Insert cell
Insert cell
rawLocations = fetch('https://gistcdn.githack.com/hydrosquall/bf72b223dbd68a8a506a30b412e10426/raw/12f4bf34a643c7db47d219c20922cf53f7a97769/facilities.json').then(d => d.json())
Insert cell
locations = rawLocations.map(loc => loc.attributes)
Insert cell
Table(locations)
Insert cell
Insert cell
Table(locations, {columns: ["FacilityName", "Phone", "Borough"]})
Insert cell
Insert cell
// Table(locations, {sort: "body_mass_g", reverse: true})
Insert cell
Insert cell
// viewof selection = Table(penguins)
Insert cell
// selection // Try selecting rows above!
Insert cell
Insert cell
// Table(penguins, {
// format: {
// culmen_length_mm: x => x.toFixed(1),
// culmen_depth_mm: x => x.toFixed(1),
// sex: x => x === "MALE" ? "M" : x === "FEMALE" ? "F" : ""
// }
// })
Insert cell
Insert cell
Insert cell
Insert cell
function Table(
data,
{
columns = data.columns, // array of column names
rows = 11.5, // maximum number of rows to show
sort, // name of column to sort by, if any
reverse = false, // if sorting, true for ascending and false for descending
format, // object of column name to format function
align = {}, // object of column name to left, right, or center
width = {}, // object of column name to width
layout = "fixed" // or "auto" if you prefer
} = {}
) {
data = arrayof(data);
if (columns === undefined) columns = columnsof(data);
format = formatof(format, data, columns);
align = alignof(align, data, columns);

let n = rows * 2;
let currentSortHeader = null, currentReverse = false;
let selected = new Set();
let anchor = null, head = null;
let index = Uint32Array.from(data, (_, i) => i);

const tbody = html`<tbody>`;
const tr = html`<tr><td><input type=checkbox></td>${columns.map(column => html`<td style=${{textAlign: align[column]}}>`)}`;
const theadr = html`<tr><th><input type=checkbox onclick=${reselectAll}></th>${columns.map((column) => html`<th title=${column} style=${{width: width[column], textAlign: align[column]}} onclick=${event => resort(event, column)}><span></span>${column}</th>`)}</tr>`;
const root = html`<div class=${id} style="max-height: ${(rows + 1) * 24 - 1}px;">
<table style=${{tableLayout: layout}}>
<thead>${data.length || columns.length ? theadr : null}</thead>
${tbody}
</table>
</div>`;

function inputof(tr) {
return tr.firstChild.firstChild;
}
function orderof(th) {
return th.firstChild;
}

function render(i, j) {
return Array.from(index.subarray(i, j), i => {
const itr = tr.cloneNode(true);
itr.classList.toggle("selected", selected.has(i));
const input = inputof(itr);
input.onclick = reselect;
input.checked = selected.has(i);
input.name = i;
for (let j = 0; j < columns.length; ++j) {
let column = columns[j];
let value = data[i][column];
if (!defined(value)) continue;
value = format[column](value);
if (!(value instanceof Node)) value = document.createTextNode(value);
itr.childNodes[j + 1].appendChild(value);
}
return itr;
});
}

function unselect(i) {
let j = index.indexOf(i);
if (j < tbody.childNodes.length) {
const tr = tbody.childNodes[j];
tr.classList.toggle("selected", false);
inputof(tr).checked = false;
}
selected.delete(i);
}

function select(i) {
let j = index.indexOf(i);
if (j < tbody.childNodes.length) {
const tr = tbody.childNodes[j];
tr.classList.toggle("selected", true);
inputof(tr).checked = true;
}
selected.add(i);
}

function* range(i, j) {
i = index.indexOf(i), j = index.indexOf(j);
if (i < j) while (i <= j) yield index[i++];
else while (j <= i) yield index[j++];
}

function first(set) {
return set[Symbol.iterator]().next().value;
}

function reselectAll(event) {
if (selected.size) {
for (let i of selected) unselect(i);
anchor = head = null;
if (event.detail) event.currentTarget.blur();
} else {
selected = new Set(index);
for (const tr of tbody.childNodes) {
tr.classList.toggle("selected", true);
inputof(tr).checked = true;
}
}
reinput();
}

function reselect(event) {
let i = +this.name;
if (event.shiftKey) {
if (anchor === null) anchor = selected.size ? first(selected) : index[0];
else for (let i of range(anchor, head)) unselect(i);
head = i;
for (let i of range(anchor, head)) select(i);
} else {
anchor = head = i;
if (selected.has(i)) {
unselect(i);
anchor = head = null;
if (event.detail) event.currentTarget.blur();
} else {
select(i);
}
}
reinput();
}

function resort(event, column) {
const th = event.currentTarget;
let compare;
if (currentSortHeader === th && currentReverse) {
orderof(currentSortHeader).textContent = "";
currentSortHeader.classList.toggle("sort", false);
currentSortHeader = null;
currentReverse = false;
compare = ascending;
} else {
if (currentSortHeader === th) {
currentReverse = true;
} else {
if (currentSortHeader) {
orderof(currentSortHeader).textContent = "";
currentSortHeader.classList.toggle("sort", false);
}
currentSortHeader = th, currentReverse = false;
}
const order = currentReverse ? descending : ascending;
compare = (a, b) => order(data[a][column], data[b][column]);
orderof(th).textContent = currentReverse ? "▾" : "▴";
th.classList.toggle("sort", true);
}
index.sort(compare);
selected = new Set(Array.from(selected).sort(compare));
root.scrollTo(0, 0);
while (tbody.firstChild) tbody.firstChild.remove();
tbody.append(...render(0, n = rows * 2));
anchor = head = null;
reinput();
}

function reinput() {
theadr.classList.toggle("selected", selected.size);
inputof(theadr).checked = selected.size;
revalue();
root.dispatchEvent(new CustomEvent("input"));
}

function revalue() {
root.value = Array.from(selected.size ? selected : index, i => data[i]);
root.value.columns = columns;
}

root.onscroll = () => {
if (root.scrollHeight - root.scrollTop < 400 && n < data.length) {
tbody.append(...render(n, n += rows));
}
};

if (data.length) {
tbody.append(...render(0, n));
} else {
tbody.append(html`<tr>${columns.length ? html`<td>` : null}<td rowspan=${columns.length} style="padding-left: 1em; font-style: oblique;">No results.</td></tr>`);
}

revalue();

if (sort !== undefined) {
let i = columns.indexOf(sort);
if (i >= 0) {
if (reverse) currentSortHeader = theadr.childNodes[i + 1];
resort({currentTarget: theadr.childNodes[i + 1]}, columns[i]);
}
}
if (!Table.style) Table.style = document.body.appendChild(style);
return root;
}
Insert cell
style = html`<style>

.${id} {
overflow-y: scroll;
margin: 0 -14px;
}

.${id} table {
max-width: initial;
min-height: 33px;
border-collapse: separate;
border-spacing: 0;
font-variant-numeric: tabular-nums;
}

.${id} thead th span {
display: inline-block;
width: 0.5em;
margin-left: -0.5em;
}

.${id} td,
.${id} th {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
border-bottom: solid 1px #eee;
}

.${id} tr > :not(:first-of-type) {
padding-left: 0.7em;
}

.${id} tr > :last-of-type {
padding-right: 1em;
}

.${id} tr > :first-of-type {
width: 19px;
}

.${id} tr,
.${id} tr:last-child td {
border-bottom: none;
}

.${id} tr > :first-of-type input {
opacity: 0;
margin: 0 3px 3px 4px;
}

.${id} tr:hover > :first-of-type input,
.${id} tr > :first-of-type input:focus,
.${id} tr > :first-of-type input:checked {
opacity: inherit;
}

.${id} thead th {
position: sticky;
top: 0;
background: white;
border-bottom: solid 1px #ccc;
cursor: ns-resize;
}

</style>`
Insert cell
id = DOM.uid("table").id
Insert cell
function formatof(base = {}, data, columns) {
const format = Object.create(null);
for (const column of columns) {
if (column in base) {
format[column] = base[column];
continue;
}
switch (type(data, column)) {
case "number": format[column] = formatNumber; break;
case "date": format[column] = formatDate; break;
default: format[column] = formatDefault; break;
}
}
return format;
}
Insert cell
function alignof(base = {}, data, columns) {
const align = Object.create(null);
for (const column of columns) {
if (column in base) {
align[column] = base[column];
} else if (type(data, column) === "number") {
align[column] = "right";
}
}
return align;
}
Insert cell
function type(data, column) {
for (const d of data) {
const value = d[column];
if (value == null) continue;
if (typeof value === "number") return "number";
if (value instanceof Date) return "date";
return;
}
}
Insert cell
function columnsof(data) {
const columns = {};
for (const row of data) {
for (const name in row) {
columns[name] = true;
}
}
return Object.keys(columns);
}
Insert cell
function arrayof(x) {
return typeof x === "object" && "length" in x
? x // Array, TypedArray, NodeList, array-like
: Array.from(x); // Map, Set, iterable, string, or anything else
}
Insert cell
function pad(value, width) {
return (value + "").padStart(width, "0");
}
Insert cell
function formatDefault(value) {
return value + "";
}
Insert cell
function formatNumber(value) {
return value.toLocaleString("en");
}
Insert cell
function formatYear(year) {
return year < 0 ? "-" + pad(-year, 6) : year > 9999 ? "+" + pad(year, 6) : pad(year, 4);
}
Insert cell
function ascending(a, b) {
return defined(b) - defined(a) || (a < b ? -1 : a > b ? 1 : a >= b ? 0 : NaN);
}
Insert cell
function descending(b, a) {
return defined(a) - defined(b) || (a < b ? -1 : a > b ? 1 : a >= b ? 0 : NaN);
}
Insert cell
function defined(d) {
return d != null && !Number.isNaN(d);
}
Insert cell
function formatDate(date) {
var hours = date.getUTCHours(),
minutes = date.getUTCMinutes(),
seconds = date.getUTCSeconds(),
milliseconds = date.getUTCMilliseconds();
return isNaN(date) ? "Invalid Date"
: formatYear(date.getUTCFullYear(), 4) + "-" + pad(date.getUTCMonth() + 1, 2) + "-" + pad(date.getUTCDate(), 2)
+ (milliseconds ? "T" + pad(hours, 2) + ":" + pad(minutes, 2) + ":" + pad(seconds, 2) + "." + pad(milliseconds, 3) + "Z"
: seconds ? "T" + pad(hours, 2) + ":" + pad(minutes, 2) + ":" + pad(seconds, 2) + "Z"
: minutes || hours ? "T" + pad(hours, 2) + ":" + pad(minutes, 2) + "Z"
: "");
}
Insert cell
html = (await require("htl@0.2")).html
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more