---
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' )
```