@platforma-open/milaboratories.humanization-score.model 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@platforma-open/milaboratories.humanization-score.model",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Block model",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
8
  "dependencies": {
9
9
  "@platforma-sdk/model": "1.77.15",
10
- "@milaboratories/helpers": "1.14.2"
10
+ "@milaboratories/helpers": "1.14.2",
11
+ "@milaboratories/graph-maker": "1.4.3"
11
12
  },
12
13
  "devDependencies": {
13
14
  "@milaboratories/ts-builder": "1.5.0",
package/src/index.ts CHANGED
@@ -1,4 +1,7 @@
1
+ import type { GraphMakerState } from '@milaboratories/graph-maker';
1
2
  import type {
3
+ PColumnIdAndSpec,
4
+ PFrameHandle,
2
5
  PlDataTableStateV2,
3
6
  PlRef,
4
7
  } from '@platforma-sdk/model';
@@ -6,6 +9,7 @@ import {
6
9
  ArrayColumnProvider,
7
10
  BlockModelV3,
8
11
  DataModelBuilder,
12
+ createPFrameForGraphs,
9
13
  createPlDataTableStateV2,
10
14
  createPlDataTableV3,
11
15
  } from '@platforma-sdk/model';
@@ -19,6 +23,8 @@ type OldArgs = {
19
23
 
20
24
  type OldUiState = {
21
25
  tableState: PlDataTableStateV2;
26
+ graphStateHistogram?: GraphMakerState;
27
+ graphStateBoxplot?: GraphMakerState;
22
28
  };
23
29
 
24
30
  export type BlockData = {
@@ -26,17 +32,59 @@ export type BlockData = {
26
32
  inputAnchor?: PlRef;
27
33
  mem?: number;
28
34
  tableState: PlDataTableStateV2;
35
+ // Distribution of the per-clonotype humanness score across the whole dataset.
36
+ graphStateHistogram: GraphMakerState;
37
+ // Per-sample distribution of humanness (box/violin), grouped by sampleId.
38
+ graphStateBoxplot: GraphMakerState;
29
39
  };
30
40
 
41
+ // Humanness score column name emitted by `clonotype-process.tpl.tengo`.
42
+ export const HUMANNESS_SCORE_COLUMN = 'pl7.app/humannessScore';
43
+
44
+ export const defaultGraphStateHistogram = (): GraphMakerState => ({
45
+ title: 'Humanness Score Distribution',
46
+ template: 'bins',
47
+ currentTab: null,
48
+ axesSettings: {
49
+ other: { binsCount: 20 },
50
+ },
51
+ });
52
+
53
+ export const defaultGraphStateBoxplot = (): GraphMakerState => ({
54
+ title: 'Humanness by Sample',
55
+ template: 'box',
56
+ currentTab: null,
57
+ });
58
+
59
+ // Selectors for the input dataset anchor — shared between `inputOptions`
60
+ // (the dropdown) and `subtitle` (so the default label matches the dataset name).
61
+ const inputSelectors = [{
62
+ axes: [
63
+ { name: 'pl7.app/sampleId' },
64
+ { name: 'pl7.app/vdj/clonotypeKey' },
65
+ ],
66
+ annotations: { 'pl7.app/isAnchor': 'true' },
67
+ }, {
68
+ axes: [
69
+ { name: 'pl7.app/sampleId' },
70
+ { name: 'pl7.app/vdj/scClonotypeKey' },
71
+ ],
72
+ annotations: { 'pl7.app/isAnchor': 'true' },
73
+ }];
74
+
31
75
  const dataModel = new DataModelBuilder()
32
76
  .from<BlockData>('v1')
33
77
  .upgradeLegacy<OldArgs, OldUiState>(({ args, uiState }) => ({
34
78
  ...args,
35
79
  tableState: uiState.tableState,
80
+ graphStateHistogram: uiState.graphStateHistogram ?? defaultGraphStateHistogram(),
81
+ graphStateBoxplot: uiState.graphStateBoxplot ?? defaultGraphStateBoxplot(),
36
82
  }))
37
83
  .init(() => ({
38
84
  customBlockLabel: '',
39
85
  tableState: createPlDataTableStateV2(),
86
+ graphStateHistogram: defaultGraphStateHistogram(),
87
+ graphStateBoxplot: defaultGraphStateBoxplot(),
40
88
  }));
41
89
 
42
90
  export const platforma = BlockModelV3.create(dataModel)
@@ -45,30 +93,20 @@ export const platforma = BlockModelV3.create(dataModel)
45
93
  if (!data.inputAnchor) throw new Error('Input anchor is required');
46
94
 
47
95
  return {
48
- customBlockLabel: data.customBlockLabel || 'Humanness Score',
96
+ // Empty when unset; the workflow falls back to the input dataset name so
97
+ // the provenance trace label matches the block subtitle.
98
+ customBlockLabel: data.customBlockLabel || '',
49
99
  inputAnchor: data.inputAnchor,
50
100
  mem: data.mem,
51
101
  };
52
102
  })
53
103
 
54
104
  .output('inputOptions', (ctx) =>
55
- ctx.resultPool.getOptions([{
56
- axes: [
57
- { name: 'pl7.app/sampleId' },
58
- { name: 'pl7.app/vdj/clonotypeKey' },
59
- ],
60
- annotations: { 'pl7.app/isAnchor': 'true' },
61
- }, {
62
- axes: [
63
- { name: 'pl7.app/sampleId' },
64
- { name: 'pl7.app/vdj/scClonotypeKey' },
65
- ],
66
- annotations: { 'pl7.app/isAnchor': 'true' },
67
- }]),
105
+ ctx.resultPool.getOptions(inputSelectors),
68
106
  )
69
107
 
70
108
  .outputWithStatus('pt', (ctx) => {
71
- const pCols = ctx.outputs?.resolve('outputLiabilities')?.getPColumns();
109
+ const pCols = ctx.outputs?.resolve('outputHumanness')?.getPColumns();
72
110
  if (pCols === undefined) {
73
111
  return undefined;
74
112
  }
@@ -80,14 +118,87 @@ export const platforma = BlockModelV3.create(dataModel)
80
118
  });
81
119
  })
82
120
 
121
+ // --- Score distribution (histogram) ---------------------------------------
122
+ // One row per clonotype, so the histogram counts UNIQUE clonotypes by humanness
123
+ // score — it is deliberately NOT weighted by clonotype abundance. The question it
124
+ // answers is "how many distinct candidates sit below/above a humanness level"
125
+ // (i.e. how much humanization work is there), not "how human is the repertoire by
126
+ // read mass". No human-like threshold line is drawn: this score is a 9-mer
127
+ // fraction rescaled to 0..100, not a cutoff validated against therapeutic mAbs.
128
+ .outputWithStatus('histogramPf', (ctx): PFrameHandle | undefined => {
129
+ const pCols = ctx.outputs?.resolve('outputHumanness')?.getPColumns();
130
+ if (pCols === undefined) return undefined;
131
+ return createPFrameForGraphs(ctx, pCols);
132
+ })
133
+
134
+ .output('histogramPfPcols', (ctx): PColumnIdAndSpec[] | undefined => {
135
+ const pCols = ctx.outputs?.resolve('outputHumanness')?.getPColumns();
136
+ if (pCols === undefined || pCols.length === 0) return undefined;
137
+ return pCols.map((c) => ({ columnId: c.id, spec: c.spec }));
138
+ })
139
+
140
+ // --- Per-sample distribution (box / violin) --------------------------------
141
+ // The humanness column is keyed by clonotypeKey only (sample-agnostic). To get
142
+ // a per-sample view we join it with the input dataset's primary abundance
143
+ // column, which carries the [sampleId, clonotypeKey] axes. graph-maker joins on
144
+ // the shared clonotypeKey axis, so each (sample, clonotype) pair contributes the
145
+ // clonotype's score — grouping by sampleId then yields a distribution per sample.
146
+ // This is a box/violin (median + spread + tails) on purpose, not a per-sample
147
+ // mean: the spread is exactly what a single mean would hide.
148
+ // Degrades gracefully: if the dataset has no primary-abundance column the join
149
+ // adds nothing, the sampleId axis is absent, and the page simply can't preselect
150
+ // a grouping (the chart still opens). VDJ datasets almost always carry abundance.
151
+ .outputWithStatus('perSamplePf', (ctx): PFrameHandle | undefined => {
152
+ const humanness = ctx.outputs?.resolve('outputHumanness')?.getPColumns();
153
+ if (humanness === undefined) return undefined;
154
+
155
+ const ref = ctx.data.inputAnchor;
156
+ if (ref === undefined) return undefined;
157
+
158
+ const abundance = ctx.resultPool.getAnchoredPColumns({ main: ref }, [{
159
+ axes: [{ anchor: 'main', idx: 0 }, { anchor: 'main', idx: 1 }],
160
+ annotations: {
161
+ 'pl7.app/isAbundance': 'true',
162
+ 'pl7.app/abundance/normalized': 'false',
163
+ 'pl7.app/abundance/isPrimary': 'true',
164
+ },
165
+ }]);
166
+
167
+ return createPFrameForGraphs(ctx, [...humanness, ...(abundance ?? [])]);
168
+ })
169
+
170
+ .output('perSamplePfPcols', (ctx): PColumnIdAndSpec[] | undefined => {
171
+ const humanness = ctx.outputs?.resolve('outputHumanness')?.getPColumns();
172
+ if (humanness === undefined || humanness.length === 0) return undefined;
173
+
174
+ const ref = ctx.data.inputAnchor;
175
+ if (ref === undefined) return undefined;
176
+
177
+ const abundance = ctx.resultPool.getAnchoredPColumns({ main: ref }, [{
178
+ axes: [{ anchor: 'main', idx: 0 }, { anchor: 'main', idx: 1 }],
179
+ annotations: {
180
+ 'pl7.app/isAbundance': 'true',
181
+ 'pl7.app/abundance/normalized': 'false',
182
+ 'pl7.app/abundance/isPrimary': 'true',
183
+ },
184
+ }]);
185
+
186
+ return [...humanness, ...(abundance ?? [])].map((c) => ({ columnId: c.id, spec: c.spec }));
187
+ })
188
+
83
189
  .output('isRunning', (ctx) => ctx.outputs?.getIsReadyOrError() === false)
84
190
 
85
- .title(() => 'Humanness Score')
191
+ .title(() => 'Humanization Score')
86
192
 
87
- .subtitle((ctx) => ctx.data.customBlockLabel || 'Humanness Score')
193
+ .subtitle((ctx) => {
194
+ if (ctx.data.customBlockLabel) return ctx.data.customBlockLabel;
195
+ return 'Humanization Score';
196
+ })
88
197
 
89
198
  .sections((_) => [
90
199
  { type: 'link', href: '/', label: 'Table' },
200
+ { type: 'link', href: '/histogram', label: 'Score Distribution' },
201
+ { type: 'link', href: '/by-sample', label: 'By Sample' },
91
202
  ])
92
203
 
93
204
  .done();