Published
Edited
Oct 6, 2019
Fork of Tables
Importers
1 star
Insert cell
md`# Tables

[the forum](https://talk.observablehq.com/t/it-would-be-cool-if-table-tags-had-a-spiffy-default-style/588/4)

\`\`\`js
import {table} from "@bchoatejr/tables@526"
\`\`\`

### Table API:

- **data** \`Array<Object>\`: with consistent data types across rows
- **options**:
- **style**: \`enum(normal, compact)\`: a string value of a default style. By default, tables are designed for comfortable reading with large fonts. To display more information in less screen space, use compact.
- **header** \`boolean\` (default true): set to false to entirely omit the table header.
- **sortable** \`boolean\` (default false): make columns sortable
- **paged** \`number\` (default 25): split data by pages, if it has more than the given number of items.
- **rank** \`boolean\` (default false): show 1… rank numbers in the first column. Cannot be combined with sortable.
- **columns** \`Object\`: column-specific configuration, indexed by key:
- \`[key]\`
- **formatter** \`Function(value, rowIndex, row) ⇒ output\`: value formatter for cells. This can return a string, number, or element. If it returns a \`<td>\` element, that element can ‘take over’ the cell.
- **title** \`string\`: alternate text for table header - by default, the header cell is named after the key in the data object
- **type** \`string\`: override auto-detected type for this column - display numbers as strings, etc. Optional, should be one of 'string' or 'number'.

An example of a dataset that table() easily accepts:

\`\`\`json
${JSON.stringify(opinionPolls.slice(0, 2), null, 2)}
\`\`\`
`
Insert cell
md`
<br /><br />

*Click the caret on the left to view code for any example.*


### Titanic survivors

This is a table of the Titanic survivors dataset - it shows the data in natural order, first, but the columns are sortable. It shows usage of custom formatters for the Name and Fare columns. The Fare column formatter uses the rowIndex to determine the appropriate format based on the row number. (i.e. $ for the first row only).`
Insert cell
Insert cell
Insert cell
// table(spotify, {
// rank: true,
// style: 'compact',
// columns: {
// Track: {
// formatter(val, i) {
// return html`<strong>${val[0]}</strong> by ${val[1]}`;
// }
// },
// Streams: {
// formatter: d3.format(',')
// }
// }
// })
Insert cell
table = (data, options) => {
options = Object.assign({}, defaultOptions, options);
const { sortable, rank, paged } = options;
let sortKey = undefined;
let sortDirection = true;
let page = 0;
if (sortable && rank) {
throw new Error("A table can either be ranked or sortable, but not both");
}
let columns = Object.keys(data[0]).map(key => {
const opts = options.columns[key] || {};
return {
key: key,
type: opts.type || typeof data[0][key],
options: opts
};
});

function bake() {
if (sortKey) {
data = data.slice().sort((a, b) => {
let as = a[sortKey];
let bs = b[sortKey];
// make this sort stable
if (as == bs) return JSON.stringify(a).localeCompare(JSON.stringify(b));
let res = as > bs ? 1 : as < bs ? -1 : 0;
if (sortDirection) res = -res;
return res;
});
}
let rows = data.slice(page * paged, page * paged + paged);
let pages = Math.ceil(data.length / paged);
return html`<div><div>
<style>
.pretty-pager {
padding-top: 1rem;
}
.pretty-pager button {
cursor: pointer;
border-radius: 3px;
border: 1px solid #fff;
font-size: inherit;
}
.pretty-pager button:hover {
border: 1px solid #888;
}
.pretty-table.normal {
font-size: 15px;
}
.pretty-table.normal th,
.pretty-table.normal td {
padding: 3px 2px;
}
.pretty-table th,
.pretty-table td {
vertical-align: top;
}
.pretty-table thead th {
text-transform: uppercase;
font-weight:500;
}
.pretty-table thead th.column-type-number string {
order: 1;
}
.pretty-table th.sortable {
cursor: pointer;
}
.pretty-table thead th.column-type-number,
.pretty-table tbody td.cell-type-number,
.pretty-table tbody td.cell-rank {
text-align:right;
}
.pretty-table tbody td.cell-type-number,
.pretty-table tbody td.cell-rank {
font-family: menlo,consolas,monaco,monospace;
font-size: 90%;
}
.pretty-table tbody td.cell-rank {
padding-right: 1em;
color: #666;
}

</style>
<table class='pretty-table ${options.style}'>
${
options.header === false
? ``
: html`<thead>
${rank ? html`<th></th>` : ""}
${columns.map(c => {
return th(c, sortKey, sortDirection, sortable);
})}
</thead>`
}
<tbody>
${rows.map(
(row, i) => html`<tr>
${rank ? html`<td class='cell-rank'>${i + 1}</td>` : ""}
${columns.map(c => {
let displayValue = (c.options.formatter || identity)(
row[c.key],
i,
row
);
if (
displayValue instanceof window.HTMLElement &&
displayValue.tagName == "TD"
) {
return displayValue;
}
return html`<td class='cell-type-${
c.type
}'>${displayValue}</td>`;
})}
</tr>`
)}
</tbody>
</table>
${
pages
? html`<div class='pretty-pager'>
<button data-action="previous">Previous</button>
${Array.from({ length: pages }).map(
(_, i) => html`<button data-page="${i}">${i + 1}</button>`
)}
<button data-action="next">Next</button>
</div>`
: ""
}
</div></div>`;
}

let dom = bake();

function rerender() {
dom.firstChild.remove();
dom.appendChild(bake().firstChild);
}

dom.addEventListener("click", e => {
if (e.target.tagName === "TH" && sortable) {
if (sortKey == e.target.dataset.key) {
sortDirection = !sortDirection;
}
sortKey = e.target.dataset.key;
rerender();
}
if (e.target.tagName === "BUTTON") {
if (e.target.dataset.action) {
switch (e.target.dataset.action) {
case "next":
page++, rerender();
break;
case "previous":
page--, rerender();
break;
}
} else if (e.target.dataset.page) {
(page = parseInt(e.target.dataset.page)), rerender();
}
}
});

return dom;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// spotifyCharts = d3.csv('https://crossorigin.me/https://spotifycharts.com/regional/global/daily/latest/download')
Insert cell
// spotify = spotifyCharts.map(row => {
// return {
// Track: [row['Track Name'], row['Artist']],
// Streams: +row.Streams
// };
// })
Insert cell
Insert cell
Insert cell
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