H5Web App & Providers

H5Web is a collection of React components
to visualize and explore data. It consists of two main packages:
@h5web/app
exposes the HDF5 viewer component App
, as well as the following
built-in data providers:
H5GroveProvider
for use with server implementations based on
H5Grove, like
jupyterlab-h5web;
HsdsProvider
for use with HSDS;
MockProvider
for testing purposes.
Prerequisites
The react
and react-dom
dependencies must be installed in your project. Note
that as of version 10, @h5web/app
requires React 18.
This package supports TypeScript out of the box without the need to install a
separate @types/
package.
Getting started 🚀
npm install @h5web/app
import '@h5web/app/styles.css';
import React from 'react';
import { App, MockProvider } from '@h5web/app';
function MyApp() {
return (
<div style={{ height: '100vh' }}>
<MockProvider>
<App />
</MockProvider>
</div>
);
}
export default MyApp;
Examples
The following code sandboxes demonstrate how to set up and use @h5web/app
with
various front-end development stacks:
Browser support
H5Web works out of the box on Firefox 78 ESR. Support for Firefox 68 ESR is
possible by polyfilling the ResizeObserver
API. Older versions of Firefox are
not supported.
API reference
App
Renders the HDF5 viewer.
For App
to work, it must be wrapped in a data provider:
<MockProvider>
<App />
</MockProvider>
Whether the viewer should start with the sidebar open. The sidebar contains the
explorer and search panels. Defaults to true
. Pass false
to hide the sidebar
on initial render, thus giving more space to the visualization. This is useful
when H5Web is embeded inside another app.
<App sidebarOpen={false} />
This replaces prop explorerOpen
, which was deprecated in v7.1.0 and removed
in v8.0.0.
initialPath?: string
(optional)
The path to select within the file when the viewer is first rendered. Defaults
to '/'
.
<MockProvider>
<App initialPath="/nD_datasets/threeD" />
</MockProvider>
getFeedbackURL?: (context: FeedbackContext) => string
(optional)
If provided, a "Give feedback" button appears in the breadcrumbs bar, which
invokes the function when clicked. The function should return a valid URL, for
instance a mailto:
URL with a pre-filled subject and body:
mailto:some@email.com?subject=Feedback&body=<url-encoded-text>
. If the app is
publicly available, we recommend returning the URL of a secure online contact
form instead.
<App getFeedbackURL={() => 'https://my-feedback-form.com'} />
<App
getFeedbackURL={(context) => {
const {
filePath,
entityPath,
} = context;
return `mailto:some@email.com?subject=Feedback&body=${encodeURIComponent(...)}`;
}}
/>
disableDarkMode?: boolean
(optional)
By default, the viewer follows your browser's and/or operating system's dark
mode setting. This prop disables this beahviour by forcing the viewer into light
mode.
<App disableDarkMode />
propagateErrors?: boolean
(optional)
The viewer has a top-level ErrorBoundary
that, by default, handles errors
thrown outside of the visualization area. These include errors thrown by the
data provider when fetching metadata for the explorer. If you prefer to
implement your own error boundary, you may choose to let errors through the
viewer's top-level boundary:
import { ErrorBoundary } from 'react-error-boundary';
<ErrorBoundary FallbackComponent={MyErrorFallback}>
<MockProvider>
<App propagateErrors />
</MockProvider>
</ErrorBoundary>;
H5GroveProvider
Data provider for H5Grove.
<H5GroveProvider url="https://h5grove.server.url" filepath="some-file.h5">
<App />
</H5GroveProvider>
url: string
(required)
The base URL of the H5Grove server.
filepath: string
(required)
The path and/or name of the file to display in the UI.
fetcher?: Fetcher
(optional)
An optional asynchronous function to fetch data and metadata from an h5grove
back-end. The function accepts a URL together with some query parameters and
options, and is expected to return a promise that resolves to an ArrayBuffer
.
If fetcher
is not provided, H5GroveProvider
creates one based on the native
Fetch API using
createBasicFetcher
but without any form of authentication. In production, we recommend deploying a
secure h5grove back-end and initialising a fetcher that sends the expected
authentication headers.
If you have to initialise the fetcher
during render, make sure to memoise it
so the fetching cache isn't cleared every time your app re-renders.
getExportURL?: (...args) => URL | (() => Promise<URL | Blob>) | undefined
(optional)
Some visualizations allow exporting the current dataset/slice to various
formats. For instance, the Line visualization allows exporting to CSV and NPY;
the Heatmap visualization to NPY and TIFF, etc.
For each format, the viewer invokes the provider's getExportURL
method. If
this method returns a URL
or an async function, then the export menu in the
toolbar shows an entry for the corresponding export format.
In the case of JSON and CSV, the viewer itself takes care of the export by
providing its own "exporter" function to the getExportURL
method. When this
happens, the getExportURL
method just returns a function that calls the
exporter.
In the case of NPY and TIFF, H5GroveApi#getExportURL
returns a URL
so the
export can be generated server-side by h5grove
.
The optional getExportURL
prop is called internally by the getExportURL
method and allows taking over the export process. It enables advanced use cases
like generating exports from an authenticated endpoint.
Advanced examples
getExportURL={(format, dataset, selection) => async () => {
const query = new URLSearchParams({ format, path: dataset.path, selection });
const response = await fetch(`${AUTH_EXPORT_ENDPOINT}?${query.toString()}`, {
headers: { }
})
return response.blob();
}}
getExportURL={(format, dataset, selection) => async () => {
const query = new URLSearchParams({ format, path: dataset.path, selection });
const response = await fetch(`${AUTH_TOKEN_ENDPOINT}?${query.toString()}`, {
headers: { }
})
return new URL(await response.body());
}}
getExportURL={(format, dataset, selection, builtInExporter) => async () => {
if (!builtInExporter || format !== 'csv') {
return undefined;
}
const csvPayload = builtInExporter();
return csvPayload.split('\n').slice(0, 100).join('\n');
}}
resetKeys?: unknown[]
(optional)
You can pass variables in resetKeys
that, when changed, will reset the
provider's internal fetch cache. You may want to do this, for instance, when the
content of the current file changes and you want the viewer to refetch the
latest metadata and dataset values.
It is up to you to decide what sort of keys to use and when to update them. For
instance:
- Your server could send over a hash of the file via WebSocket.
- You could show a toast notification with a Refresh button when the file
changes and simply increment a number when the button is clicked (cf.
contrived example below).
function MyApp() {
const [key, setKey] = useState(0);
const incrementKey = useCallback(() => setKey((val) => val + 1), []);
return (
<>
<button type="button" onClick={incrementKey}>
Refresh
</button>
<H5GroveProvider resetKeys={[key]} /* ... */>
<App />
</H5GroveProvider>
</>
);
}
HsdsProvider
Data provider for HSDS.
<HsdsProvider
url="https://hsds.server.url"
username="foo"
password="abc123"
filepath="/home/reader/some-file.h5"
>
<App />
</HsdsProvider>
url: string
(required)
The base URL of the HSDS server.
username: string; password: string
(required)
The credentials to use to authenticate to the HSDS server. Note that this
authentication mechanism is not secure; please do not use it to grant access to
private data.
filepath: string
(required)
The path of the file to request.
fetcher?: Fetcher
(optional)
An asynchronous function to fetch data and metadata from an HSDS back-end. The
function accepts a URL together with some query parameters and options, and is
expected to return a promise that resolves to an ArrayBuffer
. The fetcher must
also send the required HSDS authentication headers.
To get you started, if your HSDS back-end is configured with basic HTTP
authentication, you can use
createBasicFetcher
together with
buildBasicAuthHeader
:
const fetcher = createBasicFetcher({
headers: buildBasicAuthHeader(USERNAME, PASSWORD),
});
However, beware that this authentication mechanism is not secure — do not use it
to grant access to private data.
If you have to initialise the fetcher
during render, make sure to memoise it
so the fetching cache isn't cleared every time your app re-renders.
getExportURL?: (...args) => URL | (() => Promise<URL | Blob>) | undefined
(optional)
See
H5GroveProvider#getExportURL
.
HsdsProvider
doesn't support the NPY and TIFF export formats out of the box.
resetKeys?: unknown[]
(optional)
See
H5GroveProvider#resetKeys
.
MockProvider
Data provider for demonstration and testing purposes.
<MockProvider>
<App />
</MockProvider>
getExportURL?: (...args) => URL | (() => Promise<URL | Blob>) | undefined
(optional)
See
H5GroveProvider#getExportURL
.
MockProvider
doesn't support the NPY and TIFF export formats out of the box.
Utilities
createBasicFetcher: (fetchOpts?: Omit<RequestInit, 'signal'>) => Fetcher
Create a fetcher
function based on the native
Fetch API. Accepts an optional
RequestInit
object to configure requests, for instance with an Authentication
header:
const fetcher = createBasicFetcher({
headers: { Authorization: `Bearer ${token}` },
});
To add custom query parameters to the requests:
const basicFetcher = createBasicFetcher();
async function fetcher(...args: Parameters<Fetcher>) {
const [url, params, opts] = args;
return basicFetcher(url, { ...params, myOwnParam }, opts);
}
createAxiosFetcher: (axiosInstance: AxiosInstance) => Fetcher
Create a fetcher
function from an
axios instance. Note that you will need to install
axios
in your application.
const fetcher = createAxiosFetcher(axios.create({ adapter: 'fetch' }));
function MyApp() {
return (
<H5GroveProvider url={URL} filepath={FILE} fetcher={fetcher}>
{/*...*/}
</H5GroveProvider>
);
}
You can configure the axios instance
as you see fit. For instance, you can specify authentication headers, or set up
request interceptors to refresh
tokens and retry requests automatically. However, do note that some options have
no effect, notably url
/baseUrl
, responseType
, signal
and
onDownloadProgress
.
Build an Authorization
header for basic HTTP authentication from a username
and password.
getFeedbackMailto: (context: FeedbackContext, email: string, subject?) => string
Generate a feedback mailto:
URL using H5Web's built-in feedback email
template.
(context: FeedbackContext, email: string, subject = 'Feedback') => string;
import { getFeedbackMailto } from '@h5web/app';
...
<App getFeedbackURL={(context) => {
return getFeedbackMailto(context, 'some@email.com');
}} />
enableBigIntSerialization: () => void
Invoke this function before rendering your application to allow the Raw
visualization and metadata inspector to serialize and display
big integers:
enableBigIntSerialization();
createRoot(document.querySelector('#root')).render(<MyApp />);
This is recommended if you work with a provider that supports 64-bit integers —
i.e. one that may provide dataset and attribute values that include primitive
bigint
numbers — currently only MockProvider
.
The Raw visualization and metadata inspector rely on JSON.stringify()
to
render dataset and attribute values. By default, JSON.stringify()
does not
know how to serialize bigint
numbers and throws an error if it encounters one.
enableBigIntSerialization()
teaches JSON.stringify()
to convert big integers
to strings:
> JSON.stringify(123n);
TypeError: Do not know how to serialize a BigInt
> enableBigIntSerialization();
> JSON.stringify(123n);
"123n"
The n
suffix (i.e. the same suffix used for bigint
literals as
demonstrated above) is added to help distinguish big integer strings from
other strings.
If you're application already implements bigint
serialization, you don't
need to call enableBigIntSerialization()
. Doing so would override the
existing implementation, which might have unintended effects.
Context
The viewer component App
communicates with its wrapping data provider through
a React context called DataContext
. This context is available via a custom
hook called useDataContext
. This means you can use the built-in data providers
in your own applications:
<MockProvider>
<MyApp />
</MockProvider>;
function MyApp() {
const { filename } = useDataContext();
return <p>{filename}</p>;
}
useDataContext
returns the following object:
interface DataContextValue {
filepath: string;
filename: string;
entitiesStore: EntitiesStore;
valuesStore: ValuesStore;
attrValuesStore: AttrValuesStore;
}
The three stores are created with the
react-suspense-fetch library,
which relies on
React Suspense. A
component that uses one of these stores (e.g.
entitiesStore.get('/path/to/entity')
) must have a Suspense
ancestor to
manage the loading state.
<MockProvider>
<Suspense fallback={<span>Loading...</span>}>
<MyApp />
</Suspense>
</MockProvider>;
function MyApp() {
const { entitiesStore } = useDataContext();
const group = entitiesStore.get('/resilience/slow_metadata');
return <pre>{JSON.stringify(group, null, 2)}</pre>;
}
For convenience, the following hooks are available to access data through the
data context: useEntity
, useDatasetValue
, useDatasetsValues
,
usePrefetchValues
.
We also provide a large number of type guards and assertion functions to narrow
down the kind/shape/type of HDF5 entities returned by useEntity
, as well as a
memoised hook called useNdArray
to create ndarrays that can be passed to the
visualization components (available in @h5web/lib
).
function MyApp() {
const entity = useEntity('/nD_datasets/twoD');
assertDataset(entity);
assertArrayShape(entity);
assertFloatType(entity);
const value = useDatasetValue(entity);
const dataArray = useNdArray(value, entity.shape);
const domain = useDomain(dataArray);
return (
<HeatmapVis
style={{ width: '100vw', height: '100vh' }}
dataArray={dataArray}
domain={domain}
/>
);
}
When accessing the values of multiple datasets with multiple consecutive calls
to useDatasetValue
(and/or useDatasetsValues
), invoke usePrefetchValues
first to ensure that the values are requested in parallel rather than
sequentially:
const axesDatasets = [abscissasDataset, ordinatesDataset];
usePrefetchValues([valuesDataset, ...axesDatasets]);
const values = useDatasetValue(valuesDataset);
const [abscissas, ordinates] = useDatasetsValues(axesDatasets);
All three hooks accept a selection
parameter to request specific slices from
n-dimensional datasets:
const selection = '0,:,:';
usePrefetchValues([valuesDataset], selection);
usePrefetchValues([abscissasDataset, ordinatesDataset]);
const values = useDatasetValue(valuesDataset, selection);
const abscissas = useDatasetValue(abscissasDataset);
const ordinates = useDatasetValue(ordinatesDataset);