@datagrok/bio 2.9.0 → 2.10.2

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.
@@ -3,15 +3,19 @@ import * as DG from 'datagrok-api/dg';
3
3
  import * as ui from 'datagrok-api/ui';
4
4
 
5
5
  import {ALPHABET, NOTATION} from '@datagrok-libraries/bio/src/utils/macromolecule';
6
+ import {UnitsHandler} from '@datagrok-libraries/bio/src/utils/units-handler';
7
+ import {ColumnInputOptions} from '@datagrok-libraries/utils/src/type-declarations';
8
+
6
9
  import {runKalign} from './multiple-sequence-alignment';
7
10
  import {pepseaMethods, runPepsea} from './pepsea';
8
11
  import {checkInputColumnUI} from './check-input-column';
9
- import {NotationConverter} from '@datagrok-libraries/bio/src/utils/notation-converter';
10
- import {_package} from '../package';
11
12
  import {multipleSequenceAlginmentUIOptions} from './types';
12
13
  import {kalignVersion, msaDefaultOptions} from './constants';
14
+
15
+ import {_package} from '../package';
16
+
13
17
  import '../../css/msa.css';
14
- import {ColumnInputOptions} from '@datagrok-libraries/utils/src/type-declarations';
18
+
15
19
  export class MsaWarning extends Error {
16
20
  constructor(message: string, options?: ErrorOptions) {
17
21
  super(message, options);
@@ -66,12 +70,12 @@ export async function multipleSequenceAlignmentUI(
66
70
  //TODO: remove when the new version of datagrok-api is available
67
71
  //TODO: allow only macromolecule colums to be chosen
68
72
  const colInput = ui.columnInput('Sequence', table, seqCol, async () => {
69
- performAlignment = await onColInputChange(
70
- colInput.value, table, pepseaInputRootStyles, kalignInputRootStyles,
71
- methodInput, clustersColInput, gapOpenInput, gapExtendInput, terminalGapInput,
72
- );
73
- //@ts-ignore
74
- }, {filter: (col: DG.Column) => col.semType === DG.SEMTYPE.MACROMOLECULE} as ColumnInputOptions
73
+ performAlignment = await onColInputChange(
74
+ colInput.value, table, pepseaInputRootStyles, kalignInputRootStyles,
75
+ methodInput, clustersColInput, gapOpenInput, gapExtendInput, terminalGapInput,
76
+ );
77
+ //@ts-ignore
78
+ }, {filter: (col: DG.Column) => col.semType === DG.SEMTYPE.MACROMOLECULE} as ColumnInputOptions
75
79
  ) as DG.InputBase<DG.Column<string>>;
76
80
  colInput.setTooltip('Sequences column to use for alignment');
77
81
  const clustersColInput = ui.columnInput('Clusters', table, options.clustersCol);
@@ -100,7 +104,7 @@ export async function multipleSequenceAlignmentUI(
100
104
  }
101
105
 
102
106
  async function onDialogOk(
103
- colInput: DG.InputBase< DG.Column<any>>,
107
+ colInput: DG.InputBase<DG.Column<any>>,
104
108
  table: DG.DataFrame,
105
109
  performAlignment: (() => Promise<DG.Column<string> | null>) | undefined,
106
110
  resolve: (value: DG.Column<any>) => void,
@@ -151,9 +155,9 @@ async function onColInputChange(
151
155
  gapOpenInput.value = null;
152
156
  gapExtendInput.value = null;
153
157
  terminalGapInput.value = null;
154
- const potentialColNC = new NotationConverter(col);
155
- const performCol: DG.Column<string> = potentialColNC.isFasta() ? col :
156
- potentialColNC.convert(NOTATION.FASTA);
158
+ const potentialColUH = UnitsHandler.getOrCreate(col);
159
+ const performCol: DG.Column<string> = potentialColUH.isFasta() ? col :
160
+ potentialColUH.convert(NOTATION.FASTA);
157
161
  return async () => await runKalign(performCol, false, unusedName, clustersColInput.value);
158
162
  } else if (checkInputColumnUI(col, col.name,
159
163
  [NOTATION.HELM], [], false)
@@ -163,18 +167,18 @@ async function onColInputChange(
163
167
  gapExtendInput.value ??= msaDefaultOptions.pepsea.gapExtend;
164
168
 
165
169
  return async () => await runPepsea(col, unusedName, methodInput.value!,
166
- gapOpenInput.value!, gapExtendInput.value!, clustersColInput.value);
170
+ gapOpenInput.value!, gapExtendInput.value!, clustersColInput.value);
167
171
  } else if (checkInputColumnUI(col, col.name, [NOTATION.SEPARATOR], [ALPHABET.UN], false)) {
168
172
  //if the column is separator with unknown alphabet, it might be helm. check if it can be converted to helm
169
- const potentialColNC = new NotationConverter(col);
170
- const helmCol = potentialColNC.convert(NOTATION.HELM);
173
+ const potentialColUH = UnitsHandler.getOrCreate(col);
174
+ const helmCol = potentialColUH.convert(NOTATION.HELM);
171
175
  switchDialog(pepseaInputRootStyles, kalignInputRootStyles, 'pepsea');
172
176
  gapOpenInput.value ??= msaDefaultOptions.pepsea.gapOpen;
173
177
  gapExtendInput.value ??= msaDefaultOptions.pepsea.gapExtend;
174
178
  // convert to helm and assign alignment function to PepSea
175
179
 
176
180
  return async () => await runPepsea(helmCol, unusedName, methodInput.value!,
177
- gapOpenInput.value!, gapExtendInput.value!, clustersColInput.value);
181
+ gapOpenInput.value!, gapExtendInput.value!, clustersColInput.value);
178
182
  } else {
179
183
  gapOpenInput.value = null;
180
184
  gapExtendInput.value = null;
@@ -8,14 +8,15 @@ import {fromEvent, Observable, Subject, Unsubscribable} from 'rxjs';
8
8
  import {UnitsHandler} from '@datagrok-libraries/bio/src/utils/units-handler';
9
9
  import {SeqPalette} from '@datagrok-libraries/bio/src/seq-palettes';
10
10
  import {
11
- monomerToShort, pickUpPalette, pickUpSeqCol, TAGS as bioTAGS
11
+ monomerToShort, pickUpPalette, pickUpSeqCol, TAGS as bioTAGS, positionSeparator
12
12
  } from '@datagrok-libraries/bio/src/utils/macromolecule';
13
13
  import {
14
- FilterSources, HorizontalAlignments, IWebLogoViewer, PositionHeight, PositionMarginStates, positionSeparator,
15
- TAGS as wlTAGS, VerticalAlignments, WebLogoProps, WebLogoPropsDefault
14
+ FilterSources, HorizontalAlignments, IWebLogoViewer, PositionHeight, PositionMarginStates,
15
+ VerticalAlignments, WebLogoProps, WebLogoPropsDefault
16
16
  } from '@datagrok-libraries/bio/src/viewers/web-logo';
17
17
  import {errorToConsole} from '@datagrok-libraries/utils/src/to-console';
18
18
  import {intToHtmlA} from '@datagrok-libraries/utils/src/color';
19
+ import {TAGS} from '@datagrok-libraries/bio/src/utils/macromolecule';
19
20
  import {ISeqSplitted} from '@datagrok-libraries/bio/src/utils/macromolecule/types';
20
21
 
21
22
  import {_package} from '../package';
@@ -143,7 +144,7 @@ export class PositionInfo {
143
144
  }
144
145
 
145
146
  calcScreen(
146
- posIdx: number, firstVisiblePosIdx: number,
147
+ isGap: (m: string) => boolean, posIdx: number, firstVisiblePosIdx: number,
147
148
  absoluteMaxHeight: number, heightMode: PositionHeight, alphabetSizeLog: number,
148
149
  positionWidthWithMargin: number, positionWidth: number, dpr: number, axisHeight: number
149
150
  ): void {
@@ -154,13 +155,13 @@ export class PositionInfo {
154
155
 
155
156
  const entries = Object.entries(this._freqs)
156
157
  .sort((a, b) => {
157
- if (a[0] !== '-' && b[0] !== '-')
158
+ if (!isGap(a[0]) && !isGap(b[0]))
158
159
  return b[1].count - a[1].count;
159
- else if (a[0] === '-' && b[0] === '-')
160
+ else if (isGap(a[0]) && isGap(b[0]))
160
161
  return 0;
161
- else if (a[0] === '-')
162
+ else if (isGap(a[0]))
162
163
  return -1;
163
- else /* (b[0] === '-') */
164
+ else /* (isGap(b[0])) */
164
165
  return +1;
165
166
  });
166
167
  for (const entry of entries) {
@@ -176,10 +177,11 @@ export class PositionInfo {
176
177
  }
177
178
 
178
179
  render(g: CanvasRenderingContext2D,
180
+ isGap: (m: string) => boolean,
179
181
  fontStyle: string, uppercaseLetterAscent: number, uppercaseLetterHeight: number, cp: SeqPalette
180
182
  ) {
181
183
  for (const [monomer, pmInfo] of Object.entries(this._freqs)) {
182
- if (monomer !== '-') {
184
+ if (!isGap(monomer)) {
183
185
  const monomerTxt = monomerToShort(monomer, 5);
184
186
  const b = pmInfo.bounds!;
185
187
  const left = b.left;
@@ -415,21 +417,33 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
415
417
  // -- Data --
416
418
 
417
419
  setData(): void {
418
- if (this.viewed) {
419
- this.destroyView();
420
- this.viewed = false;
421
- }
422
-
423
- this.updateSeqCol();
420
+ if (!this.setDataInProgress) this.setDataInProgress = true; else return;
421
+ _package.logger.debug(`Bio: WebLogoViewer<${this.viewerId}>.setData() `);
424
422
 
425
- if (!this.viewed) {
426
- this.buildView();
427
- this.viewed = true;
428
- }
423
+ this.viewPromise = this.viewPromise.then(async () => { // setData
424
+ if (this.viewed) {
425
+ await this.destroyView();
426
+ this.viewed = false;
427
+ }
428
+ }).then(async () => {
429
+ await this.detachPromise;
430
+
431
+ this.updateSeqCol();
432
+ }).then(async () => {
433
+ if (!this.viewed) {
434
+ await this.buildView();
435
+ this.viewed = true;
436
+ }
437
+ }).finally(() => {
438
+ this.setDataInProgress = false;
439
+ });
429
440
  }
430
441
 
431
442
  // -- View --
432
443
 
444
+ private viewPromise: Promise<void> = Promise.resolve();
445
+ private detachPromise: Promise<void> = Promise.resolve();
446
+ private setDataInProgress: boolean = false;
433
447
  private viewSubs: Unsubscribable[] = [];
434
448
 
435
449
  private async destroyView(): Promise<void> {
@@ -438,7 +452,6 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
438
452
 
439
453
  const dataFrameTxt = `${this.dataFrame ? 'data' : 'null'}`;
440
454
  _package.logger.debug(`Bio: WebLogoViewer<${this.viewerId}>.destroyView( dataFrame = ${dataFrameTxt} ) start`);
441
- super.detach();
442
455
 
443
456
  this.viewSubs.forEach((sub) => sub.unsubscribe());
444
457
  this.host!.remove();
@@ -524,8 +537,6 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
524
537
  }
525
538
  if (this.seqCol) {
526
539
  try {
527
- const units: string = this.seqCol!.getTag(DG.TAGS.UNITS);
528
- const separator: string = this.seqCol!.getTag(bioTAGS.separator);
529
540
  this.unitsHandler = UnitsHandler.getOrCreate(this.seqCol);
530
541
 
531
542
  this.cp = pickUpPalette(this.seqCol);
@@ -560,8 +571,8 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
560
571
  }).reduce((max, l) => Math.max(max, l), 0);
561
572
 
562
573
  /** positionNames and positionLabel can be set up through the column's tags only */
563
- const positionNamesTxt = this.seqCol.getTag(wlTAGS.positionNames);
564
- const positionLabelsTxt = this.seqCol.getTag(wlTAGS.positionLabels);
574
+ const positionNamesTxt = this.seqCol.getTag(TAGS.positionNames);
575
+ const positionLabelsTxt = this.seqCol.getTag(TAGS.positionLabels);
565
576
  this.positionNames = !!positionNamesTxt ? positionNamesTxt.split(positionSeparator).map((v) => v.trim()) :
566
577
  [...Array(maxLength).keys()].map((jPos) => `${jPos + 1}`)/* fallback if tag is not provided */;
567
578
  this.positionLabels = !!positionLabelsTxt ? positionLabelsTxt.split(positionSeparator).map((v) => v.trim()) :
@@ -810,10 +821,18 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
810
821
 
811
822
  /** Remove all handlers when table is a detach */
812
823
  public override async detach() {
813
- if (this.viewed) {
814
- this.destroyView();
815
- this.viewed = false;
816
- }
824
+ if (this.setDataInProgress) return;
825
+ _package.logger.debug(`Bio: WebLogoViewer<${this.viewerId}>.detach(), `);
826
+
827
+ const superDetach = super.detach.bind(this);
828
+ this.detachPromise = this.detachPromise.then(async () => { // detach
829
+ await this.viewPromise;
830
+ if (this.viewed) {
831
+ await this.destroyView();
832
+ this.viewed = false;
833
+ }
834
+ superDetach();
835
+ });
817
836
  }
818
837
 
819
838
  private _onSizeChanged: Subject<void> = new Subject<void>();
@@ -860,8 +879,10 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
860
879
  /** Function for removing empty positions */
861
880
  protected _removeEmptyPositions() {
862
881
  if (this.skipEmptyPositions) {
863
- this.positions = wu(this.positions)
864
- .filter((pi) => !pi.hasMonomer('-') || pi.getFreq('-').count !== pi.rowCount).toArray();
882
+ this.positions = wu(this.positions).filter((pi) => {
883
+ const gapSymbol: string = this.unitsHandler!.defaultGapSymbol;
884
+ return !pi.hasMonomer(gapSymbol) || pi.getFreq(gapSymbol).count !== pi.rowCount;
885
+ }).toArray();
865
886
  }
866
887
  }
867
888
 
@@ -898,7 +919,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
898
919
  if (dfFilter.get(rowI)) {
899
920
  const seqMList: ISeqSplitted = splitted[rowI];
900
921
  for (let jPos = 0; jPos < length; ++jPos) {
901
- const m: string = seqMList[this.startPosition + jPos] || '-';
922
+ const m: string = seqMList[this.startPosition + jPos] || this.unitsHandler.defaultGapSymbol;
902
923
  const pmInfo = this.positions[jPos].getFreq(m);
903
924
  pmInfo.count++;
904
925
  }
@@ -907,7 +928,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
907
928
 
908
929
  //#region Polish freq counts
909
930
  for (let jPos = 0; jPos < length; jPos++) {
910
- // delete this.positions[jPos].freq['-'];
931
+ // delete this.positions[jPos].freq[this.unitsHandler.defaultGapSymbol];
911
932
  this.positions[jPos].calcHeights(this.positionHeight as PositionHeight);
912
933
  }
913
934
  //#endregion
@@ -927,7 +948,13 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
927
948
  const alphabetSizeLog = Math.log2(alphabetSize);
928
949
 
929
950
  for (let jPos = Math.floor(this.slider.min); jPos <= Math.floor(this.slider.max); ++jPos) {
930
- this.positions[jPos].calcScreen(jPos, this.slider.min, absoluteMaxHeight, this.positionHeight,
951
+ if (!(jPos in this.positions)) {
952
+ console.warn(`Bio: WebLogoViewer<${this.viewerId}>.render.calculateLayoutInt() ` +
953
+ `this.positions.length = ${this.positions.length}, jPos = ${jPos}`);
954
+ continue;
955
+ }
956
+ this.positions[jPos].calcScreen((m) => { return this.unitsHandler!.isGap(m); },
957
+ jPos, this.slider.min, absoluteMaxHeight, this.positionHeight,
931
958
  alphabetSizeLog, this._positionWidthWithMargin, this._positionWidth, dpr, positionLabelsHeight);
932
959
  }
933
960
  _package.logger.debug(`Bio: WebLogoViewer<${this.viewerId}>.render.calculateLayoutInt(), end `);
@@ -990,7 +1017,8 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
990
1017
  const uppercaseLetterAscent = 0.25;
991
1018
  const uppercaseLetterHeight = 12.2;
992
1019
  for (let jPos = Math.floor(this.slider.min); jPos <= Math.floor(this.slider.max); jPos++) {
993
- this.positions[jPos].render(g, fontStyle, uppercaseLetterAscent, uppercaseLetterHeight,
1020
+ this.positions[jPos].render(g, (m) => { return this.unitsHandler!.isGap(m); },
1021
+ fontStyle, uppercaseLetterAscent, uppercaseLetterHeight,
994
1022
  /* this._positionWidthWithMargin, firstVisiblePosIdx,*/ this.cp);
995
1023
  }
996
1024
 
@@ -1034,36 +1062,37 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
1034
1062
  min: this.slider.min, max: this.slider.max,
1035
1063
  maxRange: this.slider.maxRange
1036
1064
  };
1037
- _package.logger.debug(`Bio: WebLogoViewer<${this.viewerId}>.sliderOnValuesChanged( ${JSON.stringify(val)} ), start`);
1065
+ _package.logger.debug(
1066
+ `Bio: WebLogoViewer<${this.viewerId}>.sliderOnValuesChanged( ${JSON.stringify(val)} ), start`);
1038
1067
  this.render(WlRenderLevel.Layout, 'sliderOnValuesChanged').then(() => {});
1039
1068
  } catch (err: any) {
1040
1069
  const errMsg = errorToConsole(err);
1041
- _package.logger.error('Bio: WebLogoViewer<${this.viewerId}>.sliderOnValuesChanged() error:\n' + errMsg);
1070
+ _package.logger.error(`Bio: WebLogoViewer<${this.viewerId}>.sliderOnValuesChanged() error:\n` + errMsg);
1042
1071
  //throw err; // Do not throw to prevent disabling event handler
1043
1072
  }
1044
1073
  }
1045
1074
 
1046
1075
  private dataFrameFilterOnChanged(_value: any): void {
1047
- _package.logger.debug('Bio: WebLogoViewer<${this.viewerId}>.dataFrameFilterChanged()');
1076
+ _package.logger.debug(`Bio: WebLogoViewer<${this.viewerId}>.dataFrameFilterChanged()`);
1048
1077
  try {
1049
1078
  this.updatePositions();
1050
1079
  if (this.filterSource === FilterSources.Filtered)
1051
1080
  this.render(WlRenderLevel.Freqs, 'dataFrameFilterOnChanged').then(() => {});
1052
1081
  } catch (err: any) {
1053
1082
  const errMsg = errorToConsole(err);
1054
- _package.logger.error('Bio: WebLogoViewer<${this.viewerId}>.dataFrameFilterOnChanged() error:\n' + errMsg);
1083
+ _package.logger.error(`Bio: WebLogoViewer<${this.viewerId}>.dataFrameFilterOnChanged() error:\n` + errMsg);
1055
1084
  //throw err; // Do not throw to prevent disabling event handler
1056
1085
  }
1057
1086
  }
1058
1087
 
1059
1088
  private dataFrameSelectionOnChanged(_value: any): void {
1060
- _package.logger.debug('Bio: WebLogoViewer<${this.viewerId}>.dataFrameSelectionOnChanged()');
1089
+ _package.logger.debug(`Bio: WebLogoViewer<${this.viewerId}>.dataFrameSelectionOnChanged()`);
1061
1090
  try {
1062
1091
  if (this.filterSource === FilterSources.Selected)
1063
1092
  this.render(WlRenderLevel.Freqs, 'dataFrameSelectionOnChanged').then(() => {});
1064
1093
  } catch (err: any) {
1065
1094
  const errMsg = errorToConsole(err);
1066
- _package.logger.error('Bio: WebLogoViewer<${this.viewerId}>.dataFrameSelectionOnChanged() error:\n' + errMsg);
1095
+ _package.logger.error(`Bio: WebLogoViewer<${this.viewerId}>.dataFrameSelectionOnChanged() error:\n` + errMsg);
1067
1096
  //throw err; // Do not throw to prevent disabling event handler
1068
1097
  }
1069
1098
  }
@@ -1097,7 +1126,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
1097
1126
  }
1098
1127
  } catch (err: any) {
1099
1128
  const errMsg = errorToConsole(err);
1100
- _package.logger.error('Bio: WebLogoViewer<${this.viewerId}>.canvasOnMouseMove() error:\n' + errMsg);
1129
+ _package.logger.error(`Bio: WebLogoViewer<${this.viewerId}>.canvasOnMouseMove() error:\n` + errMsg);
1101
1130
  //throw err; // Do not throw to prevent disabling event handler
1102
1131
  }
1103
1132
  }
@@ -1120,7 +1149,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
1120
1149
  }
1121
1150
  } catch (err: any) {
1122
1151
  const errMsg = errorToConsole(err);
1123
- _package.logger.error('Bio: WebLogoViewer<${this.viewerId}>.canvasOnMouseDown() error:\n' + errMsg);
1152
+ _package.logger.error(`Bio: WebLogoViewer<${this.viewerId}>.canvasOnMouseDown() error:\n` + errMsg);
1124
1153
  //throw err; // Do not throw to prevent disabling event handler
1125
1154
  }
1126
1155
  }
@@ -1134,7 +1163,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
1134
1163
  this.slider.scrollBy(this.slider.min + countOfScrollPositions);
1135
1164
  } catch (err: any) {
1136
1165
  const errMsg = errorToConsole(err);
1137
- _package.logger.error('Bio: WebLogoViewer<${this.viewerId}>.canvasOnWheel() error:\n' + errMsg);
1166
+ _package.logger.error(`Bio: WebLogoViewer<${this.viewerId}>.canvasOnWheel() error:\n` + errMsg);
1138
1167
  //throw err; // Do not throw to prevent disabling event handler
1139
1168
  }
1140
1169
  }
@@ -1159,7 +1188,7 @@ export function checkSeqForMonomerAtPos(
1159
1188
  ): boolean {
1160
1189
  const seqMList: ISeqSplitted = unitsHandler.splitted[rowI];
1161
1190
  const seqM = at.pos < seqMList.length ? seqMList[at.pos] : null;
1162
- return ((seqM === monomer) || (seqM === '' && monomer === '-'));
1191
+ return ((seqM === monomer) || (seqM === '' && monomer === unitsHandler.defaultGapSymbol));
1163
1192
  }
1164
1193
 
1165
1194
  export function countForMonomerAtPosition(