jsonstat-io 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +200 -0
- package/dist/arrow/index.cjs +60 -0
- package/dist/arrow/index.cjs.map +1 -0
- package/dist/arrow/index.d.cts +125 -0
- package/dist/arrow/index.d.ts +125 -0
- package/dist/arrow/index.js +3 -0
- package/dist/arrow/index.js.map +1 -0
- package/dist/arrowToCube-BdRWZ5oG.d.cts +29 -0
- package/dist/arrowToCube-Mjr79ZHM.d.ts +29 -0
- package/dist/chunk-2HY7NRKX.js +259 -0
- package/dist/chunk-2HY7NRKX.js.map +1 -0
- package/dist/chunk-62NIY7ML.js +191 -0
- package/dist/chunk-62NIY7ML.js.map +1 -0
- package/dist/chunk-AXUZF6O5.cjs +352 -0
- package/dist/chunk-AXUZF6O5.cjs.map +1 -0
- package/dist/chunk-EFRDFQOT.js +335 -0
- package/dist/chunk-EFRDFQOT.js.map +1 -0
- package/dist/chunk-GDKDYLZ4.cjs +263 -0
- package/dist/chunk-GDKDYLZ4.cjs.map +1 -0
- package/dist/chunk-IDTQZG4B.js +432 -0
- package/dist/chunk-IDTQZG4B.js.map +1 -0
- package/dist/chunk-KFIKGBIJ.cjs +200 -0
- package/dist/chunk-KFIKGBIJ.cjs.map +1 -0
- package/dist/chunk-L65CRXTV.js +64 -0
- package/dist/chunk-L65CRXTV.js.map +1 -0
- package/dist/chunk-PHLCWTHB.cjs +446 -0
- package/dist/chunk-PHLCWTHB.cjs.map +1 -0
- package/dist/chunk-VQCS4LVD.cjs +166 -0
- package/dist/chunk-VQCS4LVD.cjs.map +1 -0
- package/dist/chunk-VYS7DPB3.js +162 -0
- package/dist/chunk-VYS7DPB3.js.map +1 -0
- package/dist/chunk-ZEDTWRDN.cjs +70 -0
- package/dist/chunk-ZEDTWRDN.cjs.map +1 -0
- package/dist/cli/index.cjs +337 -0
- package/dist/cli/index.cjs.map +1 -0
- package/dist/cli/index.d.cts +90 -0
- package/dist/cli/index.d.ts +90 -0
- package/dist/cli/index.js +333 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cubeBuilder-BEGA4C5Y.js +4 -0
- package/dist/cubeBuilder-BEGA4C5Y.js.map +1 -0
- package/dist/cubeBuilder-ZU76VVEX.cjs +25 -0
- package/dist/cubeBuilder-ZU76VVEX.cjs.map +1 -0
- package/dist/cubeReader-7QVUF3TI.js +4 -0
- package/dist/cubeReader-7QVUF3TI.js.map +1 -0
- package/dist/cubeReader-UN2BHQWK.cjs +21 -0
- package/dist/cubeReader-UN2BHQWK.cjs.map +1 -0
- package/dist/index.cjs +172 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +445 -0
- package/dist/index.d.ts +445 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/ir-BArfNxo-.d.cts +143 -0
- package/dist/ir-Jae2ymov.d.ts +143 -0
- package/dist/jsonstat-BX4mBLci.d.cts +179 -0
- package/dist/jsonstat-BX4mBLci.d.ts +179 -0
- package/dist/sources/csv.cjs +40 -0
- package/dist/sources/csv.cjs.map +1 -0
- package/dist/sources/csv.d.cts +112 -0
- package/dist/sources/csv.d.ts +112 -0
- package/dist/sources/csv.js +3 -0
- package/dist/sources/csv.js.map +1 -0
- package/dist/sources/csvw.cjs +236 -0
- package/dist/sources/csvw.cjs.map +1 -0
- package/dist/sources/csvw.d.cts +143 -0
- package/dist/sources/csvw.d.ts +143 -0
- package/dist/sources/csvw.js +230 -0
- package/dist/sources/csvw.js.map +1 -0
- package/dist/sources/duckdb.cjs +121 -0
- package/dist/sources/duckdb.cjs.map +1 -0
- package/dist/sources/duckdb.d.cts +111 -0
- package/dist/sources/duckdb.d.ts +111 -0
- package/dist/sources/duckdb.js +115 -0
- package/dist/sources/duckdb.js.map +1 -0
- package/dist/sources/parquet.cjs +117 -0
- package/dist/sources/parquet.cjs.map +1 -0
- package/dist/sources/parquet.d.cts +83 -0
- package/dist/sources/parquet.d.ts +83 -0
- package/dist/sources/parquet.js +111 -0
- package/dist/sources/parquet.js.map +1 -0
- package/dist/sources/polars.cjs +94 -0
- package/dist/sources/polars.cjs.map +1 -0
- package/dist/sources/polars.d.cts +91 -0
- package/dist/sources/polars.d.ts +91 -0
- package/dist/sources/polars.js +87 -0
- package/dist/sources/polars.js.map +1 -0
- package/docs/api.md +322 -0
- package/docs/architecture.md +140 -0
- package/docs/cli.md +159 -0
- package/docs/formats/arrow.md +111 -0
- package/docs/formats/csv.md +115 -0
- package/docs/formats/csvw.md +152 -0
- package/docs/formats/duckdb.md +151 -0
- package/docs/formats/parquet.md +132 -0
- package/docs/formats/polars.md +111 -0
- package/docs/mapping.md +136 -0
- package/package.json +119 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
export { a as ArrowConversionError, A as ArrowToCubeOptions, b as arrowToCube, c as arrowToDataset } from '../arrowToCube-Mjr79ZHM.js';
|
|
2
|
+
import { Table, Field, Schema } from 'apache-arrow';
|
|
3
|
+
import { O as Observations, R as RoleMap, D as DatasetMeta } from '../ir-Jae2ymov.js';
|
|
4
|
+
import '../jsonstat-BX4mBLci.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* arrowFromCube — the inverse of [`arrowToCube`](./arrowToCube.ts): converts the
|
|
8
|
+
* [`Observations`](../model/ir.ts) IR into an Apache Arrow [`Table`].
|
|
9
|
+
*
|
|
10
|
+
* ## Role
|
|
11
|
+
*
|
|
12
|
+
* This is the **Phase-2 export seam**: the forward path from JSON-stat to the
|
|
13
|
+
* columnar lakehouse stack. Export = [`readDataset`](../core/cubeReader.ts)
|
|
14
|
+
* (JSON-stat → IR) → `cubeToArrow` (IR → Arrow) → Arrow-to-Parquet/DuckDB/
|
|
15
|
+
* Polars/CSV writers (which Arrow ecosystems provide natively).
|
|
16
|
+
*
|
|
17
|
+
* It is shipped in v0.1 (alongside [`cubeReader`](../core/cubeReader.ts)) for
|
|
18
|
+
* two reasons: (1) the round-trip tests use it to assert that JSON-stat → IR →
|
|
19
|
+
* Arrow → IR → JSON-stat is stable; (2) proving the IR is bidirectional now
|
|
20
|
+
* de-risks Phase 2 to "just write the sink writers".
|
|
21
|
+
*
|
|
22
|
+
* ## Mapping
|
|
23
|
+
*
|
|
24
|
+
* - Each dimension → a dictionary-encoded Utf8 column (dictionary = the
|
|
25
|
+
* resolved category list), annotated with `jsonstat.*` field metadata so the
|
|
26
|
+
* round-trip preserves labels/units/roles/hierarchies.
|
|
27
|
+
* - The measure → a Float64 column (nulls preserved), marked
|
|
28
|
+
* `jsonstat.measure=true`.
|
|
29
|
+
* - The status column (if any) → a Utf8 column marked `jsonstat.status=true`.
|
|
30
|
+
* - Dataset-level metadata (label/source/updated/extension/roles/valueForm) is
|
|
31
|
+
* written to the schema metadata.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Convert the [`Observations`](../model/ir.ts) IR into an Arrow [`Table`].
|
|
36
|
+
*
|
|
37
|
+
* Each dimension becomes a dictionary column; the measure becomes a Float64
|
|
38
|
+
* column; the optional status becomes a Utf8 column. All `jsonstat.*`
|
|
39
|
+
* metadata is attached so the result round-trips through [`arrowToCube`](./arrowToCube.ts).
|
|
40
|
+
*
|
|
41
|
+
* @throws Error if a dimension column referenced by the model is missing.
|
|
42
|
+
*/
|
|
43
|
+
declare function cubeToArrow(obs: Observations): Table;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Arrow schema metadata contract for JSON-stat.
|
|
47
|
+
*
|
|
48
|
+
* Producers (Parquet/DuckDB/Polars/CSVW adapters, or hand-built Arrow tables)
|
|
49
|
+
* annotate Arrow [`Field`](https://arrow.apache.org/docs/js/) metadata with
|
|
50
|
+
* `jsonstat.*` keys so that [`arrowToCube`](./arrowToCube.ts) can reconstruct
|
|
51
|
+
* the full JSON-stat model — not just the raw values. Without these keys the
|
|
52
|
+
* converter falls back to heuristic inference (dictionary columns = dimensions,
|
|
53
|
+
* first numeric column = measure); with them, the mapping is exact and
|
|
54
|
+
* lossless, which is what the round-trip tests require.
|
|
55
|
+
*
|
|
56
|
+
* ## Metadata key reference
|
|
57
|
+
*
|
|
58
|
+
* Field-level keys (on `Field.metadata`):
|
|
59
|
+
*
|
|
60
|
+
* | Key | Value | Meaning |
|
|
61
|
+
* |------------------------------|----------------------------------------|---------|
|
|
62
|
+
* | `jsonstat.role` | `"time"` / `"geo"` / `"metric"` | Dimension role |
|
|
63
|
+
* | `jsonstat.measure` | `"true"` | Marks this column as the measure (overrides type heuristics) |
|
|
64
|
+
* | `jsonstat.status` | `"true"` | Marks this column as the per-row status column |
|
|
65
|
+
* | `jsonstat.label` | string | Dimension label |
|
|
66
|
+
* | `jsonstat.categoryLabels` | JSON object: `{id: label}` | Category labels |
|
|
67
|
+
* | `jsonstat.categoryUnits` | JSON object: `{id: {decimals,...}}` | Per-category unit metadata (metric role) |
|
|
68
|
+
* | `jsonstat.categoryCoords` | JSON object: `{id: [lon,lat]}` | Per-category coordinates (geo role) |
|
|
69
|
+
* | `jsonstat.categoryChild` | JSON object: `{parent: [child,...]}` | Hierarchy |
|
|
70
|
+
* | `jsonstat.categoryOrder` | JSON array: `[id,...]` | Explicit category ordering (e.g. chronological) |
|
|
71
|
+
*
|
|
72
|
+
* Schema-level keys (on `Schema.metadata`):
|
|
73
|
+
*
|
|
74
|
+
* | Key | Value | Meaning |
|
|
75
|
+
* |------------------------------|----------------------------------------|---------|
|
|
76
|
+
* | `jsonstat.label` | string | Dataset label |
|
|
77
|
+
* | `jsonstat.source` | string | Dataset source |
|
|
78
|
+
* | `jsonstat.updated` | ISO 8601 string | Dataset update time |
|
|
79
|
+
* | `jsonstat.extension` | JSON object | Provider extension data |
|
|
80
|
+
* | `jsonstat.valueForm` | `"auto"`/`"dense"`/`"sparse"` | Value emission hint |
|
|
81
|
+
*
|
|
82
|
+
* See [docs/mapping.md](../../docs/mapping.md) for the full fidelity table.
|
|
83
|
+
*/
|
|
84
|
+
|
|
85
|
+
/** Metadata namespace prefix used for all JSON-stat keys. */
|
|
86
|
+
declare const META_PREFIX = "jsonstat.";
|
|
87
|
+
/** Read a single string metadata value from a field. */
|
|
88
|
+
declare function getFieldMeta(field: Field, key: string): string | undefined;
|
|
89
|
+
/** Read and JSON-parse a field metadata value, with a fallback. */
|
|
90
|
+
declare function getFieldMetaJson<T>(field: Field, key: string): T | undefined;
|
|
91
|
+
/** Is this field marked as the measure column? */
|
|
92
|
+
declare function isMeasureField(field: Field): boolean;
|
|
93
|
+
/** Is this field marked as the status column? */
|
|
94
|
+
declare function isStatusField(field: Field): boolean;
|
|
95
|
+
/** Read the dimension role assigned to a field, if any. */
|
|
96
|
+
declare function getFieldRole(field: Field): "time" | "geo" | "metric" | undefined;
|
|
97
|
+
/** Build a metadata Map for a field from JSON-stat-oriented hints. */
|
|
98
|
+
declare function buildFieldMeta(hints: {
|
|
99
|
+
role?: "time" | "geo" | "metric";
|
|
100
|
+
measure?: boolean;
|
|
101
|
+
status?: boolean;
|
|
102
|
+
label?: string;
|
|
103
|
+
categoryLabels?: Record<string, string>;
|
|
104
|
+
categoryUnits?: Record<string, unknown>;
|
|
105
|
+
categoryCoords?: Record<string, [number, number]>;
|
|
106
|
+
categoryChild?: Record<string, string[]>;
|
|
107
|
+
categoryOrder?: string[];
|
|
108
|
+
}): Map<string, string>;
|
|
109
|
+
/** Read dataset-level metadata from a schema. */
|
|
110
|
+
declare function readSchemaMeta(schema: Schema): {
|
|
111
|
+
meta?: DatasetMeta;
|
|
112
|
+
roles?: RoleMap;
|
|
113
|
+
valueForm?: "auto" | "dense" | "sparse";
|
|
114
|
+
};
|
|
115
|
+
/** Build a schema-level metadata Map from dataset hints. */
|
|
116
|
+
declare function buildSchemaMeta(hints: {
|
|
117
|
+
label?: string;
|
|
118
|
+
source?: string;
|
|
119
|
+
updated?: string;
|
|
120
|
+
extension?: Record<string, unknown>;
|
|
121
|
+
roles?: RoleMap;
|
|
122
|
+
valueForm?: "auto" | "dense" | "sparse";
|
|
123
|
+
}): Map<string, string>;
|
|
124
|
+
|
|
125
|
+
export { META_PREFIX, buildFieldMeta, buildSchemaMeta, cubeToArrow, getFieldMeta, getFieldMetaJson, getFieldRole, isMeasureField, isStatusField, readSchemaMeta };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { ArrowConversionError, META_PREFIX, arrowToCube, arrowToDataset, buildFieldMeta, buildSchemaMeta, cubeToArrow, getFieldMeta, getFieldMetaJson, getFieldRole, isMeasureField, isStatusField, readSchemaMeta } from '../chunk-IDTQZG4B.js';
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
3
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { J as JsonStatDataset } from './jsonstat-BX4mBLci.cjs';
|
|
2
|
+
import { Table } from 'apache-arrow';
|
|
3
|
+
import { R as RoleMap, O as Observations } from './ir-BArfNxo-.cjs';
|
|
4
|
+
|
|
5
|
+
declare class ArrowConversionError extends Error {
|
|
6
|
+
constructor(message: string);
|
|
7
|
+
}
|
|
8
|
+
interface ArrowToCubeOptions {
|
|
9
|
+
/** Explicit measure column name. Overrides detection + metadata. */
|
|
10
|
+
measure?: string;
|
|
11
|
+
/** Explicit dimension column names, in order. Overrides detection. */
|
|
12
|
+
dimensions?: string[];
|
|
13
|
+
/** Explicit status column name. Overrides metadata. */
|
|
14
|
+
status?: string;
|
|
15
|
+
/** Explicit role assignments (merge with, and take precedence over, metadata). */
|
|
16
|
+
roles?: RoleMap;
|
|
17
|
+
/** Value-form hint forwarded to the cube model. */
|
|
18
|
+
valueForm?: "auto" | "dense" | "sparse";
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Convert an Arrow [`Table`] into the [`Observations`] IR.
|
|
22
|
+
*
|
|
23
|
+
* @throws [`ArrowConversionError`](#arrowconversionerror) on mapping problems.
|
|
24
|
+
*/
|
|
25
|
+
declare function arrowToCube(table: Table, options?: ArrowToCubeOptions): Observations;
|
|
26
|
+
/** Convenience: convert an Arrow table straight to a JSON-stat dataset. */
|
|
27
|
+
declare function arrowToDataset(table: Table, options?: ArrowToCubeOptions): Promise<JsonStatDataset>;
|
|
28
|
+
|
|
29
|
+
export { type ArrowToCubeOptions as A, ArrowConversionError as a, arrowToCube as b, arrowToDataset as c };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { J as JsonStatDataset } from './jsonstat-BX4mBLci.js';
|
|
2
|
+
import { Table } from 'apache-arrow';
|
|
3
|
+
import { R as RoleMap, O as Observations } from './ir-Jae2ymov.js';
|
|
4
|
+
|
|
5
|
+
declare class ArrowConversionError extends Error {
|
|
6
|
+
constructor(message: string);
|
|
7
|
+
}
|
|
8
|
+
interface ArrowToCubeOptions {
|
|
9
|
+
/** Explicit measure column name. Overrides detection + metadata. */
|
|
10
|
+
measure?: string;
|
|
11
|
+
/** Explicit dimension column names, in order. Overrides detection. */
|
|
12
|
+
dimensions?: string[];
|
|
13
|
+
/** Explicit status column name. Overrides metadata. */
|
|
14
|
+
status?: string;
|
|
15
|
+
/** Explicit role assignments (merge with, and take precedence over, metadata). */
|
|
16
|
+
roles?: RoleMap;
|
|
17
|
+
/** Value-form hint forwarded to the cube model. */
|
|
18
|
+
valueForm?: "auto" | "dense" | "sparse";
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Convert an Arrow [`Table`] into the [`Observations`] IR.
|
|
22
|
+
*
|
|
23
|
+
* @throws [`ArrowConversionError`](#arrowconversionerror) on mapping problems.
|
|
24
|
+
*/
|
|
25
|
+
declare function arrowToCube(table: Table, options?: ArrowToCubeOptions): Observations;
|
|
26
|
+
/** Convenience: convert an Arrow table straight to a JSON-stat dataset. */
|
|
27
|
+
declare function arrowToDataset(table: Table, options?: ArrowToCubeOptions): Promise<JsonStatDataset>;
|
|
28
|
+
|
|
29
|
+
export { type ArrowToCubeOptions as A, ArrowConversionError as a, arrowToCube as b, arrowToDataset as c };
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { totalCells, flatPosition } from './chunk-L65CRXTV.js';
|
|
2
|
+
|
|
3
|
+
// src/core/cubeBuilder.ts
|
|
4
|
+
var CubeBuilderError = class extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "CubeBuilderError";
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
function resolveDimension(id, column) {
|
|
11
|
+
let categories;
|
|
12
|
+
if (column.categoryOrder && column.categoryOrder.length > 0) {
|
|
13
|
+
const known = new Set(column.categoryOrder);
|
|
14
|
+
for (const v of column.values) {
|
|
15
|
+
if (!known.has(v)) {
|
|
16
|
+
throw new CubeBuilderError(
|
|
17
|
+
`Dimension "${id}": value "${v}" not present in categoryOrder`
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
categories = column.categoryOrder.slice();
|
|
22
|
+
} else {
|
|
23
|
+
const seen = /* @__PURE__ */ new Set();
|
|
24
|
+
categories = [];
|
|
25
|
+
for (const v of column.values) {
|
|
26
|
+
if (v == null) {
|
|
27
|
+
throw new CubeBuilderError(
|
|
28
|
+
`Dimension "${id}": contains a null category value; dimensions cannot be null`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
if (!seen.has(v)) {
|
|
32
|
+
seen.add(v);
|
|
33
|
+
categories.push(v);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const position = /* @__PURE__ */ new Map();
|
|
38
|
+
categories.forEach((c, i) => position.set(c, i));
|
|
39
|
+
return { id, column, categories, position };
|
|
40
|
+
}
|
|
41
|
+
function buildDimensionBlock(resolved) {
|
|
42
|
+
const dimension = {};
|
|
43
|
+
const size = [];
|
|
44
|
+
for (const r of resolved) {
|
|
45
|
+
const cat = {};
|
|
46
|
+
cat.index = r.categories.slice();
|
|
47
|
+
if (r.column.categoryLabels) cat.label = { ...r.column.categoryLabels };
|
|
48
|
+
if (r.column.categoryUnits) cat.unit = { ...r.column.categoryUnits };
|
|
49
|
+
if (r.column.categoryCoordinates)
|
|
50
|
+
cat.coordinates = { ...r.column.categoryCoordinates };
|
|
51
|
+
if (r.column.categoryChild) cat.child = { ...r.column.categoryChild };
|
|
52
|
+
const dim = { category: cat };
|
|
53
|
+
if (r.column.label) dim.label = r.column.label;
|
|
54
|
+
if (r.column.href) dim.href = r.column.href;
|
|
55
|
+
dimension[r.id] = dim;
|
|
56
|
+
size.push(r.categories.length);
|
|
57
|
+
}
|
|
58
|
+
return { dimension, size };
|
|
59
|
+
}
|
|
60
|
+
function scatterValues(obs, resolved, size) {
|
|
61
|
+
const total = totalCells(size);
|
|
62
|
+
const dense = new Array(total).fill(null);
|
|
63
|
+
const seen = new Uint8Array(total);
|
|
64
|
+
let duplicates = 0;
|
|
65
|
+
const n = obs.measure.values.length;
|
|
66
|
+
const k = resolved.length;
|
|
67
|
+
for (let row = 0; row < n; row++) {
|
|
68
|
+
const indices = new Array(k);
|
|
69
|
+
for (let d = 0; d < k; d++) {
|
|
70
|
+
const r = resolved[d];
|
|
71
|
+
const v = r.column.values[row];
|
|
72
|
+
const p = r.position.get(v);
|
|
73
|
+
if (p === void 0) {
|
|
74
|
+
throw new CubeBuilderError(
|
|
75
|
+
`Dimension "${r.id}": value "${v}" has no position (row ${row})`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
indices[d] = p;
|
|
79
|
+
}
|
|
80
|
+
const pos = flatPosition(indices, size);
|
|
81
|
+
if (seen[pos] === 1) duplicates++;
|
|
82
|
+
seen[pos] = 1;
|
|
83
|
+
dense[pos] = obs.measure.values[row];
|
|
84
|
+
}
|
|
85
|
+
return { dense, duplicates };
|
|
86
|
+
}
|
|
87
|
+
function chooseValueForm(dense, requested, threshold) {
|
|
88
|
+
const total = dense.length;
|
|
89
|
+
const nullCount = dense.reduce((acc, v) => acc + (v === null ? 1 : 0), 0);
|
|
90
|
+
const nullRatio = total === 0 ? 0 : nullCount / total;
|
|
91
|
+
let form;
|
|
92
|
+
if (requested === "dense") form = "dense";
|
|
93
|
+
else if (requested === "sparse") form = "sparse";
|
|
94
|
+
else form = nullRatio > threshold ? "sparse" : "dense";
|
|
95
|
+
if (form === "dense") {
|
|
96
|
+
return { value: dense, form, nullRatio };
|
|
97
|
+
}
|
|
98
|
+
const obj = {};
|
|
99
|
+
for (let i = 0; i < total; i++) {
|
|
100
|
+
const v = dense[i];
|
|
101
|
+
if (v !== null) obj[String(i)] = v;
|
|
102
|
+
}
|
|
103
|
+
return { value: obj, form, nullRatio };
|
|
104
|
+
}
|
|
105
|
+
function emitStatus(perCell, requested) {
|
|
106
|
+
if (requested === "none") return void 0;
|
|
107
|
+
const total = perCell.length;
|
|
108
|
+
if (total === 0) return void 0;
|
|
109
|
+
const first = perCell[0];
|
|
110
|
+
let uniform = true;
|
|
111
|
+
const distinct = /* @__PURE__ */ new Set();
|
|
112
|
+
for (const s of perCell) {
|
|
113
|
+
distinct.add(s);
|
|
114
|
+
if (s !== first) uniform = false;
|
|
115
|
+
}
|
|
116
|
+
if (requested === "string") return first;
|
|
117
|
+
if (requested === "array") return perCell.slice();
|
|
118
|
+
if (requested === "object") {
|
|
119
|
+
return sparseStatus(perCell);
|
|
120
|
+
}
|
|
121
|
+
if (uniform && first !== "") return first;
|
|
122
|
+
const counts = /* @__PURE__ */ new Map();
|
|
123
|
+
for (const s of perCell) counts.set(s, (counts.get(s) ?? 0) + 1);
|
|
124
|
+
let dominant = "";
|
|
125
|
+
let dominantCount = -1;
|
|
126
|
+
for (const [s, c] of counts) {
|
|
127
|
+
if (c > dominantCount) {
|
|
128
|
+
dominant = s;
|
|
129
|
+
dominantCount = c;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const nonDominant = total - dominantCount;
|
|
133
|
+
if (dominant !== "" && nonDominant / total < 0.4) {
|
|
134
|
+
return sparseStatus(perCell, dominant);
|
|
135
|
+
}
|
|
136
|
+
return perCell.slice();
|
|
137
|
+
}
|
|
138
|
+
function sparseStatus(perCell, defaultStatus) {
|
|
139
|
+
const obj = {};
|
|
140
|
+
for (let i = 0; i < perCell.length; i++) {
|
|
141
|
+
const s = perCell[i];
|
|
142
|
+
if (s === "" || s === defaultStatus) continue;
|
|
143
|
+
obj[String(i)] = s;
|
|
144
|
+
}
|
|
145
|
+
return obj;
|
|
146
|
+
}
|
|
147
|
+
function validate(obs, resolved) {
|
|
148
|
+
const n = obs.measure.values.length;
|
|
149
|
+
for (const r of resolved) {
|
|
150
|
+
if (r.column.values.length !== n) {
|
|
151
|
+
throw new CubeBuilderError(
|
|
152
|
+
`Dimension "${r.id}": ${r.column.values.length} values \u2260 ${n} measure values`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (obs.status && obs.status.values.length !== n) {
|
|
157
|
+
throw new CubeBuilderError(
|
|
158
|
+
`Status column: ${obs.status.values.length} values \u2260 ${n} measure values`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
const modelIds = new Set(obs.model.dimensionIds);
|
|
162
|
+
const colIds = new Set(Object.keys(obs.dimensions));
|
|
163
|
+
for (const id of modelIds) {
|
|
164
|
+
if (!colIds.has(id)) {
|
|
165
|
+
throw new CubeBuilderError(`model.dimensionIds lists "${id}" but no column exists`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
for (const id of colIds) {
|
|
169
|
+
if (!modelIds.has(id)) {
|
|
170
|
+
throw new CubeBuilderError(`Column "${id}" is not listed in model.dimensionIds`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function buildDataset(obs, options = {}) {
|
|
175
|
+
const model = obs.model;
|
|
176
|
+
const dimensionIds = model.dimensionIds;
|
|
177
|
+
const resolved = dimensionIds.map((id) => {
|
|
178
|
+
const col = obs.dimensions[id];
|
|
179
|
+
if (!col) throw new CubeBuilderError(`Missing dimension column "${id}"`);
|
|
180
|
+
return resolveDimension(id, col);
|
|
181
|
+
});
|
|
182
|
+
validate(obs, resolved);
|
|
183
|
+
const { dimension, size } = buildDimensionBlock(resolved);
|
|
184
|
+
const { dense, duplicates } = scatterValues(obs, resolved, size);
|
|
185
|
+
const valueForm = options.valueForm ?? model.valueForm ?? "auto";
|
|
186
|
+
const threshold = options.sparseThreshold ?? model.sparseThreshold ?? 0.5;
|
|
187
|
+
const { value, form, nullRatio } = chooseValueForm(dense, valueForm, threshold);
|
|
188
|
+
let status;
|
|
189
|
+
if (obs.status) {
|
|
190
|
+
const statusForm = options.statusForm ?? model.statusForm ?? "auto";
|
|
191
|
+
if (statusForm !== "none") {
|
|
192
|
+
status = buildPerCellStatus(obs, resolved, size, statusForm);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const dataset = {
|
|
196
|
+
version: "2.0",
|
|
197
|
+
class: "dataset",
|
|
198
|
+
id: dimensionIds.slice(),
|
|
199
|
+
size,
|
|
200
|
+
dimension,
|
|
201
|
+
value
|
|
202
|
+
};
|
|
203
|
+
const role = filterRoles(model.roles);
|
|
204
|
+
if (role) dataset.role = role;
|
|
205
|
+
const meta = options.meta ?? model.meta;
|
|
206
|
+
if (meta) {
|
|
207
|
+
if (meta.label) dataset.label = meta.label;
|
|
208
|
+
if (meta.source) dataset.source = meta.source;
|
|
209
|
+
if (meta.updated) dataset.updated = meta.updated;
|
|
210
|
+
if (meta.extension) dataset.extension = meta.extension;
|
|
211
|
+
}
|
|
212
|
+
if (status) dataset.status = status;
|
|
213
|
+
return {
|
|
214
|
+
dataset,
|
|
215
|
+
diagnostics: {
|
|
216
|
+
valueForm: form,
|
|
217
|
+
nullRatio,
|
|
218
|
+
duplicates,
|
|
219
|
+
cellCount: dense.length
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function buildPerCellStatus(obs, resolved, size, statusForm) {
|
|
224
|
+
const total = totalCells(size);
|
|
225
|
+
const perCell = new Array(total).fill("");
|
|
226
|
+
const n = obs.measure.values.length;
|
|
227
|
+
const k = resolved.length;
|
|
228
|
+
const statusVals = obs.status?.values;
|
|
229
|
+
if (!statusVals) return void 0;
|
|
230
|
+
for (let row = 0; row < n; row++) {
|
|
231
|
+
const indices = new Array(k);
|
|
232
|
+
for (let d = 0; d < k; d++) {
|
|
233
|
+
indices[d] = resolved[d].position.get(resolved[d].column.values[row]);
|
|
234
|
+
}
|
|
235
|
+
const pos = flatPosition(indices, size);
|
|
236
|
+
perCell[pos] = statusVals[row] ?? "";
|
|
237
|
+
}
|
|
238
|
+
return emitStatus(perCell, statusForm);
|
|
239
|
+
}
|
|
240
|
+
function filterRoles(roles) {
|
|
241
|
+
if (!roles) return void 0;
|
|
242
|
+
const out = {};
|
|
243
|
+
let any = false;
|
|
244
|
+
for (const key of ["time", "geo", "metric"]) {
|
|
245
|
+
const arr = roles[key];
|
|
246
|
+
if (arr && arr.length > 0) {
|
|
247
|
+
out[key] = arr.slice();
|
|
248
|
+
any = true;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return any ? out : void 0;
|
|
252
|
+
}
|
|
253
|
+
function toDataset(obs, options) {
|
|
254
|
+
return buildDataset(obs, options).dataset;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export { CubeBuilderError, buildDataset, toDataset };
|
|
258
|
+
//# sourceMappingURL=chunk-2HY7NRKX.js.map
|
|
259
|
+
//# sourceMappingURL=chunk-2HY7NRKX.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/cubeBuilder.ts"],"names":[],"mappings":"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","file":"chunk-2HY7NRKX.js","sourcesContent":["/**\n * CubeBuilder — turns an [`Observations`](../model/ir.ts) IR into a JSON-stat\n * [`Dataset`](../model/jsonstat.ts).\n *\n * This is the central transform of the importer: a tidy long table → an\n * N-dimensional cube laid out in row-major order (\"what does not change, first\";\n * see wiki/format-specification.md).\n *\n * Responsibilities:\n * 1. Resolve dimension order and enumerate categories (first-seen order unless\n * `categoryOrder` pins it).\n * 2. Build the `dimension` block with `category.index`/`label`/`unit`/\n * `coordinates`/`child` and dimension `label`.\n * 3. Scatter the measure values into a row-major `value` array (dense) or a\n * sparse object, per [`CubeModel.valueForm`](../model/ir.ts) and the null\n * ratio threshold.\n * 4. Emit `status` in the most compact correct form (string/array/object).\n * 5. Attach `role`, and dataset-level `label`/`source`/`updated`/`extension`.\n *\n * Pure: no I/O, no side effects. All index math delegated to\n * [`strides`](./strides.ts).\n */\n\nimport type {\n DatasetMeta,\n DimensionColumn,\n Observations,\n} from \"../model/ir\";\nimport type {\n JsonStatCategory,\n JsonStatDataset,\n JsonStatDimension,\n JsonStatRole,\n JsonStatStatus,\n JsonStatValue,\n} from \"../model/jsonstat\";\nimport { enumerateCells, flatPosition, totalCells } from \"./strides\";\n\n// ---------------------------------------------------------------------------\n// Errors\n// ---------------------------------------------------------------------------\n\n/** Thrown when the IR violates a structural invariant the builder relies on. */\nexport class CubeBuilderError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"CubeBuilderError\";\n }\n}\n\n// ---------------------------------------------------------------------------\n// Options\n// ---------------------------------------------------------------------------\n\nexport interface BuildOptions {\n /** Override the emitted value form. Defaults to `model.valueForm ?? \"auto\"`. */\n valueForm?: \"auto\" | \"dense\" | \"sparse\";\n /** Override the sparse threshold (null ratio). Defaults to 0.5. */\n sparseThreshold?: number;\n /** Override status emission. Defaults to `model.statusForm ?? \"auto\"`. */\n statusForm?: \"auto\" | \"array\" | \"string\" | \"object\" | \"none\";\n /** Override dataset metadata. Defaults to `model.meta`. */\n meta?: DatasetMeta;\n}\n\n// ---------------------------------------------------------------------------\n// Category resolution\n// ---------------------------------------------------------------------------\n\n/**\n * Resolved view of a dimension: the ordered category IDs and a map from\n * category ID → its position (0-based). The position is what gets multiplied\n * by the dimension's stride when scattering values.\n */\ninterface ResolvedDimension {\n id: string;\n column: DimensionColumn;\n categories: string[];\n position: Map<string, number>;\n}\n\nfunction resolveDimension(id: string, column: DimensionColumn): ResolvedDimension {\n let categories: string[];\n if (column.categoryOrder && column.categoryOrder.length > 0) {\n // Honor an explicit order, but verify every observed value is covered.\n const known = new Set(column.categoryOrder);\n for (const v of column.values) {\n if (!known.has(v)) {\n throw new CubeBuilderError(\n `Dimension \"${id}\": value \"${v}\" not present in categoryOrder`,\n );\n }\n }\n categories = column.categoryOrder.slice();\n } else {\n // First-seen order, deduplicated.\n const seen = new Set<string>();\n categories = [];\n for (const v of column.values) {\n if (v == null) {\n throw new CubeBuilderError(\n `Dimension \"${id}\": contains a null category value; dimensions cannot be null`,\n );\n }\n if (!seen.has(v)) {\n seen.add(v);\n categories.push(v);\n }\n }\n }\n const position = new Map<string, number>();\n categories.forEach((c, i) => position.set(c, i));\n return { id, column, categories, position };\n}\n\n// ---------------------------------------------------------------------------\n// Dimension block\n// ---------------------------------------------------------------------------\n\n/** Build the JSON-stat `dimension` object and the `size` array. */\nfunction buildDimensionBlock(\n resolved: ResolvedDimension[],\n): { dimension: Record<string, JsonStatDimension>; size: number[] } {\n const dimension: Record<string, JsonStatDimension> = {};\n const size: number[] = [];\n for (const r of resolved) {\n const cat: JsonStatCategory = {};\n // index: array form is the canonical, simplest representation.\n cat.index = r.categories.slice();\n if (r.column.categoryLabels) cat.label = { ...r.column.categoryLabels };\n if (r.column.categoryUnits) cat.unit = { ...r.column.categoryUnits };\n if (r.column.categoryCoordinates)\n cat.coordinates = { ...r.column.categoryCoordinates };\n if (r.column.categoryChild) cat.child = { ...r.column.categoryChild };\n\n const dim: JsonStatDimension = { category: cat };\n if (r.column.label) dim.label = r.column.label;\n if (r.column.href) dim.href = r.column.href;\n dimension[r.id] = dim;\n size.push(r.categories.length);\n }\n return { dimension, size };\n}\n\n// ---------------------------------------------------------------------------\n// Value scattering (long table → row-major cube)\n// ---------------------------------------------------------------------------\n\n/**\n * Scatter measure values into a dense array of length `total` (null-filled)\n * using row-major positions computed from the resolved dimensions.\n *\n * If two observations map to the same cell, the *last* one wins and a warning\n * is surfaced via the returned diagnostics (the cube model assumes one value\n * per cell; duplicates indicate an ill-formed source).\n */\nfunction scatterValues(\n obs: Observations,\n resolved: ResolvedDimension[],\n size: number[],\n): { dense: (number | null)[]; duplicates: number } {\n const total = totalCells(size);\n const dense: (number | null)[] = new Array(total).fill(null);\n const seen = new Uint8Array(total); // 0 = empty, 1 = filled\n let duplicates = 0;\n const n = obs.measure.values.length;\n const k = resolved.length;\n for (let row = 0; row < n; row++) {\n const indices = new Array<number>(k);\n for (let d = 0; d < k; d++) {\n const r = resolved[d];\n const v = r.column.values[row];\n const p = r.position.get(v);\n if (p === undefined) {\n // resolveDimension guarantees coverage when categoryOrder is used, but\n // guard regardless.\n throw new CubeBuilderError(\n `Dimension \"${r.id}\": value \"${v}\" has no position (row ${row})`,\n );\n }\n indices[d] = p;\n }\n const pos = flatPosition(indices, size);\n if (seen[pos] === 1) duplicates++;\n seen[pos] = 1;\n dense[pos] = obs.measure.values[row];\n }\n return { dense, duplicates };\n}\n\n// ---------------------------------------------------------------------------\n// Value form selection (dense vs sparse)\n// ---------------------------------------------------------------------------\n\n/**\n * Decide between dense (array) and sparse (object) `value` representation.\n *\n * Per wiki/sparse-cubes.md: sparse is preferred when many cells are null. The\n * threshold is the null ratio above which the object form is more compact.\n */\nfunction chooseValueForm(\n dense: (number | null)[],\n requested: \"auto\" | \"dense\" | \"sparse\",\n threshold: number,\n): { value: JsonStatValue; form: \"dense\" | \"sparse\"; nullRatio: number } {\n const total = dense.length;\n const nullCount = dense.reduce<number>((acc, v) => acc + (v === null ? 1 : 0), 0);\n const nullRatio = total === 0 ? 0 : nullCount / total;\n\n let form: \"dense\" | \"sparse\";\n if (requested === \"dense\") form = \"dense\";\n else if (requested === \"sparse\") form = \"sparse\";\n else form = nullRatio > threshold ? \"sparse\" : \"dense\";\n\n if (form === \"dense\") {\n return { value: dense, form, nullRatio };\n }\n // Sparse object: only non-null cells, keyed by stringified flat position.\n const obj: Record<string, number> = {};\n for (let i = 0; i < total; i++) {\n const v = dense[i];\n if (v !== null) obj[String(i)] = v;\n }\n return { value: obj, form, nullRatio };\n}\n\n// ---------------------------------------------------------------------------\n// Status emission\n// ---------------------------------------------------------------------------\n\n/**\n * Emit `status` in the most compact correct form.\n *\n * - If all statuses are identical and non-empty → string form.\n * - Else if few distinct non-default statuses → object form (sparse status).\n * - Else → array form (one entry per cell, aligned with dense `value`).\n *\n * The caller passes the *dense* per-cell status array (length = total cells).\n */\nfunction emitStatus(\n perCell: string[],\n requested: \"auto\" | \"array\" | \"string\" | \"object\" | \"none\",\n): JsonStatStatus | undefined {\n if (requested === \"none\") return undefined;\n const total = perCell.length;\n if (total === 0) return undefined;\n\n // Collect distinct statuses and whether they're uniform.\n const first = perCell[0];\n let uniform = true;\n const distinct = new Set<string>();\n for (const s of perCell) {\n distinct.add(s);\n if (s !== first) uniform = false;\n }\n\n if (requested === \"string\") return first; // caller's explicit choice\n if (requested === \"array\") return perCell.slice();\n\n if (requested === \"object\") {\n return sparseStatus(perCell);\n }\n\n // auto:\n if (uniform && first !== \"\") return first;\n // If most cells share the dominant status, sparse object form is compact.\n const counts = new Map<string, number>();\n for (const s of perCell) counts.set(s, (counts.get(s) ?? 0) + 1);\n let dominant = \"\";\n let dominantCount = -1;\n for (const [s, c] of counts) {\n if (c > dominantCount) {\n dominant = s;\n dominantCount = c;\n }\n }\n // Use object form when fewer than ~40% of cells deviate from the dominant.\n const nonDominant = total - dominantCount;\n if (dominant !== \"\" && nonDominant / total < 0.4) {\n return sparseStatus(perCell, dominant);\n }\n return perCell.slice();\n}\n\n/** Build the sparse object status form. Omits `defaultStatus` entries. */\nfunction sparseStatus(\n perCell: string[],\n defaultStatus?: string,\n): Record<string, string> {\n const obj: Record<string, string> = {};\n for (let i = 0; i < perCell.length; i++) {\n const s = perCell[i];\n if (s === \"\" || s === defaultStatus) continue;\n obj[String(i)] = s;\n }\n return obj;\n}\n\n// ---------------------------------------------------------------------------\n// Validation\n// ---------------------------------------------------------------------------\n\n/** Validate the structural invariants the builder relies on. */\nfunction validate(obs: Observations, resolved: ResolvedDimension[]): void {\n const n = obs.measure.values.length;\n for (const r of resolved) {\n if (r.column.values.length !== n) {\n throw new CubeBuilderError(\n `Dimension \"${r.id}\": ${r.column.values.length} values ≠ ${n} measure values`,\n );\n }\n }\n if (obs.status && obs.status.values.length !== n) {\n throw new CubeBuilderError(\n `Status column: ${obs.status.values.length} values ≠ ${n} measure values`,\n );\n }\n // Every model.dimensionIds entry must have a column, and vice versa.\n const modelIds = new Set(obs.model.dimensionIds);\n const colIds = new Set(Object.keys(obs.dimensions));\n for (const id of modelIds) {\n if (!colIds.has(id)) {\n throw new CubeBuilderError(`model.dimensionIds lists \"${id}\" but no column exists`);\n }\n }\n for (const id of colIds) {\n if (!modelIds.has(id)) {\n throw new CubeBuilderError(`Column \"${id}\" is not listed in model.dimensionIds`);\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Build result & public API\n// ---------------------------------------------------------------------------\n\nexport interface BuildResult {\n dataset: JsonStatDataset;\n /** Diagnostics from the build (non-fatal observations). */\n diagnostics: {\n valueForm: \"dense\" | \"sparse\";\n nullRatio: number;\n duplicates: number;\n cellCount: number;\n };\n}\n\n/**\n * Build a JSON-stat [`Dataset`](../model/jsonstat.ts) from an\n * [`Observations`](../model/ir.ts) IR.\n *\n * @throws [`CubeBuilderError`](#cubebuildererror) on invariant violations.\n */\nexport function buildDataset(obs: Observations, options: BuildOptions = {}): BuildResult {\n const model = obs.model;\n const dimensionIds = model.dimensionIds;\n\n // Resolve dimensions in the model-declared order (this defines stride order).\n const resolved = dimensionIds.map((id) => {\n const col = obs.dimensions[id];\n if (!col) throw new CubeBuilderError(`Missing dimension column \"${id}\"`);\n return resolveDimension(id, col);\n });\n\n validate(obs, resolved);\n\n const { dimension, size } = buildDimensionBlock(resolved);\n\n // Scatter measure values into the dense row-major array.\n const { dense, duplicates } = scatterValues(obs, resolved, size);\n\n // Choose value form.\n const valueForm = options.valueForm ?? model.valueForm ?? \"auto\";\n const threshold = options.sparseThreshold ?? model.sparseThreshold ?? 0.5;\n const { value, form, nullRatio } = chooseValueForm(dense, valueForm, threshold);\n\n // Status: if present, build a dense per-cell status array aligned with `value`.\n let status: JsonStatStatus | undefined;\n if (obs.status) {\n const statusForm = options.statusForm ?? model.statusForm ?? \"auto\";\n if (statusForm !== \"none\") {\n status = buildPerCellStatus(obs, resolved, size, statusForm);\n }\n }\n\n const dataset: JsonStatDataset = {\n version: \"2.0\",\n class: \"dataset\",\n id: dimensionIds.slice(),\n size,\n dimension,\n value,\n };\n\n // Roles (only attach defined role keys, and only if non-empty).\n const role = filterRoles(model.roles);\n if (role) dataset.role = role;\n\n // Dataset-level metadata.\n const meta = options.meta ?? model.meta;\n if (meta) {\n if (meta.label) dataset.label = meta.label;\n if (meta.source) dataset.source = meta.source;\n if (meta.updated) dataset.updated = meta.updated;\n if (meta.extension) dataset.extension = meta.extension;\n }\n\n if (status) dataset.status = status;\n\n return {\n dataset,\n diagnostics: {\n valueForm: form,\n nullRatio,\n duplicates,\n cellCount: dense.length,\n },\n };\n}\n\n/**\n * Build a per-cell status array by scattering the status column into row-major\n * order, then reduce to the requested form. Cells with no observation default\n * to \"\" (empty), which the emitter treats as \"no status\".\n */\nfunction buildPerCellStatus(\n obs: Observations,\n resolved: ResolvedDimension[],\n size: number[],\n statusForm: \"auto\" | \"array\" | \"string\" | \"object\" | \"none\",\n): JsonStatStatus | undefined {\n const total = totalCells(size);\n const perCell: string[] = new Array(total).fill(\"\");\n const n = obs.measure.values.length;\n const k = resolved.length;\n const statusVals = obs.status?.values;\n if (!statusVals) return undefined;\n for (let row = 0; row < n; row++) {\n const indices = new Array<number>(k);\n for (let d = 0; d < k; d++) {\n indices[d] = resolved[d].position.get(resolved[d].column.values[row])!;\n }\n const pos = flatPosition(indices, size);\n perCell[pos] = statusVals[row] ?? \"\";\n }\n return emitStatus(perCell, statusForm);\n}\n\n/** Return a role object with only non-empty arrays, or undefined if all empty. */\nfunction filterRoles(roles?: JsonStatRole): JsonStatRole | undefined {\n if (!roles) return undefined;\n const out: JsonStatRole = {};\n let any = false;\n for (const key of [\"time\", \"geo\", \"metric\"] as const) {\n const arr = roles[key];\n if (arr && arr.length > 0) {\n out[key] = arr.slice();\n any = true;\n }\n }\n return any ? out : undefined;\n}\n\n/**\n * Convenience: build and return *just* the dataset (no diagnostics).\n * Useful for the common programmatic case.\n */\nexport function toDataset(obs: Observations, options?: BuildOptions): JsonStatDataset {\n return buildDataset(obs, options).dataset;\n}\n\n// Re-export for consumers who want to enumerate cells themselves.\nexport { enumerateCells };\n"]}
|