remove demo (#37)
* remove demo * remove more references to the demo + fix the image * remove unused dependencies * set new demo URL
@ -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/
|
||||
|
||||
[](https://hyparam.github.io/hyparquet/)
|
||||
[](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 |
124
demo/App.tsx
@ -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 |
@ -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
601
demo/demo.css
@ -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;
|
||||
}
|
||||
12
demo/demo.js
@ -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 }))
|
||||
3
demo/hyparquet-compressors.min.js
vendored
@ -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>>()
|
||||
2
demo/workers/worker.min.js
vendored
@ -10,8 +10,6 @@ export default [
|
||||
typescript,
|
||||
},
|
||||
|
||||
ignores: ['demo/**/*.min.js'],
|
||||
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
|
||||
BIN
favicon.png
|
Before Width: | Height: | Size: 1.0 KiB |
80
index.html
@ -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>
|
||||
14
package.json
@ -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"]
|
||||
}
|
||||
|
||||