@platforma-sdk/model 1.65.9 → 1.66.2

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.
Files changed (121) hide show
  1. package/dist/block_model.cjs +8 -11
  2. package/dist/block_model.cjs.map +1 -1
  3. package/dist/block_model.d.ts.map +1 -1
  4. package/dist/block_model.js +8 -10
  5. package/dist/block_model.js.map +1 -1
  6. package/dist/columns/column_collection_builder.cjs +61 -74
  7. package/dist/columns/column_collection_builder.cjs.map +1 -1
  8. package/dist/columns/column_collection_builder.d.ts +16 -22
  9. package/dist/columns/column_collection_builder.d.ts.map +1 -1
  10. package/dist/columns/column_collection_builder.js +62 -75
  11. package/dist/columns/column_collection_builder.js.map +1 -1
  12. package/dist/columns/column_selector.cjs.map +1 -1
  13. package/dist/columns/column_selector.d.ts +1 -1
  14. package/dist/columns/column_selector.js.map +1 -1
  15. package/dist/columns/column_snapshot.cjs.map +1 -1
  16. package/dist/columns/column_snapshot.d.ts +4 -4
  17. package/dist/columns/column_snapshot.d.ts.map +1 -1
  18. package/dist/columns/column_snapshot.js.map +1 -1
  19. package/dist/columns/ctx_column_sources.cjs.map +1 -1
  20. package/dist/columns/ctx_column_sources.d.ts +1 -1
  21. package/dist/columns/ctx_column_sources.d.ts.map +1 -1
  22. package/dist/columns/ctx_column_sources.js.map +1 -1
  23. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV2.cjs +2 -2
  24. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV2.cjs.map +1 -1
  25. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV2.js +2 -2
  26. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV2.js.map +1 -1
  27. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV3.cjs +17 -18
  28. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV3.cjs.map +1 -1
  29. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV3.js +17 -18
  30. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV3.js.map +1 -1
  31. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.cjs +99 -91
  32. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.cjs.map +1 -1
  33. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.d.ts +16 -16
  34. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.d.ts.map +1 -1
  35. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.js +102 -94
  36. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.js.map +1 -1
  37. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.cjs +32 -23
  38. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.cjs.map +1 -1
  39. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.d.ts +5 -5
  40. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.d.ts.map +1 -1
  41. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.js +33 -24
  42. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.js.map +1 -1
  43. package/dist/components/PlDataTable/createPlDataTable/index.cjs.map +1 -1
  44. package/dist/components/PlDataTable/createPlDataTable/index.d.ts +2 -3
  45. package/dist/components/PlDataTable/createPlDataTable/index.d.ts.map +1 -1
  46. package/dist/components/PlDataTable/createPlDataTable/index.js.map +1 -1
  47. package/dist/components/PlDataTable/createPlDataTable/utils.cjs +133 -16
  48. package/dist/components/PlDataTable/createPlDataTable/utils.cjs.map +1 -1
  49. package/dist/components/PlDataTable/createPlDataTable/utils.d.ts +8 -6
  50. package/dist/components/PlDataTable/createPlDataTable/utils.d.ts.map +1 -1
  51. package/dist/components/PlDataTable/createPlDataTable/utils.js +130 -17
  52. package/dist/components/PlDataTable/createPlDataTable/utils.js.map +1 -1
  53. package/dist/components/PlDataTable/labels.cjs +1 -2
  54. package/dist/components/PlDataTable/labels.cjs.map +1 -1
  55. package/dist/components/PlDataTable/labels.js +1 -2
  56. package/dist/components/PlDataTable/labels.js.map +1 -1
  57. package/dist/filters/distill.cjs +73 -30
  58. package/dist/filters/distill.cjs.map +1 -1
  59. package/dist/filters/distill.d.ts.map +1 -1
  60. package/dist/filters/distill.js +73 -30
  61. package/dist/filters/distill.js.map +1 -1
  62. package/dist/index.cjs +19 -15
  63. package/dist/index.d.ts +4 -2
  64. package/dist/index.js +6 -4
  65. package/dist/labels/derive_distinct_tooltips.cjs +85 -0
  66. package/dist/labels/derive_distinct_tooltips.cjs.map +1 -0
  67. package/dist/labels/derive_distinct_tooltips.d.ts +17 -0
  68. package/dist/labels/derive_distinct_tooltips.d.ts.map +1 -0
  69. package/dist/labels/derive_distinct_tooltips.js +84 -0
  70. package/dist/labels/derive_distinct_tooltips.js.map +1 -0
  71. package/dist/labels/index.cjs +1 -0
  72. package/dist/labels/index.d.ts +2 -1
  73. package/dist/labels/index.js +1 -0
  74. package/dist/package.cjs +1 -1
  75. package/dist/package.js +1 -1
  76. package/dist/render/api.cjs +8 -13
  77. package/dist/render/api.cjs.map +1 -1
  78. package/dist/render/api.d.ts +8 -11
  79. package/dist/render/api.d.ts.map +1 -1
  80. package/dist/render/api.js +8 -13
  81. package/dist/render/api.js.map +1 -1
  82. package/dist/services/get_services.cjs +19 -0
  83. package/dist/services/get_services.cjs.map +1 -0
  84. package/dist/services/get_services.d.ts +7 -0
  85. package/dist/services/get_services.d.ts.map +1 -0
  86. package/dist/services/get_services.js +19 -0
  87. package/dist/services/get_services.js.map +1 -0
  88. package/dist/services/index.cjs +1 -0
  89. package/dist/services/index.d.ts +2 -1
  90. package/dist/services/index.js +1 -0
  91. package/dist/services/service_bridge.cjs +4 -4
  92. package/dist/services/service_bridge.cjs.map +1 -1
  93. package/dist/services/service_bridge.d.ts +4 -4
  94. package/dist/services/service_bridge.d.ts.map +1 -1
  95. package/dist/services/service_bridge.js +4 -4
  96. package/dist/services/service_bridge.js.map +1 -1
  97. package/package.json +6 -6
  98. package/src/block_model.ts +8 -11
  99. package/src/columns/column_collection_builder.test.ts +75 -30
  100. package/src/columns/column_collection_builder.ts +96 -133
  101. package/src/columns/column_selector.ts +1 -1
  102. package/src/columns/column_snapshot.ts +7 -4
  103. package/src/columns/ctx_column_sources.ts +1 -3
  104. package/src/components/PFrameForGraphs.test.ts +4 -4
  105. package/src/components/PlDataTable/createPlDataTable/createPTableDefV2.ts +2 -2
  106. package/src/components/PlDataTable/createPlDataTable/createPTableDefV3.ts +44 -21
  107. package/src/components/PlDataTable/createPlDataTable/createPlDataTableV3.ts +202 -218
  108. package/src/components/PlDataTable/createPlDataTable/discoverColumns.ts +69 -56
  109. package/src/components/PlDataTable/createPlDataTable/index.ts +6 -7
  110. package/src/components/PlDataTable/createPlDataTable/utils.test.ts +97 -1
  111. package/src/components/PlDataTable/createPlDataTable/utils.ts +190 -35
  112. package/src/components/PlDataTable/labels.ts +3 -7
  113. package/src/filters/distill.test.ts +91 -0
  114. package/src/filters/distill.ts +102 -46
  115. package/src/labels/derive_distinct_tooltips.test.ts +233 -0
  116. package/src/labels/derive_distinct_tooltips.ts +130 -0
  117. package/src/labels/index.ts +1 -0
  118. package/src/render/api.ts +15 -50
  119. package/src/services/get_services.ts +28 -0
  120. package/src/services/index.ts +1 -0
  121. package/src/services/service_bridge.ts +5 -5
@@ -1,57 +1,61 @@
1
- import type {
2
- PColumnSpec,
3
- PlRef,
4
- PObjectId,
5
- RequireServices,
6
- Services,
7
- SUniversalPColumnId,
8
- } from "@milaboratories/pl-model-common";
9
- import { isPlRef } from "@milaboratories/pl-model-common";
1
+ import type { PColumnSpec, PlRef, PObjectId } from "@milaboratories/pl-model-common";
2
+ import { createDiscoveredPColumnId, isPlRef } from "@milaboratories/pl-model-common";
10
3
  import type { RenderCtxBase } from "../../../render";
11
- import type { ColumnSource, ColumnMatch, RelaxedColumnSelector } from "../../../columns";
4
+ import type {
5
+ ColumnSource,
6
+ ColumnMatch,
7
+ RelaxedColumnSelector,
8
+ ColumnSnapshotProvider,
9
+ ColumnSnapshot,
10
+ } from "../../../columns";
12
11
  import { ColumnCollectionBuilder } from "../../../columns";
13
12
  import { toColumnSnapshotProvider } from "../../../columns/column_snapshot_provider";
14
13
  import { collectCtxColumnSnapshotProviders } from "../../../columns/ctx_column_sources";
15
14
  import { throwError } from "@milaboratories/helpers";
16
15
  import type { ColumnsSelectorConfig, TableColumnSnapshot } from "./createPlDataTableV3";
16
+ import { isNil } from "es-toolkit";
17
17
 
18
- export type DiscoveredTableColumnOptions = {
18
+ export type DiscoverTableColumnOptions = {
19
19
  sources?: ColumnSource[];
20
20
  anchors: Record<string, PlRef | PObjectId | PColumnSpec | RelaxedColumnSelector>;
21
- columnsSelector: ColumnsSelectorConfig;
21
+ selector: ColumnsSelectorConfig;
22
22
  };
23
23
 
24
24
  /** Discover columns from sources/anchors and normalize into a flat DiscoveredColumn list. */
25
- export function discoverTableColumnSnaphots<
26
- A,
27
- U,
28
- S extends RequireServices<typeof Services.PFrameSpec>,
29
- >(
30
- ctx: RenderCtxBase<A, U, S>,
31
- options: DiscoveredTableColumnOptions,
32
- ): TableColumnSnapshot<SUniversalPColumnId>[] | undefined {
25
+ export function discoverTableColumnSnaphots(
26
+ ctx: RenderCtxBase,
27
+ options: DiscoverTableColumnOptions,
28
+ ): TableColumnSnapshot[] | undefined {
33
29
  // Resolve PlRef anchors to PColumnSpec
34
- const resolvedOptions = { ...options, anchors: resolveAnchors(ctx, options.anchors) };
30
+ const resolvedOptions = {
31
+ ...options,
32
+ anchors: resolveAnchors(ctx, options.anchors),
33
+ };
35
34
 
36
35
  // Resolve providers
37
36
  const providers = resolveProviders(ctx, resolvedOptions.sources);
38
37
  if (providers.length === 0) return undefined;
39
38
 
40
39
  // Build collection (anchored or plain)
41
- const pframeSpec =
42
- ctx.services.pframeSpec ?? throwError("PFrameSpec service is required for column discovery.");
43
- const collection = new ColumnCollectionBuilder(pframeSpec)
40
+ const collection = new ColumnCollectionBuilder(ctx.getService("pframeSpec"))
44
41
  .addSources(providers)
45
42
  .build(resolvedOptions);
46
43
  if (collection === undefined) return undefined;
47
44
 
48
45
  try {
49
- const matched = collection.findColumns(resolvedOptions.columnsSelector ?? undefined);
46
+ const columns = collection.findColumns({
47
+ ...(resolvedOptions.selector ?? {}),
48
+ exclude: [
49
+ ...(Array.isArray(resolvedOptions.selector?.exclude)
50
+ ? resolvedOptions.selector.exclude
51
+ : !isNil(resolvedOptions.selector.exclude)
52
+ ? [resolvedOptions.selector.exclude]
53
+ : []),
54
+ { name: "pl7.app/label" },
55
+ ],
56
+ });
50
57
  const anchors = collection.getAnchors();
51
- return mapToDiscoveredColumns(
52
- matched,
53
- Array.from(Array.from(anchors.values()).map((v) => v.columnId)),
54
- );
58
+ return mapToDiscoveredColumns(columns, anchors);
55
59
  } finally {
56
60
  collection.dispose();
57
61
  }
@@ -60,8 +64,8 @@ export function discoverTableColumnSnaphots<
60
64
  // --- Pure helper functions ---
61
65
 
62
66
  /** Resolve PlRef values in anchors to PColumnSpec via the result pool. */
63
- function resolveAnchors<A, U>(
64
- ctx: RenderCtxBase<A, U>,
67
+ function resolveAnchors(
68
+ ctx: RenderCtxBase,
65
69
  anchors: Record<string, PlRef | PObjectId | PColumnSpec | RelaxedColumnSelector>,
66
70
  ): Record<string, PObjectId | PColumnSpec | RelaxedColumnSelector> {
67
71
  const result: Record<string, PObjectId | PColumnSpec | RelaxedColumnSelector> = {};
@@ -80,7 +84,10 @@ function resolveAnchors<A, U>(
80
84
  }
81
85
 
82
86
  /** Resolve column snapshot providers from explicit sources or context. */
83
- function resolveProviders<A, U>(ctx: RenderCtxBase<A, U>, sources: undefined | ColumnSource[]) {
87
+ function resolveProviders(
88
+ ctx: RenderCtxBase,
89
+ sources: undefined | ColumnSource[],
90
+ ): ColumnSnapshotProvider[] {
84
91
  return sources !== undefined
85
92
  ? sources.map(toColumnSnapshotProvider)
86
93
  : collectCtxColumnSnapshotProviders(ctx);
@@ -89,34 +96,40 @@ function resolveProviders<A, U>(ctx: RenderCtxBase<A, U>, sources: undefined | C
89
96
  /** Map matched columns into a flat DiscoveredColumn list with deduped IDs. */
90
97
  function mapToDiscoveredColumns(
91
98
  matched: readonly ColumnMatch[],
92
- anchorColumnIds: readonly PObjectId[],
93
- ): TableColumnSnapshot<SUniversalPColumnId>[] {
94
- const hitCounts = matched.reduce(
95
- (acc, match) => acc.set(match.originalId, (acc.get(match.originalId) ?? 0) + 1),
96
- new Map<PObjectId, number>(),
99
+ anchors: Map<string, ColumnSnapshot<PObjectId>>,
100
+ ): TableColumnSnapshot[] {
101
+ const columnIdToAnchorName = new Map<PObjectId, string>(
102
+ Array.from(anchors.entries(), ([key, { id }]) => [id, key] as const),
97
103
  );
98
104
 
99
- const hitIndices = new Map<PObjectId, number>();
100
- return matched.map((match) => {
105
+ return matched.flatMap((match) => {
101
106
  const snap = match.column;
107
+ const isPrimary = columnIdToAnchorName.get(match.column.id) !== undefined;
102
108
 
103
- let id = snap.id;
104
- if ((hitCounts.get(match.originalId) ?? 0) > 1) {
105
- const idx = hitIndices.get(match.originalId) ?? 0;
106
- hitIndices.set(match.originalId, idx + 1);
107
- id = `${snap.id}#q${idx}` as SUniversalPColumnId;
108
- }
109
+ return match.variants.map((variant): TableColumnSnapshot => {
110
+ const discoveredId = createDiscoveredPColumnId({
111
+ column: snap.id,
112
+ path: variant.path.map((p) => ({
113
+ type: "linker",
114
+ column: p.linker.id,
115
+ qualifications: p.qualifications,
116
+ })),
117
+ columnQualifications: variant.qualifications.forHit,
118
+ queriesQualifications: variant.qualifications.forQueries,
119
+ });
120
+ return {
121
+ id: discoveredId,
122
+ isPrimary,
123
+
124
+ originalId: snap.id,
125
+ spec: snap.spec,
126
+ data: snap.data,
127
+ dataStatus: snap.dataStatus,
109
128
 
110
- return {
111
- id,
112
- originalId: match.originalId,
113
- spec: snap.spec,
114
- data: snap.data,
115
- dataStatus: snap.dataStatus,
116
- isPrimary: anchorColumnIds.some(
117
- (anchor) => snap.id === anchor || match.originalId === anchor,
118
- ),
119
- linkerPath: match.path,
120
- };
129
+ linkerPath: variant.path,
130
+ qualifications: variant.qualifications,
131
+ distinctiveQualifications: variant.distinctiveQualifications,
132
+ };
133
+ });
121
134
  });
122
135
  }
@@ -1,20 +1,19 @@
1
- import { Services, type RequireServices } from "@milaboratories/pl-model-common";
2
1
  import type { RenderCtxBase } from "../../../render";
3
2
  import type { PlDataTableModel } from "../typesV5";
4
3
  import { createPlDataTableOptionsV2, createPlDataTableV2 } from "./createPlDataTableV2";
5
4
  import { createPlDataTableV3 } from "./createPlDataTableV3";
6
5
  import type { createPlDataTableOptionsV3 } from "./createPlDataTableV3";
7
6
 
8
- export function createPlDataTable<A, U, S extends RequireServices<typeof Services.PFrameSpec>>(
9
- ctx: RenderCtxBase<A, U, S>,
7
+ export function createPlDataTable<A, U>(
8
+ ctx: RenderCtxBase<A, U>,
10
9
  options: { version: "v2" } & createPlDataTableOptionsV2,
11
10
  ): ReturnType<typeof createPlDataTableV2>;
12
- export function createPlDataTable<A, U, S extends RequireServices<typeof Services.PFrameSpec>>(
13
- ctx: RenderCtxBase<A, U, S>,
11
+ export function createPlDataTable<A, U>(
12
+ ctx: RenderCtxBase<A, U>,
14
13
  options: { version?: "v3" } & createPlDataTableOptionsV3,
15
14
  ): ReturnType<typeof createPlDataTableV3>;
16
- export function createPlDataTable<A, U, S extends RequireServices<typeof Services.PFrameSpec>>(
17
- ctx: RenderCtxBase<A, U, S>,
15
+ export function createPlDataTable<A, U>(
16
+ ctx: RenderCtxBase<A, U>,
18
17
  options:
19
18
  | ({ version: "v2" } & createPlDataTableOptionsV2)
20
19
  | ({ version?: "v3" } & createPlDataTableOptionsV3),
@@ -6,8 +6,10 @@ import {
6
6
  type PColumnSpec,
7
7
  type PObjectId,
8
8
  } from "@milaboratories/pl-model-common";
9
+ import { SpecDriver } from "@milaboratories/pf-spec-driver";
9
10
  import { describe, expect, test } from "vitest";
10
- import { deriveAllLabels, type LabelableColumn } from "./utils";
11
+ import { deriveAllLabels, evaluateRules, type LabelableColumn, type RuleColumn } from "./utils";
12
+ import type { ColumnOrderRule, ColumnVisibilityRule } from "./createPlDataTableV3";
11
13
 
12
14
  // ---------------------------------------------------------------------------
13
15
  // Helpers
@@ -255,3 +257,97 @@ describe("deriveAxisLabels via deriveAllLabels", () => {
255
257
  expect(result["c2"]).not.toContain("via ");
256
258
  });
257
259
  });
260
+
261
+ // ---------------------------------------------------------------------------
262
+ // evaluateRules
263
+ // ---------------------------------------------------------------------------
264
+
265
+ function makeRuleColumn(id: string, spec: Partial<PColumnSpec> = {}): RuleColumn {
266
+ return {
267
+ id: id as PObjectId,
268
+ spec: makeSpec({ axesSpec: [{ name: "id", type: "String" }], ...spec }),
269
+ };
270
+ }
271
+
272
+ describe("evaluateRules", () => {
273
+ test("returns empty map when rules or columns are empty", () => {
274
+ const driver = new SpecDriver();
275
+ expect(evaluateRules([], [makeRuleColumn("c1")], driver).size).toBe(0);
276
+ expect(
277
+ evaluateRules<ColumnVisibilityRule>([{ match: () => true, visibility: "hidden" }], [], driver)
278
+ .size,
279
+ ).toBe(0);
280
+ });
281
+
282
+ test("evaluates predicate rules without touching the driver", () => {
283
+ const driver = new Proxy({} as SpecDriver, {
284
+ get() {
285
+ throw new Error("driver should not be called for predicate-only rules");
286
+ },
287
+ });
288
+
289
+ const rules: ColumnOrderRule[] = [
290
+ { match: (spec) => spec.name === "alpha", priority: 10 },
291
+ { match: (spec) => spec.name === "beta", priority: 5 },
292
+ ];
293
+ const result = evaluateRules(
294
+ rules,
295
+ [
296
+ makeRuleColumn("a", { name: "alpha" }),
297
+ makeRuleColumn("b", { name: "beta" }),
298
+ makeRuleColumn("c", { name: "gamma" }),
299
+ ],
300
+ driver,
301
+ );
302
+
303
+ expect(result.get("a" as PObjectId)?.priority).toBe(10);
304
+ expect(result.get("b" as PObjectId)?.priority).toBe(5);
305
+ expect(result.has("c" as PObjectId)).toBe(false);
306
+ });
307
+
308
+ test("evaluates selector rules via PFrameSpec.discoverColumns", () => {
309
+ const driver = new SpecDriver();
310
+ const rules: ColumnVisibilityRule[] = [
311
+ { match: { name: "^note$" }, visibility: "hidden" },
312
+ { match: { name: "^score$" }, visibility: "optional" },
313
+ ];
314
+ const columns = [
315
+ makeRuleColumn("n", { name: "note" }),
316
+ makeRuleColumn("s", { name: "score" }),
317
+ makeRuleColumn("x", { name: "other" }),
318
+ ];
319
+
320
+ const result = evaluateRules(rules, columns, driver);
321
+
322
+ expect(result.get("n" as PObjectId)?.visibility).toBe("hidden");
323
+ expect(result.get("s" as PObjectId)?.visibility).toBe("optional");
324
+ expect(result.has("x" as PObjectId)).toBe(false);
325
+ });
326
+
327
+ test("preserves original rule order when predicate and selector rules are mixed", () => {
328
+ const driver = new SpecDriver();
329
+ const rules: ColumnOrderRule[] = [
330
+ { match: (spec) => spec.name === "alpha", priority: 100 },
331
+ { match: { name: "^alpha$" }, priority: 1 }, // shadowed by the predicate above
332
+ { match: { name: "^beta$" }, priority: 50 },
333
+ ];
334
+ const result = evaluateRules(
335
+ rules,
336
+ [makeRuleColumn("a", { name: "alpha" }), makeRuleColumn("b", { name: "beta" })],
337
+ driver,
338
+ );
339
+
340
+ expect(result.get("a" as PObjectId)?.priority).toBe(100);
341
+ expect(result.get("b" as PObjectId)?.priority).toBe(50);
342
+ });
343
+
344
+ test("dedupes columns by id before building spec frame (no duplicate-key crash)", () => {
345
+ const driver = new SpecDriver();
346
+ const rules: ColumnVisibilityRule[] = [{ match: { name: "^dup$" }, visibility: "hidden" }];
347
+ const dup = makeRuleColumn("d", { name: "dup" });
348
+
349
+ const result = evaluateRules(rules, [dup, dup, dup], driver);
350
+
351
+ expect(result.get("d" as PObjectId)?.visibility).toBe("hidden");
352
+ });
353
+ });
@@ -1,10 +1,12 @@
1
1
  import {
2
2
  type PColumn,
3
3
  type PColumnSpec,
4
+ type PFrameSpecDriver,
4
5
  type PObjectId,
5
6
  Annotation,
6
7
  canonicalizeAxisId,
7
8
  canonicalizeJson,
9
+ DiscoveredPColumnId,
8
10
  getAxisId,
9
11
  readAnnotation,
10
12
  readAnnotationJson,
@@ -13,7 +15,14 @@ import {
13
15
  deriveDistinctLabels,
14
16
  type DeriveLabelsOptions,
15
17
  } from "../../../labels/derive_distinct_labels";
16
- import type { ColumnsDisplayOptions } from "./createPlDataTableV3";
18
+ import {
19
+ deriveDistinctTooltips,
20
+ type TooltipEntry,
21
+ } from "../../../labels/derive_distinct_tooltips";
22
+ import type { MatchQualifications, MatchVariant } from "../../../columns";
23
+ import type { ColumnMatcher, ColumnOrderRule, ColumnVisibilityRule } from "./createPlDataTableV3";
24
+ import type { ColumnSelector } from "../../../columns";
25
+ import { ArrayColumnProvider, ColumnCollectionBuilder } from "../../../columns";
17
26
  import { isNil } from "es-toolkit";
18
27
 
19
28
  /** Check if column should be omitted from the table */
@@ -26,36 +35,93 @@ export function isColumnOptional(spec: { annotations?: Annotation }): boolean {
26
35
  return readAnnotation(spec, Annotation.Table.Visibility) === "optional";
27
36
  }
28
37
 
29
- /** Get effective visibility for a column, considering display config rules first, then annotations. */
38
+ /** Column shape consumed by rule evaluation. */
39
+ export type RuleColumn = Pick<PColumn<PObjectId>, "id" | "spec">;
40
+
41
+ /** Get effective visibility for a column. Rule map lookup first, then annotation fallback. */
30
42
  export function getEffectiveVisibility(
31
- spec: PColumnSpec,
32
- displayConfig?: ColumnsDisplayOptions,
43
+ col: RuleColumn,
44
+ visibilityByColId?: Map<PObjectId, ColumnVisibilityRule>,
33
45
  ): undefined | "default" | "optional" | "hidden" {
34
- if (displayConfig?.visibility) {
35
- for (const rule of displayConfig.visibility) {
36
- if (rule.match(spec)) {
37
- return rule.visibility;
38
- }
39
- }
40
- }
41
- if (isColumnHidden(spec)) return "hidden";
42
- if (isColumnOptional(spec)) return "optional";
46
+ const rule = visibilityByColId?.get(col.id);
47
+ if (rule !== undefined) return rule.visibility;
48
+ if (isColumnHidden(col.spec)) return "hidden";
49
+ if (isColumnOptional(col.spec)) return "optional";
43
50
  return undefined;
44
51
  }
45
52
 
46
- /** Get ordering priority for a column. Display config rules first, then annotation fallback. */
53
+ /** Get ordering priority for a column. Rule map lookup first, then annotation fallback. */
47
54
  export function getOrderPriority(
48
- spec: PColumnSpec,
49
- displayConfig?: ColumnsDisplayOptions,
55
+ col: RuleColumn,
56
+ orderByColId?: Map<PObjectId, ColumnOrderRule>,
50
57
  ): undefined | number {
51
- if (displayConfig?.ordering) {
52
- for (const rule of displayConfig.ordering) {
53
- if (rule.match(spec)) {
54
- return rule.priority;
58
+ const rule = orderByColId?.get(col.id);
59
+ if (rule !== undefined) return rule.priority;
60
+ return readAnnotationJson(col.spec, Annotation.Table.OrderPriority);
61
+ }
62
+
63
+ /**
64
+ * Evaluate display rules against a set of columns and return a map of `colId → winning rule`
65
+ * (first-match-wins, preserving original rule order).
66
+ *
67
+ * Predicate-based rules (`ColumnMatcher`) are evaluated directly on the spec.
68
+ * Selector-based rules (`ColumnSelector`) are matched via `PFrameSpecDriver.discoverColumns`
69
+ * using the same engine as `ColumnCollection.findColumns` — no client-side matcher.
70
+ */
71
+ export function evaluateRules<R extends { match: ColumnMatcher | ColumnSelector }>(
72
+ rules: R[],
73
+ columns: RuleColumn[],
74
+ pframeSpec: PFrameSpecDriver,
75
+ ): Map<PObjectId, R> {
76
+ const result = new Map<PObjectId, R>();
77
+ if (rules.length === 0 || columns.length === 0) return result;
78
+
79
+ const hasSelectorRules = rules.some((rule) => typeof rule.match !== "function");
80
+ const selectorHitsByRule = new Map<R, Set<PObjectId>>();
81
+
82
+ if (hasSelectorRules) {
83
+ const dedupedColumns = dedupeById(columns);
84
+ const pColumns = dedupedColumns.map((c) => ({ id: c.id, spec: c.spec, data: undefined }));
85
+ const collection = new ColumnCollectionBuilder(pframeSpec)
86
+ .addSource(new ArrayColumnProvider(pColumns))
87
+ .build();
88
+ if (collection === undefined) return result;
89
+
90
+ try {
91
+ for (const rule of rules) {
92
+ if (typeof rule.match === "function") continue;
93
+ const hits = collection.findColumns({ include: rule.match });
94
+ selectorHitsByRule.set(rule, new Set(hits.map((h) => h.id)));
55
95
  }
96
+ } finally {
97
+ collection.dispose();
56
98
  }
57
99
  }
58
- return readAnnotationJson(spec, Annotation.Table.OrderPriority);
100
+
101
+ for (const col of columns) {
102
+ for (const rule of rules) {
103
+ const matches =
104
+ typeof rule.match === "function"
105
+ ? rule.match(col.spec)
106
+ : (selectorHitsByRule.get(rule)?.has(col.id) ?? false);
107
+ if (matches) {
108
+ result.set(col.id, rule);
109
+ break;
110
+ }
111
+ }
112
+ }
113
+ return result;
114
+ }
115
+
116
+ function dedupeById(columns: RuleColumn[]): RuleColumn[] {
117
+ const seen = new Set<PObjectId>();
118
+ const result: RuleColumn[] = [];
119
+ for (const col of columns) {
120
+ if (seen.has(col.id)) continue;
121
+ seen.add(col.id);
122
+ result.push(col);
123
+ }
124
+ return result;
59
125
  }
60
126
 
61
127
  /**
@@ -65,10 +131,9 @@ export function getOrderPriority(
65
131
  * For each column: writes derived label into Annotation.Label (if present in derivedLabels).
66
132
  * For each axis in column specs: writes derived axis label into AxisSpec annotations.
67
133
  */
68
- export function withLabelAnnotations<Data>(
69
- derivedLabels: undefined | Record<string, string>,
70
- columns: PColumn<Data>[],
71
- ): PColumn<Data>[] {
134
+ export function withLabelAnnotations<
135
+ T extends { readonly id: PObjectId; readonly spec: PColumnSpec },
136
+ >(derivedLabels: undefined | Record<string, string>, columns: T[]): T[] {
72
137
  if (derivedLabels === undefined) return columns;
73
138
  return columns.map((col) => {
74
139
  const colLabel = derivedLabels[col.id];
@@ -86,26 +151,29 @@ export function withLabelAnnotations<Data>(
86
151
  : { ...axis, annotations: { ...axis.annotations, [Annotation.Label]: label } };
87
152
  }),
88
153
  },
89
- };
154
+ } as T;
90
155
  });
91
156
  }
92
157
 
93
158
  /**
94
- * Writes effective display properties (OrderPriority, Visibility) from ColumnDisplayOptions
159
+ * Writes effective display properties (OrderPriority, Visibility) from precomputed rule maps
95
160
  * into column annotations. Returns new column objects — originals are not mutated.
96
161
  */
97
- export function withTableVisualAnnotations<Data>(
98
- displayOptions: undefined | ColumnsDisplayOptions,
99
- columns: PColumn<Data>[],
100
- ): PColumn<Data>[] {
101
- if (displayOptions === undefined) return columns;
162
+ export function withTableVisualAnnotations<
163
+ T extends { readonly id: PObjectId; readonly spec: PColumnSpec },
164
+ >(
165
+ visibilityByColId: undefined | Map<PObjectId, ColumnVisibilityRule>,
166
+ orderByColId: undefined | Map<PObjectId, ColumnOrderRule>,
167
+ columns: T[],
168
+ ): T[] {
169
+ if (visibilityByColId === undefined && orderByColId === undefined) return columns;
102
170
  return columns.map((col) => {
103
171
  const annotations = { ...col.spec.annotations };
104
172
 
105
- const visibility = getEffectiveVisibility(col.spec, displayOptions);
173
+ const visibility = getEffectiveVisibility(col, visibilityByColId);
106
174
  if (!isNil(visibility)) annotations[Annotation.Table.Visibility] = visibility;
107
175
 
108
- const orderPriority = getOrderPriority(col.spec, displayOptions);
176
+ const orderPriority = getOrderPriority(col, orderByColId);
109
177
  if (!isNil(orderPriority)) annotations[Annotation.Table.OrderPriority] = String(orderPriority);
110
178
 
111
179
  return {
@@ -114,10 +182,49 @@ export function withTableVisualAnnotations<Data>(
114
182
  ...col.spec,
115
183
  annotations: annotations,
116
184
  },
117
- };
185
+ } as T;
118
186
  });
119
187
  }
120
188
 
189
+ /**
190
+ * Writes derived info annotations into column annotations.
191
+ * Columns without an info entry are passed through unchanged.
192
+ */
193
+ export function withInfoAnnotations<
194
+ T extends { readonly id: PObjectId; readonly spec: PColumnSpec },
195
+ >(infoById: undefined | Record<string, string>, columns: T[]): T[] {
196
+ if (isNil(infoById)) return columns;
197
+ return columns.map((col) => {
198
+ const info = infoById[col.id];
199
+ if (isNil(info)) return col;
200
+ return {
201
+ ...col,
202
+ spec: {
203
+ ...col.spec,
204
+ annotations: { ...col.spec.annotations, [Annotation.Table.Info]: info },
205
+ },
206
+ } as T;
207
+ });
208
+ }
209
+
210
+ export function withHidenAxesAnnotations<T extends { readonly spec: PColumnSpec }>(
211
+ columns: T[],
212
+ ): T[] {
213
+ return columns.map(
214
+ (col) =>
215
+ ({
216
+ ...col,
217
+ spec: {
218
+ ...col.spec,
219
+ axesSpec: col.spec.axesSpec.map((axis) => ({
220
+ ...axis,
221
+ annotations: { ...axis.annotations, [Annotation.Table.Visibility]: "hidden" },
222
+ })),
223
+ },
224
+ }) as T,
225
+ );
226
+ }
227
+
121
228
  /** Column shape required by label derivation. */
122
229
  export type LabelableColumn = {
123
230
  readonly id: PObjectId;
@@ -158,3 +265,51 @@ function deriveAxisLabels(
158
265
  }
159
266
  return result;
160
267
  }
268
+
269
+ /** Column shape required by tooltip derivation. */
270
+ export type TooltipableColumn = {
271
+ readonly id: DiscoveredPColumnId;
272
+ readonly spec: PColumnSpec;
273
+ readonly originalId: PObjectId;
274
+ readonly linkerPath?: MatchVariant["path"];
275
+ readonly qualifications?: MatchQualifications;
276
+ readonly distinctiveQualifications?: MatchQualifications;
277
+ };
278
+
279
+ /** Derive origin tooltips for columns whose qualifications or linker path carry info. */
280
+ export function deriveAllTooltips(options: {
281
+ columns: TooltipableColumn[];
282
+ }): Record<DiscoveredPColumnId, string> {
283
+ const { columns } = options;
284
+
285
+ const variantCountByOriginal = columns.reduce<Map<PObjectId, number>>((acc, c) => {
286
+ return acc.set(c.originalId, (acc.get(c.originalId) ?? 0) + 1);
287
+ }, new Map());
288
+
289
+ const { entries } = columns.reduce(
290
+ ({ entries, variantSeen }, c) => {
291
+ const variantCount = variantCountByOriginal.get(c.originalId);
292
+ const variantIndex =
293
+ (variantSeen.set(c.originalId, (variantSeen.get(c.originalId) ?? 0) + 1),
294
+ variantSeen.get(c.originalId));
295
+
296
+ entries.push({
297
+ spec: c.spec,
298
+ linkerPath: c.linkerPath,
299
+ qualifications: c.qualifications,
300
+ distinctiveQualifications: c.distinctiveQualifications,
301
+ variantIndex,
302
+ variantCount,
303
+ });
304
+
305
+ return { entries, variantSeen };
306
+ },
307
+ { entries: [] as TooltipEntry[], variantSeen: new Map<PObjectId, number>() },
308
+ );
309
+
310
+ const tooltips = deriveDistinctTooltips(entries);
311
+
312
+ return Object.fromEntries(
313
+ tooltips.flatMap((t, i) => (isNil(t) ? [] : [[columns[i].id, t] as const])),
314
+ );
315
+ }
@@ -4,22 +4,18 @@ import {
4
4
  isLabelColumn,
5
5
  matchAxisId,
6
6
  PColumnName,
7
- Services,
8
- type RequireServices,
9
7
  } from "@milaboratories/pl-model-common";
10
8
  import type { PColumnDataUniversal, RenderCtxBase } from "../../render";
11
9
  import { ColumnCollectionBuilder, collectCtxColumnSnapshotProviders } from "../../columns";
12
- import { throwError } from "@milaboratories/helpers";
13
10
 
14
11
  /**
15
12
  * Get all label columns visible in the current render context
16
13
  * (result pool + block outputs + prerun).
17
14
  */
18
- export function getAllLabelColumns<A, U, S extends RequireServices<typeof Services.PFrameSpec>>(
19
- ctx: RenderCtxBase<A, U, S>,
15
+ export function getAllLabelColumns<A, U>(
16
+ ctx: RenderCtxBase<A, U>,
20
17
  ): PColumn<PColumnDataUniversal>[] {
21
- const pframeSpec =
22
- ctx.services.pframeSpec ?? throwError("PFrameSpec service is required for label discovery.");
18
+ const pframeSpec = ctx.getService("pframeSpec");
23
19
  const collection = new ColumnCollectionBuilder(pframeSpec)
24
20
  .addSources(collectCtxColumnSnapshotProviders(ctx))
25
21
  .build({ allowPartialColumnList: true });