From b01fbe7b75da29a00da511d929750ee960a6d707 Mon Sep 17 00:00:00 2001 From: Kenny Daniel Date: Sun, 5 May 2024 10:35:23 -0700 Subject: [PATCH] Demo table --- demo.css | 115 ++++++++++++++++++++++++++++++++++++++++++++++------- demo.js | 99 ++++++++++++++++++++++++++++++++++----------- index.html | 41 +++++++++++-------- 3 files changed, 199 insertions(+), 56 deletions(-) diff --git a/demo.css b/demo.css index c4d52c7..07148bd 100644 --- a/demo.css +++ b/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; } diff --git a/demo.js b/demo.js index 1797dd7..700788c 100644 --- a/demo.js +++ b/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 = `${url}` - dropzone.innerHTML += `
Error fetching file\n${head.status} ${head.statusText}
` + content.innerHTML = `${url}` + content.innerHTML += `
Error fetching file\n${head.status} ${head.statusText}
` return } const size = head.headers.get('content-length') if (!size) { - dropzone.innerHTML = `${url}` - dropzone.innerHTML += '
Error fetching file\nNo content-length header
' + content.innerHTML = `${url}` + content.innerHTML += '
Error fetching file\nNo content-length header
' 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 = `${url}` - renderSidebar(asyncBuffer, metadata, url) + await render(asyncBuffer, metadata, `${url}`) } catch (e) { console.error('Error fetching file', e) - dropzone.innerHTML = `${url}` - dropzone.innerHTML += `
Error fetching file\n${e}
` + content.innerHTML = `${url}` + content.innerHTML += `
Error fetching file\n${e}
` } } 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 = `${file.name}` - dropzone.innerHTML += `
Error parsing file\n${e}
` + content.innerHTML = `${file.name}` + content.innerHTML += `
Error parsing file\n${e}
` } } reader.onerror = e => { console.error('Error reading file', e) - dropzone.innerText = `Error reading file\n${e.target.error}` + content.innerHTML = `${file.name}` + content.innerHTML += `
Error reading file\n${e.target.error}
` } 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 = `${name}` // 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 +} diff --git a/index.html b/index.html index 41524d1..084ba95 100644 --- a/index.html +++ b/index.html @@ -7,24 +7,31 @@ -
- +
+ Drop .parquet file +
+ +
+
+ Drop .parquet file here +
+