@platforma-sdk/model 1.27.10 → 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,5 +1,7 @@
1
1
  import type {
2
+ AnyFunction,
2
3
  AxisId,
4
+ DataInfo,
3
5
  Option,
4
6
  PColumn,
5
7
  PColumnSelector,
@@ -8,6 +10,7 @@ import type {
8
10
  PFrameDef,
9
11
  PFrameHandle,
10
12
  PObject,
13
+ PObjectId,
11
14
  PObjectSpec,
12
15
  PSpecPredicate,
13
16
  PTableDef,
@@ -16,28 +19,18 @@ import type {
16
19
  PTableSorting,
17
20
  PlRef,
18
21
  ResultCollection,
19
- ValueOrError,
20
- AxisFilter,
21
- PValue,
22
22
  SUniversalPColumnId,
23
- AnyFunction,
24
- DataInfo,
25
- BinaryPartitionedDataInfoEntries,
26
- JsonPartitionedDataInfoEntries,
27
- PObjectId } from '@milaboratories/pl-model-common';
23
+ ValueOrError,
24
+ } from '@milaboratories/pl-model-common';
28
25
  import {
29
- mapDataInfo,
30
- entriesToDataInfo,
31
- isDataInfo,
32
26
  AnchoredIdDeriver,
33
- getAxisId,
34
- resolveAnchors,
35
- canonicalizeAxisId,
36
27
  ensurePColumn,
37
28
  extractAllColumns,
29
+ isDataInfo,
38
30
  isPColumn,
39
31
  isPColumnSpec,
40
32
  isPlRef,
33
+ mapDataInfo,
41
34
  mapPObjectData,
42
35
  mapPTableDef,
43
36
  mapValueInVOE,
@@ -50,11 +43,10 @@ import type { FutureRef } from './future';
50
43
  import type { AccessorHandle, GlobalCfgRenderCtx } from './internal';
51
44
  import { MainAccessorName, StagingAccessorName } from './internal';
52
45
  import type { LabelDerivationOps } from './util/label';
46
+ import { PColumnCollection, type AxisLabelProvider, type ColumnProvider } from './util/column_collection';
53
47
  import { deriveLabels } from './util/label';
54
- import type { APColumnSelectorWithSplit } from './split_selectors';
55
- import { getUniquePartitionKeys, parsePColumnData } from './util/pcolumn_data';
56
- import type { TraceEntry } from './util/label';
57
- import { filterDataInfoEntries } from './util/axis_filtering';
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,
@@ -658,6 +401,59 @@ export class ResultPool {
658
401
  }
659
402
  return undefined;
660
403
  }
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
+
445
+ /**
446
+ * Find labels data for a given axis id of a p-column.
447
+ * @returns a map of axis value => label
448
+ */
449
+ public findLabelsForColumnAxis(column: PColumn<TreeNodeAccessor>, axisIdx: number): Record<string | number, string> | undefined {
450
+ const labels = this.findLabels(column.spec.axesSpec[axisIdx]);
451
+ if (!labels) return undefined;
452
+ return Object.fromEntries(column.data.listInputFields().map((field) => {
453
+ const r = JSON.parse(field) as [string];
454
+ return [r[axisIdx], labels[r[axisIdx]] ?? 'Unlabelled'];
455
+ }));
456
+ }
661
457
  }
662
458
 
663
459
  /** Main entry point to the API available within model lambdas (like outputs, sections, etc..) */
@@ -727,6 +523,7 @@ export class RenderCtx<Args, UiState> {
727
523
  // Removed redundant explicitColumns check
728
524
  }
729
525
 
526
+ // TODO remove all non-PColumn fields
730
527
  public createPFrame(def: PFrameDef<TreeNodeAccessor | PColumnValues | DataInfo<TreeNodeAccessor>>): PFrameHandle {
731
528
  this.verifyInlineAndExplicitColumnsSupport(def);
732
529
  return this.ctx.createPFrame(
@@ -734,6 +531,7 @@ export class RenderCtx<Args, UiState> {
734
531
  );
735
532
  }
736
533
 
534
+ // TODO remove all non-PColumn fields
737
535
  public createPTable(def: PTableDef<PColumn<TreeNodeAccessor | PColumnValues | DataInfo<TreeNodeAccessor>>>): PTableHandle;
738
536
  public createPTable(def: {
739
537
  columns: PColumn<TreeNodeAccessor | PColumnValues | DataInfo<TreeNodeAccessor>>[];