@platforma-sdk/model 1.24.11 → 1.26.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,7 +1,5 @@
1
1
  import type {
2
- APColumnSelector,
3
2
  AxisId,
4
- CanonicalPColumnId,
5
3
  Option,
6
4
  PColumn,
7
5
  PColumnSelector,
@@ -19,11 +17,22 @@ import type {
19
17
  PlRef,
20
18
  ResultCollection,
21
19
  ValueOrError,
20
+ AxisFilter,
21
+ PValue,
22
+ SUniversalPColumnId,
22
23
  AnyFunction,
23
- } from '@milaboratories/pl-model-common';
24
+ DataInfo,
25
+ BinaryPartitionedDataInfoEntries,
26
+ JsonPartitionedDataInfoEntries,
27
+ PObjectId } from '@milaboratories/pl-model-common';
24
28
  import {
25
- AnchorIdDeriver,
29
+ AnchoredIdDeriver,
30
+ getAxisId,
31
+ isDataInfo,
32
+ mapDataInfo,
26
33
  resolveAnchors,
34
+ canonicalizeAxisId,
35
+ entriesToDataInfo,
27
36
  } from '@milaboratories/pl-model-common';
28
37
  import {
29
38
  ensurePColumn,
@@ -40,10 +49,98 @@ import type { Optional } from 'utility-types';
40
49
  import { getCfgRenderCtx } from '../internal';
41
50
  import { TreeNodeAccessor, ifDef } from './accessor';
42
51
  import type { FutureRef } from './future';
43
- import type { GlobalCfgRenderCtx } from './internal';
52
+ import type { AccessorHandle, GlobalCfgRenderCtx } from './internal';
44
53
  import { MainAccessorName, StagingAccessorName } from './internal';
45
54
  import type { LabelDerivationOps } from './util/label';
46
55
  import { deriveLabels } from './util/label';
56
+ import type { APColumnSelectorWithSplit } from './split_selectors';
57
+ import { getUniquePartitionKeys, parsePColumnData } from './util/pcolumn_data';
58
+ import type { TraceEntry } from './util/label';
59
+ import { filterDataInfoEntries } from './util/axis_filtering';
60
+
61
+ /**
62
+ * Helper function to match domain objects
63
+ * @param query Optional domain to match against
64
+ * @param target Optional domain to match
65
+ * @returns true if domains match, false otherwise
66
+ */
67
+ function matchDomain(query?: Record<string, string>, target?: Record<string, string>) {
68
+ if (query === undefined) return target === undefined;
69
+ if (target === undefined) return true;
70
+ for (const k in target) {
71
+ if (query[k] !== target[k]) return false;
72
+ }
73
+ return true;
74
+ }
75
+
76
+ export type UniversalColumnOption = { label: string; value: SUniversalPColumnId };
77
+
78
+ /**
79
+ * Transforms PColumn data into the internal representation expected by the platform
80
+ * @param data Data from a PColumn to transform
81
+ * @returns Transformed data compatible with platform API
82
+ */
83
+ function transformPColumnData(data: PColumn<TreeNodeAccessor | PColumnValues | DataInfo<TreeNodeAccessor>>):
84
+ PColumn<PColumnValues | AccessorHandle | DataInfo<AccessorHandle>> {
85
+ return mapPObjectData(data, (d) => {
86
+ if (d instanceof TreeNodeAccessor) {
87
+ return d.handle;
88
+ } else if (isDataInfo(d)) {
89
+ return mapDataInfo(d, (accessor) => accessor.handle);
90
+ } else {
91
+ return d;
92
+ }
93
+ });
94
+ }
95
+
96
+ /**
97
+ * Describes a single filter applied due to a split axis.
98
+ */
99
+ export type AxisFilterInfo = {
100
+ axisIdx: number;
101
+ axisId: AxisId;
102
+ value: PValue;
103
+ label: string;
104
+ };
105
+
106
+ /**
107
+ * Represents a column specification with potential split axis filtering information
108
+ * used in canonical options generation.
109
+ */
110
+ export type UniversalPColumnEntry = {
111
+ id: SUniversalPColumnId;
112
+ obj: PColumnSpec;
113
+ ref: PlRef;
114
+ axisFilters?: AxisFilterInfo[];
115
+ label: string;
116
+ };
117
+
118
+ /**
119
+ * Converts an array of SplitAxisFilter objects into an array of TraceEntry objects
120
+ * suitable for label generation.
121
+ */
122
+ function splitFiltersToTrace(splitFilters?: AxisFilterInfo[]): TraceEntry[] | undefined {
123
+ if (!splitFilters) return undefined;
124
+ return splitFilters.map((filter) => ({
125
+ type: `split:${canonicalizeAxisId(filter.axisId)}`,
126
+ label: filter.label,
127
+ importance: 1_000_000, // High importance for split filters in labels
128
+ }));
129
+ }
130
+
131
+ /**
132
+ * Converts an array of SplitAxisFilter objects into an array of AxisFilter tuples
133
+ * suitable for deriving anchored IDs.
134
+ */
135
+ function splitFiltersToAxisFilter(splitFilters?: AxisFilterInfo[]): AxisFilter[] | undefined {
136
+ if (!splitFilters) return undefined;
137
+ return splitFilters.map((filter) => [filter.axisIdx, filter.value]);
138
+ }
139
+
140
+ type UniversalPColumnOpts = {
141
+ labelOps?: LabelDerivationOps;
142
+ dontWaitAllData?: boolean;
143
+ };
47
144
 
48
145
  export class ResultPool {
49
146
  private readonly ctx: GlobalCfgRenderCtx = getCfgRenderCtx();
@@ -55,10 +152,6 @@ export class ResultPool {
55
152
  return this.ctx.calculateOptions(predicate);
56
153
  }
57
154
 
58
- // @TODO: unused, what is this for?
59
- private defaultLabelFn = (spec: PObjectSpec, _ref: PlRef) =>
60
- spec.annotations?.['pl7.app/label'] ?? `Unlabelled`;
61
-
62
155
  public getOptions(
63
156
  predicateOrSelector: ((spec: PObjectSpec) => boolean) | PColumnSelector | PColumnSelector[],
64
157
  label?: ((spec: PObjectSpec, ref: PlRef) => string) | LabelDerivationOps,
@@ -79,6 +172,258 @@ export class ResultPool {
79
172
  }));
80
173
  }
81
174
 
175
+ /**
176
+ * Internal implementation that generates UniversalPColumnEntry objects from the provided
177
+ * anchors and selectors.
178
+ */
179
+ public getUniversalPColumnEntries(
180
+ anchorsOrCtx: AnchoredIdDeriver | Record<string, PColumnSpec | PlRef>,
181
+ predicateOrSelectors: ((spec: PColumnSpec) => boolean) | APColumnSelectorWithSplit | APColumnSelectorWithSplit[],
182
+ opts?: UniversalPColumnOpts,
183
+ ): UniversalPColumnEntry[] | undefined {
184
+ // Handle PlRef objects by resolving them to PColumnSpec
185
+ const resolvedAnchors: Record<string, PColumnSpec> = {};
186
+
187
+ if (!(anchorsOrCtx instanceof AnchoredIdDeriver)) {
188
+ for (const [key, value] of Object.entries(anchorsOrCtx)) {
189
+ if (isPlRef(value)) {
190
+ const resolvedSpec = this.getPColumnSpecByRef(value);
191
+ if (!resolvedSpec)
192
+ return undefined;
193
+ resolvedAnchors[key] = resolvedSpec;
194
+ } else {
195
+ // It's already a PColumnSpec
196
+ resolvedAnchors[key] = value;
197
+ }
198
+ }
199
+ }
200
+
201
+ const selectorsArray = typeof predicateOrSelectors === 'function'
202
+ ? [predicateOrSelectors]
203
+ : Array.isArray(predicateOrSelectors)
204
+ ? predicateOrSelectors
205
+ : [predicateOrSelectors];
206
+
207
+ const anchorIdDeriver = anchorsOrCtx instanceof AnchoredIdDeriver
208
+ ? anchorsOrCtx
209
+ : new AnchoredIdDeriver(resolvedAnchors);
210
+
211
+ const result: Omit<UniversalPColumnEntry, 'id' | 'label'>[] = [];
212
+
213
+ // Process each selector individually
214
+ for (const selector of selectorsArray) {
215
+ // Create predicate for this specific selector
216
+ const predicate = typeof selector === 'function'
217
+ ? selector
218
+ : selectorsToPredicate(resolveAnchors(resolvedAnchors, selector));
219
+
220
+ // Filter specs based on this specific predicate
221
+ const filtered = this.getSpecs().entries.filter(({ obj: spec }) => {
222
+ if (!isPColumnSpec(spec)) return false;
223
+ return predicate(spec);
224
+ });
225
+
226
+ if (filtered.length === 0)
227
+ continue;
228
+
229
+ // Check if this selector has any split axes
230
+ const splitAxisIdxs = typeof selector === 'object'
231
+ && 'axes' in selector
232
+ && selector.axes !== undefined
233
+ && selector.partialAxesMatch === undefined
234
+ ? selector.axes
235
+ .map((axis, index) => ('split' in axis && axis.split === true) ? index : -1)
236
+ .filter((index) => index !== -1)
237
+ : [];
238
+ splitAxisIdxs.sort((a, b) => a - b);
239
+
240
+ if (splitAxisIdxs.length > 0) { // Handle split axes
241
+ const maxSplitIdx = splitAxisIdxs[splitAxisIdxs.length - 1]; // Last one is max since they're sorted
242
+
243
+ for (const { ref, obj: spec } of filtered) {
244
+ if (!isPColumnSpec(spec)) throw new Error(`Assertion failed: expected PColumnSpec, got ${spec.kind}`);
245
+
246
+ const columnData = this.getDataByRef(ref);
247
+ if (!columnData) {
248
+ if (opts?.dontWaitAllData) continue;
249
+ return undefined;
250
+ }
251
+ if (!isPColumn(columnData)) throw new Error(`Assertion failed: expected PColumn, got ${columnData.spec.kind}`);
252
+
253
+ const uniqueKeys = getUniquePartitionKeys(columnData.data);
254
+ if (!uniqueKeys) {
255
+ if (opts?.dontWaitAllData) continue;
256
+ return undefined;
257
+ }
258
+
259
+ if (maxSplitIdx >= uniqueKeys.length)
260
+ throw new Error(`Not enough partition keys for the requested split axes in column ${spec.name}`);
261
+
262
+ // Pre-fetch labels for all involved split axes
263
+ const axesLabels: (Record<string | number, string> | undefined)[] = splitAxisIdxs
264
+ .map((idx) => this.findLabels(getAxisId(spec.axesSpec[idx])));
265
+
266
+ const keyCombinations: (string | number)[][] = [];
267
+ const generateCombinations = (currentCombo: (string | number)[], sAxisIdx: number) => {
268
+ if (sAxisIdx >= splitAxisIdxs.length) {
269
+ keyCombinations.push([...currentCombo]);
270
+ return;
271
+ }
272
+ const axisIdx = splitAxisIdxs[sAxisIdx];
273
+ const axisValues = uniqueKeys[axisIdx];
274
+ for (const val of axisValues) {
275
+ currentCombo.push(val);
276
+ generateCombinations(currentCombo, sAxisIdx + 1);
277
+ currentCombo.pop();
278
+ }
279
+ };
280
+ generateCombinations([], 0);
281
+
282
+ // Generate entries for each key combination
283
+ for (const keyCombo of keyCombinations) {
284
+ const splitFilters: AxisFilterInfo[] = keyCombo.map((value, sAxisIdx) => {
285
+ const axisIdx = splitAxisIdxs[sAxisIdx];
286
+ const axisId = getAxisId(spec.axesSpec[axisIdx]);
287
+ const axisLabelMap = axesLabels[sAxisIdx];
288
+ const label = axisLabelMap?.[value] ?? String(value);
289
+ return { axisIdx, axisId, value: value as PValue, label };
290
+ });
291
+
292
+ result.push({
293
+ obj: spec,
294
+ ref,
295
+ axisFilters: splitFilters,
296
+ });
297
+ }
298
+ }
299
+ } else {
300
+ // No split axes, simply add each filtered item without filters
301
+ for (const { ref, obj: spec } of filtered) {
302
+ if (!isPColumnSpec(spec)) continue;
303
+ result.push({
304
+ obj: spec,
305
+ ref,
306
+ // No splitFilters needed here
307
+ });
308
+ }
309
+ }
310
+ }
311
+
312
+ if (result.length === 0)
313
+ return [];
314
+
315
+ const labelResults = deriveLabels(
316
+ result,
317
+ (o) => ({
318
+ spec: o.obj,
319
+ suffixTrace: splitFiltersToTrace(o.axisFilters), // Use helper function
320
+ }),
321
+ opts?.labelOps ?? {},
322
+ );
323
+
324
+ return labelResults.map((item) => ({
325
+ id: anchorIdDeriver.deriveS(
326
+ item.value.obj,
327
+ splitFiltersToAxisFilter(item.value.axisFilters), // Use helper function
328
+ ),
329
+ obj: item.value.obj,
330
+ ref: item.value.ref,
331
+ axisFilters: item.value.axisFilters,
332
+ label: item.label,
333
+ }));
334
+ }
335
+
336
+ /**
337
+ * Returns columns that match the provided anchors and selectors. It applies axis filters and label derivation.
338
+ *
339
+ * @param anchorsOrCtx - Anchor context for column selection (same as in getCanonicalOptions)
340
+ * @param predicateOrSelectors - Predicate or selectors for filtering columns (same as in getCanonicalOptions)
341
+ * @param opts - Optional configuration for label generation and data waiting
342
+ * @returns A PFrameHandle for the created PFrame, or undefined if any required data is missing
343
+ */
344
+ public getAnchoredPColumns(
345
+ anchorsOrCtx: AnchoredIdDeriver | Record<string, PColumnSpec | PlRef>,
346
+ predicateOrSelectors: ((spec: PColumnSpec) => boolean) | APColumnSelectorWithSplit | APColumnSelectorWithSplit[],
347
+ opts?: UniversalPColumnOpts,
348
+ ): PColumn<DataInfo<TreeNodeAccessor>>[] | undefined {
349
+ // Ensure includeNativeLabel is true in the labelOps
350
+ const enhancedOpts: UniversalPColumnOpts = {
351
+ ...opts,
352
+ labelOps: {
353
+ includeNativeLabel: true,
354
+ ...(opts?.labelOps || {}),
355
+ },
356
+ };
357
+
358
+ const entries = this.getUniversalPColumnEntries(
359
+ anchorsOrCtx,
360
+ predicateOrSelectors,
361
+ enhancedOpts,
362
+ );
363
+
364
+ if (!entries || entries.length === 0) return undefined;
365
+
366
+ const result: PColumn<DataInfo<TreeNodeAccessor>>[] = [];
367
+
368
+ for (const entry of entries) {
369
+ const columnData = this.getPColumnByRef(entry.ref);
370
+ if (!columnData) return undefined;
371
+
372
+ const parsedData = parsePColumnData(columnData.data);
373
+ if (!parsedData) return undefined;
374
+
375
+ let filteredEntries: JsonPartitionedDataInfoEntries<TreeNodeAccessor> | BinaryPartitionedDataInfoEntries<TreeNodeAccessor> = parsedData;
376
+ let spec = { ...columnData.spec };
377
+
378
+ if (entry.axisFilters && entry.axisFilters.length > 0) {
379
+ const axisFiltersByIdx = entry.axisFilters.map((filter) => [
380
+ filter.axisIdx,
381
+ filter.value,
382
+ ] as [number, PValue]);
383
+
384
+ filteredEntries = filterDataInfoEntries(parsedData, axisFiltersByIdx);
385
+
386
+ const axisIndicesToRemove = [...entry.axisFilters]
387
+ .map((filter) => filter.axisIdx)
388
+ .sort((a, b) => b - a);
389
+
390
+ const newAxesSpec = [...spec.axesSpec];
391
+ for (const idx of axisIndicesToRemove) {
392
+ newAxesSpec.splice(idx, 1);
393
+ }
394
+
395
+ spec = { ...spec, axesSpec: newAxesSpec };
396
+ }
397
+
398
+ const dataInfo = entriesToDataInfo(filteredEntries);
399
+
400
+ if (spec.annotations) {
401
+ spec = {
402
+ ...spec,
403
+ annotations: {
404
+ ...spec.annotations,
405
+ 'pl7.app/label': entry.label,
406
+ },
407
+ };
408
+ } else {
409
+ spec = {
410
+ ...spec,
411
+ annotations: {
412
+ 'pl7.app/label': entry.label,
413
+ },
414
+ };
415
+ }
416
+
417
+ result.push({
418
+ id: entry.id as unknown as PObjectId,
419
+ spec,
420
+ data: dataInfo,
421
+ });
422
+ }
423
+
424
+ return result;
425
+ }
426
+
82
427
  /**
83
428
  * Calculates anchored identifier options for columns matching a given predicate and returns their
84
429
  * canonicalized representations.
@@ -91,83 +436,34 @@ export class ResultPool {
91
436
  * - An existing AnchorCtx instance
92
437
  * - A record mapping anchor IDs to PColumnSpec objects
93
438
  * - A record mapping anchor IDs to PlRef objects (which will be resolved to PColumnSpec)
94
- * @param predicateOrSelector - Either:
439
+ * @param predicateOrSelectors - Either:
95
440
  * - A predicate function that takes a PColumnSpec and returns a boolean.
96
441
  * Only specs that return true will be included.
97
442
  * - An APColumnSelector object for declarative filtering, which will be
98
443
  * resolved against the provided anchors and matched using matchPColumn.
99
444
  * - An array of APColumnSelector objects - columns matching ANY selector
100
445
  * in the array will be included (OR operation).
101
- * @param labelOps - Optional configuration for label generation:
102
- * - includeNativeLabel: Whether to include native column labels
103
- * - separator: String to use between label parts (defaults to " / ")
104
- * - addLabelAsSuffix: Whether to add labels as suffix instead of prefix
446
+ * @param opts - Optional configuration for label generation:
447
+ * - labelOps: Optional configuration for label generation:
448
+ * - includeNativeLabel: Whether to include native column labels
449
+ * - separator: String to use between label parts (defaults to " / ")
450
+ * - addLabelAsSuffix: Whether to add labels as suffix instead of prefix
451
+ * - dontWaitAllData: Whether to skip columns that don't have all data (if not set, will return undefined,
452
+ * if at least one column that requires splitting is missing data)
105
453
  * @returns An array of objects with `label` (display text) and `value` (anchored ID string) properties,
106
454
  * or undefined if any PlRef resolution fails.
107
455
  */
108
- // Overload for AnchorCtx - guaranteed to never return undefined
109
- getCanonicalOptions(
110
- anchorsOrCtx: AnchorIdDeriver,
111
- predicateOrSelectors: ((spec: PColumnSpec) => boolean) | APColumnSelector | APColumnSelector[],
112
- labelOps?: LabelDerivationOps,
113
- ): { label: string; value: CanonicalPColumnId }[];
114
-
115
- // Overload for Record<string, PColumnSpec> - guaranteed to never return undefined
116
- getCanonicalOptions(
117
- anchorsOrCtx: Record<string, PColumnSpec>,
118
- predicateOrSelectors: ((spec: PColumnSpec) => boolean) | APColumnSelector | APColumnSelector[],
119
- labelOps?: LabelDerivationOps,
120
- ): { label: string; value: CanonicalPColumnId }[];
121
-
122
- // Overload for Record<string, PColumnSpec | PlRef> - may return undefined if PlRef resolution fails
123
- getCanonicalOptions(
124
- anchorsOrCtx: Record<string, PColumnSpec | PlRef>,
125
- predicateOrSelectors: ((spec: PColumnSpec) => boolean) | APColumnSelector | APColumnSelector[],
126
- labelOps?: LabelDerivationOps,
127
- ): { label: string; value: CanonicalPColumnId }[] | undefined;
128
-
129
- // Implementation
130
456
  getCanonicalOptions(
131
- anchorsOrCtx: AnchorIdDeriver | Record<string, PColumnSpec | PlRef>,
132
- predicateOrSelectors: ((spec: PColumnSpec) => boolean) | APColumnSelector | APColumnSelector[],
133
- labelOps?: LabelDerivationOps,
134
- ): { label: string; value: CanonicalPColumnId }[] | undefined {
135
- // Handle PlRef objects by resolving them to PColumnSpec
136
- const resolvedAnchors: Record<string, PColumnSpec> = {};
137
-
138
- if (!(anchorsOrCtx instanceof AnchorIdDeriver)) {
139
- for (const [key, value] of Object.entries(anchorsOrCtx)) {
140
- if (isPlRef(value)) {
141
- const resolvedSpec = this.getPColumnSpecByRef(value);
142
- if (!resolvedSpec)
143
- return undefined;
144
- resolvedAnchors[key] = resolvedSpec;
145
- } else {
146
- // It's already a PColumnSpec
147
- resolvedAnchors[key] = value;
148
- }
149
- }
150
- }
151
-
152
- const predicate = typeof predicateOrSelectors === 'function'
153
- ? predicateOrSelectors
154
- : selectorsToPredicate(Array.isArray(predicateOrSelectors)
155
- ? predicateOrSelectors.map((selector) => resolveAnchors(resolvedAnchors, selector))
156
- : resolveAnchors(resolvedAnchors, predicateOrSelectors),
157
- );
158
-
159
- const filtered = this.getSpecs().entries.filter(({ obj: spec }) => {
160
- if (!isPColumnSpec(spec)) return false;
161
- return predicate(spec);
162
- });
163
-
164
- const anchorIdDeriver = anchorsOrCtx instanceof AnchorIdDeriver
165
- ? anchorsOrCtx
166
- : new AnchorIdDeriver(resolvedAnchors);
167
-
168
- return deriveLabels(filtered, (o) => o.obj, labelOps ?? {}).map(({ value: { obj: spec }, label }) => ({
169
- value: anchorIdDeriver.deriveCanonical(spec as PColumnSpec),
170
- label,
457
+ anchorsOrCtx: AnchoredIdDeriver | Record<string, PColumnSpec | PlRef>,
458
+ predicateOrSelectors: ((spec: PColumnSpec) => boolean) | APColumnSelectorWithSplit | APColumnSelectorWithSplit[],
459
+ opts?: UniversalPColumnOpts,
460
+ ): { label: string; value: SUniversalPColumnId }[] | undefined {
461
+ const entries = this.getUniversalPColumnEntries(anchorsOrCtx, predicateOrSelectors, opts);
462
+ if (!entries) return undefined;
463
+ // Generate final options using the entries from the helper method
464
+ return entries.map((item) => ({
465
+ value: item.id,
466
+ label: item.label,
171
467
  }));
172
468
  }
173
469
 
@@ -241,8 +537,11 @@ export class ResultPool {
241
537
  return this.getData().entries.find(
242
538
  (f) => f.ref.blockId === ref.blockId && f.ref.name === ref.name,
243
539
  )?.obj;
540
+ const data = this.ctx.getDataFromResultPoolByRef(ref.blockId, ref.name); // Keep original call
541
+ // Need to handle undefined case before mapping
542
+ if (!data) return undefined;
244
543
  return mapPObjectData(
245
- this.ctx.getDataFromResultPoolByRef(ref.blockId, ref.name),
544
+ data,
246
545
  (handle) => new TreeNodeAccessor(handle, [ref.blockId, ref.name]),
247
546
  );
248
547
  }
@@ -327,15 +626,40 @@ export class ResultPool {
327
626
  }
328
627
  return result;
329
628
  }
330
- }
331
629
 
332
- function matchDomain(query?: Record<string, string>, target?: Record<string, string>) {
333
- if (query === undefined) return target === undefined;
334
- if (target === undefined) return true;
335
- for (const k in target) {
336
- if (query[k] !== target[k]) return false;
630
+ /**
631
+ * Find labels data for a given axis id. It will search for a label column and return its data as a map.
632
+ * @returns a map of axis value => label
633
+ */
634
+ public findLabels(axis: AxisId): Record<string | number, string> | undefined {
635
+ const dataPool = this.getData();
636
+ for (const column of dataPool.entries) {
637
+ if (!isPColumn(column.obj)) continue;
638
+
639
+ const spec = column.obj.spec;
640
+ if (
641
+ spec.name === 'pl7.app/label'
642
+ && spec.axesSpec.length === 1
643
+ && spec.axesSpec[0].name === axis.name
644
+ && spec.axesSpec[0].type === axis.type
645
+ && matchDomain(axis.domain, spec.axesSpec[0].domain)
646
+ ) {
647
+ if (column.obj.data.resourceType.name !== 'PColumnData/Json') {
648
+ throw Error(`Expected JSON column for labels, got: ${column.obj.data.resourceType.name}`);
649
+ }
650
+ const labels: Record<string | number, string> = Object.fromEntries(
651
+ Object.entries(
652
+ column.obj.data.getDataAsJson<{
653
+ data: Record<string | number, string>;
654
+ }>().data,
655
+ ).map((e) => [JSON.parse(e[0])[0], e[1]]),
656
+ );
657
+
658
+ return labels;
659
+ }
660
+ }
661
+ return undefined;
337
662
  }
338
- return true;
339
663
  }
340
664
 
341
665
  /** Main entry point to the API available within model lambdas (like outputs, sections, etc..) */
@@ -391,68 +715,45 @@ export class RenderCtx<Args, UiState> {
391
715
  /**
392
716
  * Find labels data for a given axis id. It will search for a label column and return its data as a map.
393
717
  * @returns a map of axis value => label
718
+ * @deprecated Use resultPool.findLabels instead
394
719
  */
395
720
  public findLabels(axis: AxisId): Record<string | number, string> | undefined {
396
- const dataPool = this.resultPool.getData();
397
- for (const column of dataPool.entries) {
398
- if (!isPColumn(column.obj)) continue;
399
-
400
- const spec = column.obj.spec;
401
- if (
402
- spec.name === 'pl7.app/label'
403
- && spec.axesSpec.length === 1
404
- && spec.axesSpec[0].name === axis.name
405
- && spec.axesSpec[0].type === axis.type
406
- && matchDomain(axis.domain, spec.axesSpec[0].domain)
407
- ) {
408
- if (column.obj.data.resourceType.name !== 'PColumnData/Json') {
409
- throw Error(`Expected JSON column for labels, got: ${column.obj.data.resourceType.name}`);
410
- }
411
- const labels: Record<string | number, string> = Object.fromEntries(
412
- Object.entries(
413
- column.obj.data.getDataAsJson<{
414
- data: Record<string | number, string>;
415
- }>().data,
416
- ).map((e) => [JSON.parse(e[0])[0], e[1]]),
417
- );
418
-
419
- return labels;
420
- }
421
- }
422
- return undefined;
721
+ return this.resultPool.findLabels(axis);
423
722
  }
424
723
 
425
- private verifyInlineColumnsSupport(columns: PColumn<TreeNodeAccessor | PColumnValues>[]) {
426
- const hasInlineColumns = columns.some((c) => !(c.data instanceof TreeNodeAccessor));
724
+ private verifyInlineAndExplicitColumnsSupport(columns: PColumn<TreeNodeAccessor | PColumnValues | DataInfo<TreeNodeAccessor>>[]) {
725
+ const hasInlineColumns = columns.some((c) => !(c.data instanceof TreeNodeAccessor) || isDataInfo(c.data)); // Updated check for DataInfo
427
726
  const inlineColumnsSupport = this.ctx.featureFlags?.inlineColumnsSupport === true;
428
- if (hasInlineColumns && !inlineColumnsSupport) throw Error(`inline columns not supported`);
727
+ if (hasInlineColumns && !inlineColumnsSupport) throw Error(`Inline or explicit columns not supported`); // Combined check
728
+
729
+ // Removed redundant explicitColumns check
429
730
  }
430
731
 
431
- public createPFrame(def: PFrameDef<TreeNodeAccessor | PColumnValues>): PFrameHandle {
432
- this.verifyInlineColumnsSupport(def);
732
+ public createPFrame(def: PFrameDef<TreeNodeAccessor | PColumnValues | DataInfo<TreeNodeAccessor>>): PFrameHandle {
733
+ this.verifyInlineAndExplicitColumnsSupport(def);
433
734
  return this.ctx.createPFrame(
434
- def.map((c) => mapPObjectData(c, (d) => (d instanceof TreeNodeAccessor ? d.handle : d))),
735
+ def.map((c) => transformPColumnData(c)),
435
736
  );
436
737
  }
437
738
 
438
- public createPTable(def: PTableDef<PColumn<TreeNodeAccessor | PColumnValues>>): PTableHandle;
739
+ public createPTable(def: PTableDef<PColumn<TreeNodeAccessor | PColumnValues | DataInfo<TreeNodeAccessor>>>): PTableHandle;
439
740
  public createPTable(def: {
440
- columns: PColumn<TreeNodeAccessor | PColumnValues>[];
741
+ columns: PColumn<TreeNodeAccessor | PColumnValues | DataInfo<TreeNodeAccessor>>[];
441
742
  filters?: PTableRecordFilter[];
442
743
  /** Table sorting */
443
744
  sorting?: PTableSorting[];
444
745
  }): PTableHandle;
445
746
  public createPTable(
446
747
  def:
447
- | PTableDef<PColumn<TreeNodeAccessor | PColumnValues>>
748
+ | PTableDef<PColumn<TreeNodeAccessor | PColumnValues | DataInfo<TreeNodeAccessor>>>
448
749
  | {
449
- columns: PColumn<TreeNodeAccessor | PColumnValues>[];
750
+ columns: PColumn<TreeNodeAccessor | PColumnValues | DataInfo<TreeNodeAccessor>>[];
450
751
  filters?: PTableRecordFilter[];
451
752
  /** Table sorting */
452
753
  sorting?: PTableSorting[];
453
754
  },
454
755
  ): PTableHandle {
455
- let rawDef: PTableDef<PColumn<TreeNodeAccessor | PColumnValues>>;
756
+ let rawDef: PTableDef<PColumn<TreeNodeAccessor | PColumnValues | DataInfo<TreeNodeAccessor>>>;
456
757
  if ('columns' in def) {
457
758
  rawDef = {
458
759
  src: {
@@ -465,11 +766,9 @@ export class RenderCtx<Args, UiState> {
465
766
  } else {
466
767
  rawDef = def;
467
768
  }
468
- this.verifyInlineColumnsSupport(extractAllColumns(rawDef.src));
769
+ this.verifyInlineAndExplicitColumnsSupport(extractAllColumns(rawDef.src));
469
770
  return this.ctx.createPTable(
470
- mapPTableDef(rawDef, (po) =>
471
- mapPObjectData(po, (d) => (d instanceof TreeNodeAccessor ? d.handle : d)),
472
- ),
771
+ mapPTableDef(rawDef, (po) => transformPColumnData(po)),
473
772
  );
474
773
  }
475
774
 
@@ -16,6 +16,7 @@ import type {
16
16
  PTableHandle,
17
17
  ResultCollection,
18
18
  ValueOrError,
19
+ DataInfo,
19
20
  } from '@milaboratories/pl-model-common';
20
21
 
21
22
  export const StagingAccessorName = 'staging';
@@ -140,9 +141,9 @@ export interface GlobalCfgRenderCtxMethods<AHandle = AccessorHandle, FHandle = F
140
141
  // PFrame / PTable
141
142
  //
142
143
 
143
- createPFrame(def: PFrameDef<AHandle | PColumnValues>): PFrameHandle;
144
+ createPFrame(def: PFrameDef<AHandle | PColumnValues | DataInfo<AHandle>>): PFrameHandle;
144
145
 
145
- createPTable(def: PTableDef<PColumn<AHandle | PColumnValues>>): PTableHandle;
146
+ createPTable(def: PTableDef<PColumn<AHandle | PColumnValues | DataInfo<AHandle>>>): PTableHandle;
146
147
 
147
148
  //
148
149
  // Computable
@@ -152,6 +153,7 @@ export interface GlobalCfgRenderCtxMethods<AHandle = AccessorHandle, FHandle = F
152
153
  }
153
154
 
154
155
  export const GlobalCfgRenderCtxFeatureFlags = {
156
+ explicitColumnsSupport: true as const,
155
157
  inlineColumnsSupport: true as const,
156
158
  activeArgs: true as const,
157
159
  };