Skip to content

Commit c20f64d

Browse files
committed
Generate CSV/JSON exports
1 parent 027df8a commit c20f64d

24 files changed

+415
-239
lines changed

packages/app/src/index.ts

-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ export { default as H5GroveProvider } from './providers/h5grove/H5GroveProvider'
99
export { enableBigIntSerialization } from './utils';
1010
export { getFeedbackMailto } from './breadcrumbs/utils';
1111
export type { FeedbackContext } from './breadcrumbs/models';
12-
export type { ExportFormat, ExportURL } from './providers/models';
1312
export type GetExportURL = NonNullable<DataProviderApi['getExportURL']>;
1413

1514
// Context

packages/app/src/providers/api.ts

+9-10
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
import {
2-
type ArrayShape,
32
type AttributeValues,
43
type Dataset,
54
type Entity,
65
type ProvidedEntity,
7-
type Value,
86
} from '@h5web/shared/hdf5-models';
97
import { type OnProgress } from '@h5web/shared/react-suspense-fetch';
10-
118
import {
9+
type BuiltInExporter,
1210
type ExportFormat,
1311
type ExportURL,
14-
type ValuesStoreParams,
15-
} from './models';
12+
} from '@h5web/shared/vis-models';
13+
14+
import { type ValuesStoreParams } from './models';
1615

1716
export abstract class DataProviderApi {
1817
public constructor(public readonly filepath: string) {}
@@ -35,12 +34,12 @@ export abstract class DataProviderApi {
3534
* - `() => Promise<Blob>` Export is generated client-side
3635
* - `undefined` Export scenario is not supported
3736
*/
38-
public getExportURL?<D extends Dataset<ArrayShape>>( // optional, so can't be abstract
37+
public getExportURL?( // optional, so can't be abstract
3938
format: ExportFormat,
40-
dataset: D,
41-
selection: string | undefined,
42-
value: Value<D>,
43-
): ExportURL;
39+
dataset: Dataset,
40+
selection?: string,
41+
builtInExporter?: BuiltInExporter,
42+
): ExportURL | undefined;
4443

4544
public getSearchablePaths?(path: string): Promise<string[]>; // optional, so can't be abstract
4645
}

packages/app/src/providers/h5grove/h5grove-api.ts

+34-15
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,29 @@
1-
import { hasNumericType, hasScalarShape } from '@h5web/shared/guards';
21
import {
3-
type ArrayShape,
2+
hasArrayShape,
3+
hasNumericType,
4+
hasScalarShape,
5+
} from '@h5web/shared/guards';
6+
import {
47
type AttributeValues,
58
type Dataset,
69
DTypeClass,
710
type Entity,
811
type ProvidedEntity,
9-
type Value,
1012
} from '@h5web/shared/hdf5-models';
1113
import { type OnProgress } from '@h5web/shared/react-suspense-fetch';
14+
import {
15+
type BuiltInExporter,
16+
type ExportFormat,
17+
type ExportURL,
18+
} from '@h5web/shared/vis-models';
1219
import axios, {
1320
AxiosError,
1421
type AxiosInstance,
1522
type AxiosRequestConfig,
1623
} from 'axios';
1724

1825
import { DataProviderApi } from '../api';
19-
import {
20-
type ExportFormat,
21-
type ExportURL,
22-
type ValuesStoreParams,
23-
} from '../models';
26+
import { type ValuesStoreParams } from '../models';
2427
import { createAxiosProgressHandler } from '../utils';
2528
import {
2629
type H5GroveAttrValuesResponse,
@@ -34,6 +37,8 @@ import {
3437
parseEntity,
3538
} from './utils';
3639

40+
const SUPPORTED_EXPORT_FORMATS = new Set<ExportFormat>(['npy', 'tiff']);
41+
3742
export class H5GroveApi extends DataProviderApi {
3843
private readonly client: AxiosInstance;
3944

@@ -106,18 +111,32 @@ export class H5GroveApi extends DataProviderApi {
106111
return attributes.length > 0 ? this.fetchAttrValues(path) : {};
107112
}
108113

109-
public override getExportURL<D extends Dataset<ArrayShape>>(
114+
public override getExportURL(
110115
format: ExportFormat,
111-
dataset: D,
112-
selection: string | undefined,
113-
value: Value<D>,
114-
): ExportURL {
115-
const url = this._getExportURL?.(format, dataset, selection, value);
116+
dataset: Dataset,
117+
selection?: string,
118+
builtInExporter?: BuiltInExporter,
119+
): ExportURL | undefined {
120+
const url = this._getExportURL?.(
121+
format,
122+
dataset,
123+
selection,
124+
builtInExporter,
125+
);
126+
116127
if (url) {
117128
return url;
118129
}
119130

120-
if (format !== 'json' && !hasNumericType(dataset)) {
131+
if (builtInExporter) {
132+
return async () => new Blob([builtInExporter()]);
133+
}
134+
135+
if (
136+
!SUPPORTED_EXPORT_FORMATS.has(format) ||
137+
!hasArrayShape(dataset) ||
138+
!hasNumericType(dataset)
139+
) {
121140
return undefined;
122141
}
123142

packages/app/src/providers/hsds/hsds-api.ts

+27-12
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,18 @@ import {
1414
type Group,
1515
type GroupWithChildren,
1616
type ProvidedEntity,
17-
type Value,
1817
} from '@h5web/shared/hdf5-models';
1918
import { buildEntityPath, getChildEntity } from '@h5web/shared/hdf5-utils';
2019
import { type OnProgress } from '@h5web/shared/react-suspense-fetch';
21-
import axios, { AxiosError, type AxiosInstance } from 'axios';
22-
23-
import { DataProviderApi } from '../api';
2420
import {
21+
type BuiltInExporter,
2522
type ExportFormat,
2623
type ExportURL,
27-
type ValuesStoreParams,
28-
} from '../models';
24+
} from '@h5web/shared/vis-models';
25+
import axios, { AxiosError, type AxiosInstance } from 'axios';
26+
27+
import { DataProviderApi } from '../api';
28+
import { type ValuesStoreParams } from '../models';
2929
import { createAxiosProgressHandler } from '../utils';
3030
import {
3131
type BaseHsdsEntity,
@@ -171,13 +171,28 @@ export class HsdsApi extends DataProviderApi {
171171
);
172172
}
173173

174-
public override getExportURL<D extends Dataset<ArrayShape>>(
174+
public override getExportURL(
175175
format: ExportFormat,
176-
dataset: D,
177-
selection: string | undefined,
178-
value: Value<D>,
179-
): ExportURL {
180-
return this._getExportURL?.(format, dataset, selection, value);
176+
dataset: Dataset<ArrayShape>,
177+
selection?: string,
178+
builtInExporter?: BuiltInExporter,
179+
): ExportURL | undefined {
180+
const url = this._getExportURL?.(
181+
format,
182+
dataset,
183+
selection,
184+
builtInExporter,
185+
);
186+
187+
if (url) {
188+
return url;
189+
}
190+
191+
if (!builtInExporter) {
192+
return undefined;
193+
}
194+
195+
return async () => new Blob([builtInExporter()]);
181196
}
182197

183198
private async fetchRootId(): Promise<HsdsId> {

packages/app/src/providers/mock/mock-api.ts

+27-43
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,24 @@
1-
import {
2-
assertArrayShape,
3-
assertDefined,
4-
hasNumericType,
5-
} from '@h5web/shared/guards';
1+
import { assertArrayShape, assertDefined } from '@h5web/shared/guards';
62
import {
73
type ArrayShape,
8-
type ArrayValue,
94
type AttributeValues,
105
type Dataset,
116
type Entity,
127
type GroupWithChildren,
13-
type NumericType,
148
type ProvidedEntity,
15-
type Value,
169
} from '@h5web/shared/hdf5-models';
1710
import {
1811
assertMockAttribute,
1912
assertMockDataset,
2013
} from '@h5web/shared/mock-utils';
21-
22-
import { DataProviderApi } from '../api';
2314
import {
15+
type BuiltInExporter,
2416
type ExportFormat,
2517
type ExportURL,
26-
type ValuesStoreParams,
27-
} from '../models';
18+
} from '@h5web/shared/vis-models';
19+
20+
import { DataProviderApi } from '../api';
21+
import { type ValuesStoreParams } from '../models';
2822
import { makeMockFile } from './mock-file';
2923
import {
3024
cancellableDelay,
@@ -99,45 +93,35 @@ export class MockApi extends DataProviderApi {
9993
);
10094
}
10195

102-
public override getExportURL<D extends Dataset<ArrayShape>>(
96+
public override getExportURL(
10397
format: ExportFormat,
104-
dataset: D,
105-
selection: string | undefined,
106-
value: Value<D>,
107-
): ExportURL {
108-
const url = this._getExportURL?.(format, dataset, selection, value);
98+
dataset: Dataset<ArrayShape>,
99+
selection?: string,
100+
builtInExporter?: BuiltInExporter,
101+
): ExportURL | undefined {
102+
const url = this._getExportURL?.(
103+
format,
104+
dataset,
105+
selection,
106+
builtInExporter,
107+
);
108+
109109
if (url) {
110110
return url;
111111
}
112112

113-
if (format === 'json') {
114-
return async () => {
115-
const json = JSON.stringify(value, null, 2);
116-
return new Blob([json]);
117-
};
113+
if (!builtInExporter) {
114+
return undefined;
118115
}
119116

120-
if (
121-
hasNumericType(dataset) &&
122-
selection === undefined &&
123-
format === 'csv'
124-
) {
125-
return async () => {
126-
let csv = '';
127-
(value as ArrayValue<NumericType>).forEach((val) => {
128-
csv += `${val.toString()}\n`;
129-
});
130-
131-
const finalCsv = csv.slice(0, -2);
132-
133-
// Demonstrate both `Blob` and `URL` techniques (cf. `src/providers/api.ts`)
134-
return dataset.name === 'oneD'
135-
? new Blob([finalCsv])
136-
: new URL(`data:text/plain,${encodeURIComponent(finalCsv)}`);
137-
};
138-
}
117+
return async () => {
118+
const csv = builtInExporter();
139119

140-
return undefined;
120+
// Demonstrate both `Blob` and `URL` techniques (cf. `src/providers/api.ts`)
121+
return dataset.name === 'oneD'
122+
? new Blob([csv])
123+
: new URL(`data:text/plain,${encodeURIComponent(csv)}`);
124+
};
141125
}
142126

143127
public override async getSearchablePaths(path: string): Promise<string[]> {

packages/app/src/providers/models.ts

-3
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,4 @@ export interface AttrValuesStore extends FetchStore<Entity, AttributeValues> {
2525
export type ImageAttribute = 'CLASS' | 'IMAGE_SUBCLASS';
2626
export type AttrName = NxAttribute | ImageAttribute | '_FillValue';
2727

28-
export type ExportFormat = 'json' | 'csv' | 'npy' | 'tiff';
29-
export type ExportURL = URL | (() => Promise<URL | Blob>) | undefined;
30-
3128
export type ProgressCallback = (prog: number[]) => void;

packages/app/src/vis-packs/core/compound/MappedCompoundVis.tsx

+17-9
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,17 @@ import {
1111
import { createPortal } from 'react-dom';
1212

1313
import { type DimensionMapping } from '../../../dimension-mapper/models';
14-
import { useDataContext } from '../../../providers/DataProvider';
1514
import visualizerStyles from '../../../visualizer/Visualizer.module.css';
16-
import { useMappedArray, useSlicedDimsAndMapping } from '../hooks';
15+
import {
16+
useExportEntries,
17+
useMappedArray,
18+
useSlicedDimsAndMapping,
19+
} from '../hooks';
1720
import { type MatrixVisConfig } from '../matrix/config';
1821
import MatrixToolbar from '../matrix/MatrixToolbar';
19-
import { getCellWidth, getFormatter } from '../matrix/utils';
22+
import { getCellWidth, getCsvFormatter, getFormatter } from '../matrix/utils';
2023
import { getSliceSelection } from '../utils';
24+
import { generateCsv } from './utils';
2125

2226
interface Props {
2327
dataset: Dataset<ScalarShape | ArrayShape, CompoundType<PrintableType>>;
@@ -49,12 +53,19 @@ function MappedCompoundVis(props: Props) {
4953
slicedMapping,
5054
);
5155

56+
const selection = getSliceSelection(dimMapping);
5257
const fieldFormatters = Object.values(fields).map((field) =>
5358
getFormatter(field, notation),
5459
);
5560

56-
const { getExportURL } = useDataContext();
57-
const selection = getSliceSelection(dimMapping);
61+
const exportEntries = useExportEntries(['npy', 'csv'], dataset, selection, {
62+
csv: () =>
63+
generateCsv(
64+
fieldNames,
65+
mappedArray,
66+
Object.values(fields).map((field) => getCsvFormatter(field)),
67+
),
68+
});
5869

5970
return (
6071
<>
@@ -64,10 +75,7 @@ function MappedCompoundVis(props: Props) {
6475
cellWidth={cellWidth}
6576
isSlice={selection !== undefined}
6677
config={config}
67-
getExportURL={
68-
getExportURL &&
69-
((format) => getExportURL(format, dataset, selection, value))
70-
}
78+
exportEntries={exportEntries}
7179
/>,
7280
toolbarContainer,
7381
)}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import {
2+
type PrintableType,
3+
type ScalarValue,
4+
} from '@h5web/shared/hdf5-models';
5+
import { type NdArray } from 'ndarray';
6+
7+
export function generateCsv(
8+
names: string[],
9+
compoundArray: NdArray<ScalarValue<PrintableType>[]>,
10+
formatters: ((val: ScalarValue<PrintableType>) => string)[],
11+
): string {
12+
let csv = names.join(','); // column headers
13+
const [dim1, dim2] = compoundArray.shape;
14+
15+
for (let i = 0; i < dim1; i += 1) {
16+
let line = '\n';
17+
18+
for (let j = 0; j < dim2; j += 1) {
19+
line += `${formatters[j](compoundArray.get(i, j))},`;
20+
}
21+
22+
csv += line.slice(0, -1); // trim trailing comma
23+
}
24+
25+
return csv;
26+
}

0 commit comments

Comments
 (0)