@platforma-sdk/model 1.65.10 → 1.67.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.
Files changed (151) 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/block_state_util.cjs +2 -1
  7. package/dist/block_state_util.cjs.map +1 -1
  8. package/dist/block_state_util.js +2 -1
  9. package/dist/block_state_util.js.map +1 -1
  10. package/dist/columns/column_collection_builder.cjs +62 -75
  11. package/dist/columns/column_collection_builder.cjs.map +1 -1
  12. package/dist/columns/column_collection_builder.d.ts +17 -23
  13. package/dist/columns/column_collection_builder.d.ts.map +1 -1
  14. package/dist/columns/column_collection_builder.js +63 -76
  15. package/dist/columns/column_collection_builder.js.map +1 -1
  16. package/dist/columns/column_selector.cjs.map +1 -1
  17. package/dist/columns/column_selector.d.ts +1 -1
  18. package/dist/columns/column_selector.js.map +1 -1
  19. package/dist/columns/column_snapshot.cjs.map +1 -1
  20. package/dist/columns/column_snapshot.d.ts +4 -4
  21. package/dist/columns/column_snapshot.d.ts.map +1 -1
  22. package/dist/columns/column_snapshot.js.map +1 -1
  23. package/dist/columns/ctx_column_sources.cjs.map +1 -1
  24. package/dist/columns/ctx_column_sources.d.ts +1 -1
  25. package/dist/columns/ctx_column_sources.d.ts.map +1 -1
  26. package/dist/columns/ctx_column_sources.js.map +1 -1
  27. package/dist/columns/index.cjs +2 -2
  28. package/dist/columns/index.d.ts +1 -1
  29. package/dist/columns/index.js +2 -2
  30. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV2.cjs +2 -2
  31. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV2.cjs.map +1 -1
  32. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV2.js +2 -2
  33. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV2.js.map +1 -1
  34. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV3.cjs +17 -18
  35. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV3.cjs.map +1 -1
  36. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV3.js +17 -18
  37. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV3.js.map +1 -1
  38. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.cjs +99 -91
  39. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.cjs.map +1 -1
  40. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.d.ts +17 -17
  41. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.d.ts.map +1 -1
  42. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.js +102 -94
  43. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.js.map +1 -1
  44. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.cjs +32 -23
  45. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.cjs.map +1 -1
  46. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.d.ts +6 -6
  47. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.d.ts.map +1 -1
  48. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.js +33 -24
  49. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.js.map +1 -1
  50. package/dist/components/PlDataTable/createPlDataTable/index.cjs.map +1 -1
  51. package/dist/components/PlDataTable/createPlDataTable/index.d.ts +2 -3
  52. package/dist/components/PlDataTable/createPlDataTable/index.d.ts.map +1 -1
  53. package/dist/components/PlDataTable/createPlDataTable/index.js.map +1 -1
  54. package/dist/components/PlDataTable/createPlDataTable/utils.cjs +133 -16
  55. package/dist/components/PlDataTable/createPlDataTable/utils.cjs.map +1 -1
  56. package/dist/components/PlDataTable/createPlDataTable/utils.d.ts +8 -6
  57. package/dist/components/PlDataTable/createPlDataTable/utils.d.ts.map +1 -1
  58. package/dist/components/PlDataTable/createPlDataTable/utils.js +130 -17
  59. package/dist/components/PlDataTable/createPlDataTable/utils.js.map +1 -1
  60. package/dist/components/PlDataTable/labels.cjs +1 -2
  61. package/dist/components/PlDataTable/labels.cjs.map +1 -1
  62. package/dist/components/PlDataTable/labels.js +1 -2
  63. package/dist/components/PlDataTable/labels.js.map +1 -1
  64. package/dist/components/PlDatasetSelector/build_dataset_options.cjs +41 -0
  65. package/dist/components/PlDatasetSelector/build_dataset_options.cjs.map +1 -0
  66. package/dist/components/PlDatasetSelector/build_dataset_options.d.ts +19 -0
  67. package/dist/components/PlDatasetSelector/build_dataset_options.d.ts.map +1 -0
  68. package/dist/components/PlDatasetSelector/build_dataset_options.js +41 -0
  69. package/dist/components/PlDatasetSelector/build_dataset_options.js.map +1 -0
  70. package/dist/components/PlDatasetSelector/filter_discovery.cjs +60 -0
  71. package/dist/components/PlDatasetSelector/filter_discovery.cjs.map +1 -0
  72. package/dist/components/PlDatasetSelector/filter_discovery.d.ts +31 -0
  73. package/dist/components/PlDatasetSelector/filter_discovery.d.ts.map +1 -0
  74. package/dist/components/PlDatasetSelector/filter_discovery.js +56 -0
  75. package/dist/components/PlDatasetSelector/filter_discovery.js.map +1 -0
  76. package/dist/components/PlDatasetSelector/index.cjs +2 -0
  77. package/dist/components/PlDatasetSelector/index.d.ts +2 -0
  78. package/dist/components/PlDatasetSelector/index.js +2 -0
  79. package/dist/components/index.cjs +3 -0
  80. package/dist/components/index.d.ts +2 -0
  81. package/dist/components/index.js +3 -0
  82. package/dist/filters/distill.cjs +73 -30
  83. package/dist/filters/distill.cjs.map +1 -1
  84. package/dist/filters/distill.d.ts.map +1 -1
  85. package/dist/filters/distill.js +73 -30
  86. package/dist/filters/distill.js.map +1 -1
  87. package/dist/index.cjs +29 -19
  88. package/dist/index.d.ts +10 -6
  89. package/dist/index.js +12 -8
  90. package/dist/labels/derive_distinct_tooltips.cjs +85 -0
  91. package/dist/labels/derive_distinct_tooltips.cjs.map +1 -0
  92. package/dist/labels/derive_distinct_tooltips.d.ts +17 -0
  93. package/dist/labels/derive_distinct_tooltips.d.ts.map +1 -0
  94. package/dist/labels/derive_distinct_tooltips.js +84 -0
  95. package/dist/labels/derive_distinct_tooltips.js.map +1 -0
  96. package/dist/labels/index.cjs +1 -0
  97. package/dist/labels/index.d.ts +2 -1
  98. package/dist/labels/index.js +1 -0
  99. package/dist/package.cjs +1 -1
  100. package/dist/package.js +1 -1
  101. package/dist/render/api.cjs +8 -13
  102. package/dist/render/api.cjs.map +1 -1
  103. package/dist/render/api.d.ts +8 -11
  104. package/dist/render/api.d.ts.map +1 -1
  105. package/dist/render/api.js +8 -13
  106. package/dist/render/api.js.map +1 -1
  107. package/dist/services/get_services.cjs +19 -0
  108. package/dist/services/get_services.cjs.map +1 -0
  109. package/dist/services/get_services.d.ts +7 -0
  110. package/dist/services/get_services.d.ts.map +1 -0
  111. package/dist/services/get_services.js +19 -0
  112. package/dist/services/get_services.js.map +1 -0
  113. package/dist/services/index.cjs +1 -0
  114. package/dist/services/index.d.ts +2 -1
  115. package/dist/services/index.js +1 -0
  116. package/dist/services/service_bridge.cjs +4 -4
  117. package/dist/services/service_bridge.cjs.map +1 -1
  118. package/dist/services/service_bridge.d.ts +4 -4
  119. package/dist/services/service_bridge.d.ts.map +1 -1
  120. package/dist/services/service_bridge.js +4 -4
  121. package/dist/services/service_bridge.js.map +1 -1
  122. package/package.json +6 -6
  123. package/src/block_model.ts +8 -11
  124. package/src/block_state_util.ts +2 -1
  125. package/src/columns/column_collection_builder.test.ts +75 -30
  126. package/src/columns/column_collection_builder.ts +96 -133
  127. package/src/columns/column_selector.ts +1 -1
  128. package/src/columns/column_snapshot.ts +7 -4
  129. package/src/columns/ctx_column_sources.ts +1 -3
  130. package/src/components/PlDataTable/createPlDataTable/createPTableDefV2.ts +2 -2
  131. package/src/components/PlDataTable/createPlDataTable/createPTableDefV3.ts +44 -21
  132. package/src/components/PlDataTable/createPlDataTable/createPlDataTableV3.ts +202 -218
  133. package/src/components/PlDataTable/createPlDataTable/discoverColumns.ts +69 -56
  134. package/src/components/PlDataTable/createPlDataTable/index.ts +6 -7
  135. package/src/components/PlDataTable/createPlDataTable/utils.test.ts +97 -1
  136. package/src/components/PlDataTable/createPlDataTable/utils.ts +190 -35
  137. package/src/components/PlDataTable/labels.ts +3 -7
  138. package/src/components/PlDatasetSelector/build_dataset_options.ts +56 -0
  139. package/src/components/PlDatasetSelector/filter_discovery.test.ts +156 -0
  140. package/src/components/PlDatasetSelector/filter_discovery.ts +77 -0
  141. package/src/components/PlDatasetSelector/index.ts +2 -0
  142. package/src/components/index.ts +1 -0
  143. package/src/filters/distill.test.ts +91 -0
  144. package/src/filters/distill.ts +102 -46
  145. package/src/labels/derive_distinct_tooltips.test.ts +233 -0
  146. package/src/labels/derive_distinct_tooltips.ts +130 -0
  147. package/src/labels/index.ts +1 -0
  148. package/src/render/api.ts +15 -50
  149. package/src/services/get_services.ts +28 -0
  150. package/src/services/index.ts +1 -0
  151. package/src/services/service_bridge.ts +5 -5
@@ -0,0 +1,233 @@
1
+ import {
2
+ Annotation,
3
+ type AxisQualification,
4
+ type PColumnSpec,
5
+ type PObjectId,
6
+ } from "@milaboratories/pl-model-common";
7
+ import { describe, expect, test } from "vitest";
8
+ import { deriveDistinctTooltips, type TooltipEntry } from "./derive_distinct_tooltips";
9
+ import type { ColumnSnapshot, MatchVariant } from "../columns";
10
+
11
+ function createSpec(name: string, label?: string): PColumnSpec {
12
+ return {
13
+ kind: "PColumn",
14
+ name,
15
+ valueType: "Int",
16
+ axesSpec: [],
17
+ annotations: label !== undefined ? { [Annotation.Label]: label } : {},
18
+ } as PColumnSpec;
19
+ }
20
+
21
+ function axisQualification(
22
+ axisName: string,
23
+ contextDomain: Record<string, string>,
24
+ ): AxisQualification {
25
+ return { axis: { name: axisName }, contextDomain };
26
+ }
27
+
28
+ function linkerSnapshot(name: string, label?: string): ColumnSnapshot<PObjectId> {
29
+ return {
30
+ id: `linker-${name}` as PObjectId,
31
+ spec: createSpec(name, label),
32
+ dataStatus: "ready",
33
+ data: undefined,
34
+ };
35
+ }
36
+
37
+ function pathStep(
38
+ linkerName: string,
39
+ qualifications: AxisQualification[],
40
+ label?: string,
41
+ ): MatchVariant["path"][number] {
42
+ return { linker: linkerSnapshot(linkerName, label), qualifications };
43
+ }
44
+
45
+ describe("deriveDistinctTooltips", () => {
46
+ test("empty entry (no qualifications, no linker path) → undefined", () => {
47
+ const entries: TooltipEntry[] = [{ spec: createSpec("col1") }];
48
+ expect(deriveDistinctTooltips(entries)).toEqual([undefined]);
49
+ });
50
+
51
+ test("only header info but no sections → undefined (single section filtered)", () => {
52
+ const entries: TooltipEntry[] = [
53
+ {
54
+ spec: createSpec("col1", "Column 1"),
55
+ variantIndex: 1,
56
+ variantCount: 1,
57
+ },
58
+ ];
59
+ expect(deriveDistinctTooltips(entries)).toEqual([undefined]);
60
+ });
61
+
62
+ test("linker path produces Origin path section", () => {
63
+ const entries: TooltipEntry[] = [
64
+ {
65
+ spec: createSpec("hit_col", "Hit Col"),
66
+ linkerPath: [
67
+ pathStep("linker_a", [axisQualification("sample", { batch: "A" })], "Linker A"),
68
+ ],
69
+ },
70
+ ];
71
+ const [tooltip] = deriveDistinctTooltips(entries);
72
+ expect(tooltip).toBeDefined();
73
+ expect(tooltip).toContain("Origin path");
74
+ expect(tooltip).toContain("linker 1: Linker A");
75
+ expect(tooltip).toContain("qualifies: sample context: batch=A");
76
+ expect(tooltip).toContain("hit column: Hit Col");
77
+ });
78
+
79
+ test("qualifications.forQueries produces Anchors section", () => {
80
+ const entries: TooltipEntry[] = [
81
+ {
82
+ spec: createSpec("col1", "Col 1"),
83
+ qualifications: {
84
+ forQueries: {
85
+ ["main" as PObjectId]: [axisQualification("sample", { batch: "A" })],
86
+ ["other" as PObjectId]: [axisQualification("gene", { source: "X" })],
87
+ },
88
+ forHit: [],
89
+ },
90
+ },
91
+ ];
92
+ const [tooltip] = deriveDistinctTooltips(entries);
93
+ expect(tooltip).toContain("Anchors (bound via this variant)");
94
+ expect(tooltip).toContain("main sample context: batch=A");
95
+ expect(tooltip).toContain("other gene context: source=X");
96
+ });
97
+
98
+ test("qualifications.forHit produces Hit section", () => {
99
+ const entries: TooltipEntry[] = [
100
+ {
101
+ spec: createSpec("col1", "Col 1"),
102
+ qualifications: {
103
+ forQueries: {},
104
+ forHit: [axisQualification("sample", { batch: "B" })],
105
+ },
106
+ },
107
+ ];
108
+ const [tooltip] = deriveDistinctTooltips(entries);
109
+ expect(tooltip).toContain("Hit column qualifications");
110
+ expect(tooltip).toContain("sample context: batch=B");
111
+ });
112
+
113
+ test("distinctiveQualifications produces Distinctive section", () => {
114
+ const entries: TooltipEntry[] = [
115
+ {
116
+ spec: createSpec("col1", "Col 1"),
117
+ distinctiveQualifications: {
118
+ forQueries: { ["main" as PObjectId]: [axisQualification("sample", { batch: "A" })] },
119
+ forHit: [axisQualification("gene", { source: "Y" })],
120
+ },
121
+ },
122
+ ];
123
+ const [tooltip] = deriveDistinctTooltips(entries);
124
+ expect(tooltip).toContain("Distinctive (what separates this variant)");
125
+ expect(tooltip).toContain("main: sample context: batch=A");
126
+ expect(tooltip).toContain("hit: gene context: source=Y");
127
+ });
128
+
129
+ test("variantCount > 1 adds Variant N of M line in header", () => {
130
+ const entries: TooltipEntry[] = [
131
+ {
132
+ spec: createSpec("col1", "Col 1"),
133
+ variantIndex: 1,
134
+ variantCount: 2,
135
+ qualifications: {
136
+ forQueries: { ["main" as PObjectId]: [axisQualification("sample", { batch: "A" })] },
137
+ forHit: [],
138
+ },
139
+ },
140
+ ];
141
+ const [tooltip] = deriveDistinctTooltips(entries);
142
+ expect(tooltip).toContain("Variant: 1 of 2");
143
+ });
144
+
145
+ test("axis qualification with empty contextDomain shows only axis name", () => {
146
+ const entries: TooltipEntry[] = [
147
+ {
148
+ spec: createSpec("col1", "Col 1"),
149
+ qualifications: {
150
+ forQueries: { ["main" as PObjectId]: [axisQualification("sample", {})] },
151
+ forHit: [],
152
+ },
153
+ },
154
+ ];
155
+ const [tooltip] = deriveDistinctTooltips(entries);
156
+ expect(tooltip).toContain("main sample");
157
+ expect(tooltip).not.toContain("context:");
158
+ });
159
+
160
+ test("empty forQueries object → no Anchors section", () => {
161
+ const entries: TooltipEntry[] = [
162
+ {
163
+ spec: createSpec("col1", "Col 1"),
164
+ qualifications: { forQueries: {}, forHit: [] },
165
+ linkerPath: [pathStep("linker_a", [], "Linker A")],
166
+ },
167
+ ];
168
+ const [tooltip] = deriveDistinctTooltips(entries);
169
+ expect(tooltip).not.toContain("Anchors");
170
+ });
171
+
172
+ test("multi-step linker path numbers sequentially", () => {
173
+ const entries: TooltipEntry[] = [
174
+ {
175
+ spec: createSpec("hit_col", "Hit Col"),
176
+ linkerPath: [
177
+ pathStep("linker_a", [], "Linker A"),
178
+ pathStep("linker_b", [axisQualification("sample", { batch: "B" })], "Linker B"),
179
+ ],
180
+ },
181
+ ];
182
+ const [tooltip] = deriveDistinctTooltips(entries);
183
+ expect(tooltip).toContain("linker 1: Linker A");
184
+ expect(tooltip).toContain("linker 2: Linker B");
185
+ });
186
+
187
+ test("all sections compose with double-newline separators", () => {
188
+ const entries: TooltipEntry[] = [
189
+ {
190
+ spec: createSpec("hit_col", "Hit"),
191
+ variantIndex: 2,
192
+ variantCount: 2,
193
+ linkerPath: [pathStep("linker_a", [axisQualification("sample", { batch: "B" })], "LA")],
194
+ qualifications: {
195
+ forQueries: { ["main" as PObjectId]: [axisQualification("sample", { batch: "B" })] },
196
+ forHit: [axisQualification("sample", { batch: "B" })],
197
+ },
198
+ distinctiveQualifications: {
199
+ forQueries: { ["main" as PObjectId]: [axisQualification("sample", { batch: "B" })] },
200
+ forHit: [],
201
+ },
202
+ },
203
+ ];
204
+ const [tooltip] = deriveDistinctTooltips(entries);
205
+ expect(tooltip).toBeDefined();
206
+ const sections = tooltip!.split("\n\n");
207
+ expect(sections.length).toBe(5);
208
+ expect(sections[0]).toContain("Variant: 2 of 2");
209
+ expect(sections[1]).toContain("Origin path");
210
+ expect(sections[2]).toContain("Anchors");
211
+ expect(sections[3]).toContain("Hit column qualifications");
212
+ expect(sections[4]).toContain("Distinctive");
213
+ });
214
+
215
+ test("parallel results — array aligns with input", () => {
216
+ const entries: TooltipEntry[] = [
217
+ { spec: createSpec("a") },
218
+ {
219
+ spec: createSpec("b", "B"),
220
+ qualifications: {
221
+ forQueries: { ["main" as PObjectId]: [axisQualification("sample", { batch: "A" })] },
222
+ forHit: [],
223
+ },
224
+ },
225
+ { spec: createSpec("c") },
226
+ ];
227
+ const result = deriveDistinctTooltips(entries);
228
+ expect(result.length).toBe(3);
229
+ expect(result[0]).toBeUndefined();
230
+ expect(result[1]).toBeDefined();
231
+ expect(result[2]).toBeUndefined();
232
+ });
233
+ });
@@ -0,0 +1,130 @@
1
+ import {
2
+ Annotation,
3
+ PObjectId,
4
+ readAnnotation,
5
+ type AxisQualification,
6
+ type PColumnSpec,
7
+ } from "@milaboratories/pl-model-common";
8
+ import { isNil } from "es-toolkit";
9
+ import type { MatchQualifications, MatchVariant } from "../columns";
10
+
11
+ export type TooltipEntry = {
12
+ /** Main column spec — used for column-name fallback when no label. */
13
+ spec: PColumnSpec;
14
+ /** Full qualifications carried by this variant. */
15
+ qualifications?: MatchQualifications;
16
+ /** Minimal qualifications that separate this variant from its siblings. */
17
+ distinctiveQualifications?: MatchQualifications;
18
+ /** Linker steps traversed to reach the hit column. */
19
+ linkerPath?: MatchVariant["path"];
20
+ /** Position of this variant within the same physical column (1-based). */
21
+ variantIndex?: number;
22
+ /** Total variants for the same physical column. */
23
+ variantCount?: number;
24
+ };
25
+
26
+ /** Format tooltip strings for each entry. Returns `undefined` when nothing useful. */
27
+ export function deriveDistinctTooltips(entries: TooltipEntry[]): (undefined | string)[] {
28
+ return entries.map(formatTooltip);
29
+ }
30
+
31
+ function formatTooltip(entry: TooltipEntry): undefined | string {
32
+ const sections: string[] = [];
33
+
34
+ const header = formatHeader(entry);
35
+ if (header !== undefined) sections.push(header);
36
+
37
+ const origin = formatOriginPath(entry);
38
+ if (origin !== undefined) sections.push(origin);
39
+
40
+ const anchors = formatAnchors(entry.qualifications);
41
+ if (anchors !== undefined) sections.push(anchors);
42
+
43
+ const hit = formatHit(entry.qualifications);
44
+ if (hit !== undefined) sections.push(hit);
45
+
46
+ const distinctive = formatDistinctive(entry.distinctiveQualifications);
47
+ if (distinctive !== undefined) sections.push(distinctive);
48
+
49
+ if (sections.length <= 1) return undefined;
50
+ return sections.join("\n\n");
51
+ }
52
+
53
+ const BULLET_1 = " • ";
54
+ const SUB_BULLET = " ";
55
+
56
+ function formatHeader(entry: TooltipEntry): undefined | string {
57
+ const lines: string[] = [];
58
+ if (entry.variantCount !== undefined && entry.variantCount > 1) {
59
+ lines.push(`Variant: ${entry.variantIndex ?? "?"} of ${entry.variantCount}`);
60
+ }
61
+ return lines.join("\n");
62
+ }
63
+
64
+ function formatOriginPath(entry: TooltipEntry): undefined | string {
65
+ const path = entry.linkerPath ?? [];
66
+ if (path.length === 0) return undefined;
67
+
68
+ const lines = ["Origin path"];
69
+ path.forEach((step, i) => {
70
+ const label =
71
+ readAnnotation(step.linker.spec, Annotation.LinkLabel) ??
72
+ readAnnotation(step.linker.spec, Annotation.Label) ??
73
+ step.linker.spec.name;
74
+ lines.push(`${BULLET_1}linker ${i + 1}: ${label}`);
75
+ const qs = formatAxisQualifications(step.qualifications);
76
+ if (qs !== undefined) lines.push(`${SUB_BULLET}qualifies: ${qs}`);
77
+ });
78
+ const hitName = readAnnotation(entry.spec, Annotation.Label) ?? entry.spec.name;
79
+ lines.push(`${BULLET_1}hit column: ${hitName}`);
80
+ return lines.join("\n");
81
+ }
82
+
83
+ function formatAnchors(q: undefined | MatchQualifications): undefined | string {
84
+ if (isNil(q)) return undefined;
85
+ const ids = Object.keys(q.forQueries);
86
+ if (ids.length === 0) return undefined;
87
+
88
+ const lines = [];
89
+ for (const id of ids) {
90
+ const item = q.forQueries[id as PObjectId];
91
+ if (item.length === 0) continue;
92
+ const rendered = formatAxisQualifications(item);
93
+ lines.push(`${BULLET_1}${id}${rendered !== undefined ? ` ${rendered}` : ""}`);
94
+ }
95
+ return lines.length > 0
96
+ ? ["Anchors (bound via this variant)"].concat(lines).join("\n")
97
+ : undefined;
98
+ }
99
+
100
+ function formatHit(q: undefined | MatchQualifications): undefined | string {
101
+ if (isNil(q) || q.forHit.length === 0) return undefined;
102
+ const rendered = formatAxisQualifications(q.forHit);
103
+ if (rendered === undefined) return undefined;
104
+ return ["Hit column qualifications", `${BULLET_1}${rendered}`].join("\n");
105
+ }
106
+
107
+ function formatDistinctive(q: undefined | MatchQualifications): undefined | string {
108
+ if (isNil(q)) return undefined;
109
+ const bullets: string[] = [];
110
+ for (const id of Object.keys(q.forQueries)) {
111
+ for (const item of q.forQueries[id as PObjectId])
112
+ bullets.push(`${BULLET_1}${id}: ${formatQualification(item)}`);
113
+ }
114
+ for (const item of q.forHit) bullets.push(`${BULLET_1}hit: ${formatQualification(item)}`);
115
+ if (bullets.length === 0) return undefined;
116
+ return ["Distinctive (what separates this variant)", ...bullets].join("\n");
117
+ }
118
+
119
+ function formatAxisQualifications(qs: AxisQualification[]): undefined | string {
120
+ if (qs.length === 0) return undefined;
121
+ return qs.map(formatQualification).join("; ");
122
+ }
123
+
124
+ function formatQualification(q: AxisQualification): string {
125
+ const axisName = typeof q.axis === "string" ? q.axis : (q.axis.name ?? JSON.stringify(q.axis));
126
+ const entries = Object.entries(q.contextDomain);
127
+ if (entries.length === 0) return axisName;
128
+ const kv = entries.map(([k, v]) => `${k}=${v}`).join(", ");
129
+ return `${axisName} context: ${kv}`;
130
+ }
@@ -1 +1,2 @@
1
1
  export * from "./derive_distinct_labels";
2
+ export * from "./derive_distinct_tooltips";
package/src/render/api.ts CHANGED
@@ -3,6 +3,7 @@ import type {
3
3
  AnyFunction,
4
4
  AxisId,
5
5
  DataInfo,
6
+ ModelServices,
6
7
  Option,
7
8
  PColumn,
8
9
  PColumnLazy,
@@ -23,7 +24,6 @@ import type {
23
24
  PlRef,
24
25
  ResolveAnchorsOptions,
25
26
  ResultCollection,
26
- ServiceName,
27
27
  SUniversalPColumnId,
28
28
  ValueOrError,
29
29
  } from "@milaboratories/pl-model-common";
@@ -55,11 +55,8 @@ import type {
55
55
  PluginHandle,
56
56
  PluginFactoryLike,
57
57
  InferFactoryData,
58
- InferFactoryModelServices,
59
58
  InferFactoryParams,
60
59
  } from "../plugin_handle";
61
- import type { BlockDefaultModelServices } from "../services/service_resolve";
62
- import type { ModelServices as AllModelServices } from "@milaboratories/pl-model-common";
63
60
  import { TreeNodeAccessor, ifDef } from "./accessor";
64
61
  import type { FutureRef } from "./future";
65
62
  import type { AccessorHandle, GlobalCfgRenderCtx } from "./internal";
@@ -75,6 +72,7 @@ import type { APColumnSelectorWithSplit } from "./util/split_selectors";
75
72
  import { patchInSetFilters } from "./util/pframe_upgraders";
76
73
  import { allPColumnsReady } from "./util/pcolumn_data";
77
74
  import type { PColumnDataUniversal } from "./internal";
75
+ import { getService } from "../services";
78
76
 
79
77
  /**
80
78
  * Helper function to match domain objects
@@ -554,40 +552,11 @@ export class ResultPool implements ColumnProvider, AxisLabelProvider {
554
552
  }
555
553
 
556
554
  /** Main entry point to the API available within model lambdas (like outputs, sections, etc..) */
557
- export abstract class RenderCtxBase<
558
- Args = unknown,
559
- Data = unknown,
560
- ModelServices = Partial<AllModelServices>,
561
- > {
555
+ export abstract class RenderCtxBase<Args = unknown, Data = unknown> {
562
556
  protected readonly ctx: GlobalCfgRenderCtx;
563
- private readonly requiredServiceNames: ServiceName[];
564
- private cachedServices?: ModelServices;
565
557
 
566
- constructor(requiredServiceNames: ServiceName[] = []) {
558
+ constructor() {
567
559
  this.ctx = getCfgRenderCtx();
568
- this.requiredServiceNames = requiredServiceNames;
569
- }
570
-
571
- get services(): ModelServices {
572
- if (this.cachedServices) return this.cachedServices;
573
- const ctx = this.ctx;
574
- const services = Object.freeze(
575
- Object.fromEntries(
576
- this.requiredServiceNames.map((id) => [
577
- id,
578
- Object.freeze(
579
- Object.fromEntries(
580
- (ctx.getServiceMethods(id) as string[]).map((method) => [
581
- method,
582
- (...args: unknown[]) => ctx.callServiceMethod(id, method, ...args),
583
- ]),
584
- ),
585
- ),
586
- ]),
587
- ),
588
- ) as ModelServices;
589
- this.cachedServices = services;
590
- return services;
591
560
  }
592
561
 
593
562
  private dataCache?: { v: Data };
@@ -619,6 +588,10 @@ export abstract class RenderCtxBase<
619
588
  return this.activeArgsCache.v;
620
589
  }
621
590
 
591
+ public getService<T extends keyof ModelServices>(name: T): ModelServices[T] {
592
+ return getService(name);
593
+ }
594
+
622
595
  // /** Can be used to determine features provided by the desktop instance. */
623
596
  // public get featureFlags() {
624
597
  // return this.ctx.featureFlags;
@@ -771,11 +744,7 @@ export abstract class RenderCtxBase<
771
744
  }
772
745
 
773
746
  /** Main entry point to the API available within model lambdas (like outputs, sections, etc..) for v3+ blocks */
774
- export class BlockRenderCtx<
775
- Args = unknown,
776
- Data = unknown,
777
- ModelServices = BlockDefaultModelServices,
778
- > extends RenderCtxBase<Args, Data, ModelServices> {
747
+ export class BlockRenderCtx<Args = unknown, Data = unknown> extends RenderCtxBase<Args, Data> {
779
748
  private argsCache?: { v: Args | undefined };
780
749
  public get args(): Args | undefined {
781
750
  if (this.argsCache === undefined) {
@@ -824,19 +793,15 @@ export class RenderCtxLegacy<Args = unknown, UiState = unknown> extends RenderCt
824
793
  *
825
794
  * @typeParam F - PluginFactoryLike phantom carrying data/params/outputs types
826
795
  */
827
- export class PluginRenderCtx<
828
- F extends PluginFactoryLike = PluginFactoryLike,
829
- ModelServices = InferFactoryModelServices<F>,
830
- > extends RenderCtxBase<unknown, InferFactoryData<F>, ModelServices> {
796
+ export class PluginRenderCtx<F extends PluginFactoryLike = PluginFactoryLike> extends RenderCtxBase<
797
+ unknown,
798
+ InferFactoryData<F>
799
+ > {
831
800
  private readonly handle: PluginHandle<F>;
832
801
  private readonly wrappedInputs: Record<string, () => unknown>;
833
802
 
834
- constructor(
835
- handle: PluginHandle<F>,
836
- wrappedInputs: Record<string, () => unknown>,
837
- requiredServiceNames: ServiceName[] = [],
838
- ) {
839
- super(requiredServiceNames);
803
+ constructor(handle: PluginHandle<F>, wrappedInputs: Record<string, () => unknown>) {
804
+ super();
840
805
  this.handle = handle;
841
806
  this.wrappedInputs = wrappedInputs;
842
807
  }
@@ -0,0 +1,28 @@
1
+ import { ModelServices, ServiceName } from "@milaboratories/pl-model-common";
2
+ import { getCfgRenderCtx } from "../internal";
3
+ import { ValueOf } from "@milaboratories/helpers";
4
+ import { createServiceProxy } from "./service_bridge";
5
+ import { GlobalCfgRenderCtx } from "../render/internal";
6
+
7
+ const cachedServices = new WeakMap<
8
+ GlobalCfgRenderCtx,
9
+ Map<keyof ModelServices, ValueOf<ModelServices>>
10
+ >();
11
+
12
+ export function getService<T extends keyof ModelServices>(name: T): ModelServices[T] {
13
+ const ctx = getCfgRenderCtx();
14
+
15
+ const map = cachedServices.has(ctx)
16
+ ? cachedServices.get(ctx)!
17
+ : (() => {
18
+ cachedServices.set(ctx, new Map());
19
+ return cachedServices.get(ctx)!;
20
+ })();
21
+
22
+ return map.has(name)
23
+ ? (map.get(name) as ModelServices[T])
24
+ : (() => {
25
+ map.set(name, createServiceProxy(ctx)(name as ServiceName) as ModelServices[T]);
26
+ return map.get(name) as ModelServices[T];
27
+ })();
28
+ }
@@ -1,3 +1,4 @@
1
1
  export * from "./block_services";
2
2
  export * from "./service_resolve";
3
3
  export * from "./service_bridge";
4
+ export * from "./get_services";
@@ -15,7 +15,7 @@ import { UiServiceRegistry } from "@milaboratories/pl-model-common";
15
15
  // Makes a remote node service appear local.
16
16
  // Given a service ID, returns an object implementing the service's UI interface.
17
17
  // Provided by the desktop app (e.g. backed by Electron IPC).
18
- export type NodeServiceProxy = <S extends ServiceTypesLike>(
18
+ export type ServiceProxy = <S extends ServiceTypesLike>(
19
19
  serviceId: ServiceName<S>,
20
20
  ) => InferServiceUi<S>;
21
21
 
@@ -53,10 +53,10 @@ export function buildServices<S extends Partial<AllUiServices> = Partial<AllUiSe
53
53
  }
54
54
 
55
55
  /**
56
- * Builds a NodeServiceProxy from a ServiceDispatch.
56
+ * Builds a ServiceProxy from a ServiceDispatch.
57
57
  * Each service method call is forwarded to dispatch.callServiceMethod.
58
58
  */
59
- export function createNodeServiceProxy(dispatch: ServiceDispatch): NodeServiceProxy {
59
+ export function createServiceProxy(dispatch: ServiceDispatch): ServiceProxy {
60
60
  return ((serviceId: ServiceName) =>
61
61
  Object.freeze(
62
62
  Object.fromEntries(
@@ -64,8 +64,8 @@ export function createNodeServiceProxy(dispatch: ServiceDispatch): NodeServicePr
64
64
  .getServiceMethods(serviceId)
65
65
  .map((method) => [
66
66
  method,
67
- async (...args: unknown[]) => dispatch.callServiceMethod(serviceId, method, ...args),
67
+ (...args: unknown[]) => dispatch.callServiceMethod(serviceId, method, ...args),
68
68
  ]),
69
69
  ),
70
- )) as NodeServiceProxy;
70
+ )) as ServiceProxy;
71
71
  }