@datagrok/bio 2.25.17 → 2.26.1
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/CHANGELOG.md +4 -0
- package/dist/282.js +2 -0
- package/dist/282.js.map +1 -0
- package/dist/287.js +2 -0
- package/dist/287.js.map +1 -0
- package/dist/288.js +2 -0
- package/dist/288.js.map +1 -0
- package/dist/422.js +2 -0
- package/dist/422.js.map +1 -0
- package/dist/455.js +1 -1
- package/dist/455.js.map +1 -1
- package/dist/767.js +2 -0
- package/dist/767.js.map +1 -0
- package/dist/package-test.js +5 -5
- package/dist/package-test.js.map +1 -1
- package/dist/package.js +3 -3
- package/dist/package.js.map +1 -1
- package/files/samples/antibodies.csv +494 -0
- package/package.json +2 -2
- package/src/package-api.ts +28 -0
- package/src/package.g.ts +31 -1
- package/src/package.ts +40 -1
- package/src/tests/substructure-filters-tests.ts +1 -0
- package/src/utils/annotations/annotation-actions.ts +130 -0
- package/src/utils/annotations/annotation-manager-ui.ts +118 -0
- package/src/utils/annotations/annotation-manager.ts +163 -0
- package/src/utils/annotations/liability-scanner-ui.ts +88 -0
- package/src/utils/annotations/liability-scanner.ts +147 -0
- package/src/utils/annotations/numbering-ui.ts +472 -0
- package/src/utils/antibody-numbering (WIP)/alignment.ts +578 -0
- package/src/utils/antibody-numbering (WIP)/annotator.ts +120 -0
- package/src/utils/antibody-numbering (WIP)/data/blosum62.ts +55 -0
- package/src/utils/antibody-numbering (WIP)/data/consensus-aho.ts +155 -0
- package/src/utils/antibody-numbering (WIP)/data/consensus-imgt.ts +162 -0
- package/src/utils/antibody-numbering (WIP)/data/consensus-kabat.ts +157 -0
- package/src/utils/antibody-numbering (WIP)/data/consensus-martin.ts +152 -0
- package/src/utils/antibody-numbering (WIP)/data/consensus.ts +36 -0
- package/src/utils/antibody-numbering (WIP)/data/regions.ts +63 -0
- package/src/utils/antibody-numbering (WIP)/index.ts +31 -0
- package/src/utils/antibody-numbering (WIP)/testdata.ts +5356 -0
- package/src/utils/antibody-numbering (WIP)/types.ts +69 -0
- package/src/utils/context-menu.ts +42 -2
- package/src/utils/get-region-func-editor.ts +18 -2
- package/src/utils/get-region.ts +167 -17
- package/src/utils/sequence-column-input.ts +57 -0
- package/src/viewers/vd-regions-viewer.ts +2 -0
- package/src/widgets/representations.ts +53 -2
- package/src/widgets/sequence-scrolling-widget.ts +28 -18
- package/test-console-output-1.log +587 -551
- package/test-record-1.mp4 +0 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** Supported numbering schemes */
|
|
2
|
+
export type Scheme = 'imgt' | 'kabat' | 'chothia' | 'aho';
|
|
3
|
+
|
|
4
|
+
/** Antibody chain types */
|
|
5
|
+
export type ChainType = 'H' | 'K' | 'L';
|
|
6
|
+
|
|
7
|
+
/** Human-readable chain group */
|
|
8
|
+
export type ChainGroup = 'Heavy' | 'Light';
|
|
9
|
+
|
|
10
|
+
/** A single numbered position with its amino acid */
|
|
11
|
+
export interface NumberingEntry {
|
|
12
|
+
/** Position code (e.g. "1", "27A", "111A") */
|
|
13
|
+
position: string;
|
|
14
|
+
/** Single-letter amino acid at this position */
|
|
15
|
+
aa: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Region annotation for a numbered antibody */
|
|
19
|
+
export interface RegionAnnotation {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
description: string;
|
|
23
|
+
start: string;
|
|
24
|
+
end: string;
|
|
25
|
+
visualType: 'region';
|
|
26
|
+
category: 'structure';
|
|
27
|
+
sourceScheme: string;
|
|
28
|
+
autoGenerated: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Result of numbering a single antibody sequence */
|
|
32
|
+
export interface NumberingResult {
|
|
33
|
+
/** Comma-separated position names for each numbered residue */
|
|
34
|
+
positionNames: string;
|
|
35
|
+
/** Chain type: 'Heavy' or 'Light' */
|
|
36
|
+
chainType: ChainGroup;
|
|
37
|
+
/** Chain type code: 'H', 'K', or 'L' */
|
|
38
|
+
chainTypeCode: ChainType | '';
|
|
39
|
+
/** Region annotations as structured objects */
|
|
40
|
+
annotations: RegionAnnotation[];
|
|
41
|
+
/** Detailed numbering: position -> amino acid */
|
|
42
|
+
numberingDetail: NumberingEntry[];
|
|
43
|
+
/** Map from position code to character index in the original sequence */
|
|
44
|
+
numberingMap: Record<string, number>;
|
|
45
|
+
/** Percent identity of the alignment (0-1) */
|
|
46
|
+
percentIdentity: number;
|
|
47
|
+
/** Error message if numbering failed, empty string otherwise */
|
|
48
|
+
error: string;
|
|
49
|
+
/**
|
|
50
|
+
* Full numbering array aligned to input sequence.
|
|
51
|
+
* Each element is a position code or '-' for residues outside the variable region.
|
|
52
|
+
*/
|
|
53
|
+
numbering: string[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Consensus profile: for each position, which amino acids are expected */
|
|
57
|
+
export type ConsensusProfile = Map<number, string[]>;
|
|
58
|
+
|
|
59
|
+
/** Internal alignment result */
|
|
60
|
+
export interface AlignmentResult {
|
|
61
|
+
/** Position code for each residue in the input, or '-' */
|
|
62
|
+
numbering: string[];
|
|
63
|
+
/** Percent identity */
|
|
64
|
+
percentIdentity: number;
|
|
65
|
+
/** Detected chain type */
|
|
66
|
+
chainType: ChainType;
|
|
67
|
+
/** Error message or empty string */
|
|
68
|
+
error: string;
|
|
69
|
+
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/* eslint-disable max-len */
|
|
2
2
|
import * as grok from 'datagrok-api/grok';
|
|
3
3
|
import * as DG from 'datagrok-api/dg';
|
|
4
|
-
import * as ui from 'datagrok-api/ui';
|
|
5
4
|
|
|
6
5
|
import {ALPHABET, NOTATION} from '@datagrok-libraries/bio/src/utils/macromolecule';
|
|
7
6
|
import {ISeqHelper} from '@datagrok-libraries/bio/src/utils/seq-helper';
|
|
8
7
|
|
|
9
8
|
import {_package} from '../package';
|
|
10
|
-
import {
|
|
9
|
+
import {AnnotationRenderer} from '@datagrok-libraries/bio/src/utils/cell-renderer-annotations';
|
|
10
|
+
import {getColumnAnnotations} from './annotations/annotation-manager';
|
|
11
|
+
import {AnnotationCategory} from '@datagrok-libraries/bio/src/utils/macromolecule/annotations';
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
export function addCopyMenuUI(cell: DG.Cell, menu: DG.Menu, seqHelper: ISeqHelper): void {
|
|
@@ -30,4 +31,43 @@ export function addCopyMenuUI(cell: DG.Cell, menu: DG.Menu, seqHelper: ISeqHelpe
|
|
|
30
31
|
grok.shell.info(`Value of notation '${tgtNotation}' copied to clipboard`);
|
|
31
32
|
}
|
|
32
33
|
});
|
|
34
|
+
|
|
35
|
+
// Annotation context menu items
|
|
36
|
+
const srcCol = cell.column;
|
|
37
|
+
const annotations = getColumnAnnotations(srcCol);
|
|
38
|
+
if (annotations.length > 0) {
|
|
39
|
+
const annotRenderer = new AnnotationRenderer(srcCol);
|
|
40
|
+
if (annotRenderer.hasAnnotations()) {
|
|
41
|
+
// Add annotation info submenu
|
|
42
|
+
const structAnnots = annotations.filter((a) => a.category === AnnotationCategory.Structure);
|
|
43
|
+
if (structAnnots.length > 0) {
|
|
44
|
+
const annotMenu = menu.group('Annotations');
|
|
45
|
+
|
|
46
|
+
// Extract region actions (uses per-row data for unaligned sequences)
|
|
47
|
+
for (const annot of structAnnots) {
|
|
48
|
+
if (annot.start && annot.end) {
|
|
49
|
+
annotMenu.item(`Extract ${annot.name} as Column`, () => {
|
|
50
|
+
import('./annotations/annotation-actions').then((m) => {
|
|
51
|
+
try {
|
|
52
|
+
m.extractAnnotatedRegion(srcCol.dataFrame, srcCol, annot.name, seqHelper);
|
|
53
|
+
} catch (err: any) {
|
|
54
|
+
grok.shell.error(`Failed to extract region: ${err.message ?? err}`);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Filter by liability hits
|
|
62
|
+
const liabAnnots = annotations.filter((a) => a.category === AnnotationCategory.Liability);
|
|
63
|
+
if (liabAnnots.length > 0) {
|
|
64
|
+
annotMenu.item('Filter: Rows with Liability Hits', () => {
|
|
65
|
+
import('./annotations/annotation-actions').then((m) => {
|
|
66
|
+
m.filterByLiabilityHits(srcCol.dataFrame, srcCol);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
33
73
|
}
|
|
@@ -140,8 +140,24 @@ export class GetRegionFuncEditor {
|
|
|
140
140
|
|
|
141
141
|
private updateRegionItems(): void {
|
|
142
142
|
const seqCol = this.inputs.sequence.value;
|
|
143
|
-
|
|
144
|
-
|
|
143
|
+
// Read from .annotations first (new system), fall back to .regions (legacy)
|
|
144
|
+
let regionList: SeqRegion[] | null = null;
|
|
145
|
+
const annotationsTag: string | null = seqCol ? seqCol.getTag(bioTAGS.annotations) : null;
|
|
146
|
+
if (annotationsTag) {
|
|
147
|
+
try {
|
|
148
|
+
const annotations = JSON.parse(annotationsTag);
|
|
149
|
+
const structAnnots = annotations.filter((a: any) => a.category === 'structure' && a.start && a.end);
|
|
150
|
+
if (structAnnots.length > 0) {
|
|
151
|
+
regionList = structAnnots.map((a: any) => ({
|
|
152
|
+
name: a.name, description: a.description ?? '', start: a.start, end: a.end,
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
} catch { /* ignore parse errors */ }
|
|
156
|
+
}
|
|
157
|
+
if (!regionList) {
|
|
158
|
+
const regionsTagTxt: string | null = seqCol ? seqCol.getTag(bioTAGS.regions) : null;
|
|
159
|
+
regionList = regionsTagTxt ? JSON.parse(regionsTagTxt) : null;
|
|
160
|
+
}
|
|
145
161
|
|
|
146
162
|
const regionSE = (this.inputs.region.input as HTMLSelectElement);
|
|
147
163
|
for (let i = regionSE.options.length - 1; i >= 0; --i) regionSE.options.remove(i);
|
package/src/utils/get-region.ts
CHANGED
|
@@ -2,36 +2,127 @@ import * as grok from 'datagrok-api/grok';
|
|
|
2
2
|
import * as ui from 'datagrok-api/ui';
|
|
3
3
|
import * as DG from 'datagrok-api/dg';
|
|
4
4
|
|
|
5
|
+
import {TAGS as bioTAGS} from '@datagrok-libraries/bio/src/utils/macromolecule';
|
|
6
|
+
import {
|
|
7
|
+
SeqAnnotation, SeqAnnotationHit, AnnotationCategory,
|
|
8
|
+
} from '@datagrok-libraries/bio/src/utils/macromolecule/annotations';
|
|
9
|
+
import {getAnnotationColumnName, cacheAllRowAnnotations} from './annotations/annotation-manager';
|
|
5
10
|
import {_package} from '../package';
|
|
6
11
|
|
|
7
12
|
export function getRegionUI(col: DG.Column<string>): void {
|
|
13
|
+
showGetRegionDialog(col);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Shows the Get Region dialog for the given column.
|
|
17
|
+
* When the user confirms, the region column is extracted, added to the dataframe, and
|
|
18
|
+
* {@link onRegionCreated} is called with it (if provided).
|
|
19
|
+
* Returns the dialog instance for further control. */
|
|
20
|
+
export function showGetRegionDialog(
|
|
21
|
+
col: DG.Column<string>,
|
|
22
|
+
onRegionCreated?: (regCol: DG.Column<string>) => void,
|
|
23
|
+
): DG.Dialog {
|
|
8
24
|
const sh = _package.seqHelper.getSeqHandler(col);
|
|
9
25
|
|
|
10
26
|
const nameInput = ui.input.string('Name', {value: ''});
|
|
11
27
|
const startPositionInput = ui.input.choice('Start Position', {value: sh.posList[0], items: sh.posList,
|
|
12
|
-
onValueChanged: () =>
|
|
13
|
-
const endPositionInput = ui.input.choice('End Position', {value: sh.posList[sh.posList.length], items: sh.posList,
|
|
14
|
-
onValueChanged: () =>
|
|
28
|
+
onValueChanged: () => updateNamePlaceholder()});
|
|
29
|
+
const endPositionInput = ui.input.choice('End Position', {value: sh.posList[sh.posList.length - 1], items: sh.posList,
|
|
30
|
+
onValueChanged: () => updateNamePlaceholder()});
|
|
31
|
+
|
|
32
|
+
let selectedRegionName: string | null = null;
|
|
15
33
|
|
|
16
34
|
const getDefaultName = (): string => {
|
|
17
|
-
return
|
|
35
|
+
return selectedRegionName
|
|
36
|
+
? `${col.name}(${selectedRegionName}): ${startPositionInput.value}-${endPositionInput.value}`
|
|
37
|
+
: `${col.name}:${startPositionInput.value}-${endPositionInput.value}`;
|
|
18
38
|
};
|
|
19
39
|
|
|
20
|
-
|
|
21
|
-
nameInput
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
40
|
+
const updateNamePlaceholder = (): void => {
|
|
41
|
+
if (!nameInput.value)
|
|
42
|
+
nameInput.input.setAttribute('placeholder', getDefaultName());
|
|
43
|
+
};
|
|
44
|
+
updateNamePlaceholder();
|
|
45
|
+
|
|
46
|
+
// Build region presets from annotations (new system) or .regions tag (legacy)
|
|
47
|
+
const regionInput = _buildRegionPresetsInput(col, startPositionInput, endPositionInput, (regionName) => {
|
|
48
|
+
selectedRegionName = regionName;
|
|
49
|
+
updateNamePlaceholder();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const inputsList: DG.InputBase[] = [];
|
|
53
|
+
if (regionInput) inputsList.push(regionInput);
|
|
54
|
+
inputsList.push(nameInput, startPositionInput, endPositionInput);
|
|
55
|
+
|
|
56
|
+
const dlg = ui.dialog({title: 'Get Region'}).add(ui.inputs(inputsList))
|
|
57
|
+
.onOK(() => {
|
|
58
|
+
const pi = DG.TaskBarProgressIndicator.create('Getting region...');
|
|
59
|
+
try {
|
|
60
|
+
const name: string = nameInput.value || getDefaultName();
|
|
61
|
+
const regCol = getRegionDo(col, startPositionInput.value, endPositionInput.value, name);
|
|
62
|
+
col.dataFrame.columns.add(regCol);
|
|
63
|
+
regCol.setTag(DG.TAGS.CELL_RENDERER, 'sequence');
|
|
64
|
+
onRegionCreated?.(regCol);
|
|
65
|
+
} catch (err: any) {
|
|
66
|
+
grok.shell.error(err.toString());
|
|
67
|
+
} finally { pi.close(); }
|
|
68
|
+
});
|
|
69
|
+
dlg.show();
|
|
70
|
+
return dlg;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Builds a Region preset dropdown from column annotations / legacy .regions tag.
|
|
74
|
+
* Returns null if the column has no annotated regions. */
|
|
75
|
+
function _buildRegionPresetsInput(
|
|
76
|
+
col: DG.Column<string>,
|
|
77
|
+
startInput: DG.InputBase<string | null>,
|
|
78
|
+
endInput: DG.InputBase<string | null>,
|
|
79
|
+
onRegionSelected?: (regionName: string | null) => void,
|
|
80
|
+
): DG.InputBase | null {
|
|
81
|
+
type RegionPreset = { name: string, start: string, end: string };
|
|
82
|
+
let regionList: RegionPreset[] | null = null;
|
|
83
|
+
|
|
84
|
+
// New annotation system
|
|
85
|
+
const annotationsTag: string | null = col.getTag(bioTAGS.annotations);
|
|
86
|
+
if (annotationsTag) {
|
|
26
87
|
try {
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
88
|
+
const annotations = JSON.parse(annotationsTag);
|
|
89
|
+
const structAnnots = annotations.filter(
|
|
90
|
+
(a: any) => a.category === AnnotationCategory.Structure && a.start && a.end);
|
|
91
|
+
if (structAnnots.length > 0) {
|
|
92
|
+
regionList = structAnnots.map((a: any) => ({
|
|
93
|
+
name: a.name, start: a.start, end: a.end,
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
} catch { /* ignore parse errors */ }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Legacy .regions tag
|
|
100
|
+
if (!regionList) {
|
|
101
|
+
const regionsTagTxt: string | null = col.getTag(bioTAGS.regions);
|
|
102
|
+
if (regionsTagTxt) {
|
|
103
|
+
try { regionList = JSON.parse(regionsTagTxt); } catch { /* ignore */ }
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!regionList || regionList.length === 0) return null;
|
|
108
|
+
|
|
109
|
+
const items = ['', ...regionList.map((r) => `${r.name}: ${r.start}-${r.end}`)];
|
|
110
|
+
const regionInput = ui.input.choice('Region', {
|
|
111
|
+
value: '', items: items,
|
|
112
|
+
onValueChanged: (value: string) => {
|
|
113
|
+
if (!value) {
|
|
114
|
+
onRegionSelected?.(null);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const preset = regionList!.find((r) => `${r.name}: ${r.start}-${r.end}` === value);
|
|
118
|
+
if (preset) {
|
|
119
|
+
startInput.value = preset.start;
|
|
120
|
+
endInput.value = preset.end;
|
|
121
|
+
onRegionSelected?.(preset.name);
|
|
122
|
+
}
|
|
123
|
+
},
|
|
34
124
|
});
|
|
125
|
+
return regionInput;
|
|
35
126
|
}
|
|
36
127
|
|
|
37
128
|
/** {@link startPosName} and {@link endPosName} are according positionNames tag (or default ['1', '2',...]) */
|
|
@@ -57,6 +148,65 @@ export function getRegionDo(
|
|
|
57
148
|
|
|
58
149
|
const regColName: string = !!name ? name : `${col.name}: (${startPosName ?? ''}-${endPosName ?? ''})`;
|
|
59
150
|
|
|
151
|
+
// Try per-row extraction for unaligned data: find a matching structure annotation with per-row spans
|
|
152
|
+
const perRowCol = _tryPerRowExtraction(col, sh, startPosName, endPosName, regColName);
|
|
153
|
+
if (perRowCol) return perRowCol;
|
|
154
|
+
|
|
155
|
+
// Fall back to column-level extraction (aligned/MSA data)
|
|
60
156
|
const regCol = sh.getRegion(startPosIdx, endPosIdx, regColName);
|
|
61
157
|
return regCol;
|
|
62
158
|
}
|
|
159
|
+
|
|
160
|
+
/** Attempts per-row region extraction using the companion annotation column.
|
|
161
|
+
* Returns null if no per-row data is available for the given start/end. */
|
|
162
|
+
function _tryPerRowExtraction(
|
|
163
|
+
col: DG.Column<string>,
|
|
164
|
+
sh: ReturnType<typeof _package.seqHelper.getSeqHandler>,
|
|
165
|
+
startPosName: string | null,
|
|
166
|
+
endPosName: string | null,
|
|
167
|
+
regColName: string,
|
|
168
|
+
): DG.Column<string> | null {
|
|
169
|
+
if (!startPosName || !endPosName || !col.dataFrame) return null;
|
|
170
|
+
|
|
171
|
+
// Find matching structure annotation
|
|
172
|
+
const annotTag = col.getTag(bioTAGS.annotations);
|
|
173
|
+
if (!annotTag) return null;
|
|
174
|
+
|
|
175
|
+
let annotations: SeqAnnotation[];
|
|
176
|
+
try { annotations = JSON.parse(annotTag); } catch { return null; }
|
|
177
|
+
|
|
178
|
+
const annot = annotations.find((a) =>
|
|
179
|
+
a.category === AnnotationCategory.Structure && a.start === startPosName && a.end === endPosName);
|
|
180
|
+
if (!annot) return null;
|
|
181
|
+
|
|
182
|
+
// Check for companion annotation column with per-row region spans
|
|
183
|
+
const annotColName = getAnnotationColumnName(col.name);
|
|
184
|
+
let annotCol: DG.Column<string> | null = null;
|
|
185
|
+
try { annotCol = col.dataFrame.columns.byName(annotColName) as DG.Column<string>; } catch { return null; }
|
|
186
|
+
if (!annotCol) return null;
|
|
187
|
+
|
|
188
|
+
const allRowData = cacheAllRowAnnotations(annotCol);
|
|
189
|
+
const hasPerRowRegions = allRowData.some((rd) =>
|
|
190
|
+
rd?.some((h: SeqAnnotationHit) => h.annotationId === annot.id && h.endPositionIndex != null));
|
|
191
|
+
if (!hasPerRowRegions) return null;
|
|
192
|
+
|
|
193
|
+
// Extract per-row using row-specific character indices
|
|
194
|
+
const df = col.dataFrame;
|
|
195
|
+
const regCol = DG.Column.fromType(DG.COLUMN_TYPE.STRING, regColName, df.rowCount);
|
|
196
|
+
for (let i = 0; i < df.rowCount; i++) {
|
|
197
|
+
const rowHits = allRowData[i];
|
|
198
|
+
const regionHit = rowHits?.find((h: SeqAnnotationHit) =>
|
|
199
|
+
h.annotationId === annot.id && h.endPositionIndex != null);
|
|
200
|
+
if (regionHit) {
|
|
201
|
+
const splitted = sh.getSplitted(i);
|
|
202
|
+
const parts: string[] = [];
|
|
203
|
+
for (let p = regionHit.positionIndex; p <= regionHit.endPositionIndex!; p++) {
|
|
204
|
+
if (p < splitted.length)
|
|
205
|
+
parts.push(splitted.getOriginal(p));
|
|
206
|
+
}
|
|
207
|
+
regCol.set(i, parts.join(sh.separator || ''));
|
|
208
|
+
} else
|
|
209
|
+
regCol.set(i, '');
|
|
210
|
+
}
|
|
211
|
+
return regCol;
|
|
212
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as grok from 'datagrok-api/grok';
|
|
2
|
+
import * as ui from 'datagrok-api/ui';
|
|
3
|
+
import * as DG from 'datagrok-api/dg';
|
|
4
|
+
|
|
5
|
+
import {showGetRegionDialog} from './get-region';
|
|
6
|
+
import {ISequenceColumnInput} from '@datagrok-libraries/bio/src/utils/sequence-column-input';
|
|
7
|
+
|
|
8
|
+
/** A column input that filters to macromolecule columns and provides a
|
|
9
|
+
* "get region" button so users can extract a sub-region and use it instead. */
|
|
10
|
+
export class SequenceColumnInput implements ISequenceColumnInput {
|
|
11
|
+
private readonly colInput: DG.InputBase<DG.Column | null>;
|
|
12
|
+
|
|
13
|
+
private constructor(
|
|
14
|
+
name: string,
|
|
15
|
+
options: ui.input.IColumnInputInitOptions<DG.Column>,
|
|
16
|
+
) {
|
|
17
|
+
const filter = options.filter;
|
|
18
|
+
this.colInput = ui.input.column(name, {
|
|
19
|
+
...options,
|
|
20
|
+
filter: (col: DG.Column) => {
|
|
21
|
+
if (col.semType !== DG.SEMTYPE.MACROMOLECULE) return false;
|
|
22
|
+
return filter ? filter(col) : true;
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const regionIcon = ui.iconFA('cut', () => this.onRegionIconClick(), 'Extract a region from the sequence');
|
|
27
|
+
this.colInput.addOptions(regionIcon);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Creates a new SequenceColumnInput.
|
|
31
|
+
* @param name - Caption for the input.
|
|
32
|
+
* @param options - Same options as {@link ui.input.column}, table is required.
|
|
33
|
+
* The `filter` option is extended to always require semType === Macromolecule. */
|
|
34
|
+
static create(
|
|
35
|
+
name: string,
|
|
36
|
+
options: ui.input.IColumnInputInitOptions<DG.Column>,
|
|
37
|
+
): SequenceColumnInput {
|
|
38
|
+
return new SequenceColumnInput(name, options);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get root(): HTMLElement { return this.colInput.root; }
|
|
42
|
+
get value(): DG.Column | null { return this.colInput.value; }
|
|
43
|
+
set value(col: DG.Column | null) { this.colInput.value = col; }
|
|
44
|
+
get inputBase(): DG.InputBase<DG.Column | null> { return this.colInput; }
|
|
45
|
+
|
|
46
|
+
private onRegionIconClick(): void {
|
|
47
|
+
const col = this.colInput.value;
|
|
48
|
+
if (!col) {
|
|
49
|
+
grok.shell.warning('Select a macromolecule column first.');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
showGetRegionDialog(col, (regCol) => {
|
|
54
|
+
this.colInput.value = regCol;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -322,6 +322,8 @@ export class VdRegionsViewer extends DG.JsViewer implements IVdRegionsViewer {
|
|
|
322
322
|
for (const chain of this.chains) {
|
|
323
323
|
const region: VdRegion | undefined = regionsFiltered
|
|
324
324
|
.find((r) => r.order == orderList[orderI] && r.chain == chain);
|
|
325
|
+
if (!region)
|
|
326
|
+
continue;
|
|
325
327
|
logoPromiseList.push((async (): Promise<[number, string, WebLogoViewer]> => {
|
|
326
328
|
const wl: WebLogoViewer = await this.dataFrame.plot.fromType('WebLogo', {
|
|
327
329
|
sequenceColumnName: region!.sequenceColumnName,
|
|
@@ -8,7 +8,9 @@ import {TAGS as mmcrTAGS} from '@datagrok-libraries/bio/src/utils/cell-renderer'
|
|
|
8
8
|
import {MmcrTemps, rendererSettingsChangedState} from '@datagrok-libraries/bio/src/utils/cell-renderer-consts';
|
|
9
9
|
|
|
10
10
|
import {_package} from '../package';
|
|
11
|
-
import {NOTATION} from '@datagrok-libraries/bio/src/utils/macromolecule';
|
|
11
|
+
import {NOTATION, TAGS as bioTAGS} from '@datagrok-libraries/bio/src/utils/macromolecule';
|
|
12
|
+
import {getColumnAnnotations, clearAnnotations} from '../utils/annotations/annotation-manager';
|
|
13
|
+
import {AnnotationCategory} from '@datagrok-libraries/bio/src/utils/macromolecule/annotations';
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* @export
|
|
@@ -142,5 +144,54 @@ export function getMacromoleculeColumnPropertyPanel(col: DG.Column): DG.Widget {
|
|
|
142
144
|
|
|
143
145
|
|
|
144
146
|
const sequenceConfigInputs = ui.inputs(inputsArray);
|
|
145
|
-
|
|
147
|
+
|
|
148
|
+
// --- Annotations section ---
|
|
149
|
+
const annotations = getColumnAnnotations(col);
|
|
150
|
+
const annotationsDiv = ui.divV([]);
|
|
151
|
+
|
|
152
|
+
if (annotations.length > 0) {
|
|
153
|
+
const scheme = col.getTag(bioTAGS.numberingScheme);
|
|
154
|
+
const structAnnots = annotations.filter((a) => a.category === AnnotationCategory.Structure);
|
|
155
|
+
const liabAnnots = annotations.filter((a) => a.category === AnnotationCategory.Liability);
|
|
156
|
+
|
|
157
|
+
if (scheme)
|
|
158
|
+
annotationsDiv.append(ui.divText(`Numbering: ${scheme}`, {style: {fontSize: '12px', marginBottom: '4px'}}));
|
|
159
|
+
|
|
160
|
+
if (structAnnots.length > 0) {
|
|
161
|
+
const regionNames = structAnnots.map((a) => a.name).join(', ');
|
|
162
|
+
annotationsDiv.append(ui.divText(`Regions: ${regionNames}`, {style: {fontSize: '12px', marginBottom: '4px'}}));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (liabAnnots.length > 0) {
|
|
166
|
+
const totalHits = liabAnnots.reduce((sum, a) => {
|
|
167
|
+
const match = a.description?.match(/\((\d+) hits\)/);
|
|
168
|
+
return sum + (match ? parseInt(match[1]) : 0);
|
|
169
|
+
}, 0);
|
|
170
|
+
annotationsDiv.append(ui.divText(`Liabilities: ${liabAnnots.length} rules (${totalHits} hits)`, {style: {fontSize: '12px', marginBottom: '4px'}}));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const manageBtn = ui.button('Manage', () => {
|
|
174
|
+
import('../utils/annotations/annotation-manager-ui').then((m) => m.showAnnotationManagerDialog());
|
|
175
|
+
});
|
|
176
|
+
const clearBtn = ui.button('Clear All', () => {
|
|
177
|
+
clearAnnotations(col.dataFrame, col);
|
|
178
|
+
col.dataFrame.fireValuesChanged();
|
|
179
|
+
});
|
|
180
|
+
annotationsDiv.append(ui.divH([manageBtn, clearBtn], {style: {gap: '4px', marginTop: '4px'}}));
|
|
181
|
+
} else {
|
|
182
|
+
annotationsDiv.append(ui.divText('No annotations', {style: {fontSize: '12px', color: '#888'}}));
|
|
183
|
+
const scanBtn = ui.button('Scan Liabilities', () => {
|
|
184
|
+
import('../utils/annotations/liability-scanner-ui').then((m) => m.showLiabilityScannerDialog());
|
|
185
|
+
});
|
|
186
|
+
const numberBtn = ui.button('Apply Numbering', () => {
|
|
187
|
+
import('../utils/annotations/numbering-ui').then((m) => m.showNumberingSchemeDialog());
|
|
188
|
+
});
|
|
189
|
+
annotationsDiv.append(ui.divH([scanBtn, numberBtn], {style: {gap: '4px', marginTop: '4px'}}));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const acc = ui.accordion();
|
|
193
|
+
acc.addPane('Renderer Settings', () => sequenceConfigInputs);
|
|
194
|
+
acc.addPane('Annotations', () => annotationsDiv);
|
|
195
|
+
|
|
196
|
+
return new DG.Widget(acc.root);
|
|
146
197
|
}
|
|
@@ -10,6 +10,7 @@ import * as ui from 'datagrok-api/ui';
|
|
|
10
10
|
import {ConservationTrack, MSAHeaderTrack, MSAScrollingHeader, WebLogoTrack} from '@datagrok-libraries/bio/src/utils/sequence-position-scroller';
|
|
11
11
|
import {MonomerPlacer} from '@datagrok-libraries/bio/src/utils/cell-renderer-monomer-placer';
|
|
12
12
|
import {ALPHABET, TAGS as bioTAGS} from '@datagrok-libraries/bio/src/utils/macromolecule';
|
|
13
|
+
import {AnnotationTrack} from '@datagrok-libraries/bio/src/utils/annotation-track';
|
|
13
14
|
import {_package} from '../package';
|
|
14
15
|
import {ISeqHandler} from '@datagrok-libraries/bio/src/utils/macromolecule/seq-handler';
|
|
15
16
|
import * as rxjs from 'rxjs';
|
|
@@ -399,16 +400,20 @@ export function handleSequenceHeaderRendering() {
|
|
|
399
400
|
|
|
400
401
|
// Do not Skip if sequences are too short, rather, just don't render the tracks by default
|
|
401
402
|
|
|
403
|
+
// Annotation track adds 24px (20px track + 4px gap) when structure annotations exist
|
|
404
|
+
const hasAnnotations = !!seqCol.getTag(bioTAGS.annotations);
|
|
405
|
+
const annotationSpace = hasAnnotations ? 24 : 0; // ANNOTATION_TRACK_HEIGHT(20) + TRACK_GAP(4)
|
|
406
|
+
|
|
402
407
|
const STRICT_THRESHOLDS = {
|
|
403
408
|
WITH_TITLE: 58, // BASE + TITLE_HEIGHT(16) + TRACK_GAP(4)
|
|
404
|
-
WITH_WEBLOGO: 107, // WITH_TITLE + DEFAULT_TRACK_HEIGHT(45) + TRACK_GAP(4)
|
|
405
|
-
WITH_BOTH: 156 // WITH_WEBLOGO + DEFAULT_TRACK_HEIGHT(45) + TRACK_GAP(4)
|
|
409
|
+
WITH_WEBLOGO: 107 + annotationSpace, // WITH_TITLE + DEFAULT_TRACK_HEIGHT(45) + TRACK_GAP(4) + annotation
|
|
410
|
+
WITH_BOTH: 156 + annotationSpace, // WITH_WEBLOGO + DEFAULT_TRACK_HEIGHT(45) + TRACK_GAP(4) + annotation
|
|
406
411
|
};
|
|
407
412
|
|
|
408
413
|
let initialHeaderHeight: number;
|
|
409
414
|
if (seqCol.length > 100_000 || maxSeqLen < 50) {
|
|
410
|
-
// Single sequence: just dotted cells
|
|
411
|
-
initialHeaderHeight = STRICT_THRESHOLDS.WITH_TITLE;
|
|
415
|
+
// Single sequence: just dotted cells (+ annotation track if present)
|
|
416
|
+
initialHeaderHeight = STRICT_THRESHOLDS.WITH_TITLE + annotationSpace;
|
|
412
417
|
} else {
|
|
413
418
|
if (seqCol.length > 50_000)
|
|
414
419
|
initialHeaderHeight = STRICT_THRESHOLDS.WITH_WEBLOGO;
|
|
@@ -442,18 +447,11 @@ export function handleSequenceHeaderRendering() {
|
|
|
442
447
|
const initializeHeaders = (monomerLib: IMonomerLib) => {
|
|
443
448
|
const tracks: { id: string, track: MSAHeaderTrack, priority: number }[] = [];
|
|
444
449
|
|
|
445
|
-
//
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
maxSeqLen,
|
|
451
|
-
45, // DEFAULT_TRACK_HEIGHT
|
|
452
|
-
'default',
|
|
453
|
-
'Conservation'
|
|
454
|
-
);
|
|
455
|
-
conservationTrackRef = conservationTrack; // Store reference
|
|
456
|
-
tracks.push({id: 'conservation', track: conservationTrack, priority: 1});
|
|
450
|
+
// Priority ordering: annotations (0) → weblogo (1) → conservation (2)
|
|
451
|
+
// Annotation track is primary when annotation data exists
|
|
452
|
+
const annotationTrack = new AnnotationTrack(seqCol, 'Annotations');
|
|
453
|
+
if (annotationTrack.hasRegions())
|
|
454
|
+
tracks.push({id: 'annotations', track: annotationTrack, priority: 0});
|
|
457
455
|
|
|
458
456
|
// OPTIMIZED: Pass seqHandler directly
|
|
459
457
|
const webLogoTrack = new LazyWebLogoTrack(
|
|
@@ -470,7 +468,18 @@ export function handleSequenceHeaderRendering() {
|
|
|
470
468
|
}
|
|
471
469
|
|
|
472
470
|
webLogoTrack.setupDefaultTooltip();
|
|
473
|
-
tracks.push({id: 'weblogo', track: webLogoTrack, priority:
|
|
471
|
+
tracks.push({id: 'weblogo', track: webLogoTrack, priority: 1});
|
|
472
|
+
|
|
473
|
+
// OPTIMIZED: Pass seqHandler directly instead of column/splitter
|
|
474
|
+
const conservationTrack = new LazyConservationTrack(
|
|
475
|
+
sh,
|
|
476
|
+
maxSeqLen,
|
|
477
|
+
45, // DEFAULT_TRACK_HEIGHT
|
|
478
|
+
'default',
|
|
479
|
+
'Conservation'
|
|
480
|
+
);
|
|
481
|
+
conservationTrackRef = conservationTrack; // Store reference
|
|
482
|
+
tracks.push({id: 'conservation', track: conservationTrack, priority: 2});
|
|
474
483
|
|
|
475
484
|
// Create the scrolling header
|
|
476
485
|
const scroller = new MSAScrollingHeader({
|
|
@@ -503,7 +512,8 @@ export function handleSequenceHeaderRendering() {
|
|
|
503
512
|
|
|
504
513
|
scroller.setupTooltipHandling();
|
|
505
514
|
|
|
506
|
-
// Add tracks to scroller
|
|
515
|
+
// Add tracks to scroller sorted by priority (annotations first, then weblogo, then conservation)
|
|
516
|
+
tracks.sort((a, b) => a.priority - b.priority);
|
|
507
517
|
tracks.forEach(({id, track}) => {
|
|
508
518
|
scroller.addTrack(id, track);
|
|
509
519
|
});
|