@platforma-sdk/model 1.27.17 → 1.28.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/src/render/api.ts CHANGED
@@ -1,10 +1,7 @@
1
1
  import type {
2
2
  AnyFunction,
3
- AxisFilter,
4
3
  AxisId,
5
- BinaryPartitionedDataInfoEntries,
6
4
  DataInfo,
7
- JsonPartitionedDataInfoEntries,
8
5
  Option,
9
6
  PColumn,
10
7
  PColumnSelector,
@@ -20,19 +17,15 @@ import type {
20
17
  PTableHandle,
21
18
  PTableRecordFilter,
22
19
  PTableSorting,
23
- PValue,
24
20
  PlRef,
25
21
  ResultCollection,
26
22
  SUniversalPColumnId,
27
- ValueOrError
23
+ ValueOrError,
28
24
  } from '@milaboratories/pl-model-common';
29
25
  import {
30
26
  AnchoredIdDeriver,
31
- canonicalizeAxisId,
32
27
  ensurePColumn,
33
- entriesToDataInfo,
34
28
  extractAllColumns,
35
- getAxisId,
36
29
  isDataInfo,
37
30
  isPColumn,
38
31
  isPColumnSpec,
@@ -41,7 +34,6 @@ import {
41
34
  mapPObjectData,
42
35
  mapPTableDef,
43
36
  mapValueInVOE,
44
- resolveAnchors,
45
37
  selectorsToPredicate,
46
38
  } from '@milaboratories/pl-model-common';
47
39
  import type { Optional } from 'utility-types';
@@ -50,11 +42,11 @@ import { TreeNodeAccessor, ifDef } from './accessor';
50
42
  import type { FutureRef } from './future';
51
43
  import type { AccessorHandle, GlobalCfgRenderCtx } from './internal';
52
44
  import { MainAccessorName, StagingAccessorName } from './internal';
53
- import type { APColumnSelectorWithSplit } from './split_selectors';
54
- import { filterDataInfoEntries } from './util/axis_filtering';
55
- import type { LabelDerivationOps, TraceEntry } from './util/label';
45
+ import type { LabelDerivationOps } from './util/label';
46
+ import { PColumnCollection, type AxisLabelProvider, type ColumnProvider } from './util/column_collection';
56
47
  import { deriveLabels } from './util/label';
57
- import { getUniquePartitionKeys, parsePColumnData } from './util/pcolumn_data';
48
+ import type { APColumnSelectorWithSplit } from './util/split_selectors';
49
+ import canonicalize from 'canonicalize';
58
50
 
59
51
  /**
60
52
  * Helper function to match domain objects
@@ -91,56 +83,12 @@ PColumn<PColumnValues | AccessorHandle | DataInfo<AccessorHandle>> {
91
83
  });
92
84
  }
93
85
 
94
- /**
95
- * Describes a single filter applied due to a split axis.
96
- */
97
- export type AxisFilterInfo = {
98
- axisIdx: number;
99
- axisId: AxisId;
100
- value: PValue;
101
- label: string;
102
- };
103
-
104
- /**
105
- * Represents a column specification with potential split axis filtering information
106
- * used in canonical options generation.
107
- */
108
- export type UniversalPColumnEntry = {
109
- id: SUniversalPColumnId;
110
- obj: PColumnSpec;
111
- ref: PlRef;
112
- axisFilters?: AxisFilterInfo[];
113
- label: string;
114
- };
115
-
116
- /**
117
- * Converts an array of SplitAxisFilter objects into an array of TraceEntry objects
118
- * suitable for label generation.
119
- */
120
- function splitFiltersToTrace(splitFilters?: AxisFilterInfo[]): TraceEntry[] | undefined {
121
- if (!splitFilters) return undefined;
122
- return splitFilters.map((filter) => ({
123
- type: `split:${canonicalizeAxisId(filter.axisId)}`,
124
- label: filter.label,
125
- importance: 1_000_000, // High importance for split filters in labels
126
- }));
127
- }
128
-
129
- /**
130
- * Converts an array of SplitAxisFilter objects into an array of AxisFilter tuples
131
- * suitable for deriving anchored IDs.
132
- */
133
- function splitFiltersToAxisFilter(splitFilters?: AxisFilterInfo[]): AxisFilter[] | undefined {
134
- if (!splitFilters) return undefined;
135
- return splitFilters.map((filter) => [filter.axisIdx, filter.value]);
136
- }
137
-
138
86
  type UniversalPColumnOpts = {
139
87
  labelOps?: LabelDerivationOps;
140
88
  dontWaitAllData?: boolean;
141
89
  };
142
90
 
143
- export class ResultPool {
91
+ export class ResultPool implements ColumnProvider, AxisLabelProvider {
144
92
  private readonly ctx: GlobalCfgRenderCtx = getCfgRenderCtx();
145
93
 
146
94
  /**
@@ -170,165 +118,20 @@ export class ResultPool {
170
118
  }));
171
119
  }
172
120
 
173
- /**
174
- * Internal implementation that generates UniversalPColumnEntry objects from the provided
175
- * anchors and selectors.
176
- */
177
- public getUniversalPColumnEntries(
178
- anchorsOrCtx: AnchoredIdDeriver | Record<string, PColumnSpec | PlRef>,
179
- predicateOrSelectors: ((spec: PColumnSpec) => boolean) | APColumnSelectorWithSplit | APColumnSelectorWithSplit[],
180
- opts?: UniversalPColumnOpts,
181
- ): UniversalPColumnEntry[] | undefined {
182
- // Handle PlRef objects by resolving them to PColumnSpec
121
+ public resolveAnchorCtx(anchorsOrCtx: AnchoredIdDeriver | Record<string, PColumnSpec | PlRef>): AnchoredIdDeriver | undefined {
122
+ if (anchorsOrCtx instanceof AnchoredIdDeriver) return anchorsOrCtx;
183
123
  const resolvedAnchors: Record<string, PColumnSpec> = {};
184
-
185
- if (!(anchorsOrCtx instanceof AnchoredIdDeriver)) {
186
- for (const [key, value] of Object.entries(anchorsOrCtx)) {
187
- if (isPlRef(value)) {
188
- const resolvedSpec = this.getPColumnSpecByRef(value);
189
- if (!resolvedSpec)
190
- return undefined;
191
- resolvedAnchors[key] = resolvedSpec;
192
- } else {
193
- // It's already a PColumnSpec
194
- resolvedAnchors[key] = value;
195
- }
196
- }
197
- }
198
-
199
- const selectorsArray = typeof predicateOrSelectors === 'function'
200
- ? [predicateOrSelectors]
201
- : Array.isArray(predicateOrSelectors)
202
- ? predicateOrSelectors
203
- : [predicateOrSelectors];
204
-
205
- const anchorIdDeriver = anchorsOrCtx instanceof AnchoredIdDeriver
206
- ? anchorsOrCtx
207
- : new AnchoredIdDeriver(resolvedAnchors);
208
-
209
- const result: Omit<UniversalPColumnEntry, 'id' | 'label'>[] = [];
210
-
211
- // Process each selector individually
212
- for (const selector of selectorsArray) {
213
- // Create predicate for this specific selector
214
- const predicate = typeof selector === 'function'
215
- ? selector
216
- : selectorsToPredicate(resolveAnchors(resolvedAnchors, selector));
217
-
218
- // Filter specs based on this specific predicate
219
- const filtered = this.getSpecs().entries.filter(({ obj: spec }) => {
220
- if (!isPColumnSpec(spec)) return false;
221
- return predicate(spec);
222
- });
223
-
224
- if (filtered.length === 0)
225
- continue;
226
-
227
- // Check if this selector has any split axes
228
- const splitAxisIdxs = typeof selector === 'object'
229
- && 'axes' in selector
230
- && selector.axes !== undefined
231
- && selector.partialAxesMatch === undefined
232
- ? selector.axes
233
- .map((axis, index) => ('split' in axis && axis.split === true) ? index : -1)
234
- .filter((index) => index !== -1)
235
- : [];
236
- splitAxisIdxs.sort((a, b) => a - b);
237
-
238
- if (splitAxisIdxs.length > 0) { // Handle split axes
239
- const maxSplitIdx = splitAxisIdxs[splitAxisIdxs.length - 1]; // Last one is max since they're sorted
240
-
241
- for (const { ref, obj: spec } of filtered) {
242
- if (!isPColumnSpec(spec)) throw new Error(`Assertion failed: expected PColumnSpec, got ${spec.kind}`);
243
-
244
- const columnData = this.getDataByRef(ref);
245
- if (!columnData) {
246
- if (opts?.dontWaitAllData) continue;
247
- return undefined;
248
- }
249
- if (!isPColumn(columnData)) throw new Error(`Assertion failed: expected PColumn, got ${columnData.spec.kind}`);
250
-
251
- const uniqueKeys = getUniquePartitionKeys(columnData.data);
252
- if (!uniqueKeys) {
253
- if (opts?.dontWaitAllData) continue;
254
- return undefined;
255
- }
256
-
257
- if (maxSplitIdx >= uniqueKeys.length)
258
- throw new Error(`Not enough partition keys for the requested split axes in column ${spec.name}`);
259
-
260
- // Pre-fetch labels for all involved split axes
261
- const axesLabels: (Record<string | number, string> | undefined)[] = splitAxisIdxs
262
- .map((idx) => this.findLabels(getAxisId(spec.axesSpec[idx])));
263
-
264
- const keyCombinations: (string | number)[][] = [];
265
- const generateCombinations = (currentCombo: (string | number)[], sAxisIdx: number) => {
266
- if (sAxisIdx >= splitAxisIdxs.length) {
267
- keyCombinations.push([...currentCombo]);
268
- return;
269
- }
270
- const axisIdx = splitAxisIdxs[sAxisIdx];
271
- const axisValues = uniqueKeys[axisIdx];
272
- for (const val of axisValues) {
273
- currentCombo.push(val);
274
- generateCombinations(currentCombo, sAxisIdx + 1);
275
- currentCombo.pop();
276
- }
277
- };
278
- generateCombinations([], 0);
279
-
280
- // Generate entries for each key combination
281
- for (const keyCombo of keyCombinations) {
282
- const splitFilters: AxisFilterInfo[] = keyCombo.map((value, sAxisIdx) => {
283
- const axisIdx = splitAxisIdxs[sAxisIdx];
284
- const axisId = getAxisId(spec.axesSpec[axisIdx]);
285
- const axisLabelMap = axesLabels[sAxisIdx];
286
- const label = axisLabelMap?.[value] ?? String(value);
287
- return { axisIdx, axisId, value: value as PValue, label };
288
- });
289
-
290
- result.push({
291
- obj: spec,
292
- ref,
293
- axisFilters: splitFilters,
294
- });
295
- }
296
- }
124
+ for (const [key, value] of Object.entries(anchorsOrCtx)) {
125
+ if (isPlRef(value)) {
126
+ const resolvedSpec = this.getPColumnSpecByRef(value);
127
+ if (!resolvedSpec)
128
+ return undefined;
129
+ resolvedAnchors[key] = resolvedSpec;
297
130
  } else {
298
- // No split axes, simply add each filtered item without filters
299
- for (const { ref, obj: spec } of filtered) {
300
- if (!isPColumnSpec(spec)) continue;
301
- result.push({
302
- obj: spec,
303
- ref,
304
- // No splitFilters needed here
305
- });
306
- }
131
+ resolvedAnchors[key] = value;
307
132
  }
308
133
  }
309
-
310
- if (result.length === 0)
311
- return [];
312
-
313
- const labelResults = deriveLabels(
314
- result,
315
- (o) => ({
316
- spec: o.obj,
317
- suffixTrace: splitFiltersToTrace(o.axisFilters), // Use helper function
318
- }),
319
- opts?.labelOps ?? {},
320
- );
321
-
322
- return labelResults.map((item) => ({
323
- id: anchorIdDeriver.deriveS(
324
- item.value.obj,
325
- splitFiltersToAxisFilter(item.value.axisFilters), // Use helper function
326
- ),
327
- obj: item.value.obj,
328
- ref: item.value.ref,
329
- axisFilters: item.value.axisFilters,
330
- label: item.label,
331
- }));
134
+ return new AnchoredIdDeriver(resolvedAnchors);
332
135
  }
333
136
 
334
137
  /**
@@ -343,83 +146,16 @@ export class ResultPool {
343
146
  anchorsOrCtx: AnchoredIdDeriver | Record<string, PColumnSpec | PlRef>,
344
147
  predicateOrSelectors: ((spec: PColumnSpec) => boolean) | APColumnSelectorWithSplit | APColumnSelectorWithSplit[],
345
148
  opts?: UniversalPColumnOpts,
346
- ): PColumn<DataInfo<TreeNodeAccessor>>[] | undefined {
347
- // Ensure includeNativeLabel is true in the labelOps
348
- const enhancedOpts: UniversalPColumnOpts = {
349
- ...opts,
350
- labelOps: {
351
- includeNativeLabel: true,
352
- ...(opts?.labelOps || {}),
353
- },
354
- };
355
-
356
- const entries = this.getUniversalPColumnEntries(
357
- anchorsOrCtx,
358
- predicateOrSelectors,
359
- enhancedOpts,
360
- );
361
-
362
- if (!entries || entries.length === 0) return undefined;
363
-
364
- const result: PColumn<DataInfo<TreeNodeAccessor>>[] = [];
365
-
366
- for (const entry of entries) {
367
- const columnData = this.getPColumnByRef(entry.ref);
368
- if (!columnData) return undefined;
369
-
370
- const parsedData = parsePColumnData(columnData.data);
371
- if (!parsedData) return undefined;
372
-
373
- let filteredEntries: JsonPartitionedDataInfoEntries<TreeNodeAccessor> | BinaryPartitionedDataInfoEntries<TreeNodeAccessor> = parsedData;
374
- let spec = { ...columnData.spec };
375
-
376
- if (entry.axisFilters && entry.axisFilters.length > 0) {
377
- const axisFiltersByIdx = entry.axisFilters.map((filter) => [
378
- filter.axisIdx,
379
- filter.value,
380
- ] as [number, PValue]);
381
-
382
- filteredEntries = filterDataInfoEntries(parsedData, axisFiltersByIdx);
383
-
384
- const axisIndicesToRemove = [...entry.axisFilters]
385
- .map((filter) => filter.axisIdx)
386
- .sort((a, b) => b - a);
387
-
388
- const newAxesSpec = [...spec.axesSpec];
389
- for (const idx of axisIndicesToRemove) {
390
- newAxesSpec.splice(idx, 1);
391
- }
392
-
393
- spec = { ...spec, axesSpec: newAxesSpec };
394
- }
395
-
396
- const dataInfo = entriesToDataInfo(filteredEntries);
397
-
398
- if (spec.annotations) {
399
- spec = {
400
- ...spec,
401
- annotations: {
402
- ...spec.annotations,
403
- 'pl7.app/label': entry.label,
404
- },
405
- };
406
- } else {
407
- spec = {
408
- ...spec,
409
- annotations: {
410
- 'pl7.app/label': entry.label,
411
- },
412
- };
413
- }
414
-
415
- result.push({
416
- id: entry.id as unknown as PObjectId,
417
- spec,
418
- data: dataInfo,
149
+ ): PColumn<DataInfo<TreeNodeAccessor> | TreeNodeAccessor>[] | undefined {
150
+ const anchorCtx = this.resolveAnchorCtx(anchorsOrCtx);
151
+ if (!anchorCtx) return undefined;
152
+ return new PColumnCollection()
153
+ .addColumnProvider(this)
154
+ .addAxisLabelProvider(this)
155
+ .getColumns(predicateOrSelectors, {
156
+ ...opts,
157
+ anchorCtx,
419
158
  });
420
- }
421
-
422
- return result;
423
159
  }
424
160
 
425
161
  /**
@@ -456,9 +192,16 @@ export class ResultPool {
456
192
  predicateOrSelectors: ((spec: PColumnSpec) => boolean) | APColumnSelectorWithSplit | APColumnSelectorWithSplit[],
457
193
  opts?: UniversalPColumnOpts,
458
194
  ): { label: string; value: SUniversalPColumnId }[] | undefined {
459
- const entries = this.getUniversalPColumnEntries(anchorsOrCtx, predicateOrSelectors, opts);
195
+ const anchorCtx = this.resolveAnchorCtx(anchorsOrCtx);
196
+ if (!anchorCtx) return undefined;
197
+ const entries = new PColumnCollection()
198
+ .addColumnProvider(this)
199
+ .addAxisLabelProvider(this)
200
+ .getUniversalEntries(predicateOrSelectors, {
201
+ ...opts,
202
+ anchorCtx,
203
+ });
460
204
  if (!entries) return undefined;
461
- // Generate final options using the entries from the helper method
462
205
  return entries.map((item) => ({
463
206
  value: item.id,
464
207
  label: item.label,
@@ -659,6 +402,46 @@ export class ResultPool {
659
402
  return undefined;
660
403
  }
661
404
 
405
+ /**
406
+ * Selects columns based on the provided selectors, returning PColumn objects
407
+ * with lazily loaded data.
408
+ *
409
+ * @param selectors - A predicate function, a single selector, or an array of selectors.
410
+ * @returns An array of PColumn objects matching the selectors. Data is loaded on first access.
411
+ */
412
+ public selectColumns(
413
+ selectors: ((spec: PColumnSpec) => boolean) | PColumnSelector | PColumnSelector[],
414
+ ): PColumn<TreeNodeAccessor | undefined>[] {
415
+ const predicate = typeof selectors === 'function' ? selectors : selectorsToPredicate(selectors);
416
+
417
+ const matchedSpecs = this.getSpecs().entries.filter(({ obj: spec }) => {
418
+ if (!isPColumnSpec(spec)) return false;
419
+ return predicate(spec);
420
+ });
421
+
422
+ // Map specs to PColumn objects with lazy data loading
423
+ return matchedSpecs.map(({ ref, obj: spec }) => {
424
+ // Type assertion needed because filter ensures it's PColumnSpec
425
+ const pcolumnSpec = spec as PColumnSpec;
426
+ let _cachedData: TreeNodeAccessor | undefined | null = null; // Use null to distinguish initial state from undefined result
427
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
428
+ const self = this; // Capture 'this' for use inside the getter
429
+
430
+ return {
431
+ id: canonicalize(ref) as PObjectId,
432
+ spec: pcolumnSpec,
433
+ get data(): TreeNodeAccessor | undefined {
434
+ if (_cachedData !== null) {
435
+ return _cachedData; // Return cached data (could be undefined if fetch failed)
436
+ }
437
+
438
+ _cachedData = self.getPColumnByRef(ref)?.data;
439
+ return _cachedData;
440
+ },
441
+ } satisfies PColumn<TreeNodeAccessor | undefined>; // Cast needed because 'data' is a getter
442
+ });
443
+ }
444
+
662
445
  /**
663
446
  * Find labels data for a given axis id of a p-column.
664
447
  * @returns a map of axis value => label
@@ -668,7 +451,7 @@ export class ResultPool {
668
451
  if (!labels) return undefined;
669
452
  return Object.fromEntries(column.data.listInputFields().map((field) => {
670
453
  const r = JSON.parse(field) as [string];
671
- return [r[axisIdx], labels[r[axisIdx]] ?? "Unlabelled"];
454
+ return [r[axisIdx], labels[r[axisIdx]] ?? 'Unlabelled'];
672
455
  }));
673
456
  }
674
457
  }
@@ -740,6 +523,7 @@ export class RenderCtx<Args, UiState> {
740
523
  // Removed redundant explicitColumns check
741
524
  }
742
525
 
526
+ // TODO remove all non-PColumn fields
743
527
  public createPFrame(def: PFrameDef<TreeNodeAccessor | PColumnValues | DataInfo<TreeNodeAccessor>>): PFrameHandle {
744
528
  this.verifyInlineAndExplicitColumnsSupport(def);
745
529
  return this.ctx.createPFrame(
@@ -747,6 +531,7 @@ export class RenderCtx<Args, UiState> {
747
531
  );
748
532
  }
749
533
 
534
+ // TODO remove all non-PColumn fields
750
535
  public createPTable(def: PTableDef<PColumn<TreeNodeAccessor | PColumnValues | DataInfo<TreeNodeAccessor>>>): PTableHandle;
751
536
  public createPTable(def: {
752
537
  columns: PColumn<TreeNodeAccessor | PColumnValues | DataInfo<TreeNodeAccessor>>[];