Hyperparam style for demo

This commit is contained in:
Kenny Daniel 2024-09-12 00:43:28 -07:00
parent c6111423c1
commit 0711e86d33
No known key found for this signature in database
GPG Key ID: 90AB653A8CAD7E45
15 changed files with 477 additions and 358 deletions

2
.gitattributes vendored Normal file

@ -0,0 +1,2 @@
*.min.js -diff
*.min.js.map -diff

97
demo/App.tsx Normal file

@ -0,0 +1,97 @@
import HighTable, { DataFrame } from 'hightable'
import { compressors } from 'hyparquet-compressors'
import React, { useState } from 'react'
import { FileMetaData, parquetMetadata, parquetMetadataAsync, parquetSchema } from '../src/metadata.js'
import { parquetRead } from '../src/read.js'
import type { AsyncBuffer } from '../src/types.js'
import { asyncBufferFromUrl } from '../src/utils.js'
import Dropzone from './Dropzone.js'
import Layout from './Layout.js'
/**
* Hyparquet demo viewer page
* @returns {ReactNode}
*/
export default function App() {
const [progress, setProgress] = useState<number>()
const [error, setError] = useState<Error>()
const [df, setDf] = useState<DataFrame>()
const [name, setName] = useState<string>()
const [byteLength, setByteLength] = useState<number>()
async function onFileDrop(file: File) {
const arrayBuffer = await file.arrayBuffer()
const metadata = parquetMetadata(arrayBuffer)
setName(file.name)
setByteLength(file.size)
setDf(parquetDataFrame(arrayBuffer, metadata))
document.getElementById('welcome')?.remove()
}
async function onUrlDrop(url: string) {
const asyncBuffer = await asyncBufferFromUrl(url)
const metadata = await parquetMetadataAsync(asyncBuffer)
setName(url)
setByteLength(asyncBuffer.byteLength)
setDf(parquetDataFrame(asyncBuffer, metadata))
document.getElementById('welcome')?.remove()
}
return <Layout progress={progress} error={error}>
<Dropzone
onError={(e) => setError(e)}
onFileDrop={onFileDrop}
onUrlDrop={onUrlDrop}>
{df && <>
<div className='top-header'>{name}</div>
<div className='view-header'>
{byteLength !== undefined && <span title={byteLength.toLocaleString() + ' bytes'}>{formatFileSize(byteLength)}</span>}
<span>{df.numRows.toLocaleString()} rows</span>
</div>
<HighTable data={df} />
</>}
</Dropzone>
</Layout>
}
/**
* Convert a parquet file into a dataframe.
*
* @param {AsyncBuffer} file - parquet file asyncbuffer
* @param {FileMetaData} metadata - parquet file metadata
* @returns {DataFrame} dataframe
*/
function parquetDataFrame(file: AsyncBuffer, metadata: FileMetaData): DataFrame {
const { children } = parquetSchema(metadata)
return {
header: children.map(child => child.element.name),
numRows: Number(metadata.num_rows),
/**
* @param {number} rowStart
* @param {number} rowEnd
* @returns {Promise<any[][]>}
*/
rows(rowStart, rowEnd) {
console.log(`reading rows ${rowStart}-${rowEnd}`)
return new Promise((resolve, reject) => {
parquetRead({ file, compressors, rowStart, rowEnd, onComplete: resolve })
.catch(reject)
})
},
}
}
/**
* Returns the file size in human readable format.
*
* @param {number} bytes file size in bytes
* @returns {string} formatted file size string
*/
function formatFileSize(bytes: number): string {
const sizes = ['b', 'kb', 'mb', 'gb', 'tb']
if (bytes === 0) return '0 b'
const i = Math.floor(Math.log2(bytes) / 10)
if (i === 0) return bytes + ' b'
const base = bytes / Math.pow(1024, i)
return (base < 10 ? base.toFixed(1) : Math.round(base)) + ' ' + sizes[i]
}

131
demo/Dropzone.tsx Normal file

@ -0,0 +1,131 @@
import React from 'react'
import { ReactNode, useEffect, useRef, useState } from 'react'
interface DropzoneProps {
children: ReactNode
onFileDrop: (file: File) => void
onUrlDrop: (url: string) => void
onError: (error: Error) => void
}
/**
* A dropzone component for uploading files.
*
* Shows a fullscreen overlay when files are dragged over the dropzone.
*
* You can have an element inside the dropzone that triggers the file input
* dialog when clicked by adding the class 'dropzone-select' to it.
*
* @param {Object} props
* @param {ReactNode} props.children - message to display in dropzone.
* @param {Function} props.onFileDrop - called when a file is dropped.
* @param {Function} props.onUrlDrop - called when a url is dropped.
* @param {Function} props.onError - called when an error occurs.
* @returns {ReactNode}
*/
export default function Dropzone({ children, onFileDrop, onUrlDrop, onError }: DropzoneProps) {
const dropzoneRef = useRef<HTMLDivElement>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
// number of dragenter events minus dragleave events
const [enters, setEnters] = useState(0)
/**
* Trigger file input dialog.
* @param {MouseEvent} e - click
*/
function triggerFileSelect(e: React.MouseEvent<HTMLDivElement>) {
// If click inside '.dropzone-select', activate file input dialog
if ((e.target as Element).closest('.dropzone-select')) {
fileInputRef.current?.click()
}
}
/**
* Handle file selection event.
* Recursively upload files and directories, in parallel.
* @param {ChangeEvent} e
* @returns {void}
*/
function handleFileSelect(e: React.ChangeEvent<HTMLInputElement>) {
const { files } = e.target
if (!files) return
for (let i = 0; i < files.length; i++) {
const file = files[i]
// TODO: Load file view
}
}
useEffect(() => {
const dropzone = dropzoneRef.current
if (!dropzone) return
// Attach drag-and-drop event listeners
function onDragEnter(e: DragEvent) {
// check if any of the items are files (not strings)
const items = e.dataTransfer?.items
if (!items) return
if (!Array.from(items).some(item => item.kind === 'file')) return
setEnters(enters => enters + 1)
}
function onDragOver(e: DragEvent) {
e.preventDefault()
}
function onDragLeave() {
setEnters(enters => enters - 1)
}
function handleFileDrop(e: DragEvent) {
e.preventDefault()
setEnters(0)
if (!e.dataTransfer) throw new Error('Missing dataTransfer')
const { files, items } = e.dataTransfer
if (files.length > 0) {
const file = files[0]
onFileDrop(file)
}
if (items.length > 0) {
const item = items[0]
if (item.kind === 'string') {
item.getAsString(url => {
if (url.startsWith('http')) {
onUrlDrop(url)
}
})
}
}
}
dropzone.addEventListener('dragenter', onDragEnter)
dropzone.addEventListener('dragover', onDragOver)
dropzone.addEventListener('dragleave', onDragLeave)
dropzone.addEventListener('drop', handleFileDrop)
// Cleanup event listeners when component is unmounted
return () => {
dropzone.removeEventListener('dragenter', onDragEnter)
dropzone.removeEventListener('dragover', onDragOver)
dropzone.removeEventListener('dragleave', onDragLeave)
dropzone.removeEventListener('drop', handleFileDrop)
}
})
return (
<div
className={enters > 0 ? 'dropzone hover' : 'dropzone'}
onClick={triggerFileSelect}
ref={dropzoneRef}>
{children}
<div className='overlay'>
<div className='target'>
<div>Drop files to view. 👀</div>
</div>
</div>
<input
multiple
onChange={handleFileSelect}
ref={fileInputRef}
style={{ display: 'none' }}
type="file" />
</div>
)
}

57
demo/Layout.tsx Normal file

@ -0,0 +1,57 @@
import React, { ReactNode, useEffect } from 'react'
interface LayoutProps {
children: ReactNode
className?: string
progress?: number
error?: Error
}
/**
* Layout for shared UI.
* Content div style can be overridden by className prop.
*
* @param {Object} props
* @param {ReactNode} props.children - content to display inside the layout
* @param {string | undefined} props.className - additional class names to apply to the content container
* @param {number | undefined} props.progress - progress bar value
* @param {Error} props.error - error message to display
* @returns {ReactNode}
*/
export default function Layout({ children, className, progress, error }: LayoutProps) {
const errorMessage = error?.toString()
if (error) console.error(error)
useEffect(() => {
document.title = 'hyparquet demo - apache parquet file viewer online'
}, [])
return <>
<div className='content-container'>
<div className={cn('content', className)}>
{children}
</div>
<div className={cn('error-bar', error && 'show-error')}>{errorMessage}</div>
</div>
{progress !== undefined && progress < 1 &&
<div className={'progress-bar'} role='progressbar'>
<div style={{ width: `${100 * progress}%` }} />
</div>
}
</>
}
/**
* Helper function to join class names.
* Filters out falsy values and joins the rest.
*
* @param {...string | undefined | false} names - class name(s) to join
* @returns {string}
*/
export function cn(...names: (string | undefined | false)[]): string {
return names.filter(n => n).join(' ')
}
export function Spinner({ className }: { className: string }) {
return <div className={cn('spinner', className)}></div>
}

4
demo/bundle.min.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -10,32 +10,40 @@ body {
height: 100vh;
width: 100vw;
}
nav {
border-right: 1px solid #ddd;
min-width: 320px;
overflow-x: hidden;
overflow-y: auto;
padding: 10px;
min-width: 0;
max-width: 40%;
width: 320px;
}
h1 {
font-size: 20pt;
font-size: 22pt;
}
h2 {
margin-top: 10px;
font-size: 12pt;
}
p {
margin: 10px 0;
margin: 15px 0;
}
code {
font-family: monospace;
padding: 10px;
white-space: pre-wrap;
word-break: break-all;
}
.error {
color: #c11;
font-family: monospace;
white-space: pre-wrap;
}
#overlay {
/* dropzone */
.dropzone {
display: flex;
flex-direction: column;
height: 100%;
}
.dropzone.hover .overlay {
display: flex;
}
.overlay {
align-items: center;
font-size: 125%;
position: absolute;
@ -50,37 +58,157 @@ p {
z-index: 40;
}
#dropzone {
display: flex;
flex: 1;
max-width: 100vw;
/* sidebar */
nav {
height: 100vh;
min-width: 48px;
background-image: linear-gradient(to bottom, #667, #585669);
box-shadow: 0 0 4px rgba(10, 10, 10, 0.5);
height: 100vh;
}
/* brand logo */
.brand {
color: #fff;
display: flex;
align-items: center;
filter: drop-shadow(0 0 2px #444);
font-family: 'Century Gothic', 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-size: 1.1em;
font-weight: bold;
text-orientation: mixed;
opacity: 0.85;
padding: 10px 12px;
user-select: none;
writing-mode: vertical-rl;
text-decoration: none;
}
.brand:hover {
color: #fff;
filter: drop-shadow(0 0 2px #333);
opacity: 0.9;
text-decoration: none;
}
.brand::before {
content: '';
background: url(logo.svg) no-repeat 0 center;
background-size: 26px;
height: 26px;
width: 26px;
margin-bottom: 10px;
}
/* content area */
main,
#content {
display: flex;
flex-direction: column;
flex: 1;
min-width: 0;
overflow: auto;
}
#content {
position: relative;
}
#app {
flex: 1;
}
/* content area */
.content-container {
min-width: 0;
height: 100vh;
display: flex;
flex-direction: column;
flex: 1;
}
.content {
display: flex;
flex-direction: column;
flex: 1;
height: 100vh;
padding: 0;
/* no outer scrollbars */
overflow: hidden;
}
.top-header {
align-items: center;
background: linear-gradient(to right, #353540, #24202b);
color: #dde4ea;
display: flex;
height: 32px;
justify-content: space-between;
min-height: 32px;
padding-left: 8px;
}
.top-header {
color: #f0f8ff;
font-family: 'Courier New', Courier, monospace;
font-size: 18px;
text-overflow: ellipsis;
white-space: nowrap;
text-decoration-thickness: 1px;
}
.view-header {
align-items: center;
background-color: #ccc;
color: #444;
display: flex;
gap: 16px;
height: 24px;
padding: 0 8px;
/* all one line */
text-overflow: ellipsis;
white-space: nowrap;
}
.viewer {
display: flex;
flex: 1;
flex-direction: column;
white-space: pre-wrap;
overflow-y: auto;
}
#table {
display: flex;
flex: 1;
min-height: 0;
}
/* welcome */
#welcome {
border: 2px dashed #08e;
position: absolute;
bottom: 0;
top: 0;
right: 0;
left: 0;
border: 2px #777;
border-radius: 10px;
color: #444;
margin: 10px;
padding: 10px;
align-items: center;
cursor: pointer;
display: flex;
flex-direction: column;
flex: 1;
font-size: 20px;
justify-content: center;
max-width: 640px;
margin: 0 auto;
}
#welcome ul {
margin-top: 10px;
margin-left: 30px;
}
/* file upload */
input[type="file"] {
display: none;
}
#overlay {
.overlay {
font-size: 125%;
justify-content: center;
position: absolute;
@ -94,7 +222,7 @@ input[type="file"] {
padding: 12px;
z-index: 40;
}
.over #overlay {
.over .overlay {
display: flex;
}
@ -222,6 +350,10 @@ td:first-child {
}
/* layout */
.layout {
margin: 10px;
max-width: 480px;
}
.layout,
.layout .group,
.layout .cell {
@ -249,7 +381,6 @@ td:first-child {
font-size: 10px;
}
nav ul,
.layout div ul {
list-style: none;
}
@ -258,27 +389,3 @@ nav ul,
padding: 2px 4px;
text-align: right;
}
.collapsed > :not(:first-child) {
display: none;
}
.layout h2 {
cursor: pointer;
user-select: none;
}
.layout h2::before {
content: "▼";
display: inline-block;
font-size: 10px;
margin: 0 4px;
vertical-align: middle;
}
.layout.collapsed h2::before {
content: "▶";
}
.layout pre {
white-space: pre-wrap;
word-break: break-all;
}

@ -1,113 +1,10 @@
import HighTable from 'hightable'
import { compressors } from 'hyparquet-compressors'
import React from 'react'
import ReactDOM from 'react-dom'
import {
parquetMetadata, parquetMetadataAsync, parquetRead, parquetSchema, toJson,
} from '../src/hyparquet.js'
import { asyncBufferFromUrl } from '../src/utils.js'
import { initDropzone } from './dropzone.js'
import { fileLayout, fileMetadata } from './layout.js'
import ReactDOM from 'react-dom/client'
import App from './App.js'
/**
* @typedef {import('../src/types.js').AsyncBuffer} AsyncBuffer
* @typedef {import('../src/types.js').FileMetaData} FileMetaData
*/
const app = document.getElementById('app')
if (!app) throw new Error('missing app element')
const content = document.querySelectorAll('#content')[0]
// Initialize drag-and-drop
initDropzone(handleFileDrop, handleUrlDrop)
/**
* @param {string} url
*/
async function handleUrlDrop(url) {
content.innerHTML = ''
try {
const asyncBuffer = await asyncBufferFromUrl(url)
const metadata = await parquetMetadataAsync(asyncBuffer)
await render(asyncBuffer, metadata, `<a href="${url}">${url}</a>`)
} catch (e) {
console.error('Error fetching url', e)
content.innerHTML += `<div class="error">Error fetching url ${url}\n${e}</div>`
}
}
/**
* @param {File} file
*/
function handleFileDrop(file) {
content.innerHTML = ''
const reader = new FileReader()
reader.onload = async e => {
try {
const arrayBuffer = e.target?.result
if (!(arrayBuffer instanceof ArrayBuffer)) throw new Error('Missing arrayBuffer')
const metadata = parquetMetadata(arrayBuffer)
await render(arrayBuffer, metadata, file.name)
} catch (e) {
console.error('Error parsing file', e)
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)
content.innerHTML = `<strong>${file.name}</strong>`
content.innerHTML += `<div class="error">Error reading file\n${e.target?.error}</div>`
}
reader.readAsArrayBuffer(file)
}
/**
* @param {AsyncBuffer} file
* @param {FileMetaData} metadata
* @param {string} name
*/
function render(file, metadata, name) {
renderSidebar(file, metadata, name)
const { children } = parquetSchema(metadata)
const dataframe = {
header: children.map(child => child.element.name),
numRows: Number(metadata.num_rows),
/**
* @param {number} rowStart
* @param {number} rowEnd
* @returns {Promise<any[][]>}
*/
rows(rowStart, rowEnd) {
console.log(`reading rows ${rowStart}-${rowEnd}`)
return new Promise((resolve, reject) => {
parquetRead({ file, compressors, rowStart, rowEnd, onComplete: resolve })
.catch(reject)
})
},
}
renderTable(dataframe)
}
/**
* @param {AsyncBuffer} asyncBuffer
* @param {FileMetaData} metadata
* @param {string} name
*/
function renderSidebar(asyncBuffer, metadata, name) {
const sidebar = /** @type {HTMLElement} */ (document.getElementById('sidebar'))
sidebar.innerHTML = `<div id="filename">${name}</div>`
sidebar.appendChild(fileMetadata(toJson(metadata)))
sidebar.appendChild(fileLayout(metadata, asyncBuffer))
}
/**
* @param {import('hightable').DataFrame} data
*/
function renderTable(data) {
// Load HighTable.tsx and render
const container = document.getElementById('content')
// @ts-expect-error ReactDOM type issue
const root = ReactDOM.createRoot(container)
root.render(React.createElement(HighTable, { data }))
}
// @ts-expect-error TODO: fix react createRoot type
const root = ReactDOM.createRoot(document.getElementById('app'))
root.render(React.createElement(App))

@ -1,60 +0,0 @@
/**
* Initialize the dropzone for file and url drag-and-drop.
*
* @param {Function} handleFileDrop
* @param {Function} handleUrlDrop
*/
export function initDropzone(handleFileDrop, handleUrlDrop) {
let enterCount = 0
const dropzone = /** @type {HTMLElement} */ (document.getElementById('dropzone'))
const fileInput = /** @type {HTMLInputElement} */ (document.getElementById('file-input'))
const welcome = document.querySelectorAll('#welcome')[0]
// Click to select file
welcome.addEventListener('click', () => {
fileInput?.click()
})
fileInput?.addEventListener('change', () => {
if (fileInput.files?.length) {
handleFileDrop(fileInput.files[0])
}
})
dropzone.addEventListener('dragenter', e => {
if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'
dropzone.classList.add('over')
enterCount++
})
dropzone.addEventListener('dragover', e => {
e.preventDefault()
})
dropzone.addEventListener('dragleave', () => {
enterCount--
if (!enterCount) dropzone.classList.remove('over')
})
dropzone.addEventListener('drop', e => {
e.preventDefault() // prevent dropped file from being "downloaded"
dropzone.classList.remove('over')
if (!e.dataTransfer) throw new Error('Missing dataTransfer')
const { files, items } = e.dataTransfer
if (files.length > 0) {
const file = files[0]
handleFileDrop(file)
}
if (items.length > 0) {
const item = items[0]
if (item.kind === 'string') {
item.getAsString(str => {
if (str.startsWith('http')) {
handleUrlDrop(str)
}
})
}
}
})
}

@ -1,122 +0,0 @@
/**
* @typedef {import('../src/types.js').FileMetaData} FileMetaData
*/
import { getColumnRange } from '../src/column.js'
/**
* @param {FileMetaData} metadata
* @returns {HTMLDivElement}
*/
export function fileMetadata(metadata) {
let html = '<h2>Metadata</h2>'
html += `<pre>${JSON.stringify(metadata, null, 2)}</pre>`
const div = document.createElement('div')
div.innerHTML = html
div.classList.add('layout', 'collapsed') // start collapsed
div.children[0].addEventListener('click', () => {
div.classList.toggle('collapsed')
})
return div
}
/**
* Render parquet file layout.
*
* @param {FileMetaData} metadata
* @param {import('../src/types.js').AsyncBuffer} asyncBuffer
* @returns {HTMLDivElement}
*/
export function fileLayout(metadata, asyncBuffer) {
let html = '<h2>File layout</h2>'
html += cell('PAR1', 0n, 4n) // magic number
// data pages by row group and column
/** @type {[string, bigint, bigint][]} */
const indexPages = []
for (const rowGroupIndex in metadata.row_groups) {
const rowGroup = metadata.row_groups[rowGroupIndex]
html += group(`RowGroup ${rowGroupIndex}`, rowGroup.total_byte_size)
for (const column of rowGroup.columns) {
const columnName = column.meta_data?.path_in_schema.join('.')
html += group(`Column ${columnName}`, column.meta_data?.total_compressed_size)
if (column.meta_data) {
const end = getColumnRange(column.meta_data)[1]
/* eslint-disable no-extra-parens */
const pages = (/** @type {[string, bigint][]} */
([
['Dictionary', column.meta_data.dictionary_page_offset],
['Data', column.meta_data.data_page_offset],
['Index', column.meta_data.index_page_offset],
['End', end],
]))
.filter(([, offset]) => offset !== undefined)
.sort((a, b) => Number(a[1]) - Number(b[1]))
for (let i = 0; i < pages.length - 1; i++) {
const [name, start] = pages[i]
const end = pages[i + 1][1]
html += cell(name, start, end)
}
}
if (column.column_index_offset) {
indexPages.push([`ColumnIndex RowGroup${rowGroupIndex} ${columnName}`, column.column_index_offset, BigInt(column.column_index_length || 0)])
}
if (column.offset_index_offset) {
indexPages.push([`OffsetIndex RowGroup${rowGroupIndex} ${columnName}`, column.offset_index_offset, BigInt(column.offset_index_length || 0)])
}
html += '</div>'
}
html += '</div>'
}
// column and offset indexes
for (const [name, start, length] of indexPages.sort((a, b) => Number(a[1]) - Number(b[1]))) {
html += cell(name, start, start + length)
}
// metadata footer
const metadataStart = BigInt(asyncBuffer.byteLength - metadata.metadata_length - 4)
const metadataEnd = BigInt(asyncBuffer.byteLength - 4)
html += cell('Metadata', metadataStart, metadataEnd)
html += cell('PAR1', metadataEnd, BigInt(asyncBuffer.byteLength)) // magic number
const div = document.createElement('div')
div.innerHTML = html
div.classList.add('layout', 'collapsed') // start collapsed
div.children[0].addEventListener('click', () => {
div.classList.toggle('collapsed')
})
return div
}
/**
* @param {string} name
* @param {bigint} [bytes]
* @returns {string}
*/
function group(name, bytes) {
return `<div class="group">
<div class="group-header">
<label>${name}</label>
<span>${bytes === undefined ? '' : `bytes ${bytes.toLocaleString()}`}</span>
</div>`
}
/**
* @param {string} name
* @param {bigint} start
* @param {bigint} end
* @returns {string}
*/
function cell(name, start, end) {
const bytes = end - start
return `
<div class="cell">
<label>${name}</label>
<ul>
<li>start ${start.toLocaleString()}</li>
<li>bytes ${bytes.toLocaleString()}</li>
<li>end ${end.toLocaleString()}</li>
</ul>
</div>`
}

8
demo/logo.svg Normal file

@ -0,0 +1,8 @@
<svg width="96" height="96" version="1.1" viewBox="0 0 96 96" xmlns="http://www.w3.org/2000/svg">
<path d="m48 3.5 38.37 22.25v44.5l-38.37 22.25-38.37-22.25v-44.5z" fill="#43a" stroke="#43a" stroke-linejoin="round" stroke-width="7"/>
<g fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="8">
<path d="m48 48-29.14-17 2.81e-4 34 29.14 17z"/>
<path d="m77.14 31-29.14 17v34l29.14-17z"/>
<path d="m48 48-29.14-17 29.14-17 29.14 17z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 466 B

@ -12,32 +12,30 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<div id="dropzone">
<div id="overlay">
Drop .parquet file
</div>
<nav>
<nav>
<a class="brand" href='https://hyparam.github.io/hyparquet/'>
hyparquet
</a>
</nav>
<main id="content">
<div id="app"></div>
<div id="welcome">
<h1>hyparquet</h1>
<h2>parquet file reader</h2>
<sub>/haɪ pɑːrˈkeɪ/</sub>
<h2>in-browser parquet file reader</h2>
<p>
Online demo of <a href="https://github.com/hyparam/hyparquet">hyparquet</a>: a parser for apache parquet files.
Uses <a href="https://github.com/hyparam/hightable">hightable</a> for high performance windowed table viewing.
</p>
<p>
Drag and drop a parquet file onto the dropzone to see parquet data.
Drag and drop a parquet file (or url) to see your 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>
<li><a href="https://github.com/hyparam/hyparquet">hyparquet github</a></li>
<li><a href="https://www.npmjs.com/package/hyparquet">hyparquet npm</a></li>
</ul>
<div id="sidebar"></div>
</nav>
<div id="content">
<div id="welcome">
Drop .parquet file here
</div>
</div>
</div>
</main>
<input id="file-input" type="file">
<script type="module" src="demo/bundle.min.js"></script>

@ -31,6 +31,7 @@
"@rollup/plugin-node-resolve": "15.2.3",
"@rollup/plugin-replace": "5.0.7",
"@rollup/plugin-terser": "0.4.4",
"@rollup/plugin-typescript": "11.1.6",
"@types/node": "22.5.4",
"@types/react": "18.3.5",
"@types/react-dom": "18.3.0",

@ -2,6 +2,7 @@ import commonjs from '@rollup/plugin-commonjs'
import resolve from '@rollup/plugin-node-resolve'
import replace from '@rollup/plugin-replace'
import terser from '@rollup/plugin-terser'
import typescript from '@rollup/plugin-typescript'
export default {
input: 'demo/demo.js',
@ -18,5 +19,6 @@ export default {
}),
resolve({ browser: true }),
terser(),
typescript(),
],
}

@ -2,6 +2,7 @@
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"jsx": "react",
"lib": ["esnext", "dom"],
"module": "nodenext",
"noEmit": true,