Skip to content

Commit a78392a

Browse files
authored
demo for hyparquet (#42)
* demo for hyparquet * remove todo * move html to Welcome.tsx + fix p > ul * extract to a function * remove typo * fix worker * fix dropzone * add github ci * add missing dependency * add column information in parquet layout * fix readme
1 parent 2253659 commit a78392a

34 files changed

+1627
-0
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: apps/hyparquet-demo
2+
on:
3+
push:
4+
paths:
5+
- 'apps/hyparquet-demo/**'
6+
- '.github/workflows/ci_apps_hyparquet_demo.yml'
7+
8+
defaults:
9+
run:
10+
working-directory: ./apps/hyparquet-demo
11+
12+
jobs:
13+
lint:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v4
17+
- run: npm i
18+
- run: npm run lint
19+
20+
typecheck:
21+
runs-on: ubuntu-latest
22+
steps:
23+
- uses: actions/checkout@v4
24+
- run: npm i
25+
- run: tsc
26+
27+
buildcheck:
28+
runs-on: ubuntu-latest
29+
steps:
30+
- uses: actions/checkout@v4
31+
- run: npm i
32+
- run: npm run build

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ It contains the following package:
88
It also contains the following applications:
99
- [`hyperparam`](./apps/cli): a cli tool for viewing arbitrarily large datasets in the browser.
1010
- [`hightable-demo`](./apps/hightable-demo): an example project showing how to use [hightable](https://github.com/hyparam/hightable).
11+
- [`hyparquet-demo`](./apps/hyparquet-demo): an example project showing how to use [hyparquet](https://github.com/hyparam/hyparquet).

apps/hyparquet-demo/.gitignore

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
dist
12+
dist-ssr
13+
*.local
14+
15+
# Editor directories and files
16+
.vscode/*
17+
!.vscode/extensions.json
18+
.idea
19+
.DS_Store
20+
*.suo
21+
*.ntvs*
22+
*.njsproj
23+
*.sln
24+
*.sw?

apps/hyparquet-demo/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Hyparquet demo
2+
3+
This is an example project showing how to use [hyparquet](https://github.com/hyparam/hyparquet).
4+
5+
## Build
6+
7+
```bash
8+
cd apps/hyparquet-demo
9+
npm i
10+
npm run build
11+
```
12+
13+
The build artifacts will be stored in the `dist/` directory and can be served using any static server, eg. `http-server`:
14+
15+
```bash
16+
npm i -g http-server
17+
http-server dist/
18+
```
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import js from '@eslint/js'
2+
import react from 'eslint-plugin-react'
3+
import reactHooks from 'eslint-plugin-react-hooks'
4+
import reactRefresh from 'eslint-plugin-react-refresh'
5+
import globals from 'globals'
6+
import tseslint from 'typescript-eslint'
7+
import { sharedJsRules, sharedTsRules } from '../../shared.eslint.config.js'
8+
9+
export default tseslint.config(
10+
{ ignores: ['dist'] },
11+
{
12+
extends: [js.configs.recommended, ...tseslint.configs.strictTypeChecked, ...tseslint.configs.stylisticTypeChecked],
13+
// Set the react version
14+
settings: { react: { version: '18.3' } },
15+
files: ['src/**/*.{ts,tsx}'],
16+
languageOptions: {
17+
ecmaVersion: 2020,
18+
globals: globals.browser,
19+
parserOptions: {
20+
project: './tsconfig.json',
21+
tsconfigRootDir: import.meta.dirname,
22+
},
23+
},
24+
plugins: {
25+
react,
26+
'react-hooks': reactHooks,
27+
'react-refresh': reactRefresh,
28+
},
29+
rules: {
30+
...react.configs.recommended.rules,
31+
...react.configs['jsx-runtime'].rules,
32+
...reactHooks.configs.recommended.rules,
33+
'react-refresh/only-export-components': [
34+
'warn',
35+
{ allowConstantExport: true },
36+
],
37+
...js.configs.recommended.rules,
38+
...tseslint.configs.recommended.rules,
39+
...sharedJsRules,
40+
...sharedTsRules,
41+
},
42+
},
43+
)

apps/hyparquet-demo/index.html

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<title>hyparquet parquet file parser demo</title>
6+
<link rel="icon" href="favicon.png" />
7+
<!-- <link rel="stylesheet" href="demo/demo.css"> -->
8+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Mulish:wght@400;600&display=swap"/>
9+
<meta name="description" content="Online demo of hyparquet: a parser for apache parquet files. Drag and drop parquet files to view parquet data.">
10+
<meta name="author" content="Hyperparam">
11+
<meta name="keywords" content="hyparquet, parquet, parquet file, parquet parser, parquet reader, parquet viewer, parquet data, apache parquet, hightable">
12+
<meta name="viewport" content="width=device-width, initial-scale=1" />
13+
</head>
14+
<body>
15+
<nav>
16+
<a class="brand" href='https://hyparam.github.io/hyparquet/'>
17+
hyparquet
18+
</a>
19+
</nav>
20+
<main id="content">
21+
<div id="app"></div>
22+
</main>
23+
<input id="file-input" type="file">
24+
25+
<script type="module" src="/src/main.tsx"></script>
26+
</body>
27+
</html>

apps/hyparquet-demo/package.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"name": "hyparquet-demo",
3+
"private": true,
4+
"version": "0.0.0",
5+
"type": "module",
6+
"scripts": {
7+
"dev": "vite",
8+
"build": "tsc -b && vite build",
9+
"lint": "eslint .",
10+
"preview": "vite preview"
11+
},
12+
"dependencies": {
13+
"hyparquet": "1.5.0",
14+
"hyparquet-compressors": "0.1.4",
15+
"hightable": "0.7.0",
16+
"react": "^18.3.1",
17+
"react-dom": "^18.3.1"
18+
},
19+
"devDependencies": {
20+
"@eslint/js": "^9.13.0",
21+
"@types/react": "^18.3.12",
22+
"@types/react-dom": "^18.3.1",
23+
"@vitejs/plugin-react": "^4.3.3",
24+
"eslint": "^9.13.0",
25+
"eslint-plugin-react": "^7.37.2",
26+
"eslint-plugin-react-hooks": "^5.0.0",
27+
"eslint-plugin-react-refresh": "^0.4.14",
28+
"globals": "^15.11.0",
29+
"typescript": "~5.6.2",
30+
"typescript-eslint": "^8.11.0",
31+
"vite": "^5.4.10"
32+
}
33+
}
1.04 KB
Loading

apps/hyparquet-demo/src/App.tsx

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { ReactNode } from 'react'
2+
import Page, { PageProps } from './Page.js'
3+
import Welcome from './Welcome.js'
4+
5+
import { DataFrame, rowCache } from 'hightable'
6+
import { FileMetaData, byteLengthFromUrl, parquetMetadataAsync, parquetSchema } from 'hyparquet'
7+
import { useCallback, useEffect, useState } from 'react'
8+
import Dropzone from './Dropzone.js'
9+
import Layout from './Layout.js'
10+
import { asyncBufferFrom } from './utils.js'
11+
import { parquetQueryWorker } from './workers/parquetWorkerClient.js'
12+
import { AsyncBufferFrom, Row } from './workers/types.js'
13+
14+
export default function App(): ReactNode {
15+
const params = new URLSearchParams(location.search)
16+
const url = params.get('key') ?? undefined
17+
18+
const [error, setError] = useState<Error>()
19+
const [pageProps, setPageProps] = useState<PageProps>()
20+
21+
const setUnknownError = useCallback((e: unknown) => {
22+
setError(e instanceof Error ? e : new Error(String(e)))
23+
}, [])
24+
25+
const onUrlDrop = useCallback(
26+
(url: string) => {
27+
// Add key=url to query string
28+
const params = new URLSearchParams(location.search)
29+
params.set('key', url)
30+
history.pushState({}, '', `${location.pathname}?${params}`)
31+
byteLengthFromUrl(url).then(byteLength => setAsyncBuffer(url, { url, byteLength })).catch(setUnknownError)
32+
},
33+
[setUnknownError],
34+
)
35+
36+
useEffect(() => {
37+
if (!pageProps && url) {
38+
onUrlDrop(url)
39+
}
40+
}, [ url, pageProps, onUrlDrop])
41+
42+
function onFileDrop(file: File) {
43+
// Clear query string
44+
history.pushState({}, '', location.pathname)
45+
setAsyncBuffer(file.name, { file, byteLength: file.size }).catch(setUnknownError)
46+
}
47+
48+
async function setAsyncBuffer(name: string, from: AsyncBufferFrom) {
49+
const asyncBuffer = await asyncBufferFrom(from)
50+
const metadata = await parquetMetadataAsync(asyncBuffer)
51+
const df = rowCache(parquetDataFrame(from, metadata))
52+
setPageProps({ metadata, df, name, byteLength: from.byteLength, setError })
53+
}
54+
55+
return <Layout error={error}>
56+
<Dropzone
57+
onError={(e) => { setError(e) }}
58+
onFileDrop={onFileDrop}
59+
onUrlDrop={onUrlDrop}>
60+
{pageProps ? <Page {...pageProps} /> : <Welcome />}
61+
</Dropzone>
62+
</Layout>
63+
}
64+
65+
/**
66+
* Convert a parquet file into a dataframe.
67+
*/
68+
function parquetDataFrame(from: AsyncBufferFrom, metadata: FileMetaData): DataFrame {
69+
const { children } = parquetSchema(metadata)
70+
return {
71+
header: children.map(child => child.element.name),
72+
numRows: Number(metadata.num_rows),
73+
/**
74+
* @param {number} rowStart
75+
* @param {number} rowEnd
76+
* @param {string} orderBy
77+
* @returns {Promise<any[][]>}
78+
*/
79+
rows(rowStart: number, rowEnd: number, orderBy: string): Promise<Row[]> {
80+
console.log(`reading rows ${rowStart}-${rowEnd}`, orderBy)
81+
return parquetQueryWorker({ from, metadata, rowStart, rowEnd, orderBy })
82+
},
83+
sortable: true,
84+
}
85+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { ReactNode, useEffect, useRef, useState } from 'react'
2+
import { cn } from './utils.js'
3+
4+
interface DropdownProps {
5+
label?: string
6+
className?: string
7+
children: ReactNode
8+
}
9+
10+
/**
11+
* Dropdown menu component.
12+
*
13+
* @param {Object} props
14+
* @param {string} props.label - button label
15+
* @param {string} props.className - custom class name for the dropdown container
16+
* @param {ReactNode} props.children - dropdown menu items
17+
* @returns {ReactNode}
18+
* @example
19+
* <Dropdown label='Menu'>
20+
* <button>Item 1</button>
21+
* <button>Item 2</button>
22+
* </Dropdown>
23+
*/
24+
export default function Dropdown({ label, className, children }: DropdownProps): ReactNode {
25+
const [isOpen, setIsOpen] = useState(false)
26+
const dropdownRef = useRef<HTMLDivElement>(null)
27+
const menuRef = useRef<HTMLDivElement>(null)
28+
29+
function toggleDropdown() {
30+
setIsOpen(!isOpen)
31+
}
32+
33+
useEffect(() => {
34+
function handleClickInside(event: MouseEvent) {
35+
const target = event.target as Element
36+
if (menuRef.current && menuRef.current.contains(target) && target.tagName !== 'INPUT') {
37+
setIsOpen(false)
38+
}
39+
}
40+
function handleClickOutside(event: MouseEvent) {
41+
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
42+
setIsOpen(false)
43+
}
44+
}
45+
function handleEscape(event: KeyboardEvent) {
46+
if (event.key === 'Escape') {
47+
setIsOpen(false)
48+
}
49+
}
50+
document.addEventListener('click', handleClickInside)
51+
document.addEventListener('keydown', handleEscape)
52+
document.addEventListener('mousedown', handleClickOutside)
53+
return () => {
54+
document.removeEventListener('click', handleClickInside)
55+
document.removeEventListener('keydown', handleEscape)
56+
document.removeEventListener('mousedown', handleClickOutside)
57+
}
58+
}, [])
59+
60+
return (
61+
<div
62+
className={cn('dropdown', className, isOpen && 'open')}
63+
ref={dropdownRef}>
64+
<button className='dropdown-button' onClick={toggleDropdown}>
65+
{label}
66+
</button>
67+
<div className='dropdown-content' ref={menuRef}>
68+
{children}
69+
</div>
70+
</div>
71+
)
72+
}

0 commit comments

Comments
 (0)