@datagrok/bio 2.10.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.
@@ -0,0 +1,11 @@
1
+ HELM,Activity
2
+ ,5.30751
3
+ PEPTIDE1{meI.hHis.Aca.Cys_SEt.T.dK.Thr_PO3H2.Aca.Tyr_PO3H2.D-Chg.dV.Phe_ab-dehydro.N.D-Orn.D-aThr.Phe_4Me}$$$$,5.72388
4
+ ,5.18581
5
+ PEPTIDE1{meI.hHis.unkn1.Cys_SEt.unkn2.dK.Thr_PO3H2.Aca.Tyr_PO3H2.D-Chg.dV.Thr_PO3H2.N.D-Orn.D-aThr.Phe_4Me}$$$$,6.22350
6
+ PEPTIDE1{meI.hHis.Aca.N.T.dK.Thr_PO3H2.Aca.D-Tyr_Et.Tyr_ab-dehydroMe.dV.Chg.N.D-Orn.D-aThr.Phe_4Me}$$$$,3.84591
7
+ PEPTIDE1{meI.hHis.Aca.N.unkn3.dK.Thr_PO3H2.Aca.D-Tyr_Et.Tyr_Bn.dV.E.N.dV.Phe_4Me}$$$$,3.27920
8
+ ,2.10585
9
+ PEPTIDE1{meI.hHis.Aca.N.T.dK.Thr_PO3H2.Aca.D-Tyr_Et.meQ.dV.E.N.dV.Phe_4Me}$$$$,1.80369
10
+ PEPTIDE1{meI.hHis.Aca.N.T.dK.Thr_PO3H2.Aca.Oic_3aS-7aS.D-Chg.dV.E.N.D-Orn.D-aThr.Phe_4Me}$$$$,6.38806
11
+ PEPTIDE1{meI.hHis.Aca.N.T.dK.Thr_PO3H2.Aca.meM.D-Chg.dV.E.N.D-Orn.D-aThr.Phe_4Me}$$$$,4.44165
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "name": "Leonid Stolbov",
6
6
  "email": "lstolbov@datagrok.ai"
7
7
  },
8
- "version": "2.10.0",
8
+ "version": "2.10.2",
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",
@@ -34,7 +34,7 @@
34
34
  ],
35
35
  "dependencies": {
36
36
  "@biowasm/aioli": "^3.1.0",
37
- "@datagrok-libraries/bio": "^5.38.0",
37
+ "@datagrok-libraries/bio": "^5.38.2",
38
38
  "@datagrok-libraries/chem-meta": "^1.0.1",
39
39
  "@datagrok-libraries/ml": "^6.3.39",
40
40
  "@datagrok-libraries/tutorials": "^1.3.6",
@@ -7,38 +7,44 @@ import {IWebLogoViewer} from '@datagrok-libraries/bio/src/viewers/web-logo';
7
7
 
8
8
  import {_package} from '../package';
9
9
 
10
- const csv = `seq,value
10
+ const defaultData: GetRegionAppData = {
11
+ df: DG.DataFrame.fromCsv(`seq,value
11
12
  ATCCGTCGT,0.5
12
13
  TGTTCGTCA,0.4
13
14
  ATGGTCGTA,0.7
14
- ATCCGTGCA,0.1`;
15
+ ATCCGTGCA,0.1`),
16
+ colName: 'seq',
17
+ positionNames: ['1', '1A', '1C', '2', '4', '4A', '4B', '5', '6'].join(positionSeparator),
18
+ regions: [
19
+ {name: 'first region', start: '1', end: '2'},
20
+ {name: 'second region', start: '1C', end: '4'},
21
+ {name: 'overlapping second', start: '1C', end: '4A'},
22
+ {name: 'whole sequence', start: '1', end: '6'},
23
+ {name: 'bad start', start: '0', end: '6'},
24
+ {name: 'bad end', start: '1', end: '4C'},
25
+ {name: 'bad start & end', start: '0', end: '4C'},
26
+ ]
27
+ };
15
28
 
16
- const positionNames = ['1', '1A', '1C', '2', '4', '4A', '4B', '5', '6'].join(positionSeparator);
17
-
18
- const regions = [
19
- {name: 'first region', start: '1', end: '2'},
20
- {name: 'second region', start: '1C', end: '4'},
21
- {name: 'overlapping second', start: '1C', end: '4A'},
22
- {name: 'whole sequence', start: '1', end: '6'},
23
- {name: 'bad start', start: '0', end: '6'},
24
- {name: 'bad end', start: '1', end: '4C'},
25
- {name: 'bad start & end', start: '0', end: '4C'},
26
- ];
29
+ export type GetRegionAppData = {
30
+ df: DG.DataFrame, colName: string,
31
+ positionNames?: string, regions?: { name: string, start: string, end: string }[]
32
+ };
27
33
 
28
34
  export class GetRegionApp {
29
- df: DG.DataFrame;
30
35
  view: DG.TableView;
36
+ data!: GetRegionAppData;
31
37
 
32
38
  constructor(
33
39
  private readonly urlParams: URLSearchParams,
34
40
  private readonly funcName: string
35
41
  ) {}
36
42
 
37
- async init(): Promise<void> {
38
- this.df = DG.DataFrame.fromCsv(csv);
39
- const seqCol = this.df.getCol('seq');
40
- seqCol.setTag(TAGS.positionNames, positionNames);
41
- seqCol.setTag(TAGS.regions, JSON.stringify(regions));
43
+ async init(data?: GetRegionAppData): Promise<void> {
44
+ this.data = data ?? defaultData;
45
+ const seqCol = this.data.df.getCol(this.data.colName);
46
+ if (!!this.data.positionNames) seqCol.setTag(TAGS.positionNames, this.data.positionNames);
47
+ if (!!this.data.regions) seqCol.setTag(TAGS.regions, JSON.stringify(this.data.regions));
42
48
 
43
49
  await this.buildView();
44
50
  }
@@ -46,11 +52,14 @@ export class GetRegionApp {
46
52
  // -- View --
47
53
 
48
54
  async buildView(): Promise<void> {
49
- this.view = grok.shell.addTableView(this.df);
55
+ // To allow showing a WebLogoViewer
56
+ await grok.data.detectSemanticTypes(this.data.df);
57
+
58
+ this.view = grok.shell.addTableView(this.data.df);
50
59
  this.view.path = this.view.basePath = `func/${_package.name}.${this.funcName}`;
51
60
 
52
61
  const viewer: DG.Viewer & IWebLogoViewer = (await this.view.dataFrame.plot
53
- .fromType('WebLogo')) as DG.Viewer & IWebLogoViewer;
62
+ .fromType('WebLogo', {sequenceColumnName: this.data.colName})) as DG.Viewer & IWebLogoViewer;
54
63
  this.view.dockManager.dock(viewer, DG.DOCK_TYPE.DOWN, null, 'WebLogo', 0.35);
55
64
  }
56
65
  }
package/src/package.ts CHANGED
@@ -365,13 +365,13 @@ export function getRegion(
365
365
  //input: string end {optional: true} [Region end position name]
366
366
  //input: string name {optional: true} [Region column name]
367
367
  //editor: Bio:GetRegionEditor
368
- export function getRegionTopMenu(
368
+ export async function getRegionTopMenu(
369
369
  table: DG.DataFrame, sequence: DG.Column,
370
370
  start: string | undefined, end: string | undefined, name: string | undefined
371
- ): void {
371
+ ): Promise<void> {
372
372
  const regCol = getRegionDo(sequence, start ?? null, end ?? null, name ?? null);
373
373
  sequence.dataFrame.columns.add(regCol);
374
- regCol.setTag(DG.TAGS.CELL_RENDERER, 'sequence');
374
+ await grok.data.detectSemanticTypes(sequence.dataFrame); // to set renderer
375
375
  }
376
376
 
377
377
  //top-menu: Bio | Analyze | Activity Cliffs...
@@ -656,13 +656,6 @@ export async function compositionAnalysis(): Promise<void> {
656
656
  await handler(col);
657
657
  }
658
658
 
659
- //top-menu: Bio | Convert | SDF to JSON Library...
660
- //name: SDF to JSON Library
661
- //input: dataframe table
662
- export async function sdfToJsonLib(table: DG.DataFrame) {
663
- const _jsonMonomerLibrary = createJsonMonomerLibFromSdf(table);
664
- }
665
-
666
659
  // 2023-05-17 Representations does not work at BioIT
667
660
  // //name: Representations
668
661
  // //tags: panel, widgets
@@ -915,6 +908,19 @@ export async function getRegionApp(): Promise<void> {
915
908
  }
916
909
  }
917
910
 
911
+ //name: getRegionHelmApp
912
+ export async function getRegionHelmApp(): Promise<void> {
913
+ const pi = DG.TaskBarProgressIndicator.create('getRegion ...');
914
+ try {
915
+ const urlParams = new URLSearchParams(window.location.search);
916
+ const df = await _package.files.readCsv('data/sample_HELM_empty_vals.csv');
917
+ const app = new GetRegionApp(urlParams, 'getRegionHelmApp');
918
+ await app.init({df: df, colName: 'HELM'});
919
+ } finally {
920
+ pi.close();
921
+ }
922
+ }
923
+
918
924
  // -- Handle context menu --
919
925
 
920
926
  ///name: addCopyMenu
@@ -38,6 +38,7 @@ category('detectors', () => {
38
38
  sepUn1 = 'sepUn1',
39
39
  sepUn2 = 'sepUn2',
40
40
  sepMsaDna1 = 'sepMsaDna1',
41
+ sepMsaUnWEmpty = 'sepMsaUnWEmpty',
41
42
  fastaMsaDna1 = 'fastaMsaDna1',
42
43
  fastaMsaPt1 = 'fastaMsaPt1',
43
44
  }
@@ -109,6 +110,11 @@ rut12/rty/her2/abc/cfr3//wert/rut12`;
109
110
  A-C--G-T--C-T
110
111
  C-A-C--T--G-T
111
112
  A-C-C-G-T-A-C-T`;
113
+ [csvTests.sepMsaUnWEmpty]: string = `seq
114
+ m1-M-m3-mon4-mon5-N-T-MON8-N9
115
+ m1-mon2-m3-mon4-mon5-Num--MON8-N9
116
+
117
+ mon1-M-mon3-mon4-mon5---MON8-N9`;
112
118
  [csvTests.fastaMsaDna1]: string = `seq
113
119
  AC-GT-CT
114
120
  CAC-T-GT
@@ -234,6 +240,11 @@ MWRSWY-CKHP`;
234
240
  await _testPos(readCsv(csvTests.fastaUn), 'seq',
235
241
  NOTATION.FASTA, ALIGNMENT.SEQ_MSA, ALPHABET.UN, 12, true);
236
242
  });
243
+
244
+ test('SepMsaUnWEmpty', async () => {
245
+ await _testPos(readCsv(csvTests.sepMsaUnWEmpty), 'seq',
246
+ NOTATION.SEPARATOR, ALIGNMENT.SEQ_MSA, ALPHABET.UN, 14, true);
247
+ });
237
248
  test('FastaMsaDna1', async () => {
238
249
  await _testPos(readCsv(csvTests.fastaMsaDna1), 'seq',
239
250
  NOTATION.FASTA, ALIGNMENT.SEQ_MSA, ALPHABET.DNA, 4, false);
@@ -274,13 +285,13 @@ MWRSWY-CKHP`;
274
285
  await _testDf(readSamples(Samples.fastaCsv), {
275
286
  'Sequence': new PosCol(NOTATION.FASTA, ALIGNMENT.SEQ, ALPHABET.PT, 20, false),
276
287
  });
277
- }, {skipReason: 'GROK-13851: Unhandled exceptions'});
288
+ });
278
289
 
279
290
  test('samplesFastaFasta', async () => {
280
291
  await _testDf(readSamples(Samples.fastaFasta), {
281
292
  'sequence': new PosCol(NOTATION.FASTA, ALIGNMENT.SEQ, ALPHABET.PT, 20, false),
282
293
  });
283
- }, {skipReason: 'GROK-13851: Unhandled exceptions'});
294
+ });
284
295
 
285
296
  // peptidesComplex contains monomers with spaces in AlignedSequence columns, which are forbidden
286
297
  // test('samplesPeptidesComplexPositiveAlignedSequence', async () => {
@@ -2,12 +2,14 @@ import * as grok from 'datagrok-api/grok';
2
2
  import * as DG from 'datagrok-api/dg';
3
3
  import * as ui from 'datagrok-api/ui';
4
4
 
5
- import {_package} from '../package-test';
5
+ import wu from 'wu';
6
6
  import {category, test} from '@datagrok-libraries/utils/src/test';
7
7
  import {MonomerPlacer} from '@datagrok-libraries/bio/src/utils/cell-renderer-monomer-placer';
8
8
  import {monomerToShort} from '@datagrok-libraries/bio/src/utils/macromolecule';
9
9
  import {UnitsHandler} from '@datagrok-libraries/bio/src/utils/units-handler';
10
10
 
11
+ import {_package} from '../package-test';
12
+
11
13
  category('renderers: monomerPlacer', () => {
12
14
  const tests = {
13
15
  splitter: {
@@ -34,21 +36,27 @@ category('renderers: monomerPlacer', () => {
34
36
  },
35
37
  splitterMsa: {
36
38
  /** For charWidth=7 and sepWidth=12, MSA
37
- * Array(10) [0, 26, 52, 78, 104, 130, 156, 175, 201, 227]
39
+ * Array(10) [5, 38, 71, 104, 137, 170, 203, 222, 255, 281]
38
40
  */
39
41
  csv: 'id,seq\n' +
40
- 'id1,m1-M-m3-mon4-mon5-N-T-MON8-N9\n' + //Array(10) [0, 26, 52, 78, 104, 130, 156, 175, 201, 227]
41
- 'id2,m1-mon2-m3-mon4-mon5-Num--MON8-N9\n' + //
42
- 'id3,mon1-M-mon3-mon4-mon5---MON8-N9\n', //
42
+ 'id1,m1-M-m3-mon4-mon5-N-T-MON8-N9\n' +
43
+ 'id2,m1-mon2-m3-mon4-mon5-Num--MON8-N9\n' +
44
+ 'id3,\n' + // empty
45
+ 'id4,mon1-M-mon3-mon4-mon5---MON8-N9\n',
43
46
  testList: [
44
47
  {src: {row: 0, x: -1}, tgt: {pos: null}},
45
48
  {src: {row: 1, x: 0}, tgt: {pos: null}},
46
49
  {src: {row: 1, x: 1}, tgt: {pos: null}},
47
- {src: {row: 1, x: 26}, tgt: {pos: 0}},
50
+ {src: {row: 1, x: 4}, tgt: {pos: null}},
51
+ {src: {row: 1, x: 5}, tgt: {pos: 0}},
52
+ {src: {row: 1, x: 37}, tgt: {pos: 0}},
53
+ {src: {row: 1, x: 38}, tgt: {pos: 1}},
48
54
  {src: {row: 1, x: 170}, tgt: {pos: 4}},
49
55
  {src: {row: 1, x: 200}, tgt: {pos: 5}},
50
- {src: {row: 2, x: 200}, tgt: {pos: 5}},
51
- {src: {row: 2, x: 203}, tgt: {pos: 5}},
56
+ {src: {row: 2, x: 20}, tgt: {pos: null}}, // empty value
57
+ {src: {row: 3, x: 170}, tgt: {pos: 4}},
58
+ {src: {row: 3, x: 200}, tgt: {pos: 5}},
59
+ {src: {row: 3, x: 282}, tgt: {pos: null}},
52
60
  ]
53
61
  },
54
62
  fastaMsa: {
@@ -58,6 +66,7 @@ category('renderers: monomerPlacer', () => {
58
66
  csv: `id,seq
59
67
  id1,QQYNIYPLT
60
68
  id2,QQWSSFPYT
69
+ id3,
61
70
  id3,QHIRE--LT
62
71
  `,
63
72
  testList: [
@@ -67,14 +76,15 @@ id3,QHIRE--LT
67
76
  {src: {row: 1, x: 19}, tgt: {pos: 0}},
68
77
  {src: {row: 1, x: 170}, tgt: {pos: 8}},
69
78
  {src: {row: 1, x: 171}, tgt: {pos: 8}},
70
- {src: {row: 2, x: 170}, tgt: {pos: 8}},
71
- {src: {row: 2, x: 181}, tgt: {pos: null}},
79
+ {src: {row: 2, x: 5}, tgt: {pos: null}}, // empty value
80
+ {src: {row: 3, x: 170}, tgt: {pos: 8}},
81
+ {src: {row: 3, x: 181}, tgt: {pos: null}},
72
82
  ]
73
83
  },
74
84
  };
75
85
 
76
86
  for (const [testName, testData] of Object.entries(tests)) {
77
- test(`getPosition_${testName}`, async () => {
87
+ test(`getPosition-${testName}`, async () => {
78
88
  const df: DG.DataFrame = DG.DataFrame.fromCsv(testData.csv);
79
89
  await grok.data.detectSemanticTypes(df);
80
90
  const seqCol: DG.Column = df.getCol('seq');
@@ -94,9 +104,12 @@ id3,QHIRE--LT
94
104
  });
95
105
 
96
106
  const testList = testData.testList;
107
+ // simulate rendering
108
+ for (let rowI: number = 0; rowI < seqCol.length; ++rowI)
109
+ colTemp.getCellMonomerLengths(rowI);
97
110
 
98
111
  const errorList: string[] = [];
99
- for (const test of testList) {
112
+ for (const [test, _testI] of wu.enumerate(testList)) {
100
113
  const res = {pos: colTemp.getPosition(test.src.row, test.src.x)};
101
114
  if (test.tgt.pos != res.pos) {
102
115
  errorList.push(`Test src ${JSON.stringify(test.src)} expected tgt ${JSON.stringify(test.tgt)},` +
@@ -29,7 +29,7 @@ export class GetRegionFuncEditor {
29
29
  region: DG.InputBase<SeqRegion>;
30
30
  start: DG.InputBase<string>;
31
31
  end: DG.InputBase<string>;
32
- name: DG.InputBase;
32
+ name: DG.InputBase<string>;
33
33
  }();
34
34
 
35
35
  constructor(
@@ -54,8 +54,9 @@ export class GetRegionFuncEditor {
54
54
  this.inputs.region = ui.choiceInput<SeqRegion>('Region', null as unknown as SeqRegion, [],
55
55
  this.regionInputChanged.bind(this)) as DG.InputBase<SeqRegion>;
56
56
 
57
- this.inputs.name = ui.stringInput('Name', '', () => {},
58
- {placeholder: this.getDefaultName()});
57
+ this.inputs.name = ui.stringInput('Column name', this.getDefaultName(),
58
+ this.nameInputChanged.bind(this), {clearIcon: true});
59
+ this.inputs.name.onInput(this.nameInputInput.bind(this)); // To catch clear event
59
60
 
60
61
  // tooltips
61
62
  for (const paramName in this.call.inputParams) {
@@ -73,7 +74,7 @@ export class GetRegionFuncEditor {
73
74
  this.updateRegionItems();
74
75
  this.updateStartEndInputItems();
75
76
  this.updateRegion(true);
76
- this.updateNameInputPlaceholder();
77
+ this.updateNameInput();
77
78
  }
78
79
 
79
80
  private fixRegion: boolean = false;
@@ -99,12 +100,24 @@ export class GetRegionFuncEditor {
99
100
 
100
101
  private startInputChanged(): void {
101
102
  this.updateRegion(false);
102
- this.updateNameInputPlaceholder();
103
+ this.updateNameInput();
103
104
  }
104
105
 
105
106
  private endInputChanged(): void {
106
107
  this.updateRegion(false);
107
- this.updateNameInputPlaceholder();
108
+ this.updateNameInput();
109
+ }
110
+
111
+ private nameInputChanged(): void {
112
+ if (!this.defaultNameUpdating)
113
+ this.defaultName = false;
114
+ }
115
+
116
+ private nameInputInput(): void {
117
+ if (!this.inputs.name.value) {
118
+ this.defaultName = true;
119
+ this.inputs.name.input.focus();
120
+ }
108
121
  }
109
122
 
110
123
  private updateStartEndInputItems(): void {
@@ -168,9 +181,16 @@ export class GetRegionFuncEditor {
168
181
  }
169
182
  }
170
183
 
171
- private updateNameInputPlaceholder(): void {
172
- // @ts-ignore
173
- this.inputs.name.input.attributes['placeholder'].value = this.getDefaultName();
184
+ private defaultName: boolean = true;
185
+ private defaultNameUpdating: boolean = false;
186
+
187
+ private updateNameInput(): void {
188
+ this.defaultNameUpdating = true;
189
+ try {
190
+ if (this.defaultName) this.inputs.name.value = this.getDefaultName();
191
+ } finally {
192
+ this.defaultNameUpdating = false;
193
+ }
174
194
  }
175
195
 
176
196
  private getDefaultName(): string {
@@ -192,7 +212,7 @@ export class GetRegionFuncEditor {
192
212
  sequence: this.inputs.sequence.value!,
193
213
  start: this.getStart(),
194
214
  end: this.getEnd(),
195
- name: this.getName() ?? this.getDefaultName(),
215
+ name: this.getName(),
196
216
  };
197
217
  }
198
218
 
@@ -144,7 +144,7 @@ export class PositionInfo {
144
144
  }
145
145
 
146
146
  calcScreen(
147
- posIdx: number, firstVisiblePosIdx: number,
147
+ isGap: (m: string) => boolean, posIdx: number, firstVisiblePosIdx: number,
148
148
  absoluteMaxHeight: number, heightMode: PositionHeight, alphabetSizeLog: number,
149
149
  positionWidthWithMargin: number, positionWidth: number, dpr: number, axisHeight: number
150
150
  ): void {
@@ -155,13 +155,13 @@ export class PositionInfo {
155
155
 
156
156
  const entries = Object.entries(this._freqs)
157
157
  .sort((a, b) => {
158
- if (a[0] !== '-' && b[0] !== '-')
158
+ if (!isGap(a[0]) && !isGap(b[0]))
159
159
  return b[1].count - a[1].count;
160
- else if (a[0] === '-' && b[0] === '-')
160
+ else if (isGap(a[0]) && isGap(b[0]))
161
161
  return 0;
162
- else if (a[0] === '-')
162
+ else if (isGap(a[0]))
163
163
  return -1;
164
- else /* (b[0] === '-') */
164
+ else /* (isGap(b[0])) */
165
165
  return +1;
166
166
  });
167
167
  for (const entry of entries) {
@@ -177,10 +177,11 @@ export class PositionInfo {
177
177
  }
178
178
 
179
179
  render(g: CanvasRenderingContext2D,
180
+ isGap: (m: string) => boolean,
180
181
  fontStyle: string, uppercaseLetterAscent: number, uppercaseLetterHeight: number, cp: SeqPalette
181
182
  ) {
182
183
  for (const [monomer, pmInfo] of Object.entries(this._freqs)) {
183
- if (monomer !== '-') {
184
+ if (!isGap(monomer)) {
184
185
  const monomerTxt = monomerToShort(monomer, 5);
185
186
  const b = pmInfo.bounds!;
186
187
  const left = b.left;
@@ -416,21 +417,33 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
416
417
  // -- Data --
417
418
 
418
419
  setData(): void {
419
- if (this.viewed) {
420
- this.destroyView();
421
- this.viewed = false;
422
- }
423
-
424
- this.updateSeqCol();
420
+ if (!this.setDataInProgress) this.setDataInProgress = true; else return;
421
+ _package.logger.debug(`Bio: WebLogoViewer<${this.viewerId}>.setData() `);
425
422
 
426
- if (!this.viewed) {
427
- this.buildView();
428
- this.viewed = true;
429
- }
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
+ });
430
440
  }
431
441
 
432
442
  // -- View --
433
443
 
444
+ private viewPromise: Promise<void> = Promise.resolve();
445
+ private detachPromise: Promise<void> = Promise.resolve();
446
+ private setDataInProgress: boolean = false;
434
447
  private viewSubs: Unsubscribable[] = [];
435
448
 
436
449
  private async destroyView(): Promise<void> {
@@ -439,7 +452,6 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
439
452
 
440
453
  const dataFrameTxt = `${this.dataFrame ? 'data' : 'null'}`;
441
454
  _package.logger.debug(`Bio: WebLogoViewer<${this.viewerId}>.destroyView( dataFrame = ${dataFrameTxt} ) start`);
442
- super.detach();
443
455
 
444
456
  this.viewSubs.forEach((sub) => sub.unsubscribe());
445
457
  this.host!.remove();
@@ -525,8 +537,6 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
525
537
  }
526
538
  if (this.seqCol) {
527
539
  try {
528
- const units: string = this.seqCol!.getTag(DG.TAGS.UNITS);
529
- const separator: string = this.seqCol!.getTag(bioTAGS.separator);
530
540
  this.unitsHandler = UnitsHandler.getOrCreate(this.seqCol);
531
541
 
532
542
  this.cp = pickUpPalette(this.seqCol);
@@ -811,10 +821,18 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
811
821
 
812
822
  /** Remove all handlers when table is a detach */
813
823
  public override async detach() {
814
- if (this.viewed) {
815
- this.destroyView();
816
- this.viewed = false;
817
- }
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
+ });
818
836
  }
819
837
 
820
838
  private _onSizeChanged: Subject<void> = new Subject<void>();
@@ -861,8 +879,10 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
861
879
  /** Function for removing empty positions */
862
880
  protected _removeEmptyPositions() {
863
881
  if (this.skipEmptyPositions) {
864
- this.positions = wu(this.positions)
865
- .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();
866
886
  }
867
887
  }
868
888
 
@@ -899,7 +919,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
899
919
  if (dfFilter.get(rowI)) {
900
920
  const seqMList: ISeqSplitted = splitted[rowI];
901
921
  for (let jPos = 0; jPos < length; ++jPos) {
902
- const m: string = seqMList[this.startPosition + jPos] || '-';
922
+ const m: string = seqMList[this.startPosition + jPos] || this.unitsHandler.defaultGapSymbol;
903
923
  const pmInfo = this.positions[jPos].getFreq(m);
904
924
  pmInfo.count++;
905
925
  }
@@ -908,7 +928,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
908
928
 
909
929
  //#region Polish freq counts
910
930
  for (let jPos = 0; jPos < length; jPos++) {
911
- // delete this.positions[jPos].freq['-'];
931
+ // delete this.positions[jPos].freq[this.unitsHandler.defaultGapSymbol];
912
932
  this.positions[jPos].calcHeights(this.positionHeight as PositionHeight);
913
933
  }
914
934
  //#endregion
@@ -928,7 +948,13 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
928
948
  const alphabetSizeLog = Math.log2(alphabetSize);
929
949
 
930
950
  for (let jPos = Math.floor(this.slider.min); jPos <= Math.floor(this.slider.max); ++jPos) {
931
- 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,
932
958
  alphabetSizeLog, this._positionWidthWithMargin, this._positionWidth, dpr, positionLabelsHeight);
933
959
  }
934
960
  _package.logger.debug(`Bio: WebLogoViewer<${this.viewerId}>.render.calculateLayoutInt(), end `);
@@ -991,7 +1017,8 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
991
1017
  const uppercaseLetterAscent = 0.25;
992
1018
  const uppercaseLetterHeight = 12.2;
993
1019
  for (let jPos = Math.floor(this.slider.min); jPos <= Math.floor(this.slider.max); jPos++) {
994
- this.positions[jPos].render(g, fontStyle, uppercaseLetterAscent, uppercaseLetterHeight,
1020
+ this.positions[jPos].render(g, (m) => { return this.unitsHandler!.isGap(m); },
1021
+ fontStyle, uppercaseLetterAscent, uppercaseLetterHeight,
995
1022
  /* this._positionWidthWithMargin, firstVisiblePosIdx,*/ this.cp);
996
1023
  }
997
1024
 
@@ -1040,32 +1067,32 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
1040
1067
  this.render(WlRenderLevel.Layout, 'sliderOnValuesChanged').then(() => {});
1041
1068
  } catch (err: any) {
1042
1069
  const errMsg = errorToConsole(err);
1043
- _package.logger.error('Bio: WebLogoViewer<${this.viewerId}>.sliderOnValuesChanged() error:\n' + errMsg);
1070
+ _package.logger.error(`Bio: WebLogoViewer<${this.viewerId}>.sliderOnValuesChanged() error:\n` + errMsg);
1044
1071
  //throw err; // Do not throw to prevent disabling event handler
1045
1072
  }
1046
1073
  }
1047
1074
 
1048
1075
  private dataFrameFilterOnChanged(_value: any): void {
1049
- _package.logger.debug('Bio: WebLogoViewer<${this.viewerId}>.dataFrameFilterChanged()');
1076
+ _package.logger.debug(`Bio: WebLogoViewer<${this.viewerId}>.dataFrameFilterChanged()`);
1050
1077
  try {
1051
1078
  this.updatePositions();
1052
1079
  if (this.filterSource === FilterSources.Filtered)
1053
1080
  this.render(WlRenderLevel.Freqs, 'dataFrameFilterOnChanged').then(() => {});
1054
1081
  } catch (err: any) {
1055
1082
  const errMsg = errorToConsole(err);
1056
- _package.logger.error('Bio: WebLogoViewer<${this.viewerId}>.dataFrameFilterOnChanged() error:\n' + errMsg);
1083
+ _package.logger.error(`Bio: WebLogoViewer<${this.viewerId}>.dataFrameFilterOnChanged() error:\n` + errMsg);
1057
1084
  //throw err; // Do not throw to prevent disabling event handler
1058
1085
  }
1059
1086
  }
1060
1087
 
1061
1088
  private dataFrameSelectionOnChanged(_value: any): void {
1062
- _package.logger.debug('Bio: WebLogoViewer<${this.viewerId}>.dataFrameSelectionOnChanged()');
1089
+ _package.logger.debug(`Bio: WebLogoViewer<${this.viewerId}>.dataFrameSelectionOnChanged()`);
1063
1090
  try {
1064
1091
  if (this.filterSource === FilterSources.Selected)
1065
1092
  this.render(WlRenderLevel.Freqs, 'dataFrameSelectionOnChanged').then(() => {});
1066
1093
  } catch (err: any) {
1067
1094
  const errMsg = errorToConsole(err);
1068
- _package.logger.error('Bio: WebLogoViewer<${this.viewerId}>.dataFrameSelectionOnChanged() error:\n' + errMsg);
1095
+ _package.logger.error(`Bio: WebLogoViewer<${this.viewerId}>.dataFrameSelectionOnChanged() error:\n` + errMsg);
1069
1096
  //throw err; // Do not throw to prevent disabling event handler
1070
1097
  }
1071
1098
  }
@@ -1099,7 +1126,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
1099
1126
  }
1100
1127
  } catch (err: any) {
1101
1128
  const errMsg = errorToConsole(err);
1102
- _package.logger.error('Bio: WebLogoViewer<${this.viewerId}>.canvasOnMouseMove() error:\n' + errMsg);
1129
+ _package.logger.error(`Bio: WebLogoViewer<${this.viewerId}>.canvasOnMouseMove() error:\n` + errMsg);
1103
1130
  //throw err; // Do not throw to prevent disabling event handler
1104
1131
  }
1105
1132
  }
@@ -1122,7 +1149,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
1122
1149
  }
1123
1150
  } catch (err: any) {
1124
1151
  const errMsg = errorToConsole(err);
1125
- _package.logger.error('Bio: WebLogoViewer<${this.viewerId}>.canvasOnMouseDown() error:\n' + errMsg);
1152
+ _package.logger.error(`Bio: WebLogoViewer<${this.viewerId}>.canvasOnMouseDown() error:\n` + errMsg);
1126
1153
  //throw err; // Do not throw to prevent disabling event handler
1127
1154
  }
1128
1155
  }
@@ -1136,7 +1163,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
1136
1163
  this.slider.scrollBy(this.slider.min + countOfScrollPositions);
1137
1164
  } catch (err: any) {
1138
1165
  const errMsg = errorToConsole(err);
1139
- _package.logger.error('Bio: WebLogoViewer<${this.viewerId}>.canvasOnWheel() error:\n' + errMsg);
1166
+ _package.logger.error(`Bio: WebLogoViewer<${this.viewerId}>.canvasOnWheel() error:\n` + errMsg);
1140
1167
  //throw err; // Do not throw to prevent disabling event handler
1141
1168
  }
1142
1169
  }
@@ -1161,7 +1188,7 @@ export function checkSeqForMonomerAtPos(
1161
1188
  ): boolean {
1162
1189
  const seqMList: ISeqSplitted = unitsHandler.splitted[rowI];
1163
1190
  const seqM = at.pos < seqMList.length ? seqMList[at.pos] : null;
1164
- return ((seqM === monomer) || (seqM === '' && monomer === '-'));
1191
+ return ((seqM === monomer) || (seqM === '' && monomer === unitsHandler.defaultGapSymbol));
1165
1192
  }
1166
1193
 
1167
1194
  export function countForMonomerAtPosition(