@platforma-sdk/model 1.68.5 → 1.68.7
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/dist/columns/column_collection_builder.cjs +8 -2
- package/dist/columns/column_collection_builder.cjs.map +1 -1
- package/dist/columns/column_collection_builder.d.ts +14 -3
- package/dist/columns/column_collection_builder.d.ts.map +1 -1
- package/dist/columns/column_collection_builder.js +8 -2
- package/dist/columns/column_collection_builder.js.map +1 -1
- package/dist/columns/ctx_column_sources.d.ts +1 -1
- package/dist/columns/index.d.ts +1 -1
- package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.cjs +50 -50
- package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.cjs.map +1 -1
- package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.d.ts +5 -10
- package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.d.ts.map +1 -1
- package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.js +50 -50
- package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.js.map +1 -1
- package/dist/components/PlDataTable/createPlDataTable/discoverColumns.cjs +16 -17
- package/dist/components/PlDataTable/createPlDataTable/discoverColumns.cjs.map +1 -1
- package/dist/components/PlDataTable/createPlDataTable/discoverColumns.d.ts +4 -4
- package/dist/components/PlDataTable/createPlDataTable/discoverColumns.d.ts.map +1 -1
- package/dist/components/PlDataTable/createPlDataTable/discoverColumns.js +16 -17
- package/dist/components/PlDataTable/createPlDataTable/discoverColumns.js.map +1 -1
- package/dist/components/PlDataTable/createPlDataTable/utils.cjs +8 -2
- package/dist/components/PlDataTable/createPlDataTable/utils.cjs.map +1 -1
- package/dist/components/PlDataTable/createPlDataTable/utils.js +8 -2
- package/dist/components/PlDataTable/createPlDataTable/utils.js.map +1 -1
- package/dist/components/PlDatasetSelector/filter_discovery.d.ts +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/labels/derive_distinct_labels.cjs +121 -50
- package/dist/labels/derive_distinct_labels.cjs.map +1 -1
- package/dist/labels/derive_distinct_labels.d.ts +30 -14
- package/dist/labels/derive_distinct_labels.d.ts.map +1 -1
- package/dist/labels/derive_distinct_labels.js +121 -50
- package/dist/labels/derive_distinct_labels.js.map +1 -1
- package/dist/labels/derive_distinct_tooltips.cjs +0 -10
- package/dist/labels/derive_distinct_tooltips.cjs.map +1 -1
- package/dist/labels/derive_distinct_tooltips.d.ts +2 -3
- package/dist/labels/derive_distinct_tooltips.d.ts.map +1 -1
- package/dist/labels/derive_distinct_tooltips.js +0 -10
- package/dist/labels/derive_distinct_tooltips.js.map +1 -1
- package/dist/labels/index.d.ts +1 -1
- package/dist/package.cjs +1 -1
- package/dist/package.js +1 -1
- package/package.json +4 -4
- package/src/columns/column_collection_builder.test.ts +0 -2
- package/src/columns/column_collection_builder.ts +26 -3
- package/src/components/PlDataTable/createPlDataTable/createPlDataTableV3.ts +90 -75
- package/src/components/PlDataTable/createPlDataTable/discoverColumns.ts +31 -34
- package/src/components/PlDataTable/createPlDataTable/utils.test.ts +1 -1
- package/src/components/PlDataTable/createPlDataTable/utils.ts +11 -4
- package/src/labels/derive_distinct_labels.test.ts +396 -52
- package/src/labels/derive_distinct_labels.ts +205 -103
- package/src/labels/derive_distinct_tooltips.test.ts +1 -22
- package/src/labels/derive_distinct_tooltips.ts +1 -18
|
@@ -2,18 +2,33 @@ import {
|
|
|
2
2
|
Annotation,
|
|
3
3
|
parseJson,
|
|
4
4
|
readAnnotation,
|
|
5
|
+
type AxisQualification,
|
|
6
|
+
type PObjectId,
|
|
5
7
|
type PObjectSpec,
|
|
6
8
|
type StringifiedJson,
|
|
7
9
|
type Trace,
|
|
8
10
|
} from "@milaboratories/pl-model-common";
|
|
9
11
|
import { throwError } from "@milaboratories/helpers";
|
|
10
12
|
import { isFunction, isNil } from "es-toolkit";
|
|
13
|
+
import type { MatchQualifications } from "../columns/column_collection_builder";
|
|
11
14
|
|
|
12
15
|
export type { Trace, TraceEntry } from "@milaboratories/pl-model-common";
|
|
13
16
|
|
|
14
17
|
const DISTANCE_PENALTY = 0.001;
|
|
15
18
|
const LABEL_TYPE = "__LABEL__";
|
|
16
19
|
const LABEL_TYPE_FULL = "__LABEL__@1";
|
|
20
|
+
const LINKER_TYPE = "__LINKER__";
|
|
21
|
+
const LINKER_TYPE_FULL = "__LINKER__@1";
|
|
22
|
+
const HIT_QUAL_TYPE = "__HIT_QUAL__";
|
|
23
|
+
const ANCHOR_QUAL_TYPE_PREFIX = "__ANCHOR_QUAL__:";
|
|
24
|
+
|
|
25
|
+
function isAnchorQualType(t: string): boolean {
|
|
26
|
+
return t.startsWith(ANCHOR_QUAL_TYPE_PREFIX);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isSyntheticType(t: string): boolean {
|
|
30
|
+
return t === LINKER_TYPE || t === HIT_QUAL_TYPE || isAnchorQualType(t);
|
|
31
|
+
}
|
|
17
32
|
|
|
18
33
|
/** SDK-internal trace shape — adds fields used by this algorithm only, not part of the on-disk contract. */
|
|
19
34
|
type ExtendedTraceEntry = Trace[number] & {
|
|
@@ -21,33 +36,66 @@ type ExtendedTraceEntry = Trace[number] & {
|
|
|
21
36
|
position?: "prefix" | "suffix";
|
|
22
37
|
};
|
|
23
38
|
|
|
39
|
+
export type LinkerStep = {
|
|
40
|
+
spec: PObjectSpec;
|
|
41
|
+
qualifications?: AxisQualification[];
|
|
42
|
+
};
|
|
43
|
+
|
|
24
44
|
export type Entry =
|
|
25
45
|
| PObjectSpec
|
|
26
46
|
| {
|
|
27
47
|
spec: PObjectSpec;
|
|
28
48
|
/** Extra trace entries merged with the base trace from annotations. */
|
|
29
49
|
extraTrace?: ExtendedTraceEntry[];
|
|
30
|
-
/** Linker steps traversed to discover this column;
|
|
31
|
-
linkerPath?:
|
|
50
|
+
/** Linker steps traversed to discover this column; rendered as "via …" only when needed for uniqueness. */
|
|
51
|
+
linkerPath?: LinkerStep[];
|
|
52
|
+
/** Axis qualifications applied to the hit column / already-bound anchors; rendered as "[…]" suffixes. */
|
|
53
|
+
qualifications?: MatchQualifications;
|
|
32
54
|
};
|
|
33
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Per-zone formatters. Each one receives raw inputs and returns the rendered text for that zone,
|
|
58
|
+
* or `undefined` to suppress the zone entirely (no synthetic injection → no minimization, no render).
|
|
59
|
+
*/
|
|
60
|
+
export type DeriveLabelsFormatters = {
|
|
61
|
+
/** Native column label. Default: identity. `undefined` → label entry not added (treated as if spec had no label). */
|
|
62
|
+
native?: (label: string, spec: PObjectSpec, index: number) => string | undefined;
|
|
63
|
+
/** Linker zone (whole "via …" piece). Receives step labels with step-quals already inlined.
|
|
64
|
+
* Default: `via ${steps.join(" > ")}`. */
|
|
65
|
+
linker?: (linkerLabels: string[], spec: PObjectSpec, index: number) => string | undefined;
|
|
66
|
+
/** Per-step linker qualifications inlined into the step base label.
|
|
67
|
+
* Default: `[${formatQualifications(qs)}]`. `undefined` → step rendered without quals. */
|
|
68
|
+
linkerStepQualification?: (
|
|
69
|
+
qualifications: AxisQualification[],
|
|
70
|
+
stepIndex: number,
|
|
71
|
+
stepSpec: PObjectSpec,
|
|
72
|
+
) => string | undefined;
|
|
73
|
+
/** Hit-axis qualifications block. Default: `[${formatQualifications(qs)}]`. */
|
|
74
|
+
hitQualification?: (
|
|
75
|
+
qualifications: AxisQualification[],
|
|
76
|
+
spec: PObjectSpec,
|
|
77
|
+
index: number,
|
|
78
|
+
) => string | undefined;
|
|
79
|
+
/** Per-anchor qualifications block. Default: `[${anchorId}: ${formatQualifications(qs)}]`. */
|
|
80
|
+
anchorQualification?: (
|
|
81
|
+
anchorId: PObjectId,
|
|
82
|
+
qualifications: AxisQualification[],
|
|
83
|
+
spec: PObjectSpec,
|
|
84
|
+
index: number,
|
|
85
|
+
) => string | undefined;
|
|
86
|
+
};
|
|
87
|
+
|
|
34
88
|
export type DeriveLabelsOptions = {
|
|
35
|
-
/** Separator to use between label parts (" / " by default) */
|
|
89
|
+
/** Separator to use between label parts (" / " by default). */
|
|
36
90
|
separator?: string;
|
|
37
|
-
/** If true, label
|
|
91
|
+
/** If true, native label is appended at the end of the trace zone. By default it is prepended (label is the most important name). */
|
|
38
92
|
addLabelAsSuffix?: boolean;
|
|
39
|
-
/** Force inclusion of native column label */
|
|
93
|
+
/** Force inclusion of native column label even when not needed for uniqueness. */
|
|
40
94
|
includeNativeLabel?: boolean;
|
|
41
|
-
/** Trace
|
|
95
|
+
/** Trace types that must be included in the label. */
|
|
42
96
|
forceTraceElements?: string[];
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
* If returns undefined, no linker suffix is appended. By default labels are joined with " > " and prefixed with "via ". */
|
|
46
|
-
linkerLabelFormatter?: (
|
|
47
|
-
linkerLabels: string[],
|
|
48
|
-
spec: PObjectSpec,
|
|
49
|
-
index: number,
|
|
50
|
-
) => string | undefined;
|
|
97
|
+
/** Per-zone custom formatters. Returning `undefined` from any formatter suppresses the corresponding zone. */
|
|
98
|
+
formatters?: DeriveLabelsFormatters;
|
|
51
99
|
};
|
|
52
100
|
|
|
53
101
|
export function deriveDistinctLabels(values: Entry[], options: DeriveLabelsOptions = {}): string[] {
|
|
@@ -57,23 +105,21 @@ export function deriveDistinctLabels(values: Entry[], options: DeriveLabelsOptio
|
|
|
57
105
|
: undefined;
|
|
58
106
|
const separator = options.separator ?? " / ";
|
|
59
107
|
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
const spec = "spec" in v && typeof v.spec === "object" ? v.spec : (v as PObjectSpec);
|
|
63
|
-
const linkerLabels = extractLinkerLabels(v);
|
|
64
|
-
if (linkerLabels.length === 0) return undefined;
|
|
65
|
-
return isFunction(options.linkerLabelFormatter)
|
|
66
|
-
? options.linkerLabelFormatter(linkerLabels, spec, i)
|
|
67
|
-
: `via ${linkerLabels.join(" > ")}`;
|
|
68
|
-
});
|
|
108
|
+
const records = values.map((v, i) => enrichRecord(v, i, options));
|
|
109
|
+
const stats = collectTypeStats(records);
|
|
69
110
|
|
|
70
|
-
|
|
71
|
-
const
|
|
111
|
+
const hasAnySynthetic = records.some((r) => r.fullTrace.some((ft) => isSyntheticType(ft.type)));
|
|
112
|
+
const labelForced =
|
|
113
|
+
(options.includeNativeLabel === true || hasAnySynthetic) &&
|
|
114
|
+
stats.countByType.has(LABEL_TYPE_FULL);
|
|
115
|
+
// Tied to labeled-step presence, not path presence: entries with a non-empty linkerPath
|
|
116
|
+
// but no labeled steps contribute no LINKER_TYPE trace entry, so they do not count here.
|
|
117
|
+
const linkerForced = stats.countByType.get(LINKER_TYPE_FULL) === values.length;
|
|
72
118
|
|
|
73
|
-
|
|
74
|
-
|
|
119
|
+
const forcedSet = new Set<string>();
|
|
120
|
+
if (labelForced) forcedSet.add(LABEL_TYPE_FULL);
|
|
121
|
+
if (linkerForced) forcedSet.add(LINKER_TYPE_FULL);
|
|
75
122
|
|
|
76
|
-
// Phase 3: classify types into main (present everywhere) and secondary
|
|
77
123
|
const { mainTypes, secondaryTypes } = classifyTypes(stats, values.length);
|
|
78
124
|
|
|
79
125
|
const build = (typeSet: Set<string>, force: boolean) =>
|
|
@@ -83,28 +129,16 @@ export function deriveDistinctLabels(values: Entry[], options: DeriveLabelsOptio
|
|
|
83
129
|
if (secondaryTypes.length !== 0)
|
|
84
130
|
throw new Error("Non-empty secondary types list while main types list is empty.");
|
|
85
131
|
|
|
86
|
-
return
|
|
87
|
-
build(new Set(LABEL_TYPE_FULL), true) ??
|
|
88
|
-
|
|
89
|
-
linkerSuffixes,
|
|
132
|
+
return (
|
|
133
|
+
build(new Set([LABEL_TYPE_FULL]), true) ??
|
|
134
|
+
throwError("Failed to derive labels using native column labels")
|
|
90
135
|
);
|
|
91
136
|
}
|
|
92
137
|
|
|
93
|
-
// Phase 4: search for minimal type set that produces unique labels
|
|
94
|
-
//
|
|
95
|
-
// includedCount = 2
|
|
96
|
-
// * *
|
|
97
|
-
// T0 T1 T2 T3 T4 T5
|
|
98
|
-
// *
|
|
99
|
-
// additionalType = 3
|
|
100
|
-
//
|
|
101
|
-
// Resulting set: T0, T1, T3
|
|
102
|
-
//
|
|
103
138
|
let includedCount = 0;
|
|
104
139
|
let additionalType = -1;
|
|
105
140
|
while (includedCount < mainTypes.length) {
|
|
106
|
-
const currentSet = new Set<string>();
|
|
107
|
-
if (options.includeNativeLabel) currentSet.add(LABEL_TYPE_FULL);
|
|
141
|
+
const currentSet = new Set<string>(forcedSet);
|
|
108
142
|
for (let i = 0; i < includedCount; ++i) currentSet.add(mainTypes[i]);
|
|
109
143
|
if (additionalType >= 0) currentSet.add(mainTypes[additionalType]);
|
|
110
144
|
|
|
@@ -115,13 +149,10 @@ export function deriveDistinctLabels(values: Entry[], options: DeriveLabelsOptio
|
|
|
115
149
|
records,
|
|
116
150
|
stats,
|
|
117
151
|
forceTraceElements,
|
|
118
|
-
|
|
152
|
+
forcedSet,
|
|
119
153
|
separator,
|
|
120
154
|
);
|
|
121
|
-
return
|
|
122
|
-
build(minimized, false) ?? throwError("Failed to derive unique labels"),
|
|
123
|
-
linkerSuffixes,
|
|
124
|
-
);
|
|
155
|
+
return build(minimized, false) ?? throwError("Failed to derive unique labels");
|
|
125
156
|
}
|
|
126
157
|
|
|
127
158
|
additionalType++;
|
|
@@ -131,42 +162,16 @@ export function deriveDistinctLabels(values: Entry[], options: DeriveLabelsOptio
|
|
|
131
162
|
}
|
|
132
163
|
}
|
|
133
164
|
|
|
134
|
-
|
|
135
|
-
const fallbackSet = new Set([...mainTypes, ...secondaryTypes]);
|
|
165
|
+
const fallbackSet = new Set([...forcedSet, ...mainTypes, ...secondaryTypes]);
|
|
136
166
|
const minimized = minimizeTypeSet(
|
|
137
167
|
fallbackSet,
|
|
138
168
|
records,
|
|
139
169
|
stats,
|
|
140
170
|
forceTraceElements,
|
|
141
|
-
|
|
171
|
+
forcedSet,
|
|
142
172
|
separator,
|
|
143
173
|
);
|
|
144
|
-
return
|
|
145
|
-
build(minimized, true) ?? throwError("Failed to derive unique labels"),
|
|
146
|
-
linkerSuffixes,
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/** Apply pre-formatted linker suffixes to labels that have them. */
|
|
151
|
-
function applyLinkerSuffixes(labels: string[], suffixes: (string | undefined)[]): string[] {
|
|
152
|
-
return labels.map((label, i) => (isNil(suffixes[i]) ? label : `${label} ${suffixes[i]}`));
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/** Extract linker labels from every step of the linkers path. */
|
|
156
|
-
function extractLinkerLabels(entry: Entry): string[] {
|
|
157
|
-
if (!("spec" in entry) || typeof entry.spec !== "object") return [];
|
|
158
|
-
const path = entry.linkerPath;
|
|
159
|
-
if (path === undefined || path.length === 0) return [];
|
|
160
|
-
const labels: string[] = [];
|
|
161
|
-
for (const step of path) {
|
|
162
|
-
const label = (
|
|
163
|
-
readAnnotation(step.spec, Annotation.LinkLabel) ?? readAnnotation(step.spec, Annotation.Label)
|
|
164
|
-
)?.trim();
|
|
165
|
-
if (label !== undefined && label.length > 0) {
|
|
166
|
-
labels.push(label);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
return labels;
|
|
174
|
+
return build(minimized, true) ?? throwError("Failed to derive unique labels");
|
|
170
175
|
}
|
|
171
176
|
|
|
172
177
|
// --- Pure helpers ---
|
|
@@ -176,19 +181,57 @@ type EnrichedRecord = {
|
|
|
176
181
|
fullTrace: FullTraceEntry[];
|
|
177
182
|
};
|
|
178
183
|
|
|
179
|
-
function
|
|
184
|
+
function extractEntryParts(entry: Entry): {
|
|
180
185
|
spec: PObjectSpec;
|
|
181
186
|
extraTrace: ExtendedTraceEntry[] | undefined;
|
|
182
|
-
linkerPath:
|
|
187
|
+
linkerPath: LinkerStep[] | undefined;
|
|
188
|
+
qualifications: MatchQualifications | undefined;
|
|
183
189
|
} {
|
|
184
190
|
const isEnriched = "spec" in entry && typeof entry.spec === "object";
|
|
191
|
+
if (!isEnriched) {
|
|
192
|
+
return {
|
|
193
|
+
spec: entry as PObjectSpec,
|
|
194
|
+
extraTrace: undefined,
|
|
195
|
+
linkerPath: undefined,
|
|
196
|
+
qualifications: undefined,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
185
199
|
return {
|
|
186
|
-
spec:
|
|
187
|
-
extraTrace:
|
|
188
|
-
linkerPath:
|
|
200
|
+
spec: entry.spec,
|
|
201
|
+
extraTrace: entry.extraTrace,
|
|
202
|
+
linkerPath: entry.linkerPath,
|
|
203
|
+
qualifications: entry.qualifications,
|
|
189
204
|
};
|
|
190
205
|
}
|
|
191
206
|
|
|
207
|
+
function formatQualification(q: AxisQualification): string {
|
|
208
|
+
const ctx = q.contextDomain ?? {};
|
|
209
|
+
const keys = Object.keys(ctx);
|
|
210
|
+
if (keys.length === 0) return q.axis.name;
|
|
211
|
+
const pairs = keys.map((k) => `${k}=${ctx[k]}`).join(", ");
|
|
212
|
+
return Object.prototype.hasOwnProperty.call(ctx, q.axis.name) ? pairs : `${q.axis.name} ${pairs}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function formatQualifications(qs: AxisQualification[]): string {
|
|
216
|
+
return qs.map(formatQualification).join("; ");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function computeStepLabel(
|
|
220
|
+
step: LinkerStep,
|
|
221
|
+
stepIndex: number,
|
|
222
|
+
formatters: DeriveLabelsFormatters | undefined,
|
|
223
|
+
): string | undefined {
|
|
224
|
+
const base = (
|
|
225
|
+
readAnnotation(step.spec, Annotation.LinkLabel) ?? readAnnotation(step.spec, Annotation.Label)
|
|
226
|
+
)?.trim();
|
|
227
|
+
if (isNil(base) || base.length === 0) return undefined;
|
|
228
|
+
if (step.qualifications === undefined || step.qualifications.length === 0) return base;
|
|
229
|
+
const qualText = isFunction(formatters?.linkerStepQualification)
|
|
230
|
+
? formatters.linkerStepQualification(step.qualifications, stepIndex, step.spec)
|
|
231
|
+
: `[${formatQualifications(step.qualifications)}]`;
|
|
232
|
+
return isNil(qualText) ? base : `${base} ${qualText}`;
|
|
233
|
+
}
|
|
234
|
+
|
|
192
235
|
function buildFullTrace(trace: ExtendedTraceEntry[]): FullTraceEntry[] {
|
|
193
236
|
const result: FullTraceEntry[] = [];
|
|
194
237
|
const occurrences = new Map<string, number>();
|
|
@@ -208,22 +251,65 @@ function buildFullTrace(trace: ExtendedTraceEntry[]): FullTraceEntry[] {
|
|
|
208
251
|
return result;
|
|
209
252
|
}
|
|
210
253
|
|
|
211
|
-
function enrichRecord(value: Entry, options: DeriveLabelsOptions): EnrichedRecord {
|
|
212
|
-
const { spec, extraTrace } =
|
|
254
|
+
function enrichRecord(value: Entry, index: number, options: DeriveLabelsOptions): EnrichedRecord {
|
|
255
|
+
const { spec, extraTrace, linkerPath, qualifications } = extractEntryParts(value);
|
|
256
|
+
const formatters = options.formatters;
|
|
213
257
|
|
|
214
|
-
const
|
|
258
|
+
const rawLabel = readAnnotation(spec, Annotation.Label);
|
|
215
259
|
const traceStr = readAnnotation(spec, Annotation.Trace);
|
|
216
260
|
const baseTrace = traceStr
|
|
217
261
|
? (parseJson(traceStr as StringifiedJson<ExtendedTraceEntry[]>) ?? [])
|
|
218
262
|
: [];
|
|
219
263
|
const prefixExtra = extraTrace?.filter((e) => e.position === "prefix") ?? [];
|
|
220
264
|
const suffixExtra = extraTrace?.filter((e) => e.position !== "prefix") ?? [];
|
|
221
|
-
const trace = [...prefixExtra, ...baseTrace, ...suffixExtra];
|
|
265
|
+
const trace: ExtendedTraceEntry[] = [...prefixExtra, ...baseTrace, ...suffixExtra];
|
|
266
|
+
|
|
267
|
+
if (!isNil(rawLabel)) {
|
|
268
|
+
const label = isFunction(formatters?.native)
|
|
269
|
+
? formatters.native(rawLabel, spec, index)
|
|
270
|
+
: rawLabel;
|
|
271
|
+
if (!isNil(label)) {
|
|
272
|
+
const labelEntry = { label, type: LABEL_TYPE, importance: -2 };
|
|
273
|
+
if (options.addLabelAsSuffix === true) trace.push(labelEntry);
|
|
274
|
+
else trace.splice(0, 0, labelEntry);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
222
277
|
|
|
223
|
-
if (
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
278
|
+
if (linkerPath !== undefined && linkerPath.length > 0) {
|
|
279
|
+
const stepLabels = linkerPath
|
|
280
|
+
.map((step, i) => computeStepLabel(step, i, formatters))
|
|
281
|
+
.filter((s): s is string => !isNil(s));
|
|
282
|
+
if (stepLabels.length > 0) {
|
|
283
|
+
const linkerText = isFunction(formatters?.linker)
|
|
284
|
+
? formatters.linker(stepLabels, spec, index)
|
|
285
|
+
: `via ${stepLabels.join(" > ")}`;
|
|
286
|
+
if (!isNil(linkerText)) {
|
|
287
|
+
trace.push({ type: LINKER_TYPE, label: linkerText, importance: -10 });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (qualifications !== undefined) {
|
|
293
|
+
for (const [anchorId, qs] of Object.entries(qualifications.forQueries)) {
|
|
294
|
+
if (qs.length === 0) continue;
|
|
295
|
+
const anchorText = isFunction(formatters?.anchorQualification)
|
|
296
|
+
? formatters.anchorQualification(anchorId as PObjectId, qs, spec, index)
|
|
297
|
+
: `[${anchorId}: ${formatQualifications(qs)}]`;
|
|
298
|
+
if (isNil(anchorText)) continue;
|
|
299
|
+
trace.push({
|
|
300
|
+
type: `${ANCHOR_QUAL_TYPE_PREFIX}${anchorId}`,
|
|
301
|
+
label: anchorText,
|
|
302
|
+
importance: -11,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
if (qualifications.forHit.length > 0) {
|
|
306
|
+
const hitText = isFunction(formatters?.hitQualification)
|
|
307
|
+
? formatters.hitQualification(qualifications.forHit, spec, index)
|
|
308
|
+
: `[${formatQualifications(qualifications.forHit)}]`;
|
|
309
|
+
if (!isNil(hitText)) {
|
|
310
|
+
trace.push({ type: HIT_QUAL_TYPE, label: hitText, importance: -12 });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
227
313
|
}
|
|
228
314
|
|
|
229
315
|
return { fullTrace: buildFullTrace(trace) };
|
|
@@ -283,20 +369,40 @@ function buildLabels(
|
|
|
283
369
|
const result: string[] = [];
|
|
284
370
|
|
|
285
371
|
for (const r of records) {
|
|
286
|
-
const
|
|
372
|
+
const traceParts: string[] = [];
|
|
373
|
+
const anchorParts: string[] = [];
|
|
374
|
+
let linkerLabel: string | undefined;
|
|
375
|
+
let hitLabel: string | undefined;
|
|
376
|
+
|
|
287
377
|
for (const ft of r.fullTrace) {
|
|
288
|
-
if (includedTypes.has(ft.fullType) || forceTraceElements?.has(ft.type))
|
|
289
|
-
|
|
290
|
-
|
|
378
|
+
if (!(includedTypes.has(ft.fullType) || forceTraceElements?.has(ft.type))) continue;
|
|
379
|
+
if (ft.type === LINKER_TYPE) linkerLabel = ft.label;
|
|
380
|
+
else if (ft.type === HIT_QUAL_TYPE) hitLabel = ft.label;
|
|
381
|
+
else if (isAnchorQualType(ft.type)) anchorParts.push(ft.label);
|
|
382
|
+
else traceParts.push(ft.label);
|
|
291
383
|
}
|
|
292
384
|
|
|
293
|
-
|
|
385
|
+
const isEmpty =
|
|
386
|
+
traceParts.length === 0 &&
|
|
387
|
+
anchorParts.length === 0 &&
|
|
388
|
+
linkerLabel === undefined &&
|
|
389
|
+
hitLabel === undefined;
|
|
390
|
+
|
|
391
|
+
if (isEmpty) {
|
|
294
392
|
if (!force) return undefined;
|
|
295
393
|
result.push("Unlabeled");
|
|
296
394
|
continue;
|
|
297
395
|
}
|
|
298
396
|
|
|
299
|
-
|
|
397
|
+
let label = traceParts.join(separator);
|
|
398
|
+
const append = (part: string) => {
|
|
399
|
+
label = label.length === 0 ? part : `${label} ${part}`;
|
|
400
|
+
};
|
|
401
|
+
if (linkerLabel !== undefined) append(linkerLabel);
|
|
402
|
+
for (const a of anchorParts) append(a);
|
|
403
|
+
if (hitLabel !== undefined) append(hitLabel);
|
|
404
|
+
|
|
405
|
+
result.push(label);
|
|
300
406
|
}
|
|
301
407
|
|
|
302
408
|
return result;
|
|
@@ -312,7 +418,7 @@ function minimizeTypeSet(
|
|
|
312
418
|
records: EnrichedRecord[],
|
|
313
419
|
stats: TypeStats,
|
|
314
420
|
forceTraceElements: Set<string> | undefined,
|
|
315
|
-
|
|
421
|
+
forcedSet: Set<string>,
|
|
316
422
|
separator: string,
|
|
317
423
|
): Set<string> {
|
|
318
424
|
const initialResult = buildLabels(records, typeSet, forceTraceElements, separator, false);
|
|
@@ -322,11 +428,7 @@ function minimizeTypeSet(
|
|
|
322
428
|
const result = new Set(typeSet);
|
|
323
429
|
|
|
324
430
|
const removable = [...result]
|
|
325
|
-
.filter(
|
|
326
|
-
(t) =>
|
|
327
|
-
!forceTraceElements?.has(t.split("@")[0]) &&
|
|
328
|
-
!(options.includeNativeLabel && t === LABEL_TYPE_FULL),
|
|
329
|
-
)
|
|
431
|
+
.filter((t) => !forceTraceElements?.has(t.split("@")[0]) && !forcedSet.has(t))
|
|
330
432
|
.sort((a, b) => (stats.importances.get(a) ?? 0) - (stats.importances.get(b) ?? 0));
|
|
331
433
|
|
|
332
434
|
for (const typeToRemove of removable) {
|
|
@@ -110,22 +110,6 @@ describe("deriveDistinctTooltips", () => {
|
|
|
110
110
|
expect(tooltip).toContain("sample context: batch=B");
|
|
111
111
|
});
|
|
112
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
113
|
test("variantCount > 1 adds Variant N of M line in header", () => {
|
|
130
114
|
const entries: TooltipEntry[] = [
|
|
131
115
|
{
|
|
@@ -195,21 +179,16 @@ describe("deriveDistinctTooltips", () => {
|
|
|
195
179
|
forQueries: { ["main" as PObjectId]: [axisQualification("sample", { batch: "B" })] },
|
|
196
180
|
forHit: [axisQualification("sample", { batch: "B" })],
|
|
197
181
|
},
|
|
198
|
-
distinctiveQualifications: {
|
|
199
|
-
forQueries: { ["main" as PObjectId]: [axisQualification("sample", { batch: "B" })] },
|
|
200
|
-
forHit: [],
|
|
201
|
-
},
|
|
202
182
|
},
|
|
203
183
|
];
|
|
204
184
|
const [tooltip] = deriveDistinctTooltips(entries);
|
|
205
185
|
expect(tooltip).toBeDefined();
|
|
206
186
|
const sections = tooltip!.split("\n\n");
|
|
207
|
-
expect(sections.length).toBe(
|
|
187
|
+
expect(sections.length).toBe(4);
|
|
208
188
|
expect(sections[0]).toContain("Variant: 2 of 2");
|
|
209
189
|
expect(sections[1]).toContain("Origin path");
|
|
210
190
|
expect(sections[2]).toContain("Anchors");
|
|
211
191
|
expect(sections[3]).toContain("Hit column qualifications");
|
|
212
|
-
expect(sections[4]).toContain("Distinctive");
|
|
213
192
|
});
|
|
214
193
|
|
|
215
194
|
test("parallel results — array aligns with input", () => {
|
|
@@ -11,10 +11,8 @@ import type { MatchQualifications, MatchVariant } from "../columns";
|
|
|
11
11
|
export type TooltipEntry = {
|
|
12
12
|
/** Main column spec — used for column-name fallback when no label. */
|
|
13
13
|
spec: PColumnSpec;
|
|
14
|
-
/**
|
|
14
|
+
/** Qualifications carried by this variant. */
|
|
15
15
|
qualifications?: MatchQualifications;
|
|
16
|
-
/** Minimal qualifications that separate this variant from its siblings. */
|
|
17
|
-
distinctiveQualifications?: MatchQualifications;
|
|
18
16
|
/** Linker steps traversed to reach the hit column. */
|
|
19
17
|
linkerPath?: MatchVariant["path"];
|
|
20
18
|
/** Position of this variant within the same physical column (1-based). */
|
|
@@ -43,9 +41,6 @@ function formatTooltip(entry: TooltipEntry): undefined | string {
|
|
|
43
41
|
const hit = formatHit(entry.qualifications);
|
|
44
42
|
if (hit !== undefined) sections.push(hit);
|
|
45
43
|
|
|
46
|
-
const distinctive = formatDistinctive(entry.distinctiveQualifications);
|
|
47
|
-
if (distinctive !== undefined) sections.push(distinctive);
|
|
48
|
-
|
|
49
44
|
if (sections.length <= 1) return undefined;
|
|
50
45
|
return sections.join("\n\n");
|
|
51
46
|
}
|
|
@@ -104,18 +99,6 @@ function formatHit(q: undefined | MatchQualifications): undefined | string {
|
|
|
104
99
|
return ["Hit column qualifications", `${BULLET_1}${rendered}`].join("\n");
|
|
105
100
|
}
|
|
106
101
|
|
|
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
102
|
function formatAxisQualifications(qs: AxisQualification[]): undefined | string {
|
|
120
103
|
if (qs.length === 0) return undefined;
|
|
121
104
|
return qs.map(formatQualification).join("; ");
|