---
title: 'Tabular Data Viewer'
author: Taras Novak (@RandomFractals)
categories: [ignore]
format:
html:
code-copy: true
code-fold: true
code-summary: 'code'
code-tools: true
---
Use this [ Tabular Data Viewer 中 ](https://twitter.com/hashtag/TabularDataViewer?src=hashtag_click) notebook 📓 to view sample datasets or preview any public
[ Apache Arrow ](https://observablehq.com/@randomfractals/apache-arrow) ,
**CSV**, or **JSON array** data with [ Summary Table ](https://observablehq.com/@observablehq/summary-table) and [ Tabulator ](http://tabulator.info/) . Just paste your data url to fetch it.
Also, try our [ Data Table ](https://github.com/RandomFractals/vscode-data-table/releases) 🈸 notebook 📓 cell ⌗ output renderer extension with [ VSCode Notebooks ](https://marketplace.visualstudio.com/search?target=VSCode&category=Notebooks&sortBy=Installs) 📚.
```{ojs}
// viewof dataSet = Inputs.select(dataSets, {
// label: html`<b>dataset</b>`
// })
// viewof dataUrl = Inputs.text(
// label: html`<b>dataUrl</b>`,
// placeholder: 'type data url and click fetch',
// value: `${dataUrlParam ? dataUrlParam : 'https://raw.githubusercontent.com/vega/vega-datasets/master/data/' + dataSet}`,
// submit: 'fetchData'
// })
dataPath = "https://raw.githubusercontent.com/Post45-Data-Collective/data/refs/heads/main/hathitrust_fiction/hathitrust_post45fiction_metadata.csv"
data = fetchData(dataPath)
defaultDisplayedColumns = [
"shorttitle",
"inferreddate",
"author",
"recordid",
"hathi_rights",
"genres",
"id",
"docid",
"juvenileprob",
"nonficprob",
"author_authorized_heading",
"author_lccn",
"author_viaf",
"author_wikidata_qid"
];
categoryColumns = ["hathi_rights", "genres","geographics"];
// Reorder the available columns to match defaultDisplayedColumns, appending any extra columns at the end
orderedColumns = [
...defaultDisplayedColumns,
...Object.keys(data[0] || {}).filter(col => !defaultDisplayedColumns.includes(col))
];
```
::: {.callout-note icon="false" collapse="true"}
## View Summary of Columns
```{ojs}
//|echo: false
// Create a checkbox input with ordered columns
// Generate Summary Data based on selected columns
summary_data = {
const selected = new Set(selectedColumns);
return data.map(row => {
let newRow = {};
for (let key in row) {
if (selected.has(key)) {
newRow[key] = row[key];
}
}
return newRow;
});
}
viewof dataSummaryView = SummaryTable(summary_data);
```
:::
## Select Columns
```{ojs}
viewof selectedColumns = Inputs.checkbox(orderedColumns, {
label: "Select Columns",
value: defaultDisplayedColumns, // Default selected columns
});
```
```{ojs}
function generateTabulatorTableForObservable(
containerId,
data,
{
displayedColumns = null,
columnWidths = {},
currencyColumns = [],
categoryColumns = [],
numericColumns = [],
rangeValues = {},
sortColumns = [],
sortOrders = [],
columnPopups = [],
height = 645,
paginationSize = 1000,
} = {}
) {
if (!data || data.length === 0) {
console.error("No data provided to generate the table.");
return null;
}
if (!displayedColumns) {
displayedColumns = Object.keys(data[0]);
}
const tabulatorColumns = displayedColumns.map((column) => ({
title: column,
field: column,
maxInitialWidth: 150,
resizable: true,
headerFilter: numericColumns.includes(column)
? "input"
: categoryColumns.includes(column)
? "list"
: "input",
headerFilterParams: categoryColumns.includes(column)
? { valuesLookup: true, clearable: true }
: {},
formatter: currencyColumns.includes(column) ? "money" : undefined,
formatterParams: currencyColumns.includes(column)
? { symbol: "$", precision: 0 }
: undefined,
}));
const table = new Tabulator(containerId, {
height: height,
data: data,
columns: tabulatorColumns,
columnDefaults: {
headerMenu: columnHeaderMenu
},
// movableColumns: true,
layout: "fitDataStretch",
pagination: true,
paginationSize: paginationSize,
paginationSizeSelector: [10, 100, 1000, 10000],
paginationCounter: "rows",
persistenceMode: false,
initialSort: sortColumns.map((col, idx) => ({
column: col,
dir: sortOrders[idx] || "asc",
})),
});
return table;
}
```
```{ojs}
// Add fullscreen event listener
document.addEventListener("fullscreenchange", () => {
if (document.fullscreenElement) {
// Fullscreen mode: adjust table height
table.setHeight(window.innerHeight - 50); // Adjust for padding/margins
} else {
// Exit fullscreen: reset table height
table.setHeight(645);
}
});
// Add download buttons
document
.querySelectorAll("#button-container button")
.forEach((button) => {
button.addEventListener("click", () => {
const type = button.getAttribute("id").replace("download-", "");
const downloadFilename = `${filename}.${type}`;
if (type === "xlsx") {
table.download("xlsx", downloadFilename, { sheetName: "Sheet1" });
} else if (type === "pdf") {
table.download("pdf", downloadFilename, {
orientation: "portrait",
title: "Table Data"
});
} else if (type === "html") {
table.download("html", downloadFilename, { style: true });
} else {
table.download(type, downloadFilename);
}
});
});
// Update raw data download button dynamically
// Raw data download using `download` function
const rawDownloadButton = document.getElementById("download-raw");
rawDownloadButton.addEventListener("click", () => {
download(dataPath, `${filename}.csv`);
});
// Copy URL button
document.getElementById("copy-url").addEventListener("click", () => {
const tempInput = document.createElement("input");
tempInput.value = dataPath;
document.body.appendChild(tempInput);
tempInput.select();
document.execCommand("copy");
document.body.removeChild(tempInput);
alert("URL copied to clipboard: " + dataPath);
});
```
```{ojs}
// Observable Inputs
// Create Tabulator Table
html`<div id="table-container" style="height: 600px"></div>`
generateTabulatorTableForObservable("#table-container", data, {
displayedColumns: selectedColumns,
columnPopups: [
"Shortened title of the work", // shorttitle
"Inferred date of the work", // inferreddate
"Author of the work", // author
"Unique record ID", // recordid
"Rights code from HathiTrust", // hathi_rights
"Genres associated with the work", // genres
"Unique identifier for the title in the titles dataset (may contain duplicates for reprinted works)", // id
"Unique volume ID from HathiTrust", // docid (htid)
"Probability that the work is for a juvenile audience", // juvenileprob
"Probability that the work is nonfiction", // nonficprob
"Author’s authorized Name Authority Cooperative (NACO) heading", // author_authorized_heading
"Author’s LCCN from id.loc.gov", // author_lccn
"Author’s viaf.org cluster number", // author_viaf
"Author’s Wikidata Q number" // author_wikidata_qid
],
// columnWidths: { "gender": "50px", "role": "75px", "mfa_degree": "100px", "prize_name": "100px" },
currencyColumns: ["prize_amount"],
categoryColumns: ["hathi_rights", "genres","geographics"],
// sortColumns: ["prize_year"],
// sortOrders: ["desc"]
paginationSize: 1000,
});
```
<div id='button-container2'>
<button id="download-raw"><i class="fas fa-download"></i></i> Download Full Data</button>
<button id="copy-url"><i class="fas fa-copy"></i> Copy Full Data URL</button>
</div>
<div id='button-container'>
<button id="download-csv"><i class="fas fa-download"></i></i> Download CSV</button>
<button id="download-json"><i class="fas fa-download"></i></i> Download JSON</button>
<button id="download-xlsx"><i class="fas fa-download"></i></i> Download Excel</button>
</div>
## Tabular Data View
```{ojs}
columnHeaderMenu = [
{
label: 'Hide Column',
action: function (e, column) {
column.hide();
}
},
{
label: "Freeze Column",
action: function (e, column) {
column.updateDefinition({ frozen: true });
}
},
{
label: 'Delete Column',
action: function (e, column) {
column.delete();
}
}
];
```
### Table UX Tips
- Hold **CTRL** or **Shift** key when clicking on column headers to **Sort** table data by multiple columns.
- See [ Clipboard ](http://tabulator.info/docs/5.0/clipboard) options for the clipboard copy and paste data options.
- Use row context menu to **Delete** or **Freeze** a row.
## Data
```{ojs}
// data = fetchData(dataUrl)
dataUrlParam = new URLSearchParams(html`<a href>`.search).get('dataUrl')
async function fetchData(dataUrl) {
let data = [];
if (dataUrl.endsWith('.csv')) {
data = await d3.csvParse(await d3.text(dataUrl), d3.autoType);
}
else if (dataUrl.endsWith('.json')) {
data = await d3.json(dataUrl);
}
else if (dataUrl.endsWith('.arrow')) {
data = loadArrowData(dataUrl);
}
return data;
}
async function loadArrowData(dataUrl){
const response = await fetch(dataUrl);
const arrayBuffer = await response.arrayBuffer();
const table = arrow.Table.from(new Uint8Array(arrayBuffer));
const rows = Array(table.length);
const fields = table.schema.fields.map(d => d.name);
for (let i=0, n=rows.length; i<n; ++i) {
const proto = {};
fields.forEach((fieldName, index) => {
const column = table.getColumnAt(index);
proto[fieldName] = column.get(i);
});
rows[i] = proto;
}
return rows;
}
async function download(dataurl, fileName) {
const response = await fetch(dataurl);
const blob = await response.blob();
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = fileName;
link.click();
URL.revokeObjectURL(link.href); // Clean up
}
```
## Table Styles
```{ojs}
tableStyles = html `
<style>
/* add space for sort direction symbol */
thead th span {
margin-left: 0 !important;
padding-right: 0.4rem;
}
.tabulator .tabulator-tableholder{
background-color: aliceblue;
}
</style>`
html`<link href='https://unpkg.com/tabulator-tables@5.3.1/dist/css/tabulator.min.css' rel='stylesheet' />
tabulator.min.css`
```
## Imports
```{ojs}
// import Juan's summary table
import { SummaryTable } from "fcb971390dae8f6d"
Tabulator = require('https://unpkg.com/tabulator-tables@5.3.1');
arrow = require('apache-arrow@6.0.1')
```