remove demo (#37)

* remove demo

* remove more references to the demo + fix the image

* remove unused dependencies

* set new demo URL
This commit is contained in:
Sylvain Lesage 2024-11-19 18:56:09 +01:00 committed by GitHub
parent 4070c8b755
commit 2c40b61c58
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 5 additions and 1428 deletions

@ -20,9 +20,11 @@ Hyparquet allows you to read and extract data from Parquet files directly in Jav
Online parquet file reader demo available at:
https://hyparam.github.io/hyparquet/
https://hyparam.github.io/hyperparam-cli/apps/hyparquet-demo/
[![hyparquet demo](demo/assets/demo.png)](https://hyparam.github.io/hyparquet/)
[![hyparquet demo](./demo.png)](https://hyparam.github.io/hyperparam-cli/apps/hyparquet-demo/)
See the [source code](https://github.com/hyparam/hyperparam-cli/tree/master/apps/hyparquet-demo).
## Features

Before

Width:  |  Height:  |  Size: 558 KiB

After

Width:  |  Height:  |  Size: 558 KiB

@ -1,124 +0,0 @@
import HighTable, { DataFrame, rowCache } from 'hightable'
import React, { useEffect, useState } from 'react'
import { FileMetaData, parquetMetadataAsync, parquetSchema } from '../src/metadata.js'
import { byteLengthFromUrl } from '../src/utils.js'
import Dropdown from './Dropdown.js'
import Dropzone from './Dropzone.js'
import Layout from './Layout.js'
import ParquetLayout from './ParquetLayout.js'
import ParquetMetadata from './ParquetMetadata.js'
import { AsyncBufferFrom, asyncBufferFrom, parquetQueryWorker } from './workers/parquetWorkerClient.js'
type Lens = 'table' | 'metadata' | 'layout'
/**
* Hyparquet demo viewer page
* @param {Object} props
* @param {string} [props.url]
* @returns {ReactNode}
*/
export default function App({ url }: { url?: string }) {
const [progress, setProgress] = useState<number>()
const [error, setError] = useState<Error>()
const [df, setDf] = useState<DataFrame>()
const [name, setName] = useState<string>()
const [lens, setLens] = useState<Lens>('table')
const [metadata, setMetadata] = useState<FileMetaData>()
const [byteLength, setByteLength] = useState<number>()
useEffect(() => {
if (!df && url) {
onUrlDrop(url)
}
}, [ url ])
async function onFileDrop(file: File) {
// Clear query string
history.pushState({}, '', location.pathname)
setAsyncBuffer(file.name, { file, byteLength: file.size })
}
async function onUrlDrop(url: string) {
// Add key=url to query string
const params = new URLSearchParams(location.search)
params.set('key', url)
history.pushState({}, '', `${location.pathname}?${params}`)
try {
const byteLength = await byteLengthFromUrl(url)
setAsyncBuffer(url, { url, byteLength })
} catch (e) {
setError(e as Error)
}
}
async function setAsyncBuffer(name: string, from: AsyncBufferFrom) {
// TODO: Replace welcome with spinner
const asyncBuffer = await asyncBufferFrom(from)
const metadata = await parquetMetadataAsync(asyncBuffer)
setMetadata(metadata)
setName(name)
setByteLength(from.byteLength)
let df = parquetDataFrame(from, metadata)
df = rowCache(df)
setDf(df)
document.getElementById('welcome')?.remove()
}
return <Layout progress={progress} error={error}>
<Dropzone
onError={(e) => setError(e)}
onFileDrop={onFileDrop}
onUrlDrop={onUrlDrop}>
{metadata && 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>
<Dropdown label={lens}>
<button onClick={() => setLens('table')}>Table</button>
<button onClick={() => setLens('metadata')}>Metadata</button>
<button onClick={() => setLens('layout')}>Layout</button>
</Dropdown>
</div>
{lens === 'table' && <HighTable cacheKey={name} data={df} onError={setError} />}
{lens === 'metadata' && <ParquetMetadata metadata={metadata} />}
{lens === 'layout' && <ParquetLayout byteLength={byteLength!} metadata={metadata} />}
</>}
</Dropzone>
</Layout>
}
/**
* Convert a parquet file into a dataframe.
*/
function parquetDataFrame(from: AsyncBufferFrom, 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
* @param {string} orderBy
* @returns {Promise<any[][]>}
*/
rows(rowStart, rowEnd, orderBy) {
console.log(`reading rows ${rowStart}-${rowEnd}`, orderBy)
return parquetQueryWorker({ from, metadata, rowStart, rowEnd, orderBy })
},
sortable: true,
}
}
/**
* 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]
}

@ -1,73 +0,0 @@
import React from 'react'
import { ReactNode, useEffect, useRef, useState } from 'react'
import { cn } from './Layout.js'
interface DropdownProps {
label?: string
className?: string
children: ReactNode
}
/**
* Dropdown menu component.
*
* @param {Object} props
* @param {string} props.label - button label
* @param {string} props.className - custom class name for the dropdown container
* @param {ReactNode} props.children - dropdown menu items
* @returns {ReactNode}
* @example
* <Dropdown label='Menu'>
* <button>Item 1</button>
* <button>Item 2</button>
* </Dropdown>
*/
export default function Dropdown({ label, className, children }: DropdownProps) {
const [isOpen, setIsOpen] = useState(false)
const dropdownRef = useRef<HTMLDivElement>(null)
const menuRef = useRef<HTMLDivElement>(null)
function toggleDropdown() {
setIsOpen(!isOpen)
}
useEffect(() => {
function handleClickInside(event: MouseEvent) {
const target = event.target as Element
if (menuRef.current && menuRef.current.contains(target) && target?.tagName !== 'INPUT') {
setIsOpen(false)
}
}
function handleClickOutside(event: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false)
}
}
function handleEscape(event: KeyboardEvent) {
if (event.key === 'Escape') {
setIsOpen(false)
}
}
document.addEventListener('click', handleClickInside)
document.addEventListener('keydown', handleEscape)
document.addEventListener('mousedown', handleClickOutside)
return () => {
document.removeEventListener('click', handleClickInside)
document.removeEventListener('keydown', handleEscape)
document.removeEventListener('mousedown', handleClickOutside)
}
}, [])
return (
<div
className={cn('dropdown', className, isOpen && 'open')}
ref={dropdownRef}>
<button className='dropdown-button' onClick={toggleDropdown}>
{label}
</button>
<div className='dropdown-content' ref={menuRef}>
{children}
</div>
</div>
)
}

@ -1,130 +0,0 @@
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', activate file input dialog
if ((e.target as Element).classList.contains('dropzone')) {
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 || files.length !== 1) return
onFileDrop(files[0])
}
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(e: DragEvent) {
const items = e.dataTransfer?.items
if (!items) return
if (!Array.from(items).some(item => item.kind === 'file')) return
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)
}
})
}
}
}
window.addEventListener('dragenter', onDragEnter)
window.addEventListener('dragover', onDragOver)
window.addEventListener('dragleave', onDragLeave)
dropzone.addEventListener('drop', handleFileDrop)
// Cleanup event listeners when component is unmounted
return () => {
window.removeEventListener('dragenter', onDragEnter)
window.removeEventListener('dragover', onDragOver)
window.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
onChange={handleFileSelect}
ref={fileInputRef}
style={{ display: 'none' }}
type="file" />
</div>
)
}

@ -1,57 +0,0 @@
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>
}

@ -1,120 +0,0 @@
import React, { ReactNode } from 'react'
import { getColumnRange } from '../src/column.js'
import type { FileMetaData } from '../src/metadata.js'
import { ColumnChunk } from '../src/types.js'
interface LayoutProps {
byteLength: number
metadata: FileMetaData
}
/**
* Renders the file layout of a parquet file as nested rowgroups and columns.
* @param {Object} props
* @param {number} props.byteLength
* @param {FileMetaData} props.metadata
* @returns {ReactNode}
*/
export default function ParquetLayout({ byteLength, metadata }: LayoutProps) {
const metadataStart = byteLength - metadata.metadata_length - 4
const metadataEnd = byteLength - 4
return <div className='viewer'>
<div className='layout'>
<Cell name='PAR1' start={0n} end={4n} />
<RowGroups metadata={metadata} />
<ColumnIndexes metadata={metadata} />
<Cell name='Metadata' start={metadataStart} end={metadataEnd} />
<Cell name='PAR1' start={metadataEnd} end={byteLength} />
</div>
</div>
}
function Cell<N extends bigint | number>({ name, start, end }: { name: string, start: N, end: N }) {
const bytes = end - start
return <div className="cell">
<label>{name}</label>
<ul>
<li>start {start.toLocaleString()}</li>
<li>bytes {bytes.toLocaleString()}</li>
<li>end {end.toLocaleString()}</li>
</ul>
</div>
}
function Group({ children, name, bytes }: { children: ReactNode, name?: string, bytes?: bigint }) {
return <div className="group">
<div className="group-header">
<label>{name}</label>
<span>{bytes === undefined ? '' : `bytes ${bytes.toLocaleString()}`}</span>
</div>
{children}
</div>
}
function RowGroups({ metadata }: { metadata: FileMetaData }) {
return <>
{metadata.row_groups.map((rowGroup, i) => (
<Group key={i} name={`RowGroup ${i}`} bytes={rowGroup.total_byte_size}>
{rowGroup.columns.map((column, j) => (
<Column key={j} column={column} />
))}
</Group>
))}
</>
}
function Column({ key, column }: { key: number, column: ColumnChunk }) {
if (!column.meta_data) return null
const end = getColumnRange(column.meta_data)[1]
const pages = [
{ name: 'Dictionary', offset: column.meta_data.dictionary_page_offset },
{ name: 'Data', offset: column.meta_data.data_page_offset },
{ name: 'Index', offset: column.meta_data.index_page_offset },
{ name: 'End', offset: end },
]
.filter(({ offset }) => offset !== undefined)
.sort((a, b) => Number(a.offset) - Number(b.offset))
const children = pages.slice(0, -1).map(({ name, offset }, index) => (
<Cell key={name} name={name} start={offset!} end={pages[index + 1].offset!} />
))
return <Group
key={key}
name={`Column ${column.meta_data?.path_in_schema.join('.')}`}
bytes={column.meta_data?.total_compressed_size}>
{children}
</Group>
}
function ColumnIndexes({ metadata }: { metadata: FileMetaData }) {
const indexPages = []
for (const rowGroup of metadata.row_groups) {
for (const column of rowGroup.columns) {
const columnName = column.meta_data?.path_in_schema.join('.')
if (column.column_index_offset) {
indexPages.push({
name: `ColumnIndex ${columnName}`,
start: column.column_index_offset,
end: column.column_index_offset + BigInt(column.column_index_length || 0),
})
}
if (column.offset_index_offset) {
indexPages.push({
name: `OffsetIndex ${columnName}`,
start: column.offset_index_offset,
end: column.offset_index_offset + BigInt(column.offset_index_length || 0),
})
}
}
}
return <Group name='ColumnIndexes'>
{indexPages.map(({ name, start, end }, index) => (
<Cell key={index} name={name} start={start} end={end} />
))}
</Group>
}

@ -1,19 +0,0 @@
import React from 'react'
import type { FileMetaData } from '../src/metadata.js'
import { toJson } from '../src/utils.js'
interface MetadataProps {
metadata: FileMetaData
}
/**
* Renders the metadata of a parquet file as JSON.
* @param {Object} props
* @param {FileMetaData} props.metadata
* @returns {ReactNode}
*/
export default function ParquetMetadata({ metadata }: MetadataProps) {
return <code className='viewer'>
{JSON.stringify(toJson(metadata), null, ' ')}
</code>
}

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" width="20" height="20">
<path fill-rule="evenodd" d="M9.383 3.076A1 1 0 0110 4v12a1 1 0 01-1.707.707L4.586 13H2a1 1 0 01-1-1V8a1 1 0 011-1h2.586l3.707-3.707a1 1 0 011.09-.217zM14.657 2.929a1 1 0 011.414 0A9.972 9.972 0 0119 10a9.972 9.972 0 01-2.929 7.071 1 1 0 01-1.414-1.414A7.971 7.971 0 0017 10c0-2.21-.894-4.208-2.343-5.657a1 1 0 010-1.414zm-2.829 2.828a1 1 0 011.415 0A5.983 5.983 0 0115 10a5.984 5.984 0 01-1.757 4.243 1 1 0 01-1.415-1.415A3.984 3.984 0 0013 10a3.983 3.983 0 00-1.172-2.828 1 1 0 010-1.415z" fill="#555"/>
</svg>

Before

Width:  |  Height:  |  Size: 598 B

@ -1,3 +0,0 @@
<svg width="128" height="128" version="1.1" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
<path d="m59.2 108c16.4-2.91 30-5.31 30.2-5.34l0.284-0.0602-15.5-18.5c-8.54-10.2-15.5-18.5-15.5-18.6 0-0.0911 16-44.2 16.1-44.4 0.0302-0.0524 10.9 18.8 26.4 45.7 14.5 25.2 26.5 45.9 26.6 46.1l0.202 0.353-98.7-0.012 29.9-5.28zm-59.2-5.63c0-0.026 7.32-12.7 16.3-28.2l16.3-28.2 18.9-15.9c10.4-8.74 19-15.9 19-15.9 0.0343-0.0127-0.103 0.332-0.305 0.767-0.202 0.435-9.46 20.3-20.6 44.1l-20.2 43.3-14.7 0.0184c-8.08 0.0102-14.7-0.00282-14.7-0.0288z" fill="#222"/>
</svg>

Before

Width:  |  Height:  |  Size: 568 B

@ -1,3 +0,0 @@
<svg width="92" height="92" version="1.1" viewBox="0 0 92 92" xmlns="http://www.w3.org/2000/svg">
<path d="m89.7 41.9-39.7-39.7a5.86 5.86 0 0 0-8.29 0l-8.25 8.25 10.5 10.5a6.97 6.97 0 0 1 7.16 1.66 6.98 6.98 0 0 1 1.65 7.21l10.1 10.1a6.97 6.97 0 0 1 7.21 1.65 6.98 6.98 0 0 1 0 9.87 6.98 6.98 0 0 1-9.87 0 6.98 6.98 0 0 1-1.52-7.59l-9.41-9.41v24.8a6.98 6.98 0 0 1 1.84 11.2 6.98 6.98 0 0 1-9.87 0 6.98 6.98 0 0 1 0-9.87 6.97 6.97 0 0 1 2.29-1.53v-25a6.94 6.94 0 0 1-2.29-1.53 6.99 6.99 0 0 1-1.51-7.63l-10.3-10.3-27.2 27.2a5.87 5.87 0 0 0 0 8.29l39.7 39.7a5.87 5.87 0 0 0 8.29 0l39.5-39.5a5.87 5.87 0 0 0 0-8.29" fill="#333"/>
</svg>

Before

Width:  |  Height:  |  Size: 635 B

@ -1,4 +0,0 @@
<svg width="128" height="128" version="1.1" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
<path d="m126 101c0.658-0.987 1.1-2.1 1.29-3.27 0.195-1.17 0.14-2.37-0.16-3.52-0.436-1.66-1.34-3.03-2.56-4.02 0.589-0.968 0.975-2.04 1.14-3.17 0.353-2.44-0.457-4.87-2.28-6.86-1.42-1.54-3.42-2.39-5.64-2.39-0.258 0-0.521 0.0138-0.788 0.04 1.68-5.38 2.53-11 2.53-16.6 0-30.6-24.8-55.5-55.5-55.5-30.6 0-55.5 24.8-55.5 55.5-0.00551 5.62 0.843 11.2 2.52 16.6h-0.0592c-2.22 0-4.22 0.85-5.64 2.39-1.82 1.98-2.63 4.42-2.28 6.86 0.161 1.12 0.547 2.2 1.14 3.17-1.22 0.989-2.12 2.37-2.56 4.02-0.301 1.15-0.355 2.35-0.159 3.52s0.637 2.29 1.29 3.27c-0.117 0.183-0.225 0.371-0.326 0.565-1.1 2.08-1.17 4.43-0.199 6.62 1.47 3.32 5.11 5.94 12.2 8.74 4.4 1.75 8.44 2.86 8.47 2.87 5.82 1.51 11.1 2.28 15.7 2.28 7.49 0 13.1-2.05 16.6-6.1 6.04 0.958 12.2 0.922 18.2-0.104 3.56 4.12 9.18 6.2 16.7 6.2 9.05-0.333 16.8-2.27 24.1-5.15 7.08-2.81 10.7-5.42 12.2-8.74 0.968-2.19 0.897-4.54-0.2-6.62-0.0998-0.195-0.21-0.384-0.326-0.565zm-77.8 13.4c-2.69 1.47-6.1 1.98-9.56 1.98-5.47 0-11.1-1.28-14.2-2.09-0.155-0.04-19.3-5.44-16.8-10 0.407-0.773 1.08-1.08 1.92-1.08 3.41 0 9.6 5.07 12.3 5.07 0.595 0 1.02-0.253 1.19-0.872 1.14-4.07-17.3-5.79-15.7-11.7 0.273-1.04 1.01-1.47 2.06-1.47 4.5-6.57e-4 14.6 7.92 16.7 7.92 0.162 0 0.278-0.0479 0.341-0.148 0.0092-0.0151 0.0184-0.0295 0.027-0.0453 0.992-1.64 0.423-2.83-6.37-6.99l-0.653-0.397c-7.48-4.53-12.7-7.25-9.74-10.5 0.343-0.375 0.83-0.541 1.42-0.541 0.701 0 1.55 0.234 2.48 0.628 3.95 1.67 9.42 6.21 11.7 8.19 0.36 0.312 0.717 0.628 1.07 0.946 0 0 2.9 3.01 4.65 3.01 0.403 0 0.745-0.159 0.977-0.551 1.24-2.09-11.5-11.8-12.3-15.8-0.488-2.71 0.343-4.08 1.88-4.08 0.731 0 1.62 0.311 2.61 0.936 3.05 1.94 8.95 12.1 11.1 16 0.723 1.32 1.96 1.88 3.07 1.88 2.21 0 3.93-2.19 0.202-4.98-5.61-4.2-3.64-11.1-0.964-11.5 0.114-0.0184 0.23-0.0276 0.345-0.0276 2.43 0 3.51 4.19 3.51 4.19s3.15 7.9 8.55 13.3c4.91 4.9 5.5 8.88 2.73 13.9-1.1 1.99-2.8 3.84-4.54 4.82zm22-3.79c-3.86 0.491-7.8 0.461-11.6 0.0735h-0.0098c3-6.7 1.48-12.9-4.58-19-3.98-3.97-6.62-9.83-7.17-11.1-1.11-3.81-4.05-8.05-8.94-8.05-0.413 0-0.825 0.0328-1.23 0.0972-2.14 0.337-4.01 1.57-5.34 3.42-1.44-1.79-2.84-3.22-4.11-4.02-1.91-1.21-3.82-1.83-5.67-1.83-2.32 0-4.39 0.952-5.83 2.68l-0.0368 0.044c-0.0276-0.114-0.0538-0.227-0.0808-0.341l-0.0033-0.0151c-0.869-3.68-1.28-7.56-1.29-11.3 0-27.5 22.3-49.8 49.8-49.8s49.8 22.3 49.8 49.8c0.01 1.65-0.0843 3.34-0.234 4.92v0.0085c-0.233 2.23-0.541 4.08-0.991 6.12-1.4-1.36-3.26-2.1-5.3-2.1-1.86 0-3.76 0.614-5.67 1.83-1.27 0.805-2.67 2.23-4.11 4.02-1.34-1.85-3.21-3.08-5.34-3.42-0.408-0.0643-0.82-0.0972-1.23-0.0972-4.88 0-7.82 4.24-8.94 8.05-0.551 1.29-3.2 7.15-7.18 11.1-6.06 6.04-7.59 12.3-4.63 18.9zm50.4-12c-0.884 0.876-2.23 1.64-3.75 2.35-1.4 0.645-3.02 1.26-4.38 1.79-0.899 0.346-1.76 0.691-2.63 1.06-2.52 1.08-4.31 2.18-3.95 3.55 0.0571 0.217 0.162 0.397 0.292 0.548 0.423 0.488 1.26 0.392 2.29 0.0223 0.497-0.176 1-0.416 1.45-0.628 1.26-0.614 2.68-1.44 4.09-2.19 0.565-0.305 1.14-0.595 1.72-0.87 1.34-0.63 2.6-1.08 3.61-1.08 1.02 0.0419 1.36 0.379 1.92 1.08 0.5 0.949 0.0814 1.93-0.897 2.89-0.939 0.925-2.4 1.83-4.06 2.67-3.77 1.85-7.82 3.28-11.9 4.46-1.73 0.447-4.19 1.03-6.97 1.47-3.51 0.553-6.95 0.799-10.4 0.464-4.35-0.347-8.2-2.22-10.5-5.91-1.13-1.8-1.96-4.17-2.06-6.03-0.0368-2.81 1.38-5.5 4.42-8.53 5.41-5.4 8.55-13.3 8.55-13.3 0.438-1.63 1.25-3.21 2.81-4.07 0.351-0.131 0.694-0.109 1.05-0.0926 0.487 0.0775 0.951 0.368 1.35 0.815 1.51 1.87 1.57 4.58 0.766 6.72-0.769 2.24-2.62 3.42-4.19 4.91-0.987 1.04-1.22 1.95-0.995 2.64 0.213 0.564 0.573 0.876 0.99 1.08 1.27 0.614 2.13 0.152 3.19-0.276 0.523-0.331 0.864-0.815 1.19-1.31 1.28-2.34 2.63-4.64 4.04-6.9 1.91-2.84 3.81-5.84 5.8-8.02 0.502-0.552 1.03-0.954 1.63-1.31 1.54-0.904 2.84-0.946 3.58-0.206 0.448 0.448 0.696 1.19 0.682 2.22 0.0245 2.39-1.68 4.26-3.17 5.94-2.69 3.31-6.7 6.09-8.95 9.84-0.214 0.357-0.307 0.766-0.325 1.13-0.0124 0.252 0.217 0.402 0.377 0.548 0.177 0.162 0.431 0.211 0.672 0.204 0.913-0.0532 1.64-0.545 2.37-1.04 0.197-0.135 0.335-0.249 0.525-0.393 4.28-3.79 8.5-7.6 13.5-10.2 1.44-0.802 2.74-1.36 4.2-1.04 0.972 0.215 1.28 1.04 1.56 1.86 0.15 1.17-0.382 1.86-1.05 2.69-1.55 1.56-3.44 2.74-5.24 3.84-3.12 1.94-6.24 3.64-9.27 5.85-1.05 0.815-2.24 1.67-2.39 3.07 0.024 0.378 0.225 0.724 0.36 1.01 0.748 0.446 1.87-0.267 2.44-0.566 0.0584-0.0315 0.123-0.0643 0.185-0.0985 1.98-1.08 4.02-2.37 5.7-3.35 1.78-1.09 3.59-2.04 5.45-2.85l0.22-0.0893 0.0263-0.0105c1.17-0.462 2.22-0.746 3.08-0.746 0.951-0.0126 1.6 0.433 2 1.28 0.021 0.0624 0.0394 0.123 0.0584 0.188 0.241 1.26-0.185 2.07-0.99 2.94z" fill="#222" stroke-width="1.52"/>
<path d="m45.1 37.7c-0.547-0.0094-1.89 0.0702-3.19 0.69-1.31 0.62-2.87 2.1-3.55 3.74s-0.717 3.48-0.113 5.16c0.604 1.67 2.07 3.2 3.38 3.89 1.31 0.698 1.85-0.576 2.49-1.51 0.645-0.935 1.15-1.64 1.9-1.91 0.746-0.264 1.53 0.0731 2.56 0.483 1.03 0.41 2.15 1.06 2.73-0.0282s0.943-2.67 0.802-4.08c-0.142-1.41-0.699-2.74-1.6-3.84-0.901-1.09-2.46-2-3.46-2.3-0.997-0.301-1.62-0.301-1.95-0.306-0.328-0.0056 0.547 0.0094 0 2.6e-5zm36 0.0064c-0.788-0.0538-1.58 0.0698-2.34 0.299-1.01 0.305-1.95 0.831-2.73 1.54s-1.4 1.58-1.81 2.56c-0.407 0.977-0.593 2.03-0.544 3.09 0.0496 1.06 0.333 2.09 0.83 3.02 0.491 0.924 1.58 0.488 2.73 0.0282 0.901-0.361 1.84-0.737 2.56-0.483 0.776 0.274 1.35 1.11 1.9 1.91 0.742 1.07 1.43 2.07 2.49 1.51 1.25-0.665 2.27-1.69 2.94-2.93 0.67-1.25 0.956-2.67 0.821-4.07-0.101-1.05-0.435-2.07-0.977-2.98-0.542-0.909-1.24-1.74-2.16-2.28-1.2-0.7-2.56-1.13-3.71-1.21zm-0.798 23.2c-1.84 0-3.13 1.1-4.49 1.77-3.31 1.64-7.71 3.82-12.6 3.82-4.84 0-9.24-2.18-12.6-3.82-3.72-1.84-6.06-3-6.06 0.41 0 4.37 2.09 11.5 7.83 15.7 1.64e-4 -3.41e-4 -5.51e-4 -2e-3 0-0.0013 2.72 1.98 6.26 3.28 10.8 3.28 4.82 0 8.52-1.47 11.3-3.67 2.76e-4 5.19e-4 0.0013 6.57e-4 0.0013 0.0013 5.35-4.21 7.31-11.1 7.31-15.3 0-1.67-0.563-2.25-1.58-2.18h-3.3e-5z" fill="#222" stroke-width="1.52"/>
</svg>

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

@ -1,3 +0,0 @@
<svg width="128" height="128" version="1.1" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
<path class="st0" d="m64 2.561c28.87 0 52.27 10.96 52.27 24.46 0 13.51-23.41 24.46-52.27 24.46s-52.27-10.96-52.27-24.46 23.41-24.46 52.27-24.46zm-52.27 81.83v18.78c9.3 33.03 101.2 26.65 104.6-1.69v-18.76c-4.59 31.11-97.2 33.35-104.6 1.67zm-0.26-48.89v18.34c9.3 32.26 101.7 27.9 105.1 0.23v-18.33c-4.6 30.39-97.72 30.7-105.1-0.24zm0 23.7v18.78c9.3 33.03 101.7 28.57 105.1 0.23v-18.76c-4.6 31.11-97.72 31.43-105.1-0.25z" fill="#333"/>
</svg>

Before

Width:  |  Height:  |  Size: 543 B

11
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

@ -1,601 +0,0 @@
* {
box-sizing: border-box;
font-family: 'Mulish', 'Helvetica Neue', Helvetica, Arial, sans-serif;
margin: 0;
padding: 0;
}
body {
display: flex;
font-family: sans-serif;
height: 100vh;
width: 100vw;
}
h1 {
font-size: 22pt;
}
h2 {
margin-top: 10px;
font-size: 12pt;
}
p {
margin: 15px 0;
}
code {
font-family: monospace;
padding: 10px;
white-space: pre-wrap;
word-break: break-all;
}
sub {
align-items: center;
display: flex;
gap: 5px;
}
sub img {
cursor: pointer;
}
.error {
color: #c11;
font-family: monospace;
white-space: pre-wrap;
}
/* dropzone */
.dropzone {
display: flex;
flex-direction: column;
height: 100%;
}
.dropzone.hover .overlay {
display: flex;
}
.overlay {
font-size: 125%;
position: fixed;
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;
}
.target {
border: 6px dashed #444;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
height: 100%;
width: 100%;
}
/* 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;
}
#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;
}
/* error bar */
.error-bar {
max-height: 0;
padding: 0;
background-color: #dd111199;
font-family: monospace;
overflow-y: auto;
transition: max-height 0.3s;
white-space: pre-wrap;
}
.show-error {
max-height: 30%;
padding: 10px;
}
.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;
}
/* dropdown */
.dropdown {
display: inline-block;
position: relative;
text-overflow: ellipsis;
user-select: none;
white-space: nowrap;
}
.dropdown-button,
.dropdown-button:active,
.dropdown-button:focus,
.dropdown-button:hover {
background: transparent;
border: none;
color: inherit;
cursor: pointer;
font-size: inherit;
height: 24px;
max-width: 300px;
overflow: hidden;
}
/* dropdown caret */
.dropdown-button::before {
content: "\25bc";
display: inline-block;
font-size: 10px;
margin-right: 4px;
transform: rotate(-90deg);
transition: transform 0.1s;
}
.open .dropdown-button::before {
transform: rotate(0deg);
}
/* dropdown menu options */
.dropdown-content {
position: absolute;
left: 0;
background-color: #ccc;
border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px;
box-shadow: 0px 8px 8px 0px rgba(0, 0, 0, 0.2);
max-height: 0;
max-width: 200px;
min-width: 120px;
transition: max-height 0.1s ease-out;
overflow-y: hidden;
z-index: 20;
}
.dropdown-content > button {
background: none;
border: none;
padding: 8px 16px;
text-align: left;
}
/* dropdown menu options hover */
.dropdown-content > button:active,
.dropdown-content > button:focus,
.dropdown-content > button:hover {
background-color: rgba(95, 75, 133, 0.4);
}
/* roll out dropdown menu */
.open .dropdown-content {
display: flex;
flex-direction: column;
max-height: 170px;
}
/* welcome */
#welcome {
position: absolute;
bottom: 0;
top: 0;
right: 0;
left: 0;
border: 2px #777;
border-radius: 10px;
color: #444;
margin: 10px;
padding: 10px;
display: flex;
flex-direction: column;
flex: 1;
font-size: 20px;
justify-content: center;
max-width: 640px;
margin: 0 auto;
}
/* quick link buttons */
.quick-links {
display: flex;
flex-wrap: wrap;
gap: 10px;
list-style: none;
}
.quick-links li {
display: flex;
flex: 1 1 calc(50% - 10px);
min-width: 0;
}
.quick-links a {
background-position: 10px center;
background-size: 18px;
border: 1px solid #444;
border-radius: 4px;
font-size: 8pt;
overflow: hidden;
padding: 12px;
padding-left: 36px;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
.quick-links a:hover {
background-color: #cec;
}
.huggingface {
background: url('assets/huggingface.svg') no-repeat 8px center;
}
.github {
background: url('assets/git.svg') no-repeat 8px center;
}
.aws {
background: url('assets/s3.svg') no-repeat 8px center;
}
.azure {
background: url('assets/azure.svg') no-repeat 8px center;
}
/* file upload */
input[type="file"] {
display: none;
}
.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;
}
.over .overlay {
display: flex;
}
/* table */
.table-container {
display: flex;
flex-direction: column;
min-height: 0;
flex: 1;
position: relative;
}
.table-scroll {
flex: 1;
overflow: auto;
}
.table-scroll > div {
position: relative;
}
.table-scroll .table {
position: absolute;
}
table {
border-collapse: separate;
border-spacing: 0;
}
table:focus-visible {
outline: none;
}
/* header */
.table thead th {
background-color: #eaeaeb;
border: none;
border-bottom: 2px solid #c9c9c9;
box-sizing: content-box;
color: #444;
height: 20px;
padding-top: 8px;
position: sticky;
top: -1px; /* fix 1px gap above thead */
user-select: none;
z-index: 10;
}
.table thead th:first-child {
border: none;
}
.table thead th:first-child span {
cursor: default;
width: 0;
}
.table tbody tr:first-child td {
border-top: 1px solid transparent;
}
/* sortable */
.table.sortable thead th {
cursor: pointer;
}
.table thead th.orderby ::after {
position: absolute;
right: 8px;
top: 8px;
padding-left: 2px;
background-color: #eaeaeb;
content: "▾";
}
/* cells */
.table th,
.table 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: nowrap;
}
/* pending cell state */
.table td.pending {
position: relative;
}
.table td.pending::after {
content: '';
position: absolute;
top: 8px;
left: 8px;
right: 8px;
bottom: 8px;
border-radius: 4px;
background: linear-gradient(
60deg,
rgba(0, 0, 0, 0.05) 25%,
rgba(0, 0, 0, 0.08) 50%,
rgba(0, 0, 0, 0.05) 75%
);
background-size: 120px 100%;
animation: textshimmer 3s infinite linear;
}
/* stagger row shimmering */
.table tr:nth-child(2n) td.pending::after { animation-delay: -1s; }
.table tr:nth-child(2n+1) td.pending::after { animation-delay: -3s; }
.table tr:nth-child(3n) td.pending::after { animation-delay: -2s; }
.table tr:nth-child(5n) td.pending::after { animation-delay: -4s; }
.table tr:nth-child(7n) td.pending::after { animation-delay: -1.5s; }
@keyframes textshimmer {
0% {
background-position: -120px 0;
}
100% {
background-position: 120px 0;
}
}
/* pending table state */
.table th::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 4px;
background-color: #706fb1;
z-index: 100;
}
.pending .table th::before {
animation: shimmer 2s infinite linear;
}
@keyframes shimmer {
0%, 100% { background-color: #6fb176; }
50% { background-color: #adc6b0; }
}
/* column resize */
.table thead span {
position: absolute;
border-right: 1px solid #ddd;
top: 0;
right: 0;
bottom: 0;
width: 8px;
cursor: col-resize;
transition: background-color 0.2s ease;
}
.table thead span:hover {
background-color: #aab;
}
/* row numbers */
td:first-child {
background-color: #eaeaeb;
border-right: 1px solid #ddd;
color: #888;
font-size: 10px;
padding: 0 2px;
position: sticky;
left: 0;
text-align: center;
user-select: none;
min-width: 32px;
max-width: none;
width: 32px;
}
/* table corner */
.table-corner {
background-color: #e4e4e6;
border-right: 1px solid #ccc;
position: absolute;
height: 34px;
width: 32px;
top: 0;
left: 0;
z-index: 15;
box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.2);
}
/* mock row numbers */
.mock-row-label {
content: "";
position: absolute;
top: 0;
left: 0;
bottom: 0;
background: #eaeaeb;
z-index: -10;
}
#filename {
font-size: 10pt;
margin-top: 20px;
}
.sidebar {
word-break: break-all;
}
.sidebar a {
color: #445;
text-decoration: none;
}
.sidebar a:hover {
text-decoration: underline;
}
/* layout */
.layout {
margin: 10px;
max-width: 480px;
}
.layout,
.layout .group,
.layout .cell {
background-color: rgba(100, 80, 180, 0.05);
border: 1px solid #ccc;
border-radius: 4px;
font-size: 12px;
margin-top: 4px;
padding: 4px;
word-break: break-all;
}
.cell,
.group-header {
display: flex;
}
.group-header > label,
.cell > label {
display: flex;
flex: 1;
font-size: 12px;
font-weight: normal;
justify-content: flex-start;
}
.group-header > span {
font-size: 10px;
}
.layout div ul {
list-style: none;
}
.layout div li {
font-size: 10px;
padding: 2px 4px;
text-align: right;
}

@ -1,12 +0,0 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.js'
const app = document.getElementById('app')
if (!app) throw new Error('missing app element')
const params = new URLSearchParams(location.search)
const url = params.get('key') || undefined
const root = ReactDOM.createRoot(app)
root.render(React.createElement(App, { url }))

File diff suppressed because one or more lines are too long

@ -1,8 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 466 B

@ -1,21 +0,0 @@
import { compressors } from 'hyparquet-compressors'
import { parquetQuery } from '../../src/query.js'
import { asyncBufferFrom } from './parquetWorkerClient.js'
self.onmessage = async ({ data }) => {
const { metadata, from, rowStart, rowEnd, orderBy, columns, queryId, chunks } = data
const file = await asyncBufferFrom(from)
/**
* @typedef {import('../../src/hyparquet.js').ColumnData} ColumnData
* @type {((chunk: ColumnData) => void) | undefined}
*/
const onChunk = chunks ? chunk => self.postMessage({ chunk, queryId }) : undefined
try {
const result = await parquetQuery({
metadata, file, rowStart, rowEnd, orderBy, columns, compressors, onChunk,
})
self.postMessage({ result, queryId })
} catch (error) {
self.postMessage({ error, queryId })
}
}

@ -1,88 +0,0 @@
import { cachedAsyncBuffer } from '../../src/asyncBuffer.js'
import type { AsyncBuffer, ParquetReadOptions } from '../../src/hyparquet.js'
import { asyncBufferFromUrl } from '../../src/utils.js'
// Serializable constructors for AsyncBuffers
interface AsyncBufferFromFile {
file: File
byteLength: number
}
interface AsyncBufferFromUrl {
url: string
byteLength: number
}
export type AsyncBufferFrom = AsyncBufferFromFile | AsyncBufferFromUrl
// Same as ParquetReadOptions, but AsyncBufferFrom instead of AsyncBuffer
interface ParquetReadWorkerOptions extends Omit<ParquetReadOptions, 'file'> {
from: AsyncBufferFrom
orderBy?: string
}
let worker: Worker | undefined
let nextQueryId = 0
interface QueryAgent {
resolve: (value: any) => void
reject: (error: any) => void
onChunk?: (chunk: any) => void
}
const pending = new Map<number, QueryAgent>()
function getWorker() {
if (!worker) {
worker = new Worker(new URL('demo/workers/worker.min.js', import.meta.url))
worker.onmessage = ({ data }) => {
const { resolve, reject, onChunk } = pending.get(data.queryId)!
if (data.error) {
reject(data.error)
} else if (data.result) {
resolve(data.result)
} else if (data.chunk) {
onChunk?.(data.chunk)
} else {
reject(new Error('Unexpected message from worker'))
}
}
}
return worker
}
/**
* Presents almost the same interface as parquetRead, but runs in a worker.
* This is useful for reading large parquet files without blocking the main thread.
* Instead of taking an AsyncBuffer, it takes a AsyncBufferFrom, because it needs
* to be serialized to the worker.
*/
export function parquetQueryWorker(
{ metadata, from, rowStart, rowEnd, orderBy, onChunk }: ParquetReadWorkerOptions
): Promise<Record<string, any>[]> {
return new Promise((resolve, reject) => {
const queryId = nextQueryId++
pending.set(queryId, { resolve, reject, onChunk })
const worker = getWorker()
// If caller provided an onChunk callback, worker will send chunks as they are parsed
const chunks = onChunk !== undefined
worker.postMessage({
queryId, metadata, from, rowStart, rowEnd, orderBy, chunks,
})
})
}
/**
* Convert AsyncBufferFrom to AsyncBuffer.
*/
export async function asyncBufferFrom(from: AsyncBufferFrom): Promise<AsyncBuffer> {
if ('url' in from) {
// Cached asyncBuffer for urls only
const key = JSON.stringify(from)
const cached = cache.get(key)
if (cached) return cached
const asyncBuffer = asyncBufferFromUrl(from).then(cachedAsyncBuffer)
cache.set(key, asyncBuffer)
return asyncBuffer
} else {
return from.file.arrayBuffer()
}
}
const cache = new Map<string, Promise<AsyncBuffer>>()

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -10,8 +10,6 @@ export default [
typescript,
},
ignores: ['demo/**/*.min.js'],
languageOptions: {
globals: {
...globals.browser,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

@ -1,80 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>hyparquet parquet file parser demo</title>
<link rel="icon" href="favicon.png" />
<link rel="stylesheet" href="demo/demo.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Mulish:wght@400;600&display=swap"/>
<meta name="description" content="Online demo of hyparquet: a parser for apache parquet files. Drag and drop parquet files to view parquet data.">
<meta name="author" content="Hyperparam">
<meta name="keywords" content="hyparquet, parquet, parquet file, parquet parser, parquet reader, parquet viewer, parquet data, apache parquet, hightable">
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<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>
<sub>
/haɪ pɑːrˈkeɪ/
<img src="demo/assets/audio.svg" alt="play hyparquet pronunciation" height="18" width="18" onclick="audio.play()">
</sub>
<audio id="audio" src="demo/assets/hyparquet.mp3"></audio>
<h2>in-browser parquet file reader</h2>
<p>
<a href="https://www.npmjs.com/package/hyparquet"><img src="https://img.shields.io/npm/v/hyparquet" alt="npm hyparquet"></a>
<a href="https://github.com/hyparam/hyparquet"><img src="https://img.shields.io/github/stars/hyparam/hyparquet?style=social" alt="star hyparquet"></a>
</p>
<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 (or url) to see your parquet data. 👀
</p>
<p>
Example files:
<ul class="quick-links">
<li>
<a
class="aws"
href="?key=https://hyperparam-public.s3.amazonaws.com/wiki-en-00000-of-00041.parquet">
s3://wiki-en-00000-of-00041.parquet
</a>
</li>
<li>
<a
class="azure"
href="?key=https://hyperparam.blob.core.windows.net/hyperparam/starcoderdata-js-00000-of-00065.parquet">
azure://starcoderdata-js-00000-of-00065.parquet
</a>
</li>
<li>
<a
class="huggingface"
href="?key=https://huggingface.co/datasets/codeparrot/github-code/resolve/main/data/train-00000-of-01126.parquet?download=true">
huggingface://github-code-00000-of-01126.parquet
</a>
</li>
<li>
<a
class="github"
href="?key=https://raw.githubusercontent.com/hyparam/hyparquet/master/test/files/rowgroups.parquet">
github://rowgroups.parquet
</a>
</li>
</ul>
</p>
</div>
</main>
<input id="file-input" type="file">
<script type="module" src="demo/bundle.min.js"></script>
</body>
</html>

@ -21,29 +21,15 @@
"types": "src/hyparquet.d.ts",
"scripts": {
"coverage": "vitest run --coverage --coverage.include=src",
"demo": "http-server -o",
"demo:build": "rollup -c",
"lint": "eslint .",
"test": "vitest run"
},
"devDependencies": {
"@rollup/plugin-commonjs": "28.0.1",
"@rollup/plugin-node-resolve": "15.3.0",
"@rollup/plugin-replace": "6.0.1",
"@rollup/plugin-terser": "0.4.4",
"@rollup/plugin-typescript": "12.1.1",
"@types/node": "22.8.6",
"@types/react": "18.3.12",
"@types/react-dom": "18.3.1",
"@vitest/coverage-v8": "2.1.4",
"eslint": "9.14.0",
"eslint-plugin-jsdoc": "50.4.3",
"hightable": "0.6.3",
"http-server": "14.1.1",
"hyparquet-compressors": "0.1.4",
"react": "18.3.1",
"react-dom": "18.3.1",
"rollup": "4.24.3",
"typescript": "5.6.3",
"typescript-eslint": "8.12.2",
"vitest": "2.1.4"

@ -1,42 +0,0 @@
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 [
// demo bundle
{
input: 'demo/demo.js',
output: {
file: 'demo/bundle.min.js',
format: 'umd',
sourcemap: true,
},
plugins: [
commonjs(),
replace({
'process.env.NODE_ENV': JSON.stringify('production'), // or 'development' based on your build environment
preventAssignment: true,
}),
resolve({ browser: true }),
terser(),
typescript(),
],
},
// web worker
{
input: 'demo/workers/parquetWorker.js',
output: {
file: 'demo/workers/worker.min.js',
format: 'umd',
sourcemap: true,
},
plugins: [
commonjs(),
resolve({ browser: true }),
terser(),
typescript(),
],
},
]

@ -9,5 +9,5 @@
"resolveJsonModule": true,
"strict": true
},
"include": ["src", "test", "demo"]
"include": ["src", "test"]
}