Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/cluster-level-property.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@platforma-open/milaboratories.repertoire-mutation-heatmap.workflow': minor
'@platforma-open/milaboratories.repertoire-mutation-heatmap.model': minor
'@platforma-open/milaboratories.repertoire-mutation-heatmap.block': minor
---

Support cluster-level inputs. A value computed per cluster (keyed by `pl7.app/clusterId`) is now resolved down to the individual variants through the clustering block's `variant→cluster` linker (`pl7.app/link`) with an explicit join in the precalc, so each variant inherits its cluster's value and the grid stays per variant. Applies to all three cluster-capable inputs:

- the `property` value mode,
- the property-range subset filter,
- the composition-enrichment view's per-round frequencies.

Discovery now traverses the linker (`findColumns` `maxHops: 1`) for both the property picker and the round-frequency picker, so cluster-keyed columns surface alongside variant-keyed ones; the linker column itself is excluded from the property picker.
39 changes: 22 additions & 17 deletions model/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,13 @@ export type BlockArgs = {
stateMatrixRef: PlRef;
/** Per-sample abundance `[sampleId, variantKey]` (required in abundance mode). */
abundanceRef?: PlRef;
/** Per-variant property `[variantKey] -> value` (required in property mode). */
/** Property column, property mode: variant- or cluster-keyed; a cluster value is resolved onto variants in the workflow. */
propertyRef?: SUniversalPColumnId;
/** Pre-aggregated known×sample abundance `[sampleId, knownVariantKey]` (heat map 2). */
knownAbundanceRef?: PlRef;
/**
* Ordered per-round frequency columns from the enrichment block (composition-enrichment view).
* Each is one round's `[variantKey] -> frequency` (`pl7.app/frequency`); `[0]` is the baseline round R0.
* Each is one round's `pl7.app/frequency` (variant- or cluster-keyed); `[0]` is the baseline round R0.
* Empty = composition-enrichment view off.
*/
roundFrequencyRefs: SUniversalPColumnId[];
Expand Down Expand Up @@ -233,12 +233,12 @@ export const platforma = BlockModelV3.create(dataModel)
),
)

// Per-variant numeric property columns for the `property` value-mode and the
// property-range filter. Discovered via findColumns (not getCanonicalOptions — see
// roundFrequencyOptions for why) anchored on BOTH the state matrix (profiler-keyed
// properties, e.g. mutations) and the abundance (enrichment-keyed properties). The
// anchor names `main` / `freqAnchor` must match the workflow's `bb.addAnchor(...)`.
// Numeric only (A-0015) — a mean of the property is what a cell shows.
// Numeric property columns (variant- or cluster-keyed) for the `property` value-mode
// and the property-range filter, anchored on BOTH the state matrix (profiler-keyed
// properties, e.g. mutations) and the abundance (enrichment-keyed). The anchor names
// `main` / `freqAnchor` must match the workflow's `bb.addAnchor(...)`. findColumns, not
// getCanonicalOptions — the latter's ids bake in column domains that fail to round-trip
// in the workflow's anchored query. Numeric only, since a cell shows a mean.
.output("propertyOptions", (ctx) => {
const stateMatrixRef = ctx.data.stateMatrixRef;
if (stateMatrixRef === undefined) return undefined;
Expand Down Expand Up @@ -266,14 +266,20 @@ export const platforma = BlockModelV3.create(dataModel)
const numeric = new Set(["Int", "Long", "Float", "Double"]);
const seen = new Set<string>();
const options: { label: string; value: SUniversalPColumnId }[] = [];
for (const m of collection.findColumns({ mode: "enrichment", maxHops: 0 })) {
if (!numeric.has(m.column.spec.valueType as string)) continue;
// maxHops 1 surfaces cluster-keyed properties across the variant→cluster linker
// (0-hop variant-keyed ones still match); the workflow resolves cluster ones per-variant.
for (const m of collection.findColumns({ mode: "enrichment", maxHops: 1 })) {
const s = m.column.spec;
// The variant→cluster linker itself is Int-valued and would otherwise pass the
// numeric filter and show up as a bogus "property".
if (s.annotations?.["pl7.app/isLinkerColumn"] === "true") continue;
if (!numeric.has(s.valueType as string)) continue;
// Dedup a column reachable via both anchors (by identity, not anchored id).
const dedupKey = m.column.spec.name + "|" + JSON.stringify(m.column.spec.domain ?? {});
const dedupKey = s.name + "|" + JSON.stringify(s.domain ?? {});
if (seen.has(dedupKey)) continue;
seen.add(dedupKey);
options.push({
label: (m.column.spec.annotations?.["pl7.app/label"] as string | undefined) ?? m.column.id,
label: (s.annotations?.["pl7.app/label"] as string | undefined) ?? m.column.id,
value: m.column.id as SUniversalPColumnId,
});
}
Expand All @@ -292,9 +298,8 @@ export const platforma = BlockModelV3.create(dataModel)
// in the workflow's anchored query. The anchor name `freqAnchor` must match the workflow's
// `bb.addAnchor("freqAnchor", ...)`; the discovered `column.id` resolves against it there.
//
// v1: variant-level only (maxHops 0). Cluster-keyed frequencies (enrichment on clustered
// abundance) need linker traversal (maxHops > 0) + adding the `variant→cluster` linker in
// the workflow (A-0016) — deferred, shared with the property-view cluster path.
// maxHops 1 so frequencies from enrichment run on clustered abundance (keyed by clusterId)
// surface across the variant→cluster linker; the workflow resolves them per-variant.
.output("roundFrequencyOptions", (ctx) => {
const abundanceRef = ctx.data.abundanceRef;
if (abundanceRef === undefined) return undefined;
Expand All @@ -316,7 +321,7 @@ export const platforma = BlockModelV3.create(dataModel)
const matches = collection.findColumns({
include: { name: "pl7.app/frequency" },
mode: "enrichment",
maxHops: 0,
maxHops: 1,
});

return matches.map((m) => ({
Expand Down Expand Up @@ -461,7 +466,7 @@ export const platforma = BlockModelV3.create(dataModel)
const sections: { type: "link"; href: `/${string}`; label: string }[] = [
{ type: "link", href: "/", label: "Mutation Heatmap" },
];
// Needs a baseline + at least one comparison round (see workflow's hasComposition).
// Needs a baseline + at least one comparison round.
if (ctx.data.roundFrequencyRefs.length >= 2) {
sections.push({ type: "link", href: "/composition", label: "Composition Enrichment" });
}
Expand Down
Loading
Loading