Published
Edited
Jan 3, 2021
Fork of HyperGrid
Insert cell
Insert cell
HyperGrid(randomData, { columnWidth: 120 })
Insert cell
md`## Example invocations

\`\`\`js
// No options
HyperGrid(data)

// Basic options
HyperGrid(data, {tableHeight: 500})

// Custom columns
HyperGrid(data, {
columns: [{
name: "sum",
formatter: d => d.a + d.b
}]
});
\`\`\`

## Overriding styles

You can import HyperGrid with replacement styles, but you'll need to style everything yourself. See the \`hypergridStyle\` variable in this notebook for a starting point or check out the [override demo](https://observablehq.com/@chicagoreporter/hypergrid-style-override-example).

\`\`\`js
import { HyperGrid } with {
html\`<style>...</style>\` as hypergridStyle
} from "@chicagoreporter/hypergrid"
\`\`\`

## Column formatting

**Warning:** If \`columns\` are specifed as an option, *only* the columns specified in the options will be drawn. If columns are not specified, the data must be an array of objects with the same columns and the grid will be drawn with keys as columns and equal column widths.

This all or nothing approach lets us handle the simple case effectively while providing maximum flexibility for more complex cases or datasets.

Column formatters must be a function that takes the active row, the column definition, row index, and entire dataset as parameters:

\`\`\`js
{
...
formatter: (row, column, index, data) => html\`Row number $\{index\} - $\{row.first_name\}\`
}
\`\`\`

## API

- **data** \`Array<Object>\`: All objects must have the same keys unless columns are specified as an option.
- **options**
- **tableHeight** \`number\`: (default: 250) Height of the table container in pixels.
- **itemHeight** \`number\`: (default: 28) Height of each row in pixels.
- **columnWidth** \`number\`: (default: 80) Default column width in pixels.
- **columnMargin** \`number\`: (default: 5) Default column margin in pixels.
- **showHeader** \`Boolean\`: (default: true) Show table header.
- **columns** \`Array\`:
- **formatter** \`Function(row, columnName) => output\`: value formatter for cells. Return elements from the row using this function. This can return a string, number, or element. (**Required**).
- **name** \`string\`: Column label.
- **width** \`number\`: Column width in pixels.
- **margin** \`number\`: Column margin in pixels.

## Roadmap & bugs

* Doesn't handle dynamic row height, e.g. when text breaks onto multiple lines. Instead, text truncation with ellipsis is used.
* For accessibility, could have [hidden pagination controls](https://twitter.com/thomaswilburn/status/1232015803748409345).

Includes a lot of inspiration from @tmcw's [tables](https://observablehq.com/@tmcw/tables).
`
Insert cell
Insert cell
HyperGrid(randomData)
Insert cell
Insert cell
HyperGrid(randomData, { tableHeight: 220, itemHeight: 38, columnWidth: 120 })
Insert cell
Insert cell
HyperGrid(randomData, { showHeader: false })
Insert cell
Insert cell
Insert cell
Insert cell
HyperGrid([])
Insert cell
Insert cell
function HyperGrid(
data,
{
tableHeight = 320,
itemHeight = 28,
showHeader = true,
columns = null,
columnWidth = 80,
columnMargin = 5
} = {}
) {
if (!data) {
return "No data";
} else if (!data.length) {
return "Empty data";
}

const renderColumns = columns
? columns.map(c => {
c.width = c.hasOwnProperty("width") ? c.width : columnWidth;
c.margin = c.hasOwnProperty("margin") ? c.margin : columnMargin;
return c;
})
: Object.keys(data[0]).map(key => ({
name: key,
formatter: defaultFormatter,
width: columnWidth,
margin: columnMargin
}));

// Create a container element or find one that already exists in the DOM.
const parentElement = html`<div class="vtable" style="height: ${tableHeight}px;"></div>`;
const innerElement = document.createElement('div');
parentElement.append(innerElement);

const headerWidth = renderColumns.reduce(
(acc, column) => acc + column.width + column.margin,
0
);

const list = HyperList.create(innerElement, {
width,
height: tableHeight,
total: data.length,
itemHeight: itemHeight,
applyPatch: (element, fragment) => {
element.innerHTML = '';
if (showHeader) {
element.appendChild(html`
<div class="vrow-inner vrow-header" style="width: ${headerWidth}px;">
${renderColumns.map(column =>
hypergridCell(html`${column.name}`, column, data, 0, itemHeight)
)}
</div>
`);
fragment.childNodes.forEach(n => {
n.style.transform = `translateY(${itemHeight}px)`;
});
}
element.appendChild(fragment);
},
// Wire up the data to the index. The index is then mapped to a Y position
// in the container.
generate(index) {
return html`
<div class="vrow-inner">
${renderColumns.map(column =>
hypergridCell(
html`${column.formatter(data[index], column, index, data)}`,
column,
data,
index,
itemHeight
)
)}
</div>`;
}
});

parentElement.append(hypergridStyle());
return parentElement;
}
Insert cell
hypergridCell = (content, column, data, index, itemHeight) => html`
<div style="height: ${itemHeight}px; width: ${column.width}px; margin-right: ${column.margin}px;">
<span>${content}</span>
</div>`
Insert cell
// Default formatter
defaultFormatter = (row, column) => row[column.name]
Insert cell
fontSize = 13
Insert cell
hypergridStyle = () => html`<style>
.vtable {
font-size: 12px;
line-height: 14px;
overflow-x: hidden;
overflow-y: hidden;
font-family: Helvetica Neue, Helvetica, sans-serif;
}
.vrow-inner {
display: flex;
}
.vrow-inner > div {
display: flex;
align-items: center;
}
.vrow-inner > div > span {
display: inline-block;
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.vrow-header {
position: sticky;
z-index: 99;
left: 0;
top: 0;
background: rgb(221,221,221);
background: linear-gradient(0deg, rgba(255,255,255,1) 15%, rgba(248,248,248,1) 90%);
border-bottom: 1px solid #666;
font-weight: bold;
}
</style>`
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more