@datagrok/bio 2.26.0 → 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/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "name": "Davit Rizhinashvili",
6
6
  "email": "drizhinashvili@datagrok.ai"
7
7
  },
8
- "version": "2.26.0",
8
+ "version": "2.26.1",
9
9
  "description": "Bioinformatics support (import/export of sequences, conversion, visualization, analysis). [See more](https://github.com/datagrok-ai/public/blob/master/packages/Bio/README.md) for details.",
10
10
  "repository": {
11
11
  "type": "git",
@@ -44,7 +44,7 @@
44
44
  ],
45
45
  "dependencies": {
46
46
  "@biowasm/aioli": "^3.1.0",
47
- "@datagrok-libraries/bio": "^5.63.0",
47
+ "@datagrok-libraries/bio": "^5.63.2",
48
48
  "@datagrok-libraries/chem-meta": "^1.2.9",
49
49
  "@datagrok-libraries/math": "^1.2.6",
50
50
  "@datagrok-libraries/ml": "^6.10.9",
@@ -178,6 +178,13 @@ export namespace funcs {
178
178
  return await grok.functions.call('Bio:ManageAnnotations', {});
179
179
  }
180
180
 
181
+ /**
182
+ Creates a new input for sequence columns with ability to extract a region
183
+ */
184
+ export async function sequenceColumnInput(name: string , options: any ): Promise<any> {
185
+ return await grok.functions.call('Bio:SequenceColumnInput', { name, options });
186
+ }
187
+
181
188
  /**
182
189
  Detects pairs of molecules with similar structure and significant difference in any given property
183
190
  */
package/src/package.g.ts CHANGED
@@ -244,6 +244,15 @@ export function manageAnnotations() : void {
244
244
  PackageFunctions.manageAnnotations();
245
245
  }
246
246
 
247
+ //name: Sequence Column Input
248
+ //description: Creates a new input for sequence columns with ability to extract a region
249
+ //input: string name
250
+ //input: dynamic options
251
+ //output: dynamic result
252
+ export function sequenceColumnInput(name: string, options: any) : any {
253
+ return PackageFunctions.sequenceColumnInput(name, options);
254
+ }
255
+
247
256
  //name: Sequence Activity Cliffs
248
257
  //description: Detects pairs of molecules with similar structure and significant difference in any given property
249
258
  //input: dataframe table { description: Input data table }
package/src/package.ts CHANGED
@@ -79,6 +79,8 @@ import {handleSequenceHeaderRendering} from './widgets/sequence-scrolling-widget
79
79
  import {PolymerType} from '@datagrok-libraries/js-draw-lite/src/types/org';
80
80
  import {BilnNotationProvider} from './utils/biln';
81
81
  import {showMonomerCollectionsView} from './utils/monomer-lib/monomer-collections-view';
82
+ import {ISequenceColumnInput} from '@datagrok-libraries/bio/src/utils/sequence-column-input';
83
+ import {SequenceColumnInput} from './utils/sequence-column-input';
82
84
 
83
85
  import * as api from './package-api';
84
86
  export const _package = new BioPackage(/*{debug: true}/**/);
@@ -488,6 +490,14 @@ export class PackageFunctions {
488
490
  import('./utils/annotations/annotation-manager-ui').then((m) => m.showAnnotationManagerDialog());
489
491
  }
490
492
 
493
+ @grok.decorators.func({
494
+ name: 'Sequence Column Input',
495
+ description: 'Creates a new input for sequence columns with ability to extract a region',
496
+ })
497
+ static sequenceColumnInput(name: string, options: any): ISequenceColumnInput {
498
+ return SequenceColumnInput.create(name, options);
499
+ }
500
+
491
501
  @grok.decorators.func({
492
502
  name: 'Sequence Activity Cliffs',
493
503
  description: 'Detects pairs of molecules with similar structure and significant difference in any given property',
@@ -428,6 +428,7 @@ category('bio-substructure-filters', async () => {
428
428
  await awaitGrid(view.grid);
429
429
 
430
430
  const seqFilter = fg.filters[0] as BioSubstructureFilter;
431
+ await awaitCheck(() => seqFilter.bioFilter !== null, 'FastaBioFilter hasn\'t been created', 1000);
431
432
  const seqBf = seqFilter.bioFilter as FastaBioFilter;
432
433
  await testEvent(df.onRowsFiltered, () => {}, () => {
433
434
  seqBf.props = new BioFilterProps(fSubStr, undefined, _package.logger);
@@ -45,7 +45,7 @@ function getCanonicalString(sh: ISeqHandler, rowIdx: number): string {
45
45
  const splitted = sh.getSplitted(rowIdx);
46
46
  const chars: string[] = new Array(splitted.length);
47
47
  for (let i = 0; i < splitted.length; i++)
48
- chars[i] = splitted.getCanonical(i);
48
+ chars[i] = splitted.getOriginal(i);
49
49
  return chars.join('');
50
50
  }
51
51
 
@@ -14,6 +14,8 @@ import {
14
14
  } from './annotation-manager';
15
15
  import {_package} from '../../package';
16
16
  import type {NumberingResult, Scheme} from '../antibody-numbering (WIP)';
17
+ import {VdRegionsViewer} from '../../viewers/vd-regions-viewer';
18
+ import { VdRegion, VdRegionType } from '@datagrok-libraries/bio/src/viewers/vd-regions';
17
19
 
18
20
  const BUILTIN_ENGINE_KEY = '__builtin__';
19
21
  const BUILTIN_ENGINE_LABEL = 'Built-in (TypeScript)';
@@ -34,8 +36,10 @@ function discoverEngines(): NumberingEngine[] {
34
36
  const engines: NumberingEngine[] = [];
35
37
 
36
38
  const funcs = DG.Func.find({meta: {role: 'antibodyNumbering'}});
37
- if (funcs.length === 0)
39
+ if (funcs.length === 0) {
40
+ grok.shell.error('No antibody numbering engines found. Make sure that Proteomics plugin is installed and up to date.');
38
41
  throw new Error('No external antibody numbering engines found. Make sure that Proteomics plugin is installed and up to date.');
42
+ }
39
43
  for (const f of funcs) {
40
44
  const pkgName = f.package?.name ?? '';
41
45
  const label = f.friendlyName || f.name;
@@ -439,6 +443,23 @@ function applyNumberingResults(
439
443
  const regionAnnotations: SeqAnnotation[] = JSON.parse(annotationsJson);
440
444
  const existing = getColumnAnnotations(alignment.alignedCol).filter((a) => a.category !== AnnotationCategory.Structure);
441
445
  setColumnAnnotations(alignment.alignedCol, [...existing, ...regionAnnotations]);
446
+ // chunk for vd regions viewer if that becomes a desired feature in the future
447
+ // if (grok.shell.tv?.dataFrame === df) {
448
+ // (async () => {
449
+ // const vdRegionsViewer: VdRegionsViewer = await grok.shell.tv.dataFrame.plot.fromType('VdRegions',) as VdRegionsViewer;
450
+ // vdRegionsViewer.chains = [chainType];
451
+ // vdRegionsViewer.setData(regionAnnotations.map((a, i) => ({
452
+ // type: a.name?.toLowerCase().startsWith('cdr') ? VdRegionType.CDR : VdRegionType.FR,
453
+ // name: a.name,
454
+ // chain: chainType,
455
+ // order: i,
456
+ // sequenceColumnName: alignment.alignedCol.name,
457
+ // positionStartName: a.start ?? '',
458
+ // positionEndName: a.end ?? '',
459
+ // } satisfies VdRegion)));
460
+ // grok.shell.tv.addViewer(vdRegionsViewer);
461
+ // })();
462
+ // }
442
463
  } catch (err) {
443
464
  console.warn('Failed to set column-level annotations on aligned column:', err);
444
465
  }
@@ -446,5 +467,6 @@ function applyNumberingResults(
446
467
  }
447
468
 
448
469
  df.fireValuesChanged();
470
+
449
471
  grok.shell.info(`Numbering applied: ${schemeName}, chain type: ${chainType}`);
450
472
  }
@@ -10,33 +10,119 @@ import {getAnnotationColumnName, cacheAllRowAnnotations} from './annotations/ann
10
10
  import {_package} from '../package';
11
11
 
12
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 {
13
24
  const sh = _package.seqHelper.getSeqHandler(col);
14
25
 
15
26
  const nameInput = ui.input.string('Name', {value: ''});
16
27
  const startPositionInput = ui.input.choice('Start Position', {value: sh.posList[0], items: sh.posList,
17
- onValueChanged: () => { /* TODO: update name placeholder with getDefaultName() */ }});
18
- const endPositionInput = ui.input.choice('End Position', {value: sh.posList[sh.posList.length], items: sh.posList,
19
- 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;
20
33
 
21
34
  const getDefaultName = (): string => {
22
- 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}`;
38
+ };
39
+
40
+ const updateNamePlaceholder = (): void => {
41
+ if (!nameInput.value)
42
+ nameInput.input.setAttribute('placeholder', getDefaultName());
23
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
+ }
24
72
 
25
- ui.dialog({title: 'Get Region'}).add(ui.inputs([
26
- nameInput,
27
- startPositionInput,
28
- endPositionInput,
29
- ])).onOK(() => {
30
- const pi = DG.TaskBarProgressIndicator.create('Getting region...');
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) {
31
87
  try {
32
- const name: string = nameInput.value ?? getDefaultName();
33
- const regCol = getRegionDo(col, startPositionInput.value, endPositionInput.value, name);
34
- col.dataFrame.columns.add(regCol);
35
- regCol.setTag(DG.TAGS.CELL_RENDERER, 'sequence');
36
- } catch (err: any) {
37
- grok.shell.error(err.toString());
38
- } 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
+ },
39
124
  });
125
+ return regionInput;
40
126
  }
41
127
 
42
128
  /** {@link startPosName} and {@link endPosName} are according positionNames tag (or default ['1', '2',...]) */
@@ -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,