@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.
Files changed (50) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/282.js +2 -0
  3. package/dist/282.js.map +1 -0
  4. package/dist/287.js +2 -0
  5. package/dist/287.js.map +1 -0
  6. package/dist/288.js +2 -0
  7. package/dist/288.js.map +1 -0
  8. package/dist/422.js +2 -0
  9. package/dist/422.js.map +1 -0
  10. package/dist/455.js +1 -1
  11. package/dist/455.js.map +1 -1
  12. package/dist/767.js +2 -0
  13. package/dist/767.js.map +1 -0
  14. package/dist/package-test.js +5 -5
  15. package/dist/package-test.js.map +1 -1
  16. package/dist/package.js +3 -3
  17. package/dist/package.js.map +1 -1
  18. package/files/samples/antibodies.csv +494 -0
  19. package/package.json +2 -2
  20. package/src/package-api.ts +28 -0
  21. package/src/package.g.ts +31 -1
  22. package/src/package.ts +40 -1
  23. package/src/tests/substructure-filters-tests.ts +1 -0
  24. package/src/utils/annotations/annotation-actions.ts +130 -0
  25. package/src/utils/annotations/annotation-manager-ui.ts +118 -0
  26. package/src/utils/annotations/annotation-manager.ts +163 -0
  27. package/src/utils/annotations/liability-scanner-ui.ts +88 -0
  28. package/src/utils/annotations/liability-scanner.ts +147 -0
  29. package/src/utils/annotations/numbering-ui.ts +472 -0
  30. package/src/utils/antibody-numbering (WIP)/alignment.ts +578 -0
  31. package/src/utils/antibody-numbering (WIP)/annotator.ts +120 -0
  32. package/src/utils/antibody-numbering (WIP)/data/blosum62.ts +55 -0
  33. package/src/utils/antibody-numbering (WIP)/data/consensus-aho.ts +155 -0
  34. package/src/utils/antibody-numbering (WIP)/data/consensus-imgt.ts +162 -0
  35. package/src/utils/antibody-numbering (WIP)/data/consensus-kabat.ts +157 -0
  36. package/src/utils/antibody-numbering (WIP)/data/consensus-martin.ts +152 -0
  37. package/src/utils/antibody-numbering (WIP)/data/consensus.ts +36 -0
  38. package/src/utils/antibody-numbering (WIP)/data/regions.ts +63 -0
  39. package/src/utils/antibody-numbering (WIP)/index.ts +31 -0
  40. package/src/utils/antibody-numbering (WIP)/testdata.ts +5356 -0
  41. package/src/utils/antibody-numbering (WIP)/types.ts +69 -0
  42. package/src/utils/context-menu.ts +42 -2
  43. package/src/utils/get-region-func-editor.ts +18 -2
  44. package/src/utils/get-region.ts +167 -17
  45. package/src/utils/sequence-column-input.ts +57 -0
  46. package/src/viewers/vd-regions-viewer.ts +2 -0
  47. package/src/widgets/representations.ts +53 -2
  48. package/src/widgets/sequence-scrolling-widget.ts +28 -18
  49. package/test-console-output-1.log +587 -551
  50. 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 {TAGS as bioTags} from '@datagrok-libraries/bio/src/utils/macromolecule';
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
- const regionsTagTxt: string | null = seqCol ? seqCol.getTag(bioTAGS.regions) : null;
144
- const regionList: SeqRegion[] | null = regionsTagTxt ? JSON.parse(regionsTagTxt) : null;
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);
@@ -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: () => { /* TODO: update name placeholder with getDefaultName() */ }});
13
- const endPositionInput = ui.input.choice('End Position', {value: sh.posList[sh.posList.length], items: sh.posList,
14
- onValueChanged: () => { /* TODO: update name placeholder with getDefaultName() */ }});
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 `${col.name}:${startPositionInput.value}-${endPositionInput.value}`;
35
+ return selectedRegionName
36
+ ? `${col.name}(${selectedRegionName}): ${startPositionInput.value}-${endPositionInput.value}`
37
+ : `${col.name}:${startPositionInput.value}-${endPositionInput.value}`;
18
38
  };
19
39
 
20
- ui.dialog({title: 'Get Region'}).add(ui.inputs([
21
- nameInput,
22
- startPositionInput,
23
- endPositionInput,
24
- ])).onOK(() => {
25
- const pi = DG.TaskBarProgressIndicator.create('Getting region...');
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 name: string = nameInput.value ?? getDefaultName();
28
- const regCol = getRegionDo(col, name, startPositionInput.value, endPositionInput.value);
29
- col.dataFrame.columns.add(regCol);
30
- regCol.setTag(DG.TAGS.CELL_RENDERER, 'sequence');
31
- } catch (err: any) {
32
- grok.shell.error(err.toString());
33
- } finally { pi.close(); }
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
- return new DG.Widget(sequenceConfigInputs);
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
- // Create lazy tracks only for MSA sequences
446
-
447
- // OPTIMIZED: Pass seqHandler directly instead of column/splitter
448
- const conservationTrack = new LazyConservationTrack(
449
- sh,
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: 2});
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
  });