Demo table

This commit is contained in:
Kenny Daniel 2024-05-05 10:35:23 -07:00
parent 5fdb71fca3
commit b01fbe7b75
No known key found for this signature in database
GPG Key ID: 90AB653A8CAD7E45
3 changed files with 199 additions and 56 deletions

115
demo.css

@ -8,11 +8,15 @@ body {
display: flex;
font-family: sans-serif;
height: 100vh;
width: 100vw;
}
nav {
width: 320px;
border-right: 1px solid #ddd;
min-width: 320px;
overflow-x: hidden;
overflow-y: auto;
padding: 10px;
width: 320px;
}
h1 {
font-size: 20pt;
@ -24,32 +28,110 @@ p {
margin: 10px 0;
width: 300px;
}
#welcome {
align-items: center;
cursor: pointer;
display: flex;
font-size: 20px;
height: 100%;
justify-content: center;
.error {
color: #c11;
font-family: monospace;
white-space: pre-wrap;
}
#overlay {
align-items: center;
font-size: 125%;
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
background-color: rgba(240, 240, 240, 0.6);
backdrop-filter: blur(4px);
display: none;
padding: 12px;
z-index: 40;
}
#dropzone {
display: flex;
flex: 1;
max-width: 100vw;
}
#content {
display: flex;
flex-direction: column;
flex: 1;
min-width: 0;
overflow: auto;
}
#welcome {
border: 2px dashed #08e;
border-radius: 10px;
color: #444;
flex: 1;
margin: 10px;
overflow: auto;
padding: 10px;
align-items: center;
cursor: pointer;
display: flex;
flex: 1;
font-size: 20px;
justify-content: center;
}
input[type="file"] {
display: none;
}
.over {
background-color: lightblue;
#overlay {
font-size: 125%;
justify-content: center;
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
background-color: rgba(240, 240, 240, 0.6);
backdrop-filter: blur(4px);
display: none;
padding: 12px;
z-index: 40;
}
.error {
color: #c11;
.over #overlay {
display: flex;
}
/* table */
table {
border-collapse: separate;
border-spacing: 0;
}
table:focus-visible {
outline: none;
}
th {
background-color: #f8f8f8;
border: 1px solid #ddd;
padding: 8px;
text-align: left;
background-color: #eaeaeb;
border: none;
border-top: 4px solid #706fb1;
border-bottom: 2px solid #c9c9c9;
box-sizing: content-box;
color: #444;
position: sticky;
top: -1px; /* fix 1px gap above thead */
user-select: none;
}
th, td {
border-bottom: 1px solid #ddd;
border-right: 1px solid #ddd;
height: 32px;
max-width: 1000px; /* prevent columns expanding */
padding: 4px 12px;
text-align: left;
text-overflow: ellipsis;
overflow: hidden;
white-space: pre-wrap;
}
#layout {
margin-top: 20px;
word-break: break-all;
@ -61,6 +143,9 @@ input[type="file"] {
.layout a:hover {
text-decoration: underline;
}
.layout > strong {
font-size: 10pt;
}
.layout div {
background-color: rgba(0, 0, 0, 0.05);
border: 1px solid #ccc;
@ -120,5 +205,5 @@ nav ul,
#metadata pre {
white-space: pre-wrap;
break-word: break-all;
word-break: break-all;
}

99
demo.js

@ -1,18 +1,28 @@
import { parquetMetadata, parquetMetadataAsync, parquetRead, toJson } from './src/hyparquet.js'
import { parquetMetadata, parquetMetadataAsync, parquetRead, parquetSchema, toJson } from './src/hyparquet.js'
const dropzone = document.getElementById('dropzone')
const fileInput = document.getElementById('file-input')
const content = document.getElementById('content')
const welcome = document.getElementById('welcome')
const layout = document.getElementById('layout')
const metadataDiv = document.getElementById('metadata')
const fileInput = document.getElementById('file-input')
let enterCount = 0
dropzone.addEventListener('dragenter', e => {
e.dataTransfer.dropEffect = 'copy'
dropzone.classList.add('over')
enterCount++
})
dropzone.addEventListener('dragover', e => {
e.preventDefault()
e.dataTransfer.dropEffect = 'copy'
dropzone.classList.add('over')
})
dropzone.addEventListener('dragleave', () => {
dropzone.classList.remove('over')
enterCount--
if (!enterCount) dropzone.classList.remove('over')
})
dropzone.addEventListener('drop', e => {
@ -37,18 +47,19 @@ dropzone.addEventListener('drop', e => {
})
async function processUrl(url) {
content.innerHTML = ''
try {
// Check if file is accessible and get its size
const head = await fetch(url, { method: 'HEAD' })
if (!head.ok) {
dropzone.innerHTML = `<strong>${url}</strong>`
dropzone.innerHTML += `<div class="error">Error fetching file\n${head.status} ${head.statusText}</div>`
content.innerHTML = `<strong>${url}</strong>`
content.innerHTML += `<div class="error">Error fetching file\n${head.status} ${head.statusText}</div>`
return
}
const size = head.headers.get('content-length')
if (!size) {
dropzone.innerHTML = `<strong>${url}</strong>`
dropzone.innerHTML += '<div class="error">Error fetching file\nNo content-length header</div>'
content.innerHTML = `<strong>${url}</strong>`
content.innerHTML += '<div class="error">Error fetching file\nNo content-length header</div>'
return
}
// Construct an AsyncBuffer that fetches file chunks
@ -56,6 +67,7 @@ async function processUrl(url) {
byteLength: Number(size),
slice: async (start, end) => {
const rangeEnd = end === undefined ? '' : end - 1
console.log(`Fetch ${url} bytes=${start}-${rangeEnd}`)
const res = await fetch(url, {
headers: { Range: `bytes=${start}-${rangeEnd}` },
})
@ -63,40 +75,54 @@ async function processUrl(url) {
},
}
const metadata = await parquetMetadataAsync(asyncBuffer)
url = `<a href="${url}">${url}</a>`
renderSidebar(asyncBuffer, metadata, url)
await render(asyncBuffer, metadata, `<a href="${url}">${url}</a>`)
} catch (e) {
console.error('Error fetching file', e)
dropzone.innerHTML = `<strong>${url}</strong>`
dropzone.innerHTML += `<div class="error">Error fetching file\n${e}</div>`
content.innerHTML = `<strong>${url}</strong>`
content.innerHTML += `<div class="error">Error fetching file\n${e}</div>`
}
}
function processFile(file) {
content.innerHTML = ''
const reader = new FileReader()
reader.onload = e => {
reader.onload = async e => {
try {
const arrayBuffer = e.target.result
const metadata = parquetMetadata(arrayBuffer)
renderSidebar(arrayBuffer, metadata, file.name)
const startTime = performance.now()
// parquetRead({ file: arrayBuffer, onComplete(data) {
// const ms = performance.now() - startTime
// console.log(`parsed ${file.name} in ${ms.toFixed(0)} ms`)
// } }) // TODO
await render(arrayBuffer, metadata, file.name)
} catch (e) {
console.error('Error parsing file', e)
dropzone.innerHTML = `<strong>${file.name}</strong>`
dropzone.innerHTML += `<div class="error">Error parsing file\n${e}</div>`
content.innerHTML = `<strong>${file.name}</strong>`
content.innerHTML += `<div class="error">Error parsing file\n${e}</div>`
}
}
reader.onerror = e => {
console.error('Error reading file', e)
dropzone.innerText = `Error reading file\n${e.target.error}`
content.innerHTML = `<strong>${file.name}</strong>`
content.innerHTML += `<div class="error">Error reading file\n${e.target.error}</div>`
}
reader.readAsArrayBuffer(file)
}
async function render(asyncBuffer, metadata, name) {
renderSidebar(asyncBuffer, metadata, name)
const { children } = parquetSchema(metadata)
const header = children.map(child => child.element.name)
const startTime = performance.now()
await parquetRead({
file: asyncBuffer,
rowEnd: 1000,
onComplete(data) {
const ms = performance.now() - startTime
console.log(`parsed ${name} in ${ms.toFixed(0)} ms`)
content.appendChild(renderTable(header, data))
},
})
}
function renderSidebar(asyncBuffer, metadata, name) {
layout.innerHTML = `<strong>${name}</strong>`
// render file layout
@ -106,7 +132,7 @@ function renderSidebar(asyncBuffer, metadata, name) {
metadataDiv.appendChild(fileMetadata(toJson(metadata)))
}
dropzone.addEventListener('click', () => {
welcome.addEventListener('click', () => {
fileInput.click()
})
@ -175,3 +201,28 @@ function fileMetadata(metadata) {
})
return div
}
function renderTable(header, data) {
const table = document.createElement('table')
const thead = document.createElement('thead')
const tbody = document.createElement('tbody')
const headerRow = document.createElement('tr')
for (const columnName of header) {
const th = document.createElement('th')
th.innerText = columnName
headerRow.appendChild(th)
}
thead.appendChild(headerRow)
table.appendChild(thead)
for (const row of data) {
const tr = document.createElement('tr')
for (const value of Object.values(row)) {
const td = document.createElement('td')
td.innerText = value
tr.appendChild(td)
}
tbody.appendChild(tr)
}
table.appendChild(tbody)
return table
}

@ -7,24 +7,31 @@
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Mulish:wght@400;600&display=swap"/>
</head>
<body>
<nav>
<h1>hyparquet</h1>
<h2>parquet file reader</h2>
<p>
Online demo of <a href="https://github.com/hyparam/hyparquet">hyparquet</a>: a parser for apache parquet files.
</p>
<p>
Drag and drop a parquet file onto the dropzone to see parquet data.
</p>
<ul>
<li><a href="https://github.com/hyparam/hyparquet">github</a></li>
<li><a href="https://www.npmjs.com/package/hyparquet">npm</a></li>
</ul>
<div id="layout" class="layout"></div>
<div id="metadata" class="layout"></div>
</nav>
<div id="dropzone">
<label id="welcome">Drop .parquet file here</label>
<div id="overlay">
Drop .parquet file
</div>
<nav>
<h1>hyparquet</h1>
<h2>parquet file reader</h2>
<p>
Online demo of <a href="https://github.com/hyparam/hyparquet">hyparquet</a>: a parser for apache parquet files.
</p>
<p>
Drag and drop a parquet file onto the dropzone to see parquet data.
</p>
<ul>
<li><a href="https://github.com/hyparam/hyparquet">github</a></li>
<li><a href="https://www.npmjs.com/package/hyparquet">npm</a></li>
</ul>
<div id="layout" class="layout"></div>
<div id="metadata" class="layout"></div>
</nav>
<div id="content">
<div id="welcome">
Drop .parquet file here
</div>
</div>
</div>
<input id="file-input" type="file">