mirror of
https://github.com/asadbek064/hyparquet.git
synced 2025-12-25 14:56:37 +00:00
React metadata and file layout components
This commit is contained in:
parent
0711e86d33
commit
28ec43b49b
21
demo/App.tsx
21
demo/App.tsx
@ -5,8 +5,13 @@ import { FileMetaData, parquetMetadata, parquetMetadataAsync, parquetSchema } fr
|
||||
import { parquetRead } from '../src/read.js'
|
||||
import type { AsyncBuffer } from '../src/types.js'
|
||||
import { asyncBufferFromUrl } 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'
|
||||
|
||||
type Lens = 'table' | 'metadata' | 'layout'
|
||||
|
||||
/**
|
||||
* Hyparquet demo viewer page
|
||||
@ -17,11 +22,14 @@ export default function App() {
|
||||
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>()
|
||||
|
||||
async function onFileDrop(file: File) {
|
||||
const arrayBuffer = await file.arrayBuffer()
|
||||
const metadata = parquetMetadata(arrayBuffer)
|
||||
setMetadata(metadata)
|
||||
setName(file.name)
|
||||
setByteLength(file.size)
|
||||
setDf(parquetDataFrame(arrayBuffer, metadata))
|
||||
@ -30,6 +38,7 @@ export default function App() {
|
||||
async function onUrlDrop(url: string) {
|
||||
const asyncBuffer = await asyncBufferFromUrl(url)
|
||||
const metadata = await parquetMetadataAsync(asyncBuffer)
|
||||
setMetadata(metadata)
|
||||
setName(url)
|
||||
setByteLength(asyncBuffer.byteLength)
|
||||
setDf(parquetDataFrame(asyncBuffer, metadata))
|
||||
@ -41,13 +50,20 @@ export default function App() {
|
||||
onError={(e) => setError(e)}
|
||||
onFileDrop={onFileDrop}
|
||||
onUrlDrop={onUrlDrop}>
|
||||
{df && <>
|
||||
{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>
|
||||
<HighTable data={df} />
|
||||
{lens === 'table' && <HighTable data={df} />}
|
||||
{lens === 'metadata' && <ParquetMetadata metadata={metadata} />}
|
||||
{lens === 'layout' && <ParquetLayout byteLength={byteLength!} metadata={metadata} />}
|
||||
</>}
|
||||
</Dropzone>
|
||||
</Layout>
|
||||
@ -80,7 +96,6 @@ function parquetDataFrame(file: AsyncBuffer, metadata: FileMetaData): DataFrame
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the file size in human readable format.
|
||||
*
|
||||
|
||||
73
demo/Dropdown.tsx
Normal file
73
demo/Dropdown.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
113
demo/ParquetLayout.tsx
Normal file
113
demo/ParquetLayout.tsx
Normal file
@ -0,0 +1,113 @@
|
||||
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
|
||||
}
|
||||
|
||||
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>
|
||||
}
|
||||
13
demo/ParquetMetadata.tsx
Normal file
13
demo/ParquetMetadata.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import React from 'react'
|
||||
import type { FileMetaData } from '../src/metadata.js'
|
||||
import { toJson } from '../src/utils.js'
|
||||
|
||||
interface MetadataProps {
|
||||
metadata: FileMetaData
|
||||
}
|
||||
|
||||
export default function ParquetMetadata({ metadata }: MetadataProps) {
|
||||
return <code className='viewer'>
|
||||
{JSON.stringify(toJson(metadata), null, ' ')}
|
||||
</code>
|
||||
}
|
||||
4
demo/bundle.min.js
vendored
4
demo/bundle.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -172,10 +172,72 @@ main,
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#table {
|
||||
/* 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: 1;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
max-height: 170px;
|
||||
}
|
||||
|
||||
/* welcome */
|
||||
|
||||
Loading…
Reference in New Issue
Block a user